Repository: svthalia/concrexit

Issue:
Deleting a user results in a full crash
### Describe the bug
Have a user with profile and payments and such. If we try to delete the user, Django will crash completely:

```
Fatal Python error: Cannot recover from stack overflow.
Python runtime state: initialized
[...]
Process finished with exit code 250
```

### How to reproduce
1. have a user with a profile and payments
2. try to delete the user (via the auth user admin)

### Expected behaviour
Not crash (but disallow deleting? as we don't want it, we can only minimize their data) 

### Screenshots
If applicable, add screenshots to help explain your problem.

### Additional context
I think it is related to the payment, because in the stack trace there are references to the Payment __init__ code. But I can't see the problem directly


Discussion:
I've figured out this has to do with these lines: https://github.com/svthalia/concrexit/blob/9f861a474a9ea4daf7dc40343f8439d551442c10/website/payments/models.py#L133-L134

When commented out, deleting users does not crash. I think these lines create an infinite loop of database lookups

Resolving patch:
diff --git a/website/payments/models.py b/website/payments/models.py
--- a/website/payments/models.py
+++ b/website/payments/models.py
@@ -8,7 +8,7 @@
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.db import models
-from django.db.models import Q, Sum
+from django.db.models import DEFERRED, Q, Sum
 from django.urls import reverse
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
@@ -130,12 +130,47 @@ class Payment(models.Model):
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self._batch = self.batch
+
+        # This is a pretty ugly hack, but it's necessary to something like this
+        # when you want to check things against the old version of the model in
+        # the clean() method---so some of the old data is saved here in init.
+
+        # Previously we had something like this:
+        # self._batch = self.batch
+        # but this breaks when deleting the Payment instance, and creates a
+        # stack overflow crash.
+
+        # Instead we try to get the foreign key out of the init function arguments.
+        # This works because when Django does database lookups via the .objects
+        # manager, it uses the init method to create this model instance and it
+        # includes the id. When we create a Payment in our own code we also use
+        # this init method but use keyword arguments most of the time.
+
+        # In the future we might want to remove this code and instead move
+        # cleaning code to a place where we do have the old information available.
+        self._batch_id = None
+        if not kwargs:
+            batch_id_idx = [
+                i
+                for i, x in enumerate(self._meta.concrete_fields)
+                if x.attname == "batch_id"
+            ]
+            if (
+                batch_id_idx
+                and len(args) >= batch_id_idx[0]
+                and args[batch_id_idx[0]] != DEFERRED
+            ):
+                self._batch_id = args[batch_id_idx[0]]
+        else:
+            # This should be okay as keyword arguments are only used for manual
+            # instantiation.
+            self._batch_id = self.batch_id
+
         self._type = self.type
 
     def save(self, **kwargs):
         self.clean()
-        self._batch = self.batch
+        self._batch_id = self.batch.id if self.batch else None
         super().save(**kwargs)
 
     def clean(self):
@@ -145,7 +180,7 @@ def clean(self):
             raise ValidationError(
                 {"batch": _("Non Thalia Pay payments cannot be added to a batch")}
             )
-        if self._batch and self._batch.processed:
+        if self._batch_id and Batch.objects.get(pk=self._batch_id).processed:
             raise ValidationError(
                 _("Cannot change a payment that is part of a processed batch")
             )


Test patch:
diff --git a/website/payments/tests/test_models.py b/website/payments/tests/test_models.py
--- a/website/payments/tests/test_models.py
+++ b/website/payments/tests/test_models.py
@@ -83,6 +83,12 @@ def test_add_payment_from_processed_batch_to_new_batch(self) -> None:
         with self.assertRaises(ValidationError):
             self.payment.save()
 
+    def test_deleting_member_who_made_a_payment_doesnt_crach(self) -> None:
+        """
+        Check that https://github.com/svthalia/concrexit/issues/1328 is fixed
+        """
+        self.member.delete()
+
     def test_clean(self):
         """
         Tests the model clean functionality