forked from CGM_Public/pretix_original
WIP: use api
This commit is contained in:
66
src/pretix/plugins/wallet/api.py
Normal file
66
src/pretix/plugins/wallet/api.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from rest_framework import viewsets
|
||||||
|
from django.db import transaction
|
||||||
|
from .styles import PassLayout, get_platform_styles, get_platforms
|
||||||
|
from .models import WalletLayout
|
||||||
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
|
import django_filters.rest_framework
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
class WalletLayoutSerializer(I18nAwareModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = WalletLayout
|
||||||
|
fields = ("event","platform","name","style","layout")
|
||||||
|
read_only_fields = ("event", "platform")
|
||||||
|
|
||||||
|
def validate_layout(self, value):
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise ValidationError(_("Layout must be a dict"))
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def validate_platform(self, value):
|
||||||
|
if value not in get_platforms():
|
||||||
|
raise ValidationError(_("Invalid Platform"))
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
if "style" in data and "layout" in data and "platform" in data:
|
||||||
|
platform_styles = get_platform_styles(data['platform'])
|
||||||
|
if data['style'] not in platform_styles:
|
||||||
|
raise ValidationError(_("Invalid style"))
|
||||||
|
style = get_platform_styles(data['platform'])[data['style']]
|
||||||
|
|
||||||
|
layout = PassLayout(
|
||||||
|
style=style, layout=data["layout"]
|
||||||
|
)
|
||||||
|
breakpoint()
|
||||||
|
layout.validate(data['event'])
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class WalletLayoutViewSet(viewsets.ModelViewSet):
|
||||||
|
model = WalletLayout
|
||||||
|
queryset = WalletLayout.objects.none()
|
||||||
|
serializer_class = WalletLayoutSerializer
|
||||||
|
filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
|
||||||
|
filterset_fields = ['platform']
|
||||||
|
permission = "event.settings.general:write"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.request.event.wallet_layouts.all()
|
||||||
|
|
||||||
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
return super().get_serializer(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
super().perform_update(serializer)
|
||||||
|
serializer.instance.log_action(
|
||||||
|
action='pretix.plugins.wallet.layout.changed',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data,
|
||||||
|
)
|
||||||
@@ -37,7 +37,7 @@ class WalletLayout(LoggedModel):
|
|||||||
)
|
)
|
||||||
platform = models.CharField(max_length=10)
|
platform = models.CharField(max_length=10)
|
||||||
style = models.CharField(max_length=255)
|
style = models.CharField(max_length=255)
|
||||||
layout = models.JSONField()
|
layout = models.JSONField(default={})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
|
|||||||
@@ -1,46 +1,86 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { computed, ref, watchEffect } from "vue";
|
||||||
import StyleSettings from './style-settings.vue'
|
import StyleSettings from "./style-settings.vue";
|
||||||
import Select from './input/select.vue'
|
import Select from "./input/select.vue";
|
||||||
import Input from './input/input.vue'
|
import Input from "./input/input.vue";
|
||||||
|
|
||||||
const gettext = (window as any).gettext
|
const gettext = (window as any).gettext;
|
||||||
|
|
||||||
// TODO: Move to store?
|
const isLoading = ref<boolean>(true);
|
||||||
const STYLES: Styles = JSON.parse(document.querySelector('#styles')?.textContent ?? '{}')
|
const wallet_layout = ref<Layout | null>(null);
|
||||||
const VARIABLES: VariableConfig = JSON.parse(document.querySelector('#variables')?.textContent ?? '{}')
|
|
||||||
const FORM_ERRORS: Record<string, Array<string>> = JSON.parse(document.querySelector('#form_errors')?.textContent ?? '{}')
|
|
||||||
const LAYOUT: Layout = JSON.parse(document.querySelector('#layout')?.textContent ?? '{}')
|
|
||||||
|
|
||||||
const name = ref<string>(LAYOUT.name ?? '')
|
const STYLES: Styles = JSON.parse(
|
||||||
const style = ref<string | null>(LAYOUT.style ?? null)
|
document.querySelector("#styles")?.textContent ?? "{}",
|
||||||
const layout = ref<LayoutData>(LAYOUT.layout ?? {fields: {}})
|
);
|
||||||
|
const VARIABLES: VariableConfig = JSON.parse(
|
||||||
|
document.querySelector("#variables")?.textContent ?? "{}",
|
||||||
|
);
|
||||||
|
const CSRF_TOKEN =
|
||||||
|
document.querySelector<HTMLInputElement>("input[name=csrfmiddlewaretoken]")
|
||||||
|
?.value ?? "";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
layoutId: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
// TODO: error handling / proper api client
|
||||||
|
isLoading.value = true;
|
||||||
|
fetch(
|
||||||
|
`/api/v1/organizers/demo/events/wallet/walletlayouts/${props.layoutId}/`,
|
||||||
|
)
|
||||||
|
.then((x) => x.json())
|
||||||
|
.then((x) => {
|
||||||
|
wallet_layout.value = x;
|
||||||
|
isLoading.value = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function saveLayout(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
isLoading.value = true;
|
||||||
|
// TODO: error handling / proper api client
|
||||||
|
fetch(
|
||||||
|
`/api/v1/organizers/demo/events/wallet/walletlayouts/${props.layoutId}/`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"X-CSRFToken": CSRF_TOKEN,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(wallet_layout.value),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((x) => x.json())
|
||||||
|
.then((x) => {
|
||||||
|
wallet_layout.value = x;
|
||||||
|
isLoading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
// TODO: add :key for all `v-for`s
|
// TODO: add :key for all `v-for`s
|
||||||
// TODO: i18n
|
// TODO: i18n textfields
|
||||||
details
|
// TODO: proper spinner
|
||||||
pre
|
template(v-if="isLoading") {{ gettext("Loading...") }}
|
||||||
code {{ FORM_ERRORS }}
|
form(v-else @submit="saveLayout")
|
||||||
.row
|
.row
|
||||||
.col-md-8
|
.col-md-8
|
||||||
// TODO: show error text
|
.form-group()
|
||||||
.form-group(:class='"name" in FORM_ERRORS ? "has-error" : ""')
|
Input(label="Name" v-model="wallet_layout.name")
|
||||||
Input(label="Name" v-model="name" name="name" :errors="FORM_ERRORS['name']")
|
|
||||||
|
|
||||||
.form-group(:class='"style" in FORM_ERRORS ? "has-error" : ""')
|
.form-group()
|
||||||
Select(label="Style" v-model="style" :choices="Object.values(STYLES).map(x => [x.identifier, x.name])" name="style" :errors="FORM_ERRORS['style']")
|
Select(label="Style" v-model="wallet_layout.style" :choices="Object.values(STYLES).map(x => [x.identifier, x.name])")
|
||||||
|
|
||||||
StyleSettings(v-if="style" v-model="layout" :style="style" :styles="STYLES" :variables="VARIABLES")
|
StyleSettings(v-if="wallet_layout.style" v-model="wallet_layout.layout" :style="STYLES[wallet_layout.style]" :variables="VARIABLES")
|
||||||
.col-md-4
|
.col-md-4
|
||||||
.panel.panel-default
|
.panel.panel-default
|
||||||
.panel-heading Preview
|
.panel-heading Preview
|
||||||
.panel-body
|
.panel-body
|
||||||
// TODO: Preview
|
// TODO: Preview
|
||||||
pre
|
pre
|
||||||
code {{ layout }}
|
code {{ wallet_layout }}
|
||||||
input(type="hidden" name="layout" :value="JSON.stringify(layout)")
|
.form-group.submit-group
|
||||||
.form-group.submit-group
|
button.btn.btn-primary.btn-save(type="submit") Submit
|
||||||
button.btn.btn-primary.btn-save(type="submit") Submit
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive } from 'vue'
|
import { computed, reactive } from "vue";
|
||||||
import Select from './input/select.vue'
|
import Select from "./input/select.vue";
|
||||||
import Input from './input/input.vue'
|
import Input from "./input/input.vue";
|
||||||
import TextContent from './text-content.vue'
|
import TextContent from "./text-content.vue";
|
||||||
|
|
||||||
const gettext = (window as any).gettext
|
const gettext = (window as any).gettext;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
field: FieldGroupDefinition
|
field: FieldGroupDefinition;
|
||||||
overflows: FieldGroupDefinition[],
|
overflows: FieldGroupDefinition[];
|
||||||
variables: Variables
|
variables: Variables;
|
||||||
}>()
|
}>();
|
||||||
const fieldConfig = defineModel<FieldConfig>({ required: true })
|
const fieldConfig = defineModel<FieldConfig>({ required: true });
|
||||||
|
|
||||||
const overflowOptions = computed((): Array<[string|null, string]> => {
|
const overflowOptions = computed((): Array<[string | null, string]> => {
|
||||||
if (props.overflows.length) {
|
if (props.overflows.length) {
|
||||||
return [...props.overflows.map((x): [string, string] => [x.identifier, x.name]), [null, "Do not overflow"]]
|
return [
|
||||||
|
...props.overflows.map((x): [string, string] => [x.identifier, x.name]),
|
||||||
|
[null, "Do not overflow"],
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
return []
|
return [];
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
function addVariable() {
|
function addVariable() {
|
||||||
fieldConfig.value.entries.push({"type": "placeholder", "label": null, "content": null})
|
fieldConfig.value.entries.push({ type: "placeholder" });
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
|
|||||||
@@ -5,27 +5,22 @@ import FieldSettings from "./field-settings.vue";
|
|||||||
const gettext = (window as any).gettext;
|
const gettext = (window as any).gettext;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
styles: Styles;
|
|
||||||
variables: VariableConfig
|
variables: VariableConfig
|
||||||
style?: string;
|
style?: Style;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const layout = defineModel<LayoutData>();
|
const layout = defineModel<LayoutData>();
|
||||||
|
|
||||||
const styleData = computed(() => {
|
|
||||||
if (!props.style || !(props.style in props.styles)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return props.styles[props.style];
|
|
||||||
});
|
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
// TODO: this seems wrooong
|
if (layout.value === undefined) {
|
||||||
if (!("fields" in layout.value)) {
|
return
|
||||||
|
}
|
||||||
|
if (layout.value.fields === undefined) {
|
||||||
layout.value.fields = {};
|
layout.value.fields = {};
|
||||||
}
|
}
|
||||||
if (props.style) {
|
if (props.style) {
|
||||||
for (const field of props.styles[props.style].fields) {
|
for (const field of props.style.fields) {
|
||||||
if (!(field.identifier in layout.value.fields)) {
|
if (!(field.identifier in layout.value.fields)) {
|
||||||
layout.value.fields[field.identifier] = {
|
layout.value.fields[field.identifier] = {
|
||||||
entries: JSON.parse(JSON.stringify(field.default_entries)),
|
entries: JSON.parse(JSON.stringify(field.default_entries)),
|
||||||
@@ -39,11 +34,11 @@ watchEffect(() => {
|
|||||||
|
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
h2.h3 {{ gettext("Field Groups") }}
|
h2.h3 {{ gettext("Field Groups") }}
|
||||||
FieldSettings(v-if="styleData"
|
FieldSettings(v-if="props.style"
|
||||||
v-for="(field, fieldId) in styleData.fields"
|
v-for="(field, fieldId) in props.style.fields"
|
||||||
v-model="layout.fields[field.identifier]"
|
v-model="layout.fields[field.identifier]"
|
||||||
:field="field"
|
:field="field"
|
||||||
:overflows="styleData.fields.slice(fieldId + 1).filter(x => x.entry_type === field.entry_type)"
|
:overflows="props.style.fields.slice(fieldId + 1).filter(x => x.entry_type === field.entry_type)"
|
||||||
:variables="variables[field.entry_type]"
|
:variables="variables[field.entry_type]"
|
||||||
)
|
)
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ type Styles = Record<string, Style>;
|
|||||||
type Variables = Record<string, Variable>;
|
type Variables = Record<string, Variable>;
|
||||||
type VariableConfig = Record<string, Variables>;
|
type VariableConfig = Record<string, Variables>;
|
||||||
|
|
||||||
|
type I18nString = string | Record<string, string>
|
||||||
|
|
||||||
type FieldEntry = {
|
type FieldEntry = {
|
||||||
type: 'placeholder' | 'text';
|
type: 'placeholder' | 'text';
|
||||||
label: string; // TODO i18n
|
label?: I18nString; // TODO i18n
|
||||||
content: string;
|
content?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type FieldConfig = {
|
type FieldConfig = {
|
||||||
|
|||||||
@@ -24,29 +24,36 @@ class GooglePlatform(WalletPlatform):
|
|||||||
name = _("Google")
|
name = _("Google")
|
||||||
|
|
||||||
|
|
||||||
class PlaceholderFieldType(enum.Enum):
|
class FieldType(enum.Enum):
|
||||||
TEXT = "text"
|
TEXT = "text"
|
||||||
CODE = "qr"
|
CODE = "qr"
|
||||||
IMAGE = "image"
|
IMAGE = "image"
|
||||||
PREDEFINED = "predefined"
|
PREDEFINED = "predefined"
|
||||||
# TODO: POWERED_BY ?
|
# TODO: POWERED_BY ?
|
||||||
|
|
||||||
|
class BaseField:
|
||||||
@dataclass
|
type: str
|
||||||
class PlaceholderField:
|
|
||||||
type: PlaceholderFieldType
|
|
||||||
label: LazyI18nString
|
label: LazyI18nString
|
||||||
content: str
|
content: str
|
||||||
|
|
||||||
def asdict(self):
|
def __init__(self, label: LazyI18nString, content: str):
|
||||||
return {"type": self.type.value, "label": self.label.data, "value": self.content}
|
self.label = label
|
||||||
|
self.content = content
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
return {"type": self.type, "label": self.label.data, "content": self.content}
|
||||||
|
|
||||||
|
class PlaceholderField(BaseField):
|
||||||
|
type = "placeholder"
|
||||||
|
|
||||||
|
class TextField(BaseField):
|
||||||
|
type = "text"
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FieldGroupDefinition:
|
class FieldGroupDefinition:
|
||||||
name: str
|
name: str
|
||||||
identifier: str
|
identifier: str
|
||||||
entry_type: PlaceholderFieldType
|
entry_type: FieldType
|
||||||
min_entries: int | None = None
|
min_entries: int | None = None
|
||||||
max_entries: int | None = None
|
max_entries: int | None = None
|
||||||
|
|
||||||
@@ -62,8 +69,8 @@ class FieldGroupDefinition:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PlaceholderFieldGroup(FieldGroupDefinition):
|
class PlaceholderFieldGroup(FieldGroupDefinition):
|
||||||
entry_type: PlaceholderFieldType = PlaceholderFieldType.TEXT
|
entry_type: FieldType = FieldType.TEXT
|
||||||
default_entries: list[PlaceholderField] = field(default_factory=list)
|
default_entries: list[PlaceholderField | TextField] = field(default_factory=list) # TODO: TextField seems wrong here
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
asdict = super().asdict()
|
asdict = super().asdict()
|
||||||
@@ -73,7 +80,7 @@ class PlaceholderFieldGroup(FieldGroupDefinition):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PredefinedFieldGroup(FieldGroupDefinition):
|
class PredefinedFieldGroup(FieldGroupDefinition):
|
||||||
entry_type: PlaceholderFieldType = PlaceholderFieldType.PREDEFINED
|
entry_type: FieldType = FieldType.PREDEFINED
|
||||||
min_entries = 0
|
min_entries = 0
|
||||||
max_entries = 1
|
max_entries = 1
|
||||||
|
|
||||||
@@ -107,9 +114,9 @@ class AppleWalletEventTicket(PassStyle):
|
|||||||
min_entries=1,
|
min_entries=1,
|
||||||
max_entries=1,
|
max_entries=1,
|
||||||
default_entries=[
|
default_entries=[
|
||||||
PlaceholderField(PlaceholderFieldType.IMAGE, LazyI18nString("logo"), "event:image")
|
PlaceholderField(LazyI18nString("logo"), "event:image")
|
||||||
],
|
],
|
||||||
entry_type=PlaceholderFieldType.IMAGE,
|
entry_type=FieldType.IMAGE,
|
||||||
),
|
),
|
||||||
PlaceholderFieldGroup(
|
PlaceholderFieldGroup(
|
||||||
identifier="primary",
|
identifier="primary",
|
||||||
@@ -117,7 +124,7 @@ class AppleWalletEventTicket(PassStyle):
|
|||||||
min_entries=1,
|
min_entries=1,
|
||||||
max_entries=1,
|
max_entries=1,
|
||||||
default_entries=[
|
default_entries=[
|
||||||
PlaceholderField(PlaceholderFieldType.TEXT, LazyI18nString("Ticket type"), "item")
|
PlaceholderField(LazyI18nString("Ticket type"), "item")
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
PlaceholderFieldGroup(
|
PlaceholderFieldGroup(
|
||||||
@@ -141,7 +148,7 @@ class GoogleWalletEventTicket(PassStyle):
|
|||||||
fields = [
|
fields = [
|
||||||
PredefinedFieldGroup(identifier="seating", name=_("Seating")),
|
PredefinedFieldGroup(identifier="seating", name=_("Seating")),
|
||||||
PlaceholderFieldGroup(
|
PlaceholderFieldGroup(
|
||||||
identifier="qrcode", name=_("QR-Code"), entry_type=PlaceholderFieldType.CODE
|
identifier="qrcode", name=_("QR-Code"), entry_type=FieldType.CODE
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load money %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load vite %}
|
||||||
|
{% load static %}
|
||||||
|
{% load compress %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %}{% trans "Wallet layouts" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans "New layout" %}</h1>
|
||||||
|
<form action="" method="post" class="form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form layout="control" %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label">
|
||||||
|
{% trans "Ticket design" %}
|
||||||
|
</label>
|
||||||
|
<div class="col-md-9 form-control-static">
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
You can modify the design after you saved this page.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group submit-group">
|
||||||
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
|
{% trans "Save" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -11,14 +11,9 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "Edit layout" %} {{ object.name }} </h1>
|
<h1>{% trans "Edit layout" %} {{ object.name }} </h1>
|
||||||
{{ styles|json_script:"styles" }}
|
{{ styles|json_script:"styles" }}
|
||||||
{{ variables|json_script:"variables" }}
|
{{ variables|json_script:"variables" }}
|
||||||
{{ form.errors|json_script:"form_errors" }}
|
<div id="editor" data-layout-id="{{ object.pk }}"></div>
|
||||||
{{ layout|json_script:"layout" }}
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div id="editor">This is a test</div>
|
|
||||||
</form>
|
|
||||||
{% vite_hmr %}
|
{% vite_hmr %}
|
||||||
{% vite_asset "src/pretix/plugins/wallet/static/pretixplugins/wallet/main.ts" %}
|
{% vite_asset "src/pretix/plugins/wallet/static/pretixplugins/wallet/main.ts" %}
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if "can_change_event_settings" in request.eventpermset %}
|
{% if "can_change_event_settings" in request.eventpermset %}
|
||||||
<strong><a href="{% url "plugins:ticketoutputpdf:edit" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}">
|
<strong><a href="{% url "plugins:wallet:edit" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}">
|
||||||
{{ l.name }}
|
{{ l.name }}
|
||||||
</a></strong>
|
</a></strong>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{% elif "can_change_event_settings" in request.eventpermset %}
|
{% elif "can_change_event_settings" in request.eventpermset %}
|
||||||
<form class="form-inline" method="post"
|
<form class="form-inline" method="post"
|
||||||
action="{% url "plugins:ticketoutputpdf:default" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}">
|
action="{% url "plugins:wallet:default" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="btn btn-default btn-sm">
|
<button class="btn btn-default btn-sm">
|
||||||
{% trans "Make default" %}
|
{% trans "Make default" %}
|
||||||
@@ -64,10 +64,10 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-right flip">
|
<td class="text-right flip">
|
||||||
{% if "can_change_event_settings" in request.eventpermset %}
|
{% if "can_change_event_settings" in request.eventpermset %}
|
||||||
<a href="{% url "plugins:ticketoutputpdf:edit" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
<a href="{% url "plugins:wallet:edit" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||||
<a href="{% url "plugins:ticketoutputpdf:add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ l.id }}"
|
<a href="{% url "plugins:wallet:add" organizer=request.event.organizer.slug event=request.event.slug platform=platform.identifier %}?copy_from={{ l.id }}"
|
||||||
class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a>
|
class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a>
|
||||||
<a href="{% url "plugins:ticketoutputpdf:delete" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
<a href="{% url "plugins:wallet:delete" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -20,18 +20,26 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from django.urls import re_path
|
from django.urls import re_path
|
||||||
|
from pretix.api.urls import event_router
|
||||||
|
|
||||||
from .views import (
|
from .views import (
|
||||||
LayoutEditorView,
|
LayoutEditorView,
|
||||||
LayoutCreateView,
|
LayoutCreateView,
|
||||||
LayoutListView
|
LayoutListView
|
||||||
)
|
)
|
||||||
|
from .api import WalletLayoutViewSet
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/$',
|
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/$',
|
||||||
LayoutListView.as_view(), name='index'),
|
LayoutListView.as_view(), name='index'),
|
||||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/edit/(?P<platform>[^/]+)/$',
|
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/add/(?P<platform>[^/]+)/$',
|
||||||
LayoutCreateView.as_view(), name='add'),
|
LayoutCreateView.as_view(), name='add'),
|
||||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/edit/(?P<platform>[^/]+)/(?P<layout>[^/]+)/$',
|
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/edit/(?P<layout>[^/]+)/$',
|
||||||
LayoutEditorView.as_view(), name='edit'),
|
LayoutEditorView.as_view(), name='edit'),
|
||||||
|
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/default/(?P<layout>[^/]+)/$', # TODO
|
||||||
|
LayoutEditorView.as_view(), name='default'),
|
||||||
|
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/delete/(?P<layout>[^/]+)/$', # TODO
|
||||||
|
LayoutEditorView.as_view(), name='delete'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
event_router.register('walletlayouts', WalletLayoutViewSet)
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from django import forms
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.views.generic import FormView, ListView, CreateView, UpdateView
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.generic import (
|
||||||
|
CreateView, DetailView, ListView
|
||||||
|
)
|
||||||
from pretix.base.pdf import get_images, get_variables
|
from pretix.base.pdf import get_images, get_variables
|
||||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||||
from .styles import PassLayout, get_platform_styles, get_platforms
|
|
||||||
from .models import WalletLayout
|
from .models import WalletLayout
|
||||||
import json
|
from .styles import get_platform_styles, get_platforms
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django import forms
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from i18nfield.fields import I18nCharField
|
|
||||||
from i18nfield.forms import I18nModelForm
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.renderers import JSONRenderer
|
|
||||||
# TODO: should this even be a list view?
|
# TODO: should this even be a list view?
|
||||||
class LayoutListView(EventPermissionRequiredMixin, ListView):
|
class LayoutListView(EventPermissionRequiredMixin, ListView):
|
||||||
model = WalletLayout
|
model = WalletLayout
|
||||||
@@ -33,102 +31,24 @@ class LayoutListView(EventPermissionRequiredMixin, ListView):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutEditorView(DetailView):
|
||||||
class LayoutEditForm(forms.ModelForm):
|
|
||||||
style = forms.ChoiceField()
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
self.platform = kwargs.pop('platform')
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = WalletLayout
|
|
||||||
fields = ("name","style","layout")
|
|
||||||
|
|
||||||
def __init__(self, event, platform, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self.event = event
|
|
||||||
self.platform = platform
|
|
||||||
self.platform_styles = get_platform_styles(platform)
|
|
||||||
self.fields["style"].choices = [
|
|
||||||
(id, style.name) for id, style in self.platform_styles.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
def coerce_style(self, value):
|
|
||||||
return self.platform_styles[value]
|
|
||||||
|
|
||||||
def clean_layout(self):
|
|
||||||
layout = self.cleaned_data["layout"]
|
|
||||||
|
|
||||||
if not isinstance(layout, dict):
|
|
||||||
raise ValidationError(_("Layout must be a dict"))
|
|
||||||
return layout
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if "style" in self.cleaned_data and "layout" in self.cleaned_data:
|
|
||||||
layout = PassLayout(
|
|
||||||
style=self.coerce_style(self.cleaned_data["style"]), layout=self.cleaned_data["layout"]
|
|
||||||
)
|
|
||||||
layout.validate(self.event)
|
|
||||||
return self.cleaned_data
|
|
||||||
|
|
||||||
class LayoutSerializer(I18nAwareModelSerializer):
|
|
||||||
# # TODO: only necessary if we save through this serializer
|
|
||||||
# style = serializers.ChoiceField(choices={})
|
|
||||||
|
|
||||||
# def __init__(self, *args, platform, **kwargs):
|
|
||||||
# super().__init__(*args, **kwargs)
|
|
||||||
# self.platform = platform
|
|
||||||
# self.platform_styles = get_platform_styles(platform)
|
|
||||||
# self.fields["style"].choices = [
|
|
||||||
# (id, style.name) for id, style in self.platform_styles.items()
|
|
||||||
# ]
|
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = WalletLayout
|
|
||||||
fields = ("name","style","layout")
|
|
||||||
|
|
||||||
class LayoutEditorView(EventPermissionRequiredMixin, UpdateView):
|
|
||||||
template_name = "pretixplugins/wallet/edit.html"
|
template_name = "pretixplugins/wallet/edit.html"
|
||||||
form_class = LayoutEditForm
|
|
||||||
model = WalletLayout
|
model = WalletLayout
|
||||||
permission = "can_change_event_settings" # TODO: new permission name
|
permission = "event.settings.general:write"
|
||||||
pk_url_kwarg = "layout"
|
pk_url_kwarg = "layout"
|
||||||
|
|
||||||
@property
|
|
||||||
def platform(self):
|
|
||||||
return self.kwargs["platform"]
|
|
||||||
|
|
||||||
def get_form_kwargs(self) -> dict[str, Any]:
|
|
||||||
kwargs = super().get_form_kwargs()
|
|
||||||
kwargs['event'] = self.request.event
|
|
||||||
kwargs["platform"] = self.platform
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def get_platform_styles(self):
|
def get_platform_styles(self):
|
||||||
if self.platform not in get_platforms():
|
if self.object.platform not in get_platforms():
|
||||||
raise Http404(
|
raise Http404(
|
||||||
_("Unknown platform '{platform}'").format(platform=self.platform)
|
_("Unknown platform '{platform}'").format(platform=self.object.platform)
|
||||||
)
|
)
|
||||||
return get_platform_styles(self.platform)
|
return get_platform_styles(self.object.platform)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs) -> dict[str, Any]:
|
def get_context_data(self, **kwargs) -> dict[str, Any]:
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["styles"] = {
|
context["styles"] = {
|
||||||
id: style.asdict() for id, style in self.get_platform_styles().items()
|
id: style.asdict() for id, style in self.get_platform_styles().items()
|
||||||
}
|
}
|
||||||
if self.request.method == "POST":
|
|
||||||
form = self.get_form()
|
|
||||||
if not form.is_valid():
|
|
||||||
layout_data = LayoutSerializer(self.object).data
|
|
||||||
layout_data.update(form.cleaned_data)
|
|
||||||
context['layout'] = layout_data
|
|
||||||
else:
|
|
||||||
context["layout"] = LayoutSerializer(self.object).data
|
|
||||||
else:
|
|
||||||
context["layout"] = LayoutSerializer(self.object).data
|
|
||||||
|
|
||||||
context["variables"] = {
|
context["variables"] = {
|
||||||
"text": {
|
"text": {
|
||||||
varname: {"label": var["label"], "editor_sample": var["editor_sample"]}
|
varname: {"label": var["label"], "editor_sample": var["editor_sample"]}
|
||||||
@@ -140,18 +60,47 @@ class LayoutEditorView(EventPermissionRequiredMixin, UpdateView):
|
|||||||
}
|
}
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
class WalletLayoutCreateForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = WalletLayout
|
||||||
|
fields = ("name",)
|
||||||
|
|
||||||
|
def __init__(self, *args, platform, event, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.platform = platform
|
||||||
|
self.event = event
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs) -> Any:
|
||||||
|
self.instance.platform = self.platform
|
||||||
|
self.instance.event = self.event
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
class LayoutCreateView(CreateView):
|
||||||
|
template_name = "pretixplugins/wallet/create.html"
|
||||||
|
form_class = WalletLayoutCreateForm
|
||||||
|
permission = "event.settings.general:write"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def platform(self):
|
||||||
|
platform = self.kwargs['platform']
|
||||||
|
if platform not in get_platforms():
|
||||||
|
raise Http404(
|
||||||
|
_("Unknown platform '{platform}'").format(platform=platform)
|
||||||
|
)
|
||||||
|
return platform
|
||||||
|
|
||||||
|
def get_form_kwargs(self) -> dict[str, Any]:
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['platform'] = self.platform
|
||||||
|
kwargs['event'] = self.request.event
|
||||||
|
return kwargs
|
||||||
|
|
||||||
def get_success_url(self) -> str:
|
def get_success_url(self) -> str:
|
||||||
return reverse(
|
return reverse(
|
||||||
"plugins:wallet:edit",
|
"plugins:wallet:edit",
|
||||||
kwargs={
|
kwargs={
|
||||||
"organizer": self.request.event.organizer.slug,
|
"organizer": self.request.event.organizer.slug,
|
||||||
"event": self.request.event.slug,
|
"event": self.request.event.slug,
|
||||||
"platform": self.platform,
|
|
||||||
"layout": self.object.pk,
|
"layout": self.object.pk,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LayoutCreateView(LayoutEditorView):
|
|
||||||
def get_object(self, queryset=None):
|
|
||||||
return WalletLayout(event=self.request.event, platform=self.platform)
|
|
||||||
Reference in New Issue
Block a user