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
This commit is contained in:
Raphael Michel
2024-03-22 11:17:02 +01:00
committed by GitHub
parent a946c10ab4
commit 273c1ae0a6
19 changed files with 109 additions and 14 deletions
+1
View File
@@ -687,6 +687,7 @@ class EventSettingsSerializer(SettingsSerializer):
'allow_modifications_after_checkin', 'allow_modifications_after_checkin',
'show_quota_left', 'show_quota_left',
'waiting_list_enabled', 'waiting_list_enabled',
'waiting_list_auto_disable',
'waiting_list_hours', 'waiting_list_hours',
'waiting_list_auto', 'waiting_list_auto',
'waiting_list_names_asked', 'waiting_list_names_asked',
+8
View File
@@ -229,6 +229,14 @@ class EventMixin:
else: else:
return self.presale_end 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 @property
def presale_has_ended(self): def presale_has_ended(self):
""" """
+5
View File
@@ -446,6 +446,11 @@ class QuotaAvailability:
self.results[q] = Quota.AVAILABILITY_RESERVED, 0 self.results[q] = Quota.AVAILABILITY_RESERVED, 0
def _compute_waitinglist(self, quotas, q_items, q_vars, size_left): 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} events = {q.event_id for q in quotas}
subevents = {q.subevent_id for q in quotas} subevents = {q.subevent_id for q in quotas}
quota_ids = {q.pk for q in quotas} quota_ids = {q.pk for q in quotas}
+3
View File
@@ -110,6 +110,9 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
continue continue
if wle.subevent and not wle.subevent.presale_is_running: if wle.subevent and not wle.subevent.presale_is_running:
continue 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(): if not wle.item.is_available():
gone.add((wle.item, wle.variation, wle.subevent)) gone.add((wle.item, wle.variation, wle.subevent))
continue continue
+13
View File
@@ -1397,6 +1397,19 @@ DEFAULTS = {
widget=forms.NumberInput(), 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': { 'waiting_list_names_asked': {
'default': 'False', 'default': 'False',
'type': bool, 'type': bool,
+1
View File
@@ -538,6 +538,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
'region', 'region',
'show_quota_left', 'show_quota_left',
'waiting_list_enabled', 'waiting_list_enabled',
'waiting_list_auto_disable',
'waiting_list_hours', 'waiting_list_hours',
'waiting_list_auto', 'waiting_list_auto',
'waiting_list_names_asked', 'waiting_list_names_asked',
@@ -361,6 +361,7 @@
{% bootstrap_field sform.waiting_list_enabled layout="control" %} {% bootstrap_field sform.waiting_list_enabled layout="control" %}
{% bootstrap_field sform.waiting_list_auto layout="control" %} {% bootstrap_field sform.waiting_list_auto layout="control" %}
{% bootstrap_field sform.waiting_list_hours 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_names_asked_required layout="control" %}
{% bootstrap_field sform.waiting_list_phones_asked_required layout="control" %} {% bootstrap_field sform.waiting_list_phones_asked_required layout="control" %}
{% bootstrap_field sform.waiting_list_phones_explanation_text layout="control" %} {% bootstrap_field sform.waiting_list_phones_explanation_text layout="control" %}
@@ -10,6 +10,10 @@
<div class="alert alert-danger"> <div class="alert alert-danger">
{% 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." %} {% 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." %}
</div> </div>
{% elif not request.event.waiting_list_active and not request.event.has_subevents %}
<div class="alert alert-danger">
{% trans "The waiting list is no longer active for this event. The waiting list no longer affects quotas and no longer notifies waiting users." %}
</div>
{% endif %} {% endif %}
<div class="row"> <div class="row">
{% if 'can_change_orders' in request.eventpermset %} {% if 'can_change_orders' in request.eventpermset %}
@@ -31,7 +31,7 @@
<span class="label label-success">{% trans "Book now" %}</span> <span class="label label-success">{% trans "Book now" %}</span>
{% endif %} {% endif %}
{% 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 %}
<span class="label label-warning">{% trans "Waiting list" %}</span> <span class="label label-warning">{% trans "Waiting list" %}</span>
{% elif subev.best_availability_state == 20 %} {% elif subev.best_availability_state == 20 %}
<span class="label label-danger">{% trans "Reserved" %}</span> <span class="label label-danger">{% trans "Reserved" %}</span>
@@ -25,7 +25,7 @@
{% if event.event.presale_is_running and show_avail %} {% if event.event.presale_is_running and show_avail %}
{% if event.event.best_availability_state == 100 %} {% if event.event.best_availability_state == 100 %}
available {% if event.event.best_availability_is_low %} low {% endif %} 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 waitinglist
{% elif event.event.best_availability_state == 20 %} {% elif event.event.best_availability_state == 20 %}
reserved reserved
@@ -74,7 +74,7 @@
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %} <span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
{% endif %} {% endif %}
{% 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 %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %} <span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %}
{% elif event.event.best_availability_state == 20 %} {% elif event.event.best_availability_state == 20 %}
{% trans "Reserved" %} {% trans "Reserved" %}
@@ -41,7 +41,7 @@
{% if event.event.presale_is_running and show_avail %} {% if event.event.presale_is_running and show_avail %}
{% if event.event.best_availability_state == 100 %} {% if event.event.best_availability_state == 100 %}
available {% if event.event.best_availability_is_low %} low {% endif %} 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 waitinglist
{% elif event.event.best_availability_state == 20 %} {% elif event.event.best_availability_state == 20 %}
reserved reserved
@@ -98,7 +98,7 @@
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %} <span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
{% endif %} {% endif %}
{% 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 %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %} <span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %}
{% elif event.event.best_availability_state == 20 %} {% elif event.event.best_availability_state == 20 %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Reserved" %} <span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Reserved" %}
@@ -13,7 +13,7 @@
{% if event.event.presale_is_running and show_avail %} {% if event.event.presale_is_running and show_avail %}
{% if event.event.best_availability_state == 100 %} {% if event.event.best_availability_state == 100 %}
available {% if event.event.best_availability_is_low %} low {% endif %} 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 waitinglist
{% elif event.event.best_availability_state == 20 %} {% elif event.event.best_availability_state == 20 %}
reserved reserved
@@ -62,7 +62,7 @@
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %} <span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
{% endif %} {% endif %}
{% 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 %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %} <span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %}
{% elif event.event.best_availability_state == 20 %} {% elif event.event.best_availability_state == 20 %}
{% trans "Reserved" %} {% trans "Reserved" %}
@@ -99,7 +99,7 @@
<span class="label label-success">{% trans "Book now" %}</span> <span class="label label-success">{% trans "Book now" %}</span>
{% endif %} {% endif %}
{% 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 %}
<span class="label label-warning">{% trans "Waiting list" %}</span> <span class="label label-warning">{% trans "Waiting list" %}</span>
{% elif e.best_availability_state == 20 %} {% elif e.best_availability_state == 20 %}
<span class="label label-danger">{% trans "Reserved" %}</span> <span class="label label-danger">{% trans "Reserved" %}</span>
+3 -3
View File
@@ -153,13 +153,13 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
Prefetch('quotas', Prefetch('quotas',
to_attr='_subevent_quotas', to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter( queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
subevent=subevent)) subevent=subevent).select_related("subevent"))
).distinct() ).distinct()
) )
prefetch_quotas = Prefetch( prefetch_quotas = Prefetch(
'quotas', 'quotas',
to_attr='_subevent_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( prefetch_bundles = Prefetch(
'bundles', 'bundles',
@@ -548,7 +548,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
context['ev'].presale_is_running 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: if not self.request.event.has_subevents or self.subevent:
# Fetch all items # Fetch all items
+4
View File
@@ -118,6 +118,10 @@ class WaitingView(EventViewMixin, FormView):
messages.error(request, pgettext_lazy('subevent', "You need to select a date.")) messages.error(request, pgettext_lazy('subevent', "You need to select a date."))
return redirect(self.get_index_url()) 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) return super().dispatch(request, *args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
+3 -3
View File
@@ -390,7 +390,7 @@ class WidgetAPIProductList(EventListMixin, View):
else: else:
availability['text'] = gettext('Book now') availability['text'] = gettext('Book now')
availability['reason'] = 'ok' 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['color'] = 'orange'
availability['text'] = gettext('Waiting list') availability['text'] = gettext('Waiting list')
availability['reason'] = 'waitinglist' availability['reason'] = 'waitinglist'
@@ -690,7 +690,7 @@ class WidgetAPIProductList(EventListMixin, View):
'display_net_prices': request.event.settings.display_net_prices, 'display_net_prices': request.event.settings.display_net_prices,
'use_native_spinners': request.event.settings.widget_use_native_spinners, 'use_native_spinners': request.event.settings.widget_use_native_spinners,
'show_variations_expanded': request.event.settings.show_variations_expanded, '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)), 'voucher_explanation_text': str(rich_text(request.event.settings.voucher_explanation_text, safelinks=False)),
'error': None, 'error': None,
'cart_exists': False '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'] = ev.seating_plan is not None
data['has_seating_plan_waitinglist'] = False 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: for i in items:
if not i.allow_waitinglist or not i.requires_seat: if not i.allow_waitinglist or not i.requires_seat:
continue continue
+19
View File
@@ -436,6 +436,25 @@ class QuotaTestCase(BaseQuotaTestCase):
self.assertEqual(qa.count_cart[self.quota], 1) self.assertEqual(qa.count_cart[self.quota], 1)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 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') @classscope(attr='o')
def test_waitinglist_item_active(self): def test_waitinglist_item_active(self):
self.quota.items.add(self.item1) self.quota.items.add(self.item1)
+20
View File
@@ -30,6 +30,7 @@ from pretix.base.models import (
Event, Item, ItemVariation, Organizer, Quota, Voucher, WaitingListEntry, Event, Item, ItemVariation, Organizer, Quota, Voucher, WaitingListEntry,
) )
from pretix.base.models.waitinglist import WaitingListException from pretix.base.models.waitinglist import WaitingListException
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
from pretix.base.services.waitinglist import ( from pretix.base.services.waitinglist import (
assign_automatically, process_waitinglist, assign_automatically, process_waitinglist,
) )
@@ -210,6 +211,25 @@ class WaitingListTestCase(TestCase):
self.event.presale_end = now() + timedelta(days=1) self.event.presale_end = now() + timedelta(days=1)
self.event.save() 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): def test_send_periodic(self):
self.event.settings.set('waiting_list_enabled', True) self.event.settings.set('waiting_list_enabled', True)
self.event.settings.set('waiting_list_auto', True) self.event.settings.set('waiting_list_auto', True)
+16
View File
@@ -54,6 +54,7 @@ from pretix.base.models import (
User, WaitingListEntry, User, WaitingListEntry,
) )
from pretix.base.models.items import SubEventItem, SubEventItemVariation from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
class EventTestMixin: class EventTestMixin:
@@ -1021,6 +1022,21 @@ class WaitingListTest(EventTestMixin, SoupTest):
) )
self.assertEqual(response.status_code, 302) 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): def test_display_link(self):
response = self.client.get( response = self.client.get(
'/%s/%s/' % (self.orga.slug, self.event.slug) '/%s/%s/' % (self.orga.slug, self.event.slug)