From 2b99214fa2c7dc62c7d02610c39eb27731199785 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 22 Sep 2026 12:45:22 +0200 Subject: [PATCH] first round of fixes --- src/pretix/api/serializers/cart.py | 6 +--- src/pretix/api/serializers/order.py | 6 +--- src/pretix/api/serializers/orderchange.py | 8 +++-- src/pretix/base/exporters/orderlist.py | 1 + src/pretix/base/forms/questions.py | 10 +++--- src/pretix/base/models/items.py | 12 ++----- src/pretix/base/models/orders.py | 18 +++++----- src/pretix/base/models/waitinglist.py | 4 +-- src/pretix/base/payment.py | 9 ++--- src/pretix/base/services/cart.py | 2 +- src/pretix/base/services/invoices.py | 5 ++- src/pretix/base/services/orders.py | 6 ++-- src/pretix/control/forms/orders.py | 3 +- src/pretix/control/forms/waitinglist.py | 2 +- .../templates/pretixcontrol/items/quotas.html | 6 +++- .../templates/pretixcontrol/order/change.html | 2 +- .../pretixcontrol/vouchers/index.html | 6 +++- .../pretixcontrol/waitinglist/index.html | 8 ++++- src/pretix/control/views/waitinglist.py | 2 +- src/pretix/plugins/checkinlists/exporters.py | 36 ++++++++++++------- src/pretix/plugins/sendmail/models.py | 2 +- src/pretix/presale/views/cart.py | 2 +- src/pretix/presale/views/waiting.py | 5 ++- 23 files changed, 86 insertions(+), 75 deletions(-) diff --git a/src/pretix/api/serializers/cart.py b/src/pretix/api/serializers/cart.py index 74dc5242b4..4f9a6db272 100644 --- a/src/pretix/api/serializers/cart.py +++ b/src/pretix/api/serializers/cart.py @@ -77,11 +77,7 @@ class BaseCartPositionCreateSerializer(I18nAwareModelSerializer): def validate_subevent(self, subevent): if self.context['event'].has_subevents: - if not subevent: - raise ValidationError( - 'You need to set a subevent.' - ) - if subevent.event != self.context['event']: + if subevent and subevent.event != self.context['event']: raise ValidationError( 'The specified subevent does not belong to this event.' ) diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 418dbc00f9..e2cc04b225 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -1094,11 +1094,7 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer): def validate_subevent(self, subevent): if self.context['event'].has_subevents: - if not subevent: - raise ValidationError( - 'You need to set a subevent.' - ) - if subevent.event != self.context['event']: + if subevent and subevent.event != self.context['event']: raise ValidationError( 'The specified subevent does not belong to this event.' ) diff --git a/src/pretix/api/serializers/orderchange.py b/src/pretix/api/serializers/orderchange.py index a92aa46b03..4f1c69bcd3 100644 --- a/src/pretix/api/serializers/orderchange.py +++ b/src/pretix/api/serializers/orderchange.py @@ -275,11 +275,15 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer): def validate_subevent(self, subevent): if self.context['event'].has_subevents: - if not subevent: + if self.instance.subevent_id and not subevent: raise ValidationError( 'You need to set a subevent.' ) - if subevent.event != self.context['event']: + if not self.instance.subevent_id and subevent: + raise ValidationError( + 'You cannot set a subevent if none was set previously.' + ) + if subevent and subevent.event != self.context['event']: raise ValidationError( 'The specified subevent does not belong to this event.' ) diff --git a/src/pretix/base/exporters/orderlist.py b/src/pretix/base/exporters/orderlist.py index bf96145ec4..f33c46e04c 100644 --- a/src/pretix/base/exporters/orderlist.py +++ b/src/pretix/base/exporters/orderlist.py @@ -207,6 +207,7 @@ class OrderListExporter(MultiSheetListExporter): if form_data.get('event_date_range'): dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['event_date_range'], self.timezone) + # Subevent-less positions in a series will never be found when this filter is set but that seems like a valid way to do this if dt_start: annotations['event_date_max'] = Case( When(**{f'{rel}event__has_subevents': True}, then=Max(f'{rel}all_positions__subevent__date_from')), diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py index bf5c758f88..448ec0af16 100644 --- a/src/pretix/base/forms/questions.py +++ b/src/pretix/base/forms/questions.py @@ -1202,8 +1202,9 @@ class TicketLevelQuestionsForm(BaseQuestionsForm): return field def clean(self): - from pretix.base.addressvalidation import \ - validate_address # local import to prevent impact on startup time + from pretix.base.addressvalidation import ( # local import to prevent impact on startup time + validate_address, + ) d = super().clean() @@ -1444,8 +1445,9 @@ class BaseInvoiceAddressForm(forms.ModelForm): self.fields['transmission_type'].widget.attrs['data-trigger-address-info'] = 'on' def clean(self): - from pretix.base.addressvalidation import \ - validate_address # local import to prevent impact on startup time + from pretix.base.addressvalidation import ( # local import to prevent impact on startup time + validate_address, + ) data = self.cleaned_data diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 7aa035970a..e3fb587a26 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -896,7 +896,7 @@ class Item(LoggedModel): return check_quotas def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None, - include_bundled=False, trust_parameters=False, fail_on_no_quotas=False): + include_bundled=False, fail_on_no_quotas=False): """ This method is used to determine whether this Item is currently available for sale. @@ -906,8 +906,6 @@ class Item(LoggedModel): to no quotas being checked at all, this method will return unlimited availability. :param include_bundled: Also take availability of bundled items into consideration. - :param trust_parameters: Disable checking of the subevent parameter and disable checking if - any variations exist (performance optimization). :returns: any of the return codes of :py:meth:`Quota.availability()`. :raises ValueError: if you call this on an item which has variations associated with it. @@ -1307,7 +1305,7 @@ class ItemVariation(models.Model): return check_quotas def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None, - include_bundled=False, trust_parameters=False, fail_on_no_quotas=False) -> Tuple[int, int]: + include_bundled=False, fail_on_no_quotas=False) -> Tuple[int, int]: """ This method is used to determine whether this ItemVariation is currently available for sale in terms of quotas. @@ -1319,8 +1317,6 @@ class ItemVariation(models.Model): :param count_waitinglist: If ``False``, waiting list entries will be ignored for quota calculation. :returns: any of the return codes of :py:meth:`Quota.availability()`. """ - if not trust_parameters and not subevent and self.item.event.has_subevents: # NOQA - raise TypeError('You need to supply a subevent.') check_quotas = self._get_quotas(ignored_quotas=ignored_quotas, subevent=subevent) quotacounter = Counter() res = Quota.AVAILABILITY_OK, None @@ -2201,9 +2197,7 @@ class Quota(LoggedModel): @staticmethod def clean_subevent(event, subevent): if event.has_subevents: - if not subevent: - raise ValidationError(_('Subevent cannot be null for event series.')) - if event != subevent.event: + if subevent and event != subevent.event: raise ValidationError(_('The subevent does not belong to this event.')) else: if subevent: diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 15dba58a98..52feaaa3ea 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -656,8 +656,8 @@ class Order(LockModel, LoggedModel): terms = [ until.datetime(se) for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True)) - ] - return min(terms) if terms else None + ] or [until.datetime(self.event)] # use event settings only if there is no subevent in the cart + return min(terms) else: return until.datetime(self.event) @@ -672,8 +672,8 @@ class Order(LockModel, LoggedModel): terms = [ until.datetime(se) for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True)) - ] - return min(terms) if terms else None + ] or [until.datetime(self.event)] # use event settings only if there is no subevent in the cart + return min(terms) else: return until.datetime(self.event) @@ -890,8 +890,8 @@ class Order(LockModel, LoggedModel): dates = [ modify_deadline.datetime(se) for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True)) - ] - return min(dates) if dates else None + ] or [modify_deadline.datetime(self.event)] # use event settings only if there is no subevent in the cart + return min(dates) elif modify_deadline: return modify_deadline.datetime(self.event) return None @@ -953,8 +953,8 @@ class Order(LockModel, LoggedModel): dates = [ dl_date.datetime(se) for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True)) - ] - dl_date = min(dates) if dates else None + ] or [dl_date.datetime(self.event)] # use event settings only if there is no subevent in the cart + dl_date = min(dates) else: dl_date = dl_date.datetime(self.event) return dl_date @@ -983,7 +983,7 @@ class Order(LockModel, LoggedModel): terms = [ term_last.datetime(se).date() for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True)) - ] + ] or [term_last.datetime(self.event)] # use event settings only if there is no subevent in the cart if terms: term_last = min(terms) else: diff --git a/src/pretix/base/models/waitinglist.py b/src/pretix/base/models/waitinglist.py index 2e90ec0694..c59b6f5454 100644 --- a/src/pretix/base/models/waitinglist.py +++ b/src/pretix/base/models/waitinglist.py @@ -300,9 +300,7 @@ class WaitingListEntry(LoggedModel): @staticmethod def clean_subevent(event, subevent): if event.has_subevents: - if not subevent: - raise ValidationError(_('Subevent cannot be null for event series.')) - if event != subevent.event: + if subevent and event != subevent.event: raise ValidationError(_('The subevent does not belong to this event.')) else: if subevent: diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 4dc51c0b42..aadd9537b2 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -637,6 +637,7 @@ class BasePaymentProvider: def _absolute_availability_date(self, rel_date, cart_id=None, order=None, aggregate_fn=min): if not rel_date: return None + if self.event.has_subevents and cart_id: dates = [ rel_date.datetime(se).date() @@ -645,16 +646,16 @@ class BasePaymentProvider: cart_id=cart_id, event=self.event ).values_list('subevent', flat=True) ) - ] - return aggregate_fn(dates) if dates else None + ] or [rel_date.datetime(self.event).date()] # Use event dates only on carts with no subevents + return aggregate_fn(dates) elif self.event.has_subevents and order: dates = [ rel_date.datetime(se).date() for se in self.event.subevents.filter( id__in=order.positions.values_list('subevent', flat=True) ) - ] - return aggregate_fn(dates) if dates else None + ] or [rel_date.datetime(self.event).date()] # Use event dates only on carts with no subevents + return aggregate_fn(dates) elif self.event.has_subevents: raise NotImplementedError('Payment provider is not subevent-ready.') else: diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index a59735daf2..e86fd829ac 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -793,7 +793,7 @@ class CartManager: operations = [] for i in items: - if self.event.has_subevents and i.get('subevents'): + if self.event.has_subevents and i.get('subevent'): subevent = self._subevents_cache[int(i.get('subevent'))] else: subevent = None diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py index 75653e9081..c7ea6b70b1 100644 --- a/src/pretix/base/services/invoices.py +++ b/src/pretix/base/services/invoices.py @@ -289,7 +289,7 @@ def build_invoice(invoice: Invoice) -> Invoice: answ.to_string_i18n() ) - if invoice.event.has_subevents: + if invoice.event.has_subevents and p.subevent_id: desc += "
" + pgettext("subevent", "Date: {}").format(p.subevent) if invoice.event.settings.invoice_event_location and location and len(locations) > 1: @@ -423,8 +423,7 @@ def _service_period_for_position(invoice, position, invoice_dt): period_start = position.subevent.date_from period_end = position.subevent.date_to else: - # Currently impossible case, but might not be in the future and never makes - # sense to use the event date here + # Does not make sense to use the parent event date here period_start = invoice_dt period_end = invoice_dt elif invoice.event.settings.invoice_period == "auto_no_event": diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 890fa06409..e8c61cc0c9 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -53,7 +53,7 @@ from django.db.models import ( Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Subquery, Sum, Value, ) -from django.db.models.functions import Cast, Greatest +from django.db.models.functions import Cast, Coalesce, Greatest from django.db.transaction import get_connection from django.dispatch import receiver from django.utils.functional import cached_property @@ -1548,7 +1548,7 @@ def send_download_reminders(sender, **kwargs): if event.has_subevents: qs = qs.annotate( - first_date=Min('all_positions__subevent__date_from') + first_date=Coalesce(Min('all_positions__subevent__date_from'), Value(event.date_from)) ).filter( Q(first_date__gte=today) ) @@ -1954,8 +1954,6 @@ class OrderChangeManager: is_bundled = True else: raise OrderError(self.error_messages['addon_invalid']) - if self.order.event.has_subevents and not subevent: - raise OrderError(self.error_messages['subevent_required']) seated = item.seat_category_mappings.filter(subevent=subevent).exists() if seated and not seat and self.event.settings.seating_choice: diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index fe7398333b..d212dd19f7 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -362,7 +362,7 @@ class OrderPositionAddForm(forms.Form): subevent = forms.ModelChoiceField( SubEvent.objects.none(), label=pgettext_lazy('subevent', 'Date'), - required=True, + required=False, empty_label=None ) @@ -416,7 +416,6 @@ class OrderPositionAddForm(forms.Form): } ) self.fields['subevent'].widget.choices = self.fields['subevent'].choices - self.fields['subevent'].required = True else: del self.fields['subevent'] change_decimal_field(self.fields['price'], order.event.currency) diff --git a/src/pretix/control/forms/waitinglist.py b/src/pretix/control/forms/waitinglist.py index cf77380f60..a68514e617 100644 --- a/src/pretix/control/forms/waitinglist.py +++ b/src/pretix/control/forms/waitinglist.py @@ -59,7 +59,7 @@ class WaitingListEntryEditForm(I18nModelForm): super().__init__(*args, **kwargs) - if self.event.has_subevents: + if self.event.has_subevents and self.instance.subevent_id: self.fields['subevent'].required = True self.fields['subevent'].queryset = self.event.subevents.all() self.fields['subevent'].widget = Select2( diff --git a/src/pretix/control/templates/pretixcontrol/items/quotas.html b/src/pretix/control/templates/pretixcontrol/items/quotas.html index 0da36c9be5..10b5019044 100644 --- a/src/pretix/control/templates/pretixcontrol/items/quotas.html +++ b/src/pretix/control/templates/pretixcontrol/items/quotas.html @@ -150,7 +150,11 @@ {% if request.event.has_subevents %} - {{ q.subevent.name }} – {{ q.subevent.get_date_range_display_with_times }} + {% if q.subevent %} + {{ q.subevent.name }} – {{ q.subevent.get_date_range_display_with_times }} + {% else %} + – + {% endif %} {% endif %} {% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %} diff --git a/src/pretix/control/templates/pretixcontrol/order/change.html b/src/pretix/control/templates/pretixcontrol/order/change.html index 00d29491c4..74513e6301 100644 --- a/src/pretix/control/templates/pretixcontrol/order/change.html +++ b/src/pretix/control/templates/pretixcontrol/order/change.html @@ -113,7 +113,7 @@ {% endif %}
- {{ position.subevent }} + {{ position.subevent|default_if_none:"–" }}
{% bootstrap_field position.form.subevent layout='inline' %} diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/index.html b/src/pretix/control/templates/pretixcontrol/vouchers/index.html index 5069641183..83aa1cc90e 100644 --- a/src/pretix/control/templates/pretixcontrol/vouchers/index.html +++ b/src/pretix/control/templates/pretixcontrol/vouchers/index.html @@ -203,7 +203,11 @@ {% if request.event.has_subevents %} - {{ v.subevent.name }} – {{ v.subevent.get_date_range_display_with_times }} + {% if v.subevent %} + {{ v.subevent.name }} – {{ v.subevent.get_date_range_display_with_times }} + {% else %} + – + {% endif %} {% endif %} diff --git a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html index e1521048a5..d93475cf00 100644 --- a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html +++ b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html @@ -209,7 +209,13 @@ {% endif %} {% if request.event.has_subevents %} - {{ e.subevent }} + + {% if e.subevent %} + {{ e.subevent }} + {% else %} + – + {% endif %} + {% endif %} {{ e.created|date:"SHORT_DATETIME_FORMAT" }} diff --git a/src/pretix/control/views/waitinglist.py b/src/pretix/control/views/waitinglist.py index 592d79a223..34b9f7e29d 100644 --- a/src/pretix/control/views/waitinglist.py +++ b/src/pretix/control/views/waitinglist.py @@ -358,7 +358,7 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa str(w.priority) ] if self.request.event.has_subevents: - row.append(str(w.subevent)) + row.append(str(w.subevent) if w.subevent else '') writer.writerow(row) r = HttpResponse(output.getvalue().encode("utf-8"), content_type='text/csv') diff --git a/src/pretix/plugins/checkinlists/exporters.py b/src/pretix/plugins/checkinlists/exporters.py index b1d9e5e574..1af9a7aa2f 100644 --- a/src/pretix/plugins/checkinlists/exporters.py +++ b/src/pretix/plugins/checkinlists/exporters.py @@ -418,7 +418,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter): str(op.item) + (" – " + str(op.variation.value) if op.variation else ""), money_filter(op.price, self.event.currency), ) - if self.event.has_subevents and not cl.subevent: + if self.event.has_subevents and op.subevent and not cl.subevent: item += '\n{} ({})'.format( op.subevent.name, date_format(op.subevent.date_from.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT') @@ -612,14 +612,19 @@ class CSVCheckinList(CheckInListMixin, ListExporter): row.append(op.attendee_email or (op.addon_to.attendee_email if op.addon_to else '') or op.order.email or '') row.append(str(op.order.phone) if op.order.phone else '') if self.event.has_subevents: - row.append(str(op.subevent.name)) - row.append(date_format(op.subevent.date_from.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT')) - if op.subevent.date_to: - row.append( - date_format(op.subevent.date_to.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT') - ) + if op.subevent: + row.append(str(op.subevent.name)) + row.append(date_format(op.subevent.date_from.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT')) + if op.subevent.date_to: + row.append( + date_format(op.subevent.date_to.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT') + ) + else: + row.append('') else: row.append('') + row.append('') + row.append('') acache = {} if op.addon_to: for a in op.addon_to.answers.all(): @@ -722,14 +727,19 @@ class CSVCheckinCodeList(CheckInListMixin, ListExporter): _('Yes') if op.order.status == Order.STATUS_PAID else _('No'), ] if self.event.has_subevents: - row.append(str(op.subevent.name)) - row.append(date_format(op.subevent.date_from.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT')) - if op.subevent.date_to: - row.append( - date_format(op.subevent.date_to.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT') - ) + if op.subevent: + row.append(str(op.subevent.name)) + row.append(date_format(op.subevent.date_from.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT')) + if op.subevent.date_to: + row.append( + date_format(op.subevent.date_to.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT') + ) + else: + row.append('') else: row.append('') + row.append('') + row.append('') row += [ date_format(op.valid_from, 'SHORT_DATETIME_FORMAT') if op.valid_from else '', diff --git a/src/pretix/plugins/sendmail/models.py b/src/pretix/plugins/sendmail/models.py index b68916c010..83519db320 100644 --- a/src/pretix/plugins/sendmail/models.py +++ b/src/pretix/plugins/sendmail/models.py @@ -55,7 +55,7 @@ class ScheduledMail(models.Model): id = models.BigAutoField(primary_key=True) rule = models.ForeignKey("Rule", on_delete=models.CASCADE) - subevent = models.ForeignKey(SubEvent, null=True, on_delete=models.CASCADE) + subevent = models.ForeignKey(SubEvent, null=True, on_delete=models.CASCADE) # must be set in a series, other case unsupported event = models.ForeignKey(Event, on_delete=models.CASCADE) last_computed = models.DateTimeField(auto_now_add=True) diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index 6285c67fc1..d9179e26ad 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -814,7 +814,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView if hasattr(self, 'voucher') and self.voucher.subevent: self.subevent = self.voucher.subevent - if not err and not self.subevent: + if not err and not self.subevent: # TODO return redirect_to_url( eventreverse( self.request.event, 'presale:event.index', diff --git a/src/pretix/presale/views/waiting.py b/src/pretix/presale/views/waiting.py index f94fdbc9a9..c75240019f 100644 --- a/src/pretix/presale/views/waiting.py +++ b/src/pretix/presale/views/waiting.py @@ -29,7 +29,7 @@ from django.shortcuts import get_object_or_404, redirect, render from django.utils.decorators import method_decorator from django.utils.functional import cached_property from django.utils.timezone import now -from django.utils.translation import gettext_lazy as _, pgettext_lazy +from django.utils.translation import gettext_lazy as _ from django.views.generic import FormView, TemplateView from pretix.base.models import Quota, SubEvent @@ -158,8 +158,7 @@ class WaitingView(EventViewMixin, FormView): except ValueError: raise Http404() else: - messages.error(request, pgettext_lazy('subevent', "You need to select a date.")) - return redirect(self.get_index_url()) + self.subevent = None if not (self.subevent or self.request.event).waiting_list_active: messages.error(request, _("Waiting lists are disabled for this event."))