From 256316e4bcc7a6ad541509e07b1d5ea1a0ece662 Mon Sep 17 00:00:00 2001 From: Kara Engelhardt Date: Wed, 12 Aug 2026 16:42:58 +0200 Subject: [PATCH] WIP proper fileupload handling, better store --- src/pretix/plugins/wallet/api.py | 27 +- .../pretixplugins/wallet/components/app.vue | 64 ++-- .../wallet/components/input/file-input.vue | 31 +- .../components/placeholder-field-settings.vue | 2 +- .../components/preview/fieldgroup-preview.vue | 4 +- .../placeholder-fieldgroup-preview.vue | 2 +- .../preview/predefined-fieldgroup-preview.vue | 2 +- .../components/preview/setting-preview.vue | 4 +- .../wallet/components/settings-field.vue | 23 +- .../wallet/components/style-settings.vue | 20 +- .../static/pretixplugins/wallet/index.d.ts | 83 +++-- .../pretixplugins/wallet/walletStore.ts | 335 +++++++++++------- .../pretixplugins/wallet/walletStore.ts.old | 252 +++++++++++++ src/pretix/plugins/wallet/styles/base.py | 21 +- 14 files changed, 594 insertions(+), 276 deletions(-) create mode 100644 src/pretix/plugins/wallet/static/pretixplugins/wallet/walletStore.ts.old diff --git a/src/pretix/plugins/wallet/api.py b/src/pretix/plugins/wallet/api.py index 354976a605..0de7c5efd6 100644 --- a/src/pretix/plugins/wallet/api.py +++ b/src/pretix/plugins/wallet/api.py @@ -1,22 +1,24 @@ -from rest_framework import viewsets, serializers +from rest_framework import viewsets 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 rest_framework import serializers +import os + class WalletPlatformLayoutSerializer(I18nAwareModelSerializer): platform = serializers.ChoiceField( choices=[p.identifier for p in AVAILABLE_PLATFORMS] ) style = serializers.CharField(allow_null=True, required=False) + file_settings = serializers.JSONField(default=dict, required=False) class Meta: model = WalletPlatformLayout - fields = ("platform", "style", "layout") + fields = ("platform", "style", "layout", "file_settings") def validate_layout(self, value): if not isinstance(value, dict): @@ -36,20 +38,25 @@ class WalletPlatformLayoutSerializer(I18nAwareModelSerializer): style = style(event=self.context["event"], layout=data["layout"]) style.validate() - data['file_settings'] = style.extract_file_settings(self.context['request']) + data["file_settings"] = style.extract_file_settings( + self.context["request"], data.get("file_settings", {}) + ) return data def to_representation(self, instance): ret = super().to_representation(instance) - ret['file_settings'] = {} + 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) + request = self.context["request"] + ret["file_settings"][file_setting.key] = { + "url": request.build_absolute_uri(url), + "name": os.path.basename(file_setting.file.name).split('.', 1)[-1] + } return ret @@ -71,7 +78,6 @@ class WalletLayoutSerializer(I18nAwareModelSerializer): platform_layouts = validated_data.pop("platform_layouts") for layout in platform_layouts: 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 @@ -81,7 +87,10 @@ class WalletLayoutSerializer(I18nAwareModelSerializer): obj.file_settings.filter(key=key).delete() elif file != "keep": - obj.file_settings.update_or_create(key=key, defaults={"file": file}) + obj, _ = obj.file_settings.get_or_create( + key=key + ) + obj.file.save(os.path.basename(file.name), file) instance.platform_layouts.exclude( platform__in={ 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 bf31b41eb1..2c9f4e925f 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue @@ -33,24 +33,17 @@ function openPreview(e: Event) { openForm("../../preview/", { csrfmiddlewaretoken: store.csrfToken, platform: store.currentPlatform, - style: store.currentPlatformLayout.style, - layout: JSON.stringify(store.currentPlatformLayout.layout), + style: store.layout.style, + layout: JSON.stringify(store.layout.layout), }); } const platformChoices = computed(() => { return [ [null, "Do not generate pass"], - ...Object.values(store.currentPlatformStyles).map((x) => [ - x.identifier, - x.name, - ]), + ...Object.values(store.platform.styles).map((x) => [x.identifier, x.name]), ]; }); - -const preview_layout = computed(() => { - return store.currentPlatformStyles[store.currentPlatformLayout.style]?.preview_layout; -}); - \ No newline at end of file + 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 index 2b96d6f596..288602296e 100644 --- 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 @@ -1,25 +1,31 @@ diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/placeholder-field-settings.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/placeholder-field-settings.vue index a63c619b25..fa88f59132 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/placeholder-field-settings.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/placeholder-field-settings.vue @@ -1,5 +1,5 @@ diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/setting-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/setting-preview.vue index 630af26572..c6cf90002c 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/setting-preview.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/setting-preview.vue @@ -12,13 +12,13 @@ const { config } = defineProps<{ const settingDef = computed(() => { return Object.fromEntries( - store.currentPlatformStyles[store.currentPlatformLayout.style].settings.map( + store.styles[store.layout.style].settings.map( (x) => [x.identifier, x], ), )[config.setting]; }); const settingValue = computed(() => { - const val = store.currentLayoutSettings[config.setting]; + const val = store.settings[config.setting]; if (settingDef.value.type == "image" && val instanceof File) { return URL.createObjectURL(val); } else { 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 index ad2ab57480..df860857a4 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/settings-field.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/settings-field.vue @@ -4,33 +4,14 @@ import FileInput from "./input/file-input.vue"; import { inject } from "vue"; import { StoreKey } from "../walletStore"; -const gettext = (window as any).gettext; - const props = defineProps<{ field?: Setting; }>(); const store = inject(StoreKey)!; - -// function uploadFile(e: Event) { -// // TODO: only store ref, upload when saving, proper handling in store -// const [file] = (e.target as HTMLInputElement).files; -// store.setSetting(props.field.identifier, file) -// // const settings = store.currentPlatformLayout.layout.settings -// // const identifier = props.field.identifier; -// // 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": store.csrfToken, -// // }, -// // }).then(x => x.json()).then(x=>settings[identifier] = x.id); -// } 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 ce22b5d2bd..ae214322c3 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 @@ -1,31 +1,27 @@ 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 07641d3781..765105ba66 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts @@ -1,3 +1,29 @@ +type Platform = { + identifier: string; + name: string; + styles: Styles; +}; + +type Style = { + identifier: string; + name: string; + fieldgroups: FieldGroupDefinition[]; + settings: Setting[]; +}; + +type Variable = { + label: string; + sample: string; + required_context: string[]; +}; + +type Styles = Record; +type Variables = Record; +type VariableConfig = Record; +type Platforms = Platform[]; + +// + type BaseFieldGroupDefinition = { type: string; identifier: string; @@ -46,38 +72,15 @@ type FieldEntry = PlaceholderFieldEntry | CustomFieldEntry; type Setting = { identifier: string; - label: I18nString; + label: string; type: "text" | "image"; required: boolean; + help_text: string; }; -type Style = { - identifier: string; - name: string; - fieldgroups: FieldGroupDefinition[]; - settings: Setting[] -}; - -type Variable = { - label: string; - sample: string; - required_context: string[]; -}; - -type Platform = { - identifier: string; - name: string; - styles: Styles; -}; - -type Styles = Record; -type Variables = Record; -type VariableConfig = Record; -type Platforms = Platform[]; type PlaceholderFieldGroupConfig = { entries: Array; overflow: string | null; - active: boolean; }; type PredefinedFieldGroupConfig = { @@ -90,7 +93,7 @@ type FieldGroupConfig = type LayoutData = { fieldgroups?: Record; - settings?: Record + settings?: Record; }; type PlatformLayout = { @@ -113,7 +116,11 @@ type WalletStore = { type PreviewLayout = Array; type PreviewRow = - | { children: Array; direction?: "row" | "column"; display?: Array;} + | { + children: Array; + direction?: "row" | "column"; + display?: Array; + } | PreviewFieldgroup | FixedPreview | SettingPreview; @@ -122,13 +129,15 @@ type PreviewProps = { relSize?: number; direction?: "row" | "column"; display?: Array; -} +}; type SettingPreview = { setting: string; -} & PreviewProps +} & PreviewProps; -type PreviewFieldgroup = PredefinedFieldgroupPreview | PlaceholderFieldGroupPreview; -type FixedPreview = {value: I18nString; label?: I18nString; } & PreviewProps +type PreviewFieldgroup = + | PredefinedFieldgroupPreview + | PlaceholderFieldGroupPreview; +type FixedPreview = { value: I18nString; label?: I18nString } & PreviewProps; type PlaceholderFieldGroupPreview = { fieldgroup: string; @@ -142,4 +151,14 @@ type PredefinedFieldgroupPreview = { type PreviewSample = { content: I18nString; label: I18nString; -} \ No newline at end of file +}; + +type NewPlatformLayout = { + style: string; + fieldgroups: {}; + settings: {}; + file_settings: Record; +}; +type ServerSideFile = { url: string; name: string }; +type ClientSideFile = { file: File | null; identifier?: string }; +type WalletFile = ServerSideFile | ClientSideFile; diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/walletStore.ts b/src/pretix/plugins/wallet/static/pretixplugins/wallet/walletStore.ts index da5741d2df..d49b9145ab 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/walletStore.ts +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/walletStore.ts @@ -1,3 +1,4 @@ +import { serialize } from "node:v8"; import { i18nstringLocalize } from "./helpers.js"; import { createStore } from "./lib/store.ts"; import { toRaw, type InjectionKey } from "vue"; @@ -5,6 +6,43 @@ import { toRaw, type InjectionKey } from "vue"; export type WidgetStore = ReturnType; export const StoreKey: InjectionKey = Symbol("WidgetStore"); +function getDefaultFieldgroupState( + fieldgroup: FieldGroupDefinition, +): FieldGroupConfig { + if (fieldgroup.type == "predefined") { + return { active: fieldgroup.required }; + } else if (fieldgroup.type == "placeholder") { + return { + overflow: null, + entries: JSON.parse(JSON.stringify(fieldgroup.default_entries)), + }; + } +} + +function parseExistingFieldgroupState( + fieldgroup: FieldGroupDefinition, + existing?: FieldGroupConfig, +): FieldGroupConfig { + if (!existing) { + return getDefaultFieldgroupState(fieldgroup); + } + if (fieldgroup.type == "predefined") { + return { + active: "active" in existing ? existing.active : fieldgroup.required, + }; + } else if (fieldgroup.type == "placeholder") { + return { + overflow: "overflow" in existing ? existing.overflow : null, + // TODO: check that placeholders are possible + entries: structuredClone( + toRaw( + "entries" in existing ? existing.entries : fieldgroup.default_entries, + ), + ), + }; + } +} + export function createWalletStore(config: { platforms: Platforms; variables: VariableConfig; @@ -15,53 +53,51 @@ export function createWalletStore(config: { return createStore({ state: () => ({ ...config, - walletLayout: null as WalletLayout | null, - currentPlatform: config.platforms[0].identifier, loaded: false, - files: {} as Record + activePlatform: config.platforms[0].identifier, + name: null as string | null, + platformLayouts: {} as Record, }), getters: { - currentPlatformStyles() { - for (const platform of this.platforms) { - if (platform.identifier === this.currentPlatform) { - return platform.styles; + platform() { + return this.getPlatform(this.activePlatform); + }, + layout() { + if (!this.loaded) { + return; + } + return this.platformLayouts[this.activePlatform] || null; + }, + style(): Style { + if (this.layout) { + return this.platform.styles[this.layout.style]; + } else { + return null; + } + }, + styles() { + return this.platform.styles; + }, + settings() { + const settings = {}; + for (const setting of this.style.settings) { + if (setting.type == "text") { + settings[setting.identifier] = + this.layout.settings[setting.identifier]; + } else if (setting.type == "image") { + settings[setting.identifier] = + this.layout.file_settings[setting.identifier]; } } - throw "Unknown platform"; + return settings; }, - currentPlatformLayout(): PlatformLayout { - if (!this.walletLayout) { - throw "currentPlatformLayout access before store was loaded"; - } - 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: {}, settings: {} }, - }; - this.walletLayout.platform_layouts.push(newLayout); - return newLayout; - }, - - currentLayoutFieldContent() { + renderedFieldGroups() { const content = {}; - const group_defs = - this.currentPlatformStyles[this.currentPlatformLayout.style] - .fieldgroups; + const group_defs = this.style.fieldgroups; for (const fieldgroup of group_defs) { if (fieldgroup.type == "placeholder") { content[fieldgroup.identifier] = []; - const layout_group = this.currentPlatformLayout.layout.fieldgroups[ + const layout_group = this.layout.fieldgroups[ fieldgroup.identifier ] as any as PlaceholderFieldGroupConfig; for (const entry of layout_group.entries) { @@ -94,7 +130,7 @@ export function createWalletStore(config: { for (const fieldgroup of group_defs) { if (fieldgroup.type == "placeholder") { const layout_group: PlaceholderFieldGroupConfig = - this.currentPlatformLayout.layout.fieldgroups[ + this.layout.fieldgroups[ fieldgroup.identifier ]; if ( @@ -116,11 +152,15 @@ export function createWalletStore(config: { return content; }, - currentLayoutSettings() { - return {...this.currentPlatformLayout.file_settings, ...this.currentPlatformLayout.layout.settings} - }, }, actions: { + getPlatform(identifier: string): Platform { + for (const platform of this.platforms) { + if (platform.identifier === identifier) { + return platform; + } + } + }, load() { // TODO: error handling / proper api client fetch( @@ -128,44 +168,134 @@ export function createWalletStore(config: { ) .then((x) => x.json()) .then((x) => { - this.walletLayout = x; + this.parseServerLayout(x); this.loaded = true; - }); + }) + .catch(alert); }, - async uploadFile(file: File) { + parseServerLayout(serverLayout) { + this.name = serverLayout.name; + for (const layout of serverLayout.platform_layouts) { + const clientLayout = { + style: layout.style, + fieldgroups: {}, + settings: layout.layout.settings, + file_settings: layout.file_settings, + }; + const styleDefinition = this.getPlatform(layout.platform).styles[ + layout.style + ]; + console.log(styleDefinition); + for (const fieldgroup of styleDefinition.fieldgroups) { + clientLayout.fieldgroups[fieldgroup.identifier] = + parseExistingFieldgroupState( + fieldgroup, + layout.layout.fieldgroups[fieldgroup.identifier], + ); + } + this.platformLayouts[layout.platform] = clientLayout; + } + }, + setPlatform(platform: string) { + this.activePlatform = platform; + }, + setStyle(style: string | null) { + if (style === null) { + delete this.platformLayouts[this.activePlatform]; + } else if (Object.keys(this.platform.styles).includes(style)) { + const newLayout = { + style, + fieldgroups: {}, + settings: {}, + file_settings: {}, + }; + for (const fieldgroup of this.platform.styles[style].fieldgroups) { + newLayout.fieldgroups[fieldgroup.identifier] = + getDefaultFieldgroupState(fieldgroup); + } + this.platformLayouts[this.activePlatform] = newLayout; + // TODO: keep old fieldgroups & settings if matching + } + }, + getSetting(identifier: string): Setting { + for (const setting of this.style.settings) { + if (setting.identifier === identifier) { + return setting; + } + } + }, + setSetting(identifier: string, value: string | File) { + const setting = this.getSetting(identifier); + if (!setting) return; + + if ( + setting.type === "image" && + (value instanceof File || value === null) + ) { + this.layout.file_settings[setting.identifier] = { + file: value as File | null, + }; + } else if (setting.type == "text" && typeof value == "string") { + this.layout.settings[setting.identifier] = value; + } + }, + async uploadFile(file: ClientSideFile) { return await fetch("/api/v1/upload", { method: "POST", - body: file, + body: file.file, headers: { - "content-disposition": `attachment; filename="${encodeURI(file.name)}"`, - "content-type": file.type || "application/octet-stream", + "content-disposition": `attachment; filename="${encodeURI(file.file.name)}"`, + "content-type": file.file.type || "application/octet-stream", "X-CSRFToken": this.csrfToken, }, }) .then((x) => x.json()) - .then((x) => x.id); + .then((x) => { + file.identifier = x.id; + return x.id; + }) + .catch(alert); }, - 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], - ); - } - } - } + async serializePlatformLayout(platform, layout: NewPlatformLayout) { + const uploadedFiles = Object.fromEntries( + ( + await Promise.all( + Object.entries(layout.file_settings).map(async ([k, v]) => { + if ("url" in v) { + return; + } else if ("identifier" in v) { + return [k, v.identifier]; + } else if ("file" in v && v.file instanceof File) { + return [k, await this.uploadFile(v)]; + } else if ("file" in v && v.file == null) { + return [k, null]; + } + }), + ) + ).filter((x) => !!x), + ); + return { + platform, + style: layout.style, + file_settings: uploadedFiles, + layout: { + fieldgroups: layout.fieldgroups, + settings: layout.settings, + }, + }; + }, + async serializeLayout() { + const layoutPromises = Object.entries(this.platformLayouts).map( + ([platform, layout]) => + this.serializePlatformLayout(platform, layout), + ); + return { + name: this.name, + platform_layouts: await Promise.all(layoutPromises), + }; + }, + async save() { + const serializedLayout = await this.serializeLayout(); // TODO: error handling / proper api client fetch( `/api/v1/organizers/demo/events/wallet/walletlayouts/${this.layoutId}/`, @@ -175,78 +305,15 @@ export function createWalletStore(config: { "content-type": "application/json", "X-CSRFToken": this.csrfToken, }, - body: JSON.stringify(layoutToSave), + body: JSON.stringify(serializedLayout), }, ) .then((x) => x.json()) .catch((x) => alert(x)) .then((x) => { - this.walletLayout = x; + this.parseServerLayout(x); }); }, - setCurrentPlatformStyle(style: string | null) { - if (style === null) { - this.currentPlatformLayout.style = null; - this.currentPlatformLayout.layout.fieldgroups = {}; - } else if (Object.keys(this.currentPlatformStyles).includes(style)) { - const oldStyle = - this.currentPlatformLayout.style !== null - ? this.currentPlatformStyles[this.currentPlatformLayout.style] - : { fieldgroups: [] }; - const newStyle = this.currentPlatformStyles[style]; - - const oldFieldGroups = Object.fromEntries( - oldStyle.fieldgroups.map((x) => [x.identifier, x]), - ); - const newFieldGroups = Object.fromEntries( - newStyle.fieldgroups.map((x) => [x.identifier, x]), - ); - const keysToKeep = new Set( - Object.keys(this.currentPlatformLayout.layout.fieldgroups).filter( - (x) => - oldFieldGroups[x]?.type === "placeholder" && - newFieldGroups[x]?.type === "placeholder" && - oldFieldGroups[x]?.content_type === - newFieldGroups[x]?.content_type, - ), - ); - const keysToDefault = new Set(Object.keys(newFieldGroups)).difference( - keysToKeep, - ); - const keysToRemove = new Set( - Object.keys(this.currentPlatformLayout.layout.fieldgroups), - ) - .difference(keysToKeep) - .difference(keysToDefault); - - for (const key of keysToRemove) { - delete this.currentPlatformLayout.layout.fieldgroups[key]; - } - - for (const key of keysToDefault) { - if (newFieldGroups[key].type == "placeholder") { - this.currentPlatformLayout.layout.fieldgroups[key] = { - overflow: null, - 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] = { - active: newFieldGroups[key].required, - }; - } - } - - this.currentPlatformLayout.style = style; - } - }, - setSetting(identifier: string, value: string | File | null) { - this.currentPlatformLayout.layout.settings[identifier] = value; - }, }, }); } diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/walletStore.ts.old b/src/pretix/plugins/wallet/static/pretixplugins/wallet/walletStore.ts.old new file mode 100644 index 0000000000..da5741d2df --- /dev/null +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/walletStore.ts.old @@ -0,0 +1,252 @@ +import { i18nstringLocalize } from "./helpers.js"; +import { createStore } from "./lib/store.ts"; +import { toRaw, type InjectionKey } from "vue"; + +export type WidgetStore = ReturnType; +export const StoreKey: InjectionKey = Symbol("WidgetStore"); + +export function createWalletStore(config: { + platforms: Platforms; + variables: VariableConfig; + locales: Record; + csrfToken: string; + layoutId: string; +}) { + return createStore({ + state: () => ({ + ...config, + walletLayout: null as WalletLayout | null, + currentPlatform: config.platforms[0].identifier, + loaded: false, + files: {} as Record + }), + getters: { + currentPlatformStyles() { + for (const platform of this.platforms) { + if (platform.identifier === this.currentPlatform) { + return platform.styles; + } + } + throw "Unknown platform"; + }, + currentPlatformLayout(): PlatformLayout { + if (!this.walletLayout) { + throw "currentPlatformLayout access before store was loaded"; + } + 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: {}, settings: {} }, + }; + this.walletLayout.platform_layouts.push(newLayout); + return newLayout; + }, + + currentLayoutFieldContent() { + const content = {}; + const group_defs = + this.currentPlatformStyles[this.currentPlatformLayout.style] + .fieldgroups; + for (const fieldgroup of group_defs) { + if (fieldgroup.type == "placeholder") { + content[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; + + let label = i18nstringLocalize(entry.label); + if (placeholder && !label) { + label = i18nstringLocalize(placeholder.label); + } + + let value = null; + if (entry.type == "custom") { + value = i18nstringLocalize(entry.content); + } else if (entry.type == "placeholder") { + value = + placeholder?.sample || + `(unknown placeholder: ${entry.content})`; + } + content[fieldgroup.identifier].push({ + entry, + label, + content: value, + }); + } + } + } + for (const fieldgroup of group_defs) { + if (fieldgroup.type == "placeholder") { + const layout_group: PlaceholderFieldGroupConfig = + this.currentPlatformLayout.layout.fieldgroups[ + fieldgroup.identifier + ]; + if ( + fieldgroup.max_entries && + content[fieldgroup.identifier].length > fieldgroup.max_entries + ) { + const overflow = content[fieldgroup.identifier].slice( + fieldgroup.max_entries, + ); + content[fieldgroup.identifier] = content[ + fieldgroup.identifier + ].slice(0, fieldgroup.max_entries); + if (layout_group.overflow) { + content[layout_group.overflow].splice(0, 0, ...overflow); + } + } + } + } + + return content; + }, + currentLayoutSettings() { + return {...this.currentPlatformLayout.file_settings, ...this.currentPlatformLayout.layout.settings} + }, + }, + actions: { + load() { + // TODO: error handling / proper api client + fetch( + `/api/v1/organizers/demo/events/wallet/walletlayouts/${this.layoutId}/`, + ) + .then((x) => x.json()) + .then((x) => { + this.walletLayout = x; + this.loaded = true; + }); + }, + 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}/`, + { + method: "PUT", + headers: { + "content-type": "application/json", + "X-CSRFToken": this.csrfToken, + }, + body: JSON.stringify(layoutToSave), + }, + ) + .then((x) => x.json()) + .catch((x) => alert(x)) + .then((x) => { + this.walletLayout = x; + }); + }, + setCurrentPlatformStyle(style: string | null) { + if (style === null) { + this.currentPlatformLayout.style = null; + this.currentPlatformLayout.layout.fieldgroups = {}; + } else if (Object.keys(this.currentPlatformStyles).includes(style)) { + const oldStyle = + this.currentPlatformLayout.style !== null + ? this.currentPlatformStyles[this.currentPlatformLayout.style] + : { fieldgroups: [] }; + const newStyle = this.currentPlatformStyles[style]; + + const oldFieldGroups = Object.fromEntries( + oldStyle.fieldgroups.map((x) => [x.identifier, x]), + ); + const newFieldGroups = Object.fromEntries( + newStyle.fieldgroups.map((x) => [x.identifier, x]), + ); + const keysToKeep = new Set( + Object.keys(this.currentPlatformLayout.layout.fieldgroups).filter( + (x) => + oldFieldGroups[x]?.type === "placeholder" && + newFieldGroups[x]?.type === "placeholder" && + oldFieldGroups[x]?.content_type === + newFieldGroups[x]?.content_type, + ), + ); + const keysToDefault = new Set(Object.keys(newFieldGroups)).difference( + keysToKeep, + ); + const keysToRemove = new Set( + Object.keys(this.currentPlatformLayout.layout.fieldgroups), + ) + .difference(keysToKeep) + .difference(keysToDefault); + + for (const key of keysToRemove) { + delete this.currentPlatformLayout.layout.fieldgroups[key]; + } + + for (const key of keysToDefault) { + if (newFieldGroups[key].type == "placeholder") { + this.currentPlatformLayout.layout.fieldgroups[key] = { + overflow: null, + 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] = { + active: newFieldGroups[key].required, + }; + } + } + + this.currentPlatformLayout.style = style; + } + }, + setSetting(identifier: string, value: string | File | null) { + this.currentPlatformLayout.layout.settings[identifier] = value; + }, + }, + }); +} diff --git a/src/pretix/plugins/wallet/styles/base.py b/src/pretix/plugins/wallet/styles/base.py index 2331bcbe91..776c1d8191 100644 --- a/src/pretix/plugins/wallet/styles/base.py +++ b/src/pretix/plugins/wallet/styles/base.py @@ -384,21 +384,18 @@ class PassStyle: except jsonschema.ValidationError as e: raise ValidationError("Invalid layout: {}".format(str(e))) - def extract_file_settings(self, request): - file_settings = {} + def extract_file_settings(self, request, file_settings): + res = {} 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] + if file_settings.get(setting.identifier) == "file:keep": + res[setting.identifier] = "keep" + elif data := file_settings.get(setting.identifier): + res[setting.identifier] = handle_file_upload(data, request.user, request.auth, {"image/png", "image/jpeg"}) + elif setting.identifier in file_settings: + res[setting.identifier] = None - return file_settings + return res def get_pass_fields(self, op: OrderPosition): context = WalletPlaceholderContext(