diff --git a/src/pretix/plugins/wallet/api.py b/src/pretix/plugins/wallet/api.py index 8abbee3fc3..7a67fec2e5 100644 --- a/src/pretix/plugins/wallet/api.py +++ b/src/pretix/plugins/wallet/api.py @@ -1,35 +1,21 @@ from rest_framework import viewsets from django.db import transaction -from .styles import PassLayout, AVAILABLE_STYLES_DICT -from .models import WalletLayout +from .styles import PassLayout, AVAILABLE_STYLES_DICT, AVAILABLE_PLATFORMS +from .models import WalletLayout, WalletPlatformLayout from pretix.api.serializers.i18n import I18nAwareModelSerializer -import django_filters.rest_framework from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from .views import get_layout_variables +from rest_framework import serializers -class WalletLayoutSerializer(I18nAwareModelSerializer): +class WalletPlatformLayoutSerializer(I18nAwareModelSerializer): + platform = serializers.ChoiceField(choices=[p.identifier for p in AVAILABLE_PLATFORMS]) + style = serializers.CharField(allow_null=True, required=False) + class Meta: - model = WalletLayout - fields = ("id", "platform", "name", "style", "layout") - read_only_fields = ("id",) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if self.instance: - self.fields['platform'].read_only = True - - def save(self, *args, **kwargs): - super().save(*args, **kwargs, event=self.context["event"]) - - def validate_platform(self, value): - if self.instance and value != self.instance.platform: - raise ValidationError(_("Platform cannot be changed")) - - if value not in AVAILABLE_STYLES_DICT: - raise ValidationError(_("Invalid platform")) - return value + model = WalletPlatformLayout + fields = ("platform", "style", "layout") def validate_layout(self, value): if not isinstance(value, dict): @@ -37,11 +23,10 @@ class WalletLayoutSerializer(I18nAwareModelSerializer): return value def validate(self, data): - if self.instance: - platform = self.instance.platform - else: - platform = data.get('platform', None) - if "style" in data and "layout" in data and platform: + platform = data.get('platform') + style = data.get('style') + layout = data.get('layout') + if platform and style and layout: platform_styles = AVAILABLE_STYLES_DICT[platform] if data["style"] not in platform_styles: @@ -54,20 +39,38 @@ class WalletLayoutSerializer(I18nAwareModelSerializer): return data +class WalletLayoutSerializer(I18nAwareModelSerializer): + platform_layouts = WalletPlatformLayoutSerializer(many=True) + + class Meta: + model = WalletLayout + fields = ("id", "name", "platform_layouts") + read_only_fields = ("id",) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs, event=self.context["event"]) + + def update(self, instance, validated_data): + 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() + return super().update(instance, validated_data) + + class WalletLayoutViewSet(viewsets.ModelViewSet): model = WalletLayout queryset = WalletLayout.objects.none() serializer_class = WalletLayoutSerializer - filter_backends = (django_filters.rest_framework.DjangoFilterBackend,) - filterset_fields = ["platform"] permission = "event.settings.general:write" def get_queryset(self): return self.request.event.wallet_layouts.all() - def get_serializer(self, *args, **kwargs): - return super().get_serializer(*args, **kwargs) - def get_serializer_context(self): ctx = super().get_serializer_context() ctx["event"] = self.request.event diff --git a/src/pretix/plugins/wallet/migrations/0001_initial.py b/src/pretix/plugins/wallet/migrations/0001_initial.py index 184f8d018a..a60822286c 100644 --- a/src/pretix/plugins/wallet/migrations/0001_initial.py +++ b/src/pretix/plugins/wallet/migrations/0001_initial.py @@ -1,8 +1,8 @@ -# Generated by Django 4.2.28 on 2026-03-17 16:29 +# Generated by Django 5.2.13 on 2026-05-19 15:39 -from django.db import migrations, models import django.db.models.deletion import pretix.base.models.base +from django.db import migrations, models class Migration(migrations.Migration): @@ -10,7 +10,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("pretixbase", "0297_outgoingmail"), + ("pretixbase", "0300_alter_customer_locale_alter_user_locale"), ] operations = [ @@ -24,9 +24,6 @@ class Migration(migrations.Migration): ), ), ("name", models.CharField(max_length=190)), - ("platform", models.CharField(max_length=10)), - ("style", models.CharField(max_length=255)), - ("layout", models.TextField()), ( "event", models.ForeignKey( @@ -37,7 +34,64 @@ class Migration(migrations.Migration): ), ], options={ - "ordering": ("name",), + "abstract": False, + }, + bases=(models.Model, pretix.base.models.base.LoggingMixin), + ), + migrations.CreateModel( + name="WalletLayoutItem", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + ( + "item", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="walletlayout_assignments", + to="pretixbase.item", + ), + ), + ( + "layout", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="item_assignments", + to="wallet.walletlayout", + ), + ), + ], + options={ + "unique_together": {("item", "layout")}, + }, + ), + migrations.CreateModel( + name="WalletPlatformLayout", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + ("platform", models.CharField(max_length=10)), + ("style", models.CharField(max_length=255)), + ("layout", models.JSONField(default=dict)), + ( + "parent", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="platform_layouts", + to="wallet.walletlayout", + ), + ), + ], + options={ + "unique_together": {("parent", "platform")}, }, bases=(models.Model, pretix.base.models.base.LoggingMixin), ), diff --git a/src/pretix/plugins/wallet/models.py b/src/pretix/plugins/wallet/models.py index 50108582c3..b91dba6306 100644 --- a/src/pretix/plugins/wallet/models.py +++ b/src/pretix/plugins/wallet/models.py @@ -24,6 +24,7 @@ from django.utils.translation import gettext_lazy as _ from pretix.base.models import LoggedModel from django_scopes import ScopedManager +from django.core.exceptions import ValidationError class WalletLayout(LoggedModel): @@ -36,28 +37,31 @@ class WalletLayout(LoggedModel): max_length=190, verbose_name=_('Name') ) + + objects = ScopedManager(organizer='event__organizer') + + +class WalletPlatformLayout(LoggedModel): + parent = models.ForeignKey(WalletLayout, on_delete=models.CASCADE, related_name="platform_layouts") + platform = models.CharField(max_length=10) style = models.CharField(max_length=255) layout = models.JSONField(default=dict) - objects = ScopedManager(organizer='event__organizer') + objects = ScopedManager(organizer='parent__event__organizer') class Meta: - ordering = ("name",) - - def __str__(self): - return self.name + unique_together = (('parent', 'platform'),) class WalletLayoutItem(models.Model): item = models.ForeignKey('pretixbase.Item', null=True, blank=True, related_name='walletlayout_assignments', on_delete=models.CASCADE) layout = models.ForeignKey(WalletLayout, on_delete=models.CASCADE, related_name='item_assignments') - sales_channel = models.ForeignKey( - "pretixbase.SalesChannel", - on_delete=models.CASCADE, - ) class Meta: - unique_together = (('item', 'layout', 'sales_channel'),) - ordering = ("id",) + unique_together = (('item', 'layout'),) + + def clean(self): + if self.item.event != self.layout.event: + raise ValidationError("cannot bind layout to item of different event") 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 2b4117abd3..72cd29b1fa 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue @@ -9,8 +9,8 @@ const gettext = (window as any).gettext; const isLoading = ref(true); const wallet_layout = ref(null); -const STYLES: Styles = JSON.parse( - document.querySelector("#styles")?.textContent ?? "{}", +const PLATFORMS: Platforms = JSON.parse( + document.querySelector("#platforms")?.textContent ?? "{}", ); const VARIABLES: VariableConfig = JSON.parse( document.querySelector("#variables")?.textContent ?? "{}", @@ -55,11 +55,36 @@ function saveLayout(e: SubmitEvent) { }, ) .then((x) => x.json()) + .catch((x) => alert(x)) .then((x) => { wallet_layout.value = x; isLoading.value = false; }); } + +const currentPlatform = ref(PLATFORMS[0].identifier); +const currentLayout = computed(() => ({})); +const platformStyles = computed(() => { + for (const platform of PLATFORMS) { + if (platform.identifier === currentPlatform.value) { + return platform.styles + } + } +}); +const platformLayout = computed(() => { + for (const layout of wallet_layout.value.platform_layouts) { + if (layout.platform === currentPlatform.value) { + return layout + } + } + const newLayout = {platform: currentPlatform, style: null, layout: {}}; + wallet_layout.value.platform_layouts.push(newLayout); + return newLayout +}); +const platformChoices = computed(() => { + return [[null, "Do not generate pass"], ...Object.values(platformStyles.value).map(x => [x.identifier, x.name])] +}); + diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/select.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/select.vue index b84dee7984..e6fdbed922 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/select.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/select.vue @@ -8,7 +8,8 @@ defineOptions({ const props = defineProps<{ label?: string choices: Array<[string, string]> - errors?: string[] + errors?: string[], + class: string }>() const modelValue = defineModel(); const id = useId() @@ -23,7 +24,7 @@ watchEffect(() => {