Repository: comic/grand-challenge.org

Issue:
`get_follow_object_pk` errors out if `obj.follow_object` is `None`
Occurs when the follow object has been deleted and the follow is not cleaned up. See https://sentry.io/organizations/grand-challenge/issues/2511041483/?project=303639&query=is%3Aunresolved


Discussion:

Resolving patch:
diff --git a/app/grandchallenge/forum_conversation/templatetags/forum_extras.py b/app/grandchallenge/forum_conversation/templatetags/forum_extras.py
--- a/app/grandchallenge/forum_conversation/templatetags/forum_extras.py
+++ b/app/grandchallenge/forum_conversation/templatetags/forum_extras.py
@@ -16,10 +16,16 @@ def get_follow_object_pk(user, follow_object):
             model=follow_object._meta.model_name,
         ),
     ).all()
-    current_follow_object = []
-    for obj in object_follows_for_user:
-        if obj.follow_object.id == follow_object.id:
-            current_follow_object = obj.pk
+
+    if not object_follows_for_user:
+        current_follow_object = []
+    else:
+        current_follow_object = []
+        for obj in object_follows_for_user:
+            if not obj.follow_object:
+                continue
+            elif obj.follow_object.id == follow_object.id:
+                current_follow_object = obj.pk
     return current_follow_object
 
 
@@ -37,8 +43,11 @@ def follow_form(*, user, object_id, content_type):
 
 @register.simple_tag()
 def get_content_type(follow_object):
-    ct = ContentType.objects.get(
-        app_label=follow_object._meta.app_label,
-        model=follow_object._meta.model_name,
-    )
+    try:
+        ct = ContentType.objects.get(
+            app_label=follow_object._meta.app_label,
+            model=follow_object._meta.model_name,
+        )
+    except AttributeError:
+        ct = None
     return ct
diff --git a/app/grandchallenge/notifications/signals.py b/app/grandchallenge/notifications/signals.py
--- a/app/grandchallenge/notifications/signals.py
+++ b/app/grandchallenge/notifications/signals.py
@@ -1,9 +1,11 @@
 from actstream import action
 from actstream.actions import follow
 from actstream.models import Action, Follow, followers
-from django.db.models.signals import post_save
+from django.contrib.contenttypes.models import ContentType
+from django.db.models.signals import post_save, pre_delete
 from django.dispatch import receiver
 from guardian.shortcuts import assign_perm
+from machina.apps.forum.models import Forum
 from machina.apps.forum_conversation.models import Post, Topic
 
 from grandchallenge.notifications.models import Notification
@@ -77,3 +79,13 @@ def add_permissions(*, instance, created, **_):
         assign_perm("change_follow", instance.user, instance)
         assign_perm("delete_follow", instance.user, instance)
         assign_perm("view_follow", instance.user, instance)
+
+
+@receiver(pre_delete, sender=Topic)
+@receiver(pre_delete, sender=Forum)
+@receiver(pre_delete, sender=Post)
+def clean_up_follows(*, instance, **_):
+    ct = ContentType.objects.filter(
+        app_label=instance._meta.app_label, model=instance._meta.model_name
+    ).get()
+    Follow.objects.filter(content_type=ct, object_id=instance.pk).delete()


Test patch:
diff --git a/app/tests/notifications_tests/test_signals.py b/app/tests/notifications_tests/test_signals.py
--- a/app/tests/notifications_tests/test_signals.py
+++ b/app/tests/notifications_tests/test_signals.py
@@ -1,5 +1,5 @@
 import pytest
-from actstream.actions import follow
+from actstream.actions import follow, is_following
 from actstream.models import Action, Follow
 from machina.apps.forum.models import Forum
 from machina.apps.forum_conversation.models import Topic
@@ -80,3 +80,46 @@ def test_notification_created_for_target_followers_on_action_creation():
     # check that the poster did not receive a notification
     assert notification.user == user2
     assert notification.user != user1
+
+
+@pytest.mark.django_db
+def test_follow_clean_up_after_topic_removal():
+    u = UserFactory()
+    f = ForumFactory(type=Forum.FORUM_POST)
+    t1 = TopicFactory(forum=f, type=Topic.TOPIC_POST)
+    t2 = TopicFactory(forum=f, type=Topic.TOPIC_POST)
+    follow(u, t1, send_action=False)
+    follow(u, t2, send_action=False)
+
+    t1.delete()
+
+    assert not is_following(u, t1)
+
+
+@pytest.mark.django_db
+def test_follow_clean_up_after_post_removal():
+    u = UserFactory()
+    f = ForumFactory(type=Forum.FORUM_POST)
+    t = TopicFactory(forum=f, type=Topic.TOPIC_POST)
+    p1 = PostFactory(topic=t, poster=u)
+    p2 = PostFactory(topic=t, poster=u)
+
+    follow(u, p1)
+    follow(u, p2)
+
+    p1.delete()
+
+    assert not is_following(u, p1)
+
+
+@pytest.mark.django_db
+def test_follow_clean_up_after_forum_removal():
+    u = UserFactory()
+    f1 = ForumFactory(type=Forum.FORUM_POST)
+    f2 = ForumFactory(type=Forum.FORUM_POST)
+    follow(u, f1, send_action=False)
+    follow(u, f2, send_action=False)
+
+    f1.delete()
+
+    assert not is_following(u, f1)