This commit is contained in:
Kara Engelhardt
2026-08-11 09:54:14 +02:00
parent d703cfa877
commit 4e02edc7d6
14 changed files with 347 additions and 221 deletions
+1 -1
View File
@@ -37,5 +37,5 @@ class WalletApp(AppConfig):
description = _("Issue wallet passes for tickets (e.g. apple wallet, google wallet)")
def ready(self):
from . import signals # NOQA
from . import receivers # NOQA
+2 -2
View File
@@ -27,8 +27,6 @@ from pretix.base.models import LoggedModel, OrderPosition
from django_scopes import ScopedManager
from django.core.exceptions import ValidationError
from pretix.plugins.wallet.styles import get_style
from pretix.plugins.wallet.styles.base import PassStyle
class WalletLayout(LoggedModel):
@@ -68,6 +66,8 @@ class WalletPlatformLayout(LoggedModel):
@property
def pass_layout(self):
from pretix.plugins.wallet.styles import get_style
style = get_style(self.platform, self.style)
if style:
return style(event=self.parent.event, layout=self.layout)
+6 -1
View File
@@ -181,6 +181,11 @@ class WalletPlaceholderContext:
k: v for k, v in self.context_args.items() if k in placeholder.required_context
}
@classmethod
def is_available(cls, placeholder: BaseWalletPlaceholder, context_args: set[str]):
missing_context = placeholder.required_context - context_args
return not missing_context
def render_placeholder(self, placeholder: BaseWalletPlaceholder):
if placeholder.identifier in self.cache:
return self.cache[placeholder.identifier]
@@ -192,7 +197,7 @@ class WalletPlaceholderContext:
return placeholder.render_sample(**self._get_placeholder_context(placeholder))
def get_wallet_placeholders(event):
def get_wallet_placeholders(event) -> dict[str, dict[str, BaseWalletPlaceholder]]:
placeholders = {
"text": {
v.identifier: v
+37
View File
@@ -0,0 +1,37 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from pretix.base.signals import register_ticket_outputs, register_global_settings, EventPluginSignal
from .ticketoutput import OUTPUTS
def connect_signals():
for output in OUTPUTS:
# DIY functools.partial to make get_defining_app happy
def get_register_func(o):
def register(sender, **kwargs):
return o
return register
register_ticket_outputs.connect(get_register_func(output), dispatch_uid=f"wallet_output_{output.identifier}")
if hasattr(output, "get_global_settings"):
register_global_settings.connect(output.get_global_settings, dispatch_uid=f"wallet_global_settings_{output.identifier}")
connect_signals()
+1 -16
View File
@@ -20,22 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
from pretix.base.signals import register_ticket_outputs, register_global_settings, EventPluginSignal
from .ticketoutput import OUTPUTS
def connect_signals():
for output in OUTPUTS:
# DIY functools.partial to make get_defining_app happy
def get_register_func(o):
def register(sender, **kwargs):
return o
return register
register_ticket_outputs.connect(get_register_func(output), dispatch_uid=f"wallet_output_{output.identifier}")
if hasattr(output, "get_global_settings"):
register_global_settings.connect(output.get_global_settings, dispatch_uid=f"wallet_global_settings_{output.identifier}")
connect_signals()
from pretix.base.signals import EventPluginSignal
register_wallet_text_placeholders = EventPluginSignal()
"""
@@ -11,43 +11,54 @@ const store = inject(StoreKey)!;
const gettext = (window as any).gettext;
const props = defineProps<{
fieldgroup: PlaceholderFieldGroupDefinition;
overflows: FieldGroupDefinition[];
fieldgroup: PlaceholderFieldGroupDefinition;
overflows: FieldGroupDefinition[];
}>();
const fieldConfig = defineModel<PlaceholderFieldGroupConfig>({
required: true,
required: true,
});
const overflowOptions = computed((): Array<[string | null, string]> => {
if (props.overflows.length) {
return [
...props.overflows.map((x): [string, string] => [x.identifier, x.name]),
[null, "Do not overflow"],
];
} else {
return [];
}
if (props.overflows.length) {
return [
...props.overflows.map((x): [string, string] => [x.identifier, x.name]),
[null, "Do not overflow"],
];
} else {
return [];
}
});
function addVariable() {
fieldConfig.value.entries.push({ type: "placeholder", label: "" });
fieldConfig.value.entries.push({ type: "placeholder", label: "" });
}
watchEffect(() => {
if (!fieldConfig.value) {
fieldConfig.value = {
overflow: null,
entries: JSON.parse(JSON.stringify(props.fieldgroup.default_entries)),
active:
props.fieldgroup.required ||
props.fieldgroup.default_entries.length > 0,
};
}
if (fieldConfig.value && !fieldConfig.value.entries) {
fieldConfig.value.entries = JSON.parse(
JSON.stringify(props.fieldgroup.default_entries),
);
}
if (!fieldConfig.value) {
fieldConfig.value = {
overflow: null,
entries: JSON.parse(JSON.stringify(props.fieldgroup.default_entries)),
active:
props.fieldgroup.required ||
props.fieldgroup.default_entries.length > 0,
};
}
if (fieldConfig.value && !fieldConfig.value.entries) {
fieldConfig.value.entries = JSON.parse(
JSON.stringify(props.fieldgroup.default_entries),
);
}
});
const placeholderChoices = computed(() => {
const availableContext = new Set(props.fieldgroup.context_args);
const choices = Object.entries(store.variables.text)
.filter(([_, { required_context }]) =>
new Set(required_context).isSubsetOf(availableContext),
)
.map(([k, v]): [string, string] => [k, v.label]);
choices.push(["other", gettext("Other…")]);
return choices;
});
</script>
@@ -66,21 +77,22 @@ watchEffect(() => {
th(:class="'col-md-' + (fieldgroup.display == 'with_label' ? '6' : '11')") {{ gettext('Content') }}
th.col-xs-1
tbody
tr(v-for="n,i in fieldConfig.entries.length" :key="i")
tr(v-for="n, i in fieldConfig.entries.length" :key="i")
td(v-if="fieldgroup.display == 'with_label'")
.i18n-form-group
I18nInput(v-model="fieldConfig.entries[n-1].label")
I18nInput(v-model="fieldConfig.entries[n - 1].label")
td
TextContent(v-if='fieldgroup.content_type == "text"'
v-model="fieldConfig.entries[n-1]")
v-model="fieldConfig.entries[n - 1]"
:placeholderChoices="placeholderChoices")
Select(v-else-if='fieldgroup.content_type == "image"'
v-model="fieldConfig.entries[n-1].content"
:choices="Object.entries(store.variables.image).map(([k,v]) => [k, v.label])"
v-model="fieldConfig.entries[n - 1].content"
:choices="Object.entries(store.variables.image).map(([k, v]) => [k, v.label])"
)
td.text-right
button.btn.btn-danger.form-control-static(type="button" @click="fieldConfig.entries.splice(n-1, 1)")
button.btn.btn-danger.form-control-static(type="button" @click="fieldConfig.entries.splice(n - 1, 1)")
i.fa.fa-trash
span.sr-only {{ gettext('Delete')}}
span.sr-only {{ gettext('Delete') }}
button.btn.btn-default(type="button" @click="addVariable")
i.fa.fa-plus
@@ -6,7 +6,7 @@ import { i18nstringLocalize } from "../../helpers";
const store = inject(StoreKey)!;
const props = defineProps<{
config: PreviewFieldgroup;
config: PredefinedFieldGroupConfig;
style_def: PredefinedFieldGroupDefinition;
}>();
@@ -17,10 +17,10 @@ const isActive = computed(
</script>
<template lang="pug">
div.fieldgroup-container(v-if="isActive" :style="{'flex-grow': config.relSize, 'flex-direction': config.direction || 'row'}")
div.fieldgroup-item(v-for="{ label, content } of config.sample")
div.fieldgroup-label.nowrap(v-if="!!label") {{ label }}
div.nowrap.content(v-if="!!content" :class="config.display") {{ content }}
div.fieldgroup-container(v-if="isActive" :style="{'flex-grow': config.relSize, 'flex-direction': config.direction || 'row'}")
div.fieldgroup-item(v-for="{ label, content } of config.sample")
div.fieldgroup-label.nowrap(v-if="!!label") {{ i18nstringLocalize(label) }}
div.nowrap.content(v-if="!!content" :class="config.display") {{ i18nstringLocalize(content) }}
</template>
@@ -9,14 +9,20 @@ const props = defineProps<{
</script>
<template lang="pug">
div.preview-row(v-if="'children' in config" :style="{flexDirection: config.direction || 'row'}")
RowPreview(v-for="child of config.children" :config="child")
div.preview-row(v-if="'children' in config" :style="{ flexDirection: config.direction || 'row' }" :class="config.display")
RowPreview(v-for="child of config.children" :config="child" )
FieldgroupPreview(v-else-if="'fieldgroup' in config" :config="config")
FixedPreview(v-else-if="'value' in config" :config="config")
</template>
<style lang="css" scoped>
.preview-row {
width: 100%; display: flex; gap: 1em; display: flex
width: 100%;
display: flex;
gap: 1em;
display: flex
}
.tight {
gap: 0;
}
</style>
@@ -7,13 +7,11 @@ import { StoreKey } from "../walletStore";
const store = inject(StoreKey)!
const gettext = (window as any).gettext
const entry = defineModel<FieldEntry>({ required: true })
const props = defineProps<{
placeholderChoices: [string|null, string][];
}>();
const selectChoices = computed(() =>{
const choices = Object.entries(store.variables.text).map(([k,v]): [string, string] => [k, v.label])
choices.push(["other", gettext("Other…")])
return choices
});
const entry = defineModel<FieldEntry>({ required: true })
const selection = computed({
get() {
@@ -53,7 +51,7 @@ const textContent = computed({
.i18n-form-group
Select(
v-model="selection"
:choices="selectChoices"
:choices="placeholderChoices"
)
I18nInput(v-model="textContent" v-if="selection === 'other'" :locales="store.locales")
</template>
@@ -19,6 +19,7 @@ type PlaceholderFieldGroupDefinition = BaseFieldGroupDefinition & {
display: FieldGroupDisplay;
min_entries: number | null;
max_entries: number | null;
context_args: string[]
};
type PredefinedFieldGroupDefinition = BaseFieldGroupDefinition & {
@@ -52,6 +53,7 @@ type Style = {
type Variable = {
label: string;
sample: string;
required_context: string[];
};
type Platform = {
+70 -59
View File
@@ -30,16 +30,24 @@ 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)
class StringResource:
# mapping string in default event locale -> LazyI18nString
entries: dict[str, LazyI18nString]
entries: dict[str, LazyI18nString | FormattedLazyI18nString]
locales: set[str]
def __init__(self, locales):
self.entries = {}
self.locales = set(locales)
def add_entry(self, key: str, value: LazyI18nString):
def add_entry(self, key: str, value: LazyI18nString | FormattedLazyI18nString):
if key in self.entries:
raise ValueError(f"{key} already exists in this StringResource")
self.entries[key] = value
@@ -126,69 +134,51 @@ class AppleWalletStyle(PassStyle):
def pass_content(self, fields, strings):
raise NotImplementedError()
def generate_pass_json(self, fields, context, strings):
def add_from_context(key):
value = context.get(key)
if not value:
raise ValueError(f"{key} must be set to a truthy value")
return value
def generate_pass_json(self, fields, op, strings):
ticket = str(op.item.name)
if op.variation:
ticket += " - " + str(op.variation)
description = FormattedLazyI18nString(
LazyI18nString.from_gettext("Ticket for {event} ({product})"),
event=self.event.name,
product=ticket,
)
strings.add_entry("description", description)
serialNumber = "%s-%s-%s-%d" % (
self.event.organizer.slug,
self.event.slug,
op.order.code,
op.pk,
)
pass_json = {
"formatVersion": 1,
"description": add_from_context("description"),
"organizationName": add_from_context("organizationName"),
"passTypeIdentifier": add_from_context("passTypeIdentifier"),
"teamIdentifier": add_from_context("teamIdentifier"),
"serialNumber": add_from_context("serialNumber"),
"description": "description",
"organizationName": self.event.organizer.name,
"passTypeIdentifier": self.event.settings.wallet_apple_pass_type_id,
"teamIdentifier": self.event.settings.wallet_apple_team_id,
"serialNumber": serialNumber,
**self.pass_content(fields, strings),
}
return pass_json
def generate(self, op: OrderPosition):
context = self.get_context(op)
order = op.order
event = order.event
filename = "{}-{}.pkpass".format(order.event.slug, order.code)
ticket = str(op.item.name)
if op.variation:
ticket += " - " + str(op.variation)
serialNumber = "%s-%s-%s-%d" % (
order.event.organizer.slug,
order.event.slug,
order.code,
op.pk,
)
context.update({
"ca_certificate": order.event.settings.wallet_apple_ca_certificate.read(),
"certificate": order.event.settings.wallet_apple_certificate.read(),
"key": order.event.settings.wallet_apple_key.read(),
"password": order.event.settings.wallet_apple_key_password,
"description": _("Ticket for {event} ({product})").format( # TODO: i18n
event=event.name, product=ticket
),
"organizationName": event.organizer.name,
"passTypeIdentifier": order.event.settings.wallet_apple_pass_type_id,
"teamIdentifier": order.event.settings.wallet_apple_team_id,
"serialNumber": serialNumber,
})
fields = self.get_pass_fields(layout, context)
fields = self.get_pass_fields(op)
pkpass = SignedZipFile(
context["ca_certificate"],
context["certificate"],
context["key"],
context["password"],
self.event.settings.wallet_apple_ca_certificate.read(),
self.event.settings.wallet_apple_certificate.read(),
self.event.settings.wallet_apple_key.read(),
self.event.settings.wallet_apple_key_password,
)
strings = StringResource(locales=context["locales"])
strings = StringResource(locales=self.event.settings.locales)
pass_json = self.generate_pass_json(fields, context, strings)
pass_json = self.generate_pass_json(fields, op, strings)
print(pass_json)
if fields["logo"]:
logo = fields["logo"][0]["value"]
@@ -210,7 +200,6 @@ class AppleWalletStyle(PassStyle):
return filename, "application/vnd.apple.pkpass", result
class AppleWalletEventTicket(AppleWalletStyle):
identifier = "event_1"
name = _("Event Ticket Layout 1")
@@ -225,7 +214,8 @@ class AppleWalletEventTicket(AppleWalletStyle):
content="poweredby",
)
],
required=True
required=True,
context_args={"event", "order", "order_position"},
),
ImageFieldGroup(
identifier="logo",
@@ -237,7 +227,8 @@ class AppleWalletEventTicket(AppleWalletStyle):
content="poweredby",
)
],
required=True
required=True,
context_args={"event", "order", "order_position"},
),
TextFieldGroup(
identifier="logo_text",
@@ -245,6 +236,7 @@ class AppleWalletEventTicket(AppleWalletStyle):
max_entries=1,
display=FieldGroupDisplay.PLAIN,
default_entries=[],
context_args={"event", "order", "order_position"},
),
TextFieldGroup(
identifier="primary",
@@ -258,13 +250,27 @@ class AppleWalletEventTicket(AppleWalletStyle):
)
], # TODO: support Lazyi18nproxy here
description=_("These fields appear prominently featured on the pass."),
required=True
required=True,
context_args={"event", "order", "order_position"},
),
TextFieldGroup(
identifier="secondary", name=_("Secondary"), max_entries=4
identifier="secondary",
name=_("Secondary"),
max_entries=4,
context_args={"event", "order", "order_position"},
), # 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."
TextFieldGroup(identifier="header", name=_("Header"), max_entries=3),
TextFieldGroup(identifier="auxiliary", name=_("Auxiliary"), max_entries=4),
TextFieldGroup(
identifier="header",
name=_("Header"),
max_entries=3,
context_args={"event", "order", "order_position"},
),
TextFieldGroup(
identifier="auxiliary",
name=_("Auxiliary"),
max_entries=4,
context_args={"event", "order", "order_position"},
),
TextFieldGroup(
identifier="code",
name=_("QR-Code"),
@@ -275,8 +281,13 @@ class AppleWalletEventTicket(AppleWalletStyle):
content="secret",
)
],
context_args={"event", "order", "order_position"},
),
TextFieldGroup(
identifier="back",
name=_("Back"),
context_args={"event", "order", "order_position"},
),
TextFieldGroup(identifier="back", name=_("Back")),
]
preview_layout = [
[
+86 -52
View File
@@ -1,24 +1,33 @@
import enum
from typing import Any
from typing import Any, TypedDict
from black.nodes import is_vararg
from i18nfield.strings import LazyI18nString
import jsonschema
from django.core.exceptions import ValidationError
from pretix.base.models import OrderPosition
from ..placeholders import WalletPlaceholderContext, get_wallet_placeholders
class WalletPlatform:
identifier: str
name: str
class LayoutContext(TypedDict):
placeholders: dict[str, dict]
class FieldGroupType(enum.Enum):
PLACEHOLDER = "placeholder"
PREDEFINED = "predefined"
class FieldGroupDisplay(enum.Enum):
PLAIN = "plain"
WITH_LABEL = "with_label"
CODE = "code"
class FieldGroup:
type: FieldGroupType
identifier: str
@@ -35,7 +44,7 @@ class FieldGroup:
def layout_schema(
self,
remaining_fields: list["FieldGroup"],
context: dict,
context: LayoutContext,
) -> dict:
raise NotImplemented()
@@ -72,16 +81,19 @@ class FieldEntry[T]:
self.content = content
def asdict(self) -> dict:
return {"type": self.type.value, "content": self.content, "label": self.label.data if self.label else None}
return {
"type": self.type.value,
"content": self.content,
"label": self.label.data if self.label 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: LazyI18nString | None = None):
self.label = label
self.content = content
@@ -92,8 +104,11 @@ class CustomFieldEntry(FieldEntry[LazyI18nString]):
content: LazyI18nString
def asdict(self) -> dict:
return {"type": self.type.value, "content": self.content.data, "label": self.label.data if self.label else None}
return {
"type": self.type.value,
"content": self.content.data,
"label": self.label.data if self.label else None,
}
class PredefinedFieldGroup(FieldGroup):
@@ -102,11 +117,10 @@ class PredefinedFieldGroup(FieldGroup):
def layout_schema(
self,
remaining_fields: list["FieldGroup"],
context: dict,
context: LayoutContext,
):
return {
"type": "object"
}
return {"type": "object"}
class PlaceholderFieldGroup(FieldGroup):
type = FieldGroupType.PLACEHOLDER
@@ -115,18 +129,22 @@ class PlaceholderFieldGroup(FieldGroup):
display: FieldGroupDisplay
min_entries: int | None
max_entries: int | None
context_args: set[
str
] # what context arguments are available when rendering this fieldgroup
def __init__(
self,
identifier: str,
name: str,
content_type: FieldContentType,
description: str="",
description: str = "",
required=False,
default_entries=None,
min_entries=None,
max_entries=None,
display=FieldGroupDisplay.WITH_LABEL,
context_args: set[str] | None = None,
):
super().__init__(identifier, name, description, required)
self.content_type = content_type
@@ -134,6 +152,7 @@ class PlaceholderFieldGroup(FieldGroup):
self.min_entries = min_entries
self.max_entries = max_entries
self.display = display
self.context_args = context_args or set()
if self.required and (self.min_entries is None or self.min_entries < 1):
self.min_entries = 1
@@ -146,18 +165,26 @@ class PlaceholderFieldGroup(FieldGroup):
"display": self.display.value,
"min_entries": self.min_entries,
"max_entries": self.max_entries,
"context_args": list(sorted(self.context_args))
}
def layout_schema(
self,
remaining_fields: list["FieldGroup"],
context: dict,
context: LayoutContext,
):
placeholders = list(context.get("placeholders", {}).get(self.content_type.value, {}).keys())
content_type_placeholders = (
context["placeholders"].get(self.content_type.value, {}).values()
)
available_placeholders = [
x.identifier
for x in content_type_placeholders
if WalletPlaceholderContext.is_available(x, self.context_args)
]
return {
"type": "object",
"properties": {
"entries": self.entries_schema(placeholders=placeholders),
"entries": self.entries_schema(placeholders=available_placeholders),
"overflow": {
"anyOf": [
{"type": "null"},
@@ -212,7 +239,6 @@ class PlaceholderFieldGroup(FieldGroup):
return schema
class TextFieldGroup(PlaceholderFieldGroup):
content_type = FieldContentType.TEXT
@@ -234,23 +260,25 @@ class PassStyle:
# 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)
fieldgroups: list[FieldGroup]
preview_layout: list | None
@classmethod
def asdict(cls):
@property
def preview_layout(self) -> list | None:
return None
def asdict(self):
return {
"identifier": cls.identifier,
"name": cls.name,
"fieldgroups": [x.asdict() for x in cls.fieldgroups],
"preview_layout": cls.preview_layout
"identifier": self.identifier,
"name": self.name,
"fieldgroups": [x.asdict() for x in self.fieldgroups],
"preview_layout": self.preview_layout,
}
@classmethod
def layout_schema(cls, context):
def layout_schema(self):
context = LayoutContext(placeholders=self.placeholders)
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
# TODO: $id
"title": cls.name,
"title": self.name,
"type": "object",
"properties": {
"fieldgroups": {
@@ -258,12 +286,12 @@ class PassStyle:
"type": "object",
"properties": {
group.identifier: group.layout_schema(
context=context, remaining_fields=cls.fieldgroups[i:]
context=context, remaining_fields=self.fieldgroups[i:]
)
for (i, group) in enumerate(cls.fieldgroups)
for (i, group) in enumerate(self.fieldgroups)
},
"required": [
group.identifier for group in cls.fieldgroups if group.required
group.identifier for group in self.fieldgroups if group.required
],
}
},
@@ -276,41 +304,37 @@ class PassStyle:
}
},
}
if any(group.required for group in cls.fieldgroups):
if any(group.required for group in self.fieldgroups):
schema["required"] = ["fieldgroups"]
return schema
@classmethod
def render_placeholder(cls, context, content_type, content):
placeholder = (
context.get("placeholders", {})
.get(content_type, {})
.get(content)
)
def render_placeholder(self, context, content_type, content):
placeholder = self.placeholders.get(content_type, {}).get(content)
if placeholder:
placeholder_value = context['placeholder_context'].render_placeholder(placeholder)
placeholder_value = context.render_placeholder(placeholder)
if placeholder_value:
return placeholder.label, placeholder_value
return None, None
def __init__(self, event, layout):
self.event = event
self.layout = layout
def get_layout_context(self):
return {"placeholders": {}}
self.placeholders = get_wallet_placeholders(self.event)
def validate(self):
schema = self.layout_schema(self.get_layout_context())
schema = self.layout_schema()
try:
jsonschema.validate(self.layout, schema)
except jsonschema.ValidationError as e:
raise ValidationError("Invalid layout: {}".format(str(e)))
def get_pass_fields(self, context):
def get_pass_fields(self, op: OrderPosition):
context = WalletPlaceholderContext(
event=self.event, order=op.order, order_position=op
)
fields = {}
for group in self.fieldgroups:
if isinstance(group, PredefinedFieldGroup):
@@ -319,14 +343,22 @@ class PassStyle:
elif isinstance(group, PlaceholderFieldGroup):
group_fields = fields.get(group.identifier, [])
if group.identifier in self.layout["fieldgroups"]:
for field in self.layout["fieldgroups"][group.identifier]["entries"]:
for field in self.layout["fieldgroups"][group.identifier][
"entries"
]:
field_entry = {}
if group.display == FieldGroupDisplay.WITH_LABEL:
field_entry["label"] = LazyI18nString(field["label"])
if field["type"] == FieldEntryType.PLACEHOLDER.value:
label, field_entry["value"] = self.render_placeholder(context, group.content_type.value, field['content'])
if group.display == FieldGroupDisplay.WITH_LABEL and not str(field_entry['label']) and label:
field_entry['label'] = LazyI18nString(label)
label, field_entry["value"] = self.render_placeholder(
context, group.content_type.value, field["content"]
)
if (
group.display == FieldGroupDisplay.WITH_LABEL
and not str(field_entry["label"])
and label
):
field_entry["label"] = LazyI18nString(label)
elif field["type"] == FieldEntryType.CUSTOM.value:
field_entry["value"] = LazyI18nString(field["content"])
@@ -337,13 +369,15 @@ class PassStyle:
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 := self.layout["fieldgroups"][group.identifier]['overflow']):
if overflow_group := self.layout["fieldgroups"][group.identifier][
"overflow"
]:
fields.setdefault(overflow_group, [])
fields[overflow_group] += group_fields[group.max_entries:]
fields[overflow_group] += group_fields[group.max_entries :]
else:
raise ValueError("Unknown field group")
return fields
def generate(self, op: OrderPosition):
raise NotImplementedError()
raise NotImplementedError()
+75 -39
View File
@@ -1,3 +1,5 @@
from i18nfield.fields import LazyI18nString
from pretix.base.models import Event, OrderPosition
from .base import (
@@ -186,11 +188,15 @@ class GoogleWalletStyle(PassStyle):
comms = Comms(self.event.settings.get("wallet_google_credentials").read())
class_object = self._generate_class()
ticket_object = self._generate_object(op, class_id=class_object['id'])
ticket_object = self._generate_object(op, class_id=class_object["id"])
# TODO: privacy screen
class_object = comms.put_item(ClassType.eventTicketClass, class_object['id'], class_object)
ticket_object = comms.put_item(ObjectType.eventTicketObject, ticket_object['id'], ticket_object)
class_object = comms.put_item(
ClassType.eventTicketClass, class_object["id"], class_object
)
ticket_object = comms.put_item(
ObjectType.eventTicketObject, ticket_object["id"], ticket_object
)
generated_jwt = comms.sign_jwt(
ButtonJWT(
@@ -201,8 +207,7 @@ class GoogleWalletStyle(PassStyle):
)
)
return "https://pay.google.com/gp/v/save/%s" % generated_jwt
return 'googlepaypass', 'text/uri-list', 'https://pay.google.com/gp/v/save/%s' % generated_jwt
class GoogleWalletEventTicket(GoogleWalletStyle):
identifier = "event"
@@ -219,6 +224,8 @@ class GoogleWalletEventTicket(GoogleWalletStyle):
)
],
),
PredefinedFieldGroup(identifier="venue", name=_("Venue")),
PredefinedFieldGroup(identifier="date", name=_("Date")),
PredefinedFieldGroup(identifier="seating", name=_("Seating")),
TextFieldGroup(
identifier="code",
@@ -230,46 +237,75 @@ class GoogleWalletEventTicket(GoogleWalletStyle):
content="secret",
)
],
context_args={"event", "order", "order_position"},
),
]
preview_layout = [
[
{
"children": [
{"fieldgroup": "logo", "relSize": 1},
{
"value": "issuerName",
"relSize": 3,
"display": ["large", "centered"],
},
]
},
{
"children": [
{"value": "venueName", "display": "small"},
{"value": "eventName", "display": "large"},
],
"direction": "column",
},
{
"children": [
{"value": "01/01/1970", "label": "Date"},
{"value": "12:34", "label": "Time"},
]
},
{"fieldgroup": "seating",
"sample": [
{"content": "5", "label": "Row"},
{"content": "2", "label": "Seat"},
]
},
{"fieldgroup": "code"},
@property
def preview_layout(self):
return [
[
{
"children": [
{"fieldgroup": "logo", "relSize": 1},
{
"value": str(self.event.organizer.name),
"relSize": 3,
"display": ["large", "centered"],
},
]
},
{
"children": [
{
"fieldgroup": "venue",
"sample": [
{"content": self.venue()[0], "label": ""},
],
},
{"value": str(self.event.name), "display": "large"},
],
"direction": "column",
"display": ["tight"]
},
{
"fieldgroup": "date",
"sample": [
{"content": "01/01/1970", "label": "Date"},
{"content": "12:34", "label": "Time"},
],
},
{
"fieldgroup": "seating",
"sample": [
{"content": "5", "label": "Row"},
{"content": "2", "label": "Seat"},
],
},
{"fieldgroup": "code"},
]
]
]
def venue(self):
if self.event.location:
name = {}
address = {}
for key, value in self.event.location.data.items():
lines = value.splitlines()
name[key] = lines[0]
# We must provide at least one address line each for the name and address - no way around it.
if len(lines) > 1:
address[key] = '\n'.join(value.splitlines()[1:])
else:
address[key] = lines[0]
return name, address
return None, None
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"]
+3 -3
View File
@@ -40,7 +40,7 @@ def get_editor_placeholders(event):
context = WalletPlaceholderContext(event=event, order=p.order, order_position=p)
placeholders = {
t: {
pid: {"label": str(p.label), "sample": str(context.render_sample(p))}
pid: {"label": str(p.label), "sample": str(context.render_sample(p)), "required_context": list(sorted(p.required_context))}
for pid, p in ps.items()
}
for t, ps in get_wallet_placeholders(event).items()
@@ -76,7 +76,7 @@ class LayoutEditorView(LayoutDetailView):
"identifier": platform.identifier,
"name": platform.name,
"styles": {
style.identifier: style.asdict()
style.identifier: style(self.request.event, None).asdict()
for style in AVAILABLE_STYLES.get(platform.identifier)
},
}
@@ -174,7 +174,7 @@ class LayoutPreviewView(EventPermissionRequiredMixin, View):
layout = style(event=event, layout=layout)
layout.validate()
fname, mimet, data = platform.generate(layout, p)
fname, mimet, data = layout.generate(p)
resp = HttpResponse(data, content_type=mimet)
ftype = fname.split(".")[-1]
if not mimet.startswith("text/"):