From eb5ff1f34a5d22654364cc3575375d12876b2860 Mon Sep 17 00:00:00 2001 From: Mira Weller Date: Wed, 27 May 2026 13:18:55 +0200 Subject: [PATCH] EventMetaProperties: Restrict changing "allowed values" if already in use (Z#23235265) --- .../organizers/property_edit.html | 3 + src/pretix/control/views/organizer.py | 59 +++++++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/pretix/control/templates/pretixcontrol/organizers/property_edit.html b/src/pretix/control/templates/pretixcontrol/organizers/property_edit.html index 19c77991e9..26862e6252 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/property_edit.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/property_edit.html @@ -72,6 +72,9 @@
{% bootstrap_field form.key layout='inline' form_group_class="" %} + {% if form.key.help_text %} + {{ form.key.help_text|safe }} + {% endif %}
{% bootstrap_field form.label layout='inline' form_group_class="" %} diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py index e4854b24cb..5f36fba87d 100644 --- a/src/pretix/control/views/organizer.py +++ b/src/pretix/control/views/organizer.py @@ -69,7 +69,7 @@ from django.utils.functional import cached_property from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.timezone import get_current_timezone, now -from django.utils.translation import gettext, gettext_lazy as _ +from django.utils.translation import gettext, gettext_lazy as _, ngettext from django.views import View from django.views.decorators.http import require_http_methods from django.views.generic import ( @@ -91,7 +91,7 @@ from pretix.base.models import ( ReusableMedium, ScheduledOrganizerExport, Team, TeamInvite, User, ) from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider -from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue +from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue, SubEvent, SubEventMetaValue from pretix.base.models.giftcards import ( GiftCardAcceptance, GiftCardTransaction, gen_giftcard_secret, ) @@ -2466,11 +2466,47 @@ class EventMetaPropertyEditorMixin: @cached_property def formset(self): - return EventMetaPropertyAllowedValueFormSet( + formset = EventMetaPropertyAllowedValueFormSet( data=self.request.POST if self.request.method == "POST" else None, organizer=self.request.organizer, initial=(self.object.choices or []) if self.object else [], ) + if self.event_value_counts or self.subevent_value_counts: + for form in formset.initial_forms: + uses = [] + key = form.initial['key'] + if key in self.event_value_counts: + count = self.event_value_counts[key] + uses += [ngettext("%d event", "%d events", count) % count] + if key in self.subevent_value_counts: + count = self.subevent_value_counts[key] + uses += [ngettext("%d subevent", "%d subevents", count) % count] + if uses: + form.fields['key'].help_text = _("Value can not be changed because it is in use (%s).") % (", ".join(uses)) + form.fields['key'].widget.attrs['readonly'] = True + return formset + + @cached_property + def event_value_counts(self): + if self.object: + return {d['attr_value']: d['count'] + for d in self.request.organizer.events.annotate( + attr_value=Subquery(EventMetaValue.objects.filter( + event=OuterRef('pk'), + property__name=self.object.name + ).values('value')), count=Count('attr_value') + ).values('attr_value', 'count')} + + @cached_property + def subevent_value_counts(self): + if self.object: + return {d['attr_value']: d['count'] + for d in SubEvent.objects.filter(event__organizer=self.request.organizer).annotate( + attr_value=Subquery(SubEventMetaValue.objects.filter( + subevent=OuterRef('pk'), + property__name=self.object.name, + ).values('value')), count=Count('attr_value') + ).values('attr_value', 'count')} def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) @@ -2493,10 +2529,25 @@ class EventMetaPropertyEditorMixin: return False return True + def all_existing_values_valid(self): + if not self.event_value_counts and not self.subevent_value_counts: + return True + choice_keys = set( + f.cleaned_data.get("key") for f in self.formset.ordered_forms if f not in self.formset.deleted_forms + ) + if not choice_keys: + return True + existing_values = (self.event_value_counts.keys() | self.subevent_value_counts.keys()) - {None} + missing_choices = existing_values - choice_keys + if missing_choices: + messages.error(self.request, _("When restricting the allowed values, you need to allow all values that already exist on your events. Missing values: %s") % (", ".join(missing_choices))) + return False + return True + def post(self, request, *args, **kwargs): self.object = self.get_object(self.get_queryset()) self.form = self.get_form() - if self.form.is_valid() and self.formset.is_valid() and self.is_default_valid(): + if self.form.is_valid() and self.formset.is_valid() and self.is_default_valid() and self.all_existing_values_valid(): return self.form_valid(self.form) else: return self.form_invalid(self.form)