WIP: use api

This commit is contained in:
Kara Engelhardt
2026-04-13 19:28:38 +02:00
parent 477b1e42d4
commit 30b64546a7
12 changed files with 299 additions and 201 deletions

View 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,
)

View File

@@ -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",)

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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 = {

View File

@@ -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
), ),
] ]

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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)