Repository: e-valuation/EvaP

Issue:
Make weak reference in signal dispatcher clearer
In lines 796 to 808 here: https://github.com/fsr-de/EvaP/blob/b3715158306c27734b29253598c0a644c840b9b0/evap/staff/views.py#L798 (and there's a similar case nearby) we register a receiver inside a view.

To my understanding, every time the view is accessed, a new receiver function is added to the signal. And as we reference the view's request inside the receiver it is captured in the [clojure](https://en.wikipedia.org/wiki/Closure_(computer_programming)) used to store the receiver. That means we keep references and objects in memory until application exit, which is bad right?


Discussion:
The `@receiver` decorator uses its arguments for `Signal.connect` which has a `weak=True`.
That way, the callback is stored only with a weak reference in the signal dispatcher. As the view function does not use the callback, it should get destroyed when the view function finishes its scope.
That's for the theory, so I tried a little good ol' print debugging:

```diff
     @receiver(RewardPointGranting.granted_by_removal)
     def notify_reward_points(grantings, **_kwargs):
+        print('user_edit')
         messages.info(request, …)
```
… prints exactly once when points are granted.

```diff
-    @receiver(RewardPointGranting.granted_by_removal)
+    @receiver(RewardPointGranting.granted_by_removal, weak=False)
     def notify_reward_points(grantings, **_kwargs):
+        print('user_edit')
         messages.info(request, …)
```
… prints multiple times when points are granted, as past instances are kept.

That's only a tiny example, no real proof. The inner function seams to be destroyed as it only has weak references, and according to the [docs](https://docs.python.org/3/library/weakref.html?highlight=garbage+collection) the gc is _free to destroy the object_. As this is a rather simple case, the reference-counting approach would suffice to do this directly after this. Even if the gc does not do it immediately, it can do so later - a memory leak should not be possible.

For testing purposes, these two courses (in the test db) grant points when all participants are removed: [`1545`](http://localhost:8000/staff/semester/21/evaluation/1545/edit) [`1499`](http://localhost:8000/staff/semester/21/evaluation/1499/edit)
So `notify_reward_points` is a receiver while the response for the POST request is worked on in the view function, which is where the signal is signaled. 
So this actually does what we want, although it's a little unconventional.
I'd still like to have some kind of comment there explaining that it will use a weak reference and thus does not cause memory leak, maybe linking to this explanation.
Agreed, it caused enough confusion for one day. If it is sufficient in your opinion, I'd explicitly set `weak=True` to replace a comment or to augment a comment stating, 'This callback will only live for the lifetime of one request' (or similiar).
the whole signal thing was used here because we thought it might be nice to separate the rewards app from everything else as much as possible.
some years later i now don't really see a good reason for handling this app differently from everything else. putting `weak` there for some explanation is fine for me and i would also be happy to discuss not using signals at all if that would be a preferred solution.

Resolving patch:
diff --git a/evap/staff/views.py b/evap/staff/views.py
--- a/evap/staff/views.py
+++ b/evap/staff/views.py
@@ -795,7 +795,12 @@ def evaluation_edit(request, semester_id, evaluation_id):
 
 @manager_required
 def helper_evaluation_edit(request, semester, evaluation):
-    @receiver(RewardPointGranting.granted_by_removal)
+    # Show a message when reward points are granted during the lifetime of the calling view.
+    # The @receiver will only live as long as the request is processed
+    # as the callback is captured by a weak reference in the Django Framework
+    # and no other strong references are being kept.
+    # See https://github.com/fsr-de/EvaP/issues/1361 for more information and discussion.
+    @receiver(RewardPointGranting.granted_by_removal, weak=True)
     def notify_reward_points(grantings, **_kwargs):
         for granting in grantings:
             messages.info(request,
@@ -1460,7 +1465,8 @@ def user_import(request):
 
 @manager_required
 def user_edit(request, user_id):
-    @receiver(RewardPointGranting.granted_by_removal)
+    # See comment in helper_evaluation_edit
+    @receiver(RewardPointGranting.granted_by_removal, weak=True)
     def notify_reward_points(grantings, **_kwargs):
         assert len(grantings) == 1
 


Test patch: