From f10bdec2342fa6046e88c0290ff92b873e98c085 Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Fri, 14 Nov 2025 15:12:21 +0530 Subject: [PATCH 01/11] fix: Immediately trigger waiting list notifications - Added signal receivers for order_canceled and order_changed events - Trigger waiting list assignment when orders are canceled - Trigger waiting list when quota size is increased - Trigger waiting list when quota is manually reopened - Fixes field name from 'item' to 'product' for compatibility This change ensures waiting list members receive voucher notifications immediately instead of waiting up to 30 minutes for the periodic task. Fixes #1253 --- app/eventyay/base/services/waitinglist.py | 86 ++++++++++++++++++++--- app/eventyay/control/views/product.py | 44 ++++++++++++ 2 files changed, 121 insertions(+), 9 deletions(-) diff --git a/app/eventyay/base/services/waitinglist.py b/app/eventyay/base/services/waitinglist.py index 4caf8b01e0..f9f12b3de4 100644 --- a/app/eventyay/base/services/waitinglist.py +++ b/app/eventyay/base/services/waitinglist.py @@ -9,7 +9,7 @@ from eventyay.base.models import Event, User, WaitingListEntry from eventyay.base.models.waitinglist import WaitingListException from eventyay.base.services.tasks import EventTask -from eventyay.base.signals import periodic_task +from eventyay.base.signals import order_canceled, order_changed, periodic_task from eventyay.celery_app import app @@ -25,8 +25,8 @@ def assign_automatically(event: Event, user_id: int = None, subevent_id: int = N qs = ( WaitingListEntry.objects.filter(event=event, voucher__isnull=True) - .select_related('item', 'variation', 'subevent') - .prefetch_related('item__quotas', 'variation__quotas') + .select_related('product', 'variation', 'subevent') + .prefetch_related('product__quotas', 'variation__quotas') .order_by('-priority', 'created') ) @@ -38,7 +38,7 @@ def assign_automatically(event: Event, user_id: int = None, subevent_id: int = N with event.lock(): for wle in qs: - if (wle.item, wle.variation, wle.subevent) in gone: + if (wle.product, wle.variation, wle.subevent) in gone: continue ev = wle.subevent or event @@ -46,19 +46,19 @@ def assign_automatically(event: Event, user_id: int = None, subevent_id: int = N continue if wle.subevent and not wle.subevent.presale_is_running: continue - if not wle.item.is_available(): - gone.add((wle.item, wle.variation, wle.subevent)) + if not wle.product.is_available(): + gone.add((wle.product, wle.variation, wle.subevent)) continue quotas = ( wle.variation.quotas.filter(subevent=wle.subevent) if wle.variation - else wle.item.quotas.filter(subevent=wle.subevent) + else wle.product.quotas.filter(subevent=wle.subevent) ) availability = ( wle.variation.check_quotas(count_waitinglist=False, _cache=quota_cache, subevent=wle.subevent) if wle.variation - else wle.item.check_quotas(count_waitinglist=False, _cache=quota_cache, subevent=wle.subevent) + else wle.product.check_quotas(count_waitinglist=False, _cache=quota_cache, subevent=wle.subevent) ) if availability[1] is None or availability[1] > 0: try: @@ -74,7 +74,7 @@ def assign_automatically(event: Event, user_id: int = None, subevent_id: int = N quota_cache[q.pk][1] - 1 if quota_cache[q.pk][1] is not None else sys.maxsize, ) else: - gone.add((wle.item, wle.variation, wle.subevent)) + gone.add((wle.product, wle.variation, wle.subevent)) return sent @@ -96,3 +96,71 @@ def process_waitinglist(sender, **kwargs): for e in qs: if e.settings.waiting_list_auto and (e.presale_is_running or e.has_subevents): assign_automatically.apply_async(args=(e.pk,)) + + +@receiver(signal=order_canceled, dispatch_uid='waitinglist_order_canceled') +def on_order_canceled(sender, order, **kwargs): + """ + When an order is canceled, immediately trigger waiting list assignment + if automatic assignment is enabled for the event. + """ + event = sender + + # Check if waiting list auto-assignment is enabled + if not event.settings.get('waiting_list_enabled', as_type=bool): + return + + if not event.settings.get('waiting_list_auto', as_type=bool): + return + + # Check if event is still selling tickets + if not (event.presale_is_running or event.has_subevents): + return + + # Get unique subevents from canceled order positions + subevents = set() + for position in order.positions.all(): + if position.subevent: + subevents.add(position.subevent.pk) + + # Trigger assignment for the main event + if not subevents or not event.has_subevents: + assign_automatically.apply_async(args=(event.pk,)) + else: + # Trigger assignment for each affected subevent + for subevent_id in subevents: + assign_automatically.apply_async(args=(event.pk, None, subevent_id)) + + +@receiver(signal=order_changed, dispatch_uid='waitinglist_order_changed') +def on_order_changed(sender, order, **kwargs): + """ + When an order is modified (e.g., positions canceled), immediately trigger + waiting list assignment if automatic assignment is enabled for the event. + """ + event = sender + + # Check if waiting list auto-assignment is enabled + if not event.settings.get('waiting_list_enabled', as_type=bool): + return + + if not event.settings.get('waiting_list_auto', as_type=bool): + return + + # Check if event is still selling tickets + if not (event.presale_is_running or event.has_subevents): + return + + # Get unique subevents from order positions + subevents = set() + for position in order.positions.all(): + if position.subevent: + subevents.add(position.subevent.pk) + + # Trigger assignment for the main event + if not subevents or not event.has_subevents: + assign_automatically.apply_async(args=(event.pk,)) + else: + # Trigger assignment for each affected subevent + for subevent_id in subevents: + assign_automatically.apply_async(args=(event.pk, None, subevent_id)) diff --git a/app/eventyay/control/views/product.py b/app/eventyay/control/views/product.py index 290ad6796e..df1a7fec9c 100644 --- a/app/eventyay/control/views/product.py +++ b/app/eventyay/control/views/product.py @@ -1045,6 +1045,18 @@ def post(self, request, *args, **kwargs): quota.save(update_fields=['closed']) quota.log_action('pretix.event.quota.opened', user=request.user) messages.success(request, _('The quota has been re-opened.')) + + # Trigger waiting list assignment when quota is reopened + event = request.event + if event.settings.get('waiting_list_enabled', as_type=bool) and \ + event.settings.get('waiting_list_auto', as_type=bool) and \ + (event.presale_is_running or event.has_subevents): + from eventyay.base.services.waitinglist import assign_automatically + if quota.subevent: + assign_automatically.apply_async(args=(event.pk, request.user.pk, quota.subevent.pk)) + else: + assign_automatically.apply_async(args=(event.pk, request.user.pk)) + if 'disable' in request.POST: quota.closed = False quota.close_when_sold_out = False @@ -1056,6 +1068,18 @@ def post(self, request, *args, **kwargs): data={'close_when_sold_out': False}, ) messages.success(request, _('The quota has been re-opened and will not close again.')) + + # Trigger waiting list assignment when quota is reopened + event = request.event + if event.settings.get('waiting_list_enabled', as_type=bool) and \ + event.settings.get('waiting_list_auto', as_type=bool) and \ + (event.presale_is_running or event.has_subevents): + from eventyay.base.services.waitinglist import assign_automatically + if quota.subevent: + assign_automatically.apply_async(args=(event.pk, request.user.pk, quota.subevent.pk)) + else: + assign_automatically.apply_async(args=(event.pk, request.user.pk)) + return redirect( reverse( 'control:event.products.quotas.show', @@ -1111,6 +1135,26 @@ def form_valid(self, form): data={'id': form.instance.pk}, ) form.instance.rebuild_cache() + + # Trigger waiting list assignment if quota size increased + if 'size' in form.changed_data: + old_size = form.initial.get('size') + new_size = form.cleaned_data.get('size') + # Check if size actually increased (handle None as unlimited) + if (old_size is not None and new_size is not None and new_size > old_size) or \ + (old_size is not None and new_size is None): + # Quota increased, trigger waiting list assignment if enabled + event = self.request.event + if event.settings.get('waiting_list_enabled', as_type=bool) and \ + event.settings.get('waiting_list_auto', as_type=bool) and \ + (event.presale_is_running or event.has_subevents): + from eventyay.base.services.waitinglist import assign_automatically + if form.instance.subevent: + assign_automatically.apply_async( + args=(event.pk, self.request.user.pk, form.instance.subevent.pk) + ) + else: + assign_automatically.apply_async(args=(event.pk, self.request.user.pk)) return super().form_valid(form) def get_success_url(self) -> str: From 63a7fde0a28a199a59379803a3ec10d09a64d9e5 Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Thu, 20 Nov 2025 14:45:49 +0530 Subject: [PATCH 02/11] refactor: Extract common logic from signal receivers into helper function --- app/eventyay/base/services/waitinglist.py | 50 ++++++++--------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/app/eventyay/base/services/waitinglist.py b/app/eventyay/base/services/waitinglist.py index f9f12b3de4..866805d383 100644 --- a/app/eventyay/base/services/waitinglist.py +++ b/app/eventyay/base/services/waitinglist.py @@ -98,14 +98,14 @@ def process_waitinglist(sender, **kwargs): assign_automatically.apply_async(args=(e.pk,)) -@receiver(signal=order_canceled, dispatch_uid='waitinglist_order_canceled') -def on_order_canceled(sender, order, **kwargs): - """ - When an order is canceled, immediately trigger waiting list assignment - if automatic assignment is enabled for the event. +def _trigger_waitinglist_for_order(event, order): """ - event = sender + Helper function to trigger waiting list assignment for an order's affected subevents. + This function checks if waiting list auto-assignment is enabled and if the event + is still selling tickets, then triggers assignment for the main event or each + affected subevent. + """ # Check if waiting list auto-assignment is enabled if not event.settings.get('waiting_list_enabled', as_type=bool): return @@ -117,7 +117,7 @@ def on_order_canceled(sender, order, **kwargs): if not (event.presale_is_running or event.has_subevents): return - # Get unique subevents from canceled order positions + # Get unique subevents from order positions subevents = set() for position in order.positions.all(): if position.subevent: @@ -132,35 +132,19 @@ def on_order_canceled(sender, order, **kwargs): assign_automatically.apply_async(args=(event.pk, None, subevent_id)) +@receiver(signal=order_canceled, dispatch_uid='waitinglist_order_canceled') +def on_order_canceled(sender, order, **kwargs): + """ + When an order is canceled, immediately trigger waiting list assignment + if automatic assignment is enabled for the event. + """ + _trigger_waitinglist_for_order(sender, order) + + @receiver(signal=order_changed, dispatch_uid='waitinglist_order_changed') def on_order_changed(sender, order, **kwargs): """ When an order is modified (e.g., positions canceled), immediately trigger waiting list assignment if automatic assignment is enabled for the event. """ - event = sender - - # Check if waiting list auto-assignment is enabled - if not event.settings.get('waiting_list_enabled', as_type=bool): - return - - if not event.settings.get('waiting_list_auto', as_type=bool): - return - - # Check if event is still selling tickets - if not (event.presale_is_running or event.has_subevents): - return - - # Get unique subevents from order positions - subevents = set() - for position in order.positions.all(): - if position.subevent: - subevents.add(position.subevent.pk) - - # Trigger assignment for the main event - if not subevents or not event.has_subevents: - assign_automatically.apply_async(args=(event.pk,)) - else: - # Trigger assignment for each affected subevent - for subevent_id in subevents: - assign_automatically.apply_async(args=(event.pk, None, subevent_id)) + _trigger_waitinglist_for_order(sender, order) From 7ba42a486d65196bab9149f7c9e8e6d78817f005 Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Thu, 20 Nov 2025 14:57:09 +0530 Subject: [PATCH 03/11] refactor: Extract duplicate quota waiting list trigger logic into helper function --- app/eventyay/control/views/product.py | 58 +++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/app/eventyay/control/views/product.py b/app/eventyay/control/views/product.py index e6d83c5ec1..889331ec21 100644 --- a/app/eventyay/control/views/product.py +++ b/app/eventyay/control/views/product.py @@ -76,6 +76,32 @@ from . import ChartContainingView, CreateView, PaginationMixin, UpdateView +def _trigger_quota_waitinglist(event, quota, user): + """ + Helper function to trigger waiting list assignment when a quota is reopened + or increased. + + This function checks if waiting list auto-assignment is enabled and if the + event is still selling tickets, then triggers assignment for the quota's + subevent or the main event. + """ + if not event.settings.get('waiting_list_enabled', as_type=bool): + return + + if not event.settings.get('waiting_list_auto', as_type=bool): + return + + if not (event.presale_is_running or event.has_subevents): + return + + from eventyay.base.services.waitinglist import assign_automatically + + if quota.subevent: + assign_automatically.apply_async(args=(event.pk, user.pk, quota.subevent.pk)) + else: + assign_automatically.apply_async(args=(event.pk, user.pk)) + + class ProductList(ListView): model = Product context_object_name = 'products' @@ -1047,15 +1073,7 @@ def post(self, request, *args, **kwargs): messages.success(request, _('The quota has been re-opened.')) # Trigger waiting list assignment when quota is reopened - event = request.event - if event.settings.get('waiting_list_enabled', as_type=bool) and \ - event.settings.get('waiting_list_auto', as_type=bool) and \ - (event.presale_is_running or event.has_subevents): - from eventyay.base.services.waitinglist import assign_automatically - if quota.subevent: - assign_automatically.apply_async(args=(event.pk, request.user.pk, quota.subevent.pk)) - else: - assign_automatically.apply_async(args=(event.pk, request.user.pk)) + _trigger_quota_waitinglist(request.event, quota, request.user) if 'disable' in request.POST: quota.closed = False @@ -1070,15 +1088,7 @@ def post(self, request, *args, **kwargs): messages.success(request, _('The quota has been re-opened and will not close again.')) # Trigger waiting list assignment when quota is reopened - event = request.event - if event.settings.get('waiting_list_enabled', as_type=bool) and \ - event.settings.get('waiting_list_auto', as_type=bool) and \ - (event.presale_is_running or event.has_subevents): - from eventyay.base.services.waitinglist import assign_automatically - if quota.subevent: - assign_automatically.apply_async(args=(event.pk, request.user.pk, quota.subevent.pk)) - else: - assign_automatically.apply_async(args=(event.pk, request.user.pk)) + _trigger_quota_waitinglist(request.event, quota, request.user) return redirect( reverse( @@ -1144,17 +1154,7 @@ def form_valid(self, form): if (old_size is not None and new_size is not None and new_size > old_size) or \ (old_size is not None and new_size is None): # Quota increased, trigger waiting list assignment if enabled - event = self.request.event - if event.settings.get('waiting_list_enabled', as_type=bool) and \ - event.settings.get('waiting_list_auto', as_type=bool) and \ - (event.presale_is_running or event.has_subevents): - from eventyay.base.services.waitinglist import assign_automatically - if form.instance.subevent: - assign_automatically.apply_async( - args=(event.pk, self.request.user.pk, form.instance.subevent.pk) - ) - else: - assign_automatically.apply_async(args=(event.pk, self.request.user.pk)) + _trigger_quota_waitinglist(self.request.event, form.instance, self.request.user) return super().form_valid(form) def get_success_url(self) -> str: From 3f8d7da3ff888a15eaac206c311328ffdc1d4a56 Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Thu, 20 Nov 2025 15:08:43 +0530 Subject: [PATCH 04/11] Update app/eventyay/base/services/waitinglist.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/eventyay/base/services/waitinglist.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/eventyay/base/services/waitinglist.py b/app/eventyay/base/services/waitinglist.py index 866805d383..4e71f4eedf 100644 --- a/app/eventyay/base/services/waitinglist.py +++ b/app/eventyay/base/services/waitinglist.py @@ -118,10 +118,7 @@ def _trigger_waitinglist_for_order(event, order): return # Get unique subevents from order positions - subevents = set() - for position in order.positions.all(): - if position.subevent: - subevents.add(position.subevent.pk) + subevents = set(order.positions.filter(subevent__isnull=False).values_list('subevent_id', flat=True)) # Trigger assignment for the main event if not subevents or not event.has_subevents: From 0195612f925a2c226e27053d8bdc41cadf96282c Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Thu, 20 Nov 2025 15:08:55 +0530 Subject: [PATCH 05/11] Update app/eventyay/control/views/product.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/eventyay/control/views/product.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/eventyay/control/views/product.py b/app/eventyay/control/views/product.py index 889331ec21..c4048799ba 100644 --- a/app/eventyay/control/views/product.py +++ b/app/eventyay/control/views/product.py @@ -1145,7 +1145,6 @@ def form_valid(self, form): data={'id': form.instance.pk}, ) form.instance.rebuild_cache() - # Trigger waiting list assignment if quota size increased if 'size' in form.changed_data: old_size = form.initial.get('size') From b8caf3c85fcbb65050d42f9194d31e69f040dfcf Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Thu, 20 Nov 2025 15:09:08 +0530 Subject: [PATCH 06/11] Update app/eventyay/control/views/product.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/eventyay/control/views/product.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/eventyay/control/views/product.py b/app/eventyay/control/views/product.py index c4048799ba..1eec9a0974 100644 --- a/app/eventyay/control/views/product.py +++ b/app/eventyay/control/views/product.py @@ -1074,7 +1074,6 @@ def post(self, request, *args, **kwargs): # Trigger waiting list assignment when quota is reopened _trigger_quota_waitinglist(request.event, quota, request.user) - if 'disable' in request.POST: quota.closed = False quota.close_when_sold_out = False From 2efbc5e2f082163d7f855f671d9836086f05c6b0 Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Thu, 20 Nov 2025 15:09:16 +0530 Subject: [PATCH 07/11] Update app/eventyay/control/views/product.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/eventyay/control/views/product.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/eventyay/control/views/product.py b/app/eventyay/control/views/product.py index 1eec9a0974..c94f7775d1 100644 --- a/app/eventyay/control/views/product.py +++ b/app/eventyay/control/views/product.py @@ -1088,7 +1088,6 @@ def post(self, request, *args, **kwargs): # Trigger waiting list assignment when quota is reopened _trigger_quota_waitinglist(request.event, quota, request.user) - return redirect( reverse( 'control:event.products.quotas.show', From d7c364864375101df032c28d432ba4a8b2e49189 Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Thu, 20 Nov 2025 15:09:31 +0530 Subject: [PATCH 08/11] Update app/eventyay/control/views/product.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/eventyay/control/views/product.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/eventyay/control/views/product.py b/app/eventyay/control/views/product.py index c94f7775d1..8fe54de7f1 100644 --- a/app/eventyay/control/views/product.py +++ b/app/eventyay/control/views/product.py @@ -1071,7 +1071,6 @@ def post(self, request, *args, **kwargs): quota.save(update_fields=['closed']) quota.log_action('eventyay.event.quota.opened', user=request.user) messages.success(request, _('The quota has been re-opened.')) - # Trigger waiting list assignment when quota is reopened _trigger_quota_waitinglist(request.event, quota, request.user) if 'disable' in request.POST: From 1e2e6cdfae00167a39e58a79a09418eed2b41775 Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Thu, 20 Nov 2025 15:09:41 +0530 Subject: [PATCH 09/11] Update app/eventyay/control/views/product.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/eventyay/control/views/product.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/eventyay/control/views/product.py b/app/eventyay/control/views/product.py index 8fe54de7f1..8e1c63aa82 100644 --- a/app/eventyay/control/views/product.py +++ b/app/eventyay/control/views/product.py @@ -1084,7 +1084,6 @@ def post(self, request, *args, **kwargs): data={'close_when_sold_out': False}, ) messages.success(request, _('The quota has been re-opened and will not close again.')) - # Trigger waiting list assignment when quota is reopened _trigger_quota_waitinglist(request.event, quota, request.user) return redirect( From 0516c8ba51d2d741cbfdab70ca9538693f77d14d Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Thu, 20 Nov 2025 15:18:47 +0530 Subject: [PATCH 10/11] fix: Use all_positions to include canceled positions for waiting list --- app/eventyay/base/services/waitinglist.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/eventyay/base/services/waitinglist.py b/app/eventyay/base/services/waitinglist.py index 4e71f4eedf..251882a54a 100644 --- a/app/eventyay/base/services/waitinglist.py +++ b/app/eventyay/base/services/waitinglist.py @@ -117,8 +117,10 @@ def _trigger_waitinglist_for_order(event, order): if not (event.presale_is_running or event.has_subevents): return - # Get unique subevents from order positions - subevents = set(order.positions.filter(subevent__isnull=False).values_list('subevent_id', flat=True)) + # Get unique subevents from ALL order positions (including canceled ones) + # This is critical: order.positions excludes canceled positions, but we need to check + # canceled positions to know which subevents had tickets freed up + subevents = set(order.all_positions.filter(subevent__isnull=False).values_list('subevent_id', flat=True)) # Trigger assignment for the main event if not subevents or not event.has_subevents: From 61043b4f1863b5d1e357409ea540395c91aa3057 Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Thu, 20 Nov 2025 15:28:29 +0530 Subject: [PATCH 11/11] Update app/eventyay/control/views/product.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/eventyay/control/views/product.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/eventyay/control/views/product.py b/app/eventyay/control/views/product.py index 8e1c63aa82..b278998e11 100644 --- a/app/eventyay/control/views/product.py +++ b/app/eventyay/control/views/product.py @@ -97,7 +97,7 @@ def _trigger_quota_waitinglist(event, quota, user): from eventyay.base.services.waitinglist import assign_automatically if quota.subevent: - assign_automatically.apply_async(args=(event.pk, user.pk, quota.subevent.pk)) + assign_automatically.apply_async(args=(event.pk, user.pk, quota.subevent_id)) else: assign_automatically.apply_async(args=(event.pk, user.pk))