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))