Skip to content

Conversation

@ArnavBallinCode
Copy link
Contributor

@ArnavBallinCode ArnavBallinCode commented Nov 14, 2025

(Fix)Immediately trigger waiting list notifications

This PR adds immediate triggers for waiting list processing in three scenarios:

1. Order Cancellations

When an order is canceled, immediately trigger waiting list assignment via signal receiver.

Files Changed:

  • app/eventyay/base/services/waitinglist.py - Added on_order_canceled() receiver

2. Order Position Cancellations

When individual positions are canceled (order modified), immediately trigger waiting list.

Files Changed:

  • app/eventyay/base/services/waitinglist.py - Added on_order_changed() receiver

3. Quota Increases

When organizers increase quota size or reopen closed quotas, immediately trigger waiting list.

Technical Details

Signal Receivers

@receiver(order_canceled, dispatch_uid="waitinglist_order_canceled")
def on_order_canceled(sender, order, **kwargs):
    """Trigger waiting list when an order is canceled"""
    if order.event.settings.waiting_list_enabled and order.event.settings.waiting_list_auto:
        assign_automatically.apply_async(args=(order.event,))

Quota Increase Detection

# In QuotaUpdate.form_valid()
old_size = self.object.size
new_size = form.cleaned_data.get('size')

if (new_size and old_size and new_size > old_size) or \
   (old_size is not None and new_size is None):
    # Quota increased or became unlimited
    if event.settings.waiting_list_enabled and event.settings.waiting_list_auto:
        assign_automatically.apply_async(args=(event,))

Fixed field name from wle.item to wle.product throughout waitinglist.py for compatibility with current model schema.

Email Generated

Subject: You have been selected from the waitinglist for Test WL Event
To: [email protected]

You now have a ticket ready for you! 
Voucher code: 8XKZDB8K663UB6EV
Valid for: 48 hours

Fixes #1253

Summary by Sourcery

Implement immediate triggering of automated waiting list assignments on order cancellations/changes and quota updates, and update model references for consistency

New Features:

  • Add signal receivers to immediately process the waiting list when orders are canceled or modified
  • Trigger waiting list assignment instantly when quotas are reopened or their size is increased in the control views

Enhancements:

  • Rename WaitingListEntry.item references to WaitingListEntry.product for schema compatibility

- 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 fossasia#1253
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Nov 14, 2025

Reviewer's Guide

This PR implements immediate triggers for waiting list assignments by adding signal receivers for order cancellations and modifications, integrating quota reopen hooks in product views, detecting quota size increases in form handling, and refactoring the waiting list processor to use the updated “product” field.

Sequence diagram for immediate waiting list trigger on order cancellation

sequenceDiagram
actor User
participant "Order Service"
participant "Signal Receiver (on_order_canceled)"
participant "Waiting List Processor"
User->>"Order Service": Cancel order
"Order Service"->>"Signal Receiver (on_order_canceled)": Emit order_canceled signal
"Signal Receiver (on_order_canceled)"->>"Waiting List Processor": assign_automatically.apply_async(event, subevent)
"Waiting List Processor"-->>User: Send waiting list notification email
Loading

Sequence diagram for immediate waiting list trigger on quota increase or reopen

sequenceDiagram
actor Organizer
participant "Quota Management View"
participant "Waiting List Processor"
Organizer->>"Quota Management View": Increase quota or reopen quota
"Quota Management View"->>"Waiting List Processor": assign_automatically.apply_async(event, user, subevent)
"Waiting List Processor"-->>Organizer: Waiting list assignment processed
Loading

Class diagram for WaitingListEntry field refactor (item → product)

classDiagram
class WaitingListEntry {
  +product
  +variation
  +subevent
  +voucher
  +priority
  +created
}
class Product
class Variation
class Subevent
class Voucher
WaitingListEntry --> Product : product
WaitingListEntry --> Variation : variation
WaitingListEntry --> Subevent : subevent
WaitingListEntry --> Voucher : voucher
Loading

File-Level Changes

Change Details Files
Refactor waiting list processor to use “product” instead of “item”
  • Replaced select_related('item') with select_related('product')
  • Updated prefetch_related('item__quotas') to prefetch_related('product__quotas')
  • Replaced all wle.item references with wle.product in logic and gone tracking
app/eventyay/base/services/waitinglist.py
Add signal receivers for order cancellations and modifications
  • Implemented on_order_canceled receiver to collect canceled subevents and dispatch assign_automatically
  • Implemented on_order_changed receiver with similar logic for modified orders
  • Enforced waiting list and event sale status checks before triggering assignments
app/eventyay/base/services/waitinglist.py
Trigger waiting list on quota reopen in product views
  • Inserted asynchronous assign_automatically calls after reopening quotas
  • Checked waiting list auto-assignment and event sale state
  • Handled both main event and subevent paths
app/eventyay/control/views/product.py
Trigger waiting list on quota size increase in form handling
  • Detected actual size increases or transition to unlimited
  • Added assign_automatically dispatch in QuotaUpdate.form_valid when size changed
  • Validated waiting list settings and event availability before dispatch
app/eventyay/control/views/product.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1253 Automatically send notification emails to attendees on the waiting list when tickets become available (via order cancellation or quota increase).
#1253 Ensure that notification emails contain valid registration links and functional voucher codes.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • The on_order_canceled and on_order_changed receivers share almost identical logic; consider extracting the common checks and subevent handling into a shared helper to reduce duplication.
  • The waiting list trigger block in control/views/product.py is duplicated in both quota reopening branches; extract it into a single utility or decorator to keep the view code DRY.
  • Relying on the view's form_valid to catch quota size increases may miss other update paths—consider moving the quota change detection into a model signal or service layer to centralize the logic.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The on_order_canceled and on_order_changed receivers share almost identical logic; consider extracting the common checks and subevent handling into a shared helper to reduce duplication.
