mirror of
https://github.com/pretix/pretix.git
synced 2026-05-18 17:24:03 +00:00
WIP
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
"build": "npm run build:control -s && npm run build:widget -s",
|
||||
"build:control": "vite build",
|
||||
"build:widget": "vite build src/pretix/static/pretixpresale/widget",
|
||||
"lint:eslint": "eslint src/pretix/static/pretixpresale/widget src/pretix/static/pretixcontrol/js/ui/checkinrules src/pretix/plugins/webcheckin",
|
||||
"lint:eslint": "eslint src/pretix/static/pretixpresale/widget src/pretix/static/pretixcontrol/js/ui/checkinrules src/pretix/plugins/webcheckin src/pretix/plugins/wallet",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import StyleSettings from './style-settings.vue'
|
||||
|
||||
const STYLES = JSON.parse(document.querySelector('#styles')?.textContent ?? '{}')
|
||||
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)
|
||||
const name = ref<string>(FORM_DATA.name ?? '')
|
||||
const layout = ref(JSON.parse(FORM_DATA.layout ?? '{}') ?? {})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
// TODO: add :key for all `v-for`s
|
||||
//- pre
|
||||
//- code {{ STYLES }}
|
||||
.row
|
||||
.col-md-8
|
||||
.form-group(:class='"name" in FORM_ERRORS ? "has-error" : ""')
|
||||
label.control-label(for="layout-info-name") Name
|
||||
input#layout-info-name.form-control(v-model="name" name="name")
|
||||
|
||||
.form-group(:class='"style" in FORM_ERRORS ? "has-error" : ""')
|
||||
label.control-label(for="layout-info-style") 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")
|
||||
.col-md-4
|
||||
.panel.panel-default
|
||||
.panel-heading Preview
|
||||
// TODO: i18n
|
||||
.panel-body
|
||||
// TODO: Preview
|
||||
pre
|
||||
code {{ layout }}
|
||||
input(type="hidden" name="layout" :value="JSON.stringify(layout)")
|
||||
.form-group.submit-group
|
||||
button.btn.btn-primary.btn-save(type="submit") Submit
|
||||
</template>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import OptionalSelect from './optional-select.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
field: any // TODO
|
||||
overflows: [string, string][],
|
||||
variables: any // TODO
|
||||
}>()
|
||||
const fieldConfig = defineModel<any>({ required: true })
|
||||
|
||||
const overflowOptions = computed(() => {
|
||||
if (props.overflows.length) {
|
||||
return [...props.overflows.map(x => [x.identifier, x.name]), [null, "Do not overflow"]]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
function addVariable() {
|
||||
fieldConfig.value.entries.push({"label": null, "value": null})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
//- pre
|
||||
//- code {{ props }}
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
h3.panel-title {{ field.name }}
|
||||
.panel-body
|
||||
.form-group(v-if="props.variables[field.entry_type]")
|
||||
span.text-muted These fields appear somewhere and are visible too.
|
||||
// TODO: for="..." / labeledby?
|
||||
h4 Fields
|
||||
.row.form-group(v-for="n in fieldConfig.entries.length")
|
||||
.col-md-5
|
||||
// TODO: i18n
|
||||
label.control-label Label
|
||||
input.form-control(v-model="fieldConfig.entries[n-1].label")
|
||||
.col-md-6
|
||||
label.control-label Value
|
||||
select.form-control(v-model="fieldConfig.entries[n-1].value")
|
||||
option(v-for="(config, id) in props.variables[field.entry_type]" :key="id" :value="id") {{ config.label }}
|
||||
.col-md-1
|
||||
label.control-label
|
||||
span.sr-only "Delete"
|
||||
button.btn.btn-danger(type="button" @click="fieldConfig.entries.splice(n-1, 1)")
|
||||
i.fa.fa-trash
|
||||
span.sr-only "Delete"
|
||||
button.btn.btn-default(type="button" @click="addVariable")
|
||||
i.fa.fa-plus
|
||||
span.sr-only Add field
|
||||
OptionalSelect(label="Overflow to ..." :choices="overflowOptions" v-model="fieldConfig.overflow")
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<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>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watchEffect } from 'vue'
|
||||
import OptionalSelect from './optional-select.vue'
|
||||
import FieldSettings from './field-settings.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
styles: any, // TODO
|
||||
variables: any, // TODO
|
||||
style?: string,
|
||||
}>()
|
||||
|
||||
const layout = defineModel()
|
||||
|
||||
const styleData = computed(() => {
|
||||
if (!props.style || !(props.style in props.styles)) {
|
||||
return null
|
||||
}
|
||||
return props.styles[props.style]
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
// TODO: this seems wrooong
|
||||
if (!('fields' in layout.value)) {
|
||||
layout.value.fields = {}
|
||||
}
|
||||
if (props.style) {
|
||||
for (const field of props.styles[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}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
h2.h3 Form Fields
|
||||
FieldSettings(v-if="styleData"
|
||||
v-for="(field, fieldId) in styleData.fields"
|
||||
v-model="layout.fields[field.identifier]"
|
||||
:field="field"
|
||||
:overflows="styleData.fields.slice(fieldId+1).filter(x => x.entry_type === field.entry_type)"
|
||||
:variables="variables"
|
||||
)
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './components/app.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
app.mount('#editor')
|
||||
|
||||
app.config.errorHandler = (error, _vm, info) => {
|
||||
// vue fatals on errors by default, which is a weird choice
|
||||
// https://github.com/vuejs/core/issues/3525
|
||||
// https://github.com/vuejs/router/discussions/2435
|
||||
console.error('[VUE]', info, error)
|
||||
}
|
||||
@@ -6,18 +6,22 @@ from django.core.exceptions import ValidationError
|
||||
from i18nfield.strings import LazyI18nString
|
||||
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 PlaceholderFieldType(enum.Enum):
|
||||
TEXT = "text"
|
||||
CODE = "qr"
|
||||
@@ -30,10 +34,11 @@ class PlaceholderFieldType(enum.Enum):
|
||||
class PlaceholderField:
|
||||
type: PlaceholderFieldType
|
||||
label: LazyI18nString
|
||||
value: LazyI18nString
|
||||
value: str
|
||||
|
||||
def asdict(self):
|
||||
return {'type': self.type.value, 'label': self.label, 'value': self.value}
|
||||
return {"type": self.type.value, "label": self.label.data, "value": self.value}
|
||||
|
||||
|
||||
@dataclass
|
||||
class FieldGroupDefinition:
|
||||
@@ -44,7 +49,13 @@ class FieldGroupDefinition:
|
||||
max_entries: int | None = None
|
||||
|
||||
def asdict(self):
|
||||
return {"identifier": self.identifier, "name": self.name, "min_entries": self.min_entries, "max_entries": self.max_entries}
|
||||
return {
|
||||
"identifier": self.identifier,
|
||||
"name": self.name,
|
||||
"entry_type": self.entry_type.value,
|
||||
"min_entries": self.min_entries,
|
||||
"max_entries": self.max_entries,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -54,7 +65,7 @@ class PlaceholderFieldGroup(FieldGroupDefinition):
|
||||
|
||||
def asdict(self):
|
||||
asdict = super().asdict()
|
||||
asdict['default_entries'] = [x.asdict() for x in self.default_entries]
|
||||
asdict["default_entries"] = [x.asdict() for x in self.default_entries]
|
||||
return asdict
|
||||
|
||||
|
||||
@@ -67,7 +78,7 @@ class PredefinedFieldGroup(FieldGroupDefinition):
|
||||
|
||||
class PassStyle:
|
||||
identifier: str # unique within platform
|
||||
name: str
|
||||
name: str
|
||||
platform: Literal["apple"] | Literal["google"]
|
||||
fields: list[FieldGroupDefinition]
|
||||
# preview_image: str # TODO: preview
|
||||
@@ -94,18 +105,28 @@ class AppleWalletEventTicket(PassStyle):
|
||||
min_entries=1,
|
||||
max_entries=1,
|
||||
default_entries=[
|
||||
PlaceholderField(PlaceholderFieldType.IMAGE, "logo", "event:image")
|
||||
PlaceholderField(PlaceholderFieldType.IMAGE, LazyI18nString("logo"), "event:image")
|
||||
],
|
||||
entry_type=PlaceholderFieldType.IMAGE,
|
||||
),
|
||||
PlaceholderFieldGroup(identifier="primary", name=_("Primary"), min_entries=1, max_entries=1),
|
||||
PlaceholderFieldGroup(
|
||||
identifier="primary",
|
||||
name=_("Primary"),
|
||||
min_entries=1,
|
||||
max_entries=1,
|
||||
default_entries=[
|
||||
PlaceholderField(PlaceholderFieldType.TEXT, 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="auxillary", name=_("Auxillary"), max_entries=4
|
||||
),
|
||||
PlaceholderFieldGroup(identifier="back", name=_("Back")),
|
||||
]
|
||||
# preview_image = "apple/event_ticket.svg"
|
||||
@@ -117,13 +138,16 @@ class GoogleWalletEventTicket(PassStyle):
|
||||
platform = "google"
|
||||
fields = [
|
||||
PredefinedFieldGroup(identifier="seating", name=_("Seating")),
|
||||
PlaceholderFieldGroup(identifier="qrcode", name=_("QR-Code"), entry_type=PlaceholderFieldType.CODE),
|
||||
PlaceholderFieldGroup(
|
||||
identifier="qrcode", name=_("QR-Code"), entry_type=PlaceholderFieldType.CODE
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
AVAILABLE_PLATFORMS = {"apple": ApplePlatform, "google": GooglePlatform}
|
||||
AVAILABLE_STYLES = [AppleWalletEventTicket(), GoogleWalletEventTicket()]
|
||||
|
||||
|
||||
def get_platforms_with_styles():
|
||||
platforms_with_styles = {}
|
||||
for style in AVAILABLE_STYLES:
|
||||
@@ -133,6 +157,7 @@ def get_platforms_with_styles():
|
||||
platforms_with_styles[platform][style.identifier] = style
|
||||
return platforms_with_styles
|
||||
|
||||
|
||||
def get_platform_styles(platform):
|
||||
platform_styles = {}
|
||||
for style in AVAILABLE_STYLES:
|
||||
@@ -140,9 +165,11 @@ def get_platform_styles(platform):
|
||||
platform_styles[style.identifier] = style
|
||||
return platform_styles
|
||||
|
||||
|
||||
def get_platforms():
|
||||
return AVAILABLE_PLATFORMS
|
||||
|
||||
|
||||
class PassLayout:
|
||||
style: PassStyle
|
||||
layout: dict
|
||||
@@ -156,15 +183,19 @@ class PassLayout:
|
||||
|
||||
def validate_fields(self):
|
||||
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"))
|
||||
layout_fields = self.layout['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):
|
||||
raise ValidationError(_("At least {min_entries} must be specified for {name}").format(min_entries=fieldgroup.min_entries, name=fieldgroup.name))
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2,18 +2,23 @@
|
||||
{% load i18n %}
|
||||
{% load money %}
|
||||
{% load bootstrap3 %}
|
||||
{% load vite %}
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
|
||||
|
||||
{% block title %}{% trans "Wallet layouts" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<pre><code>{{ styles }}</code></pre>
|
||||
<pre><code>{{ variables }}</code></pre>
|
||||
<h1>{% trans "Edit layout" %}</h1>
|
||||
{{ styles|json_script:"styles" }}
|
||||
{{ variables|json_script:"variables" }}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<button type="submit" class="btn btn-default">
|
||||
{% trans "Submit " %}
|
||||
</button>
|
||||
</form>
|
||||
{{ form.errors|json_script:"form_errors" }}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div id="editor"></div>
|
||||
</form>
|
||||
{% vite_hmr %}
|
||||
{% vite_asset "src/pretix/plugins/wallet/static/pretixplugins/wallet/main.ts" %}
|
||||
{% csrf_token %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
{% 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 %}
|
||||
10
src/pretix/plugins/wallet/templatetags/wallet.py
Normal file
10
src/pretix/plugins/wallet/templatetags/wallet.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django import template
|
||||
|
||||
from ..models import WalletLayout
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def platform_layouts(platform, event):
|
||||
return WalletLayout.objects.filter(event=event, platform=platform.identifier)
|
||||
Reference in New Issue
Block a user