forked from CGM_Public/pretix_original
WIP: i18nfields, refactoring, jsonschema-validatoin
This commit is contained in:
@@ -1,65 +1,83 @@
|
||||
from rest_framework import viewsets
|
||||
from django.db import transaction
|
||||
from .styles import PassLayout, get_platform_styles, get_platforms
|
||||
from .styles import PassLayout, AVAILABLE_STYLES_DICT
|
||||
from .models import WalletLayout
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
import django_filters.rest_framework
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .views import get_layout_variables
|
||||
|
||||
|
||||
class WalletLayoutSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = WalletLayout
|
||||
fields = ("event","platform","name","style","layout")
|
||||
read_only_fields = ("event", "platform")
|
||||
fields = ("id", "platform", "name", "style", "layout")
|
||||
read_only_fields = ("id",)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance:
|
||||
self.fields['platform'].read_only = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs, event=self.context["event"])
|
||||
|
||||
def validate_platform(self, value):
|
||||
if self.instance and value != self.instance.platform:
|
||||
raise ValidationError(_("Platform cannot be changed"))
|
||||
|
||||
if value not in AVAILABLE_STYLES_DICT:
|
||||
raise ValidationError(_("Invalid platform"))
|
||||
return value
|
||||
|
||||
def validate_layout(self, value):
|
||||
if not isinstance(value, dict):
|
||||
raise ValidationError(_("Layout must be a dict"))
|
||||
return value
|
||||
|
||||
|
||||
def validate_platform(self, value):
|
||||
if value not in get_platforms():
|
||||
raise ValidationError(_("Invalid Platform"))
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
if "style" in data and "layout" in data and "platform" in data:
|
||||
platform_styles = get_platform_styles(data['platform'])
|
||||
if data['style'] not in platform_styles:
|
||||
if self.instance:
|
||||
platform = self.instance.platform
|
||||
else:
|
||||
platform = data.get('platform', None)
|
||||
if "style" in data and "layout" in data and platform:
|
||||
platform_styles = AVAILABLE_STYLES_DICT[platform]
|
||||
|
||||
if data["style"] not in platform_styles:
|
||||
raise ValidationError(_("Invalid style"))
|
||||
style = get_platform_styles(data['platform'])[data['style']]
|
||||
style = platform_styles[data["style"]]
|
||||
|
||||
layout = PassLayout(
|
||||
style=style, layout=data["layout"]
|
||||
)
|
||||
breakpoint()
|
||||
layout.validate(data['event'])
|
||||
layout = PassLayout(style=style, layout=data["layout"])
|
||||
context = {"placeholders": {k: list(v.keys()) for k,v in get_layout_variables(self.context['event']).items()}}
|
||||
layout.validate(context=context)
|
||||
return data
|
||||
|
||||
|
||||
|
||||
class WalletLayoutViewSet(viewsets.ModelViewSet):
|
||||
model = WalletLayout
|
||||
queryset = WalletLayout.objects.none()
|
||||
serializer_class = WalletLayoutSerializer
|
||||
filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
|
||||
filterset_fields = ['platform']
|
||||
filterset_fields = ["platform"]
|
||||
permission = "event.settings.general:write"
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.wallet_layouts.all()
|
||||
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
return super().get_serializer(*args, **kwargs)
|
||||
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx["event"] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
super().perform_update(serializer)
|
||||
serializer.instance.log_action(
|
||||
action='pretix.plugins.wallet.layout.changed',
|
||||
action="pretix.plugins.wallet.layout.changed",
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data,
|
||||
|
||||
@@ -23,6 +23,7 @@ from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import LoggedModel
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
|
||||
class WalletLayout(LoggedModel):
|
||||
@@ -37,11 +38,26 @@ class WalletLayout(LoggedModel):
|
||||
)
|
||||
platform = models.CharField(max_length=10)
|
||||
style = models.CharField(max_length=255)
|
||||
layout = models.JSONField(default={})
|
||||
layout = models.JSONField(default=dict)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
ordering = ("name",)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
# TODO:ScopedManager
|
||||
|
||||
|
||||
class WalletLayoutItem(models.Model):
|
||||
item = models.ForeignKey('pretixbase.Item', null=True, blank=True, related_name='walletlayout_assignments',
|
||||
on_delete=models.CASCADE)
|
||||
layout = models.ForeignKey(WalletLayout, on_delete=models.CASCADE, related_name='item_assignments')
|
||||
sales_channel = models.ForeignKey(
|
||||
"pretixbase.SalesChannel",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('item', 'layout', 'sales_channel'),)
|
||||
ordering = ("id",)
|
||||
|
||||
@@ -10,52 +10,55 @@ const isLoading = ref<boolean>(true);
|
||||
const wallet_layout = ref<Layout | null>(null);
|
||||
|
||||
const STYLES: Styles = JSON.parse(
|
||||
document.querySelector("#styles")?.textContent ?? "{}",
|
||||
document.querySelector("#styles")?.textContent ?? "{}",
|
||||
);
|
||||
const VARIABLES: VariableConfig = JSON.parse(
|
||||
document.querySelector("#variables")?.textContent ?? "{}",
|
||||
document.querySelector("#variables")?.textContent ?? "{}",
|
||||
);
|
||||
const LOCALES: Record<string, string> = JSON.parse(
|
||||
document.querySelector("#locales")?.textContent ?? "{}",
|
||||
);
|
||||
const CSRF_TOKEN =
|
||||
document.querySelector<HTMLInputElement>("input[name=csrfmiddlewaretoken]")
|
||||
?.value ?? "";
|
||||
document.querySelector<HTMLInputElement>("input[name=csrfmiddlewaretoken]")
|
||||
?.value ?? "";
|
||||
|
||||
const props = defineProps<{
|
||||
layoutId: string;
|
||||
layoutId: string;
|
||||
}>();
|
||||
|
||||
watchEffect(() => {
|
||||
// TODO: error handling / proper api client
|
||||
isLoading.value = true;
|
||||
fetch(
|
||||
`/api/v1/organizers/demo/events/wallet/walletlayouts/${props.layoutId}/`,
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.then((x) => {
|
||||
wallet_layout.value = x;
|
||||
isLoading.value = false;
|
||||
});
|
||||
// TODO: error handling / proper api client
|
||||
isLoading.value = true;
|
||||
fetch(
|
||||
`/api/v1/organizers/demo/events/wallet/walletlayouts/${props.layoutId}/`,
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.then((x) => {
|
||||
wallet_layout.value = x;
|
||||
isLoading.value = false;
|
||||
});
|
||||
});
|
||||
|
||||
function saveLayout(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
isLoading.value = true;
|
||||
// TODO: error handling / proper api client
|
||||
fetch(
|
||||
`/api/v1/organizers/demo/events/wallet/walletlayouts/${props.layoutId}/`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
},
|
||||
body: JSON.stringify(wallet_layout.value),
|
||||
},
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.then((x) => {
|
||||
wallet_layout.value = x;
|
||||
isLoading.value = false;
|
||||
});
|
||||
e.preventDefault();
|
||||
isLoading.value = true;
|
||||
// TODO: error handling / proper api client
|
||||
fetch(
|
||||
`/api/v1/organizers/demo/events/wallet/walletlayouts/${props.layoutId}/`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
},
|
||||
body: JSON.stringify(wallet_layout.value),
|
||||
},
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.then((x) => {
|
||||
wallet_layout.value = x;
|
||||
isLoading.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -73,7 +76,7 @@ function saveLayout(e: SubmitEvent) {
|
||||
.form-group()
|
||||
Select(label="Style" v-model="wallet_layout.style" :choices="Object.values(STYLES).map(x => [x.identifier, x.name])")
|
||||
|
||||
StyleSettings(v-if="wallet_layout.style" v-model="wallet_layout.layout" :style="STYLES[wallet_layout.style]" :variables="VARIABLES")
|
||||
StyleSettings(v-if="wallet_layout.style" v-model="wallet_layout.layout" :style="STYLES[wallet_layout.style]" :variables="VARIABLES" :locales="LOCALES")
|
||||
.col-md-4
|
||||
.panel.panel-default
|
||||
.panel-heading Preview
|
||||
@@ -81,6 +84,8 @@ function saveLayout(e: SubmitEvent) {
|
||||
// TODO: Preview
|
||||
pre
|
||||
code {{ wallet_layout }}
|
||||
pre(v-if="wallet_layout.style")
|
||||
code {{ STYLES[wallet_layout.style] }}
|
||||
.form-group.submit-group
|
||||
button.btn.btn-primary.btn-save(type="submit") Submit
|
||||
</template>
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from "vue";
|
||||
import Select from "./input/select.vue";
|
||||
import Input from "./input/input.vue";
|
||||
import TextContent from "./text-content.vue";
|
||||
|
||||
const gettext = (window as any).gettext;
|
||||
|
||||
const props = defineProps<{
|
||||
field: FieldGroupDefinition;
|
||||
overflows: FieldGroupDefinition[];
|
||||
variables: Variables;
|
||||
}>();
|
||||
const fieldConfig = defineModel<FieldConfig>({ required: true });
|
||||
|
||||
const overflowOptions = computed((): Array<[string | null, string]> => {
|
||||
if (props.overflows.length) {
|
||||
return [
|
||||
...props.overflows.map((x): [string, string] => [x.identifier, x.name]),
|
||||
[null, "Do not overflow"],
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
function addVariable() {
|
||||
fieldConfig.value.entries.push({ type: "placeholder" });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
h3.panel-title {{ field.name }}
|
||||
.panel-body
|
||||
.form-group
|
||||
span.text-muted These fields appear somewhere and are visible too.
|
||||
h4 {{ gettext("Content") }}
|
||||
.row.form-group(v-for="n in fieldConfig.entries.length")
|
||||
.col-md-5
|
||||
Input(:label="gettext('Label')" v-model="fieldConfig.entries[n-1].label")
|
||||
.col-md-6(v-if='field.entry_type == "text"')
|
||||
TextContent(v-model="fieldConfig.entries[n-1]"
|
||||
:variables="props.variables")
|
||||
.col-md-6(v-else-if='field.entry_type == "image"')
|
||||
Select(:label="gettext('Content')"
|
||||
v-model="fieldConfig.entries[n-1].content"
|
||||
:choices="Object.entries(props.variables).map(([k,v]) => [k, v.label])"
|
||||
)
|
||||
.col-md-1
|
||||
label.control-label
|
||||
span.sr-only {{ gettext('Delete')}}
|
||||
button.btn.btn-danger(type="button" @click="fieldConfig.entries.splice(n-1, 1)")
|
||||
i.fa.fa-trash
|
||||
span.sr-only {{ gettext('Delete')}}
|
||||
button.btn.btn-default(type="button" @click="addVariable")
|
||||
i.fa.fa-plus
|
||||
span.sr-only {{ gettext("Add field") }}
|
||||
Select(:label="gettext('Overflow to …')" :choices="overflowOptions" v-model="fieldConfig.overflow")
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { useId, watchEffect } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
label?: string,
|
||||
errors?: string[],
|
||||
locales: Record<string, string>
|
||||
}>()
|
||||
const modelValue = defineModel<Record<string, string> | string>();
|
||||
watchEffect(() => {
|
||||
if (typeof modelValue.value === "string") {
|
||||
const oldVal = modelValue.value;
|
||||
modelValue.value = Object.fromEntries(Object.keys(props.locales).map((x): [string, string] => [x, oldVal]))
|
||||
}
|
||||
})
|
||||
const id = useId()
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
label.control-label(:for="id", v-if="props.label") {{ props.label }}
|
||||
.i18n-form-group
|
||||
input.form-control(v-for="(human_readable, locale) in locales" v-model="modelValue[locale]" v-bind="$attrs" :lang="locale" :title="human_readable" :placeholder="human_readable")
|
||||
.help-block(v-if="props.errors" v-for="error in props.errors") {{ error }}
|
||||
</template>
|
||||
@@ -25,7 +25,7 @@ watchEffect(() => {
|
||||
<template lang="pug">
|
||||
template(v-if="choices.length >= 1")
|
||||
label.control-label(:for="id") {{ props.label }}
|
||||
select.form-control(:id="id" v-model="modelValue" v-bind="$attrs")
|
||||
select.form-control(:id="id" v-model="modelValue" v-bind="$attrs" required)
|
||||
option(v-for="choice in props.choices" :key="choice[0]" :value="choice[0]") {{ choice[1] }}
|
||||
.help-block(v-if="props.errors" v-for="error in props.errors") {{ error }}
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watchEffect } from "vue";
|
||||
import Select from "./input/select.vue";
|
||||
import Input from "./input/input.vue";
|
||||
import I18nInput from "./input/i18ninput.vue";
|
||||
import TextContent from "./text-content.vue";
|
||||
|
||||
const gettext = (window as any).gettext;
|
||||
|
||||
const props = defineProps<{
|
||||
fieldgroup: FieldGroupDefinition;
|
||||
overflows: FieldGroupDefinition[];
|
||||
variables: Variables;
|
||||
locales: Record<string, string>;
|
||||
}>();
|
||||
const fieldConfig = defineModel<PlaceholderFieldGroupConfig>({ required: true });
|
||||
|
||||
const overflowOptions = computed((): Array<[string | null, string]> => {
|
||||
if (props.overflows.length) {
|
||||
return [
|
||||
...props.overflows.map((x): [string, string] => [x.identifier, x.name]),
|
||||
[null, "Do not overflow"],
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
function addVariable() {
|
||||
fieldConfig.value.entries.push({ type: "placeholder", label: "" });
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (!fieldConfig.value) {
|
||||
fieldConfig.value = {overflow: null, entries: JSON.parse(JSON.stringify(props.fieldgroup.default_entries))};
|
||||
}
|
||||
if (fieldConfig.value && !fieldConfig.value.entries) {
|
||||
fieldConfig.value.entries = JSON.parse(JSON.stringify(props.fieldgroup.default_entries))
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
h3.panel-title {{ fieldgroup.name }}
|
||||
.panel-body(v-if="fieldConfig")
|
||||
.form-group()
|
||||
span.text-muted(v-if="fieldgroup.description") {{ fieldgroup.description }}
|
||||
h4 {{ gettext("Content") }}
|
||||
.row.form-group(v-for="n in fieldConfig.entries.length")
|
||||
.col-md-5(v-if="fieldgroup.labels")
|
||||
I18nInput(:label="gettext('Label')" v-model="fieldConfig.entries[n-1].label" :locales="locales")
|
||||
div(:class="'col-md-' + (fieldgroup.labels ? '6' : '11')")
|
||||
TextContent(v-if='fieldgroup.content_type == "text"'
|
||||
v-model="fieldConfig.entries[n-1]"
|
||||
:variables="props.variables")
|
||||
Select(:label="gettext('Content')"
|
||||
v-else-if='fieldgroup.content_type == "image"'
|
||||
v-model="fieldConfig.entries[n-1].content"
|
||||
:choices="Object.entries(props.variables).map(([k,v]) => [k, v.label])"
|
||||
)
|
||||
.col-md-1
|
||||
label.control-label
|
||||
span.sr-only {{ gettext('Delete')}}
|
||||
button.btn.btn-danger(type="button" @click="fieldConfig.entries.splice(n-1, 1)")
|
||||
i.fa.fa-trash
|
||||
span.sr-only {{ gettext('Delete')}}
|
||||
button.btn.btn-default(type="button" @click="addVariable")
|
||||
i.fa.fa-plus
|
||||
span.sr-only {{ gettext("Add field") }}
|
||||
Select(:label="gettext('Overflow to …')" :choices="overflowOptions" v-model="fieldConfig.overflow")
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
const gettext = (window as any).gettext;
|
||||
|
||||
const props = defineProps<{
|
||||
fieldgroup: FieldGroupDefinition;
|
||||
}>();
|
||||
const fieldConfig = defineModel<PredefinedFieldGroupConfig>({ required: true });
|
||||
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
h3.panel-title {{ fieldgroup.name }}
|
||||
.panel-body
|
||||
.form-group
|
||||
span.text-muted These fields appear somewhere and are visible too.
|
||||
</template>
|
||||
@@ -1,44 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watchEffect } from "vue";
|
||||
import FieldSettings from "./field-settings.vue";
|
||||
import PlaceholderFieldSettings from "./placeholder-field-settings.vue";
|
||||
import PredefinedFieldSettings from "./predefined-field-settings.vue";
|
||||
|
||||
const gettext = (window as any).gettext;
|
||||
|
||||
const props = defineProps<{
|
||||
variables: VariableConfig
|
||||
style?: Style;
|
||||
variables: VariableConfig
|
||||
style?: Style;
|
||||
locales: Record<string, string>;
|
||||
}>();
|
||||
|
||||
const layout = defineModel<LayoutData>();
|
||||
|
||||
|
||||
watchEffect(() => {
|
||||
if (layout.value === undefined) {
|
||||
return
|
||||
}
|
||||
if (layout.value.fields === undefined) {
|
||||
layout.value.fields = {};
|
||||
}
|
||||
if (props.style) {
|
||||
for (const field of props.style.fields) {
|
||||
if (!(field.identifier in layout.value.fields)) {
|
||||
layout.value.fields[field.identifier] = {
|
||||
entries: JSON.parse(JSON.stringify(field.default_entries)),
|
||||
overflow: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (layout.value === undefined) {
|
||||
return
|
||||
}
|
||||
if (layout.value.fieldgroups === undefined) {
|
||||
layout.value.fieldgroups = {};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
h2.h3 {{ gettext("Field Groups") }}
|
||||
FieldSettings(v-if="props.style"
|
||||
v-for="(field, fieldId) in props.style.fields"
|
||||
v-model="layout.fields[field.identifier]"
|
||||
:field="field"
|
||||
:overflows="props.style.fields.slice(fieldId + 1).filter(x => x.entry_type === field.entry_type)"
|
||||
:variables="variables[field.entry_type]"
|
||||
)
|
||||
template(v-if="props.style && layout.fieldgroups"
|
||||
v-for="(fieldgroup, fieldgroupId) in props.style.fieldgroups")
|
||||
PlaceholderFieldSettings(
|
||||
v-if="fieldgroup.type == 'placeholder'"
|
||||
v-model="layout.fieldgroups[fieldgroup.identifier]"
|
||||
:fieldgroup="fieldgroup"
|
||||
:overflows="props.style.fieldgroups.slice(fieldgroupId + 1).filter(x => x.type == 'placeholder' && x.content_type === fieldgroup.content_type)"
|
||||
:variables="variables[fieldgroup.content_type]"
|
||||
:locales="locales"
|
||||
)
|
||||
PredefinedFieldSettings(v-else-if="fieldgroup.type == 'predefined'"
|
||||
v-model="layout.fieldgroups[fieldgroup.identifier]"
|
||||
:fieldgroup="fieldgroup")
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,41 @@
|
||||
type FieldGroupDefinition = {
|
||||
type BaseFieldGroupDefinition = {
|
||||
type: string;
|
||||
identifier: string;
|
||||
entry_type: string;
|
||||
name: string;
|
||||
default_entries: FieldConfig[];
|
||||
};
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
type FieldGroupDefinition = PlaceholderFieldGroupDefinition | PredefinedFieldGroup;
|
||||
|
||||
type PlaceholderFieldGroupDefinition = BaseFieldGroupDefinition & {
|
||||
type: 'placeholder';
|
||||
content_type: FieldContentType;
|
||||
default_entries: FieldEntry[];
|
||||
labels: boolean;
|
||||
min_entries: number|null;
|
||||
max_entries: number|null;
|
||||
}
|
||||
|
||||
type PredefinedFieldGroupDefinition = BaseFieldGroupDefinition & {
|
||||
type: 'predefined';
|
||||
}
|
||||
|
||||
type I18nString = string | Record<string, string>
|
||||
|
||||
type FieldContentType = 'text' | 'image';
|
||||
|
||||
type FieldEntry = {
|
||||
type: 'placeholder' | FieldContentType;
|
||||
label?: I18nString;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
type Style = {
|
||||
identifier: string;
|
||||
name: string;
|
||||
fields: FieldGroupDefinition[];
|
||||
fieldgroups: FieldGroupDefinition[];
|
||||
};
|
||||
|
||||
type Variable = {
|
||||
@@ -19,21 +46,19 @@ type Styles = Record<string, Style>;
|
||||
type Variables = Record<string, Variable>;
|
||||
type VariableConfig = Record<string, Variables>;
|
||||
|
||||
type I18nString = string | Record<string, string>
|
||||
|
||||
type FieldEntry = {
|
||||
type: 'placeholder' | 'text';
|
||||
label?: I18nString; // TODO i18n
|
||||
content?: string;
|
||||
}
|
||||
|
||||
type FieldConfig = {
|
||||
type PlaceholderFieldGroupConfig = {
|
||||
entries: Array<FieldEntry>;
|
||||
overflow: string | null;
|
||||
};
|
||||
|
||||
type PredefinedFieldGroupConfig = {};
|
||||
|
||||
type FieldGroupConfig = PlaceholderFieldGroupConfig | PredefinedFieldGroupConfig;
|
||||
|
||||
type LayoutData = {
|
||||
fields?: Record<string, FieldConfig>;
|
||||
fieldgroups: Record<string, FieldGroupConfig>;
|
||||
};
|
||||
|
||||
type Layout = {
|
||||
@@ -41,3 +66,4 @@ type Layout = {
|
||||
style?: string;
|
||||
layout?: LayoutData;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import Literal
|
||||
import enum
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.pdf import get_images, get_variables
|
||||
from .models import WalletLayout
|
||||
|
||||
|
||||
class WalletPlatform:
|
||||
identifier: str
|
||||
name: str
|
||||
|
||||
|
||||
class ApplePlatform(WalletPlatform):
|
||||
identifier = "apple"
|
||||
name = _("Apple")
|
||||
|
||||
|
||||
class GooglePlatform(WalletPlatform):
|
||||
identifier = "google"
|
||||
name = _("Google")
|
||||
|
||||
|
||||
class FieldType(enum.Enum):
|
||||
TEXT = "text"
|
||||
CODE = "qr"
|
||||
IMAGE = "image"
|
||||
PREDEFINED = "predefined"
|
||||
# TODO: POWERED_BY ?
|
||||
|
||||
class BaseField:
|
||||
type: str
|
||||
label: LazyI18nString
|
||||
content: str
|
||||
|
||||
def __init__(self, label: LazyI18nString, content: str):
|
||||
self.label = label
|
||||
self.content = content
|
||||
|
||||
def asdict(self):
|
||||
return {"type": self.type, "label": self.label.data, "content": self.content}
|
||||
|
||||
class PlaceholderField(BaseField):
|
||||
type = "placeholder"
|
||||
|
||||
class TextField(BaseField):
|
||||
type = "text"
|
||||
|
||||
@dataclass
|
||||
class FieldGroupDefinition:
|
||||
name: str
|
||||
identifier: str
|
||||
entry_type: FieldType
|
||||
min_entries: int | None = None
|
||||
max_entries: int | None = None
|
||||
|
||||
def asdict(self):
|
||||
return {
|
||||
"identifier": self.identifier,
|
||||
"name": self.name,
|
||||
"entry_type": self.entry_type.value,
|
||||
"min_entries": self.min_entries,
|
||||
"max_entries": self.max_entries,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlaceholderFieldGroup(FieldGroupDefinition):
|
||||
entry_type: FieldType = FieldType.TEXT
|
||||
default_entries: list[PlaceholderField | TextField] = field(default_factory=list) # TODO: TextField seems wrong here
|
||||
|
||||
def asdict(self):
|
||||
asdict = super().asdict()
|
||||
asdict["default_entries"] = [x.asdict() for x in self.default_entries]
|
||||
return asdict
|
||||
|
||||
|
||||
@dataclass
|
||||
class PredefinedFieldGroup(FieldGroupDefinition):
|
||||
entry_type: FieldType = FieldType.PREDEFINED
|
||||
min_entries = 0
|
||||
max_entries = 1
|
||||
|
||||
|
||||
class PassStyle:
|
||||
identifier: str # unique within platform
|
||||
name: str
|
||||
platform: Literal["apple"] | Literal["google"]
|
||||
fields: list[FieldGroupDefinition]
|
||||
# preview_image: str # TODO: preview
|
||||
|
||||
def asdict(self):
|
||||
return {
|
||||
"identifier": self.identifier,
|
||||
"name": self.name,
|
||||
"platform": self.platform,
|
||||
"fields": [x.asdict() for x in self.fields],
|
||||
}
|
||||
|
||||
|
||||
class AppleWalletEventTicket(PassStyle):
|
||||
identifier = "event_1"
|
||||
name = "Event Ticket Layout 1"
|
||||
platform = "apple"
|
||||
# order here limits in what order users can configure field "overspilling" (if too many fields are defined, where should the rest go) -> can only go down in the list
|
||||
# we evaluate the fields in this order, so they overspill in this order as well (fields from primary are appended to the overspilling field before fields from secondary are etc)
|
||||
fields = [
|
||||
PlaceholderFieldGroup(
|
||||
identifier="logo",
|
||||
name=_("Logo"),
|
||||
min_entries=1,
|
||||
max_entries=1,
|
||||
default_entries=[
|
||||
PlaceholderField(LazyI18nString("logo"), "event:image")
|
||||
],
|
||||
entry_type=FieldType.IMAGE,
|
||||
),
|
||||
PlaceholderFieldGroup(
|
||||
identifier="primary",
|
||||
name=_("Primary"),
|
||||
min_entries=1,
|
||||
max_entries=1,
|
||||
default_entries=[
|
||||
PlaceholderField(LazyI18nString("Ticket type"), "item")
|
||||
],
|
||||
),
|
||||
PlaceholderFieldGroup(
|
||||
identifier="secondary", name=_("Secondary"), max_entries=4
|
||||
), # TODO: validation of max field count if combined "Coupons, store cards, and generic passes with a square barcode can have a total of up to four secondary and auxiliary fields, combined."
|
||||
PlaceholderFieldGroup(
|
||||
identifier="headers", name=_("Header"), max_entries=3
|
||||
), # TODO: header image
|
||||
PlaceholderFieldGroup(
|
||||
identifier="auxillary", name=_("Auxillary"), max_entries=4
|
||||
),
|
||||
PlaceholderFieldGroup(identifier="back", name=_("Back")),
|
||||
]
|
||||
# preview_image = "apple/event_ticket.svg"
|
||||
|
||||
|
||||
class GoogleWalletEventTicket(PassStyle):
|
||||
identifier = "event"
|
||||
name = "Event Ticket"
|
||||
platform = "google"
|
||||
fields = [
|
||||
PredefinedFieldGroup(identifier="seating", name=_("Seating")),
|
||||
PlaceholderFieldGroup(
|
||||
identifier="qrcode", name=_("QR-Code"), entry_type=FieldType.CODE
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
AVAILABLE_PLATFORMS = {"apple": ApplePlatform, "google": GooglePlatform}
|
||||
AVAILABLE_STYLES = [AppleWalletEventTicket(), GoogleWalletEventTicket()]
|
||||
|
||||
|
||||
def get_platforms_with_styles():
|
||||
platforms_with_styles = {}
|
||||
for style in AVAILABLE_STYLES:
|
||||
platform = style.platform
|
||||
if platform not in platforms_with_styles:
|
||||
platforms_with_styles[platform] = {}
|
||||
platforms_with_styles[platform][style.identifier] = style
|
||||
return platforms_with_styles
|
||||
|
||||
|
||||
def get_platform_styles(platform):
|
||||
platform_styles = {}
|
||||
for style in AVAILABLE_STYLES:
|
||||
if style.platform == platform:
|
||||
platform_styles[style.identifier] = style
|
||||
return platform_styles
|
||||
|
||||
|
||||
def get_platforms():
|
||||
return AVAILABLE_PLATFORMS
|
||||
|
||||
|
||||
class PassLayout:
|
||||
style: PassStyle
|
||||
layout: dict
|
||||
|
||||
def __init__(self, style, layout):
|
||||
self.style = style
|
||||
self.layout = layout
|
||||
|
||||
def validate(self, event):
|
||||
self.validate_fields(event)
|
||||
|
||||
def validate_fields(self, event):
|
||||
placeholders = {"text": get_variables(event), "image": get_images(event)}
|
||||
|
||||
style_fields = self.style.fields
|
||||
if "fields" not in self.layout:
|
||||
raise ValidationError(_("Layout did not contain any fields"))
|
||||
layout_fields = self.layout["fields"]
|
||||
if not isinstance(layout_fields, dict):
|
||||
raise ValidationError(_("'fields' must be dict"))
|
||||
|
||||
for fieldgroup in style_fields:
|
||||
layout_field_data = layout_fields.get(fieldgroup.identifier, {})
|
||||
if fieldgroup.min_entries and fieldgroup.min_entries < len(
|
||||
layout_field_data.get('entries', [])
|
||||
):
|
||||
raise ValidationError(
|
||||
_("At least {min_entries} must be specified for {name}").format(
|
||||
min_entries=fieldgroup.min_entries, name=fieldgroup.name
|
||||
)
|
||||
)
|
||||
# TODO: move field validation to json schema
|
||||
for entry in layout_field_data.get('entries', []):
|
||||
if entry['type'] not in ('placeholder', fieldgroup.entry_type.value):
|
||||
raise ValidationError(_("Placeholder of wrong type \"{type}\" in {name}").format(type=entry['type'], name="fieldgroup.name"))
|
||||
if entry['type'] == 'placeholder' and entry['content'] not in placeholders[fieldgroup.entry_type.value]:
|
||||
raise ValidationError(_("Unknown placeholder {name}").format(name=entry['content']))
|
||||
17
src/pretix/plugins/wallet/styles/__init__.py
Normal file
17
src/pretix/plugins/wallet/styles/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from .apple import ApplePlatform, AppleWalletEventTicket
|
||||
from .google import GooglePlatform, GoogleWalletEventTicket
|
||||
from .base import PassLayout
|
||||
|
||||
AVAILABLE_PLATFORMS = {"apple": ApplePlatform, "google": GooglePlatform}
|
||||
AVAILABLE_STYLES = {
|
||||
"apple": [AppleWalletEventTicket()],
|
||||
"google": [
|
||||
GoogleWalletEventTicket()
|
||||
],
|
||||
}
|
||||
|
||||
AVAILABLE_STYLES_DICT = {
|
||||
plat: {s.identifier: s for s in styls} for plat, styls in AVAILABLE_STYLES.items()
|
||||
}
|
||||
|
||||
__all__ = ["AVAILABLE_PLATFORMS", "AVAILABLE_STYLES", "PassLayout"]
|
||||
66
src/pretix/plugins/wallet/styles/apple.py
Normal file
66
src/pretix/plugins/wallet/styles/apple.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from .base import (
|
||||
FieldEntry,
|
||||
FieldEntryContentType,
|
||||
FieldContentType,
|
||||
ImageFieldGroup,
|
||||
PlaceholderFieldGroup,
|
||||
TextFieldGroup,
|
||||
WalletPlatform,
|
||||
PassStyle,
|
||||
)
|
||||
from django.utils.translation import gettext as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
|
||||
class ApplePlatform(WalletPlatform):
|
||||
identifier = "apple"
|
||||
name = _("Apple")
|
||||
|
||||
|
||||
class AppleWalletStyle(PassStyle):
|
||||
platform = ApplePlatform
|
||||
|
||||
class AppleWalletEventTicket(AppleWalletStyle):
|
||||
identifier = "event_1"
|
||||
name = _("Event Ticket Layout 1")
|
||||
fieldgroups = [
|
||||
ImageFieldGroup(
|
||||
identifier="logo",
|
||||
name=_("Logo"),
|
||||
min_entries=1,
|
||||
max_entries=1,
|
||||
labels=False,
|
||||
default_entries=[
|
||||
FieldEntry(
|
||||
type=FieldEntryContentType.IMAGE,
|
||||
label=LazyI18nString("logo"),
|
||||
content="event:image",
|
||||
)
|
||||
],
|
||||
),
|
||||
TextFieldGroup(
|
||||
identifier="primary",
|
||||
name=_("Primary"),
|
||||
min_entries=1,
|
||||
max_entries=1,
|
||||
default_entries=[
|
||||
FieldEntry(
|
||||
type=FieldEntryContentType.PLACEHOLDER,
|
||||
label=LazyI18nString({"de": "Tickettyp", "en": "Ticket type"}),
|
||||
content="item",
|
||||
)
|
||||
], # TODO: support Lazyi18nproxy here
|
||||
description=_("These fields appear prominently featured on the pass.")
|
||||
),
|
||||
TextFieldGroup(
|
||||
identifier="secondary", name=_("Secondary"), max_entries=4
|
||||
), # TODO: validation of max field count if combined "Coupons, store cards, and generic passes with a square barcode can have a total of up to four secondary and auxiliary fields, combined."
|
||||
TextFieldGroup(
|
||||
identifier="headers", name=_("Header"), max_entries=3
|
||||
), # TODO: header image
|
||||
TextFieldGroup(identifier="auxillary", name=_("Auxillary"), max_entries=4),
|
||||
TextFieldGroup(identifier="back", name=_("Back")),
|
||||
]
|
||||
# preview_image = "apple/event_ticket.svg"
|
||||
|
||||
|
||||
269
src/pretix/plugins/wallet/styles/base.py
Normal file
269
src/pretix/plugins/wallet/styles/base.py
Normal file
@@ -0,0 +1,269 @@
|
||||
import enum
|
||||
from i18nfield.strings import LazyI18nString
|
||||
import jsonschema
|
||||
|
||||
class WalletPlatform:
|
||||
identifier: str
|
||||
name: str
|
||||
|
||||
|
||||
class FieldGroupType(enum.Enum):
|
||||
PLACEHOLDER = "placeholder"
|
||||
PREDEFINED = "predefined"
|
||||
|
||||
|
||||
class FieldGroup:
|
||||
type: FieldGroupType
|
||||
identifier: str
|
||||
name: str
|
||||
description: str
|
||||
required: bool = False
|
||||
|
||||
def __init__(self, identifier: str, name: str, description=None, required=False):
|
||||
self.identifier = identifier
|
||||
self.name = name
|
||||
self.required = required
|
||||
self.description = description or ""
|
||||
|
||||
def layout_schema(
|
||||
self,
|
||||
remaining_fields: list["FieldGroup"],
|
||||
context: dict,
|
||||
) -> dict:
|
||||
raise NotImplemented()
|
||||
|
||||
def asdict(self):
|
||||
return {
|
||||
"type": self.type.value,
|
||||
"identifier": self.identifier,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"required": self.required,
|
||||
}
|
||||
|
||||
|
||||
class FieldContentType(enum.Enum):
|
||||
IMAGE = "image"
|
||||
TEXT = "text"
|
||||
|
||||
|
||||
class FieldEntryContentType(enum.Enum):
|
||||
IMAGE = "image"
|
||||
TEXT = "text"
|
||||
PLACEHOLDER = "placeholder"
|
||||
|
||||
|
||||
class FieldEntry:
|
||||
type: FieldEntryContentType
|
||||
label: LazyI18nString | None
|
||||
content: str
|
||||
|
||||
def __init__(
|
||||
self, type: FieldEntryContentType, label: LazyI18nString | None, content: str
|
||||
):
|
||||
self.type = type
|
||||
self.label = label
|
||||
self.content = content
|
||||
|
||||
def asdict(self) -> dict:
|
||||
return {"type": self.type.value, "content": self.content, "label": self.label.data if self.label else None}
|
||||
|
||||
|
||||
class PredefinedFieldGroup(FieldGroup):
|
||||
type = FieldGroupType.PREDEFINED
|
||||
|
||||
def layout_schema(
|
||||
self,
|
||||
remaining_fields: list["FieldGroup"],
|
||||
context: dict,
|
||||
):
|
||||
return {
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
class PlaceholderFieldGroup(FieldGroup):
|
||||
type = FieldGroupType.PLACEHOLDER
|
||||
content_type: FieldContentType
|
||||
default_entries: list[FieldEntry]
|
||||
labels: bool
|
||||
min_entries: int | None
|
||||
max_entries: int | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
identifier: str,
|
||||
name: str,
|
||||
content_type: FieldContentType,
|
||||
description: str=None,
|
||||
required=False,
|
||||
default_entries=None,
|
||||
min_entries=None,
|
||||
max_entries=None,
|
||||
labels=True,
|
||||
):
|
||||
super().__init__(identifier, name, description, required)
|
||||
self.content_type = content_type
|
||||
self.default_entries = default_entries or []
|
||||
self.min_entries = min_entries
|
||||
self.max_entries = max_entries
|
||||
self.labels = labels
|
||||
|
||||
if self.required and (self.min_entries is None or self.min_entries < 1):
|
||||
self.min_entries = 1
|
||||
|
||||
def asdict(self):
|
||||
return {
|
||||
**super().asdict(),
|
||||
"content_type": self.content_type.value,
|
||||
"default_entries": [x.asdict() for x in self.default_entries],
|
||||
"labels": self.labels,
|
||||
"min_entries": self.min_entries,
|
||||
"max_entries": self.max_entries,
|
||||
}
|
||||
|
||||
def layout_schema(
|
||||
self,
|
||||
remaining_fields: list["FieldGroup"],
|
||||
context: dict,
|
||||
):
|
||||
placeholders = context.get("placeholders", {}).get(self.content_type.value, [])
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entries": self.entries_schema(placeholders=placeholders),
|
||||
"overflow": {
|
||||
"oneOf": [
|
||||
{"type": "null"},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
f.identifier
|
||||
for f in remaining_fields
|
||||
if isinstance(f, PlaceholderFieldGroup)
|
||||
and f.content_type == self.content_type
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
"required": ["entries"],
|
||||
}
|
||||
|
||||
def entries_schema(self, placeholders: list[str]):
|
||||
baseprops = {}
|
||||
if self.labels:
|
||||
baseprops["label"] = {"$ref": "#/$defs/I18nString"}
|
||||
|
||||
schema = {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
**baseprops,
|
||||
"type": {"const": "placeholder"},
|
||||
"content": {"enum": placeholders},
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
**baseprops,
|
||||
"type": {"const": self.content_type.value},
|
||||
"content": {"type": "string"},
|
||||
}
|
||||
},
|
||||
],
|
||||
"required": ["type", "content"],
|
||||
},
|
||||
}
|
||||
if self.labels:
|
||||
schema["items"]["required"].append("label")
|
||||
if self.min_entries is not None:
|
||||
schema["minItems"] = self.min_entries
|
||||
# max_entries is not enforced here, as the layout can have more fields than that (null-fields are removed, rest is overspilled)
|
||||
return schema
|
||||
|
||||
|
||||
|
||||
class TextFieldGroup(PlaceholderFieldGroup):
|
||||
content_type = FieldContentType.TEXT
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(content_type=self.content_type, **kwargs)
|
||||
|
||||
|
||||
class ImageFieldGroup(PlaceholderFieldGroup):
|
||||
content_type = FieldContentType.IMAGE
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(content_type=self.content_type, **kwargs)
|
||||
|
||||
|
||||
class PassStyle:
|
||||
platform: type[WalletPlatform]
|
||||
identifier: str # unique within platform
|
||||
name: str
|
||||
# order here limits in what order users can configure field "overspilling" (if too many fields are defined, where should the rest go) -> can only go down in the list
|
||||
# we evaluate the fields in this order, so they overspill in this order as well (fields from primary are appended to the overspilling field before fields from secondary are etc)
|
||||
|
||||
fieldgroups: list[FieldGroup]
|
||||
|
||||
def asdict(self):
|
||||
return {
|
||||
"platform": self.platform.identifier,
|
||||
"identifier": self.identifier,
|
||||
"name": self.name,
|
||||
"fieldgroups": [x.asdict() for x in self.fieldgroups],
|
||||
}
|
||||
|
||||
def layout_schema(self, context):
|
||||
schema = {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
# TODO: $id
|
||||
"title": self.name,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fieldgroups": {
|
||||
"description": "Layout Field Groups",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
group.identifier: group.layout_schema(
|
||||
context=context, remaining_fields=self.fieldgroups[i:]
|
||||
)
|
||||
for (i, group) in enumerate(self.fieldgroups)
|
||||
},
|
||||
"required": [
|
||||
group.identifier for group in self.fieldgroups if group.required
|
||||
],
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"I18nString": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "object", "additionalProperties": {"type": "string"}},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
if any(group.required for group in self.fieldgroups):
|
||||
schema["required"] = ["fieldgroups"]
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
class PassLayout:
|
||||
style: PassStyle
|
||||
layout: dict
|
||||
|
||||
def __init__(self, style, layout):
|
||||
self.style = style
|
||||
self.layout = layout
|
||||
|
||||
def validate(self, context):
|
||||
schema = self.style.layout_schema(context)
|
||||
try:
|
||||
jsonschema.validate(self.layout, schema)
|
||||
except jsonschema.ValidationError as e:
|
||||
raise ValidationError("Invalid layout: {}".format(str(e)))
|
||||
20
src/pretix/plugins/wallet/styles/google.py
Normal file
20
src/pretix/plugins/wallet/styles/google.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from .base import PassStyle, PredefinedFieldGroup, TextFieldGroup, WalletPlatform
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
class GooglePlatform(WalletPlatform):
|
||||
identifier = "google"
|
||||
name = _("Google")
|
||||
|
||||
|
||||
class GoogleWalletStyle(PassStyle):
|
||||
platform = GooglePlatform
|
||||
|
||||
|
||||
class GoogleWalletEventTicket(PassStyle):
|
||||
identifier = "event"
|
||||
name = "Event Ticket"
|
||||
platform = GooglePlatform
|
||||
fieldgroups = [
|
||||
PredefinedFieldGroup(identifier="seating", name=_("Seating")),
|
||||
TextFieldGroup(identifier="qrcode", name=_("QR-Code"), labels=False),
|
||||
]
|
||||
@@ -13,6 +13,7 @@
|
||||
<h1>{% trans "Edit layout" %} {{ object.name }} </h1>
|
||||
{{ styles|json_script:"styles" }}
|
||||
{{ variables|json_script:"variables" }}
|
||||
{{ locales|json_script:"locales" }}
|
||||
<div id="editor" data-layout-id="{{ object.pk }}"></div>
|
||||
{% vite_hmr %}
|
||||
{% vite_asset "src/pretix/plugins/wallet/static/pretixplugins/wallet/main.ts" %}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load escapejson %}
|
||||
{% load formset_tags %}
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in formset %}
|
||||
<div class="row formset-row" data-formset-form>
|
||||
{% bootstrap_form_errors form %}
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field form.entry layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<i class="fa fa-warning hidden" data-toggle="tooltip" title=""></i>
|
||||
|
||||
<button type="button" class="btn btn-default hidden" data-edit-value-map data-toggle="modal"
|
||||
data-target="#editValueMapModal" title="{% trans "Edit value mapping" %}">
|
||||
<i class="fa fa-edit"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
{% with form=formset.empty_form %}
|
||||
<div class="row formset-row" data-formset-form>
|
||||
{% bootstrap_form_errors form %}
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field form.entry layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<i class="fa fa-warning hidden" data-toggle="tooltip" title=""></i>
|
||||
|
||||
<button type="button" class="btn btn-default hidden" data-edit-value-map data-toggle="modal"
|
||||
data-target="#editValueMapModal" title="{% trans "Edit value mapping" %}">
|
||||
<i class="fa fa-edit"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add field" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
{% if external_fields %}
|
||||
{{ external_fields|json_script:external_fields_id }}
|
||||
{% endif %}
|
||||
@@ -5,14 +5,26 @@ from django import forms
|
||||
from django.http import Http404
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import (
|
||||
CreateView, DetailView, ListView
|
||||
)
|
||||
from django.views.generic import CreateView, DetailView, ListView
|
||||
from pretix.base.pdf import get_images, get_variables
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
|
||||
from django.conf import settings
|
||||
from .models import WalletLayout
|
||||
from .styles import get_platform_styles, get_platforms
|
||||
from .styles import AVAILABLE_STYLES, AVAILABLE_PLATFORMS
|
||||
|
||||
|
||||
def get_layout_variables(event):
|
||||
return {
|
||||
"text": {
|
||||
varname: {"label": var["label"], "editor_sample": var["editor_sample"]}
|
||||
for varname, var in get_variables(event).items()
|
||||
},
|
||||
"image": {
|
||||
varname: {"label": var["label"]}
|
||||
for varname, var in get_images(event).items()
|
||||
}
|
||||
| {"poweredby": {"label": _("pretix-Logo")}}, # TODO: image upload
|
||||
}
|
||||
|
||||
|
||||
# TODO: should this even be a list view?
|
||||
@@ -27,7 +39,7 @@ class LayoutListView(EventPermissionRequiredMixin, ListView):
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx["platforms"] = get_platforms()
|
||||
ctx["platforms"] = AVAILABLE_PLATFORMS
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -38,33 +50,28 @@ class LayoutEditorView(DetailView):
|
||||
pk_url_kwarg = "layout"
|
||||
|
||||
def get_platform_styles(self):
|
||||
if self.object.platform not in get_platforms():
|
||||
if self.object.platform not in AVAILABLE_STYLES:
|
||||
raise Http404(
|
||||
_("Unknown platform '{platform}'").format(platform=self.object.platform)
|
||||
)
|
||||
return get_platform_styles(self.object.platform)
|
||||
return AVAILABLE_STYLES[self.object.platform]
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["styles"] = {
|
||||
id: style.asdict() for id, style in self.get_platform_styles().items()
|
||||
}
|
||||
context["variables"] = {
|
||||
"text": {
|
||||
varname: {"label": var["label"], "editor_sample": var["editor_sample"]}
|
||||
for varname, var in get_variables(self.request.event).items()
|
||||
},
|
||||
"image": {
|
||||
varname: {"label": var['label']} for varname, var in get_images(self.request.event).items()
|
||||
} | {"poweredby": {"label": _("pretix-Logo")}} # TODO: image upload
|
||||
style.identifier: style.asdict() for style in self.get_platform_styles()
|
||||
}
|
||||
context["variables"] = get_layout_variables(self.request.event)
|
||||
context['locales'] = {l: dict(settings.LANGUAGES).get(l, l) for l in self.request.event.settings.get('locales')}
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class WalletLayoutCreateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = WalletLayout
|
||||
fields = ("name",)
|
||||
|
||||
|
||||
def __init__(self, *args, platform, event, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.platform = platform
|
||||
@@ -74,7 +81,8 @@ class WalletLayoutCreateForm(forms.ModelForm):
|
||||
self.instance.platform = self.platform
|
||||
self.instance.event = self.event
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
class LayoutCreateView(CreateView):
|
||||
template_name = "pretixplugins/wallet/create.html"
|
||||
form_class = WalletLayoutCreateForm
|
||||
@@ -82,19 +90,17 @@ class LayoutCreateView(CreateView):
|
||||
|
||||
@property
|
||||
def platform(self):
|
||||
platform = self.kwargs['platform']
|
||||
if platform not in get_platforms():
|
||||
raise Http404(
|
||||
_("Unknown platform '{platform}'").format(platform=platform)
|
||||
)
|
||||
platform = self.kwargs["platform"]
|
||||
if platform not in AVAILABLE_PLATFORMS:
|
||||
raise Http404(_("Unknown platform '{platform}'").format(platform=platform))
|
||||
return platform
|
||||
|
||||
|
||||
def get_form_kwargs(self) -> dict[str, Any]:
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['platform'] = self.platform
|
||||
kwargs['event'] = self.request.event
|
||||
kwargs["platform"] = self.platform
|
||||
kwargs["event"] = self.request.event
|
||||
return kwargs
|
||||
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse(
|
||||
"plugins:wallet:edit",
|
||||
@@ -103,4 +109,4 @@ class LayoutCreateView(CreateView):
|
||||
"event": self.request.event.slug,
|
||||
"layout": self.object.pk,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user