From 273c1ae0a66bf3aefc46a83d75caea17bfde2afd Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Fri, 22 Mar 2024 11:17:02 +0100 Subject: [PATCH] Waiting list: Allow to set auto-disable date (Z#23141338) (#4004) * Waiting list: Allow to set auto-disable date (Z#23141338) * ADd warning on non-esries events --- src/pretix/api/serializers/event.py | 1 + src/pretix/base/models/event.py | 8 ++++++++ src/pretix/base/services/quotas.py | 5 +++++ src/pretix/base/services/waitinglist.py | 3 +++ src/pretix/base/settings.py | 13 ++++++++++++ src/pretix/control/forms/event.py | 1 + .../pretixcontrol/event/settings.html | 1 + .../pretixcontrol/waitinglist/index.html | 4 ++++ .../event/fragment_subevent_list.html | 2 +- .../pretixpresale/fragment_calendar.html | 4 ++-- .../pretixpresale/fragment_day_calendar.html | 4 ++-- .../pretixpresale/fragment_week_calendar.html | 4 ++-- .../pretixpresale/organizers/index.html | 2 +- src/pretix/presale/views/event.py | 6 +++--- src/pretix/presale/views/waiting.py | 4 ++++ src/pretix/presale/views/widget.py | 6 +++--- src/tests/base/test_models.py | 19 ++++++++++++++++++ src/tests/base/test_waitinglist.py | 20 +++++++++++++++++++ src/tests/presale/test_event.py | 16 +++++++++++++++ 19 files changed, 109 insertions(+), 14 deletions(-) diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index abac72ff8..ff953320f 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -687,6 +687,7 @@ class EventSettingsSerializer(SettingsSerializer): 'allow_modifications_after_checkin', 'show_quota_left', 'waiting_list_enabled', + 'waiting_list_auto_disable', 'waiting_list_hours', 'waiting_list_auto', 'waiting_list_names_asked', diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 4914d59be..93fe9d09d 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -229,6 +229,14 @@ class EventMixin: else: return self.presale_end + @property + 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) > now() + return True + @property def presale_has_ended(self): """ diff --git a/src/pretix/base/services/quotas.py b/src/pretix/base/services/quotas.py index efefa894b..98102b94d 100644 --- a/src/pretix/base/services/quotas.py +++ b/src/pretix/base/services/quotas.py @@ -446,6 +446,11 @@ class QuotaAvailability: self.results[q] = Quota.AVAILABILITY_RESERVED, 0 def _compute_waitinglist(self, quotas, q_items, q_vars, size_left): + quotas = [ + q for q in quotas + if not q.event.settings.waiting_list_auto_disable or q.event.settings.waiting_list_auto_disable.datetime(q.subevent or q.event) > now() + ] + events = {q.event_id for q in quotas} subevents = {q.subevent_id for q in quotas} quota_ids = {q.pk for q in quotas} diff --git a/src/pretix/base/services/waitinglist.py b/src/pretix/base/services/waitinglist.py index 3bc3e7eb9..4468b0698 100644 --- a/src/pretix/base/services/waitinglist.py +++ b/src/pretix/base/services/waitinglist.py @@ -110,6 +110,9 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None) continue if wle.subevent and not wle.subevent.presale_is_running: continue + if event.settings.waiting_list_auto_disable and event.settings.waiting_list_auto_disable.datetime(wle.subevent or event) <= now(): + gone.add((wle.item, wle.variation, wle.subevent)) + continue if not wle.item.is_available(): gone.add((wle.item, wle.variation, wle.subevent)) continue diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 3c997101b..77fe13100 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -1397,6 +1397,19 @@ DEFAULTS = { widget=forms.NumberInput(), ) }, + 'waiting_list_auto_disable': { + 'default': None, + 'type': RelativeDateWrapper, + 'form_class': RelativeDateTimeField, + 'serializer_class': SerializerRelativeDateTimeField, + 'form_kwargs': dict( + label=_("Disable waiting list"), + help_text=_("The waiting list will be fully disabled after this date. This means that nobody can add " + "themselves to the waiting list any more, but also that tickets will be available for sale " + "again if quota permits, even if there are still people on the waiting list. Vouchers that " + "have already been sent remain active."), + ) + }, 'waiting_list_names_asked': { 'default': 'False', 'type': bool, diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 8cd0a74c4..49f6fcdc9 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -538,6 +538,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett 'region', 'show_quota_left', 'waiting_list_enabled', + 'waiting_list_auto_disable', 'waiting_list_hours', 'waiting_list_auto', 'waiting_list_names_asked', diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html index 9b908c7fa..1b2998c8c 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings.html @@ -361,6 +361,7 @@ {% bootstrap_field sform.waiting_list_enabled layout="control" %} {% bootstrap_field sform.waiting_list_auto layout="control" %} {% bootstrap_field sform.waiting_list_hours layout="control" %} + {% bootstrap_field sform.waiting_list_auto_disable layout="control" %} {% bootstrap_field sform.waiting_list_names_asked_required layout="control" %} {% bootstrap_field sform.waiting_list_phones_asked_required layout="control" %} {% bootstrap_field sform.waiting_list_phones_explanation_text layout="control" %} diff --git a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html index d46a28ad7..2aae7294d 100644 --- a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html +++ b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html @@ -10,6 +10,10 @@
{% trans "The waiting list is disabled, so if the event is sold out, people cannot add themselves to this list. If you want to enable it, go to the event settings." %}
+ {% elif not request.event.waiting_list_active and not request.event.has_subevents %} +
+ {% trans "The waiting list is no longer active for this event. The waiting list no longer affects quotas and no longer notifies waiting users." %} +
{% endif %}
{% if 'can_change_orders' in request.eventpermset %} diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html index 565626b7c..6bf8a9c85 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html @@ -31,7 +31,7 @@ {% trans "Book now" %} {% endif %} {% endif %} - {% elif event.settings.waiting_list_enabled and subev.best_availability_state >= 0 %} + {% elif event.waiting_list_active and subev.best_availability_state >= 0 %} {% trans "Waiting list" %} {% elif subev.best_availability_state == 20 %} {% trans "Reserved" %} diff --git a/src/pretix/presale/templates/pretixpresale/fragment_calendar.html b/src/pretix/presale/templates/pretixpresale/fragment_calendar.html index 25d7dbd99..c4b32e461 100644 --- a/src/pretix/presale/templates/pretixpresale/fragment_calendar.html +++ b/src/pretix/presale/templates/pretixpresale/fragment_calendar.html @@ -25,7 +25,7 @@ {% if event.event.presale_is_running and show_avail %} {% if event.event.best_availability_state == 100 %} available {% if event.event.best_availability_is_low %} low {% endif %} - {% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %} + {% elif event.event.waiting_list_active and event.event.best_availability_state >= 0 %} waitinglist {% elif event.event.best_availability_state == 20 %} reserved @@ -74,7 +74,7 @@ {% trans "Book now" %} {% endif %} {% endif %} - {% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %} + {% elif event.event.waiting_list_active and event.event.best_availability_state >= 0 %} {% trans "Waiting list" %} {% elif event.event.best_availability_state == 20 %} {% trans "Reserved" %} diff --git a/src/pretix/presale/templates/pretixpresale/fragment_day_calendar.html b/src/pretix/presale/templates/pretixpresale/fragment_day_calendar.html index 569084cca..7aec699b7 100644 --- a/src/pretix/presale/templates/pretixpresale/fragment_day_calendar.html +++ b/src/pretix/presale/templates/pretixpresale/fragment_day_calendar.html @@ -41,7 +41,7 @@ {% if event.event.presale_is_running and show_avail %} {% if event.event.best_availability_state == 100 %} available {% if event.event.best_availability_is_low %} low {% endif %} - {% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %} + {% elif event.event.waiting_list_active and event.event.best_availability_state >= 0 %} waitinglist {% elif event.event.best_availability_state == 20 %} reserved @@ -98,7 +98,7 @@ {% trans "Book now" %} {% endif %} {% endif %} - {% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %} + {% elif event.event.waiting_list_active and event.event.best_availability_state >= 0 %} {% trans "Waiting list" %} {% elif event.event.best_availability_state == 20 %} {% trans "Reserved" %} diff --git a/src/pretix/presale/templates/pretixpresale/fragment_week_calendar.html b/src/pretix/presale/templates/pretixpresale/fragment_week_calendar.html index acbde9ce6..84831fd10 100644 --- a/src/pretix/presale/templates/pretixpresale/fragment_week_calendar.html +++ b/src/pretix/presale/templates/pretixpresale/fragment_week_calendar.html @@ -13,7 +13,7 @@ {% if event.event.presale_is_running and show_avail %} {% if event.event.best_availability_state == 100 %} available {% if event.event.best_availability_is_low %} low {% endif %} - {% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %} + {% elif event.event.waiting_list_active and event.event.best_availability_state >= 0 %} waitinglist {% elif event.event.best_availability_state == 20 %} reserved @@ -62,7 +62,7 @@ {% trans "Book now" %} {% endif %} {% endif %} - {% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %} + {% elif event.event.waiting_list_active and event.event.best_availability_state >= 0 %} {% trans "Waiting list" %} {% elif event.event.best_availability_state == 20 %} {% trans "Reserved" %} diff --git a/src/pretix/presale/templates/pretixpresale/organizers/index.html b/src/pretix/presale/templates/pretixpresale/organizers/index.html index 85795ced4..99a85ef81 100644 --- a/src/pretix/presale/templates/pretixpresale/organizers/index.html +++ b/src/pretix/presale/templates/pretixpresale/organizers/index.html @@ -99,7 +99,7 @@ {% trans "Book now" %} {% endif %} {% endif %} - {% elif e.settings.waiting_list_enabled and e.best_availability_state >= 0 %} + {% elif e.waiting_list_active and e.best_availability_state >= 0 %} {% trans "Waiting list" %} {% elif e.best_availability_state == 20 %} {% trans "Reserved" %} diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index 775e0f31b..4dc33ba43 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -153,13 +153,13 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require Prefetch('quotas', to_attr='_subevent_quotas', queryset=event.quotas.using(settings.DATABASE_REPLICA).filter( - subevent=subevent)) + subevent=subevent).select_related("subevent")) ).distinct() ) prefetch_quotas = Prefetch( 'quotas', to_attr='_subevent_quotas', - queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent) + queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent).select_related("subevent") ) prefetch_bundles = Prefetch( 'bundles', @@ -548,7 +548,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): context['ev'].presale_is_running ) - context['allow_waitinglist'] = self.request.event.settings.waiting_list_enabled and context['ev'].presale_is_running + context['allow_waitinglist'] = context['ev'].waiting_list_active and context['ev'].presale_is_running if not self.request.event.has_subevents or self.subevent: # Fetch all items diff --git a/src/pretix/presale/views/waiting.py b/src/pretix/presale/views/waiting.py index f2e62687e..5f8b51a3b 100644 --- a/src/pretix/presale/views/waiting.py +++ b/src/pretix/presale/views/waiting.py @@ -118,6 +118,10 @@ class WaitingView(EventViewMixin, FormView): messages.error(request, pgettext_lazy('subevent', "You need to select a date.")) return redirect(self.get_index_url()) + if not (self.subevent or self.request.event).waiting_list_active: + messages.error(request, _("Waiting lists are disabled for this event.")) + return redirect(self.get_index_url()) + return super().dispatch(request, *args, **kwargs) def form_valid(self, form): diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py index 0f463c34a..5da114ee7 100644 --- a/src/pretix/presale/views/widget.py +++ b/src/pretix/presale/views/widget.py @@ -390,7 +390,7 @@ class WidgetAPIProductList(EventListMixin, View): else: availability['text'] = gettext('Book now') availability['reason'] = 'ok' - elif event.settings.waiting_list_enabled and (ev.best_availability_state is not None and ev.best_availability_state >= 0): + elif event.waiting_list_active and (ev.best_availability_state is not None and ev.best_availability_state >= 0): availability['color'] = 'orange' availability['text'] = gettext('Waiting list') availability['reason'] = 'waitinglist' @@ -690,7 +690,7 @@ class WidgetAPIProductList(EventListMixin, View): 'display_net_prices': request.event.settings.display_net_prices, 'use_native_spinners': request.event.settings.widget_use_native_spinners, 'show_variations_expanded': request.event.settings.show_variations_expanded, - 'waiting_list_enabled': request.event.settings.waiting_list_enabled, + 'waiting_list_enabled': request.event.waiting_list_active, 'voucher_explanation_text': str(rich_text(request.event.settings.voucher_explanation_text, safelinks=False)), 'error': None, 'cart_exists': False @@ -775,7 +775,7 @@ class WidgetAPIProductList(EventListMixin, View): data['has_seating_plan'] = ev.seating_plan is not None data['has_seating_plan_waitinglist'] = False - if request.event.settings.waiting_list_enabled and ev.presale_is_running: + if ev.waiting_list_active and ev.presale_is_running: for i in items: if not i.allow_waitinglist or not i.requires_seat: continue diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py index e839498f4..33afa7ec7 100644 --- a/src/tests/base/test_models.py +++ b/src/tests/base/test_models.py @@ -436,6 +436,25 @@ class QuotaTestCase(BaseQuotaTestCase): self.assertEqual(qa.count_cart[self.quota], 1) self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1)) + @classscope(attr='o') + def test_waitinglist_auto_disable(self): + self.event.settings.waiting_list_auto_disable = RelativeDateWrapper( + RelativeDate(days=0, time=None, base_date_name='date_from', minutes=20, is_after=True) + ) + self.quota.items.add(self.item1) + self.quota.size = 1 + self.quota.save() + WaitingListEntry.objects.create( + event=self.event, item=self.item1, email='foo@bar.com' + ) + self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0)) + self.assertEqual(self.item1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1)) + self.event.settings.waiting_list_auto_disable = RelativeDateWrapper( + RelativeDate(days=0, time=None, base_date_name='date_from', minutes=20, is_after=False) + ) + self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1)) + self.assertEqual(self.item1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1)) + @classscope(attr='o') def test_waitinglist_item_active(self): self.quota.items.add(self.item1) diff --git a/src/tests/base/test_waitinglist.py b/src/tests/base/test_waitinglist.py index b2c3ba7b1..2a896962f 100644 --- a/src/tests/base/test_waitinglist.py +++ b/src/tests/base/test_waitinglist.py @@ -30,6 +30,7 @@ from pretix.base.models import ( Event, Item, ItemVariation, Organizer, Quota, Voucher, WaitingListEntry, ) from pretix.base.models.waitinglist import WaitingListException +from pretix.base.reldate import RelativeDate, RelativeDateWrapper from pretix.base.services.waitinglist import ( assign_automatically, process_waitinglist, ) @@ -210,6 +211,25 @@ class WaitingListTestCase(TestCase): self.event.presale_end = now() + timedelta(days=1) self.event.save() + def test_send_periodic_auto_disable(self): + self.event.settings.set('waiting_list_enabled', True) + self.event.settings.set('waiting_list_auto', True) + self.event.settings.waiting_list_auto_disable = RelativeDateWrapper( + RelativeDate(days=0, time=None, base_date_name='date_from', minutes=20, is_after=False) + ) + self.event.save() + with scope(organizer=self.o): + for i in range(5): + WaitingListEntry.objects.create( + event=self.event, item=self.item2, variation=self.var1, email='foo{}@bar.com'.format(i) + ) + process_waitinglist(None) + with scope(organizer=self.o): + assert WaitingListEntry.objects.filter(voucher__isnull=True).count() == 5 + assert Voucher.objects.count() == 0 + self.event.presale_end = now() + timedelta(days=1) + self.event.save() + def test_send_periodic(self): self.event.settings.set('waiting_list_enabled', True) self.event.settings.set('waiting_list_auto', True) diff --git a/src/tests/presale/test_event.py b/src/tests/presale/test_event.py index 4ed921aa2..0d81201b9 100644 --- a/src/tests/presale/test_event.py +++ b/src/tests/presale/test_event.py @@ -54,6 +54,7 @@ from pretix.base.models import ( User, WaitingListEntry, ) from pretix.base.models.items import SubEventItem, SubEventItemVariation +from pretix.base.reldate import RelativeDate, RelativeDateWrapper class EventTestMixin: @@ -1021,6 +1022,21 @@ class WaitingListTest(EventTestMixin, SoupTest): ) self.assertEqual(response.status_code, 302) + def test_auto_disable(self): + self.event.settings.set('waiting_list_enabled', True) + self.event.settings.waiting_list_auto_disable = RelativeDateWrapper( + RelativeDate(days=900, time=datetime.time(9, 0, 0), base_date_name='date_from', minutes=None, is_after=False) + ) + response = self.client.get( + '/%s/%s/' % (self.orga.slug, self.event.slug) + ) + self.assertEqual(response.status_code, 200) + self.assertNotIn('waitinglist', response.rendered_content) + response = self.client.get( + '/%s/%s/waitinglist/?item=%d' % (self.orga.slug, self.event.slug, self.item.pk + 1) + ) + self.assertEqual(response.status_code, 302) + def test_display_link(self): response = self.client.get( '/%s/%s/' % (self.orga.slug, self.event.slug)