Improvements around the waiting list (#2219)

* Waiting list: Support for seated events, pre-fill customer email address

* Allow people to remove themselves

* Update src/pretix/base/settings.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/control/views/waitinglist.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/control/views/waitinglist.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/control/views/waitinglist.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/presale/views/waiting.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Resolve a review note

* Review notes

* Fix linter issues

* Fix import

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2021-09-27 20:48:02 +02:00
committed by GitHub
parent a9a4cf6fca
commit 9f2ffc3276
15 changed files with 359 additions and 62 deletions

View File

@@ -462,6 +462,16 @@ def base_placeholders(sender, **kwargs):
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
),
SimpleFunctionalMailTextPlaceholder(
'url_remove', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: build_absolute_uri(
event, 'presale:event.waitinglist.remove'
) + '?voucher=' + waiting_list_entry.voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.waitinglist.remove',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalMailTextPlaceholder(
'url', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: build_absolute_uri(

View File

@@ -21,8 +21,9 @@
#
from datetime import timedelta
from django.core.exceptions import ValidationError
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models, transaction
from django.db.models import F, Q, Sum
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
@@ -114,9 +115,12 @@ class WaitingListEntry(LoggedModel):
return '%s waits for %s' % (str(self.email), str(self.item))
def clean(self):
WaitingListEntry.clean_duplicate(self.email, self.item, self.variation, self.subevent, self.pk)
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
WaitingListEntry.clean_subevent(self.event, self.subevent)
try:
WaitingListEntry.clean_duplicate(self.email, self.item, self.variation, self.subevent, self.pk)
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
WaitingListEntry.clean_subevent(self.event, self.subevent)
except ObjectDoesNotExist:
raise ValidationError('Invalid input')
def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', [])
@@ -147,6 +151,34 @@ class WaitingListEntry(LoggedModel):
)
if availability[1] is None or availability[1] < 1:
raise WaitingListException(_('This product is currently not available.'))
ev = self.subevent or self.event
if ev.seat_category_mappings.filter(product=self.item).exists():
# Generally, we advertise the waiting list to be based on quotas only. This makes it dangerous
# to use in combination with seating plans. If your event has 50 seats and a quota of 50 and
# default settings, everything is fine and the waiting list will work as usual. However, as soon
# as those two numbers diverge, either due to misconfiguration or due to intentional features such
# as our COVID-19 minimum distance feature, things get ugly. Theoretically, there could be
# significant quota available but not a single seat! The waiting list would happily send out vouchers
# which do not work at all. Generally, we consider this a "known bug" and not fixable with the current
# design of the waiting list and seating features.
# However, we've put in a simple safeguard that makes sure the waiting list on its own does not screw
# everything up. Specifically, we will not send out vouchers if the number of available seats is less
# than the number of valid vouchers *issued through the waiting list*. Things can still go wrong due to
# manually created vouchers, manually blocked seats or the minimum distance feature, but this reduces
# the possible damage a bit.
num_free_seats_for_product = ev.free_seats().filter(product=self.item).count()
num_valid_vouchers_for_product = self.event.vouchers.filter(
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
block_quota=True,
item_id=self.item_id,
subevent_id=self.subevent_id,
waitinglistentries__isnull=False
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
if not free_seats:
raise WaitingListException(_('No seat with this product is currently available.'))
if self.voucher:
raise WaitingListException(_('A voucher has already been sent to this person.'))
if '@' not in self.email:

View File

@@ -22,12 +22,14 @@
import sys
from datetime import timedelta
from django.db.models import Exists, OuterRef, Q
from django.db.models import Exists, F, OuterRef, Q, Sum
from django.dispatch import receiver
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.base.models import Event, User, WaitingListEntry
from pretix.base.models import (
Event, SeatCategoryMapping, User, WaitingListEntry,
)
from pretix.base.models.waitinglist import WaitingListException
from pretix.base.services.tasks import EventTask
from pretix.base.signals import periodic_task
@@ -43,6 +45,19 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
quota_cache = {}
gone = set()
seats_available = {}
for m in SeatCategoryMapping.objects.filter(event=event).select_related('subevent'):
# See comment in WaitingListEntry.send_voucher() for rationale
num_free_seets_for_product = (m.subevent or event).free_seats().filter(product_id=m.product_id).count()
num_valid_vouchers_for_product = event.vouchers.filter(
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
block_quota=True,
item_id=m.product_id,
subevent_id=m.subevent_id,
waitinglistentries__isnull=False
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
seats_available[(m.product_id, m.subevent_id)] = num_free_seets_for_product - num_valid_vouchers_for_product
qs = WaitingListEntry.objects.filter(
event=event, voucher__isnull=True
@@ -70,6 +85,11 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
gone.add((wle.item, wle.variation, wle.subevent))
continue
if (wle.item_id, wle.subevent_id) in seats_available:
if seats_available[wle.item_id, wle.subevent_id] < 1:
gone.add((wle.item, wle.variation, wle.subevent))
continue
quotas = (wle.variation.quotas.filter(subevent=wle.subevent)
if wle.variation
else wle.item.quotas.filter(subevent=wle.subevent))
@@ -91,6 +111,9 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
quota_cache[q.pk][0] if quota_cache[q.pk][0] > 1 else 0,
quota_cache[q.pk][1] - 1 if quota_cache[q.pk][1] is not None else sys.maxsize
)
if (wle.item_id, wle.subevent_id) in seats_available:
seats_available[wle.item_id, wle.subevent_id] -= 1
else:
gone.add((wle.item, wle.variation, wle.subevent))

View File

@@ -1746,6 +1746,12 @@ Please note that this link is only valid within the next {hours} hours!
We will reassign the ticket to the next person on the list if you do not
redeem the voucher within that timeframe.
If you do NOT need a ticket any more, we kindly ask you to click the
following link to let us know. This way, we can send the ticket as quickly
as possible to the next person on the waiting list:
{url_remove}
Best regards,
Your {event} team"""))
},