Repository: svthalia/concrexit

Issue:
Cannot remove scheduled notifications
### Describe the bug
Cannot remove scheduled notifications

### How to reproduce
Steps to reproduce the behaviour:
1. Go to a notification
2. Delete it
3. Get a gateway error

### Expected behaviour
Message is successfully removed


Cannot remove scheduled notifications
### Describe the bug
Cannot remove scheduled notifications

### How to reproduce
Steps to reproduce the behaviour:
1. Go to a notification
2. Delete it
3. Get a gateway error

### Expected behaviour
Message is successfully removed




Discussion:
I also think that messages are not removed when an event is unpublished but I could not confirm that behaviour
I believe this is similar to #1328 in that the events model uses the antipattern of checking along instance revisions in the clean() method instead of only checking the current state of the instance. 

Although I am not sure how the event model is related to deleting scheduled notifications (afaik there is no connection between the two models). The stack trace of the crash does mention the events model though:
```
Current thread 0x0000700011ff6000 (most recent call first):
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/utils/deconstruct.py", line 16 in __new__
  File "/usr/local/Cellar/python@3.8/3.8.6/Frameworks/Python.framework/Versions/3.8/lib/python3.8/copyreg.py", line 91 in __newobj__
  File "/usr/local/Cellar/python@3.8/3.8.6/Frameworks/Python.framework/Versions/3.8/lib/python3.8/copy.py", line 264 in _reconstruct
  File "/usr/local/Cellar/python@3.8/3.8.6/Frameworks/Python.framework/Versions/3.8/lib/python3.8/copy.py", line 102 in copy
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/expressions.py", line 348 in copy
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/expressions.py", line 247 in resolve_expression
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/lookups.py", line 85 in process_lhs
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/lookups.py", line 171 in process_lhs
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/lookups.py", line 180 in as_sql
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/lookups.py", line 302 in as_sql
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 430 in compile
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/sql/where.py", line 81 in as_sql
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 430 in compile
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 513 in as_sql
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 1143 in execute_sql
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 53 in __iter__
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 1308 in _fetch_all
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 269 in __len__
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 425 in get
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/base.py", line 635 in refresh_from_db
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query_utils.py", line 149 in __get__
  File "/Users/jelle/src/concrexit/website/events/models/event.py", line 184 in __init__
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/base.py", line 513 in from_db
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 71 in __iter__
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 1308 in _fetch_all
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 269 in __len__
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 425 in get
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/base.py", line 635 in refresh_from_db
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query_utils.py", line 149 in __get__
  File "/Users/jelle/src/concrexit/website/events/models/event.py", line 183 in __init__
...
```
The events have foreignkeys that mention the scheduled notifications. So that may be the place to look at.
I also think that messages are not removed when an event is unpublished but I could not confirm that behaviour
I believe this is similar to #1328 in that the events model uses the antipattern of checking along instance revisions in the clean() method instead of only checking the current state of the instance. 

Although I am not sure how the event model is related to deleting scheduled notifications (afaik there is no connection between the two models). The stack trace of the crash does mention the events model though:
```
Current thread 0x0000700011ff6000 (most recent call first):
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/utils/deconstruct.py", line 16 in __new__
  File "/usr/local/Cellar/python@3.8/3.8.6/Frameworks/Python.framework/Versions/3.8/lib/python3.8/copyreg.py", line 91 in __newobj__
  File "/usr/local/Cellar/python@3.8/3.8.6/Frameworks/Python.framework/Versions/3.8/lib/python3.8/copy.py", line 264 in _reconstruct
  File "/usr/local/Cellar/python@3.8/3.8.6/Frameworks/Python.framework/Versions/3.8/lib/python3.8/copy.py", line 102 in copy
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/expressions.py", line 348 in copy
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/expressions.py", line 247 in resolve_expression
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/lookups.py", line 85 in process_lhs
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/lookups.py", line 171 in process_lhs
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/lookups.py", line 180 in as_sql
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/lookups.py", line 302 in as_sql
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 430 in compile
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/sql/where.py", line 81 in as_sql
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 430 in compile
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 513 in as_sql
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 1143 in execute_sql
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 53 in __iter__
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 1308 in _fetch_all
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 269 in __len__
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 425 in get
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/base.py", line 635 in refresh_from_db
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query_utils.py", line 149 in __get__
  File "/Users/jelle/src/concrexit/website/events/models/event.py", line 184 in __init__
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/base.py", line 513 in from_db
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 71 in __iter__
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 1308 in _fetch_all
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 269 in __len__
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query.py", line 425 in get
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/base.py", line 635 in refresh_from_db
  File "/Users/jelle/Library/Caches/pypoetry/virtualenvs/concrexit-8BPMTAC2-py3.8/lib/python3.8/site-packages/django/db/models/query_utils.py", line 149 in __get__
  File "/Users/jelle/src/concrexit/website/events/models/event.py", line 183 in __init__
...
```
The events have foreignkeys that mention the scheduled notifications. So that may be the place to look at.

