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)