Repository: zestedesavoir/zds-site

Issue:
Perte de forums et sujets suivis lors de l'éviction d'un groupe
Rapporté sur [le forum](https://zestedesavoir.com/forums/sujet/17427/perdu-mes-suivis-en-forum-staff/).

On perd les forums et sujets suivis lorsqu'un forum est restreint à deux groupes et qu'on est retiré d'un de ces groupes.

Pour reproduire :
1. Créer deux groupes A+B et B
2. Créer deux casquettes A+B et B liées à ces groupes
3. Créer un forum A accessible uniquement par A+B et un forum B accessible par A+B et B
4. Ajouter un utilisateur test et le mettre dans les deux groupes (en passant par la page affichant son profil)
5. Mettre aussi le groupe admin dans les deux groupes
6. En tant que test, suivre les deux forums
7. En tant qu'admin, créer un sujet dans le forum B. Test reçoit une notification
8. Retirer test du groupe A+B (en passant par la page affichant son profil)
9. En tant qu'admin, créer un sujet dans le forum B. Test ne reçoit **pas** de notification, alors qu'il devrait.

Tester aussi les sujets suivis du forum B.

Tout semble se passer dans ces lignes : https://github.com/zestedesavoir/zds-site/blob/d3c7fee63bf563599b938c24e058487f78ce8dd6/zds/notification/receivers.py#L39-L68




Discussion:

Resolving patch:
diff --git a/zds/notification/receivers.py b/zds/notification/receivers.py
--- a/zds/notification/receivers.py
+++ b/zds/notification/receivers.py
@@ -48,7 +48,11 @@ def remove_group_subscription_on_quitting_groups(*, sender, instance, action, pk
         )
         return
 
-    for forum in Forum.objects.filter(groups__pk__in=list(pk_set)):
+    all_groups_pk = instance.groups.values_list("pk", flat=True)
+    removed_groups_pk = pk_set
+    kept_groups_pk = set(all_groups_pk) - set(removed_groups_pk)
+
+    for forum in Forum.objects.filter(groups__pk__in=removed_groups_pk).exclude(groups__pk__in=kept_groups_pk):
         subscriptions = []
 
         forum_subscription = NewTopicSubscription.objects.get_existing(instance, forum, True)


Test patch:
diff --git a/zds/member/tests/factories.py b/zds/member/tests/factories.py
--- a/zds/member/tests/factories.py
+++ b/zds/member/tests/factories.py
@@ -103,6 +103,22 @@ class NonAsciiUserFactory(UserFactory):
     username = factory.Sequence("ïéàçÊÀ{}".format)
 
 
+class MultipleGroupsUserFactory(UserFactory):
+    """
+    Factory that creates a User with multiple groups
+
+    WARNING: Don't try to directly use `MultipleGroupsUserFactory` because it won't create the associated Profile.
+    Use `MultipleGroupsProfileFactory` instead.
+    """
+
+    @factory.post_generation
+    def groups(self, create, extracted, **kwargs):
+        for group_name in ("first_group", "second_group"):
+            group = Group(name=group_name)
+            group.save()
+            self.groups.add(group)
+
+
 class ProfileFactory(factory.django.DjangoModelFactory):
     """
     Use this factory when you need a complete Profile for a standard User.
@@ -153,3 +169,11 @@ class NonAsciiProfileFactory(ProfileFactory):
     """
 
     user = factory.SubFactory(NonAsciiUserFactory)
+
+
+class MultipleGroupsProfileFactory(ProfileFactory):
+    """
+    Use this factory when you need a complete Profile for a User with multiple groups.
+    """
+
+    user = factory.SubFactory(MultipleGroupsUserFactory)
diff --git a/zds/notification/tests/tests_tricky.py b/zds/notification/tests/tests_tricky.py
--- a/zds/notification/tests/tests_tricky.py
+++ b/zds/notification/tests/tests_tricky.py
@@ -9,7 +9,7 @@
 from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory
 from zds.forum.models import Topic
 from zds.gallery.tests.factories import UserGalleryFactory
-from zds.member.tests.factories import StaffProfileFactory, ProfileFactory
+from zds.member.tests.factories import StaffProfileFactory, ProfileFactory, MultipleGroupsProfileFactory
 from zds.notification.models import (
     NewTopicSubscription,
     TopicAnswerSubscription,
@@ -40,13 +40,19 @@ def setUp(self):
         self.user2 = ProfileFactory().user
         self.to_be_changed_staff = StaffProfileFactory().user
         self.staff = StaffProfileFactory().user
+        self.multi_groups_user = MultipleGroupsProfileFactory().user
         self.assertTrue(self.staff.has_perm("forum.change_topic"))
         self.category1 = ForumCategoryFactory(position=1)
         self.forum11 = ForumFactory(category=self.category1, position_in_category=1)
         self.forum12 = ForumFactory(category=self.category1, position_in_category=2)
+        self.forum13 = ForumFactory(category=self.category1, position_in_category=3)
         for group in self.staff.groups.all():
             self.forum12.groups.add(group)
+            self.forum13.groups.add(group)
         self.forum12.save()
+        for group in self.multi_groups_user.groups.all():
+            self.forum13.groups.add(group)
+        self.forum13.save()
 
     def test_ping_unknown(self):
         self.client.force_login(self.user2)
@@ -340,6 +346,37 @@ def test_no_more_new_topic_notif_on_losing_one_group(self):
         self.assertFalse(subscription.is_active)
         self.assertTrue(subscription.last_notification.is_read, "As forum is not reachable, notification is read")
 
+    def test_keep_new_topic_notif_if_losing_only_one_group(self):
+        """
+        Context: self.multi_groups_user is in two groups that both give access to self.forum13
+        If this user leaves one of the two groups, they should keep receiving new topic notifications.
+        """
+
+        NewTopicSubscription.objects.get_or_create_active(self.multi_groups_user, self.forum13)
+        self.client.force_login(self.staff)
+        self.client.post(
+            reverse("forum:topic-new") + f"?forum={self.forum13.pk}",
+            {
+                "title": "Super sujet",
+                "subtitle": "Pour tester les notifs",
+                "text": "En tout cas l'un abonnement",
+                "tags": "",
+            },
+            follow=False,
+        )
+        subscription = NewTopicSubscription.objects.get_existing(self.multi_groups_user, self.forum13, True)
+        self.assertIsNotNone(subscription, "There must be an active subscription for now")
+        self.assertIsNotNone(subscription.last_notification, "There must be a notification.")
+        self.assertFalse(subscription.last_notification.is_read, "The notification has not been read yet")
+
+        self.multi_groups_user.groups.remove(list(self.multi_groups_user.groups.all())[0])
+        self.multi_groups_user.save()
+
+        subscription = NewTopicSubscription.objects.get_existing(self.multi_groups_user, self.forum13, True)
+        self.assertIsNotNone(subscription, "There must still be an active subscription")
+        self.assertTrue(subscription.is_active)
+        self.assertFalse(subscription.last_notification.is_read, "The notification has not been read yet")
+
     def test_no_more_topic_answer_notif_on_losing_all_groups(self):
         self.client.force_login(self.to_be_changed_staff)
         self.client.post(