From 8bbf90a5d0d54dbf68ddcdd2feeb697b0c92d8e9 Mon Sep 17 00:00:00 2001 From: Kara Engelhardt Date: Tue, 28 Jul 2026 19:00:11 +0200 Subject: [PATCH] WIP: predefined preview, google api, custom placeholder class --- src/pretix/plugins/wallet/api.py | 4 +- src/pretix/plugins/wallet/placeholders.py | 275 ++++++++++++++++++ src/pretix/plugins/wallet/signals.py | 21 +- .../pretixplugins/wallet/components/app.vue | 20 +- .../wallet/components/app.vue_old | 148 ---------- .../wallet/components/input/checkbox.vue | 20 ++ .../components/placeholder-field-settings.vue | 32 +- .../components/predefined-field-settings.vue | 19 +- .../components/preview/fieldgroup-preview.vue | 16 +- .../components/preview/pass-preview.vue | 1 - .../placeholder-fieldgroup-preview.vue | 1 - .../preview/predefined-fieldgroup-preview.vue | 51 ++-- .../static/pretixplugins/wallet/index.d.ts | 8 +- .../pretixplugins/wallet/walletStore.ts | 7 +- src/pretix/plugins/wallet/styles/__init__.py | 8 - src/pretix/plugins/wallet/styles/apple.py | 12 +- src/pretix/plugins/wallet/styles/base.py | 25 +- src/pretix/plugins/wallet/styles/google.py | 97 +++--- src/pretix/plugins/wallet/ticketoutput.py | 5 +- src/pretix/plugins/wallet/views.py | 51 ++-- 20 files changed, 496 insertions(+), 325 deletions(-) create mode 100644 src/pretix/plugins/wallet/placeholders.py delete mode 100644 src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue_old create mode 100644 src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/checkbox.vue diff --git a/src/pretix/plugins/wallet/api.py b/src/pretix/plugins/wallet/api.py index 7a67fec2e5..73ee642d84 100644 --- a/src/pretix/plugins/wallet/api.py +++ b/src/pretix/plugins/wallet/api.py @@ -5,7 +5,7 @@ from .models import WalletLayout, WalletPlatformLayout from pretix.api.serializers.i18n import I18nAwareModelSerializer from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -from .views import get_layout_variables +from .views import get_editor_placeholders from rest_framework import serializers @@ -34,7 +34,7 @@ class WalletPlatformLayoutSerializer(I18nAwareModelSerializer): style = platform_styles[data["style"]] layout = PassLayout(style=style, layout=data["layout"]) - context = {"placeholders": get_layout_variables(self.context['event'])} + context = {"placeholders": get_editor_placeholders(self.context['event'])} layout.validate(context=context) return data diff --git a/src/pretix/plugins/wallet/placeholders.py b/src/pretix/plugins/wallet/placeholders.py new file mode 100644 index 0000000000..67d31185b3 --- /dev/null +++ b/src/pretix/plugins/wallet/placeholders.py @@ -0,0 +1,275 @@ +from collections.abc import Callable +from .signals import ( + register_wallet_text_placeholders, + register_wallet_image_placeholders, +) +from django.core.files import File +from django.dispatch import receiver +from pretix.base.templatetags.money import money_filter +from django.contrib.staticfiles import finders +from django.templatetags.static import static +from django.utils.translation import gettext_lazy as _ +from i18nfield.strings import LazyI18nString + + +class BaseWalletPlaceholder: + """ + This is the base class for all wallet placeholders. + """ + + @property + def required_context(self) -> set[str]: + """ + A a set of all attribute names that need to be contained in the base context so that this placeholder is available. + """ + return set() + + @property + def identifier(self) -> str: + """The unique identifier of this placeholder""" + raise NotImplementedError() + + @property + def label(self) -> LazyI18nString: + """The human readable name of this placeholder""" + raise NotImplementedError() + + @property + def control_label(self) -> LazyI18nString: + """ + The human readable name of this placeholder shown in the backend. + + Defaults to `label` + """ + return self.label + + def render(self, **context): + raise NotImplementedError() + + def render_sample(self, **context): + raise NotImplementedError() + + + +class BaseWalletTextPlaceholder(BaseWalletPlaceholder): + def render(self, **context) -> str | None: + """ + This method is called to generate the text that is being shown on the pass. + You will be passed the keyword arguments specified in ``required_context``. + You are expected to return a plain-text string. + """ + raise NotImplementedError() + + def render_sample(self, **context) -> str: + """ + This method is called to generate a text to be used in previews. + + You will be passed sample instances of the arguments specified in ``required_context``. + If those instances contain all data needed, you do not need to implement this. + """ + sample = self.render(**context) + if sample is None: + raise RuntimeError("`render` returned None when rendering a sample") + return sample + + +class BaseWalletImagePlaceholder(BaseWalletPlaceholder): + def render(self, **context) -> File | None: + """ + This method is called to generate the image that is being shown on the pass. + You will be passed the keyword arguments specified in ``required_context``. + You are expected to return a `File` object. + """ + raise NotImplementedError() + + def render_sample(self, **context) -> str | None: + """ + This method is called to generate a text to be used in previews. + + You will be passed sample instances of the arguments specified in ``required_context``. + You are expected to return a URL to a sample image or `None` if no sample can be shown. + """ + return None + + +class FunctionalWalletTextPlaceholder(BaseWalletTextPlaceholder): + def __init__( + self, + identifier: str, + label: LazyI18nString, + args: set[str], + func: Callable[..., str | None], + sample: None | str | Callable[..., str] = None, + ): + self._identifier = identifier + self._label = label + self._args = args + self.render = func + self._sample = sample + + @property + def identifier(self): + return self._identifier + + @property + def label(self): + return self._label + + @property + def required_context(self) -> set[str]: + return self._args + + def render_sample(self, **context) -> str: + if isinstance(self._sample, Callable): + return self._sample(**context) + elif self._sample: + return self._sample + else: + return super().render_sample(**context) + + +class FunctionalWalletImagePlaceholder(BaseWalletImagePlaceholder): + def __init__( + self, + identifier: str, + label: LazyI18nString, + args: set[str], + func: Callable[..., File | None], + sample: None | str | Callable[..., str] = None, + ): + self._identifier = identifier + self._label = label + self._args = args + self.render = func + self._sample = sample + + @property + def required_context(self) -> set[str]: + return self._args + + @property + def identifier(self): + return self._identifier + + @property + def label(self): + return self._label + + def render_sample(self, **context) -> str | None: + if isinstance(self._sample, Callable): + return self._sample(**context) + return self._sample + + +class MissingContextException(Exception): + pass + + +class WalletPlaceholderContext: + def __init__(self, **kwargs): + self.context_args = kwargs + self.cache = {} + + def _get_placeholder_context(self, placeholder: BaseWalletPlaceholder): + missing_context = placeholder.required_context - self.context_args.keys() + if missing_context: + raise MissingContextException( + f"Missing context args for '{placeholder.identifier}': {', '.join(missing_context)}" + ) + + return { + k: v for k, v in self.context_args.items() if k in placeholder.required_context + } + + def render_placeholder(self, placeholder: BaseWalletPlaceholder): + if placeholder.identifier in self.cache: + return self.cache[placeholder.identifier] + + value = self.cache[placeholder.identifier] = placeholder.render(**self._get_placeholder_context(placeholder)) + return value + + def render_sample(self, placeholder: BaseWalletPlaceholder): + return placeholder.render_sample(**self._get_placeholder_context(placeholder)) + + +def get_wallet_placeholders(event): + placeholders = { + "text": { + v.identifier: v + for r, vs in register_wallet_text_placeholders.send(sender=event) + for v in vs + }, + "image": { + v.identifier: v + for r, vs in register_wallet_image_placeholders.send(sender=event) + for v in vs + }, + } + return placeholders + + + +def get_static_file(name) -> File | None: + path: str | None = finders.find(name) # type: ignore + if not path: + return + return File(open(path, "rb")) + + +@receiver( + register_wallet_text_placeholders, + dispatch_uid="plugin_wallet_register_wallet_text_placeholders", +) +def base_text_placeholders(sender, **kwargs): + return [ + FunctionalWalletTextPlaceholder("name", LazyI18nString.from_gettext("Event Name"), {"event"}, lambda event: event.name), + FunctionalWalletTextPlaceholder( + "event_slug", LazyI18nString.from_gettext("Event Slug"), {"event"}, lambda event: event.slug + ), + FunctionalWalletTextPlaceholder("order", LazyI18nString.from_gettext("Order Code"), {"order"}, lambda order: order.code), + FunctionalWalletTextPlaceholder( + "total", + LazyI18nString.from_gettext("Order Total"), + {"event", "order"}, + lambda event, order: money_filter(order.total, event.currency), + ), + FunctionalWalletTextPlaceholder( + "order_email",LazyI18nString.from_gettext("Order Email"), {"order"}, lambda order: order.email + ), + FunctionalWalletTextPlaceholder( + "price",LazyI18nString.from_gettext("Item Price"), {"event", "order_position"}, lambda event, order_position: money_filter(order_position.price, event.currency) + ), + FunctionalWalletTextPlaceholder( + "secret",LazyI18nString.from_gettext("Order Secret (QR-Code-Content)"), {"order_position"}, lambda order_position: order_position.secret + ), + ] + +@receiver( + register_wallet_image_placeholders, + dispatch_uid="plugin_wallet_register_wallet_image_placeholders", +) +def base_image_placeholders(sender, **kwargs): + return [ + FunctionalWalletImagePlaceholder( + "poweredby", + LazyI18nString.from_gettext("Logo"), + set(), + # TODO: replace with paths not from another plugin + lambda: get_static_file("pretix_passbook/logo.png"), + static("pretix_passbook/logo.png"), + ), + FunctionalWalletImagePlaceholder( + "poweredby_icon", + LazyI18nString.from_gettext("Icon"), + set(), + lambda: get_static_file("pretix_passbook/icon.png"), + static("pretix_passbook/icon.png"), + ), + FunctionalWalletImagePlaceholder( + "example_no_preview", + LazyI18nString.from_gettext("Image with no preview"), + set(), + lambda: get_static_file("pretix_passbook/icon.png"), + ), + # TODO: Image upload + ] diff --git a/src/pretix/plugins/wallet/signals.py b/src/pretix/plugins/wallet/signals.py index b5facb8e77..a65907498c 100644 --- a/src/pretix/plugins/wallet/signals.py +++ b/src/pretix/plugins/wallet/signals.py @@ -20,7 +20,7 @@ # . # -from pretix.base.signals import register_ticket_outputs, register_global_settings +from pretix.base.signals import register_ticket_outputs, register_global_settings, EventPluginSignal from .ticketoutput import OUTPUTS def connect_signals(): @@ -29,9 +29,26 @@ def connect_signals(): def get_register_func(o): def register(sender, **kwargs): return o - return register + return register register_ticket_outputs.connect(get_register_func(output), dispatch_uid=f"wallet_output_{output.identifier}") if hasattr(output, "get_global_settings"): register_global_settings.connect(output.get_global_settings, dispatch_uid=f"wallet_global_settings_{output.identifier}") connect_signals() + + +register_wallet_text_placeholders = EventPluginSignal() +""" +This signal is sent out to get all known wallet placeholders. Receivers should return +an list of subclasses of pretix.plugins.wallet.placeholders.BaseWalletTextPlaceholder. + +As with all event-plugin signals, the ``sender`` keyword argument will contain the event. +""" + +register_wallet_image_placeholders = EventPluginSignal() +""" +This signal is sent out to get all known wallet placeholders. Receivers should return +an list of subclasses of pretix.plugins.wallet.placeholders.BaseWalletImagePlaceholder. + +As with all event-plugin signals, the ``sender`` keyword argument will contain the event. +""" \ No newline at end of file diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue index fcd8c1788c..80ba7d4733 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue @@ -49,8 +49,7 @@ const platformChoices = computed(() => { }); const preview_layout = computed(() => { - return store.currentPlatformStyles[store.currentPlatformLayout.style] - .preview_layout; + return store.currentPlatformStyles[store.currentPlatformLayout.style]?.preview_layout; }); @@ -93,3 +92,20 @@ const preview_layout = computed(() => { button.btn.btn-primary.btn-save(type="submit") Submit + + + \ No newline at end of file diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue_old b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue_old deleted file mode 100644 index 21df10b708..0000000000 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue_old +++ /dev/null @@ -1,148 +0,0 @@ - - - diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/checkbox.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/checkbox.vue new file mode 100644 index 0000000000..a1382af55f --- /dev/null +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/checkbox.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/placeholder-field-settings.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/placeholder-field-settings.vue index cde081c79d..b4187c189c 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/placeholder-field-settings.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/placeholder-field-settings.vue @@ -1,12 +1,12 @@ \ No newline at end of file + diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/predefined-field-settings.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/predefined-field-settings.vue index 9621aaf1d2..4f5539af58 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/predefined-field-settings.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/predefined-field-settings.vue @@ -1,4 +1,8 @@ diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue index f834cdf336..4818caf217 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue @@ -2,20 +2,26 @@ import { computed, inject, reactive, watchEffect } from "vue"; import { StoreKey } from "../../walletStore"; import PlaceholderFieldgroupPreview from "./placeholder-fieldgroup-preview.vue"; +import PredefinedFieldgroupPreview from "./predefined-fieldgroup-preview.vue"; -const store = inject(StoreKey)! +const store = inject(StoreKey)!; const props = defineProps<{ - config: PreviewFieldgroup; + config: PreviewFieldgroup; }>(); -const style_def = Object.fromEntries(store.currentPlatformStyles[store.currentPlatformLayout.style].fieldgroups.map(x => [x.identifier, x]))[props.config.fieldgroup] - +const style_def = computed(() => { + return Object.fromEntries( + store.currentPlatformStyles[ + store.currentPlatformLayout.style + ].fieldgroups.map((x) => [x.identifier, x]), + )[props.config.fieldgroup]; +});