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;
-});
@@ -59,26 +52,26 @@ const preview_layout = computed(() => {
// TODO: proper spinner
template(v-if="!store.loaded") {{ gettext("Loading...") }}
- form(v-else @submit.prevent="store.saveLayout")
+ form(v-else @submit.prevent="store.save")
.form-group
- Input(label="Name" v-model="store.walletLayout.name")
+ Input(label="Name" v-model="store.name")
nav
ul.nav.nav-tabs
- li(v-for="platform in store.platforms" :class="{'active': store.currentPlatform === platform.identifier}")
- a(role="tab" @click="store.currentPlatform = platform.identifier") {{ platform.name }}
+ li(v-for="platform in store.platforms" :class="{'active': store.platform.identifier === platform.identifier}")
+ a(role="tab" @click="store.setPlatform(platform.identifier)") {{ platform.name }}
.tabbed-form.tab-content
.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]")
+ Select.form-group(label="Style" :modelValue="store.style?.identifier || null" @update:modelValue="store.setStyle" :choices="platformChoices")
+ StyleSettings(v-if="!!store.style")
.col-md-6.col-lg-4
.panel.panel-default
.panel-heading Preview
.panel-body
- div(v-if="preview_layout")
- span.text-muted The preview below is only a rough representation of what the pass might look like. Please check the generated pass.
+ div(v-if="!!store.style?.preview_layout")
+ span.text-muted {{ gettext("The preview below is only a rough representation of what the pass might look like. Please check the generated pass.") }}
div(style="display: grid; gap: 1em; grid-template-columns: repeat(auto-fit, minmax(auto, 360px));")
- PassPreview(v-for="layout in preview_layout" :layout="layout")
+ PassPreview(v-for="layout in store.style.preview_layout" :layout="layout")
div(v-else) Preview not supported
//- pre
//- code {{ store.currentPlatformLayout }}
@@ -92,23 +85,22 @@ const preview_layout = computed(() => {
-
\ 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 @@
@@ -28,18 +34,17 @@ function onChange(e) {
br(v-if="!$attrs.required")
span.optional(v-if="!$attrs.required") {{ gettext("Optional") }}
div.col-md-9
- template(v-if="typeof modelValue == 'string'")
+ template(v-if="!!current_url")
| {{ gettext("Currently") + ': ' }}
- //- TODO: preview filename
- a(:href="modelValue") FILENAME
+ a(:href="current_url") {{ filename }}
| {{ " " }}
- button.btn.btn-sm(@click.prevent="() => {console.log('clear'); modelValue = null}") {{ gettext("Clear") }}
+ button.btn.btn-sm(@click.prevent="() => {console.log('clear'); emit('change', 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")
+ input(:id="id" @change="onChange" v-bind="$attrs" type="file" style="display: inline" ref="inputRef")
.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/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);
-// }
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]")
+ 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.settings[field.identifier]" :help_text="field.help_text")
+ FileInput(v-if='field.type == "image"' :label="field.label" :required="field.required" :help_text="field.help_text" @change="(v) => store.setSetting(field.identifier, v)" :filename="store.settings[field.identifier]?.name" :current_url="store.settings[field.identifier]?.name")
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 @@
h2.h3 {{ gettext("Settings") }}
- SettingsField(v-for="field of style.settings" :field="field" :key="field.identifier")
+ SettingsField(v-for="field of store.style.settings" :field="field" :key="field.identifier")
h2.h3 {{ gettext("Field Groups") }}
- div(v-if="props.style && layout.fieldgroups"
- v-for="(fieldgroup, fieldgroupId) in props.style.fieldgroups" :id="'fieldgroup-' + fieldgroup.identifier")
+ div(v-for="(fieldgroup, fieldgroupId) in store.style.fieldgroups" :id="'fieldgroup-' + fieldgroup.identifier")
PlaceholderFieldSettings(
v-if="fieldgroup.type == 'placeholder'"
- v-model="layout.fieldgroups[fieldgroup.identifier]"
+ v-model="store.layout.fieldgroups[fieldgroup.identifier]"
:fieldgroup="fieldgroup"
- :overflows="props.style.fieldgroups.slice(fieldgroupId + 1) \
+ :overflows="store.style.fieldgroups.slice(fieldgroupId + 1) \
.filter(x => x.type == 'placeholder' && x.content_type === fieldgroup.content_type)"
)
PredefinedFieldSettings(v-else-if="fieldgroup.type == 'predefined'"
- v-model="layout.fieldgroups[fieldgroup.identifier]"
+ v-model="store.layout.fieldgroups[fieldgroup.identifier]"
:fieldgroup="fieldgroup")
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(