Skip to content

Commit

Permalink
Corrige un bug de notification persistante de nouveau contenu d'un me…
Browse files Browse the repository at this point in the history
…mbre suivi (#6491)

Fix #5544
  • Loading branch information
philippemilink authored Aug 27, 2023
1 parent b6fd744 commit b5c6af7
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 10 deletions.
17 changes: 17 additions & 0 deletions zds/notification/managers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
Expand Down Expand Up @@ -157,6 +158,22 @@ def deactivate_subscriptions(self, user, _object):
subscription.save(update_fields=["is_active"])


class NewPublicationSubscriptionManager(SubscriptionManager):
def get_objects_followed_by(self, user):
"""
Gets objects followed by the given user.
:param user: concerned user.
:type user: django.contrib.auth.models.User
:return: All objects followed by given user.
"""
user_list = self.filter(
user=user, is_active=True, content_type=ContentType.objects.get_for_model(User)
).values_list("object_id", flat=True)

return User.objects.filter(id__in=user_list)


class NewTopicSubscriptionManager(SubscriptionManager):
def mark_read_everybody_at(self, topic):
"""
Expand Down
3 changes: 2 additions & 1 deletion zds/notification/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
SubscriptionManager,
TopicFollowedManager,
TopicAnswerSubscriptionManager,
NewPublicationSubscriptionManager,
NewTopicSubscriptionManager,
)
from zds.utils.misc import convert_camel_to_underscore
Expand Down Expand Up @@ -349,7 +350,7 @@ class NewPublicationSubscription(Subscription, MultipleNotificationsMixin):
"""

module = _("Contenu")
objects = SubscriptionManager()
objects = NewPublicationSubscriptionManager()

def __str__(self):
return _('<Abonnement du membre "{0}" aux nouvelles publications de l\'utilisateur #{1}>').format(
Expand Down
21 changes: 14 additions & 7 deletions zds/notification/receivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,20 @@ def mark_content_reactions_read(sender, *, instance, user=None, target, **__):
subscription = ContentReactionAnswerSubscription.objects.get_existing(user, instance, is_active=True)
if subscription:
subscription.mark_notification_read()
elif target == PublishableContent:
authors = list(instance.authors.all())
for author in authors:
subscription = NewPublicationSubscription.objects.get_existing(user, author)
# a subscription has to be handled only if it is active OR if it was triggered from the publication
# event that creates an "autosubscribe" which is immediately deactivated.
if subscription and (subscription.is_active or subscription.user in authors):
elif target == PublishableContent and user is not None:
# We cannot use the list of authors of the content, because the user we
# are subscribed to may have left the authorship of the content (see issue #5544).
followed_users = list(NewPublicationSubscription.objects.get_objects_followed_by(user))
if user not in followed_users:
# When a content is published, their authors are subscribed for
# notifications of their own publications, but these subscriptions
# are not activated (see receiver for signal content_published).
# Since followed_users contains only active subscriptions, current
# user should not be in it, so we add it manually:
followed_users.append(user)
for followed_user in followed_users:
subscription = NewPublicationSubscription.objects.get_existing(user, followed_user)
if subscription:
subscription.mark_notification_read(content=instance)


Expand Down
157 changes: 155 additions & 2 deletions zds/notification/tests/tests_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
PublishedContentFactory,
)
from zds.tutorialv2.models.database import ContentReaction, PublishableContent
from zds.tutorialv2.publication_utils import publish_content
from zds.tutorialv2.publication_utils import notify_update, publish_content
from zds.tutorialv2.tests import override_for_contents, TutorialTestMixin
from zds.utils import old_slugify
from zds.utils.tests.factories import SubCategoryFactory, LicenceFactory
from zds.mp.utils import send_mp, send_message_mp
Expand Down Expand Up @@ -535,8 +536,11 @@ def test_remove_subscribed_tag(self):
self.assertEqual(1, len(Notification.objects.filter(object_id=topic.pk, is_read=False, is_dead=True).all()))


class NotificationPublishableContentTest(TestCase):
@override_for_contents()
class NotificationPublishableContentTest(TutorialTestMixin, TestCase):
def setUp(self):
self.overridden_zds_app["member"]["bot_account"] = ProfileFactory().user.username

self.user1 = ProfileFactory().user
self.user2 = ProfileFactory().user

Expand Down Expand Up @@ -685,6 +689,155 @@ def test_notification_generated_when_a_tuto_is_published(self):
notifications = Notification.objects.filter(subscription=subscription, is_read=False).all()
self.assertEqual(0, len(notifications))

def test_no_persistant_notif_when_follow_and_quit_authorship(self):
"""
Related to #5544. The following scenario is tested:
1. user1 follows user2
2. user2 publishes a content written with user3
3. user1 gets a notification, but don't click on it
4. user2 quits authorship of the content (but is still an author of the published version)
5. user1 clicks on the notification, the notification should disappear
"""

user1 = ProfileFactory().user
user2 = ProfileFactory().user
user3 = ProfileFactory().user

# user1 follows user2
self.client.logout()
self.client.force_login(user1)
result = self.client.post(
reverse("content:follow", args=[user2.pk]),
{"follow": 1},
follow=False,
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(result.status_code, 200)

# user2 publishes a content written with user3
tuto = PublishableContentFactory(type="TUTORIAL")
tuto.authors.add(user2)
tuto.authors.add(user3)
UserGalleryFactory(gallery=tuto.gallery, user=user2, mode="W")
tuto.licence = LicenceFactory()
tuto.subcategory.add(SubCategoryFactory())
tuto.save()
tuto_draft = tuto.load_version()
version = tuto_draft.current_version
published = publish_content(tuto, tuto_draft, is_major_update=True)
tuto.sha_public = version
tuto.sha_draft = version
tuto.public_version = published
tuto.save()
notify_update(tuto, is_update=False, is_major=True)

# all users get a notification:
self.assertEqual(1, Notification.objects.get_unread_notifications_of(user1).count())
self.assertEqual(1, Notification.objects.get_unread_notifications_of(user2).count())
self.assertEqual(1, Notification.objects.get_unread_notifications_of(user3).count())

# user2 quits authorship of the content
self.client.logout()
self.client.force_login(user2)
result = self.client.post(
reverse("content:remove-author", args=[tuto.pk]), {"username": user2.username}, follow=False
)
self.assertEqual(result.status_code, 302)

# user1 still has the notification
notifications = Notification.objects.get_unread_notifications_of(user1)
self.assertEqual(1, notifications.count())

# user1 marks the notification as read by visiting the content
self.client.logout()
self.client.force_login(user1)
result = self.client.get(notifications[0].url)
self.assertEqual(result.status_code, 200)

# the notification is read now
self.assertEqual(0, Notification.objects.get_unread_notifications_of(user1).count())

def test_no_persistant_notif_when_follow_and_quit_authorship_and_publish(self):
"""
Related to #5544. Similar to
test_no_persistant_notif_when_follow_and_quit_authorship, but this time
the content is published. The following scenario is tested:
1. user1 follows user2
2. user2 publishes a content written with user3
3. user1 gets a notification, but don't click on it
4. user2 quits authorship of the content
5. user3 publishes the content
6. user1 clicks on the notification, the notification should disappear
"""

user1 = ProfileFactory().user
user2 = ProfileFactory().user
user3 = ProfileFactory().user

# user1 follows user2
self.client.logout()
self.client.force_login(user1)
result = self.client.post(
reverse("content:follow", args=[user2.pk]),
{"follow": 1},
follow=False,
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(result.status_code, 200)

# user2 publishes a content written with user3
tuto = PublishableContentFactory(type="TUTORIAL")
tuto.authors.add(user2)
tuto.authors.add(user3)
UserGalleryFactory(gallery=tuto.gallery, user=user2, mode="W")
tuto.licence = LicenceFactory()
tuto.subcategory.add(SubCategoryFactory())
tuto.save()
tuto_draft = tuto.load_version()
version = tuto_draft.current_version
published = publish_content(tuto, tuto_draft, is_major_update=True)
tuto.sha_public = version
tuto.sha_draft = version
tuto.public_version = published
tuto.save()
notify_update(tuto, is_update=False, is_major=True)

# all users get a notification:
self.assertEqual(1, Notification.objects.get_unread_notifications_of(user1).count())
self.assertEqual(1, Notification.objects.get_unread_notifications_of(user2).count())
self.assertEqual(1, Notification.objects.get_unread_notifications_of(user3).count())

# user2 quits authorship of the content
self.client.logout()
self.client.force_login(user2)
result = self.client.post(
reverse("content:remove-author", args=[tuto.pk]), {"username": user2.username}, follow=False
)
self.assertEqual(result.status_code, 302)

# user3 publishes the content
tuto_draft = tuto.load_version()
version = tuto_draft.current_version
published = publish_content(tuto, tuto_draft, is_major_update=True)
tuto.sha_public = version
tuto.sha_draft = version
tuto.public_version = published
tuto.save()
notify_update(tuto, is_update=False, is_major=True)

# user1 still has the notification
notifications = Notification.objects.get_unread_notifications_of(user1)
self.assertEqual(1, notifications.count())

# user1 marks the notification as read by visiting the content
self.client.logout()
self.client.force_login(user1)
result = self.client.get(notifications[0].url)
self.assertEqual(result.status_code, 200)

# the notification is read now
self.assertEqual(0, Notification.objects.get_unread_notifications_of(user1).count())

def test_no_error_on_multiple_subscription(self):
subscription = NewPublicationSubscription.objects.toggle_follow(self.user1, self.user2)

Expand Down
6 changes: 6 additions & 0 deletions zds/tutorialv2/tests/tests_opinion_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from zds.forum.tests.factories import TagFactory
from zds.gallery.tests.factories import UserGalleryFactory
from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory
from zds.notification.models import Notification
from zds.tutorialv2.tests.factories import (
PublishableContentFactory,
ExtractFactory,
Expand Down Expand Up @@ -80,6 +81,11 @@ def test_opinion_publication_author(self, opinions_management):
self.assertIsNotNone(opinion.public_version)
self.assertEqual(opinion.public_version.sha_public, opinion_draft.current_version)

# By visiting the published content, the author marks the publication notification as read:
self.assertEqual(Notification.objects.get_unread_notifications_of(self.user_author).count(), 1)
self.client.get(result.url)
self.assertEqual(Notification.objects.get_unread_notifications_of(self.user_author).count(), 0)

@patch("zds.tutorialv2.signals.opinions_management")
def test_publish_content_change_title_before_watchdog(self, opinions_management):
"""
Expand Down

0 comments on commit b5c6af7

Please sign in to comment.