prettify & cleanup preview

support multiline fields in preview

add venue

store active state for predefined fieldgroups
This commit is contained in:
Kara Engelhardt
2026-08-11 11:27:17 +02:00
parent 4e02edc7d6
commit a82d3178e2
11 changed files with 162 additions and 172 deletions
@@ -28,7 +28,7 @@ function openForm(url: string, data: Record<string, string>) {
document.body.removeChild(form);
}
function openPreview(e: SubmitEvent) {
function openPreview(e: Event) {
e.preventDefault();
openForm("../../preview/", {
csrfmiddlewaretoken: store.csrfToken,
@@ -78,7 +78,7 @@ const preview_layout = computed(() => {
.panel-body
div(v-if="preview_layout")
span.text-muted The preview below is only a rough representation of what the pass might look like. Please check the generated pass.
div
div(style="display: grid; gap: 1em; grid-template-columns: repeat(auto-fit, minmax(auto, 360px));")
PassPreview(v-for="layout in preview_layout" :layout="layout")
div(v-else) Preview not supported
//- pre
@@ -98,6 +98,7 @@ const preview_layout = computed(() => {
.walletsettings-panel .panel-heading {
.checkbox {
padding: 0;
margin: 0;
display: inline-block;
input[type=checkbox] {
margin-top: 0;
@@ -106,6 +107,9 @@ const preview_layout = computed(() => {
position: relative;
top: 1px;
}
label {
padding-left: 0;
}
}
}
</style>
@@ -22,7 +22,7 @@ watchEffect(() => {
.panel-heading
h3.panel-title.form-inline
.form-group
Checkbox(:label="fieldgroup.name" v-model="fieldConfig.active")
Checkbox(v-if="!!fieldConfig" :label="fieldgroup.name" v-model="fieldConfig.active")
.panel-body(v-if="!!fieldgroup.description")
.form-group
span.text-muted {{ fieldgroup.description }}
@@ -0,0 +1,25 @@
<script setup lang="ts">
import { computed, inject, reactive, watchEffect } from "vue";
import { StoreKey } from "../../walletStore";
import { i18nstringLocalize } from "../../helpers";
const store = inject(StoreKey)!;
const props = defineProps<{
label: I18nString;
content: I18nString;
content_type: FieldContentType;
display: FieldGroupDisplay;
display_class?: string | string[];
}>();
</script>
<template lang="pug">
div.fieldgroup-item
div.fieldgroup-item-qrcode(v-if="display == 'code'") {{ label }}
template(v-else)
div.fieldgroup-label.nowrap(v-if="display == 'with_label'") {{ i18nstringLocalize(label) }}
div.nowrap.content(v-if="content_type == 'text'" :class="display_class") {{ i18nstringLocalize(content) }}
img.fieldgroup-item-image(v-else-if="content_type == 'image' && content" :src="i18nstringLocalize(content)")
</template>
@@ -22,22 +22,4 @@ const style_def = computed(() => {
<template lang="pug">
PlaceholderFieldgroupPreview(v-if="style_def && style_def.type == 'placeholder'" :config="config" :style_def="style_def")
PredefinedFieldgroupPreview(v-else-if="style_def && style_def.type == 'predefined'" :config="config" :style_def="style_def")
//- // TODO: support predefined group
//- div(:style="{'flex-grow': config.relSize}")
//- div(style="background-color: gray; display: flex; flex-direction: row;")
//- div(v-for="entry in layout.entries.slice(0,style_def.max_entries)")
//- div(v-if="style_def.labels") {{ i18nstringLocalize(entry.label) }}
//- div(v-if="entry.type == 'custom'") {{ i18nstringLocalize(entry.content) }}
//- div(v-if="entry.type == 'placeholder'") {{ store.variables[style_def.content_type][entry.content] }}
//- pre
//- //- code {{ config }}
//- code {{ style_def }}
//- code {{ layout }}
//- div(style="position: absolute; background-color: red; top: 0.5cm; left: 0cm; height: 1cm; width: 2cm;") Logo
//- div(style="position: absolute; background-color: red; top: 0.5cm; left: 2.25cm; height: 1cm; width: 5.5cm;") Logo Text
//- div(style="position: absolute; background-color: red; top: 0.5cm; left: 8cm; height: 1cm; width: 2cm;") Header
//- div(style="position: absolute; background-color: red; top: 2.5cm; left: 0cm; height: 2cm; width: 10cm;") Primary
//- div(style="position: absolute; background-color: red; top: 4.75cm; left: 0cm; height: 1cm; width: 10cm;") Secondary
//- div(style="position: absolute; background-color: red; top: 7cm; left: 0cm; height: 1cm; width: 10cm;") Auxiliary
//- div(style="position: absolute; background-color: red; top: 10cm; left: 3cm; height: 4cm; width: 4cm;")
</template>
@@ -16,19 +16,18 @@ const props = defineProps<{ layout: Array<PreviewLayout> }>();
<style lang="css" scoped>
.pass-container {
margin: 1em;
width: 100%;
aspect-ratio: 9 / 11;
max-width: 360px;
vertical-align: top;
display: inline-block;
outline: solid 1px #949494;
border: solid 1px #949494;
border-radius: 1em;
padding: 1em;
overflow: hidden
}
.pass-content {
overflow: scroll;
display: flex;
@@ -36,3 +35,58 @@ const props = defineProps<{ layout: Array<PreviewLayout> }>();
gap: 1em;
}
</style>
<style lang="css">
.pass-container {
.fieldgroup-container {
min-width: 0;
display: flex;
gap: 0.5em;
min-height: 2lh;
}
.fieldgroup-label {
font-weight: bold;
font-size: 0.8em;
}
.fieldgroup-item {
flex: 0 1 100%;
min-width: 0;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
white-space: pre-wrap;
}
.fieldgroup-item-image {
max-height: 3lh;
}
.fieldgroup-item-qrcode {
font-weight: bold;
background-color: lightgray;
width: 50%;
aspect-ratio: 1;
margin: auto;
/* center content */
display: flex;
align-items: center;
text-align: center;
justify-content: center;
padding: 1em;
overflow-wrap: anywhere;
}
.nowrap {
text-overflow: ellipsis;
overflow: hidden;
text-wrap: nowrap;
}
.bold {
font-weight: bold;
}
.large {
font-size: 1.5em;
}
.tight {
line-height: 1;
}
}
</style>
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, inject, reactive, watchEffect } from "vue";
import { StoreKey } from "../../walletStore";
import { i18nstringLocalize } from "../../helpers";
import FieldgroupItemPreview from "./fieldgroup-item-preview.vue";
const store = inject(StoreKey)!;
@@ -13,65 +13,5 @@ const props = defineProps<{
<template lang="pug">
div.fieldgroup-container(:style="{'flex-grow': config.relSize, 'flex-direction': config.direction || 'row'}")
div.fieldgroup-item(v-for="{ entry, label, content } of store.currentLayoutFieldContent[config.fieldgroup]")
div.fieldgroup-item-qrcode(v-if="style_def.display == 'code'") {{ label }}
template(v-else)
div.fieldgroup-label.nowrap(v-if="style_def.display == 'with_label'") {{ label }}
div.nowrap.content(v-if="style_def.content_type == 'text'" :class="config.display") {{ content }}
img.fieldgroup-item-image(v-else-if="style_def.content_type == 'image' && content" :src="i18nstringLocalize(content)")
FieldgroupItemPreview(v-for="{ label, content } of store.currentLayoutFieldContent[config.fieldgroup]" :label="label" :content="content" :content_type="style_def.content_type" :display_class="config.display" :display="style_def.display")
</template>
<style lang="css" scoped>
.fieldgroup-container {
min-width: 0;
display: flex;
gap: 0.5em;
min-height: 2lh;
}
.fieldgroup-label {
font-weight: bold;
font-size: 0.8em;
}
.fieldgroup-item {
flex: 0 1 100%;
min-width: 0;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.fieldgroup-item-image {
max-height: 3lh;
}
.fieldgroup-item-qrcode {
font-weight: bold;
background-color: lightgray;
width: 50%;
aspect-ratio: 1;
margin: auto;
/* center content */
display: flex;
align-items: center;
text-align: center;
justify-content: center;
padding: 1em;
overflow-wrap: anywhere;
}
.nowrap {
text-overflow: ellipsis;
overflow: hidden;
text-wrap: nowrap;
}
.bold {
font-weight: bold;
}
.large {
font-size: 1.5em;
}
.tight {
line-height: 1;
}
</style>
@@ -1,13 +1,13 @@
<script setup lang="ts">
import { computed, inject, reactive, watchEffect } from "vue";
import { StoreKey } from "../../walletStore";
import { i18nstringLocalize } from "../../helpers";
import FieldgroupItemPreview from "./fieldgroup-item-preview.vue";
const store = inject(StoreKey)!;
const props = defineProps<{
config: PredefinedFieldGroupConfig;
style_def: PredefinedFieldGroupDefinition;
config: PredefinedFieldgroupPreview;
}>();
const isActive = computed(
@@ -18,61 +18,5 @@ const isActive = computed(
<template lang="pug">
div.fieldgroup-container(v-if="isActive" :style="{'flex-grow': config.relSize, 'flex-direction': config.direction || 'row'}")
div.fieldgroup-item(v-for="{ label, content } of config.sample")
div.fieldgroup-label.nowrap(v-if="!!label") {{ i18nstringLocalize(label) }}
div.nowrap.content(v-if="!!content" :class="config.display") {{ i18nstringLocalize(content) }}
</template>
<style lang="css" scoped>
.fieldgroup-container {
min-width: 0;
display: flex;
gap: 0.5em;
min-height: 2lh;
}
.fieldgroup-label {
font-weight: bold;
font-size: 0.8em;
}
.fieldgroup-item {
flex: 0 1 100%;
min-width: 0;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.fieldgroup-item-image {
max-height: 3lh;
}
.fieldgroup-item-qrcode {
font-weight: bold;
background-color: lightgray;
width: 50%;
aspect-ratio: 1;
margin: auto;
/* center content */
display: flex;
align-items: center;
text-align: center;
padding: 1em;
overflow-wrap: anywhere;
}
.nowrap {
text-overflow: ellipsis;
overflow: hidden;
text-wrap: nowrap;
}
.bold {
font-weight: bold;
}
.large {
font-size: 1.5em;
}
.tight {
line-height: 1;
}
</style>
FieldgroupItemPreview(v-for="{ label, content } of config.sample" :label="label" :content="content" content_type="text" display="with_label")
</template>
@@ -3,7 +3,7 @@ import FieldgroupPreview from './fieldgroup-preview.vue';
import FixedPreview from './fixed-preview.vue';
const props = defineProps<{
config: Record<string, string>;
config: PreviewRow;
}>();
</script>
@@ -19,7 +19,7 @@ type PlaceholderFieldGroupDefinition = BaseFieldGroupDefinition & {
display: FieldGroupDisplay;
min_entries: number | null;
max_entries: number | null;
context_args: string[]
context_args: string[];
};
type PredefinedFieldGroupDefinition = BaseFieldGroupDefinition & {
@@ -104,10 +104,32 @@ type WalletStore = {
};
type PreviewLayout = Array<PreviewRow>;
type PreviewRow = { children: Array<PreviewRow> } | PreviewFieldgroup;
type PreviewFieldgroup = {
fieldgroup: string;
type PreviewRow =
| { children: Array<PreviewRow>; direction?: "row" | "column"; display?: Array<string>;}
| PreviewFieldgroup
| FixedPreview;
type PreviewProps = {
relSize?: number;
direction?: 'row' | 'column',
display?: Array<string>
};
direction?: "row" | "column";
display?: Array<string>;
}
type PreviewFieldgroup = PredefinedFieldgroupPreview | PlaceholderFieldGroupPreview;
type FixedPreview = {
value: I18nString;
} & PreviewProps
type PlaceholderFieldGroupPreview = {
fieldgroup: string;
} & PreviewProps;
type PredefinedFieldgroupPreview = {
fieldgroup: string;
sample: PreviewSample[];
} & PreviewProps;
type PreviewSample = {
content: I18nString;
label: I18nString;
}
+10 -7
View File
@@ -1,6 +1,5 @@
import enum
from typing import Any, TypedDict
from black.nodes import is_vararg
from typing import TypedDict
from i18nfield.strings import LazyI18nString
import jsonschema
from django.core.exceptions import ValidationError
@@ -46,7 +45,7 @@ class FieldGroup:
remaining_fields: list["FieldGroup"],
context: LayoutContext,
) -> dict:
raise NotImplemented()
raise NotImplementedError()
def asdict(self):
return {
@@ -119,7 +118,7 @@ class PredefinedFieldGroup(FieldGroup):
remaining_fields: list["FieldGroup"],
context: LayoutContext,
):
return {"type": "object"}
return {"type": "object", "properties": {"active": {"type": "boolean"}}}
class PlaceholderFieldGroup(FieldGroup):
@@ -165,7 +164,7 @@ class PlaceholderFieldGroup(FieldGroup):
"display": self.display.value,
"min_entries": self.min_entries,
"max_entries": self.max_entries,
"context_args": list(sorted(self.context_args))
"context_args": list(sorted(self.context_args)),
}
def layout_schema(
@@ -257,8 +256,9 @@ class ImageFieldGroup(PlaceholderFieldGroup):
class PassStyle:
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)
# 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
fieldgroups: list[FieldGroup]
@property
@@ -379,5 +379,8 @@ class PassStyle:
raise ValueError("Unknown field group")
return fields
def group_is_active(self, identifier: str):
return self.layout['fieldgroups'].get(identifier, {}).get("active", False)
def generate(self, op: OrderPosition):
raise NotImplementedError()
+25 -9
View File
@@ -1,5 +1,3 @@
from i18nfield.fields import LazyI18nString
from pretix.base.models import Event, OrderPosition
from .base import (
@@ -12,7 +10,6 @@ from .base import (
WalletPlatform,
)
from django.utils.translation import gettext as _
import json
from walletobjects import ButtonJWT, EventTicketClass, EventTicketObject
from walletobjects.comms import Comms
@@ -207,7 +204,12 @@ class GoogleWalletStyle(PassStyle):
)
)
return 'googlepaypass', 'text/uri-list', 'https://pay.google.com/gp/v/save/%s' % generated_jwt
return (
"googlepaypass",
"text/uri-list",
"https://pay.google.com/gp/v/save/%s" % generated_jwt,
)
class GoogleWalletEventTicket(GoogleWalletStyle):
identifier = "event"
@@ -266,7 +268,7 @@ class GoogleWalletEventTicket(GoogleWalletStyle):
{"value": str(self.event.name), "display": "large"},
],
"direction": "column",
"display": ["tight"]
"display": ["tight"],
},
{
"fieldgroup": "date",
@@ -283,7 +285,15 @@ class GoogleWalletEventTicket(GoogleWalletStyle):
],
},
{"fieldgroup": "code"},
]
],
[
{
"fieldgroup": "venue",
"sample": [
{"content": self.venue()[1], "label": self.venue()[0]},
],
},
],
]
def venue(self):
@@ -291,17 +301,23 @@ class GoogleWalletEventTicket(GoogleWalletStyle):
name = {}
address = {}
for key, value in self.event.location.data.items():
for key, value in self.event.location.data.items():
lines = value.splitlines()
name[key] = lines[0]
# We must provide at least one address line each for the name and address - no way around it.
if len(lines) > 1:
address[key] = '\n'.join(value.splitlines()[1:])
address[key] = "\n".join(value.splitlines()[1:])
else:
address[key] = lines[0]
return name, address
return None, None
return "", ""
def _generate_class(self):
output_class = super()._generate_class()
if self.group_is_active("venue") and all(self.venue()):
output_class.venue(*self.venue())
return output_class
def _generate_object(self, op: OrderPosition, class_id: str):
output_object = super()._generate_object(op, class_id)