Files
pretix_cgo/src/pretix/plugins/wallet/styles.py
Kara Engelhardt 2e7d54174d WIP
2026-05-18 17:27:23 +02:00

170 lines
5.6 KiB
Python

from dataclasses import dataclass, field, asdict
from typing import Literal
import enum
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from i18nfield.strings import LazyI18nString
from .models import WalletLayout
class WalletPlatform:
identifier: str
name: str
class ApplePlatform(WalletPlatform):
identifier = "apple"
name = _("Apple")
class GooglePlatform(WalletPlatform):
identifier = "google"
name = _("Google")
class PlaceholderFieldType(enum.Enum):
TEXT = "text"
CODE = "qr"
IMAGE = "image"
PREDEFINED = "predefined"
# TODO: POWERED_BY ?
@dataclass
class PlaceholderField:
type: PlaceholderFieldType
label: LazyI18nString
value: LazyI18nString
def asdict(self):
return {'type': self.type.value, 'label': self.label, 'value': self.value}
@dataclass
class FieldGroupDefinition:
name: str
identifier: str
entry_type: PlaceholderFieldType
min_entries: int | None = None
max_entries: int | None = None
def asdict(self):
return {"identifier": self.identifier, "name": self.name, "min_entries": self.min_entries, "max_entries": self.max_entries}
@dataclass
class PlaceholderFieldGroup(FieldGroupDefinition):
entry_type: PlaceholderFieldType = PlaceholderFieldType.TEXT
default_entries: list[PlaceholderField] = field(default_factory=list)
def asdict(self):
asdict = super().asdict()
asdict['default_entries'] = [x.asdict() for x in self.default_entries]
return asdict
@dataclass
class PredefinedFieldGroup(FieldGroupDefinition):
entry_type: PlaceholderFieldType = PlaceholderFieldType.PREDEFINED
min_entries = 0
max_entries = 1
class PassStyle:
identifier: str # unique within platform
name: str
platform: Literal["apple"] | Literal["google"]
fields: list[FieldGroupDefinition]
# preview_image: str # TODO: preview
def asdict(self):
return {
"identifier": self.identifier,
"name": self.name,
"platform": self.platform,
"fields": [x.asdict() for x in self.fields],
}
class AppleWalletEventTicket(PassStyle):
identifier = "event_1"
name = "Event Ticket Layout 1"
platform = "apple"
# 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)
fields = [
PlaceholderFieldGroup(
identifier="logo",
name=_("Logo"),
min_entries=1,
max_entries=1,
default_entries=[
PlaceholderField(PlaceholderFieldType.IMAGE, "logo", "event:image")
],
entry_type=PlaceholderFieldType.IMAGE,
),
PlaceholderFieldGroup(identifier="primary", name=_("Primary"), min_entries=1, max_entries=1),
PlaceholderFieldGroup(
identifier="secondary", name=_("Secondary"), max_entries=4
), # 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."
PlaceholderFieldGroup(
identifier="headers", name=_("Header"), max_entries=3
), # TODO: header image
PlaceholderFieldGroup(identifier="auxillary", name=_("Auxillary"), max_entries=4),
PlaceholderFieldGroup(identifier="back", name=_("Back")),
]
# preview_image = "apple/event_ticket.svg"
class GoogleWalletEventTicket(PassStyle):
identifier = "event"
name = "Event Ticket"
platform = "google"
fields = [
PredefinedFieldGroup(identifier="seating", name=_("Seating")),
PlaceholderFieldGroup(identifier="qrcode", name=_("QR-Code"), entry_type=PlaceholderFieldType.CODE),
]
AVAILABLE_PLATFORMS = {"apple": ApplePlatform, "google": GooglePlatform}
AVAILABLE_STYLES = [AppleWalletEventTicket(), GoogleWalletEventTicket()]
def get_platforms_with_styles():
platforms_with_styles = {}
for style in AVAILABLE_STYLES:
platform = style.platform
if platform not in platforms_with_styles:
platforms_with_styles[platform] = {}
platforms_with_styles[platform][style.identifier] = style
return platforms_with_styles
def get_platform_styles(platform):
platform_styles = {}
for style in AVAILABLE_STYLES:
if style.platform == platform:
platform_styles[style.identifier] = style
return platform_styles
def get_platforms():
return AVAILABLE_PLATFORMS
class PassLayout:
style: PassStyle
layout: dict
def __init__(self, style, layout):
self.style = style
self.layout = layout
def validate(self):
self.validate_fields()
def validate_fields(self):
style_fields = self.style.fields
if 'fields' not in self.layout:
raise ValidationError(_("Layout did not contain any fields"))
layout_fields = self.layout['fields']
if not isinstance(layout_fields, dict):
raise ValidationError(_("'fields' must be dict"))
for fieldgroup in style_fields:
layout_field_data = layout_fields.get(fieldgroup.identifier, [])
if fieldgroup.min_entries and fieldgroup.min_entries < len(layout_field_data):
raise ValidationError(_("At least {min_entries} must be specified for {name}").format(min_entries=fieldgroup.min_entries, name=fieldgroup.name))