mirror of
https://github.com/pretix/pretix.git
synced 2026-05-18 17:24:03 +00:00
WIP
This commit is contained in:
0
src/pretix/plugins/wallet/serializer.py
Normal file
0
src/pretix/plugins/wallet/serializer.py
Normal file
@@ -1,37 +1,41 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import StyleSettings from './style-settings.vue'
|
import StyleSettings from './style-settings.vue'
|
||||||
|
import Select from './input/select.vue'
|
||||||
|
import Input from './input/input.vue'
|
||||||
|
|
||||||
const STYLES = JSON.parse(document.querySelector('#styles')?.textContent ?? '{}')
|
const gettext = (window as any).gettext
|
||||||
const VARIABLES = JSON.parse(document.querySelector('#variables')?.textContent ?? '{}')
|
|
||||||
const FORM_ERRORS = JSON.parse(document.querySelector('#form_errors')?.textContent ?? '{}')
|
|
||||||
const FORM_DATA = JSON.parse(document.querySelector('#form_data')?.textContent ?? '{}')
|
|
||||||
|
|
||||||
const style = ref<string | null>(FORM_DATA.style ?? null)
|
// TODO: Move to store?
|
||||||
const name = ref<string>(FORM_DATA.name ?? '')
|
const STYLES: Styles = JSON.parse(document.querySelector('#styles')?.textContent ?? '{}')
|
||||||
const layout = ref(JSON.parse(FORM_DATA.layout ?? '{}') ?? {})
|
const VARIABLES: VariableConfig = JSON.parse(document.querySelector('#variables')?.textContent ?? '{}')
|
||||||
|
const FORM_ERRORS: Record<string, Array<string>> = JSON.parse(document.querySelector('#form_errors')?.textContent ?? '{}')
|
||||||
|
const LAYOUT: Layout = JSON.parse(document.querySelector('#layout')?.textContent ?? '{}')
|
||||||
|
|
||||||
|
const name = ref<string>(LAYOUT.name ?? '')
|
||||||
|
const style = ref<string | null>(LAYOUT.style ?? null)
|
||||||
|
const layout = ref<LayoutData>(LAYOUT.layout ?? {fields: {}})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
// TODO: add :key for all `v-for`s
|
// TODO: add :key for all `v-for`s
|
||||||
//- pre
|
// TODO: i18n
|
||||||
//- code {{ STYLES }}
|
details
|
||||||
|
pre
|
||||||
|
code {{ FORM_ERRORS }}
|
||||||
.row
|
.row
|
||||||
.col-md-8
|
.col-md-8
|
||||||
|
// TODO: show error text
|
||||||
.form-group(:class='"name" in FORM_ERRORS ? "has-error" : ""')
|
.form-group(:class='"name" in FORM_ERRORS ? "has-error" : ""')
|
||||||
label.control-label(for="layout-info-name") Name
|
Input(label="Name" v-model="name" name="name" :errors="FORM_ERRORS['name']")
|
||||||
input#layout-info-name.form-control(v-model="name" name="name")
|
|
||||||
|
|
||||||
.form-group(:class='"style" in FORM_ERRORS ? "has-error" : ""')
|
.form-group(:class='"style" in FORM_ERRORS ? "has-error" : ""')
|
||||||
label.control-label(for="layout-info-style") Style
|
Select(label="Style" v-model="style" :choices="Object.values(STYLES).map(x => [x.identifier, x.name])" name="style" :errors="FORM_ERRORS['style']")
|
||||||
select#layout-info-style.form-control(v-model="style" name="style")
|
|
||||||
option(v-for="styleconfig in STYLES" :key="styleconfig.identifier" :value="styleconfig.identifier") {{ styleconfig.name }}
|
|
||||||
|
|
||||||
StyleSettings(v-model="layout" :style="style" :styles="STYLES" :variables="VARIABLES")
|
StyleSettings(v-if="style" v-model="layout" :style="style" :styles="STYLES" :variables="VARIABLES")
|
||||||
.col-md-4
|
.col-md-4
|
||||||
.panel.panel-default
|
.panel.panel-default
|
||||||
.panel-heading Preview
|
.panel-heading Preview
|
||||||
// TODO: i18n
|
|
||||||
.panel-body
|
.panel-body
|
||||||
// TODO: Preview
|
// TODO: Preview
|
||||||
pre
|
pre
|
||||||
|
|||||||
@@ -1,55 +1,60 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, reactive } from 'vue'
|
||||||
import OptionalSelect from './optional-select.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<{
|
const props = defineProps<{
|
||||||
field: any // TODO
|
field: FieldGroupDefinition
|
||||||
overflows: [string, string][],
|
overflows: FieldGroupDefinition[],
|
||||||
variables: any // TODO
|
variables: Variables
|
||||||
}>()
|
}>()
|
||||||
const fieldConfig = defineModel<any>({ required: true })
|
const fieldConfig = defineModel<FieldConfig>({ required: true })
|
||||||
|
|
||||||
const overflowOptions = computed(() => {
|
const overflowOptions = computed((): Array<[string|null, string]> => {
|
||||||
if (props.overflows.length) {
|
if (props.overflows.length) {
|
||||||
return [...props.overflows.map(x => [x.identifier, x.name]), [null, "Do not overflow"]]
|
return [...props.overflows.map((x): [string, string] => [x.identifier, x.name]), [null, "Do not overflow"]]
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
function addVariable() {
|
function addVariable() {
|
||||||
fieldConfig.value.entries.push({"label": null, "value": null})
|
fieldConfig.value.entries.push({"type": "placeholder", "label": null, "content": null})
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
//- pre
|
|
||||||
//- code {{ props }}
|
|
||||||
.panel.panel-default
|
.panel.panel-default
|
||||||
.panel-heading
|
.panel-heading
|
||||||
h3.panel-title {{ field.name }}
|
h3.panel-title {{ field.name }}
|
||||||
.panel-body
|
.panel-body
|
||||||
.form-group(v-if="props.variables[field.entry_type]")
|
.form-group
|
||||||
span.text-muted These fields appear somewhere and are visible too.
|
span.text-muted These fields appear somewhere and are visible too.
|
||||||
// TODO: for="..." / labeledby?
|
h4 {{ gettext("Content") }}
|
||||||
h4 Fields
|
|
||||||
.row.form-group(v-for="n in fieldConfig.entries.length")
|
.row.form-group(v-for="n in fieldConfig.entries.length")
|
||||||
.col-md-5
|
.col-md-5
|
||||||
// TODO: i18n
|
Input(:label="gettext('Label')" v-model="fieldConfig.entries[n-1].label")
|
||||||
label.control-label Label
|
.col-md-6(v-if='field.entry_type == "text"')
|
||||||
input.form-control(v-model="fieldConfig.entries[n-1].label")
|
TextContent(v-model="fieldConfig.entries[n-1]"
|
||||||
.col-md-6
|
:variables="props.variables")
|
||||||
label.control-label Value
|
.col-md-6(v-else-if='field.entry_type == "image"')
|
||||||
select.form-control(v-model="fieldConfig.entries[n-1].value")
|
Select(:label="gettext('Content')"
|
||||||
option(v-for="(config, id) in props.variables[field.entry_type]" :key="id" :value="id") {{ config.label }}
|
v-model="fieldConfig.entries[n-1].content"
|
||||||
|
:choices="Object.entries(props.variables).map(([k,v]) => [k, v.label])"
|
||||||
|
)
|
||||||
.col-md-1
|
.col-md-1
|
||||||
label.control-label
|
label.control-label
|
||||||
span.sr-only "Delete"
|
span.sr-only {{ gettext('Delete')}}
|
||||||
button.btn.btn-danger(type="button" @click="fieldConfig.entries.splice(n-1, 1)")
|
button.btn.btn-danger(type="button" @click="fieldConfig.entries.splice(n-1, 1)")
|
||||||
i.fa.fa-trash
|
i.fa.fa-trash
|
||||||
span.sr-only "Delete"
|
span.sr-only {{ gettext('Delete')}}
|
||||||
button.btn.btn-default(type="button" @click="addVariable")
|
button.btn.btn-default(type="button" @click="addVariable")
|
||||||
i.fa.fa-plus
|
i.fa.fa-plus
|
||||||
span.sr-only Add field
|
span.sr-only {{ gettext("Add field") }}
|
||||||
OptionalSelect(label="Overflow to ..." :choices="overflowOptions" v-model="fieldConfig.overflow")
|
Select(:label="gettext('Overflow to …')" :choices="overflowOptions" v-model="fieldConfig.overflow")
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useId } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
label?: string,
|
||||||
|
errors?: string[],
|
||||||
|
}>()
|
||||||
|
const modelValue = defineModel<string|null>();
|
||||||
|
const id = useId()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template lang="pug">
|
||||||
|
label.control-label(:for="id", v-if="props.label") {{ props.label }}
|
||||||
|
input.form-control(:id="id" v-model="modelValue" v-bind="$attrs")
|
||||||
|
.help-block(v-if="props.errors" v-for="error in props.errors") {{ error }}
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useId, watchEffect } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
label: string
|
||||||
|
choices: Array<[string, string]>
|
||||||
|
errors?: string[]
|
||||||
|
}>()
|
||||||
|
const modelValue = defineModel<string|null>();
|
||||||
|
const id = useId()
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (props.choices.length === 1) {
|
||||||
|
modelValue.value = props.choices[0][0]
|
||||||
|
} else if (props.choices.length < 1) {
|
||||||
|
modelValue.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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")
|
||||||
|
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>
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useId } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
label: string
|
|
||||||
choices: [string, string][]
|
|
||||||
}>()
|
|
||||||
const modelValue = defineModel<string|null>();
|
|
||||||
const id = useId()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template lang="pug">
|
|
||||||
template(v-if="choices.length >= 1")
|
|
||||||
label(:for="id") {{ props.label }}
|
|
||||||
select.form-control(:id="id" v-model="modelValue")
|
|
||||||
// TODO: persist/v-model
|
|
||||||
option(v-for="choice in props.choices" :key="choice[0]" :value="choice[0]") {{ choice[1] }}
|
|
||||||
</template>
|
|
||||||
@@ -1,45 +1,49 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, watchEffect } from 'vue'
|
import { computed, watchEffect } from "vue";
|
||||||
import OptionalSelect from './optional-select.vue'
|
import FieldSettings from "./field-settings.vue";
|
||||||
import FieldSettings from './field-settings.vue'
|
|
||||||
|
const gettext = (window as any).gettext;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
styles: any, // TODO
|
styles: Styles;
|
||||||
variables: any, // TODO
|
variables: VariableConfig
|
||||||
style?: string,
|
style?: string;
|
||||||
}>()
|
}>();
|
||||||
|
|
||||||
const layout = defineModel()
|
const layout = defineModel<LayoutData>();
|
||||||
|
|
||||||
const styleData = computed(() => {
|
const styleData = computed(() => {
|
||||||
if (!props.style || !(props.style in props.styles)) {
|
if (!props.style || !(props.style in props.styles)) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
return props.styles[props.style]
|
return props.styles[props.style];
|
||||||
})
|
});
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
// TODO: this seems wrooong
|
// TODO: this seems wrooong
|
||||||
if (!('fields' in layout.value)) {
|
if (!("fields" in layout.value)) {
|
||||||
layout.value.fields = {}
|
layout.value.fields = {};
|
||||||
}
|
}
|
||||||
if (props.style) {
|
if (props.style) {
|
||||||
for (const field of props.styles[props.style].fields) {
|
for (const field of props.styles[props.style].fields) {
|
||||||
if (!(field.identifier in layout.value.fields)) {
|
if (!(field.identifier in layout.value.fields)) {
|
||||||
layout.value.fields[field.identifier] = {entries: JSON.parse(JSON.stringify(field.default_entries)), overflow: null}
|
layout.value.fields[field.identifier] = {
|
||||||
|
entries: JSON.parse(JSON.stringify(field.default_entries)),
|
||||||
|
overflow: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
h2.h3 Form Fields
|
h2.h3 {{ gettext("Field Groups") }}
|
||||||
FieldSettings(v-if="styleData"
|
FieldSettings(v-if="styleData"
|
||||||
v-for="(field, fieldId) in styleData.fields"
|
v-for="(field, fieldId) in styleData.fields"
|
||||||
v-model="layout.fields[field.identifier]"
|
v-model="layout.fields[field.identifier]"
|
||||||
:field="field"
|
:field="field"
|
||||||
:overflows="styleData.fields.slice(fieldId + 1).filter(x => x.entry_type === field.entry_type)"
|
:overflows="styleData.fields.slice(fieldId + 1).filter(x => x.entry_type === field.entry_type)"
|
||||||
:variables="variables"
|
:variables="variables[field.entry_type]"
|
||||||
)
|
)
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive } from 'vue'
|
||||||
|
import Select from './input/select.vue'
|
||||||
|
import Input from './input/input.vue'
|
||||||
|
|
||||||
|
const gettext = (window as any).gettext
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
variables: Variables
|
||||||
|
}>()
|
||||||
|
const entry = defineModel<FieldEntry>({ required: true })
|
||||||
|
|
||||||
|
const selectChoices = computed(() =>{
|
||||||
|
const choices = Object.entries(props.variables).map(([k,v]): [string, string] => [k, v.label])
|
||||||
|
choices.push(["other", gettext("Other…")])
|
||||||
|
return choices
|
||||||
|
});
|
||||||
|
|
||||||
|
const selection = computed({
|
||||||
|
get() {
|
||||||
|
if (entry.value.type === 'placeholder') {
|
||||||
|
return entry.value.content
|
||||||
|
} else if (entry.value.type === 'text') {
|
||||||
|
return "other"
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown entry type "${entry.value.type}"`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set(newValue) {
|
||||||
|
if (newValue == "other") {
|
||||||
|
entry.value.type = "text"
|
||||||
|
entry.value.content = "";
|
||||||
|
} else {
|
||||||
|
entry.value.type = "placeholder"
|
||||||
|
entry.value.content = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const textContent = computed({
|
||||||
|
get() {
|
||||||
|
if (entry.value.type === 'placeholder') {
|
||||||
|
return ""
|
||||||
|
} else if (entry.value.type === 'text') {
|
||||||
|
return entry.value.content
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown entry type "${entry.value.type}"`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set(newValue) {
|
||||||
|
entry.value.content = newValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template lang="pug">
|
||||||
|
Select(:label="gettext('Content')"
|
||||||
|
v-model="selection"
|
||||||
|
:choices="selectChoices"
|
||||||
|
)
|
||||||
|
Input(v-model="textContent" v-if="selection === 'other'")
|
||||||
|
</template>
|
||||||
41
src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts
vendored
Normal file
41
src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
type FieldGroupDefinition = {
|
||||||
|
identifier: string;
|
||||||
|
entry_type: string;
|
||||||
|
name: string;
|
||||||
|
default_entries: FieldConfig[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Style = {
|
||||||
|
identifier: string;
|
||||||
|
name: string;
|
||||||
|
fields: FieldGroupDefinition[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Variable = {
|
||||||
|
label: string
|
||||||
|
};
|
||||||
|
|
||||||
|
type Styles = Record<string, Style>;
|
||||||
|
type Variables = Record<string, Variable>;
|
||||||
|
type VariableConfig = Record<string, Variables>;
|
||||||
|
|
||||||
|
type FieldEntry = {
|
||||||
|
type: 'placeholder' | 'text';
|
||||||
|
label: string; // TODO i18n
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldConfig = {
|
||||||
|
entries: Array<FieldEntry>;
|
||||||
|
overflow: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LayoutData = {
|
||||||
|
fields?: Record<string, FieldConfig>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Layout = {
|
||||||
|
name?: string;
|
||||||
|
style?: string;
|
||||||
|
layout?: LayoutData;
|
||||||
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './components/app.vue'
|
import App from './components/app.vue'
|
||||||
|
|
||||||
const app = createApp(App)
|
const mountEl = document.querySelector<HTMLElement>('#editor')!
|
||||||
app.mount('#editor')
|
const app = createApp(App, mountEl.dataset)
|
||||||
|
app.mount(mountEl)
|
||||||
|
|
||||||
app.config.errorHandler = (error, _vm, info) => {
|
app.config.errorHandler = (error, _vm, info) => {
|
||||||
// vue fatals on errors by default, which is a weird choice
|
// vue fatals on errors by default, which is a weird choice
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import enum
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
|
from pretix.base.pdf import get_images, get_variables
|
||||||
from .models import WalletLayout
|
from .models import WalletLayout
|
||||||
|
|
||||||
|
|
||||||
@@ -34,10 +36,10 @@ class PlaceholderFieldType(enum.Enum):
|
|||||||
class PlaceholderField:
|
class PlaceholderField:
|
||||||
type: PlaceholderFieldType
|
type: PlaceholderFieldType
|
||||||
label: LazyI18nString
|
label: LazyI18nString
|
||||||
value: str
|
content: str
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
return {"type": self.type.value, "label": self.label.data, "value": self.value}
|
return {"type": self.type.value, "label": self.label.data, "value": self.content}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -178,10 +180,12 @@ class PassLayout:
|
|||||||
self.style = style
|
self.style = style
|
||||||
self.layout = layout
|
self.layout = layout
|
||||||
|
|
||||||
def validate(self):
|
def validate(self, event):
|
||||||
self.validate_fields()
|
self.validate_fields(event)
|
||||||
|
|
||||||
|
def validate_fields(self, event):
|
||||||
|
placeholders = {"text": get_variables(event), "image": get_images(event)}
|
||||||
|
|
||||||
def validate_fields(self):
|
|
||||||
style_fields = self.style.fields
|
style_fields = self.style.fields
|
||||||
if "fields" not in self.layout:
|
if "fields" not in self.layout:
|
||||||
raise ValidationError(_("Layout did not contain any fields"))
|
raise ValidationError(_("Layout did not contain any fields"))
|
||||||
@@ -192,10 +196,16 @@ class PassLayout:
|
|||||||
for fieldgroup in style_fields:
|
for fieldgroup in style_fields:
|
||||||
layout_field_data = layout_fields.get(fieldgroup.identifier, {})
|
layout_field_data = layout_fields.get(fieldgroup.identifier, {})
|
||||||
if fieldgroup.min_entries and fieldgroup.min_entries < len(
|
if fieldgroup.min_entries and fieldgroup.min_entries < len(
|
||||||
layout_field_data.get('entries')
|
layout_field_data.get('entries', [])
|
||||||
):
|
):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("At least {min_entries} must be specified for {name}").format(
|
_("At least {min_entries} must be specified for {name}").format(
|
||||||
min_entries=fieldgroup.min_entries, name=fieldgroup.name
|
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']))
|
||||||
@@ -10,13 +10,14 @@
|
|||||||
{% block title %}{% trans "Wallet layouts" %}{% endblock %}
|
{% block title %}{% trans "Wallet layouts" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "Edit layout" %}</h1>
|
<h1>{% trans "Edit layout" %} {{ object.name }} </h1>
|
||||||
{{ styles|json_script:"styles" }}
|
{{ styles|json_script:"styles" }}
|
||||||
{{ variables|json_script:"variables" }}
|
{{ variables|json_script:"variables" }}
|
||||||
{{ form.errors|json_script:"form_errors" }}
|
{{ form.errors|json_script:"form_errors" }}
|
||||||
|
{{ layout|json_script:"layout" }}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div id="editor"></div>
|
<div id="editor">This is a test</div>
|
||||||
</form>
|
</form>
|
||||||
{% vite_hmr %}
|
{% vite_hmr %}
|
||||||
{% vite_asset "src/pretix/plugins/wallet/static/pretixplugins/wallet/main.ts" %}
|
{% vite_asset "src/pretix/plugins/wallet/static/pretixplugins/wallet/main.ts" %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from django.http import Http404
|
|||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.views.generic import FormView, ListView, CreateView, UpdateView
|
from django.views.generic import FormView, ListView, CreateView, UpdateView
|
||||||
from pretix.base.pdf import get_variables
|
from pretix.base.pdf import get_images, get_variables
|
||||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||||
from .styles import PassLayout, get_platform_styles, get_platforms
|
from .styles import PassLayout, get_platform_styles, get_platforms
|
||||||
from .models import WalletLayout
|
from .models import WalletLayout
|
||||||
@@ -14,7 +14,9 @@ from django import forms
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from i18nfield.fields import I18nCharField
|
from i18nfield.fields import I18nCharField
|
||||||
from i18nfield.forms import I18nModelForm
|
from i18nfield.forms import I18nModelForm
|
||||||
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.renderers import JSONRenderer
|
||||||
# TODO: should this even be a list view?
|
# TODO: should this even be a list view?
|
||||||
class LayoutListView(EventPermissionRequiredMixin, ListView):
|
class LayoutListView(EventPermissionRequiredMixin, ListView):
|
||||||
model = WalletLayout
|
model = WalletLayout
|
||||||
@@ -33,7 +35,7 @@ class LayoutListView(EventPermissionRequiredMixin, ListView):
|
|||||||
|
|
||||||
|
|
||||||
class LayoutEditForm(forms.ModelForm):
|
class LayoutEditForm(forms.ModelForm):
|
||||||
style = forms.TypedChoiceField()
|
style = forms.ChoiceField()
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.platform = kwargs.pop('platform')
|
self.platform = kwargs.pop('platform')
|
||||||
@@ -43,14 +45,14 @@ class LayoutEditForm(forms.ModelForm):
|
|||||||
model = WalletLayout
|
model = WalletLayout
|
||||||
fields = ("name","style","layout")
|
fields = ("name","style","layout")
|
||||||
|
|
||||||
def __init__(self, platform, **kwargs):
|
def __init__(self, event, platform, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
self.event = event
|
||||||
self.platform = platform
|
self.platform = platform
|
||||||
self.platform_styles = get_platform_styles(platform)
|
self.platform_styles = get_platform_styles(platform)
|
||||||
self.fields["style"].choices = [
|
self.fields["style"].choices = [
|
||||||
(id, style.name) for id, style in self.platform_styles.items()
|
(id, style.name) for id, style in self.platform_styles.items()
|
||||||
]
|
]
|
||||||
self.fields["style"].coerce = self.coerce_style
|
|
||||||
|
|
||||||
def coerce_style(self, value):
|
def coerce_style(self, value):
|
||||||
return self.platform_styles[value]
|
return self.platform_styles[value]
|
||||||
@@ -65,11 +67,28 @@ class LayoutEditForm(forms.ModelForm):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
if "style" in self.cleaned_data and "layout" in self.cleaned_data:
|
if "style" in self.cleaned_data and "layout" in self.cleaned_data:
|
||||||
layout = PassLayout(
|
layout = PassLayout(
|
||||||
style=self.cleaned_data["style"], layout=self.cleaned_data["layout"]
|
style=self.coerce_style(self.cleaned_data["style"]), layout=self.cleaned_data["layout"]
|
||||||
)
|
)
|
||||||
layout.validate()
|
layout.validate(self.event)
|
||||||
return self.cleaned_data
|
return self.cleaned_data
|
||||||
|
|
||||||
|
class LayoutSerializer(I18nAwareModelSerializer):
|
||||||
|
# # TODO: only necessary if we save through this serializer
|
||||||
|
# style = serializers.ChoiceField(choices={})
|
||||||
|
|
||||||
|
# def __init__(self, *args, platform, **kwargs):
|
||||||
|
# super().__init__(*args, **kwargs)
|
||||||
|
# self.platform = platform
|
||||||
|
# self.platform_styles = get_platform_styles(platform)
|
||||||
|
# self.fields["style"].choices = [
|
||||||
|
# (id, style.name) for id, style in self.platform_styles.items()
|
||||||
|
# ]
|
||||||
|
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WalletLayout
|
||||||
|
fields = ("name","style","layout")
|
||||||
|
|
||||||
class LayoutEditorView(EventPermissionRequiredMixin, UpdateView):
|
class LayoutEditorView(EventPermissionRequiredMixin, UpdateView):
|
||||||
template_name = "pretixplugins/wallet/edit.html"
|
template_name = "pretixplugins/wallet/edit.html"
|
||||||
form_class = LayoutEditForm
|
form_class = LayoutEditForm
|
||||||
@@ -83,6 +102,7 @@ class LayoutEditorView(EventPermissionRequiredMixin, UpdateView):
|
|||||||
|
|
||||||
def get_form_kwargs(self) -> dict[str, Any]:
|
def get_form_kwargs(self) -> dict[str, Any]:
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['event'] = self.request.event
|
||||||
kwargs["platform"] = self.platform
|
kwargs["platform"] = self.platform
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@@ -98,11 +118,25 @@ class LayoutEditorView(EventPermissionRequiredMixin, UpdateView):
|
|||||||
context["styles"] = {
|
context["styles"] = {
|
||||||
id: style.asdict() for id, style in self.get_platform_styles().items()
|
id: style.asdict() for id, style in self.get_platform_styles().items()
|
||||||
}
|
}
|
||||||
|
if self.request.method == "POST":
|
||||||
|
form = self.get_form()
|
||||||
|
if not form.is_valid():
|
||||||
|
layout_data = LayoutSerializer(self.object).data
|
||||||
|
layout_data.update(form.cleaned_data)
|
||||||
|
context['layout'] = layout_data
|
||||||
|
else:
|
||||||
|
context["layout"] = LayoutSerializer(self.object).data
|
||||||
|
else:
|
||||||
|
context["layout"] = LayoutSerializer(self.object).data
|
||||||
|
|
||||||
context["variables"] = {
|
context["variables"] = {
|
||||||
"text": {
|
"text": {
|
||||||
varname: {"label": var["label"], "editor_sample": var["editor_sample"]}
|
varname: {"label": var["label"], "editor_sample": var["editor_sample"]}
|
||||||
for varname, var in get_variables(self.request.event).items()
|
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
|
||||||
}
|
}
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user