WIP: first successful pass creation

This commit is contained in:
Kara Engelhardt
2026-06-01 15:25:59 +02:00
parent 682e27126e
commit ad9cdc9f89
5 changed files with 100 additions and 61 deletions

View File

@@ -21,7 +21,7 @@ const selection = computed({
get() {
if (entry.value.type === 'placeholder') {
return entry.value.content
} else if (entry.value.type === 'text') {
} else if (entry.value.type === 'custom') {
return "other"
} else {
throw new Error(`Unknown entry type "${entry.value.type}"`);
@@ -29,7 +29,7 @@ const selection = computed({
},
set(newValue) {
if (newValue == "other") {
entry.value.type = "text"
entry.value.type = "custom"
entry.value.content = {};
} else {
entry.value.type = "placeholder"
@@ -42,7 +42,7 @@ const textContent = computed({
get() {
if (entry.value.type === 'placeholder') {
return ""
} else if (entry.value.type === 'text') {
} else if (entry.value.type === 'custom') {
return entry.value.content
} else {
throw new Error(`Unknown entry type "${entry.value.type}"`);

View File

@@ -111,10 +111,10 @@ class SignedZipFile:
class AppleWalletStyle(PassStyle):
platform = ApplePlatform
def pass_content(self, layout, context, strings):
def pass_content(self, fields, strings):
raise NotImplementedError()
def generate_pass_json(self, layout, context, strings):
def generate_pass_json(self, fields, context, strings):
def add_from_context(key):
value = context.get(key)
if not value:
@@ -128,7 +128,7 @@ class AppleWalletStyle(PassStyle):
"passTypeIdentifier": add_from_context("passTypeIdentifier"),
"teamIdentifier": add_from_context("teamIdentifier"),
"serialNumber": add_from_context("serialNumber"),
**self.pass_content(layout, context, strings),
**self.pass_content(fields, strings),
}
return pass_json
@@ -136,6 +136,9 @@ class AppleWalletStyle(PassStyle):
for key in ["ca_certificate", "certificate", "key", "password", "locales"]:
if key not in context:
raise ValueError(f"{key} missing from context")
fields = self.get_pass_fields(layout, context)
pkpass = SignedZipFile(
context["ca_certificate"],
context["certificate"],
@@ -144,12 +147,23 @@ class AppleWalletStyle(PassStyle):
)
strings = StringResource(locales=context['locales'])
pass_json = self.generate_pass_json(layout, context, strings)
pass_json = self.generate_pass_json(fields, context, strings)
print(pass_json)
pkpass.add_file(
"icon.png", open(finders.find("pretix_passbook/icon.png"), "rb").read()
)
if fields['logo']:
logo = fields['logo'][0]['value']
else:
logo = open(finders.find("pretix_passbook/logo.png"), "rb")
if fields['icon']:
icon = fields['icon'][0]['value']
else:
icon = open(finders.find("pretix_passbook/icon.png"), "rb")
pkpass.add_file("icon.png", icon.read())
pkpass.add_file("logo.png", logo.read())
for lang, content in strings.generate().items():
pkpass.add_file(f"{lang}.lproj/pass.strings", content)
pkpass.add_file("pass.json", json.dumps(pass_json))
return pkpass.finish()
@@ -158,6 +172,18 @@ class AppleWalletEventTicket(AppleWalletStyle):
identifier = "event_1"
name = _("Event Ticket Layout 1")
fieldgroups = [
ImageFieldGroup(
identifier="icon",
name=_("Icon"),
min_entries=0,
max_entries=1,
labels=False,
default_entries=[
PlaceholderFieldEntry(
content="poweredby",
)
],
),
ImageFieldGroup(
identifier="logo",
name=_("Logo"),
@@ -194,47 +220,6 @@ class AppleWalletEventTicket(AppleWalletStyle):
]
# preview_image = "apple/event_ticket.svg"
def get_pass_fields(self, layout, context):
fields = {}
for group in self.fieldgroups:
if isinstance(group, PredefinedFieldGroup):
pass
elif isinstance(group, PlaceholderFieldGroup):
group_fields = []
if group.identifier in layout["fieldgroups"]:
for field in layout["fieldgroups"][group.identifier]["entries"]:
field_entry = {}
if group.labels:
field_entry["label"] = LazyI18nString(field["label"])
if field["type"] == FieldEntryType.PLACEHOLDER.value:
placeholder = (
context.get("placeholders")
.get(group.content_type.value, {})
.get(field["content"])
)
if placeholder:
placeholder_value = placeholder["evaluate"](
*context.get("evaluation_context", [])
)
if placeholder_value:
field_entry["value"] = placeholder_value
elif field["type"] == FieldEntryType.TEXT.value:
placeholder_value = LazyI18nString(field["content"])
elif field["type"] == FieldEntryType.IMAGE.value:
raise NotImplementedError(
"Image placeholders not implemented"
)
if "value" in field_entry and field_entry["value"]:
group_fields.append(field_entry)
if group.min_entries and len(group_fields) < group.min_entries:
raise ValueError(
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]
else:
raise ValueError("Unknown field group")
return fields
def convert_fields(self, strings, fields, prefix):
converted = []
for i,f in enumerate(fields):
@@ -243,13 +228,18 @@ class AppleWalletEventTicket(AppleWalletStyle):
strings.add_entry(f"{prefix}-{i}-label", converted_field['label'])
converted_field['label'] = f"{prefix}-{i}-label"
if isinstance(converted_field['value'], LazyI18nString):
strings.add_entry(f"{prefix}-{i}-value", converted_field['value'])
converted_field['value'] = f"{prefix}-{i}-value"
converted.append(converted_field)
return converted
def pass_content(self, layout, context, strings):
fields = self.get_pass_fields(layout, context)
def pass_content(self, fields, strings):
return {
"eventTicket": {
"primaryFields": self.convert_fields(strings, fields['primary'], 'primary')
"primaryFields": self.convert_fields(strings, fields['primary'], 'primary'),
"secondaryFields": self.convert_fields(strings, fields['secondary'], 'secondary'),
"auxillaryFields": self.convert_fields(strings, fields['auxillary'], 'auxillary'),
"backFields": self.convert_fields(strings, fields['back'], 'back'),
}
}

View File

@@ -49,8 +49,7 @@ class FieldContentType(enum.Enum):
class FieldEntryType(enum.Enum):
IMAGE = "image"
TEXT = "text"
CUSTOM = "custom"
PLACEHOLDER = "placeholder"
@@ -191,7 +190,7 @@ class PlaceholderFieldGroup(FieldGroup):
{
"properties": {
**baseprops,
"type": {"const": self.content_type.value},
"type": {"const": "custom"},
"content": {"$ref": "#/$defs/I18nString"},
}
},
@@ -276,6 +275,49 @@ class PassStyle:
def generate(self, layout, context):
raise NotImplementedError()
def render_placeholder(self, context, content_type, content):
placeholder = (
context.get("placeholders")
.get(content_type, {})
.get(content)
)
if placeholder:
placeholder_value = placeholder["evaluate"](
*context.get("evaluation_context", [])
)
if placeholder_value:
return placeholder_value
def get_pass_fields(self, layout, context):
fields = {}
for group in self.fieldgroups:
if isinstance(group, PredefinedFieldGroup):
pass
elif isinstance(group, PlaceholderFieldGroup):
group_fields = fields.get(group.identifier, [])
if group.identifier in layout["fieldgroups"]:
for field in layout["fieldgroups"][group.identifier]["entries"]:
field_entry = {}
if group.labels:
field_entry["label"] = LazyI18nString(field["label"])
if field["type"] == FieldEntryType.PLACEHOLDER.value:
field_entry["value"] = self.render_placeholder(context, group.content_type.value, field['content'])
elif field["type"] == FieldEntryType.CUSTOM.value:
field_entry["value"] = LazyI18nString(field["content"])
if "value" in field_entry and field_entry["value"]:
group_fields.append(field_entry)
if group.min_entries and len(group_fields) < group.min_entries:
raise ValueError(
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 := layout["fieldgroups"][group.identifier]['overflow']):
fields.setdefault(overflow_group, [])
fields[overflow_group] += group_fields[group.max_entries:]
else:
raise ValueError("Unknown field group")
return fields
class PassLayout:
style: PassStyle

View File

@@ -27,6 +27,8 @@ from pretix.base.settings import SettingsSandbox
from django.template.loader import render_to_string
from .styles import AVAILABLE_STYLES_DICT
from .styles.apple import ApplePlatform
from .styles.google import GooglePlatform
from .models import WalletLayout
from .views import get_layout_variables
@@ -65,12 +67,14 @@ class GoogleWalletTicketOutput(WalletOutput):
identifier = "wallet_google"
verbose_name = _("Google")
download_button_text = "Add to Google Wallet"
platform = GooglePlatform
class AppleWalletTicketOutput(WalletOutput):
identifier = "wallet_apple"
verbose_name = _("Apple")
download_button_text = "Add to Apple Wallet"
platform = ApplePlatform
def generate(self, op):
order = op.order
@@ -87,6 +91,7 @@ class AppleWalletTicketOutput(WalletOutput):
# )
# )
layout = WalletLayout.objects.get(pk=1)
platform_layout = layout.platform_layouts.get(platform=self.platform.identifier)
ticket = str(op.item.name)
if op.variation:
@@ -121,9 +126,9 @@ class AppleWalletTicketOutput(WalletOutput):
"serialNumber": serialNumber,
"locales": event.settings.locales
}
assert layout.platform == "apple"
data = AVAILABLE_STYLES_DICT[layout.platform][layout.style].generate(
layout.layout, context
data = AVAILABLE_STYLES_DICT[self.platform.identifier][platform_layout.style].generate(
platform_layout.layout, context
)
return filename, "application/vnd.apple.pkpass", data

View File

@@ -12,12 +12,14 @@ from django.conf import settings
from .models import WalletLayout
from .styles import AVAILABLE_STYLES, AVAILABLE_PLATFORMS
from django.contrib.staticfiles import finders
def get_layout_variables(event):
return {
"text": get_variables(event),
"image": get_images(event)
| {"poweredby": {"label": _("pretix-Logo")}}, # TODO: image upload
| {"poweredby": {"label": _("pretix-Logo"), "evaluate": lambda *_: open(finders.find("pretix_passbook/logo.png"), "rb")},
"poweredby_icon": {"label": _("pretix-Icon"), "evaluate": lambda *_: open(finders.find("pretix_passbook/icon.png"), "rb")}}, # TODO: image upload
}