- The waiting list trigger block in control/views/product.py is duplicated in both quota reopening branches; extract it into a single utility or decorator to keep the view code DRY.
- Relying on the view's form_valid to catch quota size increases may miss other update paths—consider moving the quota change detection into a model signal or service layer to centralize the logic.

## Individual Comments

### Comment 1
<location> `app/eventyay/base/services/waitinglist.py:135-136` </location>
<code_context>
+            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
</code_context>

<issue_to_address>
**suggestion:** The logic for subevent assignment is duplicated between order_canceled and order_changed.

Refactor the shared subevent extraction and assignment logic into a helper function to avoid duplication and enhance maintainability.

Suggested implementation:

```python
def trigger_waitinglist_assignment(event, subevents):
    """
    Helper to trigger waiting list assignment for main event or subevents.
    """
    if not subevents or not event.has_subevents:
        assign_automatically.apply_async(args=(event.pk,))
    else:
        for subevent_id in subevents:
            assign_automatically.apply_async(args=(event.pk, None, subevent_id))

```

```python
    trigger_waitinglist_assignment(event, subevents)

```

You will need to update the corresponding logic in the `on_order_changed` function to use the new `trigger_waitinglist_assignment` helper. 
If the subevent extraction logic is also duplicated in `on_order_changed`, refactor it to use the same approach as the other location and pass the subevents to the helper.
</issue_to_address>

### Comment 2
<location> `app/eventyay/base/services/waitinglist.py:121` </location>
<code_context>
@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))

</code_context>

<issue_to_address>
**issue (code-quality):** Convert for loop into set comprehension ([`set-comprehension`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/set-comprehension/))
</issue_to_address>

### Comment 3
<location> `app/eventyay/base/services/waitinglist.py:155` </location>
<code_context>
@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))

</code_context>

<issue_to_address>
**issue (code-quality):** Convert for loop into set comprehension ([`set-comprehension`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/set-comprehension/))
</issue_to_address>

### Comment 4
<location> `app/eventyay/control/views/product.py:1047` </location>
<code_context>
    def post(self, request, *args, **kwargs):
        if not request.user.has_event_permission(request.organizer, request.event, 'can_change_items', request):
            raise PermissionDenied()
        quota = self.get_object()
        if 'reopen' in request.POST:
            quota.closed = False
            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
            quota.save(update_fields=['closed', 'close_when_sold_out'])
            quota.log_action('pretix.event.quota.opened', user=request.user)
            quota.log_action(
                'pretix.event.quota.changed',
                user=self.request.user,
                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',
                kwargs={
                    'organizer': self.request.event.organizer.slug,
                    'event': self.request.event.slug,
                    'quota': quota.pk,
                },
            )
        )

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Extract duplicate code into method ([`extract-duplicate-method`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/extract-duplicate-method/))
- Extract code out into method ([`extract-method`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/extract-method/))
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +135 to +136
@receiver(signal=order_changed, dispatch_uid='waitinglist_order_changed')
def on_order_changed(sender, order, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The logic for subevent assignment is duplicated between order_canceled and order_changed.

Refactor the shared subevent extraction and assignment logic into a helper function to avoid duplication and enhance maintainability.

Suggested implementation:

def trigger_waitinglist_assignment(event, subevents):
    """
    Helper to trigger waiting list assignment for main event or subevents.
    """
    if not subevents or not event.has_subevents:
        assign_automatically.apply_async(args=(event.pk,))
    else:
        for subevent_id in subevents:
            assign_automatically.apply_async(args=(event.pk, None, subevent_id))
    trigger_waitinglist_assignment(event, subevents)

You will need to update the corresponding logic in the on_order_changed function to use the new trigger_waitinglist_assignment helper.
If the subevent extraction logic is also duplicated in on_order_changed, refactor it to use the same approach as the other location and pass the subevents to the helper.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds immediate triggers for waiting list processing to improve responsiveness when capacity becomes available. Previously, waiting list assignments were only processed periodically, but now they are triggered instantly in three key scenarios: order cancellations, order position modifications, and quota increases/reopening.

  • Signal receivers added to trigger waiting list processing when orders are canceled or modified
  • Quota reopening and size increases now immediately trigger waiting list assignment
  • Field references updated from item to product for schema compatibility

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
app/eventyay/base/services/waitinglist.py Adds signal receivers for order_canceled and order_changed events to trigger immediate waiting list assignment; updates field references from item to product
app/eventyay/control/views/product.py Adds waiting list triggers when quotas are reopened or their size is increased in QuotaView.post() and QuotaUpdate.form_valid()

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +101 to +166
@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))
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The on_order_canceled and on_order_changed functions contain nearly identical code (lines 101-132 and 135-166). This code duplication makes the codebase harder to maintain. Consider extracting the common logic into a shared helper function that both signal receivers can call.

Example refactoring:

def _trigger_waitinglist_for_order(event, order):
    """Helper to trigger waiting list assignment for an order's affected subevents."""
    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
    
    subevents = set()
    for position in order.positions.all():
        if position.subevent:
            subevents.add(position.subevent.pk)
    
    if not subevents or not event.has_subevents:
        assign_automatically.apply_async(args=(event.pk,))
    else:
        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):
    _trigger_waitinglist_for_order(sender, order)

@receiver(signal=order_changed, dispatch_uid='waitinglist_order_changed')
def on_order_changed(sender, order, **kwargs):
    _trigger_waitinglist_for_order(sender, order)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation works perfectly though, can refactor at another time?...

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ticket Component: Waiting List Notifications Not Sent Automatically When Tickets Become Available

2 participants