diff --git a/src/pretix/plugins/wallet/apps.py b/src/pretix/plugins/wallet/apps.py index 077e4d1fbb..563b5012b9 100644 --- a/src/pretix/plugins/wallet/apps.py +++ b/src/pretix/plugins/wallet/apps.py @@ -37,5 +37,5 @@ class WalletApp(AppConfig): description = _("Issue wallet passes for tickets (e.g. apple wallet, google wallet)") def ready(self): - from . import signals # NOQA + from . import receivers # NOQA diff --git a/src/pretix/plugins/wallet/models.py b/src/pretix/plugins/wallet/models.py index d6c118026c..18f3c08b7b 100644 --- a/src/pretix/plugins/wallet/models.py +++ b/src/pretix/plugins/wallet/models.py @@ -27,8 +27,6 @@ from pretix.base.models import LoggedModel, OrderPosition from django_scopes import ScopedManager from django.core.exceptions import ValidationError -from pretix.plugins.wallet.styles import get_style -from pretix.plugins.wallet.styles.base import PassStyle class WalletLayout(LoggedModel): @@ -68,6 +66,8 @@ class WalletPlatformLayout(LoggedModel): @property def pass_layout(self): + from pretix.plugins.wallet.styles import get_style + style = get_style(self.platform, self.style) if style: return style(event=self.parent.event, layout=self.layout) diff --git a/src/pretix/plugins/wallet/placeholders.py b/src/pretix/plugins/wallet/placeholders.py index 67d31185b3..8e1aa6b12d 100644 --- a/src/pretix/plugins/wallet/placeholders.py +++ b/src/pretix/plugins/wallet/placeholders.py @@ -181,6 +181,11 @@ class WalletPlaceholderContext: k: v for k, v in self.context_args.items() if k in placeholder.required_context } + @classmethod + def is_available(cls, placeholder: BaseWalletPlaceholder, context_args: set[str]): + missing_context = placeholder.required_context - context_args + return not missing_context + def render_placeholder(self, placeholder: BaseWalletPlaceholder): if placeholder.identifier in self.cache: return self.cache[placeholder.identifier] @@ -192,7 +197,7 @@ class WalletPlaceholderContext: return placeholder.render_sample(**self._get_placeholder_context(placeholder)) -def get_wallet_placeholders(event): +def get_wallet_placeholders(event) -> dict[str, dict[str, BaseWalletPlaceholder]]: placeholders = { "text": { v.identifier: v diff --git a/src/pretix/plugins/wallet/receivers.py b/src/pretix/plugins/wallet/receivers.py new file mode 100644 index 0000000000..14101effe6 --- /dev/null +++ b/src/pretix/plugins/wallet/receivers.py @@ -0,0 +1,37 @@ +# +# This file is part of pretix (Community Edition). +# +# Copyright (C) 2014-2020 Raphael Michel and contributors +# Copyright (C) 2020-today pretix GmbH and contributors +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General +# Public License as published by the Free Software Foundation in version 3 of the License. +# +# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are +# applicable granting you additional permissions and placing additional restrictions on your usage of this software. +# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive +# this file, see . +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along with this program. If not, see +# . +# + +from pretix.base.signals import register_ticket_outputs, register_global_settings, EventPluginSignal +from .ticketoutput import OUTPUTS + +def connect_signals(): + for output in OUTPUTS: + # DIY functools.partial to make get_defining_app happy + def get_register_func(o): + def register(sender, **kwargs): + return o + 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() \ No newline at end of file diff --git a/src/pretix/plugins/wallet/signals.py b/src/pretix/plugins/wallet/signals.py index a65907498c..8b638e0cdf 100644 --- a/src/pretix/plugins/wallet/signals.py +++ b/src/pretix/plugins/wallet/signals.py @@ -20,22 +20,7 @@ # . # -from pretix.base.signals import register_ticket_outputs, register_global_settings, EventPluginSignal -from .ticketoutput import OUTPUTS - -def connect_signals(): - for output in OUTPUTS: - # DIY functools.partial to make get_defining_app happy - def get_register_func(o): - def register(sender, **kwargs): - return o - 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() - +from pretix.base.signals import EventPluginSignal register_wallet_text_placeholders = EventPluginSignal() """ 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 b4187c189c..a63c619b25 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 @@ -11,43 +11,54 @@ const store = inject(StoreKey)!; const gettext = (window as any).gettext; const props = defineProps<{ - fieldgroup: PlaceholderFieldGroupDefinition; - overflows: FieldGroupDefinition[]; + fieldgroup: PlaceholderFieldGroupDefinition; + overflows: FieldGroupDefinition[]; }>(); const fieldConfig = defineModel({ - required: true, + required: true, }); const overflowOptions = computed((): Array<[string | null, string]> => { - if (props.overflows.length) { - return [ - ...props.overflows.map((x): [string, string] => [x.identifier, x.name]), - [null, "Do not overflow"], - ]; - } else { - return []; - } + if (props.overflows.length) { + return [ + ...props.overflows.map((x): [string, string] => [x.identifier, x.name]), + [null, "Do not overflow"], + ]; + } else { + return []; + } }); function addVariable() { - fieldConfig.value.entries.push({ type: "placeholder", label: "" }); + fieldConfig.value.entries.push({ type: "placeholder", label: "" }); } watchEffect(() => { - if (!fieldConfig.value) { - fieldConfig.value = { - overflow: null, - entries: JSON.parse(JSON.stringify(props.fieldgroup.default_entries)), - active: - props.fieldgroup.required || - props.fieldgroup.default_entries.length > 0, - }; - } - if (fieldConfig.value && !fieldConfig.value.entries) { - fieldConfig.value.entries = JSON.parse( - JSON.stringify(props.fieldgroup.default_entries), - ); - } + if (!fieldConfig.value) { + fieldConfig.value = { + overflow: null, + entries: JSON.parse(JSON.stringify(props.fieldgroup.default_entries)), + active: + props.fieldgroup.required || + props.fieldgroup.default_entries.length > 0, + }; + } + if (fieldConfig.value && !fieldConfig.value.entries) { + fieldConfig.value.entries = JSON.parse( + JSON.stringify(props.fieldgroup.default_entries), + ); + } +}); + +const placeholderChoices = computed(() => { + const availableContext = new Set(props.fieldgroup.context_args); + const choices = Object.entries(store.variables.text) + .filter(([_, { required_context }]) => + new Set(required_context).isSubsetOf(availableContext), + ) + .map(([k, v]): [string, string] => [k, v.label]); + choices.push(["other", gettext("Other…")]); + return choices; }); @@ -66,21 +77,22 @@ watchEffect(() => { th(:class="'col-md-' + (fieldgroup.display == 'with_label' ? '6' : '11')") {{ gettext('Content') }} th.col-xs-1 tbody - tr(v-for="n,i in fieldConfig.entries.length" :key="i") + tr(v-for="n, i in fieldConfig.entries.length" :key="i") td(v-if="fieldgroup.display == 'with_label'") .i18n-form-group - I18nInput(v-model="fieldConfig.entries[n-1].label") + I18nInput(v-model="fieldConfig.entries[n - 1].label") td TextContent(v-if='fieldgroup.content_type == "text"' - v-model="fieldConfig.entries[n-1]") + v-model="fieldConfig.entries[n - 1]" + :placeholderChoices="placeholderChoices") Select(v-else-if='fieldgroup.content_type == "image"' - v-model="fieldConfig.entries[n-1].content" - :choices="Object.entries(store.variables.image).map(([k,v]) => [k, v.label])" + v-model="fieldConfig.entries[n - 1].content" + :choices="Object.entries(store.variables.image).map(([k, v]) => [k, v.label])" ) td.text-right - button.btn.btn-danger.form-control-static(type="button" @click="fieldConfig.entries.splice(n-1, 1)") + button.btn.btn-danger.form-control-static(type="button" @click="fieldConfig.entries.splice(n - 1, 1)") i.fa.fa-trash - span.sr-only {{ gettext('Delete')}} + span.sr-only {{ gettext('Delete') }} button.btn.btn-default(type="button" @click="addVariable") i.fa.fa-plus diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/predefined-fieldgroup-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/predefined-fieldgroup-preview.vue index c1e6be5eb8..4b6adb5d20 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/predefined-fieldgroup-preview.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/predefined-fieldgroup-preview.vue @@ -6,7 +6,7 @@ import { i18nstringLocalize } from "../../helpers"; const store = inject(StoreKey)!; const props = defineProps<{ - config: PreviewFieldgroup; + config: PredefinedFieldGroupConfig; style_def: PredefinedFieldGroupDefinition; }>(); @@ -17,10 +17,10 @@ const isActive = computed( diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue index db4efd7532..ed4d80cc18 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue @@ -9,14 +9,20 @@ const props = defineProps<{ \ No newline at end of file diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/text-content.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/text-content.vue index abec809a8f..b93540df25 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/text-content.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/text-content.vue @@ -7,13 +7,11 @@ import { StoreKey } from "../walletStore"; const store = inject(StoreKey)! const gettext = (window as any).gettext -const entry = defineModel({ required: true }) +const props = defineProps<{ + placeholderChoices: [string|null, string][]; +}>(); -const selectChoices = computed(() =>{ - const choices = Object.entries(store.variables.text).map(([k,v]): [string, string] => [k, v.label]) - choices.push(["other", gettext("Other…")]) - return choices -}); +const entry = defineModel({ required: true }) const selection = computed({ get() { @@ -53,7 +51,7 @@ const textContent = computed({ .i18n-form-group Select( v-model="selection" - :choices="selectChoices" + :choices="placeholderChoices" ) I18nInput(v-model="textContent" v-if="selection === 'other'" :locales="store.locales") diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts b/src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts index c1c564cefb..d9d078bfa8 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts @@ -19,6 +19,7 @@ type PlaceholderFieldGroupDefinition = BaseFieldGroupDefinition & { display: FieldGroupDisplay; min_entries: number | null; max_entries: number | null; + context_args: string[] }; type PredefinedFieldGroupDefinition = BaseFieldGroupDefinition & { @@ -52,6 +53,7 @@ type Style = { type Variable = { label: string; sample: string; + required_context: string[]; }; type Platform = { diff --git a/src/pretix/plugins/wallet/styles/apple.py b/src/pretix/plugins/wallet/styles/apple.py index a96faec3d9..9da365e065 100644 --- a/src/pretix/plugins/wallet/styles/apple.py +++ b/src/pretix/plugins/wallet/styles/apple.py @@ -30,16 +30,24 @@ class ApplePlatform(WalletPlatform): name = _("Apple") +class FormattedLazyI18nString: + def __init__(self, base_str: LazyI18nString, **format_args: str): + self.base_str = base_str + self.format_args = format_args + + def localize(self, language): + return self.base_str.localize(language).format(**self.format_args) + + class StringResource: - # mapping string in default event locale -> LazyI18nString - entries: dict[str, LazyI18nString] + entries: dict[str, LazyI18nString | FormattedLazyI18nString] locales: set[str] def __init__(self, locales): self.entries = {} self.locales = set(locales) - def add_entry(self, key: str, value: LazyI18nString): + def add_entry(self, key: str, value: LazyI18nString | FormattedLazyI18nString): if key in self.entries: raise ValueError(f"{key} already exists in this StringResource") self.entries[key] = value @@ -126,69 +134,51 @@ class AppleWalletStyle(PassStyle): def pass_content(self, fields, strings): raise NotImplementedError() - def generate_pass_json(self, fields, context, strings): - def add_from_context(key): - value = context.get(key) - if not value: - raise ValueError(f"{key} must be set to a truthy value") - return value + def generate_pass_json(self, fields, op, strings): + ticket = str(op.item.name) + if op.variation: + ticket += " - " + str(op.variation) + + description = FormattedLazyI18nString( + LazyI18nString.from_gettext("Ticket for {event} ({product})"), + event=self.event.name, + product=ticket, + ) + strings.add_entry("description", description) + + serialNumber = "%s-%s-%s-%d" % ( + self.event.organizer.slug, + self.event.slug, + op.order.code, + op.pk, + ) pass_json = { "formatVersion": 1, - "description": add_from_context("description"), - "organizationName": add_from_context("organizationName"), - "passTypeIdentifier": add_from_context("passTypeIdentifier"), - "teamIdentifier": add_from_context("teamIdentifier"), - "serialNumber": add_from_context("serialNumber"), + "description": "description", + "organizationName": self.event.organizer.name, + "passTypeIdentifier": self.event.settings.wallet_apple_pass_type_id, + "teamIdentifier": self.event.settings.wallet_apple_team_id, + "serialNumber": serialNumber, **self.pass_content(fields, strings), } return pass_json def generate(self, op: OrderPosition): - context = self.get_context(op) - order = op.order - event = order.event filename = "{}-{}.pkpass".format(order.event.slug, order.code) - ticket = str(op.item.name) - if op.variation: - ticket += " - " + str(op.variation) - - serialNumber = "%s-%s-%s-%d" % ( - order.event.organizer.slug, - order.event.slug, - order.code, - op.pk, - ) - - context.update({ - "ca_certificate": order.event.settings.wallet_apple_ca_certificate.read(), - "certificate": order.event.settings.wallet_apple_certificate.read(), - "key": order.event.settings.wallet_apple_key.read(), - "password": order.event.settings.wallet_apple_key_password, - "description": _("Ticket for {event} ({product})").format( # TODO: i18n - event=event.name, product=ticket - ), - "organizationName": event.organizer.name, - "passTypeIdentifier": order.event.settings.wallet_apple_pass_type_id, - "teamIdentifier": order.event.settings.wallet_apple_team_id, - "serialNumber": serialNumber, - }) - - - - fields = self.get_pass_fields(layout, context) + fields = self.get_pass_fields(op) pkpass = SignedZipFile( - context["ca_certificate"], - context["certificate"], - context["key"], - context["password"], + self.event.settings.wallet_apple_ca_certificate.read(), + self.event.settings.wallet_apple_certificate.read(), + self.event.settings.wallet_apple_key.read(), + self.event.settings.wallet_apple_key_password, ) - strings = StringResource(locales=context["locales"]) + strings = StringResource(locales=self.event.settings.locales) - pass_json = self.generate_pass_json(fields, context, strings) + pass_json = self.generate_pass_json(fields, op, strings) print(pass_json) if fields["logo"]: logo = fields["logo"][0]["value"] @@ -210,7 +200,6 @@ class AppleWalletStyle(PassStyle): return filename, "application/vnd.apple.pkpass", result - class AppleWalletEventTicket(AppleWalletStyle): identifier = "event_1" name = _("Event Ticket Layout 1") @@ -225,7 +214,8 @@ class AppleWalletEventTicket(AppleWalletStyle): content="poweredby", ) ], - required=True + required=True, + context_args={"event", "order", "order_position"}, ), ImageFieldGroup( identifier="logo", @@ -237,7 +227,8 @@ class AppleWalletEventTicket(AppleWalletStyle): content="poweredby", ) ], - required=True + required=True, + context_args={"event", "order", "order_position"}, ), TextFieldGroup( identifier="logo_text", @@ -245,6 +236,7 @@ class AppleWalletEventTicket(AppleWalletStyle): max_entries=1, display=FieldGroupDisplay.PLAIN, default_entries=[], + context_args={"event", "order", "order_position"}, ), TextFieldGroup( identifier="primary", @@ -258,13 +250,27 @@ class AppleWalletEventTicket(AppleWalletStyle): ) ], # TODO: support Lazyi18nproxy here description=_("These fields appear prominently featured on the pass."), - required=True + required=True, + context_args={"event", "order", "order_position"}, ), TextFieldGroup( - identifier="secondary", name=_("Secondary"), max_entries=4 + identifier="secondary", + name=_("Secondary"), + max_entries=4, + context_args={"event", "order", "order_position"}, ), # TODO: validation of max field count if combined "Coupons, store cards, and generic passes with a square barcode can have a total of up to four secondary and auxiliary fields, combined." - TextFieldGroup(identifier="header", name=_("Header"), max_entries=3), - TextFieldGroup(identifier="auxiliary", name=_("Auxiliary"), max_entries=4), + TextFieldGroup( + identifier="header", + name=_("Header"), + max_entries=3, + context_args={"event", "order", "order_position"}, + ), + TextFieldGroup( + identifier="auxiliary", + name=_("Auxiliary"), + max_entries=4, + context_args={"event", "order", "order_position"}, + ), TextFieldGroup( identifier="code", name=_("QR-Code"), @@ -275,8 +281,13 @@ class AppleWalletEventTicket(AppleWalletStyle): content="secret", ) ], + context_args={"event", "order", "order_position"}, + ), + TextFieldGroup( + identifier="back", + name=_("Back"), + context_args={"event", "order", "order_position"}, ), - TextFieldGroup(identifier="back", name=_("Back")), ] preview_layout = [ [ diff --git a/src/pretix/plugins/wallet/styles/base.py b/src/pretix/plugins/wallet/styles/base.py index 82752f96c1..ca8fe6666a 100644 --- a/src/pretix/plugins/wallet/styles/base.py +++ b/src/pretix/plugins/wallet/styles/base.py @@ -1,24 +1,33 @@ import enum -from typing import Any +from typing import Any, TypedDict +from black.nodes import is_vararg from i18nfield.strings import LazyI18nString import jsonschema from django.core.exceptions import ValidationError from pretix.base.models import OrderPosition +from ..placeholders import WalletPlaceholderContext, get_wallet_placeholders + class WalletPlatform: identifier: str name: str +class LayoutContext(TypedDict): + placeholders: dict[str, dict] + + class FieldGroupType(enum.Enum): PLACEHOLDER = "placeholder" PREDEFINED = "predefined" + class FieldGroupDisplay(enum.Enum): PLAIN = "plain" WITH_LABEL = "with_label" CODE = "code" + class FieldGroup: type: FieldGroupType identifier: str @@ -35,7 +44,7 @@ class FieldGroup: def layout_schema( self, remaining_fields: list["FieldGroup"], - context: dict, + context: LayoutContext, ) -> dict: raise NotImplemented() @@ -72,16 +81,19 @@ class FieldEntry[T]: self.content = content def asdict(self) -> dict: - return {"type": self.type.value, "content": self.content, "label": self.label.data if self.label else None} + return { + "type": self.type.value, + "content": self.content, + "label": self.label.data if self.label else None, + } + class PlaceholderFieldEntry(FieldEntry[str]): type = FieldEntryType.PLACEHOLDER label: LazyI18nString | None content: str - def __init__( - self, content: str, label: LazyI18nString | None = None - ): + def __init__(self, content: str, label: LazyI18nString | None = None): self.label = label self.content = content @@ -92,8 +104,11 @@ class CustomFieldEntry(FieldEntry[LazyI18nString]): content: LazyI18nString def asdict(self) -> dict: - return {"type": self.type.value, "content": self.content.data, "label": self.label.data if self.label else None} - + return { + "type": self.type.value, + "content": self.content.data, + "label": self.label.data if self.label else None, + } class PredefinedFieldGroup(FieldGroup): @@ -102,11 +117,10 @@ class PredefinedFieldGroup(FieldGroup): def layout_schema( self, remaining_fields: list["FieldGroup"], - context: dict, + context: LayoutContext, ): - return { - "type": "object" - } + return {"type": "object"} + class PlaceholderFieldGroup(FieldGroup): type = FieldGroupType.PLACEHOLDER @@ -115,18 +129,22 @@ class PlaceholderFieldGroup(FieldGroup): display: FieldGroupDisplay min_entries: int | None max_entries: int | None + context_args: set[ + str + ] # what context arguments are available when rendering this fieldgroup def __init__( self, identifier: str, name: str, content_type: FieldContentType, - description: str="", + description: str = "", required=False, default_entries=None, min_entries=None, max_entries=None, display=FieldGroupDisplay.WITH_LABEL, + context_args: set[str] | None = None, ): super().__init__(identifier, name, description, required) self.content_type = content_type @@ -134,6 +152,7 @@ class PlaceholderFieldGroup(FieldGroup): self.min_entries = min_entries self.max_entries = max_entries self.display = display + self.context_args = context_args or set() if self.required and (self.min_entries is None or self.min_entries < 1): self.min_entries = 1 @@ -146,18 +165,26 @@ class PlaceholderFieldGroup(FieldGroup): "display": self.display.value, "min_entries": self.min_entries, "max_entries": self.max_entries, + "context_args": list(sorted(self.context_args)) } def layout_schema( self, remaining_fields: list["FieldGroup"], - context: dict, + context: LayoutContext, ): - placeholders = list(context.get("placeholders", {}).get(self.content_type.value, {}).keys()) + content_type_placeholders = ( + context["placeholders"].get(self.content_type.value, {}).values() + ) + available_placeholders = [ + x.identifier + for x in content_type_placeholders + if WalletPlaceholderContext.is_available(x, self.context_args) + ] return { "type": "object", "properties": { - "entries": self.entries_schema(placeholders=placeholders), + "entries": self.entries_schema(placeholders=available_placeholders), "overflow": { "anyOf": [ {"type": "null"}, @@ -212,7 +239,6 @@ class PlaceholderFieldGroup(FieldGroup): return schema - class TextFieldGroup(PlaceholderFieldGroup): content_type = FieldContentType.TEXT @@ -234,23 +260,25 @@ class PassStyle: # order here limits in what order users can configure field "overspilling" (if too many fields are defined, where should the rest go) -> can only go down in the list # we evaluate the fields in this order, so they overspill in this order as well (fields from primary are appended to the overspilling field before fields from secondary are etc) fieldgroups: list[FieldGroup] - preview_layout: list | None - @classmethod - def asdict(cls): + @property + def preview_layout(self) -> list | None: + return None + + def asdict(self): return { - "identifier": cls.identifier, - "name": cls.name, - "fieldgroups": [x.asdict() for x in cls.fieldgroups], - "preview_layout": cls.preview_layout + "identifier": self.identifier, + "name": self.name, + "fieldgroups": [x.asdict() for x in self.fieldgroups], + "preview_layout": self.preview_layout, } - @classmethod - def layout_schema(cls, context): + def layout_schema(self): + context = LayoutContext(placeholders=self.placeholders) schema = { "$schema": "https://json-schema.org/draft/2020-12/schema", # TODO: $id - "title": cls.name, + "title": self.name, "type": "object", "properties": { "fieldgroups": { @@ -258,12 +286,12 @@ class PassStyle: "type": "object", "properties": { group.identifier: group.layout_schema( - context=context, remaining_fields=cls.fieldgroups[i:] + context=context, remaining_fields=self.fieldgroups[i:] ) - for (i, group) in enumerate(cls.fieldgroups) + for (i, group) in enumerate(self.fieldgroups) }, "required": [ - group.identifier for group in cls.fieldgroups if group.required + group.identifier for group in self.fieldgroups if group.required ], } }, @@ -276,41 +304,37 @@ class PassStyle: } }, } - if any(group.required for group in cls.fieldgroups): + if any(group.required for group in self.fieldgroups): schema["required"] = ["fieldgroups"] return schema - @classmethod - def render_placeholder(cls, context, content_type, content): - placeholder = ( - context.get("placeholders", {}) - .get(content_type, {}) - .get(content) - ) + def render_placeholder(self, context, content_type, content): + placeholder = self.placeholders.get(content_type, {}).get(content) if placeholder: - placeholder_value = context['placeholder_context'].render_placeholder(placeholder) + placeholder_value = context.render_placeholder(placeholder) if placeholder_value: return placeholder.label, placeholder_value return None, None - def __init__(self, event, layout): self.event = event self.layout = layout - - def get_layout_context(self): - return {"placeholders": {}} + self.placeholders = get_wallet_placeholders(self.event) def validate(self): - schema = self.layout_schema(self.get_layout_context()) + schema = self.layout_schema() try: jsonschema.validate(self.layout, schema) except jsonschema.ValidationError as e: raise ValidationError("Invalid layout: {}".format(str(e))) - def get_pass_fields(self, context): + def get_pass_fields(self, op: OrderPosition): + context = WalletPlaceholderContext( + event=self.event, order=op.order, order_position=op + ) + fields = {} for group in self.fieldgroups: if isinstance(group, PredefinedFieldGroup): @@ -319,14 +343,22 @@ class PassStyle: elif isinstance(group, PlaceholderFieldGroup): group_fields = fields.get(group.identifier, []) if group.identifier in self.layout["fieldgroups"]: - for field in self.layout["fieldgroups"][group.identifier]["entries"]: + for field in self.layout["fieldgroups"][group.identifier][ + "entries" + ]: field_entry = {} if group.display == FieldGroupDisplay.WITH_LABEL: field_entry["label"] = LazyI18nString(field["label"]) if field["type"] == FieldEntryType.PLACEHOLDER.value: - label, field_entry["value"] = self.render_placeholder(context, group.content_type.value, field['content']) - if group.display == FieldGroupDisplay.WITH_LABEL and not str(field_entry['label']) and label: - field_entry['label'] = LazyI18nString(label) + label, field_entry["value"] = self.render_placeholder( + context, group.content_type.value, field["content"] + ) + if ( + group.display == FieldGroupDisplay.WITH_LABEL + and not str(field_entry["label"]) + and label + ): + field_entry["label"] = LazyI18nString(label) elif field["type"] == FieldEntryType.CUSTOM.value: field_entry["value"] = LazyI18nString(field["content"]) @@ -337,13 +369,15 @@ class PassStyle: f"Group {group.identifier} needs at least {group.min_entries} entries, but only {len(group_fields)} were provided" ) fields[group.identifier] = group_fields[: group.max_entries] - if (overflow_group := self.layout["fieldgroups"][group.identifier]['overflow']): + if overflow_group := self.layout["fieldgroups"][group.identifier][ + "overflow" + ]: fields.setdefault(overflow_group, []) - fields[overflow_group] += group_fields[group.max_entries:] + fields[overflow_group] += group_fields[group.max_entries :] else: raise ValueError("Unknown field group") return fields def generate(self, op: OrderPosition): - raise NotImplementedError() \ No newline at end of file + raise NotImplementedError() diff --git a/src/pretix/plugins/wallet/styles/google.py b/src/pretix/plugins/wallet/styles/google.py index 2c9d2614cd..de9b52bdd8 100644 --- a/src/pretix/plugins/wallet/styles/google.py +++ b/src/pretix/plugins/wallet/styles/google.py @@ -1,3 +1,5 @@ +from i18nfield.fields import LazyI18nString + from pretix.base.models import Event, OrderPosition from .base import ( @@ -186,11 +188,15 @@ class GoogleWalletStyle(PassStyle): comms = Comms(self.event.settings.get("wallet_google_credentials").read()) class_object = self._generate_class() - ticket_object = self._generate_object(op, class_id=class_object['id']) + ticket_object = self._generate_object(op, class_id=class_object["id"]) # TODO: privacy screen - class_object = comms.put_item(ClassType.eventTicketClass, class_object['id'], class_object) - ticket_object = comms.put_item(ObjectType.eventTicketObject, ticket_object['id'], ticket_object) + class_object = comms.put_item( + ClassType.eventTicketClass, class_object["id"], class_object + ) + ticket_object = comms.put_item( + ObjectType.eventTicketObject, ticket_object["id"], ticket_object + ) generated_jwt = comms.sign_jwt( ButtonJWT( @@ -201,8 +207,7 @@ class GoogleWalletStyle(PassStyle): ) ) - return "https://pay.google.com/gp/v/save/%s" % generated_jwt - + return 'googlepaypass', 'text/uri-list', 'https://pay.google.com/gp/v/save/%s' % generated_jwt class GoogleWalletEventTicket(GoogleWalletStyle): identifier = "event" @@ -219,6 +224,8 @@ class GoogleWalletEventTicket(GoogleWalletStyle): ) ], ), + PredefinedFieldGroup(identifier="venue", name=_("Venue")), + PredefinedFieldGroup(identifier="date", name=_("Date")), PredefinedFieldGroup(identifier="seating", name=_("Seating")), TextFieldGroup( identifier="code", @@ -230,46 +237,75 @@ class GoogleWalletEventTicket(GoogleWalletStyle): content="secret", ) ], + context_args={"event", "order", "order_position"}, ), ] - preview_layout = [ - [ - { - "children": [ - {"fieldgroup": "logo", "relSize": 1}, - { - "value": "issuerName", - "relSize": 3, - "display": ["large", "centered"], - }, - ] - }, - { - "children": [ - {"value": "venueName", "display": "small"}, - {"value": "eventName", "display": "large"}, - ], - "direction": "column", - }, - { - "children": [ - {"value": "01/01/1970", "label": "Date"}, - {"value": "12:34", "label": "Time"}, - ] - }, - {"fieldgroup": "seating", - "sample": [ - {"content": "5", "label": "Row"}, - {"content": "2", "label": "Seat"}, - ] - }, - {"fieldgroup": "code"}, + + @property + def preview_layout(self): + return [ + [ + { + "children": [ + {"fieldgroup": "logo", "relSize": 1}, + { + "value": str(self.event.organizer.name), + "relSize": 3, + "display": ["large", "centered"], + }, + ] + }, + { + "children": [ + { + "fieldgroup": "venue", + "sample": [ + {"content": self.venue()[0], "label": ""}, + ], + }, + {"value": str(self.event.name), "display": "large"}, + ], + "direction": "column", + "display": ["tight"] + }, + { + "fieldgroup": "date", + "sample": [ + {"content": "01/01/1970", "label": "Date"}, + {"content": "12:34", "label": "Time"}, + ], + }, + { + "fieldgroup": "seating", + "sample": [ + {"content": "5", "label": "Row"}, + {"content": "2", "label": "Seat"}, + ], + }, + {"fieldgroup": "code"}, + ] ] - ] + + def venue(self): + if self.event.location: + name = {} + address = {} + + for key, value in self.event.location.data.items(): + lines = value.splitlines() + name[key] = lines[0] + # We must provide at least one address line each for the name and address - no way around it. + if len(lines) > 1: + address[key] = '\n'.join(value.splitlines()[1:]) + else: + address[key] = lines[0] + + return name, address + return None, None def _generate_object(self, op: OrderPosition, class_id: str): output_object = super()._generate_object(op, class_id) - + fields = self.get_pass_fields(op) if fields["code"]: output_object.barcode( Barcode.qrCode, fields["code"][0]["value"], fields["code"][0]["value"] diff --git a/src/pretix/plugins/wallet/views.py b/src/pretix/plugins/wallet/views.py index 114a51a2ef..6878aff94d 100644 --- a/src/pretix/plugins/wallet/views.py +++ b/src/pretix/plugins/wallet/views.py @@ -40,7 +40,7 @@ def get_editor_placeholders(event): context = WalletPlaceholderContext(event=event, order=p.order, order_position=p) placeholders = { t: { - pid: {"label": str(p.label), "sample": str(context.render_sample(p))} + pid: {"label": str(p.label), "sample": str(context.render_sample(p)), "required_context": list(sorted(p.required_context))} for pid, p in ps.items() } for t, ps in get_wallet_placeholders(event).items() @@ -76,7 +76,7 @@ class LayoutEditorView(LayoutDetailView): "identifier": platform.identifier, "name": platform.name, "styles": { - style.identifier: style.asdict() + style.identifier: style(self.request.event, None).asdict() for style in AVAILABLE_STYLES.get(platform.identifier) }, } @@ -174,7 +174,7 @@ class LayoutPreviewView(EventPermissionRequiredMixin, View): layout = style(event=event, layout=layout) layout.validate() - fname, mimet, data = platform.generate(layout, p) + fname, mimet, data = layout.generate(p) resp = HttpResponse(data, content_type=mimet) ftype = fname.split(".")[-1] if not mimet.startswith("text/"):