From c501066cff6b472f098ccc17dbfa30515f76cb28 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 8 Sep 2025 09:56:57 +0200 Subject: [PATCH] Event calendar: Only show "waiting list" if products allow it (Z#23205941) (#5436) * Event calendar: Only show "waiting list" if products allow it * Add a simple test * Review notes * Update src/pretix/base/models/event.py Co-authored-by: Richard Schreiber * Update src/pretix/base/models/event.py Co-authored-by: Richard Schreiber --------- Co-authored-by: Richard Schreiber --- src/pretix/base/models/event.py | 50 ++++++++++++++++++++++++--------- src/tests/base/test_models.py | 19 +++++++++---- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index c12c5913de..5a1cdd4b28 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -243,8 +243,16 @@ class EventMixin: def waiting_list_active(self): if not self.settings.waiting_list_enabled: return False + if self.settings.waiting_list_auto_disable: - return self.settings.waiting_list_auto_disable.datetime(self) > time_machine_now() + if self.settings.waiting_list_auto_disable.datetime(self) <= time_machine_now(): + return False + + if hasattr(self, 'active_quotas'): + # Only run when called with computed quotas, i.e. event calendar + if not self.best_availability[3]: + return False + return True @property @@ -322,9 +330,7 @@ class EventMixin: sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).filter( Q(variations__isnull=True) & Q(quotas__pk=OuterRef('pk')) - ).order_by().values_list('quotas__pk').annotate( - items=GroupConcat('pk', delimiter=',') - ).values('items') + ) q_variation = ( Q(active=True) @@ -357,9 +363,7 @@ class EventMixin: q_variation &= Q(hide_without_voucher=False) q_variation &= Q(item__hide_without_voucher=False) - sq_active_variation = ItemVariation.objects.filter(q_variation).order_by().values_list('quotas__pk').annotate( - items=GroupConcat('pk', delimiter=',') - ).values('items') + sq_active_variation = ItemVariation.objects.filter(q_variation) quota_base_qs = Quota.objects.using(settings.DATABASE_REPLICA).filter( ignore_for_event_availability=False ) @@ -376,8 +380,23 @@ class EventMixin: 'quotas', to_attr='active_quotas', queryset=quota_base_qs.annotate( - active_items=Subquery(sq_active_item, output_field=models.TextField()), - active_variations=Subquery(sq_active_variation, output_field=models.TextField()), + active_items=Subquery( + sq_active_item.order_by().values_list('quotas__pk').annotate( + items=GroupConcat('pk', delimiter=',') + ).values('items'), + output_field=models.TextField() + ), + active_variations=Subquery( + sq_active_variation.order_by().values_list('quotas__pk').annotate( + items=GroupConcat('pk', delimiter=',') + ).values('items'), + output_field=models.TextField()), + has_active_items_with_waitinglist=Exists( + sq_active_item.filter(allow_waitinglist=True), + ), + has_active_variations_with_waitinglist=Exists( + sq_active_variation.filter(item__allow_waitinglist=True), + ), ).exclude( Q(active_items="") & Q(active_variations="") ).select_related('event', 'subevent') @@ -406,11 +425,12 @@ class EventMixin: @cached_property def best_availability(self): """ - Returns a 3-tuple of + Returns a 4-tuple of - The availability state of this event (one of the ``Quota.AVAILABILITY_*`` constants) - The number of tickets currently available (or ``None``) - The number of tickets "originally" available (or ``None``) + - Whether a sold out product has the waiting list enabled This can only be called on objects obtained through a queryset that has been passed through ``.annotated()``. """ @@ -433,6 +453,7 @@ class EventMixin: r = getattr(self, '_quota_cache', {}) quotas_for_item = defaultdict(list) quotas_for_variation = defaultdict(list) + waiting_list_found = False for q in self.active_quotas: if q not in r: r[q] = q.availability(allow_cache=True) @@ -441,6 +462,8 @@ class EventMixin: for item_id in q.active_items.split(","): if item_id not in items_disabled: quotas_for_item[item_id].append(q) + if q.has_active_items_with_waitinglist or q.has_active_variations_with_waitinglist: + waiting_list_found = True if q.active_variations: for var_id in q.active_variations.split(","): if var_id not in vars_disabled: @@ -448,7 +471,7 @@ class EventMixin: if not self.active_quotas or (not quotas_for_item and not quotas_for_variation): # No item is enabled for this event, treat the event as "unknown" - return None, None, None + return None, None, None, waiting_list_found # We iterate over all items and variations and keep track of # - `best_state_found` - the best availability state we have seen so far. If one item is available, the event is available! @@ -467,7 +490,7 @@ class EventMixin: quotas_that_are_not_unlimited = [q for q in quota_list if q.size is not None] if not quotas_that_are_not_unlimited: # We found an unlimited ticket, no more need to do anything else - return Quota.AVAILABILITY_OK, None, None + return Quota.AVAILABILITY_OK, None, None, waiting_list_found if worst_state_for_ticket == Quota.AVAILABILITY_OK: availability_of_this = min(max(0, r[q][1] - quota_used_for_found_tickets[q]) for q in quotas_that_are_not_unlimited) @@ -481,7 +504,8 @@ class EventMixin: quota_used_for_possible_tickets[q] += possible_of_this best_state_found = max(best_state_found, worst_state_for_ticket) - return best_state_found, num_tickets_found, num_tickets_possible + + return best_state_found, num_tickets_found, num_tickets_possible, waiting_list_found def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False): assert isinstance(sales_channel, str) or sales_channel is None diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py index e2a919dcc3..a4091694a1 100644 --- a/src/tests/base/test_models.py +++ b/src/tests/base/test_models.py @@ -2603,7 +2603,7 @@ class SubEventTest(TestCase): q.items.add(item) obj = SubEvent.annotated(SubEvent.objects, 'web').first() assert len(obj.active_quotas) == 1 - assert obj.best_availability == (Quota.AVAILABILITY_GONE, 0, 1) + assert obj.best_availability == (Quota.AVAILABILITY_GONE, 0, 1, True) # 2 quotas - 1 item. Lowest quota wins. q2 = Quota.objects.create(event=self.event, name='Quota 2', size=2, @@ -2611,14 +2611,21 @@ class SubEventTest(TestCase): q2.items.add(item) obj = SubEvent.annotated(SubEvent.objects, 'web').first() assert len(obj.active_quotas) == 2 - assert obj.best_availability == (Quota.AVAILABILITY_GONE, 0, 1) + assert obj.best_availability == (Quota.AVAILABILITY_GONE, 0, 1, True) + + # Same, but waiting list not allowed + item.allow_waitinglist = False + item.save() + obj = SubEvent.annotated(SubEvent.objects, 'web').first() + assert len(obj.active_quotas) == 2 + assert obj.best_availability == (Quota.AVAILABILITY_GONE, 0, 1, False) # 2 quotas - 2 items. Higher quota wins since second item is only connected to second quota. item2 = Item.objects.create(event=self.event, name='Regular ticket', default_price=10, active=True) q2.items.add(item2) obj = SubEvent.annotated(SubEvent.objects, 'web').first() assert len(obj.active_quotas) == 2 - assert obj.best_availability == (Quota.AVAILABILITY_OK, 1, 2) + assert obj.best_availability == (Quota.AVAILABILITY_OK, 1, 2, True) assert obj.best_availability_is_low # 1 quota - 2 items. Quota is not counted twice! @@ -2627,14 +2634,14 @@ class SubEventTest(TestCase): q2.delete() obj = SubEvent.annotated(SubEvent.objects, 'web').first() assert len(obj.active_quotas) == 1 - assert obj.best_availability == (Quota.AVAILABILITY_OK, 9, 10) + assert obj.best_availability == (Quota.AVAILABILITY_OK, 9, 10, False) assert not obj.best_availability_is_low - # Unlimited quota + # Unlimited quota, but no waiting list q.size = None q.save() obj = SubEvent.annotated(SubEvent.objects, 'web').first() - assert obj.best_availability == (Quota.AVAILABILITY_OK, None, None) + assert obj.best_availability == (Quota.AVAILABILITY_OK, None, None, False) assert not obj.best_availability_is_low