localization; bugfixes; google event ticket

This commit is contained in:
Kara Engelhardt committed 2026-11-05 13:22:46 +01:00
1 parent 2d0b22243c
commit c8e65e5db0
9 files changed
+389 -228

No files matched your search

+28 -12
View File
@@ -21,24 +21,27 @@
#
import logging
import os
import uuid
from datetime import datetime, timezone
from decimal import Decimal
from datetime import datetime, timedelta, timezone
from django.core.files.base import ContentFile
from django.db import transaction
from django.utils.timezone import now
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from i18nfield.strings import LazyI18nString
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, Order,
OrderPosition, CachedFile
CachedCombinedTicket, CachedFile, CachedTicket, Event, InvoiceAddress,
Order, OrderPosition,
)
from pretix.base.services.tasks import EventTask, ProfiledTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import register_ticket_outputs
from pretix.celery_app import app
from pretix.helpers.database import rolledback_transaction
from django.db import transaction
logger = logging.getLogger(__name__)
@@ -64,10 +67,11 @@ def generate_orderposition(order_position: int, provider: str, cf: CachedFile |
for ct in CachedTicket.objects.filter(order_position=order_position, provider=provider):
ct.delete()
ct = CachedTicket.objects.create(order_position=order_position, provider=provider,
extension=ext, type=ttype, file=None)
extension=ext, type=ttype, file=None)
ct.file.save(filename, ContentFile(data))
return ct.pk
def generate_order(order: int, provider: str, cf: CachedFile | None = None):
order = Order.objects.select_related('event').get(id=order)
@@ -89,10 +93,11 @@ def generate_order(order: int, provider: str, cf: CachedFile | None = None):
for ct in CachedCombinedTicket.objects.filter(order=order, provider=provider):
ct.delete()
ct = CachedCombinedTicket.objects.create(order=order, provider=provider, extension=ext,
type=ttype, file=None)
type=ttype, file=None)
ct.file.save(filename, ContentFile(data))
return ct.pk
@app.task(base=ProfiledTask)
def generate(model: str, pk: int, provider: str, cachedfile_pk: int | None = None):
with scopes_disabled():
@@ -109,18 +114,19 @@ def generate(model: str, pk: int, provider: str, cachedfile_pk: int | None = Non
class DummyRollbackException(Exception):
pass
def get_preview_position(event):
connection = transaction.get_connection()
if not connection.in_atomic_block:
raise RuntimeError("get_preview_position needs to be called in a rolledback_transaction")
item = event.items.create(name=_("Sample product"), default_price=Decimal('42.23'),
description=_("Sample product description"))
item2 = event.items.create(name=_("Sample workshop"), default_price=Decimal('23.40'))
item = event.items.create(name=LazyI18nString.from_gettext("Sample product"), default_price=Decimal('42.23'),
description=_("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))
end=datetime(2017, 12, 28, 0, 0, 0, tzinfo=timezone.utc))
from pretix.base.models import Order
from pretix.base.models import Order, SeatingPlan
order = event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
email='sample@pretix.eu',
locale=event.settings.locale,
@@ -129,7 +135,16 @@ def get_preview_position(event):
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
sample = {k: str(v) for k, v in scheme['sample'].items()}
position = order.positions.create(item=item, attendee_name_parts=sample, price=item.default_price)
SeatingPlan.objects.create(
name="Plan", organizer=event.organizer, layout="{}"
)
event.seat_category_mappings.create(
layout_category='Stalls', product=item
)
seat = event.seats.create(seat_number="1", row_label="C", product=item, seat_guid=uuid.uuid4())
position = order.positions.create(item=item, attendee_name_parts=sample, price=item.default_price, seat=seat)
s = event.subevents.first()
order.positions.create(item=item2, attendee_name_parts=sample, price=item.default_price, addon_to=position, subevent=s)
order.positions.create(item=item2, attendee_name_parts=sample, price=item.default_price, addon_to=position, subevent=s)
@@ -137,6 +152,7 @@ def get_preview_position(event):
InvoiceAddress.objects.create(order=order, name_parts=sample, company=_("Sample company"))
return position
def preview(event: int, provider: str, provider_arguments: dict = {}):
event = Event.objects.get(id=event)
+65
View File
@@ -0,0 +1,65 @@
from typing import Protocol, runtime_checkable
from django.utils import translation
from django.utils.formats import date_format
type MaybeTranslatedString = str | Localizable
@runtime_checkable
class Localizable(Protocol):
def localize(self, lng: str) -> str:
...
class LocalizableBase:
def localize(self, lng):
with translation.override(lng):
return str(self)
class FormattingLocalizableString(LocalizableBase):
def __init__(
self,
base_str: MaybeTranslatedString,
*format_args: MaybeTranslatedString,
**format_kwargs: MaybeTranslatedString,
):
self.base_str = base_str
self.format_args = format_args
self.format_kwargs = format_kwargs
def __str__(self):
return str(self.base_str).format(*self.format_args, **self.format_kwargs)
class GettextLocalizableString(LocalizableBase):
def __init__(self, base):
self.base = base
@classmethod
def gettext(cls, base):
"""Fake function so makemessages picks up these strings."""
return cls(base)
def __str__(self):
return translation.gettext(self.base)
class LocalizableDatetime(LocalizableBase):
def __init__(self, datetime, format):
self.datetime = datetime
self.format = format
def __str__(self):
return date_format(self.datetime, self.format)
def localizable_to_dict(string: MaybeTranslatedString, locales: list[str]):
translated = {}
for locale in locales:
with translation.override(locale):
translated[locale] = str(string)
return translated
@@ -0,0 +1,52 @@
# Generated by Django 5.2.17 on 2026-10-06 14:29
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"pretixbase",
"0312_alter_customer_locale_alter_devicelastseen_device_and_more",
),
("wallet", "0003_walletlayoutfilesetting"),
]
operations = [
migrations.AlterField(
model_name="walletlayoutitem",
name="layout",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="item_assignments",
to="wallet.walletlayout",
),
),
migrations.CreateModel(
name="GoogleWalletInstance",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("type", models.CharField()),
("identifier", models.CharField()),
("data", models.JSONField()),
(
"event",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="pretixbase.event",
),
),
],
options={
"unique_together": {("type", "identifier")},
},
),
]
+84 -78
View File
@@ -6,11 +6,14 @@ from django.contrib.staticfiles import finders
from django.core.files import File
from django.dispatch import receiver
from django.templatetags.static import static
from django.utils.formats import date_format
from i18nfield.strings import LazyI18nString
from pretix.base.models.event import Event
from pretix.base.templatetags.money import money_filter
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.plugins.wallet.i18n import (
FormattingLocalizableString, GettextLocalizableString, LocalizableDatetime,
MaybeTranslatedString,
)
from .signals import register_wallet_placeholders
@@ -38,12 +41,12 @@ class BaseWalletPlaceholder:
raise NotImplementedError()
@property
def label(self) -> LazyI18nString:
def label(self) -> MaybeTranslatedString:
"""The human readable name of this placeholder"""
raise NotImplementedError()
@property
def control_label(self) -> LazyI18nString:
def control_label(self) -> MaybeTranslatedString:
"""
The human readable name of this placeholder shown in the backend.
@@ -63,7 +66,7 @@ class BaseWalletTextPlaceholder(BaseWalletPlaceholder):
def content_type(self) -> str:
return "text"
def render(self, **context) -> LazyI18nString | str | None:
def render(self, **context) -> MaybeTranslatedString | 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``.
@@ -71,7 +74,7 @@ class BaseWalletTextPlaceholder(BaseWalletPlaceholder):
"""
raise NotImplementedError()
def render_sample(self, **context) -> LazyI18nString | str:
def render_sample(self, **context) -> MaybeTranslatedString:
"""
This method is called to generate a text to be used in previews.
@@ -79,8 +82,8 @@ class BaseWalletTextPlaceholder(BaseWalletPlaceholder):
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")
if not sample:
raise RuntimeError("`render` returned falsy value when rendering a sample")
return sample
@@ -111,11 +114,11 @@ class FunctionalWalletTextPlaceholder(BaseWalletTextPlaceholder):
def __init__(
self,
identifier: str,
label: LazyI18nString,
label: MaybeTranslatedString,
args: set[str],
func: Callable[..., LazyI18nString | str | None],
func: Callable[..., MaybeTranslatedString | None],
sample: (
None | LazyI18nString | str | Callable[..., str | LazyI18nString]
None | MaybeTranslatedString | Callable[..., MaybeTranslatedString]
) = None,
):
self._identifier = identifier
@@ -124,6 +127,9 @@ class FunctionalWalletTextPlaceholder(BaseWalletTextPlaceholder):
self.render = func
self._sample = sample
def __repr__(self):
return f"FunctionalWalletTextPlaceholder(identifier={self.identifier!r}, label={self.label!r})"
@property
def identifier(self):
return self._identifier
@@ -136,7 +142,7 @@ class FunctionalWalletTextPlaceholder(BaseWalletTextPlaceholder):
def required_context(self) -> set[str]:
return self._args
def render_sample(self, **context) -> LazyI18nString | str:
def render_sample(self, **context) -> MaybeTranslatedString:
if isinstance(self._sample, Callable):
return self._sample(**context)
elif self._sample:
@@ -149,7 +155,7 @@ class FunctionalWalletImagePlaceholder(BaseWalletImagePlaceholder):
def __init__(
self,
identifier: str,
label: LazyI18nString,
label: MaybeTranslatedString,
args: set[str],
func: Callable[..., File | None],
sample: None | str | Callable[..., str] = None,
@@ -334,13 +340,7 @@ def get_wallet_placeholder_renderer(**kwargs):
"seat",
{"order_position"},
lambda order_position: (
order_position.seat
if order_position.seat_id
else (
order_position.addon_to.seat
if order_position.addon_to_id
else None
)
order_position.seat if order_position.seat_id else None
),
),
],
@@ -364,15 +364,19 @@ def get_static_file(name) -> File | None:
def admission(subevent_or_event):
return (
subevent_or_event.date_admission.astimezone(subevent_or_event.timezone)
LocalizableDatetime(
subevent_or_event.date_admission.astimezone(subevent_or_event.timezone),
"SHORT_DATETIME_FORMAT",
)
if subevent_or_event.date_admission
else None
)
def date_from(subevent_or_event):
return subevent_or_event.get_date_from_display(
subevent_or_event.timezone, short=True
return LocalizableDatetime(
subevent_or_event.date_from.astimezone(subevent_or_event.timezone),
"SHORT_DATETIME_FORMAT",
)
@@ -395,62 +399,60 @@ def website(order_position, order):
register_wallet_placeholders,
dispatch_uid="plugin_wallet_register_wallet_placeholders",
)
def base_text_placeholders(sender, **kwargs):
def base_text_placeholders(sender: Event, **kwargs):
return [
FunctionalWalletTextPlaceholder(
"event",
LazyI18nString.from_gettext("Event"),
GettextLocalizableString.gettext("Event"),
{"event"},
lambda event: event.name,
),
FunctionalWalletTextPlaceholder(
"event_slug",
LazyI18nString.from_gettext("Event Slug"),
GettextLocalizableString.gettext("Event Slug"),
{"event"},
lambda event: event.slug,
),
FunctionalWalletTextPlaceholder(
"admission",
LazyI18nString.from_gettext("Admission"),
GettextLocalizableString.gettext("Admission"),
{"subevent_or_event"},
admission,
lambda subevent_or_event: subevent_or_event.get_date_from_display(
subevent_or_event.timezone, short=True
),
date_from,
),
FunctionalWalletTextPlaceholder(
"date_from",
LazyI18nString.from_gettext("Begin"),
GettextLocalizableString.gettext("Begin"),
{"subevent_or_event"},
date_from,
),
FunctionalWalletTextPlaceholder(
"admission_or_date_from",
LazyI18nString.from_gettext("Admission or Date From"),
GettextLocalizableString.gettext("Admission or Date From"),
{"subevent_or_event"},
admission_or_date_from,
),
FunctionalWalletTextPlaceholder(
"order",
LazyI18nString.from_gettext("Order Code"),
GettextLocalizableString.gettext("Order Code"),
{"order"},
lambda order: order.code,
),
FunctionalWalletTextPlaceholder(
"total",
LazyI18nString.from_gettext("Order Total"),
GettextLocalizableString.gettext("Order Total"),
{"event", "order"},
lambda event, order: money_filter(order.total, event.currency),
),
FunctionalWalletTextPlaceholder(
"order_email",
LazyI18nString.from_gettext("Order Email"),
GettextLocalizableString.gettext("Order Email"),
{"order"},
lambda order: order.email,
),
FunctionalWalletTextPlaceholder(
"price",
LazyI18nString.from_gettext("Item Price"),
GettextLocalizableString.gettext("Item Price"),
{"event", "order_position"},
lambda event, order_position: money_filter(
order_position.price, event.currency
@@ -458,74 +460,78 @@ def base_text_placeholders(sender, **kwargs):
),
FunctionalWalletTextPlaceholder(
"secret",
LazyI18nString.from_gettext("Order Secret (QR-Code-Content)"),
GettextLocalizableString.gettext("Order Secret (QR-Code-Content)"),
{"order_position"},
lambda order_position: order_position.secret,
),
FunctionalWalletTextPlaceholder(
"item",
LazyI18nString.from_gettext("Product"),
GettextLocalizableString.gettext("Product"),
{"item"},
lambda item: item.name,
),
FunctionalWalletTextPlaceholder(
"item_description",
LazyI18nString.from_gettext("Product description"),
GettextLocalizableString.gettext("Product description"),
{"item"},
lambda item: item.description,
),
FunctionalWalletTextPlaceholder(
"item_with_variation",
LazyI18nString.from_gettext("Product and variation"),
GettextLocalizableString.gettext("Product and variation"),
{"order_position"},
lambda order_position: (
f"{order_position.item.name} - {order_position.variation}"
FormattingLocalizableString(
"{name} - {variation}",
name=order_position.item.name,
variation=order_position.variation.name,
)
if order_position.variation
else order_position.item.name
),
),
FunctionalWalletTextPlaceholder(
"seat",
LazyI18nString.from_gettext("Seat: Full name"),
{"event", "seat"},
lambda event, seat: (
seat
if seat
else (
LazyI18nString.from_gettext("General admission")
if event.seating_plan_id is not None
else ""
)
GettextLocalizableString.gettext("Seat: Full name"),
{"seat"},
lambda seat: (
FormattingLocalizableString("{seat}", seat=seat) if seat else None
),
LazyI18nString.from_gettext("Ground floor, Row 3, Seat 4"),
GettextLocalizableString.gettext("Ground floor, Row 3, Seat 4")
),
FunctionalWalletTextPlaceholder(
"seat_zone",
LazyI18nString.from_gettext("Seat: zone"),
{"event", "seat"},
lambda event, seat: (
seat.zone_name
if seat
else (
LazyI18nString.from_gettext("General admission")
if event.seating_plan_id is not None
else ""
)
),
LazyI18nString.from_gettext("Ground floor"),
GettextLocalizableString.gettext("Seat: Zone"),
{"seat"},
lambda seat: seat.zone_name,
GettextLocalizableString.gettext("Ground floor"),
),
FunctionalWalletTextPlaceholder(
"seat_row",
GettextLocalizableString.gettext("Seat: Row"),
{"seat"},
lambda seat: seat.row_label or seat.row_name,
"C",
),
FunctionalWalletTextPlaceholder(
"seat_number",
GettextLocalizableString.gettext("Seat: Seat Number"),
{"seat"},
lambda seat: seat.seat_label or seat.seat_number,
"5",
),
FunctionalWalletTextPlaceholder(
"attendee_name",
LazyI18nString.from_gettext("Attendee name"),
GettextLocalizableString.gettext("Attendee name"),
{"order_position"},
lambda order_position: order_position.attendee_name,
),
FunctionalWalletTextPlaceholder(
"program_start",
LazyI18nString.from_gettext("Programm times: Start"),
GettextLocalizableString.gettext("Programm times: Start"),
{"item", "subevent_or_event"},
lambda item, subevent_or_event: (
date_format(
LocalizableDatetime(
min(pt.start for pt in item.program_times.all()).astimezone(
subevent_or_event.timezone
),
@@ -537,10 +543,10 @@ def base_text_placeholders(sender, **kwargs):
),
FunctionalWalletTextPlaceholder(
"program_end",
LazyI18nString.from_gettext("Programm times: End"),
GettextLocalizableString.gettext("Programm times: End"),
{"item", "subevent_or_event"},
lambda item, subevent_or_event: (
date_format(
LocalizableDatetime(
max(pt.end for pt in item.program_times.all()).astimezone(
subevent_or_event.timezone
),
@@ -552,56 +558,56 @@ def base_text_placeholders(sender, **kwargs):
),
FunctionalWalletTextPlaceholder(
"organizer",
LazyI18nString.from_gettext("Organizer"),
GettextLocalizableString.gettext("Organizer"),
{"organizer"},
lambda organizer: organizer.name,
),
FunctionalWalletTextPlaceholder(
"organizer_contact",
LazyI18nString.from_gettext("Organizer Contact"),
GettextLocalizableString.gettext("Organizer Contact"),
{"event"},
lambda event: event.settings.contact_mail,
"contact@example.com",
),
FunctionalWalletTextPlaceholder(
"order_code",
LazyI18nString.from_gettext("Order Code"),
GettextLocalizableString.gettext("Order Code"),
{"order"},
lambda order: order.code,
),
FunctionalWalletTextPlaceholder(
"purchase_date",
LazyI18nString.from_gettext("Purchase Date"),
GettextLocalizableString.gettext("Purchase Date"),
{"order", "subevent_or_event"},
lambda order, subevent_or_event: date_format(
lambda order, subevent_or_event: LocalizableDatetime(
order.datetime.astimezone(subevent_or_event.timezone),
"SHORT_DATETIME_FORMAT",
),
),
FunctionalWalletTextPlaceholder(
"website",
LazyI18nString.from_gettext("Website"),
GettextLocalizableString.gettext("Website"),
{"order_position", "order"},
website,
),
# IMAGES
FunctionalWalletImagePlaceholder(
"poweredby",
LazyI18nString.from_gettext("Logo"),
GettextLocalizableString.gettext("Logo"),
set(),
lambda: get_static_file("pretixplugins/wallet/logo.png"),
static("pretixplugins/wallet/logo.png"),
),
FunctionalWalletImagePlaceholder(
"poweredby_icon",
LazyI18nString.from_gettext("Icon"),
GettextLocalizableString.gettext("Icon"),
set(),
lambda: get_static_file("pretixplugins/wallet/icon.png"),
static("pretixplugins/wallet/icon.png"),
),
FunctionalWalletImagePlaceholder(
"example_no_preview",
LazyI18nString.from_gettext("Image with no preview"),
GettextLocalizableString.gettext("Image with no preview"),
set(),
lambda: get_static_file("pretix_passbook/icon.png"),
),
@@ -28,8 +28,8 @@ const layoutStyle = computed(() => {
v = `url(${URL.createObjectURL(val.file)})`; // TODO: encode
} else if (settingDef.type == "image" && val && val.url) {
v = `url(${val.url})`;
} else if (settingDef.type == "image" && settingDef.attrs.default) {
v = `url(${settingDef.attrs.default})`;
} else if (settingDef.type == "image" && settingDef.default) {
v = `url(${settingDef.default})`;
} else {
v = val;
}
+45 -59
View File
@@ -14,21 +14,17 @@ from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.files.uploadedfile import SimpleUploadedFile
from django.forms import ValidationError
from django.templatetags.static import static
from django.utils.encoding import force_bytes
from django.utils.translation import gettext as _, override
from i18nfield.strings import LazyI18nString
from django.utils.translation import gettext as _
from pretix.base.models import OrderPosition
from pretix.plugins.wallet.i18n import (
FormattingLocalizableString, GettextLocalizableString, Localizable,
MaybeTranslatedString,
)
from pretix.plugins.wallet.styles.base import (
ColorSettingsField,
FieldGroup,
FieldGroupDisplay,
FloatSettingsField,
ImageSettingsField,
PassStyle,
PlaceholderFieldEntry,
TextFieldGroup,
ColorSettingsField, FieldGroup, FieldGroupDisplay, FloatSettingsField,
ImageSettingsField, PassStyle, PlaceholderFieldEntry, TextFieldGroup,
WalletPlatform,
)
@@ -40,32 +36,15 @@ 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)
def lazyi18nstring_from_gettext(text: str, locales: set[str]) -> LazyI18nString:
data = {}
for locale in locales:
with override(locale):
data[locale] = _(text)
return LazyI18nString(data)
class StringResource:
entries: dict[str, LazyI18nString | FormattedLazyI18nString]
entries: dict[str, MaybeTranslatedString]
locales: set[str]
def __init__(self, locales):
self.entries = {}
self.locales = set(locales)
def add_entry(self, key: str, value: LazyI18nString | FormattedLazyI18nString):
def add_entry(self, key: str, value: MaybeTranslatedString):
if key in self.entries:
raise ValueError(f"{key} already exists in this StringResource")
self.entries[key] = value
@@ -78,9 +57,10 @@ class StringResource:
def generate_resource(self, language):
output = ""
for key, entry in self.entries.items():
output += (
f'"{self.escape(key)}" = "{self.escape(entry.localize(language))}";\n'
)
if isinstance(entry, str):
output += f'"{self.escape(key)}" = "{self.escape(entry)}";\n'
else:
output += f'"{self.escape(key)}" = "{self.escape(entry.localize(language))}";\n'
return output.strip()
def generate(self):
@@ -208,12 +188,16 @@ class AppleWalletStyle(PassStyle):
event = op.subevent or op.order.event
tz = event.timezone
ticket = str(op.item.name)
if op.variation:
ticket += " - " + str(op.variation)
ticket = (
FormattingLocalizableString(
"{name} - {variation}", name=op.item.name, variation=op.variation.name
)
if op.variation
else op.item.name
)
description = FormattedLazyI18nString(
LazyI18nString.from_gettext("Ticket for {event} ({product})"),
description = FormattingLocalizableString(
GettextLocalizableString.gettext("Ticket for {event} ({product})"),
event=self.event.name,
product=ticket,
)
@@ -236,14 +220,13 @@ class AppleWalletStyle(PassStyle):
**self.pass_content(fields, strings),
}
# TODO: Apples specs says this should be "specified as a CSS-style RGB triple, such as rgb(100, 10, 110).", but hex codes work as well
if bg_color := self.layout["settings"].get("bg_color"):
pass_json["backgroundColor"] = (
bg_color # TODO: specified as a CSS-style RGB triple, such as rgb(100, 10, 110).
)
pass_json["backgroundColor"] = bg_color
if fg_color := self.layout["settings"].get("fg_color"):
pass_json["foregroundColor"] = fg_color # TODO: specified as a CSS-style RGB triple, such as rgb(100, 10, 110).
pass_json["foregroundColor"] = fg_color
if label_color := self.layout["settings"].get("label_color"):
pass_json["labelColor"] = label_color # TODO: specified as a CSS-style RGB triple, such as rgb(100, 10, 110).
pass_json["labelColor"] = label_color
if "expirationDate" not in pass_json:
if op.valid_until:
@@ -289,7 +272,6 @@ class AppleWalletStyle(PassStyle):
"longitude": float(event.get_lon),
}
print(pass_json)
return pass_json
def generate(self, op: OrderPosition):
@@ -297,7 +279,6 @@ class AppleWalletStyle(PassStyle):
filename = "{}-{}.pkpass".format(order.event.slug, order.code)
fields = self.get_pass_fields(op)
pkpass = SignedZipFile(
self.event.settings.wallet_apple_ca_certificate.read(),
self.event.settings.wallet_apple_certificate.read(),
@@ -332,6 +313,7 @@ class AppleWalletStyle(PassStyle):
pkpass.add_file("background.png", background.read())
for lang, content in strings.generate().items():
print(lang, content)
pkpass.add_file(f"{lang}.lproj/pass.strings", content)
pkpass.add_file("pass.json", json.dumps(pass_json))
result = pkpass.finish()
@@ -360,8 +342,8 @@ class AppleWalletEventTicket(AppleWalletStyle):
default_entries=[
PlaceholderFieldEntry(
content="admission_or_date_from",
label=lazyi18nstring_from_gettext(
"Admission", self.event.settings.locales
label=GettextLocalizableString.gettext(
"Admission"
),
),
],
@@ -388,8 +370,8 @@ class AppleWalletEventTicket(AppleWalletStyle):
default_entries=[
PlaceholderFieldEntry(
content="item_with_variation",
label=lazyi18nstring_from_gettext(
"Product", self.event.settings.locales
label=GettextLocalizableString.gettext(
"Product"
),
)
],
@@ -404,8 +386,8 @@ class AppleWalletEventTicket(AppleWalletStyle):
default_entries=[
PlaceholderFieldEntry(
content="seat",
label=lazyi18nstring_from_gettext(
"Seat", locales=self.event.settings.locales
label=GettextLocalizableString.gettext(
"Seat"
),
),
PlaceholderFieldEntry(
@@ -413,14 +395,14 @@ class AppleWalletEventTicket(AppleWalletStyle):
),
PlaceholderFieldEntry(
content="program_start",
label=lazyi18nstring_from_gettext(
"From", locales=self.event.settings.locales
label=GettextLocalizableString.gettext(
"From"
),
),
PlaceholderFieldEntry(
content="program_end",
label=lazyi18nstring_from_gettext(
"To", locales=self.event.settings.locales
label=GettextLocalizableString.gettext(
"To"
),
),
],
@@ -450,8 +432,8 @@ class AppleWalletEventTicket(AppleWalletStyle):
),
PlaceholderFieldEntry(
content="order_email",
label=lazyi18nstring_from_gettext(
"Ordered by", self.event.settings.locales
label=GettextLocalizableString.gettext(
"Ordered by"
),
),
PlaceholderFieldEntry(
@@ -511,16 +493,20 @@ class AppleWalletEventTicket(AppleWalletStyle):
]
def convert_fields(self, strings, fields, prefix):
# TODO: detect if a string-value/string-label is the same as a generated field name
# eg if a placeholder returns "primary-0-label", this would not be shown to the user but instead replaces with the label
converted = []
for i, f in enumerate(fields):
converted_field = {**f, "key": f"{prefix}-{i}"}
if "label" in converted_field and isinstance(
converted_field["label"], LazyI18nString
converted_field["label"], Localizable
):
strings.add_entry(f"{prefix}-{i}-label", converted_field["label"])
converted_field["label"] = f"{prefix}-{i}-label"
if isinstance(converted_field["value"], LazyI18nString):
if isinstance(
converted_field["value"], Localizable
):
strings.add_entry(f"{prefix}-{i}-value", converted_field["value"])
converted_field["value"] = f"{prefix}-{i}-value"
converted.append(converted_field)
+26 -31
View File
@@ -4,20 +4,17 @@ from typing import Any, Literal, TypedDict
import jsonschema
from django.core.exceptions import ValidationError
from django.core.files import File
from django.templatetags.static import static
from i18nfield.strings import LazyI18nString
from pretix.api.helpers import handle_file_upload
from pretix.base.models import OrderPosition
from ..i18n import localizable_to_dict
from ..placeholders import (
WalletPlaceholderRenderer,
get_available_context,
get_wallet_placeholder_renderer,
get_wallet_placeholders,
MaybeTranslatedString, WalletPlaceholderRenderer, get_available_context,
get_wallet_placeholder_renderer, get_wallet_placeholders,
)
from django.templatetags.static import static
from urllib.parse import urljoin
from pretix.multidomain.urlreverse import eventreverse_absolute
class WalletPlatform:
@@ -61,7 +58,7 @@ class FieldGroup:
) -> dict:
raise NotImplementedError()
def asdict(self, context: LayoutContext):
def asdict(self, locales, context: LayoutContext):
return {
"type": self.type.value,
"identifier": self.identifier,
@@ -83,44 +80,42 @@ class FieldEntryType(enum.Enum):
class FieldEntry[T]:
type: FieldEntryType
label: LazyI18nString | None
label: MaybeTranslatedString | None
content: T
def __init__(
self, type: FieldEntryType, content: T, label: LazyI18nString | None = None
self, type: FieldEntryType, content: T, label: MaybeTranslatedString | None = None
):
self.type = type
self.label = label
self.content = content
def asdict(self) -> dict:
def asdict(self, locales) -> dict:
return {
"type": self.type.value,
"content": self.content,
"label": self.label.data if self.label is not None else None,
"label": localizable_to_dict(self.label, locales) if self.label is not None 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: MaybeTranslatedString | None = None):
self.label = label
self.content = content
class CustomFieldEntry(FieldEntry[LazyI18nString]):
class CustomFieldEntry(FieldEntry[MaybeTranslatedString]):
type: FieldEntryType
label: LazyI18nString | None
content: LazyI18nString
content: MaybeTranslatedString
def asdict(self) -> dict:
def asdict(self, locales) -> dict:
return {
"type": self.type.value,
"content": self.content.data,
"label": self.label.data if self.label else None,
"content": localizable_to_dict(self.content, locales),
"label": localizable_to_dict(self.label, locales) if self.label else None,
}
@@ -170,11 +165,11 @@ class PlaceholderFieldGroup(FieldGroup):
if self.required and (self.min_entries is None or self.min_entries < 1):
self.min_entries = 1
def asdict(self, context: LayoutContext):
def asdict(self, locales, context: LayoutContext):
return {
**super().asdict(context),
**super().asdict(locales, context),
"content_type": self.content_type.value,
"default_entries": [x.asdict() for x in self.default_entries],
"default_entries": [x.asdict(locales) for x in self.default_entries],
"display": self.display.value,
"min_entries": self.min_entries,
"max_entries": self.max_entries,
@@ -283,7 +278,7 @@ class SettingsField:
required: bool
default: str | None
def asdict(self):
def asdict(self, locales):
return {
"identifier": self.identifier,
"label": self.label,
@@ -398,7 +393,7 @@ class FloatSettingsField(SettingsField):
self.default = default
def layout_schema(self):
schema = {"type": "number"}
schema: dict[str, Any] = {"type": "number"}
if self.min is not None:
schema["minimum"] = self.min
if self.max is not None:
@@ -407,8 +402,8 @@ class FloatSettingsField(SettingsField):
schema = {"oneOf": [schema, {"type": "null"}]}
return schema
def asdict(self):
return super().asdict() | {"min": self.min, "max": self.max}
def asdict(self, locales):
return super().asdict(locales) | {"min": self.min, "max": self.max}
class PassStyle:
@@ -466,9 +461,9 @@ class PassStyle:
return {
"identifier": self.identifier,
"name": self.name,
"fieldgroups": [x.asdict(context) for x in self.fieldgroups],
"fieldgroups": [x.asdict(self.event.settings.locales, context) for x in self.fieldgroups],
"preview_layout": self.preview_layout,
"settings": [x.asdict() for x in self.settings],
"settings": [x.asdict(self.event.settings.locales) for x in self.settings],
}
def layout_schema(self):
@@ -580,7 +575,7 @@ class PassStyle:
for field in self.layout["fieldgroups"][group.identifier][
"entries"
]:
field_entry = {}
field_entry: dict[str, MaybeTranslatedString | None] = {}
if group.display == FieldGroupDisplay.WITH_LABEL:
field_entry["label"] = LazyI18nString(field["label"])
if field["type"] == FieldEntryType.PLACEHOLDER.value:
@@ -592,7 +587,7 @@ class PassStyle:
and not str(field_entry["label"])
and label
):
field_entry["label"] = LazyI18nString(label)
field_entry["label"] = label
elif field["type"] == FieldEntryType.CUSTOM.value:
field_entry["value"] = LazyI18nString(field["content"])
+83 -33
View File
@@ -1,43 +1,29 @@
import uuid
from functools import cached_property
from urllib.parse import urljoin
import uuid
from django.conf import settings
from django.db import transaction
from django.utils import translation
from django.utils.formats import date_format
from django.utils.translation import gettext as _
from walletobjects import ButtonJWT, EventTicketClass, EventTicketObject
from walletobjects.comms import Comms
from walletobjects.constants import (
Barcode,
ClassType,
MultipleDevicesAndHoldersAllowedStatus,
ObjectState,
ObjectType,
AnimationType, Barcode, ClassType, ConfirmationCode, DoorsOpen,
MultipleDevicesAndHoldersAllowedStatus, ObjectState, ObjectType,
ReviewStatus,
AnimationType,
DoorsOpen,
ConfirmationCode
)
from pretix.base.models import Event, OrderPosition
from pretix.base.settings import GlobalSettingsObject
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.plugins.wallet.models import GoogleWalletInstance, GoogleWalletType
from pretix.plugins.wallet.styles.base import (
ColorSettingsField,
FieldGroup,
FieldGroupDisplay,
FloatSettingsField,
ImageFieldGroup,
ImageSettingsField,
PassStyle,
PlaceholderFieldEntry,
PredefinedFieldGroup,
TextFieldGroup,
WalletPlatform,
ColorSettingsField, FieldGroupDisplay, FloatSettingsField, ImageFieldGroup,
ImageSettingsField, PassStyle, PlaceholderFieldEntry, PredefinedFieldGroup,
TextFieldGroup, WalletPlatform,
)
from django.db import transaction
SHIMMER = False
@@ -310,15 +296,31 @@ class GoogleWalletDefaultEventTicket(GoogleWalletEventTicketStyle):
{
"fieldgroup": "date",
"sample": [
{"content": "01/01/1970", "label": "Date"},
{"content": "12:34", "label": "Time"},
{
"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": "5", "label": "Row"},
{"content": "2", "label": "Seat"},
{"content": _("Ground floor"), "label": _("Section")},
{"content": "5 / 2", "label": _("Row") + " / " + _("Seat")},
],
},
{"fieldgroup": "code"},
@@ -339,6 +341,22 @@ class GoogleWalletDefaultEventTicket(GoogleWalletEventTicketStyle):
{"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": "ABCDE-1", "label": _("Ticket Number")},
{"value": "ABCDE", "label": _("Order Number")},
]
},
]
@@ -364,8 +382,22 @@ class GoogleWalletDefaultEventTicket(GoogleWalletEventTicketStyle):
output_class = super()._generate_class()
if self.group_is_active("venue") and all(self.venue()):
output_class.venue(*self.venue())
if self.group_is_active("date"):
if self.event.date_from:
output_class.start(self.event.date_from.isoformat())
if self.event.settings.show_date_to and self.event.date_to:
output_class.end(self.event.date_to.isoformat())
if self.event.date_admission:
output_class.doors_open(
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):
@@ -382,14 +414,31 @@ class GoogleWalletDefaultEventTicket(GoogleWalletEventTicketStyle):
output_object.reservation_info(op.order.code)
output_object.ticket_number(op.code)
if self.group_is_active("venue") and self.ticket_holder_name(op):
if self.group_is_active("ticket_holder") and self.ticket_holder_name(op):
output_object.ticket_holder_name(self.ticket_holder_name(op))
# output_object.ticket_type(
# get_translated_dict(
# str(op.item) + (" – " + str(op.variation.value) if op.variation else ""),
# op.order.event.settings.get('locales')
# )
# )
if self.group_is_active("seating") and (seat := self.op.seat):
if seat.zone_name:
output_object.section(seat.zone_name)
if seat.row_label:
output_object.row(seat.row_label)
elif seat.row_name:
output_object.row(seat.row_name)
if seat.seat_label:
output_object.seat(seat.seat_label)
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 = django_settings.CURRENCY_PLACES.get(op.order.event.currency, 2)
# output_object.face_value(int(op.price * 1000 ** places), op.order.event.currency)
@@ -411,4 +460,5 @@ class GoogleWalletDefaultEventTicket(GoogleWalletEventTicketStyle):
# )
# return self._comms().put_item(ObjectType.eventTicketObject, object_name, output_object)
print(output_object)
return output_object
+4 -13
View File
@@ -14,7 +14,6 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import (
CreateView, DeleteView, DetailView, ListView, View,
)
from i18nfield.strings import LazyI18nString
from pretix_vrpayment_wero.payment import HttpRequest
from pretix.base.i18n import language
@@ -23,6 +22,7 @@ from pretix.base.services.tickets import get_preview_position
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.database import rolledback_transaction
from pretix.helpers.models import modelclone
from pretix.plugins.wallet.i18n import localizable_to_dict
from .models import WalletLayout
from .placeholders import (
@@ -33,13 +33,6 @@ from .styles import (
)
def localize_lazyi18n_string(string: LazyI18nString, locales):
data = {}
for locale in locales:
data[locale] = string.localize(locale)
return data
def get_editor_placeholders(event):
with (
rolledback_transaction(),
@@ -50,11 +43,7 @@ def get_editor_placeholders(event):
placeholders = {
t: {
pid: {
"label": (
localize_lazyi18n_string(p.label, event.settings.locales)
if isinstance(p.label, LazyI18nString)
else str(p.label)
),
"label": (localizable_to_dict(p.label, event.settings.locales)),
"sample": str(context.render_sample(p)),
"required_context": list(sorted(p.required_context)),
}
@@ -220,6 +209,8 @@ class LayoutPreviewView(EventPermissionRequiredMixin, View):
layout.validate()
fname, mimet, data = layout.generate(p)
if mimet == "text/uri-list":
return redirect(data)
resp = HttpResponse(data, content_type=mimet)
ftype = fname.split(".")[-1]
if not mimet.startswith("text/"):