diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index abac72ff89..ff953320fc 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 4914d59be3..93fe9d09d6 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 efefa894b5..98102b94d4 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 3bc3e7eb90..4468b0698e 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 3c997101b2..77fe13100d 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 8cd0a74c42..49f6fcdc98 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 9b908c7fab..1b2998c8c2 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 d46a28ad7c..2aae7294d7 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 565626b7cc..6bf8a9c854 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 25d7dbd992..c4b32e461b 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 569084cca5..7aec699b7a 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 acbde9ce69..84831fd107 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 85795ced4b..99a85ef810 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 775e0f31be..4dc33ba436 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 f2e62687e4..5f8b51a3bf 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 0f463c34ab..5da114ee7b 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 e839498f4d..33afa7ec79 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 b2c3ba7b10..2a896962f9 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 4ed921aa26..0d81201b9e 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)