first round of fixes

This commit is contained in:
Raphael Michel
2026-09-23 11:33:55 +02:00
parent 0545fda71d
commit 999754f541
22 changed files with 80 additions and 71 deletions
+1 -5
View File
@@ -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.'
)
+1 -5
View File
@@ -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.'
)
+6 -2
View File
@@ -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.'
)
+1
View File
@@ -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')),
+3 -9
View File
@@ -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:
+9 -9
View File
@@ -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:
+1 -3
View File
@@ -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:
+5 -4
View File
@@ -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:
+1 -1
View File
@@ -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
+2 -3
View File
@@ -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 += "<br />" + 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":
+2 -4
View File
@@ -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:
+1 -2
View File
@@ -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)
+1 -1
View File
@@ -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(
@@ -150,7 +150,11 @@
</td>
{% if request.event.has_subevents %}
<td>
{{ 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 %}
</td>
{% endif %}
<td>{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}</td>
@@ -113,7 +113,7 @@
{% endif %}
</div>
<div class="col-sm-5">
{{ position.subevent }}
{{ position.subevent|default_if_none:"–" }}
</div>
<div class="col-sm-4">
{% bootstrap_field position.form.subevent layout='inline' %}
@@ -203,7 +203,11 @@
</td>
{% if request.event.has_subevents %}
<td>
{{ 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 %}
</td>
{% endif %}
<td class="text-right flip">
@@ -209,7 +209,13 @@
{% endif %}
</td>
{% if request.event.has_subevents %}
<td>{{ e.subevent }}</td>
<td>
{% if e.subevent %}
{{ e.subevent }}
{% else %}
–
{% endif %}
</td>
{% endif %}
<td>
{{ e.created|date:"SHORT_DATETIME_FORMAT" }}
+1 -1
View File
@@ -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')
+23 -13
View File
@@ -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 '',
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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',
+2 -3
View File
@@ -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."))