This commit is contained in:
Kara Engelhardt
2026-07-28 11:30:50 +02:00
parent 02934e7ba0
commit 802a7b3a24
7 changed files with 446 additions and 4 deletions
@@ -0,0 +1,36 @@
<script setup lang="ts">
import { computed, inject, reactive, watchEffect } from "vue";
import { StoreKey } from "../../walletStore";
import PlaceholderFieldgroupPreview from "./placeholder-fieldgroup-preview.vue";
const store = inject(StoreKey)!
const props = defineProps<{
config: Record<string, string>;
}>();
const style_def = Object.fromEntries(store.currentPlatformStyles[store.currentPlatformLayout.style].fieldgroups.map(x => [x.identifier, x]))[props.config.fieldgroup]
</script>
<template lang="pug">
PlaceholderFieldgroupPreview(v-if="style_def.type == 'placeholder'" :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>
@@ -0,0 +1,38 @@
<script setup lang="ts">
import { inject } from "vue";
import { StoreKey } from "../../walletStore.js";
import RowPreview from "./row-preview.vue";
const store = inject(StoreKey)!;
const props = defineProps<{ layout: Array<PreviewLayout> }>();
</script>
<template lang="pug">
div.pass-container
div.pass-content
RowPreview(v-for="row in layout" :config="row")
</template>
<style lang="css" scoped>
.pass-container {
margin: 1em;
width: 100%;
aspect-ratio: 9 / 11;
max-width: 360px;
max-height: 440px;
vertical-align: top;
display: inline-block;
outline: solid 1px #949494;
border-radius: 1em;
padding: 1em;
}
.pass-content {
overflow: scroll;
display: flex;
flex-direction: column;
gap: 1em;
}
</style>
@@ -0,0 +1,78 @@
<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<{
config: PreviewFieldgroup;
style_def: PlaceholderFieldGroupDefinition;
}>();
</script>
<template lang="pug">
// TODO: support predefined group
div.fieldgroup-container(:style="{'flex-grow': config.relSize, 'flex-direction': config.direction || 'row'}")
div.fieldgroup-item(v-for="{ entry, label, content } in 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)")
</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>
@@ -0,0 +1,14 @@
<script setup lang="ts">
import FieldgroupPreview from './fieldgroup-preview.vue';
const props = defineProps<{
config: Record<string, string>;
}>();
</script>
<template lang="pug">
div(v-if="'children' in config" style="width: 100%; display: flex; gap: 1em")
RowPreview(v-for="child in config.children" :config="child")
FieldgroupPreview(v-else :config="config")
</template>
@@ -0,0 +1,32 @@
export function i18nstringLocalize(s: I18nString): string {
if (typeof s === 'string') {
return s
}
if (s === null) {
return null
}
var locale = document.body.attributes['data-pretixlocale'].value
var short_locale = locale.split('-')[0]
if (locale in s)
return s[locale]
if (short_locale in s)
return s[short_locale]
for (const k of Object.keys(s)) {
if (k.split('-')[0] === short_locale && s[k]) {
return s[k]
}
}
if (s['en'])
return s['en']
for (const k of Object.keys(s)) {
if (s[k]) {
return s[k]
}
}
}
+202 -2
View File
@@ -1,10 +1,210 @@
from .base import FieldGroupDisplay, PassStyle, PredefinedFieldGroup, TextFieldGroup, WalletPlatform
from django.utils.translation import gettext_lazy as _
from pretix.base.models import Event, OrderPosition
from .base import FieldGroupDisplay, PassLayout, PassStyle, PredefinedFieldGroup, TextFieldGroup, WalletPlatform
from django.utils.translation import gettext_lazy as _, gettext
import json
from walletobjects import ButtonJWT, EventTicketClass, EventTicketObject
from walletobjects.comms import Comms
from walletobjects.constants import (
Barcode, ClassType, ConfirmationCode, DoorsOpen,
MultipleDevicesAndHoldersAllowedStatus, ObjectState, ObjectType,
ReviewStatus, Seat,
)
from pretix.base.settings import GlobalSettingsObject
import uuid
from pretix.multidomain.urlreverse import eventreverse_absolute
from django.utils import translation
def _get_instance_uuid():
gs = GlobalSettingsObject()
if not gs.settings.wallet_google_instance_uuid:
gs.settings.wallet_google_instance_uuid = str(uuid.uuid4())
return gs.settings.get("wallet_google_instance_uuid")
def get_class_id(event: Event):
instance_uuid = _get_instance_uuid()
issuer_id = event.settings.get('wallet_google_issuer_id')
return "%s.pretix-%s-%s-%s" % (issuer_id, instance_uuid, event.organizer.slug, event.slug)
def get_object_id(op: OrderPosition):
instance_uuid = _get_instance_uuid()
issuer_id = op.order.event.settings.get('wallet_google_issuer_id')
return "%s.pretix-%s-%s-%s-%s-%s" % (issuer_id, instance_uuid,
op.order.event.organizer.slug, op.order.event.slug,
op.order.code, op.positionid)
def get_translated_dict(string, locales):
translated = {}
for locale in locales:
translation.activate(locale)
translated[locale] = gettext(string)
translation.deactivate()
return translated
def get_translated_string(string, locale):
translation.activate(locale)
translated = gettext(string)
translation.deactivate()
return translated
class GooglePlatform(WalletPlatform):
identifier = "google"
name = _("Google")
@classmethod
def generate(cls, layout: PassLayout, op: OrderPosition):
class_object = cls._generate_class(op.order.event)
# ticket_object = cls._get_object(op)
# if not ticket_object:
# return False
# generated_jwt = self._comms().sign_jwt(
# ButtonJWT(
# origins=[django_settings.SITE_URL],
# issuer=self._comms().client_email,
# event_ticket_objects=[ticket_object],
# skinny=True
# )
# )
# if generated_jwt:
# return 'googlepaypass', 'text/uri-list', 'https://pay.google.com/gp/v/save/%s' % generated_jwt
# else:
# return False
data = {"class": class_object}
return 'pass.json', 'application/json', json.dumps(data)
# from ..views import get_layout_variables
# order = op.order
# event = order.event
# filename = "{}-{}.pkpass".format(order.event.slug, order.code)
# ticket = str(op.item.name)
# if op.variation:
# ticket += " - " + str(op.variation)
# serialNumber = "%s-%s-%s-%d" % (
# order.event.organizer.slug,
# order.event.slug,
# order.code,
# op.pk,
# )
# context = {
# "placeholders": get_layout_variables(op.order.event),
# "evaluation_context": [op, order, order.event],
# "ca_certificate": order.event.settings.wallet_apple_ca_certificate.read(),
# "certificate": order.event.settings.wallet_apple_certificate.read(),
# "key": order.event.settings.wallet_apple_key.read(),
# "password": order.event.settings.wallet_apple_key_password,
# "description": _("Ticket for {event} ({product})").format( # TODO: i18n
# event=event.name, product=ticket
# ),
# "organizationName": event.organizer.name,
# "passTypeIdentifier": order.event.settings.wallet_apple_pass_type_id,
# "teamIdentifier": order.event.settings.wallet_apple_team_id,
# "serialNumber": serialNumber,
# "locales": event.settings.locales,
# }
# data = layout.generate(context)
# return filename, "application/vnd.apple.pkpass", data
@classmethod
def _generate_class(cls, event: Event):
class_name = get_class_id(event)
output_class = EventTicketClass(
event.organizer.name,
class_name,
MultipleDevicesAndHoldersAllowedStatus.multipleHolders, # TODO: Make configurable
event.name,
ReviewStatus.draft, # TODO: ReviewStatus.underReview,
event.settings.locale
)
output_class.homepage_uri(
eventreverse_absolute(event, 'presale:event.index'),
get_translated_string('Website', event.settings.locale),
get_translated_dict('Website', event.settings.locales)
)
# output_class.callback_url(eventreverse_absolute(event.organizer, 'plugins:pretix_googlepaypasses:webhook'))
# if (event.settings.get('ticketoutput_googlepaypasses_latitude')
# and event.settings.get('ticketoutput_googlepaypasses_longitude')):
# output_class.locations(
# event.settings.get('ticketoutput_googlepaypasses_latitude'),
# event.settings.get('ticketoutput_googlepaypasses_longitude')
# )
# elif event.geo_lat and event.geo_lon:
# output_class.locations(
# event.geo_lat,
# event.geo_lon
# )
output_class.country_code(event.settings.locale)
output_class.hide_barcode(False)
# if event.settings.get('ticketoutput_googlepaypasses_hero'):
# output_class.hero_image(
# urljoin(django_settings.SITE_URL, event.settings.get('ticketoutput_googlepaypasses_hero').url),
# str(event.name),
# event.name,
# )
# output_class.hex_background_color(event.settings.get('primary_color'))
# output_class.event_id('pretix-%s-%s-%s' % (gs.settings.get('update_check_id'), event.organizer.id, event.id))
# if event.settings.get('ticketoutput_googlepaypasses_logo'):
# output_class.logo(
# urljoin(django_settings.SITE_URL, event.settings.get('ticketoutput_googlepaypasses_logo').url),
# str(event.name),
# event.name,
# )
# if event.location:
# name = {}
# address = {}
# for key, value in 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:])
# else:
# address[key] = lines[0]
# output_class.venue(name, address)
# if event.date_from and event.date_to and event.date_admission:
# output_class.date_time(
# DoorsOpen.doorsOpen,
# event.date_admission.isoformat(),
# event.date_from.isoformat(),
# event.date_to.isoformat(),
# )
# output_class.confirmation_code_label(ConfirmationCode.orderNumber)
# if event.seating_plan_id is not None:
# output_class.seat_label(Seat.seat)
# return self._comms().put_item(ClassType.eventTicketClass, class_name, output_class)
return output_class._eventTicketClass
class GoogleWalletStyle(PassStyle):
platform = GooglePlatform
+46 -2
View File
@@ -71,7 +71,9 @@ class WalletOutput(BaseTicketOutput):
wallet_layout = op.item.walletlayout
else:
wallet_layout = op.event.wallet_layouts.get(default=True)
platform_layout = get_object_or_404(wallet_layout.platform_layouts, platform=self.platform.identifier)
platform_layout = get_object_or_404(
wallet_layout.platform_layouts, platform=self.platform.identifier
)
return self.platform.generate(platform_layout.pass_layout, op)
@@ -81,6 +83,48 @@ class GoogleWalletTicketOutput(WalletOutput):
download_button_text = "Add to Google Wallet"
platform = GooglePlatform
def get_global_settings(sender, **kwargs):
return OrderedDict(
[
(
"wallet_google_issuer_id",
forms.CharField(
label=_("Google Wallet Issuer/Merchant ID"),
help_text=_(
"After getting accepted by Google into the Google Pay API for Passes program, "
"your Issuer ID can be found in the Merchant center at "
"https://wallet.google.com/merchant/walletobjects/"
),
required=False,
),
),
(
"wallet_google_credentials",
forms.FileField(
label=_("Google Wallet Service Account Credentials"),
help_text=_(
"Please paste the contents of the JSON credentials file "
"of the service account you tied to your Google Pay API "
"for Passes Issuer ID"
),
required=False,
# validators=[validate_json_credentials]
),
),
(
"wallet_google_instance_uuid",
forms.CharField(
label=_("Google Wallet Pass Instance UUID"),
help_text=_(
"Instance-specific part to be added to the wallet ids"
),
required=False,
),
),
]
)
class AppleWalletTicketOutput(WalletOutput):
identifier = "wallet_apple"
@@ -129,7 +173,7 @@ class AppleWalletTicketOutput(WalletOutput):
label=_("Apple Wallet Pass secret key"),
required=False,
validators=[validate_rsa_privkey],
widget=ClearableBasenameFileInput
widget=ClearableBasenameFileInput,
),
),
(