diff --git a/app/eventyay/base/services/waitinglist.py b/app/eventyay/base/services/waitinglist.py index 4caf8b01e0..251882a54a 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,54 @@ 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,)) + + +def _trigger_waitinglist_for_order(event, order): + """ + 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 + + 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 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: + 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_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. + """ + _trigger_waitinglist_for_order(sender, order) diff --git a/app/eventyay/control/views/product.py b/app/eventyay/control/views/product.py index e4cfd26b7a..b278998e11 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_id)) + else: + assign_automatically.apply_async(args=(event.pk, user.pk)) + + class ProductList(ListView): model = Product context_object_name = 'products' @@ -1045,6 +1071,8 @@ 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: quota.closed = False quota.close_when_sold_out = False @@ -1056,6 +1084,8 @@ 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( reverse( 'control:event.products.quotas.show', @@ -1111,6 +1141,15 @@ 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 + _trigger_quota_waitinglist(self.request.event, form.instance, self.request.user) return super().form_valid(form) def get_success_url(self) -> str: