diff --git a/src/pretix/api/helpers.py b/src/pretix/api/helpers.py
new file mode 100644
index 0000000000..7150d29fe7
--- /dev/null
+++ b/src/pretix/api/helpers.py
@@ -0,0 +1,25 @@
+from pretix.base.models import (
+ CachedFile
+)
+from rest_framework.exceptions import ValidationError
+from django.core.exceptions import ValidationError as DjangoValidationError
+from django.conf import settings
+
+def handle_file_upload(data, user, auth, allowed_types):
+ try:
+ cf = CachedFile.objects.get(
+ session_key=f'api-upload-{str(type(user or auth))}-{(user or auth).pk}',
+ file__isnull=False,
+ pk=data[len("file:"):],
+ )
+ except (ValidationError, DjangoValidationError, IndexError): # invalid uuid
+ raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
+ except CachedFile.DoesNotExist:
+ raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
+
+ if cf.type not in allowed_types:
+ raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
+ if cf.file.size > settings.FILE_UPLOAD_MAX_SIZE_OTHER:
+ raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
+
+ return cf.file
\ No newline at end of file
diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py
index 87477dd82c..55fffa1fcb 100644
--- a/src/pretix/api/views/checkin.py
+++ b/src/pretix/api/views/checkin.py
@@ -63,7 +63,7 @@ from pretix.api.views import RichOrderingFilter
from pretix.api.views.order import OrderPositionFilter
from pretix.base.i18n import language
from pretix.base.models import (
- CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
+ Checkin, CheckinList, Device, Event, Order, OrderPosition,
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
)
from pretix.base.models.orders import PrintLog
@@ -75,6 +75,7 @@ from pretix.base.services.checkin import (
from pretix.base.services.media import perform_media_exchange
from pretix.base.signals import checkin_annulled
from pretix.helpers import OF_SELF
+from pretix.api.helpers import handle_file_upload
with scopes_disabled():
class CheckinListFilter(FilterSet):
@@ -328,27 +329,6 @@ with scopes_disabled():
)
-def _handle_file_upload(data, user, auth):
- try:
- cf = CachedFile.objects.get(
- session_key=f'api-upload-{str(type(user or auth))}-{(user or auth).pk}',
- file__isnull=False,
- pk=data[len("file:"):],
- )
- except (ValidationError, BaseValidationError, IndexError): # invalid uuid
- raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
- except CachedFile.DoesNotExist:
- raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
-
- allowed_types = (
- 'image/png', 'image/jpeg', 'image/gif', 'application/pdf'
- )
- if cf.type not in allowed_types:
- raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
- if cf.file.size > settings.FILE_UPLOAD_MAX_SIZE_OTHER:
- raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
-
- return cf.file
def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_products=False, pdf_data=False, expand=None):
@@ -795,7 +775,12 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
try:
if q.type == Question.TYPE_FILE:
if answers_data[str(q.pk)]:
- given_answers[q] = _handle_file_upload(answers_data[str(q.pk)], user, auth)
+ given_answers[q] = handle_file_upload(
+ answers_data[str(q.pk)],
+ user,
+ auth,
+ allowed_types=('image/png', 'image/jpeg', 'image/gif', 'application/pdf')
+ )
else:
given_answers[q] = None
else:
diff --git a/src/pretix/plugins/wallet/api.py b/src/pretix/plugins/wallet/api.py
index 6bd9681dc1..354976a605 100644
--- a/src/pretix/plugins/wallet/api.py
+++ b/src/pretix/plugins/wallet/api.py
@@ -1,16 +1,17 @@
-from rest_framework import viewsets
+from rest_framework import viewsets, serializers
from django.db import transaction
from .styles import AVAILABLE_STYLES_DICT, AVAILABLE_PLATFORMS
from .models import WalletLayout, WalletPlatformLayout
from pretix.api.serializers.i18n import I18nAwareModelSerializer
+from pretix.api.serializers.fields import UploadedFileField
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
-from .views import get_editor_placeholders
from rest_framework import serializers
-
class WalletPlatformLayoutSerializer(I18nAwareModelSerializer):
- platform = serializers.ChoiceField(choices=[p.identifier for p in AVAILABLE_PLATFORMS])
+ platform = serializers.ChoiceField(
+ choices=[p.identifier for p in AVAILABLE_PLATFORMS]
+ )
style = serializers.CharField(allow_null=True, required=False)
class Meta:
@@ -23,9 +24,9 @@ class WalletPlatformLayoutSerializer(I18nAwareModelSerializer):
return value
def validate(self, data):
- platform = data.get('platform')
- style = data.get('style')
- layout = data.get('layout')
+ platform = data.get("platform")
+ style = data.get("style")
+ layout = data.get("layout")
if platform and style and layout:
platform_styles = AVAILABLE_STYLES_DICT[platform]
@@ -33,10 +34,24 @@ class WalletPlatformLayoutSerializer(I18nAwareModelSerializer):
raise ValidationError(_("Invalid style"))
style = platform_styles[data["style"]]
- style = style(event=self.context['event'], layout=data["layout"])
+ style = style(event=self.context["event"], layout=data["layout"])
style.validate()
+ data['file_settings'] = style.extract_file_settings(self.context['request'])
+
return data
+ def to_representation(self, instance):
+ ret = super().to_representation(instance)
+ ret['file_settings'] = {}
+ for file_setting in instance.file_settings.all():
+ try:
+ url = file_setting.file.url
+ except AttributeError:
+ continue
+ request = self.context['request']
+ ret['file_settings'][file_setting.key] = request.build_absolute_uri(url)
+ return ret
+
class WalletLayoutSerializer(I18nAwareModelSerializer):
platform_layouts = WalletPlatformLayoutSerializer(many=True)
@@ -53,11 +68,28 @@ class WalletLayoutSerializer(I18nAwareModelSerializer):
super().save(*args, **kwargs, event=self.context["event"])
def update(self, instance, validated_data):
- platform_layouts = validated_data.pop('platform_layouts')
+ platform_layouts = validated_data.pop("platform_layouts")
for layout in platform_layouts:
- if layout['style']:
- instance.platform_layouts.update_or_create(platform=layout['platform'], defaults=layout)
- instance.platform_layouts.exclude(platform__in={layout['platform'] for layout in platform_layouts if layout['style'] is not None}).delete()
+ if layout["style"]:
+ # TODO: better handling here
+ file_settings = layout.pop("file_settings", {})
+ obj, _ = instance.platform_layouts.update_or_create(
+ platform=layout["platform"], defaults=layout
+ )
+ for key, file in file_settings.items():
+ if not file:
+ obj.file_settings.filter(key=key).delete()
+
+ elif file != "keep":
+ obj.file_settings.update_or_create(key=key, defaults={"file": file})
+
+ instance.platform_layouts.exclude(
+ platform__in={
+ layout["platform"]
+ for layout in platform_layouts
+ if layout["style"] is not None
+ }
+ ).delete()
return super().update(instance, validated_data)
diff --git a/src/pretix/plugins/wallet/migrations/0003_walletlayoutfilesetting.py b/src/pretix/plugins/wallet/migrations/0003_walletlayoutfilesetting.py
new file mode 100644
index 0000000000..673b7ae832
--- /dev/null
+++ b/src/pretix/plugins/wallet/migrations/0003_walletlayoutfilesetting.py
@@ -0,0 +1,35 @@
+# Generated by Django 5.2.13 on 2026-08-11 15:39
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("wallet", "0002_alter_walletlayoutitem_unique_together_and_more"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="WalletLayoutFileSetting",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True, primary_key=True, serialize=False
+ ),
+ ),
+ ("key", models.CharField()),
+ ("file", models.FileField(upload_to="")),
+ (
+ "layout",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="file_settings",
+ to="wallet.walletplatformlayout",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/src/pretix/plugins/wallet/models.py b/src/pretix/plugins/wallet/models.py
index 18f3c08b7b..3d174f1af9 100644
--- a/src/pretix/plugins/wallet/models.py
+++ b/src/pretix/plugins/wallet/models.py
@@ -70,7 +70,9 @@ class WalletPlatformLayout(LoggedModel):
style = get_style(self.platform, self.style)
if style:
- return style(event=self.parent.event, layout=self.layout)
+ file_settings = dict(self.file_settings.values_list("key", "file"))
+ print(file_settings)
+ return style(event=self.parent.event, layout=self.layout, file_settings=file_settings)
else:
raise RuntimeError(f"Style {self.platform}.{self.style} not found")
@@ -83,6 +85,11 @@ class WalletLayoutItem(models.Model):
if self.item.event != self.layout.event:
raise ValidationError("cannot bind layout to item of different event")
+class WalletLayoutFileSetting(models.Model):
+ layout = models.ForeignKey(WalletPlatformLayout, on_delete=models.CASCADE, related_name="file_settings")
+ key = models.CharField()
+ file = models.FileField()
+
# smth like this for apple, lets see what the best architecture for google will be
# class AppleWalletPass(models.Model):
# platform_layout = models.ForeignKey(WalletPlatformLayout, on_delete=models.PROTECT)
diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue
index 5f89729b79..bf31b41eb1 100644
--- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue
@@ -70,7 +70,6 @@ const preview_layout = computed(() => {
.tab-pane.active.row
.col-md-6.col-lg-8
Select.form-group(label="Style" :modelValue="store.currentPlatformLayout.style" @update:modelValue="store.setCurrentPlatformStyle" :choices="platformChoices")
-
StyleSettings(v-if="store.currentPlatformLayout.style" v-model="store.currentPlatformLayout.layout" :style="store.currentPlatformStyles[store.currentPlatformLayout.style]")
.col-md-6.col-lg-4
.panel.panel-default
diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/file-input.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/file-input.vue
new file mode 100644
index 0000000000..2b96d6f596
--- /dev/null
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/file-input.vue
@@ -0,0 +1,53 @@
+
+
+
+.form-group.row
+ label.control-label.col-md-3(:for="id", v-if="!!label") {{ label }}
+ br(v-if="!$attrs.required")
+ span.optional(v-if="!$attrs.required") {{ gettext("Optional") }}
+ div.col-md-9
+ template(v-if="typeof modelValue == 'string'")
+ | {{ gettext("Currently") + ': ' }}
+ //- TODO: preview filename
+ a(:href="modelValue") FILENAME
+ | {{ " " }}
+ button.btn.btn-sm(@click.prevent="() => {console.log('clear'); modelValue = null}") {{ gettext("Clear") }}
+ //- br
+ //- a(:href="modelValue" data-lightbox="input")
+ //- img.thumb-img(:src="modelValue")
+ br
+ | {{ gettext("Change") + ': ' }}
+ input(:id="id" @change="onChange" v-bind="$attrs" type="file" style="display: inline")
+ .help-block(v-if="!!help_text") {{ help_text }}
+ .help-block(v-if="!!errors" v-for="error in errors") {{ error }}
+
+
+
diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/input.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/input.vue
index 9a286d20dd..9c2f243092 100644
--- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/input.vue
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/input.vue
@@ -1,20 +1,28 @@
- label.control-label(:for="id", v-if="props.label") {{ props.label }}
- input.form-control(:id="id" v-model="modelValue" v-bind="$attrs")
- .help-block(v-if="props.errors" v-for="error in props.errors") {{ error }}
+ label.control-label(:for="id", v-if="label") {{ label }}
+ br(v-if="!$attrs.required")
+ span.optional(v-if="!$attrs.required") {{ gettext("Optional") }}
+ div
+ input(:id="id" v-model="modelValue" v-bind="$attrs" :type="type" :class="{'form-control': type == 'text'}")
+ .help-block(v-if="!!help_text") {{ help_text }}
+ .help-block(v-if="!!errors" v-for="error in errors") {{ error }}
diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-item-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-item-preview.vue
index 68c8531ec0..30378ecbb9 100644
--- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-item-preview.vue
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-item-preview.vue
@@ -6,7 +6,7 @@ import { i18nstringLocalize } from "../../helpers";
const store = inject(StoreKey)!;
const props = defineProps<{
- label: I18nString;
+ label?: I18nString;
content: I18nString;
content_type: FieldContentType;
display: FieldGroupDisplay;
diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue
index 440073c4a8..95f79b5585 100644
--- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue
@@ -17,9 +17,16 @@ const style_def = computed(() => {
].fieldgroups.map((x) => [x.identifier, x]),
)[props.config.fieldgroup];
});
+
+function focusGroup() {
+ // TODO: highlight group? or change ui concept completely
+ const elem = document.getElementById('fieldgroup-' + props.config.fieldgroup)
+ elem && elem.scrollIntoView()
+}
+div(@click="focusGroup")
PlaceholderFieldgroupPreview(v-if="style_def && style_def.type == 'placeholder'" :config="config" :style_def="style_def")
PredefinedFieldgroupPreview(v-else-if="style_def && style_def.type == 'predefined'" :config="config" :style_def="style_def")
diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fixed-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fixed-preview.vue
deleted file mode 100644
index ae05605464..0000000000
--- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fixed-preview.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
- div.fieldgroup-item
- div.fieldgroup-label.nowrap(v-if="!!config.label") {{ config.label }}
- div.nowrap.content(:class="config.display") {{ config.value }}
-
-
-
-
\ No newline at end of file
diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/pass-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/pass-preview.vue
index 9eab779223..8d4d831f28 100644
--- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/pass-preview.vue
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/pass-preview.vue
@@ -59,6 +59,7 @@ const props = defineProps<{ layout: Array }>();
}
.fieldgroup-item-image {
max-height: 3lh;
+ object-fit: contain;
}
.fieldgroup-item-qrcode {
font-weight: bold;
diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue
index 49b7b73a75..23fb0a5794 100644
--- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue
@@ -1,7 +1,7 @@
+
+
+ //- pre
+ //- code {{ settingDef }}
+ //- pre
+ //- code {{ config }}
+ //- pre
+ //- code {{ settingValue }}
+ //- PlaceholderFieldgroupPreview(v-if="style_def && style_def.type == 'placeholder'" :config="config" :style_def="style_def")
+ //- PredefinedFieldgroupPreview(v-else-if="style_def && style_def.type == 'predefined'" :config="config" :style_def="style_def")
+ div.fieldgroup-container(:style="{'flex-grow': config.relSize, 'flex-direction': config.direction || 'row'}")
+ FieldgroupItemPreview(:content="settingValue" :content_type="settingDef.type" :display_class="config.display" display="plain")
+
+
diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/settings-field.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/settings-field.vue
new file mode 100644
index 0000000000..ad2ab57480
--- /dev/null
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/settings-field.vue
@@ -0,0 +1,36 @@
+
+
+
+ div.form-group
+ Input(v-if='field.type == "text"' :label="field.label" :type="field.type" :required="field.required" @update:modelValue="(v) => store.setSetting(field.identifier, v)" :modelValue="store.currentLayoutSettings[field.identifier]" :help_text="field.help_text")
+ FileInput(v-if='field.type == "image"' :label="field.label" :required="field.required" :help_text="field.help_text" @update:modelValue="(v) => store.setSetting(field.identifier, v)" :modelValue="store.currentLayoutSettings[field.identifier]")
+
diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/style-settings.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/style-settings.vue
index eb274796b3..ce22b5d2bd 100644
--- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/style-settings.vue
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/style-settings.vue
@@ -2,7 +2,7 @@
import { computed, inject, watchEffect } from "vue";
import PlaceholderFieldSettings from "./placeholder-field-settings.vue";
import PredefinedFieldSettings from "./predefined-field-settings.vue";
-
+import SettingsField from "./settings-field.vue";
const gettext = (window as any).gettext;
const props = defineProps<{
@@ -13,9 +13,11 @@ const layout = defineModel();
+ h2.h3 {{ gettext("Settings") }}
+ SettingsField(v-for="field of style.settings" :field="field" :key="field.identifier")
h2.h3 {{ gettext("Field Groups") }}
- template(v-if="props.style && layout.fieldgroups"
- v-for="(fieldgroup, fieldgroupId) in props.style.fieldgroups")
+ div(v-if="props.style && layout.fieldgroups"
+ v-for="(fieldgroup, fieldgroupId) in props.style.fieldgroups" :id="'fieldgroup-' + fieldgroup.identifier")
PlaceholderFieldSettings(
v-if="fieldgroup.type == 'placeholder'"
v-model="layout.fieldgroups[fieldgroup.identifier]"
diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts b/src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts
index 144744f900..07641d3781 100644
--- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts
@@ -44,10 +44,17 @@ type CustomFieldEntry = {
type FieldEntry = PlaceholderFieldEntry | CustomFieldEntry;
+type Setting = {
+ identifier: string;
+ label: I18nString;
+ type: "text" | "image";
+ required: boolean;
+};
type Style = {
identifier: string;
name: string;
fieldgroups: FieldGroupDefinition[];
+ settings: Setting[]
};
type Variable = {
@@ -82,7 +89,8 @@ type FieldGroupConfig =
| PredefinedFieldGroupConfig;
type LayoutData = {
- fieldgroups: Record;
+ fieldgroups?: Record;
+ settings?: Record
};
type PlatformLayout = {
@@ -107,18 +115,20 @@ type PreviewLayout = Array;
type PreviewRow =
| { children: Array; direction?: "row" | "column"; display?: Array;}
| PreviewFieldgroup
- | FixedPreview;
+ | FixedPreview
+ | SettingPreview;
type PreviewProps = {
relSize?: number;
direction?: "row" | "column";
display?: Array;
}
+type SettingPreview = {
+ setting: string;
+} & PreviewProps
type PreviewFieldgroup = PredefinedFieldgroupPreview | PlaceholderFieldGroupPreview;
-type FixedPreview = {
- value: I18nString;
-} & PreviewProps
+type FixedPreview = {value: I18nString; label?: I18nString; } & PreviewProps
type PlaceholderFieldGroupPreview = {
fieldgroup: string;
diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/walletStore.ts b/src/pretix/plugins/wallet/static/pretixplugins/wallet/walletStore.ts
index d4a5693e12..da5741d2df 100644
--- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/walletStore.ts
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/walletStore.ts
@@ -1,6 +1,6 @@
import { i18nstringLocalize } from "./helpers.js";
import { createStore } from "./lib/store.ts";
-import { nextTick, type InjectionKey } from "vue";
+import { toRaw, type InjectionKey } from "vue";
export type WidgetStore = ReturnType;
export const StoreKey: InjectionKey = Symbol("WidgetStore");
@@ -11,7 +11,6 @@ export function createWalletStore(config: {
locales: Record;
csrfToken: string;
layoutId: string;
- // platform_layouts: Record;
}) {
return createStore({
state: () => ({
@@ -19,6 +18,7 @@ export function createWalletStore(config: {
walletLayout: null as WalletLayout | null,
currentPlatform: config.platforms[0].identifier,
loaded: false,
+ files: {} as Record
}),
getters: {
currentPlatformStyles() {
@@ -35,13 +35,19 @@ export function createWalletStore(config: {
}
for (const layout of this.walletLayout.platform_layouts) {
if (layout.platform === this.currentPlatform) {
+ if (!("fieldgroups" in layout.layout)) {
+ layout.layout.fieldgroups = {};
+ }
+ if (!("settings" in layout.layout)) {
+ layout.layout.settings = {};
+ }
return layout;
}
}
const newLayout = {
platform: this.currentPlatform,
style: null,
- layout: { fieldgroups: {} },
+ layout: { fieldgroups: {}, settings: {} },
};
this.walletLayout.platform_layouts.push(newLayout);
return newLayout;
@@ -55,13 +61,14 @@ export function createWalletStore(config: {
for (const fieldgroup of group_defs) {
if (fieldgroup.type == "placeholder") {
content[fieldgroup.identifier] = [];
- const layout_group: PlaceholderFieldGroupConfig =
- this.currentPlatformLayout.layout.fieldgroups[
- fieldgroup.identifier
- ];
+ const layout_group = this.currentPlatformLayout.layout.fieldgroups[
+ fieldgroup.identifier
+ ] as any as PlaceholderFieldGroupConfig;
for (const entry of layout_group.entries) {
const placeholder =
- entry.type === "placeholder" ? this.variables[fieldgroup.content_type][entry.content] : null;
+ entry.type === "placeholder"
+ ? this.variables[fieldgroup.content_type][entry.content]
+ : null;
let label = i18nstringLocalize(entry.label);
if (placeholder && !label) {
@@ -72,7 +79,9 @@ export function createWalletStore(config: {
if (entry.type == "custom") {
value = i18nstringLocalize(entry.content);
} else if (entry.type == "placeholder") {
- value = placeholder?.sample || `(unknown placeholder: ${entry.content})`;
+ value =
+ placeholder?.sample ||
+ `(unknown placeholder: ${entry.content})`;
}
content[fieldgroup.identifier].push({
entry,
@@ -107,6 +116,9 @@ export function createWalletStore(config: {
return content;
},
+ currentLayoutSettings() {
+ return {...this.currentPlatformLayout.file_settings, ...this.currentPlatformLayout.layout.settings}
+ },
},
actions: {
load() {
@@ -120,7 +132,40 @@ export function createWalletStore(config: {
this.loaded = true;
});
},
- saveLayout() {
+ async uploadFile(file: File) {
+ return await fetch("/api/v1/upload", {
+ method: "POST",
+ body: file,
+ headers: {
+ "content-disposition": `attachment; filename="${encodeURI(file.name)}"`,
+ "content-type": file.type || "application/octet-stream",
+ "X-CSRFToken": this.csrfToken,
+ },
+ })
+ .then((x) => x.json())
+ .then((x) => x.id);
+ },
+ async saveLayout() {
+ const layoutToSave = structuredClone(toRaw(this.walletLayout))
+ // TODO: error handling, parallelization
+ for (const platformLayout of layoutToSave.platform_layouts) {
+ const platformStyle = this.platforms.filter(
+ (x) => x.identifier == platformLayout.platform,
+ )[0].styles[platformLayout.style];
+ for (const setting of platformStyle.settings) {
+ console.log(setting, platformLayout.layout?.settings[setting.identifier])
+ if (
+ setting.type === "image" &&
+ platformLayout.layout?.settings[setting.identifier] instanceof
+ File
+ ) {
+ platformLayout.layout.settings[setting.identifier] = await
+ this.uploadFile(
+ platformLayout.layout.settings[setting.identifier],
+ );
+ }
+ }
+ }
// TODO: error handling / proper api client
fetch(
`/api/v1/organizers/demo/events/wallet/walletlayouts/${this.layoutId}/`,
@@ -130,7 +175,7 @@ export function createWalletStore(config: {
"content-type": "application/json",
"X-CSRFToken": this.csrfToken,
},
- body: JSON.stringify(this.walletLayout),
+ body: JSON.stringify(layoutToSave),
},
)
.then((x) => x.json())
@@ -185,29 +230,22 @@ export function createWalletStore(config: {
entries: JSON.parse(
JSON.stringify(newFieldGroups[key].default_entries),
),
- active: newFieldGroups[key].required || newFieldGroups[key].default_entries.length > 0
+ active:
+ newFieldGroups[key].required ||
+ newFieldGroups[key].default_entries.length > 0,
};
} else {
this.currentPlatformLayout.layout.fieldgroups[key] = {
- active: newFieldGroups[key].required
+ active: newFieldGroups[key].required,
};
}
}
this.currentPlatformLayout.style = style;
}
- // else if (this.currentPlatformLayout.style === null && Object.keys(this.currentPlatformStyles).includes(style)) {
- // this.currentPlatformLayout.style = style;
- // this.currentPlatformLayout.layout.fieldgroups = {}
- // } else if (this.currentPlatformLayout.style !== null && Object.keys(this.currentPlatformStyles).includes(style)) {
- // const oldStyle = this.currentPlatformStyles[this.currentPlatformLayout.style];
- // const newStyle = this.currentPlatformStyles[style];
- // this.currentPlatformLayout.style = style;
- // console.log(oldStyle, newStyle)
- // }
- // this.currentPlatformLayout.style = style;
- // }
- // if (style == null) { }
+ },
+ setSetting(identifier: string, value: string | File | null) {
+ this.currentPlatformLayout.layout.settings[identifier] = value;
},
},
});
diff --git a/src/pretix/plugins/wallet/styles/apple.py b/src/pretix/plugins/wallet/styles/apple.py
index 9da365e065..1650e7c98b 100644
--- a/src/pretix/plugins/wallet/styles/apple.py
+++ b/src/pretix/plugins/wallet/styles/apple.py
@@ -1,3 +1,4 @@
+from collections import OrderedDict
from typing import Any
from .base import (
@@ -10,8 +11,9 @@ from .base import (
WalletPlatform,
PassStyle,
PlaceholderFieldEntry,
+ SettingsField,
)
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext as _, gettext_lazy, override
from i18nfield.strings import LazyI18nString
import io
import hashlib
@@ -23,6 +25,7 @@ import json
from django.contrib.staticfiles import finders
from pretix.base.models import OrderPosition
from django.utils.encoding import force_bytes
+from django import forms
class ApplePlatform(WalletPlatform):
@@ -38,6 +41,13 @@ class FormattedLazyI18nString:
def localize(self, language):
return self.base_str.localize(language).format(**self.format_args)
+def lazyi18nstring_from_gettext(text: str, locales: set[str]) -> LazyI18nString:
+ data = {}
+ for locale in locales:
+ with override(locale):
+ data[locale] = _(text)
+ return LazyI18nString(data)
+
class StringResource:
entries: dict[str, LazyI18nString | FormattedLazyI18nString]
@@ -131,6 +141,25 @@ class SignedZipFile:
class AppleWalletStyle(PassStyle):
+ @property
+ def settings(self):
+ return [
+ SettingsField(
+ identifier="logo",
+ label=_("Logo"),
+ type="image",
+ required=False,
+ help_text="Will be displayed on the top left corner of the pass"
+ ),
+ SettingsField(
+ identifier="icon",
+ label=_("Icon"),
+ type="image",
+ required=False,
+ help_text="Will be displayed as the file icon"
+ ),
+ ]
+
def pass_content(self, fields, strings):
raise NotImplementedError()
@@ -180,6 +209,7 @@ class AppleWalletStyle(PassStyle):
pass_json = self.generate_pass_json(fields, op, strings)
print(pass_json)
+ breakpoint()
if fields["logo"]:
logo = fields["logo"][0]["value"]
else:
@@ -204,32 +234,6 @@ class AppleWalletEventTicket(AppleWalletStyle):
identifier = "event_1"
name = _("Event Ticket Layout 1")
fieldgroups = [
- ImageFieldGroup(
- identifier="icon",
- name=_("Icon"),
- min_entries=0,
- max_entries=1,
- default_entries=[
- PlaceholderFieldEntry(
- content="poweredby",
- )
- ],
- required=True,
- context_args={"event", "order", "order_position"},
- ),
- ImageFieldGroup(
- identifier="logo",
- name=_("Logo"),
- min_entries=0,
- max_entries=1,
- default_entries=[
- PlaceholderFieldEntry(
- content="poweredby",
- )
- ],
- required=True,
- context_args={"event", "order", "order_position"},
- ),
TextFieldGroup(
identifier="logo_text",
name=_("Logo text"),
@@ -248,7 +252,7 @@ class AppleWalletEventTicket(AppleWalletStyle):
label=LazyI18nString({"de": "Tickettyp", "en": "Ticket type"}),
content="item",
)
- ], # TODO: support Lazyi18nproxy here
+ ], # TODO: support Lazyi18nproxy here by using lazyi18nstring_from_gettext
description=_("These fields appear prominently featured on the pass."),
required=True,
context_args={"event", "order", "order_position"},
@@ -293,7 +297,7 @@ class AppleWalletEventTicket(AppleWalletStyle):
[
{
"children": [
- {"fieldgroup": "logo", "relSize": 1},
+ {"setting": "logo"},
{
"fieldgroup": "logo_text",
"relSize": 3,
diff --git a/src/pretix/plugins/wallet/styles/base.py b/src/pretix/plugins/wallet/styles/base.py
index 8eb2f67ae3..2331bcbe91 100644
--- a/src/pretix/plugins/wallet/styles/base.py
+++ b/src/pretix/plugins/wallet/styles/base.py
@@ -1,11 +1,13 @@
import enum
-from typing import TypedDict
+from typing import Literal, OrderedDict, TypedDict
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
-
+from django import forms
+from pretix.api.helpers import handle_file_upload
+from django.core.files import File
class WalletPlatform:
identifier: str
@@ -253,6 +255,37 @@ class ImageFieldGroup(PlaceholderFieldGroup):
super().__init__(content_type=self.content_type, display=self.display, **kwargs)
+class SettingsField:
+ identifier: str
+ label: str
+ type: Literal["image", "text"]
+ help_text: str|None
+ required: bool
+
+ def __init__(
+ self,
+ identifier: str,
+ label: str,
+ type: Literal["image", "text"] = "text",
+ help_text = None,
+ required: bool = False,
+ ):
+ self.identifier = identifier
+ self.label = label
+ self.type = type
+ self.help_text = help_text
+ self.required = required
+
+ def asdict(self):
+ return {
+ "identifier": self.identifier,
+ "label": self.label,
+ "type": self.type,
+ "help_text": self.help_text,
+ "required": self.required,
+ }
+
+
class PassStyle:
identifier: str # unique within platform
name: str
@@ -261,6 +294,10 @@ class PassStyle:
# we evaluate the fields in this order, so they overspill in this order as well
fieldgroups: list[FieldGroup]
+ @property
+ def settings(self) -> list[SettingsField]:
+ return []
+
@property
def preview_layout(self) -> list | None:
return None
@@ -271,10 +308,12 @@ class PassStyle:
"name": self.name,
"fieldgroups": [x.asdict() for x in self.fieldgroups],
"preview_layout": self.preview_layout,
+ "settings": [x.asdict() for x in self.settings],
}
def layout_schema(self):
context = LayoutContext(placeholders=self.placeholders)
+ print(f"schema {self.settings=}")
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
# TODO: $id
@@ -293,6 +332,16 @@ class PassStyle:
"required": [
group.identifier for group in self.fieldgroups if group.required
],
+ },
+ "settings": {
+ "type": "object",
+ "properties": {
+ setting.identifier: {"type": ["string", "null"]}
+ for setting in self.settings
+ },
+ "required": [
+ setting.identifier for setting in self.settings if setting.required
+ ],
}
},
"$defs": {
@@ -305,7 +354,11 @@ class PassStyle:
},
}
if any(group.required for group in self.fieldgroups):
- schema["required"] = ["fieldgroups"]
+ schema.setdefault("required", [])
+ schema["required"].append("fieldgroups")
+ # if any(setting.required for setting in self.settings):
+ # schema.setdefault("required", [])
+ # schema["required"].append("settings")
return schema
@@ -318,10 +371,11 @@ class PassStyle:
return None, None
- def __init__(self, event, layout):
+ def __init__(self, event, layout = None, file_settings: dict[str, File] | None = None):
self.event = event
self.layout = layout
self.placeholders = get_wallet_placeholders(self.event)
+ self.file_settings = file_settings
def validate(self):
schema = self.layout_schema()
@@ -330,6 +384,22 @@ class PassStyle:
except jsonschema.ValidationError as e:
raise ValidationError("Invalid layout: {}".format(str(e)))
+ def extract_file_settings(self, request):
+ file_settings = {}
+ for setting in self.settings:
+ if setting.type == "image":
+ if self.layout.get("settings", {}).get(setting.identifier) == "file:keep":
+ file_settings[setting.identifier] = "keep"
+ elif data := self.layout.get("settings", {}).get(setting.identifier):
+ file_settings[setting.identifier] = handle_file_upload(data, request.user, request.auth, {"image/png", "image/jpeg"})
+ del self.layout["settings"][setting.identifier]
+ print(self.layout['settings'])
+ elif setting.identifier in self.layout.get("settings", {}):
+ file_settings[setting.identifier] = None
+ del self.layout["settings"][setting.identifier]
+
+ return file_settings
+
def get_pass_fields(self, op: OrderPosition):
context = WalletPlaceholderContext(
event=self.event, order=op.order, order_position=op
@@ -380,7 +450,7 @@ class PassStyle:
return fields
def group_is_active(self, identifier: str):
- return self.layout['fieldgroups'].get(identifier, {}).get("active", False)
+ return self.layout["fieldgroups"].get(identifier, {}).get("active", False)
def generate(self, op: OrderPosition):
raise NotImplementedError()
diff --git a/src/pretix/plugins/wallet/views.py b/src/pretix/plugins/wallet/views.py
index 6878aff94d..b438caa915 100644
--- a/src/pretix/plugins/wallet/views.py
+++ b/src/pretix/plugins/wallet/views.py
@@ -10,6 +10,7 @@ from django.urls import reverse
from django.http import HttpResponse, HttpResponseRedirect
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, ListView, DeleteView, View
+from pretix_vrpayment_wero.payment import HttpRequest
from pretix.base.i18n import language
from pretix.base.pdf import get_images, get_variables
from pretix.base.services.tickets import get_preview_position
@@ -29,7 +30,7 @@ from django.contrib.staticfiles import finders
from django.utils.functional import cached_property
from django.templatetags.static import static
from .placeholders import get_wallet_placeholders, WalletPlaceholderContext
-
+from pretix.base.middleware import add_to_response_csp
def get_editor_placeholders(event):
with (
@@ -76,7 +77,7 @@ class LayoutEditorView(LayoutDetailView):
"identifier": platform.identifier,
"name": platform.name,
"styles": {
- style.identifier: style(self.request.event, None).asdict()
+ style.identifier: style(self.request.event).asdict()
for style in AVAILABLE_STYLES.get(platform.identifier)
},
}
@@ -89,7 +90,12 @@ class LayoutEditorView(LayoutDetailView):
}
return context
-
+ def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
+ response = super().dispatch(request, *args, **kwargs)
+ add_to_response_csp(response, {
+ 'img-src': ['blob:'],
+ })
+ return response
class WalletLayoutCreateForm(forms.ModelForm):
class Meta:
@@ -171,7 +177,9 @@ class LayoutPreviewView(EventPermissionRequiredMixin, View):
language(request.event.settings.locale, request.event.settings.region),
):
p = get_preview_position(request.event)
- layout = style(event=event, layout=layout)
+ l = style(event=event, layout=layout) # TODO
+ file_settings = l.extract_file_settings(request)
+ layout = style(event, layout, file_settings)
layout.validate()
fname, mimet, data = layout.generate(p)