Repository: svthalia/concrexit

Issue:
Canceled event registrations are marked as queued
### Describe the bug
After canceling a registration, 

### How to reproduce
Steps to reproduce the behaviour:
1. Go to https://staging.thalia.nu/api/v2/admin/events/183/registrations/?limit=999999999&ordering=-date_cancelled&cancelled=true&queued=true
2. Go to https://staging.thalia.nu/api/v2/admin/events/183/registrations/?limit=999999999&ordering=-date_cancelled&cancelled=true&queued=false
3. Notice that the canceled registrations are only listed under `queued=true`, and that the queue position is 3
4. Go to https://staging.thalia.nu/api/v2/admin/events/183/registrations/?limit=999999999&ordering=-date_cancelled&cancelled=false&queued=true
5. Notice that the queued position 3 already exists.

### Expected behaviour
Canceled registrations should show up as canceled and not in queue, as they are not in the queue

### Screenshots
If applicable, add screenshots to help explain your problem.

### Additional context
I think the reason they are listed as queued is because the queue position is set. Ideally this field would be nulled or set to a special value like -1.



Discussion:
(It is possible I used the wrong method to cancel a registration, but there is no proper way to do so)
A fix for the app is to just ignore (not filter on) queued.
I agree the queue_position should be nulled for cancelled registrations.

Some condition will need to be added to https://github.com/svthalia/concrexit/blob/fff987458865d636f023877fe43c2624435f1bf2/website/events/models/event_registration.py#L117

And of course a unittest

Resolving patch:
diff --git a/website/events/models/event_registration.py b/website/events/models/event_registration.py
--- a/website/events/models/event_registration.py
+++ b/website/events/models/event_registration.py
@@ -1,7 +1,7 @@
 from django.core import validators
 from django.core.exceptions import ValidationError
 from django.db import models
-from django.db.models import Count, F, Q
+from django.db.models import Case, Count, F, Q, When
 from django.db.models.functions import Greatest, NullIf
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
@@ -115,23 +115,30 @@ def is_registered(self):
         return self.date_cancelled is None
 
     queue_position = AnnotationProperty(
-        # Get queue position by counting amount of registrations with lower date and in case of same date lower id
-        # Subsequently cast to None if this is 0 or lower, in which case it isn't in the queue
-        NullIf(
-            Greatest(
-                Count(
-                    "event__eventregistration",
-                    filter=Q(event__eventregistration__date_cancelled=None)
-                    & (
-                        Q(event__eventregistration__date__lt=F("date"))
-                        | Q(event__eventregistration__id__lte=F("id"))
-                        & Q(event__eventregistration__date__exact=F("date"))
+        Case(
+            # Get queue position by counting amount of registrations with lower date and in case of same date lower id
+            # Subsequently cast to None if this is 0 or lower, in which case it isn't in the queue
+            # If the current registration is cancelled, also force it to None.
+            When(
+                date_cancelled=None,
+                then=NullIf(
+                    Greatest(
+                        Count(
+                            "event__eventregistration",
+                            filter=Q(event__eventregistration__date_cancelled=None)
+                            & (
+                                Q(event__eventregistration__date__lt=F("date"))
+                                | Q(event__eventregistration__id__lte=F("id"))
+                                & Q(event__eventregistration__date__exact=F("date"))
+                            ),
+                        )
+                        - F("event__max_participants"),
+                        0,
                     ),
-                )
-                - F("event__max_participants"),
-                0,
+                    0,
+                ),
             ),
-            0,
+            default=None,
         )
     )
 


Test patch:
diff --git a/website/events/tests/test_models.py b/website/events/tests/test_models.py
--- a/website/events/tests/test_models.py
+++ b/website/events/tests/test_models.py
@@ -239,8 +239,10 @@ def setUpTestData(cls):
         cls.event.organisers.add(Committee.objects.get(pk=1))
         cls.member1 = Member.objects.first()
         cls.member2 = Member.objects.all()[1]
+        cls.member3 = Member.objects.all()[2]
         cls.r1 = EventRegistration.objects.create(event=cls.event, member=cls.member1)
         cls.r2 = EventRegistration.objects.create(event=cls.event, member=cls.member2)
+        cls.r3 = EventRegistration.objects.create(event=cls.event, member=cls.member3)
 
     def setUp(self):
         self.r1.refresh_from_db()
@@ -273,6 +275,17 @@ def test_queue_position(self):
         self.assertEqual(self.r1.queue_position, None)
         self.assertEqual(self.r2.queue_position, 1)
 
+    def test_queue_cancel(self):
+        self.r1.event.max_participants = 0
+        self.r1.event.save()
+
+        self.r2.date_cancelled = timezone.now()
+        self.r2.save()
+
+        self.assertEqual(self.r1.queue_position, 1)
+        self.assertEqual(self.r2.queue_position, None)
+        self.assertEqual(self.r3.queue_position, 2)
+
     def test_registration_either_name_or_member(self):
         self.r2.delete()
         self.r1.clean()