forked from CGM_Public/pretix_original
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 <schreiber@rami.io> * Update src/pretix/base/models/event.py Co-authored-by: Richard Schreiber <schreiber@rami.io> --------- Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
@@ -243,8 +243,16 @@ class EventMixin:
|
|||||||
def waiting_list_active(self):
|
def waiting_list_active(self):
|
||||||
if not self.settings.waiting_list_enabled:
|
if not self.settings.waiting_list_enabled:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.settings.waiting_list_auto_disable:
|
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
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -322,9 +330,7 @@ class EventMixin:
|
|||||||
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).filter(
|
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).filter(
|
||||||
Q(variations__isnull=True)
|
Q(variations__isnull=True)
|
||||||
& Q(quotas__pk=OuterRef('pk'))
|
& Q(quotas__pk=OuterRef('pk'))
|
||||||
).order_by().values_list('quotas__pk').annotate(
|
)
|
||||||
items=GroupConcat('pk', delimiter=',')
|
|
||||||
).values('items')
|
|
||||||
|
|
||||||
q_variation = (
|
q_variation = (
|
||||||
Q(active=True)
|
Q(active=True)
|
||||||
@@ -357,9 +363,7 @@ class EventMixin:
|
|||||||
q_variation &= Q(hide_without_voucher=False)
|
q_variation &= Q(hide_without_voucher=False)
|
||||||
q_variation &= Q(item__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(
|
sq_active_variation = ItemVariation.objects.filter(q_variation)
|
||||||
items=GroupConcat('pk', delimiter=',')
|
|
||||||
).values('items')
|
|
||||||
quota_base_qs = Quota.objects.using(settings.DATABASE_REPLICA).filter(
|
quota_base_qs = Quota.objects.using(settings.DATABASE_REPLICA).filter(
|
||||||
ignore_for_event_availability=False
|
ignore_for_event_availability=False
|
||||||
)
|
)
|
||||||
@@ -376,8 +380,23 @@ class EventMixin:
|
|||||||
'quotas',
|
'quotas',
|
||||||
to_attr='active_quotas',
|
to_attr='active_quotas',
|
||||||
queryset=quota_base_qs.annotate(
|
queryset=quota_base_qs.annotate(
|
||||||
active_items=Subquery(sq_active_item, output_field=models.TextField()),
|
active_items=Subquery(
|
||||||
active_variations=Subquery(sq_active_variation, output_field=models.TextField()),
|
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(
|
).exclude(
|
||||||
Q(active_items="") & Q(active_variations="")
|
Q(active_items="") & Q(active_variations="")
|
||||||
).select_related('event', 'subevent')
|
).select_related('event', 'subevent')
|
||||||
@@ -406,11 +425,12 @@ class EventMixin:
|
|||||||
@cached_property
|
@cached_property
|
||||||
def best_availability(self):
|
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 availability state of this event (one of the ``Quota.AVAILABILITY_*`` constants)
|
||||||
- The number of tickets currently available (or ``None``)
|
- The number of tickets currently available (or ``None``)
|
||||||
- The number of tickets "originally" 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()``.
|
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', {})
|
r = getattr(self, '_quota_cache', {})
|
||||||
quotas_for_item = defaultdict(list)
|
quotas_for_item = defaultdict(list)
|
||||||
quotas_for_variation = defaultdict(list)
|
quotas_for_variation = defaultdict(list)
|
||||||
|
waiting_list_found = False
|
||||||
for q in self.active_quotas:
|
for q in self.active_quotas:
|
||||||
if q not in r:
|
if q not in r:
|
||||||
r[q] = q.availability(allow_cache=True)
|
r[q] = q.availability(allow_cache=True)
|
||||||
@@ -441,6 +462,8 @@ class EventMixin:
|
|||||||
for item_id in q.active_items.split(","):
|
for item_id in q.active_items.split(","):
|
||||||
if item_id not in items_disabled:
|
if item_id not in items_disabled:
|
||||||
quotas_for_item[item_id].append(q)
|
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:
|
if q.active_variations:
|
||||||
for var_id in q.active_variations.split(","):
|
for var_id in q.active_variations.split(","):
|
||||||
if var_id not in vars_disabled:
|
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):
|
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"
|
# 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
|
# 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!
|
# - `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]
|
quotas_that_are_not_unlimited = [q for q in quota_list if q.size is not None]
|
||||||
if not quotas_that_are_not_unlimited:
|
if not quotas_that_are_not_unlimited:
|
||||||
# We found an unlimited ticket, no more need to do anything else
|
# 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:
|
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)
|
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
|
quota_used_for_possible_tickets[q] += possible_of_this
|
||||||
|
|
||||||
best_state_found = max(best_state_found, worst_state_for_ticket)
|
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):
|
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
||||||
assert isinstance(sales_channel, str) or sales_channel is None
|
assert isinstance(sales_channel, str) or sales_channel is None
|
||||||
|
|||||||
@@ -2603,7 +2603,7 @@ class SubEventTest(TestCase):
|
|||||||
q.items.add(item)
|
q.items.add(item)
|
||||||
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
||||||
assert len(obj.active_quotas) == 1
|
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.
|
# 2 quotas - 1 item. Lowest quota wins.
|
||||||
q2 = Quota.objects.create(event=self.event, name='Quota 2', size=2,
|
q2 = Quota.objects.create(event=self.event, name='Quota 2', size=2,
|
||||||
@@ -2611,14 +2611,21 @@ class SubEventTest(TestCase):
|
|||||||
q2.items.add(item)
|
q2.items.add(item)
|
||||||
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
||||||
assert len(obj.active_quotas) == 2
|
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.
|
# 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)
|
item2 = Item.objects.create(event=self.event, name='Regular ticket', default_price=10, active=True)
|
||||||
q2.items.add(item2)
|
q2.items.add(item2)
|
||||||
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
||||||
assert len(obj.active_quotas) == 2
|
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
|
assert obj.best_availability_is_low
|
||||||
|
|
||||||
# 1 quota - 2 items. Quota is not counted twice!
|
# 1 quota - 2 items. Quota is not counted twice!
|
||||||
@@ -2627,14 +2634,14 @@ class SubEventTest(TestCase):
|
|||||||
q2.delete()
|
q2.delete()
|
||||||
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
||||||
assert len(obj.active_quotas) == 1
|
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
|
assert not obj.best_availability_is_low
|
||||||
|
|
||||||
# Unlimited quota
|
# Unlimited quota, but no waiting list
|
||||||
q.size = None
|
q.size = None
|
||||||
q.save()
|
q.save()
|
||||||
obj = SubEvent.annotated(SubEvent.objects, 'web').first()
|
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
|
assert not obj.best_availability_is_low
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user