From 01d43818981b9aa6bb8a755b51c7a4a51dc08bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adeodato=20Sim=C3=B3?= Date: Sat, 11 Nov 2023 03:52:05 -0300 Subject: [PATCH] Create notifications for incoming invite requests Closes: #2066 --- .../0186_invite_request_notification.py | 48 ++++++++++ bookwyrm/models/notification.py | 31 +++++-- bookwyrm/templates/notifications/item.html | 2 + .../notifications/items/invite_request.html | 20 +++++ bookwyrm/tests/models/test_notification.py | 87 +++++++++++++++++++ 5 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 bookwyrm/migrations/0186_invite_request_notification.py create mode 100644 bookwyrm/templates/notifications/items/invite_request.html diff --git a/bookwyrm/migrations/0186_invite_request_notification.py b/bookwyrm/migrations/0186_invite_request_notification.py new file mode 100644 index 000000000..3680b1de7 --- /dev/null +++ b/bookwyrm/migrations/0186_invite_request_notification.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2.20 on 2023-11-14 10:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0185_alter_notification_notification_type"), + ] + + operations = [ + migrations.AddField( + model_name="notification", + name="related_invite_requests", + field=models.ManyToManyField(to="bookwyrm.InviteRequest"), + ), + migrations.AlterField( + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("BOOST", "Boost"), + ("REPLY", "Reply"), + ("MENTION", "Mention"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("IMPORT", "Import"), + ("ADD", "Add"), + ("REPORT", "Report"), + ("LINK_DOMAIN", "Link Domain"), + ("INVITE_REQUEST", "Invite Request"), + ("INVITE", "Invite"), + ("ACCEPT", "Accept"), + ("JOIN", "Join"), + ("LEAVE", "Leave"), + ("REMOVE", "Remove"), + ("GROUP_PRIVACY", "Group Privacy"), + ("GROUP_NAME", "Group Name"), + ("GROUP_DESCRIPTION", "Group Description"), + ("MOVE", "Move"), + ], + max_length=255, + ), + ), + ] diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py index 46b88f5e5..d056c05b3 100644 --- a/bookwyrm/models/notification.py +++ b/bookwyrm/models/notification.py @@ -4,6 +4,7 @@ from django.dispatch import receiver from .base_model import BookWyrmModel from . import Boost, Favorite, GroupMemberInvitation, ImportJob, LinkDomain from . import ListItem, Report, Status, User, UserFollowRequest +from .site import InviteRequest class NotificationType(models.TextChoices): @@ -29,6 +30,7 @@ class NotificationType(models.TextChoices): # Admin REPORT = "REPORT" LINK_DOMAIN = "LINK_DOMAIN" + INVITE_REQUEST = "INVITE_REQUEST" # Groups INVITE = "INVITE" @@ -64,8 +66,9 @@ class Notification(BookWyrmModel): related_list_items = models.ManyToManyField( "ListItem", symmetrical=False, related_name="notifications" ) - related_reports = models.ManyToManyField("Report", symmetrical=False) - related_link_domains = models.ManyToManyField("LinkDomain", symmetrical=False) + related_reports = models.ManyToManyField("Report") + related_link_domains = models.ManyToManyField("LinkDomain") + related_invite_requests = models.ManyToManyField("InviteRequest") @classmethod @transaction.atomic @@ -233,8 +236,7 @@ def notify_admins_on_report(sender, instance, created, *args, **kwargs): return # moderators and superusers should be notified - admins = User.admins() - for admin in admins: + for admin in User.admins(): notification, _ = Notification.objects.get_or_create( user=admin, notification_type=NotificationType.REPORT, @@ -253,8 +255,7 @@ def notify_admins_on_link_domain(sender, instance, created, *args, **kwargs): return # moderators and superusers should be notified - admins = User.admins() - for admin in admins: + for admin in User.admins(): notification, _ = Notification.objects.get_or_create( user=admin, notification_type=NotificationType.LINK_DOMAIN, @@ -263,6 +264,24 @@ def notify_admins_on_link_domain(sender, instance, created, *args, **kwargs): notification.related_link_domains.add(instance) +@receiver(models.signals.post_save, sender=InviteRequest) +@transaction.atomic +# pylint: disable=unused-argument +def notify_admins_on_invite_request(sender, instance, created, *args, **kwargs): + """need to handle a new invite request""" + if not created: + return + + # moderators and superusers should be notified + for admin in User.admins(): + notification, _ = Notification.objects.get_or_create( + user=admin, + notification_type=NotificationType.INVITE_REQUEST, + read=False, + ) + notification.related_invite_requests.add(instance) + + @receiver(models.signals.post_save, sender=GroupMemberInvitation) # pylint: disable=unused-argument def notify_user_on_group_invite(sender, instance, *args, **kwargs): diff --git a/bookwyrm/templates/notifications/item.html b/bookwyrm/templates/notifications/item.html index 7e7f0da27..a69790f52 100644 --- a/bookwyrm/templates/notifications/item.html +++ b/bookwyrm/templates/notifications/item.html @@ -21,6 +21,8 @@ {% include 'notifications/items/report.html' %} {% elif notification.notification_type == 'LINK_DOMAIN' %} {% include 'notifications/items/link_domain.html' %} +{% elif notification.notification_type == 'INVITE_REQUEST' %} + {% include 'notifications/items/invite_request.html' %} {% elif notification.notification_type == 'INVITE' %} {% include 'notifications/items/invite.html' %} {% elif notification.notification_type == 'ACCEPT' %} diff --git a/bookwyrm/templates/notifications/items/invite_request.html b/bookwyrm/templates/notifications/items/invite_request.html new file mode 100644 index 000000000..acc08d5d0 --- /dev/null +++ b/bookwyrm/templates/notifications/items/invite_request.html @@ -0,0 +1,20 @@ +{% extends 'notifications/items/layout.html' %} +{% load humanize %} +{% load i18n %} + +{% block primary_link %}{% spaceless %} +{% url 'settings-invite-requests' %} +{% endspaceless %}{% endblock %} + +{% block icon %} + +{% endblock %} + +{% block description %} + {% url 'settings-invite-requests' as path %} + {% blocktrans trimmed count counter=notification.related_invite_requests.count with display_count=notification.related_invite_requests.count|intcomma %} + New invite request awaiting response + {% plural %} + {{ display_count }} new invite requests awaiting response + {% endblocktrans %} +{% endblock %} diff --git a/bookwyrm/tests/models/test_notification.py b/bookwyrm/tests/models/test_notification.py index 1c412e1b4..352b7631d 100644 --- a/bookwyrm/tests/models/test_notification.py +++ b/bookwyrm/tests/models/test_notification.py @@ -192,3 +192,90 @@ class Notification(TestCase): notification_type=models.NotificationType.FAVORITE, ) self.assertFalse(models.Notification.objects.exists()) + + +class NotifyInviteRequest(TestCase): + """let admins know of invite requests""" + + def setUp(self): + """ensure there is one admin""" + with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( + "bookwyrm.activitystreams.populate_stream_task.delay" + ), patch("bookwyrm.lists_stream.populate_lists_task.delay"): + self.local_user = models.User.objects.create_user( + "mouse@local.com", + "mouse@mouse.mouse", + "password", + local=True, + localname="mouse", + is_superuser=True, + ) + + def test_invite_request_triggers_notification(self): + """requesting an invite notifies the admin""" + admin = models.User.objects.filter(is_superuser=True).first() + request = models.InviteRequest.objects.create(email="user@example.com") + + self.assertEqual(models.Notification.objects.count(), 1) + + notification = models.Notification.objects.first() + self.assertEqual(notification.user, admin) + self.assertEqual( + notification.notification_type, models.NotificationType.INVITE_REQUEST + ) + self.assertEqual(notification.related_invite_requests.count(), 1) + self.assertEqual(notification.related_invite_requests.first(), request) + + def test_notify_only_created(self): + """updating an invite request does not trigger a notification""" + request = models.InviteRequest.objects.create(email="user@example.com") + notification = models.Notification.objects.first() + + notification.delete() + self.assertEqual(models.Notification.objects.count(), 0) + + request.ignored = True + request.save() + self.assertEqual(models.Notification.objects.count(), 0) + + def test_notify_grouping(self): + """invites group into the same notification, until read""" + requests = [ + models.InviteRequest.objects.create(email="user1@example.com"), + models.InviteRequest.objects.create(email="user2@example.com"), + ] + self.assertEqual(models.Notification.objects.count(), 1) + + notification = models.Notification.objects.first() + self.assertEqual(notification.related_invite_requests.count(), 2) + self.assertCountEqual(notification.related_invite_requests.all(), requests) + + notification.read = True + notification.save() + + request = models.InviteRequest.objects.create(email="user3@example.com") + _, notification = models.Notification.objects.all() + + self.assertEqual(models.Notification.objects.count(), 2) + self.assertEqual(notification.related_invite_requests.count(), 1) + self.assertEqual(notification.related_invite_requests.first(), request) + + def test_notify_multiple_admins(self): + """all admins are notified""" + with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( + "bookwyrm.activitystreams.populate_stream_task.delay" + ), patch("bookwyrm.lists_stream.populate_lists_task.delay"): + self.local_user = models.User.objects.create_user( + "admin@local.com", + "admin@example.com", + "password", + local=True, + localname="root", + is_superuser=True, + ) + models.InviteRequest.objects.create(email="user@example.com") + admins = models.User.objects.filter(is_superuser=True).all() + notifications = models.Notification.objects.all() + + self.assertEqual(len(notifications), 2) + self.assertCountEqual([notif.user for notif in notifications], admins)