Resolving patch:
diff --git a/website/events/admin.py b/website/events/admin.py
--- a/website/events/admin.py
+++ b/website/events/admin.py
@@ -143,6 +143,11 @@ class EventAdmin(DoNextTranslatedModelAdmin):
         (_("Extra"), {"fields": ("slide", "documents"), "classes": ("collapse",)}),
     )
 
+    def get_form(self, request, obj=None, change=False, **kwargs):
+        form = super().get_form(request, obj, change, **kwargs)
+        form.clean = lambda form: form.instance.clean_changes(form.changed_data)
+        return form
+
     def overview_link(self, obj):
         return format_html(
             '<a href="{link}">{title}</a>',
diff --git a/website/events/models/event.py b/website/events/models/event.py
--- a/website/events/models/event.py
+++ b/website/events/models/event.py
@@ -178,11 +178,6 @@ class Event(models.Model, metaclass=ModelTranslateMeta):
         null=True,
     )
 
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self._price = self.price
-        self._registration_start = self.registration_start
-
     @property
     def after_cancel_deadline(self):
         return self.cancel_deadline and self.cancel_deadline <= timezone.now()
@@ -260,6 +255,34 @@ def is_pizza_event(self):
         except ObjectDoesNotExist:
             return False
 
+    def clean_changes(self, changed_data):
+        """Check if changes from `changed_data` are allowed.
+
+        This method should be run from a form clean() method, where changed_data
+        can be retrieved from self.changed_data
+        """
+        errors = {}
+        if self.published:
+            if "price" in changed_data:
+                errors.update(
+                    {
+                        "price": _(
+                            "You cannot change this field after the registration has started."
+                        )
+                    }
+                )
+            if "registration_start" in changed_data:
+                errors.update(
+                    {
+                        "registration_start": _(
+                            "You cannot change this field after the registration has started."
+                        )
+                    }
+                )
+
+        if errors:
+            raise ValidationError(errors)
+
     def clean(self):
         # pylint: disable=too-many-branches
         super().clean()
@@ -354,34 +377,6 @@ def clean(self):
         except ObjectDoesNotExist:
             pass
 
-        if self.published:
-            if (
-                self.price != self._price
-                and self._registration_start
-                and self._registration_start <= timezone.now()
-            ):
-                errors.update(
-                    {
-                        "price": _(
-                            "You cannot change this field after "
-                            "the registration has started."
-                        )
-                    }
-                )
-            if (
-                self._registration_start
-                and self.registration_start != self._registration_start
-                and self._registration_start <= timezone.now()
-            ):
-                errors.update(
-                    {
-                        "registration_start": _(
-                            "You cannot change this field after "
-                            "the registration has started."
-                        )
-                    }
-                )
-
         if errors:
             raise ValidationError(errors)
 


Test patch:
diff --git a/website/pushnotifications/tests.py b/website/pushnotifications/tests.py
new file mode 100644
--- /dev/null
+++ b/website/pushnotifications/tests.py
@@ -0,0 +1,41 @@
+import datetime
+
+from django.test import TestCase, override_settings
+from django.utils import timezone
+
+from activemembers.models import Committee
+from events.models import Event, Member
+from mailinglists.models import MailingList
+
+
+@override_settings(SUSPEND_SIGNALS=True)
+class PushNotificationsTest(TestCase):
+    """Tests push notifications"""
+
+    fixtures = ["members.json"]
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.mailinglist = MailingList.objects.create(name="testmail")
+
+        cls.committee = Committee.objects.create(
+            name="committee", contact_mailinglist=cls.mailinglist,
+        )
+
+        cls.event = Event.objects.create(
+            title_en="testevent",
+            organiser=cls.committee,
+            description_en="desc",
+            start=(timezone.now() + datetime.timedelta(hours=2)),
+            end=(timezone.now() + datetime.timedelta(hours=3)),
+            location_en="test location",
+            map_location="test map location",
+            price=0.00,
+            fine=5.00,
+            published=True,
+        )
+        cls.member = Member.objects.first()
+
+    def test_deleting_notification_for_event_doesnt_crash(self) -> None:
+        self.assertIsNotNone(self.event.start_reminder)
+        self.event.start_reminder.delete()