diff --git a/src/pretix/base/services/waitinglist.py b/src/pretix/base/services/waitinglist.py index 4468b0698..59e912a9f 100644 --- a/src/pretix/base/services/waitinglist.py +++ b/src/pretix/base/services/waitinglist.py @@ -20,6 +20,7 @@ # . # import sys +from collections import defaultdict from datetime import timedelta from django.db import transaction @@ -49,19 +50,28 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None) quota_cache = {} gone = set() - seats_available = {} + _seats_available_cache = {} + seats_used = defaultdict(int) - for m in SeatCategoryMapping.objects.filter(event=event).select_related('subevent'): + seated_product_set = set( + SeatCategoryMapping.objects.filter(event=event).values_list('product_id', 'subevent_id') + ) + + def _seats_available(item, 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 + subevent_id = subevent.pk if subevent else None + if (item.pk, subevent_id) not in _seats_available_cache: + num_free_seats_for_product = (subevent or event).free_seats().filter(product_id=item.pk).count() + num_valid_vouchers_for_product = event.vouchers.filter( + Q(valid_until__isnull=True) | Q(valid_until__gte=now()), + block_quota=True, + item_id=item.pk, + subevent_id=subevent_id, + waitinglistentries__isnull=False + ).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0 + _seats_available_cache[item.pk, subevent_id] = num_free_seats_for_product - num_valid_vouchers_for_product + + return _seats_available_cache[item.pk, subevent_id] - seats_used[item.pk, subevent_id] prefetch_related_objects( [event.organizer], @@ -103,7 +113,7 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None) lock_objects(quotas, shared_lock_objects=[event]) for wle in qs: - if (wle.item, wle.variation, wle.subevent) in gone: + if (wle.item_id, wle.variation_id, wle.subevent_id) in gone: continue ev = (wle.subevent or event) if not ev.presale_is_running or (wle.subevent and not wle.subevent.active): @@ -111,15 +121,15 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None) 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)) + gone.add((wle.item_id, wle.variation_id, wle.subevent_id)) continue if not wle.item.is_available(): - gone.add((wle.item, wle.variation, wle.subevent)) + gone.add((wle.item_id, wle.variation_id, wle.subevent_id)) 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)) + if (wle.item_id, wle.subevent_id) in seated_product_set: + if _seats_available(wle.item, wle.subevent) < 1: + gone.add((wle.item_id, wle.variation_id, wle.subevent_id)) continue availability = ( @@ -141,10 +151,10 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None) 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 + if (wle.item_id, wle.subevent_id) in seated_product_set: + seats_used[wle.item_id, wle.subevent_id] += 1 else: - gone.add((wle.item, wle.variation, wle.subevent)) + gone.add((wle.item_id, wle.variation_id, wle.subevent_id)) return sent diff --git a/src/tests/base/test_waitinglist.py b/src/tests/base/test_waitinglist.py index 2a896962f..ec7472d7f 100644 --- a/src/tests/base/test_waitinglist.py +++ b/src/tests/base/test_waitinglist.py @@ -194,6 +194,26 @@ class WaitingListTestCase(TestCase): assert WaitingListEntry.objects.filter(voucher__isnull=True).count() == 10 assert Voucher.objects.count() == 10 + def test_send_auto_no_seat(self): + with scope(organizer=self.o): + self.quota.items.add(self.item1) + self.quota.size = 10 + self.quota.save() + self.event.seat_category_mappings.create( + layout_category='Stalls', product=self.item1 + ) + self.event.seats.create(seat_number="Foo", product=self.item1, seat_guid="Foo", blocked=True) + self.event.seats.create(seat_number="Bar", product=self.item1, seat_guid="Bar", blocked=True) + self.event.seats.create(seat_number="Baz", product=self.item1, seat_guid="Baz", blocked=True) + WaitingListEntry.objects.create( + event=self.event, item=self.item1, email='foo@bar.com' + ) + assign_automatically.apply(args=(self.event.pk,)) + assert Voucher.objects.count() == 0 + self.event.seats.create(seat_number="Baz", product=self.item1, seat_guid="Baz", blocked=False) + assign_automatically.apply(args=(self.event.pk,)) + assert Voucher.objects.count() == 1 + def test_send_periodic_event_over(self): self.event.settings.set('waiting_list_enabled', True) self.event.settings.set('waiting_list_auto', True)