WIP: predefined preview, google api, custom placeholder class

This commit is contained in:
Kara Engelhardt
2026-07-28 19:00:11 +02:00
parent 13d2e809aa
commit 8bbf90a5d0
20 changed files with 496 additions and 325 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ from .models import WalletLayout, WalletPlatformLayout
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from .views import get_layout_variables
from .views import get_editor_placeholders
from rest_framework import serializers
@@ -34,7 +34,7 @@ class WalletPlatformLayoutSerializer(I18nAwareModelSerializer):
style = platform_styles[data["style"]]
layout = PassLayout(style=style, layout=data["layout"])
context = {"placeholders": get_layout_variables(self.context['event'])}
context = {"placeholders": get_editor_placeholders(self.context['event'])}
layout.validate(context=context)
return data
+275
View File
@@ -0,0 +1,275 @@
from collections.abc import Callable
from .signals import (
register_wallet_text_placeholders,
register_wallet_image_placeholders,
)
from django.core.files import File
from django.dispatch import receiver
from pretix.base.templatetags.money import money_filter
from django.contrib.staticfiles import finders
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from i18nfield.strings import LazyI18nString
class BaseWalletPlaceholder:
"""
This is the base class for all wallet placeholders.
"""
@property
def required_context(self) -> set[str]:
"""
A a set of all attribute names that need to be contained in the base context so that this placeholder is available.
"""
return set()
@property
def identifier(self) -> str:
"""The unique identifier of this placeholder"""
raise NotImplementedError()
@property
def label(self) -> LazyI18nString:
"""The human readable name of this placeholder"""
raise NotImplementedError()
@property
def control_label(self) -> LazyI18nString:
"""
The human readable name of this placeholder shown in the backend.
Defaults to `label`
"""
return self.label
def render(self, **context):
raise NotImplementedError()
def render_sample(self, **context):
raise NotImplementedError()
class BaseWalletTextPlaceholder(BaseWalletPlaceholder):
def render(self, **context) -> str | 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``.
You are expected to return a plain-text string.
"""
raise NotImplementedError()
def render_sample(self, **context) -> str:
"""
This method is called to generate a text to be used in previews.
You will be passed sample instances of the arguments specified in ``required_context``.
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")
return sample
class BaseWalletImagePlaceholder(BaseWalletPlaceholder):
def render(self, **context) -> File | None:
"""
This method is called to generate the image that is being shown on the pass.
You will be passed the keyword arguments specified in ``required_context``.
You are expected to return a `File` object.
"""
raise NotImplementedError()
def render_sample(self, **context) -> str | None:
"""
This method is called to generate a text to be used in previews.
You will be passed sample instances of the arguments specified in ``required_context``.
You are expected to return a URL to a sample image or `None` if no sample can be shown.
"""
return None
class FunctionalWalletTextPlaceholder(BaseWalletTextPlaceholder):
def __init__(
self,
identifier: str,
label: LazyI18nString,
args: set[str],
func: Callable[..., str | None],
sample: None | str | Callable[..., str] = None,
):
self._identifier = identifier
self._label = label
self._args = args
self.render = func
self._sample = sample
@property
def identifier(self):
return self._identifier
@property
def label(self):
return self._label
@property
def required_context(self) -> set[str]:
return self._args
def render_sample(self, **context) -> str:
if isinstance(self._sample, Callable):
return self._sample(**context)
elif self._sample:
return self._sample
else:
return super().render_sample(**context)
class FunctionalWalletImagePlaceholder(BaseWalletImagePlaceholder):
def __init__(
self,
identifier: str,
label: LazyI18nString,
args: set[str],
func: Callable[..., File | None],
sample: None | str | Callable[..., str] = None,
):
self._identifier = identifier
self._label = label
self._args = args
self.render = func
self._sample = sample
@property
def required_context(self) -> set[str]:
return self._args
@property
def identifier(self):
return self._identifier
@property
def label(self):
return self._label
def render_sample(self, **context) -> str | None:
if isinstance(self._sample, Callable):
return self._sample(**context)
return self._sample
class MissingContextException(Exception):
pass
class WalletPlaceholderContext:
def __init__(self, **kwargs):
self.context_args = kwargs
self.cache = {}
def _get_placeholder_context(self, placeholder: BaseWalletPlaceholder):
missing_context = placeholder.required_context - self.context_args.keys()
if missing_context:
raise MissingContextException(
f"Missing context args for '{placeholder.identifier}': {', '.join(missing_context)}"
)
return {
k: v for k, v in self.context_args.items() if k in placeholder.required_context
}
def render_placeholder(self, placeholder: BaseWalletPlaceholder):
if placeholder.identifier in self.cache:
return self.cache[placeholder.identifier]
value = self.cache[placeholder.identifier] = placeholder.render(**self._get_placeholder_context(placeholder))
return value
def render_sample(self, placeholder: BaseWalletPlaceholder):
return placeholder.render_sample(**self._get_placeholder_context(placeholder))
def get_wallet_placeholders(event):
placeholders = {
"text": {
v.identifier: v
for r, vs in register_wallet_text_placeholders.send(sender=event)
for v in vs
},
"image": {
v.identifier: v
for r, vs in register_wallet_image_placeholders.send(sender=event)
for v in vs
},
}
return placeholders
def get_static_file(name) -> File | None:
path: str | None = finders.find(name) # type: ignore
if not path:
return
return File(open(path, "rb"))
@receiver(
register_wallet_text_placeholders,
dispatch_uid="plugin_wallet_register_wallet_text_placeholders",
)
def base_text_placeholders(sender, **kwargs):
return [
FunctionalWalletTextPlaceholder("name", LazyI18nString.from_gettext("Event Name"), {"event"}, lambda event: event.name),
FunctionalWalletTextPlaceholder(
"event_slug", LazyI18nString.from_gettext("Event Slug"), {"event"}, lambda event: event.slug
),
FunctionalWalletTextPlaceholder("order", LazyI18nString.from_gettext("Order Code"), {"order"}, lambda order: order.code),
FunctionalWalletTextPlaceholder(
"total",
LazyI18nString.from_gettext("Order Total"),
{"event", "order"},
lambda event, order: money_filter(order.total, event.currency),
),
FunctionalWalletTextPlaceholder(
"order_email",LazyI18nString.from_gettext("Order Email"), {"order"}, lambda order: order.email
),
FunctionalWalletTextPlaceholder(
"price",LazyI18nString.from_gettext("Item Price"), {"event", "order_position"}, lambda event, order_position: money_filter(order_position.price, event.currency)
),
FunctionalWalletTextPlaceholder(
"secret",LazyI18nString.from_gettext("Order Secret (QR-Code-Content)"), {"order_position"}, lambda order_position: order_position.secret
),
]
@receiver(
register_wallet_image_placeholders,
dispatch_uid="plugin_wallet_register_wallet_image_placeholders",
)
def base_image_placeholders(sender, **kwargs):
return [
FunctionalWalletImagePlaceholder(
"poweredby",
LazyI18nString.from_gettext("Logo"),
set(),
# TODO: replace with paths not from another plugin
lambda: get_static_file("pretix_passbook/logo.png"),
static("pretix_passbook/logo.png"),
),
FunctionalWalletImagePlaceholder(
"poweredby_icon",
LazyI18nString.from_gettext("Icon"),
set(),
lambda: get_static_file("pretix_passbook/icon.png"),
static("pretix_passbook/icon.png"),
),
FunctionalWalletImagePlaceholder(
"example_no_preview",
LazyI18nString.from_gettext("Image with no preview"),
set(),
lambda: get_static_file("pretix_passbook/icon.png"),
),
# TODO: Image upload
]
+19 -2
View File
@@ -20,7 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
from pretix.base.signals import register_ticket_outputs, register_global_settings
from pretix.base.signals import register_ticket_outputs, register_global_settings, EventPluginSignal
from .ticketoutput import OUTPUTS
def connect_signals():
@@ -29,9 +29,26 @@ def connect_signals():
def get_register_func(o):
def register(sender, **kwargs):
return o
return register
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()
register_wallet_text_placeholders = EventPluginSignal()
"""
This signal is sent out to get all known wallet placeholders. Receivers should return
an list of subclasses of pretix.plugins.wallet.placeholders.BaseWalletTextPlaceholder.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_wallet_image_placeholders = EventPluginSignal()
"""
This signal is sent out to get all known wallet placeholders. Receivers should return
an list of subclasses of pretix.plugins.wallet.placeholders.BaseWalletImagePlaceholder.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -49,8 +49,7 @@ const platformChoices = computed(() => {
});
const preview_layout = computed(() => {
return store.currentPlatformStyles[store.currentPlatformLayout.style]
.preview_layout;
return store.currentPlatformStyles[store.currentPlatformLayout.style]?.preview_layout;
});
</script>
@@ -93,3 +92,20 @@ const preview_layout = computed(() => {
button.btn.btn-primary.btn-save(type="submit") Submit
</template>
<style lang="css">
.walletsettings-panel .panel-heading {
.checkbox {
padding: 0;
display: inline-block;
input[type=checkbox] {
margin-top: 0;
margin-right: 5px;
margin-left: 0px;
position: relative;
top: 1px;
}
}
}
</style>
@@ -1,148 +0,0 @@
<script setup lang="ts">
import { computed, ref, watchEffect } from "vue";
import StyleSettings from "./style-settings.vue";
import Select from "./input/select.vue";
import Input from "./input/input.vue";
const gettext = (window as any).gettext;
const isLoading = ref<boolean>(true);
const wallet_layout = ref<PlatformLayout | null>(null);
const PLATFORMS: Platforms = JSON.parse(
document.querySelector("#platforms")?.textContent ?? "{}",
);
const VARIABLES: VariableConfig = JSON.parse(
document.querySelector("#variables")?.textContent ?? "{}",
);
const LOCALES: Record<string, string> = JSON.parse(
document.querySelector("#locales")?.textContent ?? "{}",
);
const CSRF_TOKEN =
document.querySelector<HTMLInputElement>("input[name=csrfmiddlewaretoken]")
?.value ?? "";
const props = defineProps<{
layoutId: string;
}>();
watchEffect(() => {
// TODO: error handling / proper api client
isLoading.value = true;
fetch(
`/api/v1/organizers/demo/events/wallet/walletlayouts/${props.layoutId}/`,
)
.then((x) => x.json())
.then((x) => {
wallet_layout.value = x;
isLoading.value = false;
});
});
function saveLayout(e: SubmitEvent) {
e.preventDefault();
isLoading.value = true;
// TODO: error handling / proper api client
fetch(
`/api/v1/organizers/demo/events/wallet/walletlayouts/${props.layoutId}/`,
{
method: "PUT",
headers: {
"content-type": "application/json",
"X-CSRFToken": CSRF_TOKEN,
},
body: JSON.stringify(wallet_layout.value),
},
)
.then((x) => x.json())
.catch((x) => alert(x))
.then((x) => {
wallet_layout.value = x;
isLoading.value = false;
});
}
function openForm(url: string, data: Record<string, string>) {
let form = document.createElement("form");
form.target = "_blank";
form.method = "POST";
form.action = url;
form.style.display = "none";
for (var key in data) {
var input = document.createElement("input");
input.type = "hidden";
input.name = key;
input.value = data[key];
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
}
const currentPlatform = ref(PLATFORMS[0].identifier);
const currentLayout = computed(() => ({}));
const platformStyles = computed(() => {
for (const platform of PLATFORMS) {
if (platform.identifier === currentPlatform.value) {
return platform.styles
}
}
});
const platformLayout = computed(() => {
for (const layout of wallet_layout.value.platform_layouts) {
if (layout.platform === currentPlatform.value) {
return layout
}
}
const newLayout = {platform: currentPlatform, style: null, layout: {}};
wallet_layout.value.platform_layouts.push(newLayout);
return newLayout
});
const platformChoices = computed(() => {
return [[null, "Do not generate pass"], ...Object.values(platformStyles.value).map(x => [x.identifier, x.name])]
});
function openPreview(e: SubmitEvent) {
e.preventDefault();
openForm("../../preview/", {"csrfmiddlewaretoken": CSRF_TOKEN, "platform": currentPlatform.value, "style": platformLayout.value.style, "layout": JSON.stringify(platformLayout.value.layout)})
}
</script>
<template lang="pug">
// TODO: add :key for all `v-for`s
// TODO: i18n textfields
// TODO: proper spinner
template(v-if="isLoading") {{ gettext("Loading...") }}
form(v-else @submit="saveLayout")
.form-group
Input(label="Name" v-model="wallet_layout.name")
nav
ul.nav.nav-tabs
li(v-for="platform in PLATFORMS" :class="{'active': currentPlatform === platform.identifier}")
a(role="tab" @click="currentPlatform = platform.identifier") {{ platform.name }}
.tabbed-form.tab-content
.tab-pane.active.row
.col-md-8
Select.form-group(label="Style" v-model="platformLayout.style" :choices="platformChoices")
StyleSettings(v-if="platformLayout.style" v-model="platformLayout.layout" :style="platformStyles[platformLayout.style]" :variables="VARIABLES" :locales="LOCALES")
.col-md-4
.panel.panel-default
.panel-heading Preview
.panel-body
// TODO: Preview
pre
code {{ platformLayout }}
pre(v-if="wallet_layout.style")
code {{ platformStyles[wallet_layout.style] }}
pre
code {{ wallet_layout }}
.form-group.submit-group
button.btn.btn-lg.btn-default(type="button" @click="openPreview") Preview
button.btn.btn-primary.btn-save(type="submit") Submit
</template>
@@ -0,0 +1,20 @@
<script setup lang="ts">
import { useId } from 'vue'
defineOptions({
inheritAttrs: false
})
const props = defineProps<{
label?: string
}>()
const modelValue = defineModel<boolean|null>();
const id = useId()
</script>
<template lang="pug">
.checkbox
label
input(:id="id" v-model="modelValue" v-bind="$attrs" type="checkbox")
| {{ props.label }}
</template>
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { computed, inject, reactive, watchEffect } from "vue";
import Select from "./input/select.vue";
import Input from "./input/input.vue";
import Checkbox from "./input/checkbox.vue";
import I18nInput from "./input/i18ninput.vue";
import TextContent from "./text-content.vue";
import { StoreKey } from "../walletStore";
const store = inject(StoreKey)!
const store = inject(StoreKey)!;
const gettext = (window as any).gettext;
@@ -14,7 +14,9 @@ const props = defineProps<{
fieldgroup: PlaceholderFieldGroupDefinition;
overflows: FieldGroupDefinition[];
}>();
const fieldConfig = defineModel<PlaceholderFieldGroupConfig>({ required: true });
const fieldConfig = defineModel<PlaceholderFieldGroupConfig>({
required: true,
});
const overflowOptions = computed((): Array<[string | null, string]> => {
if (props.overflows.length) {
@@ -32,17 +34,25 @@ function addVariable() {
}
watchEffect(() => {
if (!fieldConfig.value) {
fieldConfig.value = {overflow: null, entries: JSON.parse(JSON.stringify(props.fieldgroup.default_entries))};
}
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),
);
}
});
</script>
<template lang="pug">
.panel.panel-default
.panel.panel-default.walletsettings-panel
.panel-heading
h3.panel-title {{ fieldgroup.name }}
.panel-body(v-if="fieldConfig")
@@ -76,4 +86,4 @@ watchEffect(() => {
i.fa.fa-plus
| {{ gettext("Add field") }}
Select(:label="gettext('Overflow to …')" :choices="overflowOptions" v-model="fieldConfig.overflow")
</template>
</template>
@@ -1,4 +1,8 @@
<script setup lang="ts">
import { watchEffect } from 'vue';
import Input from './input/input.vue';
import Checkbox from './input/checkbox.vue';
const gettext = (window as any).gettext;
const props = defineProps<{
@@ -6,13 +10,20 @@ const props = defineProps<{
}>();
const fieldConfig = defineModel<PredefinedFieldGroupConfig>({ required: true });
watchEffect(() => {
if (!fieldConfig.value) {
fieldConfig.value = {active: props.fieldgroup.required};
}
});
</script>
<template lang="pug">
.panel.panel-default
.panel.panel-default.walletsettings-panel
.panel-heading
h3.panel-title {{ fieldgroup.name }}
.panel-body
h3.panel-title.form-inline
.form-group
Checkbox(:label="fieldgroup.name" v-model="fieldConfig.active")
.panel-body(v-if="!!fieldgroup.description")
.form-group
span.text-muted These fields appear somewhere and are visible too.
span.text-muted {{ fieldgroup.description }}
</template>
@@ -2,20 +2,26 @@
import { computed, inject, reactive, watchEffect } from "vue";
import { StoreKey } from "../../walletStore";
import PlaceholderFieldgroupPreview from "./placeholder-fieldgroup-preview.vue";
import PredefinedFieldgroupPreview from "./predefined-fieldgroup-preview.vue";
const store = inject(StoreKey)!
const store = inject(StoreKey)!;
const props = defineProps<{
config: PreviewFieldgroup;
config: PreviewFieldgroup;
}>();
const style_def = Object.fromEntries(store.currentPlatformStyles[store.currentPlatformLayout.style].fieldgroups.map(x => [x.identifier, x]))[props.config.fieldgroup]
const style_def = computed(() => {
return Object.fromEntries(
store.currentPlatformStyles[
store.currentPlatformLayout.style
].fieldgroups.map((x) => [x.identifier, x]),
)[props.config.fieldgroup];
});
</script>
<template lang="pug">
PlaceholderFieldgroupPreview(v-if="style_def && style_def.type == 'placeholder'" :config="config" :style_def="style_def")
PlaceholderFieldgroupPreview(v-else-if="style_def && style_def.type == 'predefined'" :config="config" :style_def="style_def")
PredefinedFieldgroupPreview(v-else-if="style_def && style_def.type == 'predefined'" :config="config" :style_def="style_def")
//- // TODO: support predefined group
//- div(:style="{'flex-grow': config.relSize}")
//- div(style="background-color: gray; display: flex; flex-direction: row;")
@@ -21,7 +21,6 @@ const props = defineProps<{ layout: Array<PreviewLayout> }>();
width: 100%;
aspect-ratio: 9 / 11;
max-width: 360px;
max-height: 440px;
vertical-align: top;
display: inline-block;
@@ -12,7 +12,6 @@ const props = defineProps<{
</script>
<template lang="pug">
// TODO: support predefined group
div.fieldgroup-container(:style="{'flex-grow': config.relSize, 'flex-direction': config.direction || 'row'}")
div.fieldgroup-item(v-for="{ entry, label, content } of store.currentLayoutFieldContent[config.fieldgroup]")
div.fieldgroup-item-qrcode(v-if="style_def.display == 'code'") {{ label }}
@@ -9,19 +9,18 @@ const props = defineProps<{
config: PreviewFieldgroup;
style_def: PredefinedFieldGroupDefinition;
}>();
const isActive = computed(
() =>
store.currentPlatformLayout.layout.fieldgroups[props.style_def.identifier]?.active,
);
</script>
<template lang="pug">
div {{ config.fieldgroup }}
// TODO: support predefined group
div.fieldgroup-container(:style="{'flex-grow': config.relSize, 'flex-direction': config.direction || 'row'}")
div.fieldgroup-item(v-for="{ entry, label, content } of store.currentLayoutFieldContent[config.fieldgroup]")
div.fieldgroup-item-qrcode(v-if="style_def.display == 'code'") {{ label }}
template(v-else)
div.fieldgroup-label.nowrap(v-if="style_def.display == 'with_label'") {{ label }}
div.nowrap.content(v-if="style_def.content_type == 'text'" :class="config.display") {{ content }}
img.fieldgroup-item-image(v-else-if="style_def.content_type == 'image' && content" :src="i18nstringLocalize(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") {{ label }}
div.nowrap.content(v-if="!!content" :class="config.display") {{ content }}
</template>
@@ -31,7 +30,7 @@ const props = defineProps<{
min-width: 0;
display: flex;
gap: 0.5em;
min-height: 2lh;
min-height: 2lh;
}
.fieldgroup-label {
font-weight: bold;
@@ -40,26 +39,26 @@ const props = defineProps<{
.fieldgroup-item {
flex: 0 1 100%;
min-width: 0;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.fieldgroup-item-image {
max-height: 3lh;
max-height: 3lh;
}
.fieldgroup-item-qrcode {
font-weight: bold;
background-color: lightgray;
width: 50%;
aspect-ratio: 1;
margin: auto;
/* center content */
display: flex;
font-weight: bold;
background-color: lightgray;
width: 50%;
aspect-ratio: 1;
margin: auto;
/* center content */
display: flex;
align-items: center;
text-align: center;
padding: 1em;
overflow-wrap: anywhere;
padding: 1em;
overflow-wrap: anywhere;
}
.nowrap {
@@ -74,6 +73,6 @@ const props = defineProps<{
font-size: 1.5em;
}
.tight {
line-height: 1;
line-height: 1;
}
</style>
@@ -3,6 +3,7 @@ type BaseFieldGroupDefinition = {
identifier: string;
name: string;
required: boolean;
description: string;
};
type FieldGroupDefinition =
@@ -50,7 +51,7 @@ type Style = {
type Variable = {
label: string;
editor_sample: I18nString;
sample: string;
};
type Platform = {
@@ -67,9 +68,12 @@ type Platforms = Platform[];
type PlaceholderFieldGroupConfig = {
entries: Array<FieldEntry>;
overflow: string | null;
active: boolean;
};
type PredefinedFieldGroupConfig = {};
type PredefinedFieldGroupConfig = {
active: boolean;
};
type FieldGroupConfig =
| PlaceholderFieldGroupConfig
@@ -72,7 +72,7 @@ export function createWalletStore(config: {
if (entry.type == "custom") {
value = i18nstringLocalize(entry.content);
} else if (entry.type == "placeholder") {
value = placeholder.editor_sample;
value = placeholder?.sample || `(unknown placeholder: ${entry.content})`;
}
content[fieldgroup.identifier].push({
entry,
@@ -185,9 +185,12 @@ export function createWalletStore(config: {
entries: JSON.parse(
JSON.stringify(newFieldGroups[key].default_entries),
),
active: newFieldGroups[key].required || newFieldGroups[key].default_entries.length > 0
};
} else {
this.currentPlatformLayout.layout.fieldgroups[key] = {};
this.currentPlatformLayout.layout.fieldgroups[key] = {
active: newFieldGroups[key].required
};
}
}
@@ -15,14 +15,6 @@ AVAILABLE_STYLES_DICT = {
plat: {s.identifier: s for s in styls} for plat, styls in AVAILABLE_STYLES.items()
}
# TODO? move to models?
def get_platform(identifier: str):
for platform in AVAILABLE_PLATFORMS:
if platform.identifier == identifier:
return platform
def get_style(platform: str, identifier: str):
return AVAILABLE_STYLES_DICT.get(platform, {}).get(identifier)
+6 -6
View File
@@ -32,7 +32,7 @@ class ApplePlatform(WalletPlatform):
@classmethod
def generate(cls, layout: PassLayout, op: OrderPosition):
from ..views import get_layout_variables
context = cls.get_context(op)
order = op.order
event = order.event
@@ -49,9 +49,7 @@ class ApplePlatform(WalletPlatform):
op.pk,
)
context = {
"placeholders": get_layout_variables(op.order.event),
"evaluation_context": [op, order, order.event],
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(),
@@ -63,8 +61,7 @@ class ApplePlatform(WalletPlatform):
"passTypeIdentifier": order.event.settings.wallet_apple_pass_type_id,
"teamIdentifier": order.event.settings.wallet_apple_team_id,
"serialNumber": serialNumber,
"locales": event.settings.locales,
}
})
data = layout.generate(context)
return filename, "application/vnd.apple.pkpass", data
@@ -234,6 +231,7 @@ class AppleWalletEventTicket(AppleWalletStyle):
content="poweredby",
)
],
required=True
),
ImageFieldGroup(
identifier="logo",
@@ -245,6 +243,7 @@ class AppleWalletEventTicket(AppleWalletStyle):
content="poweredby",
)
],
required=True
),
TextFieldGroup(
identifier="logo_text",
@@ -265,6 +264,7 @@ class AppleWalletEventTicket(AppleWalletStyle):
)
], # TODO: support Lazyi18nproxy here
description=_("These fields appear prominently featured on the pass."),
required=True
),
TextFieldGroup(
identifier="secondary", name=_("Secondary"), max_entries=4
+21 -4
View File
@@ -1,12 +1,31 @@
import enum
from typing import Any
from i18nfield.strings import LazyI18nString
import jsonschema
from django.core.exceptions import ValidationError
from pretix.base.models import OrderPosition
class WalletPlatform:
identifier: str
name: str
@classmethod
def generate(cls, layout: "PassLayout", op: OrderPosition) -> Any: # TODO: Typing
pass
@classmethod
def get_context(cls, op):
from ..placeholders import get_wallet_placeholders, WalletPlaceholderContext
order = op.order
event = order.event
return {
"placeholders": get_wallet_placeholders(event),
"placeholder_context": WalletPlaceholderContext(event=event, order=order, order_position=op),
"locale": event.settings.locale, # TODO: should probably be order locale
"locales": event.settings.locales,
}
class FieldGroupType(enum.Enum):
PLACEHOLDER = "placeholder"
@@ -287,11 +306,9 @@ class PassStyle:
.get(content)
)
if placeholder:
placeholder_value = placeholder["evaluate"](
*context.get("evaluation_context", [])
)
placeholder_value = context['placeholder_context'].render_placeholder(placeholder)
if placeholder_value:
return placeholder["label"], placeholder_value
return placeholder.label, placeholder_value
return None, None
+34 -63
View File
@@ -10,7 +10,7 @@ from .base import (
TextFieldGroup,
WalletPlatform,
)
from django.utils.translation import gettext_lazy as _, gettext
from django.utils.translation import gettext as _
import json
from walletobjects import ButtonJWT, EventTicketClass, EventTicketObject
@@ -72,7 +72,7 @@ def get_translated_dict(string, locales):
for locale in locales:
translation.activate(locale)
translated[locale] = gettext(string)
translated[locale] = _(string)
translation.deactivate()
return translated
@@ -80,7 +80,7 @@ def get_translated_dict(string, locales):
def get_translated_string(string, locale):
translation.activate(locale)
translated = gettext(string)
translated = _(string)
translation.deactivate()
return translated
@@ -92,26 +92,24 @@ class GooglePlatform(WalletPlatform):
@classmethod
def generate(cls, layout: PassLayout, op: OrderPosition):
from ..views import get_layout_variables
context = cls.get_context(op)
order = op.order
event = order.event
context = {
"placeholders": get_layout_variables(event), # TODO: move to higher class
"evaluation_context": [op, order, event],
"credentials": event.settings.get("wallet_google_credentials").read(),
"locale": event.settings.locale,
"locales": event.settings.locales,
"issuerName": event.organizer.name,
"eventName": event.name,
# TODO: use other classId and objectId in preview mode
"classId": get_class_id(event),
"objectId": get_object_id(op),
"homepageUrl": eventreverse_absolute(event, "presale:event.index"),
# TODO: add webhook view & register in pass
"webhookUrl": "", # eventreverse_absolute(event.organizer,"plugins:wallet:google_webhook",))
}
context.update(
{
"credentials": event.settings.get("wallet_google_credentials").read(),
"issuerName": event.organizer.name,
"eventName": event.name,
# TODO: use other classId and objectId in preview mode
"classId": get_class_id(event),
"objectId": get_object_id(op),
"homepageUrl": eventreverse_absolute(event, "presale:event.index"),
# TODO: add webhook view & register in pass
# "webhookUrl": eventreverse_absolute(event.organizer,"plugins:wallet:google_webhook",)
}
)
data = layout.generate(context)
return "url", "text/plain", data
@@ -135,8 +133,9 @@ class GoogleWalletStyle(PassStyle):
get_translated_string("Website", context["locale"]),
get_translated_dict("Website", context["locales"]),
)
# TODO: add webhook view & register in pass
# output_class.callback_url(context['webhookUrl'])
if context.get("webhookUrl"): # TODO: enforce that it exists
output_class.callback_url(context["webhookUrl"])
# TODO: move to pass settings or set defaults
# if (event.settings.get('ticketoutput_googlepaypasses_latitude')
@@ -249,55 +248,21 @@ class GoogleWalletStyle(PassStyle):
class_object = self._generate_class(layout, context, fields)
ticket_object = self._generate_object(layout, context, fields)
# 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)
generated_jwt = comms.sign_jwt(
ButtonJWT(
origins=[settings.SITE_URL],
issuer=comms.client_email,
event_ticket_classes=[class_object],
event_ticket_objects=[ticket_object],
skinny=False,
skinny=True,
)
)
return "https://pay.google.com/gp/v/save/%s" % generated_jwt
# from ..views import get_layout_variables
# 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 = {
# "placeholders": get_layout_variables(op.order.event),
# "evaluation_context": [op, order, order.event],
# "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,
# "locales": event.settings.locales,
# }
# data = layout.generate(context)
# return filename, "application/vnd.apple.pkpass", data
class GoogleWalletEventTicket(GoogleWalletStyle):
identifier = "event"
@@ -352,17 +317,23 @@ class GoogleWalletEventTicket(GoogleWalletStyle):
{"value": "12:34", "label": "Time"},
]
},
{"fieldgroup": "seating"},
{"fieldgroup": "seating",
"sample": [
{"content": "5", "label": "Row"},
{"content": "2", "label": "Seat"},
]
},
{"fieldgroup": "code"},
]
]
def _generate_object(self, layout: PassLayout, context, fields):
output_object = super()._generate_object(layout, context, fields)
if fields["code"]:
output_object.barcode(Barcode.qrCode, fields["code"][0]["value"], fields["code"][0]["value"])
output_object.barcode(
Barcode.qrCode, fields["code"][0]["value"], fields["code"][0]["value"]
)
# output_object.reservation_info("%s-%s" % (op.order.event.slug, op.order.code))
# output_object.ticket_holder_name(op.attendee_name or (op.addon_to.attendee_name if op.addon_to else ''))
+1 -4
View File
@@ -26,13 +26,10 @@ from pretix.base.models import Event
from pretix.base.settings import SettingsSandbox
from django.template.loader import render_to_string
from django.shortcuts import get_object_or_404
from .styles import AVAILABLE_STYLES_DICT
from .styles.base import PassLayout, WalletPlatform
from .styles.base import WalletPlatform
from .styles.apple import ApplePlatform
from .styles.google import GooglePlatform
from collections import OrderedDict
from .models import WalletLayout
from .views import get_layout_variables
from django import forms
from .forms import CertificateFileField, validate_rsa_privkey
from pretix.control.forms import ClearableBasenameFileInput
+17 -34
View File
@@ -29,38 +29,24 @@ from django.contrib import messages
from django.contrib.staticfiles import finders
from django.utils.functional import cached_property
from django.templatetags.static import static
def get_layout_variables(event):
return {
"text": get_variables(event),
"image": get_images(event)
| {
"poweredby": {
"label": _("pretix-Logo"),
"evaluate": lambda *_: open(
finders.find("pretix_passbook/logo.png"), "rb"
),
"editor_sample": static("pretix_passbook/logo.png")
},
"poweredby_icon": {
"label": _("pretix-Icon"),
"evaluate": lambda *_: open(
finders.find("pretix_passbook/icon.png"), "rb"
),
"editor_sample": static("pretix_passbook/icon.png")
},
}, # TODO: image upload
}
from .placeholders import get_wallet_placeholders, WalletPlaceholderContext
def get_editor_variables(event):
return {
t: {
vid: {"label": v.get("label"), "editor_sample": v.get("editor_sample")}
for vid, v in vs.items()
def get_editor_placeholders(event):
with (
rolledback_transaction(),
language(event.settings.locale, event.settings.region),
):
p = get_preview_position(event)
context = WalletPlaceholderContext(event=event, order=p.order, order_position=p)
placeholders = {
t: {
pid: {"label": str(p.label), "sample": str(context.render_sample(p))}
for pid, p in ps.items()
}
for t, ps in get_wallet_placeholders(event).items()
}
for t, vs in get_layout_variables(event).items()
}
return placeholders
class WalletLayoutMixin:
@@ -97,10 +83,7 @@ class LayoutEditorView(LayoutDetailView):
}
for platform in AVAILABLE_PLATFORMS
]
# context["styles"] = {
# style.identifier: style.asdict() for style in self.get_platform_styles()
# }
context["variables"] = get_editor_variables(self.request.event)
context["variables"] = get_editor_placeholders(self.request.event)
context["locales"] = {
l: dict(settings.LANGUAGES).get(l, l)
for l in self.request.event.settings.get("locales")
@@ -190,7 +173,7 @@ class LayoutPreviewView(EventPermissionRequiredMixin, View):
):
p = get_preview_position(request.event)
layout = PassLayout(style=style, layout=layout)
context = {"placeholders": get_layout_variables(event)}
context = {"placeholders": get_wallet_placeholders(event)}
layout.validate(context=context)
fname, mimet, data = platform.generate(layout, p)