diff --git a/src/pretix/api/helpers.py b/src/pretix/api/helpers.py new file mode 100644 index 0000000000..7150d29fe7 --- /dev/null +++ b/src/pretix/api/helpers.py @@ -0,0 +1,25 @@ +from pretix.base.models import ( + CachedFile +) +from rest_framework.exceptions import ValidationError +from django.core.exceptions import ValidationError as DjangoValidationError +from django.conf import settings + +def handle_file_upload(data, user, auth, allowed_types): + try: + cf = CachedFile.objects.get( + session_key=f'api-upload-{str(type(user or auth))}-{(user or auth).pk}', + file__isnull=False, + pk=data[len("file:"):], + ) + except (ValidationError, DjangoValidationError, IndexError): # invalid uuid + raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data)) + except CachedFile.DoesNotExist: + raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data)) + + if cf.type not in allowed_types: + raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data)) + if cf.file.size > settings.FILE_UPLOAD_MAX_SIZE_OTHER: + raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data)) + + return cf.file \ No newline at end of file diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py index 87477dd82c..55fffa1fcb 100644 --- a/src/pretix/api/views/checkin.py +++ b/src/pretix/api/views/checkin.py @@ -63,7 +63,7 @@ from pretix.api.views import RichOrderingFilter from pretix.api.views.order import OrderPositionFilter from pretix.base.i18n import language from pretix.base.models import ( - CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition, + Checkin, CheckinList, Device, Event, Order, OrderPosition, Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken, ) from pretix.base.models.orders import PrintLog @@ -75,6 +75,7 @@ from pretix.base.services.checkin import ( from pretix.base.services.media import perform_media_exchange from pretix.base.signals import checkin_annulled from pretix.helpers import OF_SELF +from pretix.api.helpers import handle_file_upload with scopes_disabled(): class CheckinListFilter(FilterSet): @@ -328,27 +329,6 @@ with scopes_disabled(): ) -def _handle_file_upload(data, user, auth): - try: - cf = CachedFile.objects.get( - session_key=f'api-upload-{str(type(user or auth))}-{(user or auth).pk}', - file__isnull=False, - pk=data[len("file:"):], - ) - except (ValidationError, BaseValidationError, IndexError): # invalid uuid - raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data)) - except CachedFile.DoesNotExist: - raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data)) - - allowed_types = ( - 'image/png', 'image/jpeg', 'image/gif', 'application/pdf' - ) - if cf.type not in allowed_types: - raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data)) - if cf.file.size > settings.FILE_UPLOAD_MAX_SIZE_OTHER: - raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data)) - - return cf.file def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_products=False, pdf_data=False, expand=None): @@ -795,7 +775,12 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, try: if q.type == Question.TYPE_FILE: if answers_data[str(q.pk)]: - given_answers[q] = _handle_file_upload(answers_data[str(q.pk)], user, auth) + given_answers[q] = handle_file_upload( + answers_data[str(q.pk)], + user, + auth, + allowed_types=('image/png', 'image/jpeg', 'image/gif', 'application/pdf') + ) else: given_answers[q] = None else: diff --git a/src/pretix/plugins/wallet/api.py b/src/pretix/plugins/wallet/api.py index 6bd9681dc1..354976a605 100644 --- a/src/pretix/plugins/wallet/api.py +++ b/src/pretix/plugins/wallet/api.py @@ -1,16 +1,17 @@ -from rest_framework import viewsets +from rest_framework import viewsets, serializers from django.db import transaction from .styles import AVAILABLE_STYLES_DICT, AVAILABLE_PLATFORMS from .models import WalletLayout, WalletPlatformLayout from pretix.api.serializers.i18n import I18nAwareModelSerializer +from pretix.api.serializers.fields import UploadedFileField from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -from .views import get_editor_placeholders from rest_framework import serializers - class WalletPlatformLayoutSerializer(I18nAwareModelSerializer): - platform = serializers.ChoiceField(choices=[p.identifier for p in AVAILABLE_PLATFORMS]) + platform = serializers.ChoiceField( + choices=[p.identifier for p in AVAILABLE_PLATFORMS] + ) style = serializers.CharField(allow_null=True, required=False) class Meta: @@ -23,9 +24,9 @@ class WalletPlatformLayoutSerializer(I18nAwareModelSerializer): return value def validate(self, data): - platform = data.get('platform') - style = data.get('style') - layout = data.get('layout') + platform = data.get("platform") + style = data.get("style") + layout = data.get("layout") if platform and style and layout: platform_styles = AVAILABLE_STYLES_DICT[platform] @@ -33,10 +34,24 @@ class WalletPlatformLayoutSerializer(I18nAwareModelSerializer): raise ValidationError(_("Invalid style")) style = platform_styles[data["style"]] - style = style(event=self.context['event'], layout=data["layout"]) + style = style(event=self.context["event"], layout=data["layout"]) style.validate() + data['file_settings'] = style.extract_file_settings(self.context['request']) + return data + def to_representation(self, instance): + ret = super().to_representation(instance) + ret['file_settings'] = {} + for file_setting in instance.file_settings.all(): + try: + url = file_setting.file.url + except AttributeError: + continue + request = self.context['request'] + ret['file_settings'][file_setting.key] = request.build_absolute_uri(url) + return ret + class WalletLayoutSerializer(I18nAwareModelSerializer): platform_layouts = WalletPlatformLayoutSerializer(many=True) @@ -53,11 +68,28 @@ class WalletLayoutSerializer(I18nAwareModelSerializer): super().save(*args, **kwargs, event=self.context["event"]) def update(self, instance, validated_data): - platform_layouts = validated_data.pop('platform_layouts') + platform_layouts = validated_data.pop("platform_layouts") for layout in platform_layouts: - if layout['style']: - instance.platform_layouts.update_or_create(platform=layout['platform'], defaults=layout) - instance.platform_layouts.exclude(platform__in={layout['platform'] for layout in platform_layouts if layout['style'] is not None}).delete() + if layout["style"]: + # TODO: better handling here + file_settings = layout.pop("file_settings", {}) + obj, _ = instance.platform_layouts.update_or_create( + platform=layout["platform"], defaults=layout + ) + for key, file in file_settings.items(): + if not file: + obj.file_settings.filter(key=key).delete() + + elif file != "keep": + obj.file_settings.update_or_create(key=key, defaults={"file": file}) + + instance.platform_layouts.exclude( + platform__in={ + layout["platform"] + for layout in platform_layouts + if layout["style"] is not None + } + ).delete() return super().update(instance, validated_data) diff --git a/src/pretix/plugins/wallet/migrations/0003_walletlayoutfilesetting.py b/src/pretix/plugins/wallet/migrations/0003_walletlayoutfilesetting.py new file mode 100644 index 0000000000..673b7ae832 --- /dev/null +++ b/src/pretix/plugins/wallet/migrations/0003_walletlayoutfilesetting.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.13 on 2026-08-11 15:39 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("wallet", "0002_alter_walletlayoutitem_unique_together_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="WalletLayoutFileSetting", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + ("key", models.CharField()), + ("file", models.FileField(upload_to="")), + ( + "layout", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="file_settings", + to="wallet.walletplatformlayout", + ), + ), + ], + ), + ] diff --git a/src/pretix/plugins/wallet/models.py b/src/pretix/plugins/wallet/models.py index 18f3c08b7b..3d174f1af9 100644 --- a/src/pretix/plugins/wallet/models.py +++ b/src/pretix/plugins/wallet/models.py @@ -70,7 +70,9 @@ class WalletPlatformLayout(LoggedModel): style = get_style(self.platform, self.style) if style: - return style(event=self.parent.event, layout=self.layout) + file_settings = dict(self.file_settings.values_list("key", "file")) + print(file_settings) + return style(event=self.parent.event, layout=self.layout, file_settings=file_settings) else: raise RuntimeError(f"Style {self.platform}.{self.style} not found") @@ -83,6 +85,11 @@ class WalletLayoutItem(models.Model): if self.item.event != self.layout.event: raise ValidationError("cannot bind layout to item of different event") +class WalletLayoutFileSetting(models.Model): + layout = models.ForeignKey(WalletPlatformLayout, on_delete=models.CASCADE, related_name="file_settings") + key = models.CharField() + file = models.FileField() + # smth like this for apple, lets see what the best architecture for google will be # class AppleWalletPass(models.Model): # platform_layout = models.ForeignKey(WalletPlatformLayout, on_delete=models.PROTECT) diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue index 5f89729b79..bf31b41eb1 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue @@ -70,7 +70,6 @@ const preview_layout = computed(() => { .tab-pane.active.row .col-md-6.col-lg-8 Select.form-group(label="Style" :modelValue="store.currentPlatformLayout.style" @update:modelValue="store.setCurrentPlatformStyle" :choices="platformChoices") - StyleSettings(v-if="store.currentPlatformLayout.style" v-model="store.currentPlatformLayout.layout" :style="store.currentPlatformStyles[store.currentPlatformLayout.style]") .col-md-6.col-lg-4 .panel.panel-default diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/file-input.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/file-input.vue new file mode 100644 index 0000000000..2b96d6f596 --- /dev/null +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/file-input.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/input.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/input.vue index 9a286d20dd..9c2f243092 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/input.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/input.vue @@ -1,20 +1,28 @@ diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-item-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-item-preview.vue index 68c8531ec0..30378ecbb9 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-item-preview.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-item-preview.vue @@ -6,7 +6,7 @@ import { i18nstringLocalize } from "../../helpers"; const store = inject(StoreKey)!; const props = defineProps<{ - label: I18nString; + label?: I18nString; content: I18nString; content_type: FieldContentType; display: FieldGroupDisplay; diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue index 440073c4a8..95f79b5585 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue @@ -17,9 +17,16 @@ const style_def = computed(() => { ].fieldgroups.map((x) => [x.identifier, x]), )[props.config.fieldgroup]; }); + +function focusGroup() { + // TODO: highlight group? or change ui concept completely + const elem = document.getElementById('fieldgroup-' + props.config.fieldgroup) + elem && elem.scrollIntoView() +} diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fixed-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fixed-preview.vue deleted file mode 100644 index ae05605464..0000000000 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fixed-preview.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/pass-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/pass-preview.vue index 9eab779223..8d4d831f28 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/pass-preview.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/pass-preview.vue @@ -59,6 +59,7 @@ const props = defineProps<{ layout: Array }>(); } .fieldgroup-item-image { max-height: 3lh; + object-fit: contain; } .fieldgroup-item-qrcode { font-weight: bold; diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue index 49b7b73a75..23fb0a5794 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue @@ -1,7 +1,7 @@ + + diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/settings-field.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/settings-field.vue new file mode 100644 index 0000000000..ad2ab57480 --- /dev/null +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/settings-field.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/style-settings.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/style-settings.vue index eb274796b3..ce22b5d2bd 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/style-settings.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/style-settings.vue @@ -2,7 +2,7 @@ import { computed, inject, watchEffect } from "vue"; import PlaceholderFieldSettings from "./placeholder-field-settings.vue"; import PredefinedFieldSettings from "./predefined-field-settings.vue"; - +import SettingsField from "./settings-field.vue"; const gettext = (window as any).gettext; const props = defineProps<{ @@ -13,9 +13,11 @@ const layout = defineModel();