From ac2d25ba9ab331c83cffafc6bbd082dbcb69f607 Mon Sep 17 00:00:00 2001 From: Kara Engelhardt Date: Thu, 15 Oct 2026 18:24:27 +0200 Subject: [PATCH] Start with custom google pass layout --- src/pretix/base/services/tickets.py | 2 +- src/pretix/plugins/wallet/styles/__init__.py | 4 +- src/pretix/plugins/wallet/styles/google.py | 502 ++++++++++++------- 3 files changed, 332 insertions(+), 176 deletions(-) diff --git a/src/pretix/base/services/tickets.py b/src/pretix/base/services/tickets.py index 0ff59dc9d3..5f292c5ee3 100644 --- a/src/pretix/base/services/tickets.py +++ b/src/pretix/base/services/tickets.py @@ -121,7 +121,7 @@ def get_preview_position(event): raise RuntimeError("get_preview_position needs to be called in a rolledback_transaction") item = event.items.create(name=LazyI18nString.from_gettext("Sample product"), default_price=Decimal('42.23'), - description=_("Sample product description")) + description=LazyI18nString.from_gettext("Sample product description")) item2 = event.items.create(name=LazyI18nString.from_gettext("Sample workshop"), default_price=Decimal('23.40')) item.program_times.create(item=item, start=datetime(2017, 12, 27, 0, 0, 0, tzinfo=timezone.utc), end=datetime(2017, 12, 28, 0, 0, 0, tzinfo=timezone.utc)) diff --git a/src/pretix/plugins/wallet/styles/__init__.py b/src/pretix/plugins/wallet/styles/__init__.py index a90d355292..d4ffb9c7f8 100644 --- a/src/pretix/plugins/wallet/styles/__init__.py +++ b/src/pretix/plugins/wallet/styles/__init__.py @@ -1,13 +1,13 @@ from pretix.plugins.wallet.styles.base import PassStyle from .apple import ApplePlatform, AppleWalletEventTicket -from .google import GooglePlatform, GoogleWalletDefaultEventTicket +from .google import GooglePlatform, GoogleWalletCustomEventTicket, GoogleWalletDefaultEventTicket AVAILABLE_PLATFORMS = [ApplePlatform, GooglePlatform] AVAILABLE_STYLES: dict[str, list[type[PassStyle]]] = { "apple": [AppleWalletEventTicket], - "google": [GoogleWalletDefaultEventTicket], + "google": [GoogleWalletDefaultEventTicket, GoogleWalletCustomEventTicket], } AVAILABLE_STYLES_DICT = { diff --git a/src/pretix/plugins/wallet/styles/google.py b/src/pretix/plugins/wallet/styles/google.py index e8df5b0768..8bfdfe34d6 100644 --- a/src/pretix/plugins/wallet/styles/google.py +++ b/src/pretix/plugins/wallet/styles/google.py @@ -24,14 +24,18 @@ from walletobjects.constants import ( from pretix.base.models import Event, OrderPosition from pretix.base.settings import GlobalSettingsObject from pretix.multidomain.urlreverse import eventreverse_absolute +from pretix.plugins.wallet.i18n import localizable_to_dict from pretix.plugins.wallet.models import GoogleWalletInstance, GoogleWalletType from pretix.plugins.wallet.styles.base import ( ColorSettingsField, + FieldContentType, + FieldGroup, FieldGroupDisplay, FloatSettingsField, ImageSettingsField, PassStyle, PlaceholderFieldEntry, + PlaceholderFieldGroup, PredefinedFieldGroup, TextFieldGroup, WalletPlatform, @@ -102,30 +106,6 @@ class GooglePlatform(WalletPlatform): class GoogleWalletEventTicketStyle(PassStyle): platform = GooglePlatform - @property - def settings(self): - return [ - ImageSettingsField( - identifier="logo", - label=_("Logo"), - default="pretixplugins/wallet/logo.png", - ), - ImageSettingsField( - identifier="hero", - label=_("Hero Image"), - default=self.event.settings.logo_image, - ), - ColorSettingsField( - identifier="bg_color", - label=_("Background Color"), - default=self.event.settings.primary_color, - ), - FloatSettingsField(identifier="lat", label=_("Latitude"), min=-90, max=90), - FloatSettingsField( - identifier="long", label=_("Longitude"), min=-180, max=180 - ), - ] - @cached_property def comms(self): return Comms(self.event.settings.get("wallet_google_credentials").read()) @@ -144,57 +124,6 @@ class GoogleWalletEventTicketStyle(PassStyle): instance.save(update_fields=["data"]) return result - def _generate_class(self): - output_class = EventTicketClass( - self.event.organizer.name, - get_class_id(self.event, self.op), - MultipleDevicesAndHoldersAllowedStatus.multipleHolders, # TODO: Make configurable - self.event.name, - ReviewStatus.underReview, - self.event.settings.locale, - ) - - output_class.homepage_uri( - eventreverse_absolute(self.event, "presale:event.index"), - get_translated_string("Website", self.event.settings.locale), - get_translated_dict("Website", self.event.settings.locales), - ) - - # TODO: callback url - # output_class.callback_url(eventreverse_absolute(event.organizer,"plugins:wallet:google_webhook",)) - - if (lat := self.cleaned_settings["lat"]) and ( - long := self.cleaned_settings["long"] - ): - output_class.locations(lat, long) - elif self.event.geo_lat and self.event.geo_lon: - output_class.locations(self.event.geo_lat, self.event.geo_lon) - - output_class.country_code(self.event.settings.locale) - - if hero := self.cleaned_settings["hero"]: - output_class.hero_image( - urljoin(eventreverse_absolute(self.event, "presale:event.index"), hero), - self.event.name, - ) - - if logo := self.cleaned_settings["logo"]: - output_class.logo( - urljoin(eventreverse_absolute(self.event, "presale:event.index"), logo), - self.event.name, - ) - - output_class.hex_background_color(self.cleaned_settings["bg_color"]) - output_class.confirmation_code_label(ConfirmationCode.orderNumber) - - return output_class - - def _generate_object(self, op: OrderPosition, class_id: str): - output_object = EventTicketObject( - get_object_id(op), class_id, ObjectState.active, self.event.settings.locale - ) - return output_object - def generate(self, op): self.op = op @@ -223,18 +152,35 @@ class GoogleWalletEventTicketStyle(PassStyle): "https://pay.google.com/gp/v/save/%s" % generated_jwt, ) - -class GoogleWalletDefaultEventTicket(GoogleWalletEventTicketStyle): - identifier = "event" - name = "Event Ticket" + @property + def settings(self): + return [ + ImageSettingsField( + identifier="logo", + label=_("Logo"), + default=self.event.settings.logo_image + or "pretixplugins/wallet/logo.png", + ), + # TODO: wide logo + ImageSettingsField( + identifier="hero", + label=_("Hero Image"), + ), + ColorSettingsField( + identifier="bg_color", + label=_("Background Color"), + default=self.event.settings.primary_color, + ), + FloatSettingsField(identifier="lat", label=_("Latitude"), min=-90, max=90), + FloatSettingsField( + identifier="long", label=_("Longitude"), min=-180, max=180 + ), + ] @property - def fieldgroups(self): + def fieldgroups(self) -> list[FieldGroup]: return [ PredefinedFieldGroup(identifier="venue", name=_("Venue")), - PredefinedFieldGroup(identifier="date", name=_("Date")), - PredefinedFieldGroup(identifier="seating", name=_("Seating")), - PredefinedFieldGroup(identifier="ticket_holder", name=_("Ticket Holder")), TextFieldGroup( identifier="code", name=_("QR-Code"), @@ -281,81 +227,22 @@ class GoogleWalletDefaultEventTicket(GoogleWalletEventTicketStyle): "direction": "column", "display": ["tight"], }, - { - "fieldgroup": "date", - "sample": [ - { - "content": date_format( - self.event.date_from.astimezone( - self.event.timezone - ), - "SHORT_DATE_FORMAT", - ), - "label": "Date", - }, - { - "content": date_format( - self.event.date_from.astimezone( - self.event.timezone - ), - "TIME_FORMAT", - ), - "label": "Time", - }, - ], - }, - { - "fieldgroup": "seating", - "sample": [ - {"content": _("Ground floor"), "label": _("Section")}, - {"content": "5 / 2", "label": _("Row") + " / " + _("Seat")}, - ], - }, + *self._preview_layout_front_card, {"fieldgroup": "code"}, {"setting": "hero"}, ], }, - { - "rows": [ - { - "fieldgroup": "ticket_holder", - "sample": [ - {"content": _("John Doe"), "label": _("Ticket Holder")}, - ], - }, - { - "fieldgroup": "venue", - "sample": [ - {"content": self.venue()[1], "label": self.venue()[0]}, - ], - }, - { - "fieldgroup": "date", - "sample": [ - { - "content": date_format( - self.event.date_from.astimezone( - self.event.timezone - ), - "DATETIME_FORMAT", - ), - "label": _("Event Start Time"), - }, - ], - }, - {"value": _("Sample product"), "label": _("Ticket Type")}, - {"value": "ABCDE-1", "label": _("Ticket Number")}, - {"value": "ABCDE", "label": _("Order Number")}, - { - "value": eventreverse_absolute( - self.event, "presale:event.index" - ), - "label": _("Website"), - }, - ] - }, + {"rows": [*self._preview_layout_back_details]}, ] + @property + def _preview_layout_front_card(self): + return [] + + @property + def _preview_layout_back_details(self): + return [] + def venue(self): if self.event.location: name = {} @@ -374,10 +261,162 @@ class GoogleWalletDefaultEventTicket(GoogleWalletEventTicketStyle): return "", "" def _generate_class(self): - output_class = super()._generate_class() + output_class = EventTicketClass( + self.event.organizer.name, + get_class_id(self.event, self.op), + MultipleDevicesAndHoldersAllowedStatus.multipleHolders, # TODO: Make configurable + self.event.name, + ReviewStatus.underReview, + self.event.settings.locale, + ) + + output_class.homepage_uri( + eventreverse_absolute(self.event, "presale:event.index"), + get_translated_string("Website", self.event.settings.locale), + localizable_to_dict("Website", self.event.settings.locales), + ) + + if (lat := self.cleaned_settings["lat"]) and ( + long := self.cleaned_settings["long"] + ): + output_class.locations(lat, long) + elif self.event.geo_lat and self.event.geo_lon: + output_class.locations(self.event.geo_lat, self.event.geo_lon) + + output_class.country_code(self.event.settings.locale) + + if hero := self.cleaned_settings["hero"]: + output_class.hero_image( + urljoin(eventreverse_absolute(self.event, "presale:event.index"), hero), + self.event.name, + ) + + if logo := self.cleaned_settings["logo"]: + output_class.logo( + urljoin(eventreverse_absolute(self.event, "presale:event.index"), logo), + self.event.name, + ) + + output_class.hex_background_color(self.cleaned_settings["bg_color"]) + output_class.confirmation_code_label(ConfirmationCode.orderNumber) + if self.group_is_active("venue") and all(self.venue()): output_class.venue(*self.venue()) + if SHIMMER: + output_class.securityAnimation(AnimationType.FOIL_SHIMMER) + + # TODO: callback url + # output_class.callback_url(eventreverse_absolute(event.organizer,"plugins:wallet:google_webhook",)) + + return output_class + + def _generate_object(self, op: OrderPosition, class_id: str): + output_object = EventTicketObject( + get_object_id(op), class_id, ObjectState.active, self.event.settings.locale + ) + fields = self.get_pass_fields(op) + + if fields["code"]: + output_object.barcode( + Barcode.qrCode, fields["code"][0]["value"], fields["code"][0]["value"] + ) + output_object.reservation_info(op.order.code) + output_object.ticket_number(op.code) + + output_object.valid_time_interval(op.valid_from, op.valid_until) + output_object.ticket_type( + get_translated_dict( + str(op.item) + + (" – " + str(op.variation.value) if op.variation else ""), + op.order.event.settings.get("locales"), + ) + ) + return output_object + + +class GoogleWalletDefaultEventTicket(GoogleWalletEventTicketStyle): + identifier = "event" + name = "Default Event Ticket" + + @property + def fieldgroups(self): + return [ + PredefinedFieldGroup(identifier="date", name=_("Date")), + PredefinedFieldGroup(identifier="seating", name=_("Seating")), + PredefinedFieldGroup(identifier="ticket_holder", name=_("Ticket Holder")), + *super().fieldgroups, + ] + + @property + def _preview_layout_front_card(self): + return [ + { + "fieldgroup": "date", + "sample": [ + { + "content": date_format( + self.event.date_from.astimezone(self.event.timezone), + "SHORT_DATE_FORMAT", + ), + "label": "Date", + }, + { + "content": date_format( + self.event.date_from.astimezone(self.event.timezone), + "TIME_FORMAT", + ), + "label": "Time", + }, + ], + }, + { + "fieldgroup": "seating", + "sample": [ + {"content": _("Ground floor"), "label": _("Section")}, + {"content": "5 / 2", "label": _("Row") + " / " + _("Seat")}, + ], + }, + ] + + @property + def _preview_layout_back_details(self): + return [ + { + "fieldgroup": "ticket_holder", + "sample": [ + {"content": _("John Doe"), "label": _("Ticket Holder")}, + ], + }, + { + "fieldgroup": "venue", + "sample": [ + {"content": self.venue()[1], "label": self.venue()[0]}, + ], + }, + { + "fieldgroup": "date", + "sample": [ + { + "content": date_format( + self.event.date_from.astimezone(self.event.timezone), + "DATETIME_FORMAT", + ), + "label": _("Event Start Time"), + }, + ], + }, + {"value": _("Sample product"), "label": _("Ticket Type")}, + {"value": "ABCDE-1", "label": _("Ticket Number")}, + {"value": "ABCDE", "label": _("Order Number")}, + { + "value": eventreverse_absolute(self.event, "presale:event.index"), + "label": _("Website"), + }, + ] + + def _generate_class(self): + output_class = super()._generate_class() if self.group_is_active("date"): if self.event.date_from: output_class.start(self.event.date_from.isoformat()) @@ -390,9 +429,6 @@ class GoogleWalletDefaultEventTicket(GoogleWalletEventTicketStyle): DoorsOpen.doorsOpen, self.event.date_admission.isoformat() ) - if SHIMMER: - output_class.securityAnimation(AnimationType.FOIL_SHIMMER) - print(output_class) return output_class def ticket_holder_name(self, op): @@ -400,14 +436,6 @@ class GoogleWalletDefaultEventTicket(GoogleWalletEventTicketStyle): 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"] - ) - output_object.reservation_info(op.order.code) - output_object.ticket_number(op.code) if self.group_is_active("ticket_holder") and self.ticket_holder_name(op): output_object.ticket_holder_name(self.ticket_holder_name(op)) @@ -426,16 +454,144 @@ class GoogleWalletDefaultEventTicket(GoogleWalletEventTicketStyle): elif seat.seat_number: output_object.seat(seat.seat_number) - output_object.valid_time_interval(op.valid_from, op.valid_until) - output_object.ticket_type( - get_translated_dict( - str(op.item) - + (" – " + str(op.variation.value) if op.variation else ""), - op.order.event.settings.get("locales"), - ) - ) - # places = settings.CURRENCY_PLACES.get(op.order.event.currency, 2) # output_object.face_value(int(op.price * 1000 ** places), op.order.event.currency) return output_object + + +class GoogleWalletCustomEventTicket(GoogleWalletEventTicketStyle): + identifier = "event_custom" + name = "Custom Event Ticket" + + @property + def _preview_layout_front_card(self): + return [ + { + "fieldgroup": "second_row", + }, + { + "fieldgroup": "third_row", + }, + ] + + @property + def _preview_layout_back_details(self): + return [{"fieldgroup": "details", "direction": "column"}] + + @property + def fieldgroups(self): + return [ + TextFieldGroup( + identifier="second_row", + name=_("Second Row"), + max_entries=3, + context_args={"order_position"}, + ), + TextFieldGroup( + identifier="third_row", + name=_("Third Row"), + max_entries=3, + context_args={"order_position"}, + ), + *super().fieldgroups, + TextFieldGroup( + identifier="details", + name=_("Details"), + max_entries=20, + context_args={"order_position"}, + ), + ] + + def _generate_class(self): + output_class = super()._generate_class() + + output_class.setdefault("classTemplateInfo", {}) + output_class["classTemplateInfo"].setdefault("cardTemplateOverride", {}) + output_class["classTemplateInfo"]["cardTemplateOverride"] = { + "cardRowTemplateInfos": [ + { + "threeItems": { + "startItem": { + "firstValue": { + "fields": [ + { + "fieldPath": "object.textModulesData['second_row-0']" + } + ] + } + }, + "middleItem": { + "firstValue": { + "fields": [ + { + "fieldPath": "object.textModulesData['second_row-1']" + } + ] + } + }, + "endItem": { + "firstValue": { + "fields": [ + { + "fieldPath": "object.textModulesData['second_row-2']" + } + ] + } + }, + } + }, + { + "threeItems": { + "startItem": { + "firstValue": { + "fields": [ + { + "fieldPath": "object.textModulesData['third_row-0']" + } + ] + } + }, + "middleItem": { + "firstValue": { + "fields": [ + { + "fieldPath": "object.textModulesData['third_row-1']" + } + ] + } + }, + "endItem": { + "firstValue": { + "fields": [ + { + "fieldPath": "object.textModulesData['third_row-2']" + } + ] + } + }, + } + }, + ] + } + + output_class["classTemplateInfo"].setdefault("detailsTemplateOverride", {}) + output_class["classTemplateInfo"]["detailsTemplateOverride"][ + "detailsItemInfo" + ] = [] + + return output_class + + def _generate_object(self, op: OrderPosition, class_id: str): + output_object = super()._generate_object(op, class_id) + fields = self.get_pass_fields(op) + + for row in ["second_row", "third_row"]: + for i, field in enumerate(fields.get(row, [])): + output_object.text_modules_data( + f"{row}-{i}", + localizable_to_dict(field["label"], self.event.settings.locales), + localizable_to_dict(field["value"], self.event.settings.locales), + ) + + return output_object