ItemCreate/ItemUpdate: replace personalized with questionnaires selection

This commit is contained in:
Mira Weller
2026-09-30 21:03:27 +02:00
parent 83b19efcde
commit d3434cc719
5 changed files with 62 additions and 105 deletions
+3 -2
View File
@@ -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
+39 -5
View File
@@ -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',
@@ -64,47 +64,7 @@
</div>
</div>
<div class="form-group" data-display-dependency="#admission_on">
<label class="col-md-3 control-label">{% trans "Personalization" %}</label>
<div class="col-md-9">
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.personalized.html_name }}" {% if form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-id-card-o"></span>
<strong>{% trans "Personalized ticket" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
When this ticket is purchased, the system will ask for a name or other details according
to your event settings.
{% endblocktrans %}
{% if not request.event.settings.attendee_names_asked and not request.event.settings.attendee_emails_asked and not request.event.settings.attendee_company_asked and not request.event.settings.attendee_addresses_asked %}
<br>
<span class="text-warning">
<span class="fa fa-warning" aria-hidden="true"></span>
{% trans "This will currently have no effect since all data fields are turned off in event settings." %}
</span>
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
class="btn btn-default btn-xs" target="_blank">{% trans "Change settings" %}</a>
{% endif %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" value="" name="{{ form.personalized.html_name }}" {% if not form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-circle-o"></span>
<strong>{% trans "Non-personalized ticket" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
The system will not ask for a name or other attendee details. This only affects
system-provided fields, you can still add your own questions.
{% endblocktrans %}
</div>
</label>
</div>
</div>
</div>
{% bootstrap_field form.questionnaires layout="control" %}
{% bootstrap_field form.category layout="control" %}
<div class="form-group">
@@ -72,51 +72,7 @@
</div>
</div>
<div class="form-group" data-display-dependency="#admission_on">
<label class="col-md-3 control-label">{% trans "Personalization" %}</label>
<div class="col-md-9">
{% for e in form.errors.personalized %}
<div class="alert alert-danger has-error">
{{ e }}
</div>
{% endfor %}
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.personalized.html_name }}" {% if form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-id-card-o"></span>
<strong>{% trans "Personalized ticket" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
When this ticket is purchased, the system will ask for a name or other details according
to your event settings.
{% endblocktrans %}
{% if not request.event.settings.attendee_names_asked and not request.event.settings.attendee_emails_asked and not request.event.settings.attendee_company_asked and not request.event.settings.attendee_addresses_asked %}
<br>
<span class="text-warning">
<span class="fa fa-warning" aria-hidden="true"></span>
{% trans "This will currently have no effect since all data fields are turned off in event settings." %}
</span>
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
class="btn btn-default btn-xs" target="_blank">{% trans "Change settings" %}</a>
{% endif %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" value="" name="{{ form.personalized.html_name }}" {% if not form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-file-text-o"></span>
<strong>{% trans "Non-personalized ticket" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
The system will not ask for a name or other attendee details. This only affects
system-provided fields, you can still add your own questions.
{% endblocktrans %}
</div>
</label>
</div>
</div>
</div>
{% bootstrap_field form.questionnaires layout="control" %}
{% bootstrap_field form.description layout="control" %}
{% bootstrap_field form.picture layout="control" %}
+18 -12
View File
@@ -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