From d3434cc719c400860cccbf44dd02be5f1754df73 Mon Sep 17 00:00:00 2001 From: Mira Weller Date: Wed, 30 Sep 2026 01:10:26 +0200 Subject: [PATCH] ItemCreate/ItemUpdate: replace personalized with questionnaires selection --- src/pretix/base/models/items.py | 5 +- src/pretix/control/forms/item.py | 44 ++++++++++++++++-- .../templates/pretixcontrol/item/create.html | 42 +---------------- .../templates/pretixcontrol/item/index.html | 46 +------------------ src/pretix/control/views/item.py | 30 +++++++----- 5 files changed, 62 insertions(+), 105 deletions(-) diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 26b5ba1fa9..ba702a7e11 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -2052,9 +2052,10 @@ class Questionnaire(LoggedModel): def __lt__(self, other) -> bool: return self.sortkey < other.sortkey - def check_constraints(self): + @staticmethod + def check_constraints(event): errors = set() - all_questionnaires = Questionnaire.objects.filter(event=self.event).prefetch_related('children', 'items') + all_questionnaires = Questionnaire.objects.filter(event=event).prefetch_related('children', 'items') for (type, item_id, sdf, udf), fields in groupby(sorted( (q.type, item.id, c.system_datafield or '', c.user_datafield_id or 0, q, item) for q in all_questionnaires diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index 53ca965a17..c025c376f3 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -60,7 +60,7 @@ from pretix.base.models import ( Item, ItemCategory, ItemProgramTime, ItemVariation, Question, QuestionOption, Quota, ) -from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue +from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue, Questionnaire from pretix.base.signals import item_copy_data from pretix.control.forms import ( ButtonGroupRadioSelect, ExtFileField, ItemMultipleChoiceField, @@ -392,13 +392,20 @@ class ItemCreateForm(I18nModelForm): help_text=_('Select this option e.g. for t-shirts that come in multiple sizes. ' 'You can select the variations in the next step.'), required=False) + questionnaires = SafeModelMultipleChoiceField( + queryset=Questionnaire.objects.none(), + required=False, + label=_('Questionnaires'), + widget=forms.CheckboxSelectMultiple(attrs={ + 'class': 'scrolling-multiple-choice' + }), + ) def __init__(self, *args, **kwargs): self.event = kwargs['event'] self.user = kwargs.pop('user') kwargs.setdefault('initial', {}) kwargs['initial'].setdefault('admission', True) - kwargs['initial'].setdefault('personalized', True) super().__init__(*args, **kwargs) self.fields['category'].queryset = self.instance.event.categories.all() @@ -414,6 +421,8 @@ class ItemCreateForm(I18nModelForm): ) self.fields['category'].widget.choices = self.fields['category'].choices + self.fields['questionnaires'].queryset = self.instance.event.questionnaires.all() + self.fields['tax_rule'].queryset = self.instance.event.tax_rules.all() change_decimal_field(self.fields['default_price'], self.instance.event.currency) self.fields['copy_from'] = forms.ModelChoiceField( @@ -520,10 +529,15 @@ class ItemCreateForm(I18nModelForm): self.instance.picture.save(os.path.basename(src.picture.name), src.picture) self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1 - if not self.instance.admission: - self.instance.personalized = False instance = super().save(*args, **kwargs) + instance.questionnaires.clear() + instance.questionnaires.add(*self.cleaned_data['questionnaires']) + try: + Questionnaire.check_constraints(self.event) + except ValidationError as exc: + raise ValidationError({'questionnaires': exc}) + if not self.event.has_subevents and not self.cleaned_data.get('has_variations'): if self.cleaned_data.get('quota_option') == self.EXISTING and self.cleaned_data.get('quota_add_existing') is not None: quota = self.cleaned_data.get('quota_add_existing') @@ -647,6 +661,14 @@ class ItemUpdateForm(I18nModelForm): max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE, required=False, ) + questionnaires = SafeModelMultipleChoiceField( + queryset=Questionnaire.objects.none(), + required=False, + label=_('Questionnaires'), + widget=forms.CheckboxSelectMultiple(attrs={ + 'class': 'scrolling-multiple-choice' + }), + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -742,6 +764,9 @@ class ItemUpdateForm(I18nModelForm): ) self.fields['category'].widget.choices = self.fields['category'].choices + self.fields['questionnaires'].queryset = self.instance.event.questionnaires.all() + self.initial['questionnaires'] = self.instance.questionnaires.values_list('pk', flat=True) + self.fields['free_price_suggestion'].widget.attrs['data-display-dependency'] = '#id_free_price' self.fields['validity_dynamic_start_choice'] = forms.TypedChoiceField( @@ -836,6 +861,16 @@ class ItemUpdateForm(I18nModelForm): return d + def save(self, *args, **kwargs): + instance = super().save(*args, **kwargs) + instance.questionnaires.clear() + instance.questionnaires.add(*self.cleaned_data['questionnaires']) + try: + Questionnaire.check_constraints(self.event) + except ValidationError as exc: + raise ValidationError({'questionnaires': exc}) + return instance + class Meta: model = Item localized_fields = '__all__' @@ -847,7 +882,6 @@ class ItemUpdateForm(I18nModelForm): 'all_sales_channels', 'limit_sales_channels', 'admission', - 'personalized', 'description', 'picture', 'default_price', diff --git a/src/pretix/control/templates/pretixcontrol/item/create.html b/src/pretix/control/templates/pretixcontrol/item/create.html index 7d73ea5755..168f67a3f4 100644 --- a/src/pretix/control/templates/pretixcontrol/item/create.html +++ b/src/pretix/control/templates/pretixcontrol/item/create.html @@ -64,47 +64,7 @@ -
- -
-
- -
-
- -
-
-
- + {% bootstrap_field form.questionnaires layout="control" %} {% bootstrap_field form.category layout="control" %}
diff --git a/src/pretix/control/templates/pretixcontrol/item/index.html b/src/pretix/control/templates/pretixcontrol/item/index.html index e4678ab9fe..b55fd845b2 100644 --- a/src/pretix/control/templates/pretixcontrol/item/index.html +++ b/src/pretix/control/templates/pretixcontrol/item/index.html @@ -72,51 +72,7 @@
-
- -
- {% for e in form.errors.personalized %} -
- {{ e }} -
- {% endfor %} -
- -
-
- -
-
-
+ {% bootstrap_field form.questionnaires layout="control" %} {% bootstrap_field form.description layout="control" %} {% bootstrap_field form.picture layout="control" %} diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py index 059c20a9ab..57c723c85a 100644 --- a/src/pretix/control/views/item.py +++ b/src/pretix/control/views/item.py @@ -39,7 +39,7 @@ from itertools import groupby from json.decoder import JSONDecodeError from django.contrib import messages -from django.core.exceptions import PermissionDenied +from django.core.exceptions import PermissionDenied, ValidationError from django.core.files import File from django.db import models, transaction from django.db.models import ( @@ -1335,9 +1335,10 @@ class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView): initial['tax_rule'] = trs[0] if self.copy_from: - fields = ('name', 'internal_name', 'category', 'admission', 'personalized', 'default_price', 'tax_rule') + fields = ('name', 'internal_name', 'category', 'admission', 'default_price', 'tax_rule') for f in fields: initial[f] = getattr(self.copy_from, f) + initial['questionnaires'] = self.copy_from.questionnaires.values_list('pk', flat=True) initial['copy_from'] = self.copy_from initial['has_variations'] = self.copy_from.variations.exists() @@ -1371,7 +1372,7 @@ class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView): return super().form_invalid(form) def get_context_data(self, **kwargs): - ctx = super().get_context_data() + ctx = super().get_context_data(**kwargs) ctx['meta_forms'] = self.meta_forms return ctx @@ -1379,9 +1380,11 @@ class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView): self.object = None form = self.get_form() if form.is_valid() and all([f.is_valid() for f in self.meta_forms]): - return self.form_valid(form) - else: - return self.form_invalid(form) + try: + return self.form_valid(form) + except ValidationError as e: + form.add_error(None, e) + return self.form_invalid(form) class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView): @@ -1445,9 +1448,11 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE self.get_object() form = self.get_form() if self.is_valid(form) and all([f.is_valid() for f in self.meta_forms]): - return self.form_valid(form) - else: - return self.form_invalid(form) + try: + return self.form_valid(form) + except ValidationError as exc: + form.add_error(None, exc) + return self.form_invalid(form) def save_formset(self, key, log_base, attr='item', order=True, serializer=None, rm_verb='removed'): @@ -1496,7 +1501,6 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE @transaction.atomic def form_valid(self, form): self.save_meta() - messages.success(self.request, _('Your changes have been saved.')) change_data = { k: form.cleaned_data.get(k) @@ -1556,7 +1560,9 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE else: v.save() - return super().form_valid(form) + response = super().form_valid(form) + messages.success(self.request, _('Your changes have been saved.')) + return response def form_invalid(self, form): messages.error(self.request, _('We could not save your changes. See below for details.')) @@ -1569,7 +1575,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE return o def get_context_data(self, **kwargs): - ctx = super().get_context_data() + ctx = super().get_context_data(**kwargs) ctx['plugin_forms'] = self.plugin_forms ctx['meta_forms'] = self.meta_forms ctx['formsets'] = self.formsets