Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 60 additions & 9 deletions app/eventyay/base/services/waitinglist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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')
)

Expand All @@ -38,27 +38,27 @@ 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
if not ev.presale_is_running or (wle.subevent and not wle.subevent.active):
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:
Expand All @@ -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

Expand All @@ -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)
39 changes: 39 additions & 0 deletions app/eventyay/control/views/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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',
Expand Down Expand Up @@ -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:
Expand Down