diff --git a/bookwyrm/migrations/0151_auto_20220705_0049.py b/bookwyrm/migrations/0151_auto_20220705_0049.py
new file mode 100644
index 0000000000..6010e38e57
--- /dev/null
+++ b/bookwyrm/migrations/0151_auto_20220705_0049.py
@@ -0,0 +1,90 @@
+# Generated by Django 3.2.13 on 2022-07-05 00:49
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("bookwyrm", "0150_readthrough_stopped_date"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="notification",
+ name="related_book",
+ ),
+ migrations.AddField(
+ model_name="notification",
+ name="related_list_items",
+ field=models.ManyToManyField(
+ related_name="notifications", to="bookwyrm.ListItem"
+ ),
+ ),
+ migrations.AddField(
+ model_name="notification",
+ name="related_reports",
+ field=models.ManyToManyField(to="bookwyrm.Report"),
+ ),
+ migrations.AddField(
+ model_name="notification",
+ name="related_users",
+ field=models.ManyToManyField(
+ related_name="notifications", to=settings.AUTH_USER_MODEL
+ ),
+ ),
+ migrations.AlterField(
+ model_name="notification",
+ name="related_list_item",
+ field=models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="notifications_tmp",
+ to="bookwyrm.listitem",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="notification",
+ name="related_report",
+ field=models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="notifications_tmp",
+ to="bookwyrm.report",
+ ),
+ ),
+ migrations.RunSQL(
+ sql="""
+ INSERT INTO bookwyrm_notification_related_users (notification_id, user_id)
+ SELECT id, related_user_id
+ FROM bookwyrm_notification
+ WHERE bookwyrm_notification.related_user_id IS NOT NULL;
+
+ INSERT INTO bookwyrm_notification_related_list_items (notification_id, listitem_id)
+ SELECT id, related_list_item_id
+ FROM bookwyrm_notification
+ WHERE bookwyrm_notification.related_list_item_id IS NOT NULL;
+
+ INSERT INTO bookwyrm_notification_related_reports (notification_id, report_id)
+ SELECT id, related_report_id
+ FROM bookwyrm_notification
+ WHERE bookwyrm_notification.related_report_id IS NOT NULL;
+
+ """,
+ reverse_sql=migrations.RunSQL.noop,
+ ),
+ migrations.RemoveField(
+ model_name="notification",
+ name="related_list_item",
+ ),
+ migrations.RemoveField(
+ model_name="notification",
+ name="related_report",
+ ),
+ migrations.RemoveField(
+ model_name="notification",
+ name="related_user",
+ ),
+ ]
diff --git a/bookwyrm/migrations/0152_remove_notification_notification_type_valid.py b/bookwyrm/migrations/0152_remove_notification_notification_type_valid.py
new file mode 100644
index 0000000000..f7471c0d25
--- /dev/null
+++ b/bookwyrm/migrations/0152_remove_notification_notification_type_valid.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.13 on 2022-07-05 03:16
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("bookwyrm", "0151_auto_20220705_0049"),
+ ]
+
+ operations = [
+ migrations.RemoveConstraint(
+ model_name="notification",
+ name="notification_type_valid",
+ ),
+ ]
diff --git a/bookwyrm/migrations/0153_merge_20220706_2141.py b/bookwyrm/migrations/0153_merge_20220706_2141.py
new file mode 100644
index 0000000000..03959f9ef8
--- /dev/null
+++ b/bookwyrm/migrations/0153_merge_20220706_2141.py
@@ -0,0 +1,13 @@
+# Generated by Django 3.2.13 on 2022-07-06 21:41
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("bookwyrm", "0152_alter_report_user"),
+ ("bookwyrm", "0152_remove_notification_notification_type_valid"),
+ ]
+
+ operations = []
diff --git a/bookwyrm/models/antispam.py b/bookwyrm/models/antispam.py
index f506b6f193..dd2a6df263 100644
--- a/bookwyrm/models/antispam.py
+++ b/bookwyrm/models/antispam.py
@@ -3,7 +3,7 @@
import operator
from django.apps import apps
-from django.db import models
+from django.db import models, transaction
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
@@ -58,25 +58,20 @@ def automod_task():
return
reporter = AutoMod.objects.first().created_by
reports = automod_users(reporter) + automod_statuses(reporter)
- if reports:
- admins = User.objects.filter(
- models.Q(user_permissions__name__in=["moderate_user", "moderate_post"])
- | models.Q(is_superuser=True)
- ).all()
- notification_model = apps.get_model(
- "bookwyrm", "Notification", require_ready=True
- )
+ if not reports:
+ return
+
+ admins = User.objects.filter(
+ models.Q(user_permissions__name__in=["moderate_user", "moderate_post"])
+ | models.Q(is_superuser=True)
+ ).all()
+ notification_model = apps.get_model("bookwyrm", "Notification", require_ready=True)
+ with transaction.atomic():
for admin in admins:
- notification_model.objects.bulk_create(
- [
- notification_model(
- user=admin,
- related_report=r,
- notification_type="REPORT",
- )
- for r in reports
- ]
+ notification, _ = notification_model.objects.get_or_create(
+ user=admin, notification_type=notification_model.REPORT, read=False
)
+ notification.related_repors.add(reports)
def automod_users(reporter):
diff --git a/bookwyrm/models/group.py b/bookwyrm/models/group.py
index 05ed39a278..003b23d02c 100644
--- a/bookwyrm/models/group.py
+++ b/bookwyrm/models/group.py
@@ -140,16 +140,6 @@ def save(self, *args, **kwargs):
# make an invitation
super().save(*args, **kwargs)
- # now send the invite
- model = apps.get_model("bookwyrm.Notification", require_ready=True)
- notification_type = "INVITE"
- model.objects.create(
- user=self.user,
- related_user=self.group.user,
- related_group=self.group,
- notification_type=notification_type,
- )
-
@transaction.atomic
def accept(self):
"""turn this request into the real deal"""
@@ -157,25 +147,24 @@ def accept(self):
model = apps.get_model("bookwyrm.Notification", require_ready=True)
# tell the group owner
- model.objects.create(
- user=self.group.user,
- related_user=self.user,
+ model.notify(
+ self.group.user,
+ self.user,
related_group=self.group,
- notification_type="ACCEPT",
+ notification_type=model.ACCEPT,
)
# let the other members know about it
for membership in self.group.memberships.all():
member = membership.user
if member not in (self.user, self.group.user):
- model.objects.create(
- user=member,
- related_user=self.user,
+ model.notify(
+ member,
+ self.user,
related_group=self.group,
- notification_type="JOIN",
+ notification_type=model.JOIN,
)
def reject(self):
"""generate a Reject for this membership request"""
-
self.delete()
diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py
index 0195020e00..63dd5b23f6 100644
--- a/bookwyrm/models/list.py
+++ b/bookwyrm/models/list.py
@@ -1,7 +1,6 @@
""" make a list of books!! """
import uuid
-from django.apps import apps
from django.core.exceptions import PermissionDenied
from django.db import models
from django.db.models import Q
@@ -151,34 +150,12 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
collection_field = "book_list"
def save(self, *args, **kwargs):
- """create a notification too"""
- created = not bool(self.id)
+ """Update the list's date"""
super().save(*args, **kwargs)
# tick the updated date on the parent list
self.book_list.updated_date = timezone.now()
self.book_list.save(broadcast=False, update_fields=["updated_date"])
- list_owner = self.book_list.user
- model = apps.get_model("bookwyrm.Notification", require_ready=True)
- # create a notification if somoene ELSE added to a local user's list
- if created and list_owner.local and list_owner != self.user:
- model.objects.create(
- user=list_owner,
- related_user=self.user,
- related_list_item=self,
- notification_type="ADD",
- )
-
- if self.book_list.group:
- for membership in self.book_list.group.memberships.all():
- if membership.user != self.user:
- model.objects.create(
- user=membership.user,
- related_user=self.user,
- related_list_item=self,
- notification_type="ADD",
- )
-
def raise_not_deletable(self, viewer):
"""the associated user OR the list owner can delete"""
if self.book_list.user == viewer:
diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py
index 417bf7591a..21a992b07d 100644
--- a/bookwyrm/models/notification.py
+++ b/bookwyrm/models/notification.py
@@ -1,77 +1,123 @@
""" alert a user to activity """
-from django.db import models
+from django.db import models, transaction
from django.dispatch import receiver
from .base_model import BookWyrmModel
-from . import Boost, Favorite, ImportJob, Report, Status, User
-
-# pylint: disable=line-too-long
-NotificationType = models.TextChoices(
- "NotificationType",
- "FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD REPORT INVITE ACCEPT JOIN LEAVE REMOVE GROUP_PRIVACY GROUP_NAME GROUP_DESCRIPTION",
-)
+from . import Boost, Favorite, GroupMemberInvitation, ImportJob, ListItem, Report
+from . import Status, User, UserFollowRequest
class Notification(BookWyrmModel):
"""you've been tagged, liked, followed, etc"""
+ # Status interactions
+ FAVORITE = "FAVORITE"
+ BOOST = "BOOST"
+ REPLY = "REPLY"
+ MENTION = "MENTION"
+ TAG = "TAG"
+
+ # Relationships
+ FOLLOW = "FOLLOW"
+ FOLLOW_REQUEST = "FOLLOW_REQUEST"
+
+ # Imports
+ IMPORT = "IMPORT"
+
+ # List activity
+ ADD = "ADD"
+
+ # Admin
+ REPORT = "REPORT"
+
+ # Groups
+ INVITE = "INVITE"
+ ACCEPT = "ACCEPT"
+ JOIN = "JOIN"
+ LEAVE = "LEAVE"
+ REMOVE = "REMOVE"
+ GROUP_PRIVACY = "GROUP_PRIVACY"
+ GROUP_NAME = "GROUP_NAME"
+ GROUP_DESCRIPTION = "GROUP_DESCRIPTION"
+
+ # pylint: disable=line-too-long
+ NotificationType = models.TextChoices(
+ # there has got be a better way to do this
+ "NotificationType",
+ f"{FAVORITE} {REPLY} {MENTION} {TAG} {FOLLOW} {FOLLOW_REQUEST} {BOOST} {IMPORT} {ADD} {REPORT} {INVITE} {ACCEPT} {JOIN} {LEAVE} {REMOVE} {GROUP_PRIVACY} {GROUP_NAME} {GROUP_DESCRIPTION}",
+ )
+
user = models.ForeignKey("User", on_delete=models.CASCADE)
- related_book = models.ForeignKey("Edition", on_delete=models.CASCADE, null=True)
- related_user = models.ForeignKey(
- "User", on_delete=models.CASCADE, null=True, related_name="related_user"
+ read = models.BooleanField(default=False)
+ notification_type = models.CharField(
+ max_length=255, choices=NotificationType.choices
+ )
+
+ related_users = models.ManyToManyField(
+ "User", symmetrical=False, related_name="notifications"
)
related_group = models.ForeignKey(
"Group", on_delete=models.CASCADE, null=True, related_name="notifications"
)
related_status = models.ForeignKey("Status", on_delete=models.CASCADE, null=True)
related_import = models.ForeignKey("ImportJob", on_delete=models.CASCADE, null=True)
- related_list_item = models.ForeignKey(
- "ListItem", on_delete=models.CASCADE, null=True
- )
- related_report = models.ForeignKey("Report", on_delete=models.CASCADE, null=True)
- read = models.BooleanField(default=False)
- notification_type = models.CharField(
- max_length=255, choices=NotificationType.choices
+ related_list_items = models.ManyToManyField(
+ "ListItem", symmetrical=False, related_name="notifications"
)
+ related_reports = models.ManyToManyField("Report", symmetrical=False)
- def save(self, *args, **kwargs):
- """save, but don't make dupes"""
- # there's probably a better way to do this
- if self.__class__.objects.filter(
- user=self.user,
- related_book=self.related_book,
- related_user=self.related_user,
- related_group=self.related_group,
- related_status=self.related_status,
- related_import=self.related_import,
- related_list_item=self.related_list_item,
- related_report=self.related_report,
- notification_type=self.notification_type,
- ).exists():
+ @classmethod
+ @transaction.atomic
+ def notify(cls, user, related_user, **kwargs):
+ """Create a notification"""
+ if related_user and (not user.local or user == related_user):
return
- super().save(*args, **kwargs)
-
- class Meta:
- """checks if notifcation is in enum list for valid types"""
+ notification, _ = cls.objects.get_or_create(user=user, **kwargs)
+ if related_user:
+ notification.related_users.add(related_user)
+ notification.read = False
+ notification.save()
- constraints = [
- models.CheckConstraint(
- check=models.Q(notification_type__in=NotificationType.values),
- name="notification_type_valid",
+ @classmethod
+ @transaction.atomic
+ def notify_list_item(cls, user, list_item):
+ """Group the notifications around the list items, not the user"""
+ related_user = list_item.user
+ notification = cls.objects.filter(
+ user=user,
+ related_users=related_user,
+ related_list_items__book_list=list_item.book_list,
+ notification_type=Notification.ADD,
+ ).first()
+ if not notification:
+ notification = cls.objects.create(
+ user=user, notification_type=Notification.ADD
)
- ]
+ notification.related_users.add(related_user)
+ notification.related_list_items.add(list_item)
+ notification.read = False
+ notification.save()
+
+ @classmethod
+ def unnotify(cls, user, related_user, **kwargs):
+ """Remove a user from a notification and delete it if that was the only user"""
+ try:
+ notification = cls.objects.filter(user=user, **kwargs).get()
+ except Notification.DoesNotExist:
+ return
+ notification.related_users.remove(related_user)
+ if not notification.related_users.count():
+ notification.delete()
@receiver(models.signals.post_save, sender=Favorite)
# pylint: disable=unused-argument
def notify_on_fav(sender, instance, *args, **kwargs):
"""someone liked your content, you ARE loved"""
- if not instance.status.user.local or instance.status.user == instance.user:
- return
- Notification.objects.create(
- user=instance.status.user,
- notification_type="FAVORITE",
- related_user=instance.user,
+ Notification.notify(
+ instance.status.user,
+ instance.user,
related_status=instance.status,
+ notification_type=Notification.FAVORITE,
)
@@ -81,15 +127,16 @@ def notify_on_unfav(sender, instance, *args, **kwargs):
"""oops, didn't like that after all"""
if not instance.status.user.local:
return
- Notification.objects.filter(
- user=instance.status.user,
- related_user=instance.user,
+ Notification.unnotify(
+ instance.status.user,
+ instance.user,
related_status=instance.status,
- notification_type="FAVORITE",
- ).delete()
+ notification_type=Notification.FAVORITE,
+ )
@receiver(models.signals.post_save)
+@transaction.atomic
# pylint: disable=unused-argument
def notify_user_on_mention(sender, instance, *args, **kwargs):
"""creating and deleting statuses with @ mentions and replies"""
@@ -105,22 +152,23 @@ def notify_user_on_mention(sender, instance, *args, **kwargs):
and instance.reply_parent.user != instance.user
and instance.reply_parent.user.local
):
- Notification.objects.create(
- user=instance.reply_parent.user,
- notification_type="REPLY",
- related_user=instance.user,
+ Notification.notify(
+ instance.reply_parent.user,
+ instance.user,
related_status=instance,
+ notification_type=Notification.REPLY,
)
+
for mention_user in instance.mention_users.all():
# avoid double-notifying about this status
if not mention_user.local or (
instance.reply_parent and mention_user == instance.reply_parent.user
):
continue
- Notification.objects.create(
- user=mention_user,
- notification_type="MENTION",
- related_user=instance.user,
+ Notification.notify(
+ mention_user,
+ instance.user,
+ notification_type=Notification.MENTION,
related_status=instance,
)
@@ -135,11 +183,11 @@ def notify_user_on_boost(sender, instance, *args, **kwargs):
):
return
- Notification.objects.create(
- user=instance.boosted_status.user,
+ Notification.notify(
+ instance.boosted_status.user,
+ instance.user,
related_status=instance.boosted_status,
- related_user=instance.user,
- notification_type="BOOST",
+ notification_type=Notification.BOOST,
)
@@ -147,12 +195,12 @@ def notify_user_on_boost(sender, instance, *args, **kwargs):
# pylint: disable=unused-argument
def notify_user_on_unboost(sender, instance, *args, **kwargs):
"""unboosting a status"""
- Notification.objects.filter(
- user=instance.boosted_status.user,
+ Notification.unnotify(
+ instance.boosted_status.user,
+ instance.user,
related_status=instance.boosted_status,
- related_user=instance.user,
- notification_type="BOOST",
- ).delete()
+ notification_type=Notification.BOOST,
+ )
@receiver(models.signals.post_save, sender=ImportJob)
@@ -166,12 +214,13 @@ def notify_user_on_import_complete(
return
Notification.objects.create(
user=instance.user,
- notification_type="IMPORT",
+ notification_type=Notification.IMPORT,
related_import=instance,
)
@receiver(models.signals.post_save, sender=Report)
+@transaction.atomic
# pylint: disable=unused-argument
def notify_admins_on_report(sender, instance, *args, **kwargs):
"""something is up, make sure the admins know"""
@@ -181,8 +230,72 @@ def notify_admins_on_report(sender, instance, *args, **kwargs):
| models.Q(is_superuser=True)
).all()
for admin in admins:
- Notification.objects.create(
+ notification, _ = Notification.objects.get_or_create(
user=admin,
- related_report=instance,
- notification_type="REPORT",
+ notification_type=Notification.REPORT,
+ read=False,
+ )
+ notification.related_reports.add(instance)
+
+
+@receiver(models.signals.post_save, sender=GroupMemberInvitation)
+# pylint: disable=unused-argument
+def notify_user_on_group_invite(sender, instance, *args, **kwargs):
+ """Cool kids club here we come"""
+ Notification.notify(
+ instance.user,
+ instance.group.user,
+ related_group=instance.group,
+ notification_type=Notification.INVITE,
+ )
+
+
+@receiver(models.signals.post_save, sender=ListItem)
+@transaction.atomic
+# pylint: disable=unused-argument
+def notify_user_on_list_item_add(sender, instance, created, *args, **kwargs):
+ """Someone added to your list"""
+ if not created:
+ return
+
+ list_owner = instance.book_list.user
+ # create a notification if somoene ELSE added to a local user's list
+ if list_owner.local and list_owner != instance.user:
+ # keep the related_user singular, group the items
+ Notification.notify_list_item(list_owner, instance)
+
+ if instance.book_list.group:
+ for membership in instance.book_list.group.memberships.all():
+ if membership.user != instance.user:
+ Notification.notify_list_item(membership.user, instance)
+
+
+@receiver(models.signals.post_save, sender=UserFollowRequest)
+@transaction.atomic
+# pylint: disable=unused-argument
+def notify_user_on_follow(sender, instance, created, *args, **kwargs):
+ """Someone added to your list"""
+ if not created or not instance.user_object.local:
+ return
+
+ manually_approves = instance.user_object.manually_approves_followers
+ if manually_approves:
+ # don't group notifications
+ notification = Notification.objects.filter(
+ user=instance.user_object,
+ related_users=instance.user_subject,
+ notification_type=Notification.FOLLOW_REQUEST,
+ ).first()
+ if not notification:
+ notification = Notification.objects.create(
+ user=instance.user_object, notification_type=Notification.FOLLOW_REQUEST
+ )
+ notification.related_users.set([instance.user_subject])
+ notification.read = False
+ notification.save()
+ else:
+ Notification.notify(
+ instance.user_object,
+ instance.user_subject,
+ notification_type=Notification.FOLLOW,
)
diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py
index 3132005147..082294c0e0 100644
--- a/bookwyrm/models/relationship.py
+++ b/bookwyrm/models/relationship.py
@@ -1,5 +1,4 @@
""" defines relationships between users """
-from django.apps import apps
from django.core.cache import cache
from django.db import models, transaction, IntegrityError
from django.db.models import Q
@@ -148,14 +147,6 @@ def save(self, *args, broadcast=True, **kwargs): # pylint: disable=arguments-di
if not manually_approves:
self.accept()
- model = apps.get_model("bookwyrm.Notification", require_ready=True)
- notification_type = "FOLLOW_REQUEST" if manually_approves else "FOLLOW"
- model.objects.create(
- user=self.user_object,
- related_user=self.user_subject,
- notification_type=notification_type,
- )
-
def get_accept_reject_id(self, status):
"""get id for sending an accept or reject of a local user"""
diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py
index 1254eabd79..140f715e10 100644
--- a/bookwyrm/settings.py
+++ b/bookwyrm/settings.py
@@ -11,7 +11,7 @@
env = Env()
env.read_env()
DOMAIN = env("DOMAIN")
-VERSION = "0.4.1"
+VERSION = "0.4.2"
RELEASE_API = env(
"RELEASE_API",
diff --git a/bookwyrm/templates/landing/password_reset_request.html b/bookwyrm/templates/landing/password_reset_request.html
index 5d877442fe..b06668e578 100644
--- a/bookwyrm/templates/landing/password_reset_request.html
+++ b/bookwyrm/templates/landing/password_reset_request.html
@@ -9,7 +9,13 @@
{% trans "Reset Password" %}
- {% if message %}
{{ message }}
{% endif %}
+ {% if sent_message %}
+
+ {% blocktrans trimmed %}
+ A password reset link will be sent to {{ email }} if there is an account using that email address.
+ {% endblocktrans %}
+
+ {% endif %}
{% trans "A link to reset your password will be sent to your email address" %}
-
+
- {% trans "Requesting camera..." %}
+ {% trans "Requesting camera..." %}
{% trans "Grant access to the camera to scan a book's barcode." %}
diff --git a/bookwyrm/templates/snippets/status/content_status.html b/bookwyrm/templates/snippets/status/content_status.html
index d0d3851536..bee4af1dc5 100644
--- a/bookwyrm/templates/snippets/status/content_status.html
+++ b/bookwyrm/templates/snippets/status/content_status.html
@@ -112,9 +112,6 @@
{% with full=status.content|safe no_trim=status.content_warning itemprop="reviewBody" %}
{% include 'snippets/trimmed_text.html' %}
{% endwith %}
- {% if status.progress %}
-
{% trans "page" %} {{ status.progress }}
- {% endif %}
{% endif %}
{% if status.attachments.exists %}
diff --git a/bookwyrm/tests/models/test_list.py b/bookwyrm/tests/models/test_list.py
index de6957b575..f7e64c6f2e 100644
--- a/bookwyrm/tests/models/test_list.py
+++ b/bookwyrm/tests/models/test_list.py
@@ -7,6 +7,7 @@
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
+@patch("bookwyrm.lists_stream.remove_list_task.delay")
class List(TestCase):
"""some activitypub oddness ahead"""
@@ -21,25 +22,15 @@ def setUp(self):
work = models.Work.objects.create(title="hello")
self.book = models.Edition.objects.create(title="hi", parent_work=work)
- def test_remote_id(self, _):
+ def test_remote_id(self, *_):
"""shelves use custom remote ids"""
- with patch(
- "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
- ), patch("bookwyrm.lists_stream.remove_list_task.delay"):
- book_list = models.List.objects.create(
- name="Test List", user=self.local_user
- )
+ book_list = models.List.objects.create(name="Test List", user=self.local_user)
expected_id = f"https://{settings.DOMAIN}/list/{book_list.id}"
self.assertEqual(book_list.get_remote_id(), expected_id)
- def test_to_activity(self, _):
+ def test_to_activity(self, *_):
"""jsonify it"""
- with patch(
- "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
- ), patch("bookwyrm.lists_stream.remove_list_task.delay"):
- book_list = models.List.objects.create(
- name="Test List", user=self.local_user
- )
+ book_list = models.List.objects.create(name="Test List", user=self.local_user)
activity_json = book_list.to_activity()
self.assertIsInstance(activity_json, dict)
self.assertEqual(activity_json["id"], book_list.remote_id)
@@ -48,14 +39,11 @@ def test_to_activity(self, _):
self.assertEqual(activity_json["name"], "Test List")
self.assertEqual(activity_json["owner"], self.local_user.remote_id)
- def test_list_item(self, _):
+ def test_list_item(self, *_):
"""a list entry"""
- with patch(
- "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
- ), patch("bookwyrm.lists_stream.remove_list_task.delay"):
- book_list = models.List.objects.create(
- name="Test List", user=self.local_user, privacy="unlisted"
- )
+ book_list = models.List.objects.create(
+ name="Test List", user=self.local_user, privacy="unlisted"
+ )
item = models.ListItem.objects.create(
book_list=book_list,
@@ -68,14 +56,9 @@ def test_list_item(self, _):
self.assertEqual(item.privacy, "unlisted")
self.assertEqual(item.recipients, [])
- def test_list_item_pending(self, _):
+ def test_list_item_pending(self, *_):
"""a list entry"""
- with patch(
- "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
- ), patch("bookwyrm.lists_stream.remove_list_task.delay"):
- book_list = models.List.objects.create(
- name="Test List", user=self.local_user
- )
+ book_list = models.List.objects.create(name="Test List", user=self.local_user)
item = models.ListItem.objects.create(
book_list=book_list,
@@ -90,13 +73,8 @@ def test_list_item_pending(self, _):
self.assertEqual(item.privacy, "direct")
self.assertEqual(item.recipients, [])
- def test_embed_key(self, _):
+ def test_embed_key(self, *_):
"""embed_key should never be empty"""
- with patch(
- "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
- ), patch("bookwyrm.lists_stream.remove_list_task.delay"):
- book_list = models.List.objects.create(
- name="Test List", user=self.local_user
- )
+ book_list = models.List.objects.create(name="Test List", user=self.local_user)
self.assertIsInstance(book_list.embed_key, UUID)
diff --git a/bookwyrm/tests/models/test_notification.py b/bookwyrm/tests/models/test_notification.py
new file mode 100644
index 0000000000..3d6025a5ff
--- /dev/null
+++ b/bookwyrm/tests/models/test_notification.py
@@ -0,0 +1,183 @@
+""" testing models """
+from unittest.mock import patch
+from django.test import TestCase
+from bookwyrm import models
+
+
+class Notification(TestCase):
+ """let people know things"""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """useful things for creating a notification"""
+ 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", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
+ )
+ self.another_user = models.User.objects.create_user(
+ "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
+ )
+ with patch("bookwyrm.models.user.set_remote_server.delay"):
+ self.remote_user = models.User.objects.create_user(
+ "rat",
+ "rat@rat.com",
+ "ratword",
+ local=False,
+ remote_id="https://example.com/users/rat",
+ inbox="https://example.com/users/rat/inbox",
+ outbox="https://example.com/users/rat/outbox",
+ )
+ self.work = models.Work.objects.create(title="Test Work")
+ self.book = models.Edition.objects.create(
+ title="Test Book",
+ isbn_13="1234567890123",
+ remote_id="https://example.com/book/1",
+ parent_work=self.work,
+ )
+ self.another_book = models.Edition.objects.create(
+ title="Second Test Book",
+ parent_work=models.Work.objects.create(title="Test Work"),
+ )
+
+ def test_notification(self):
+ """New notifications are unread"""
+ notification = models.Notification.objects.create(
+ user=self.local_user, notification_type=models.Notification.FAVORITE
+ )
+ self.assertFalse(notification.read)
+
+ def test_notify(self):
+ """Create a notification"""
+ models.Notification.notify(
+ self.local_user,
+ self.remote_user,
+ notification_type=models.Notification.FAVORITE,
+ )
+ self.assertTrue(models.Notification.objects.exists())
+
+ def test_notify_grouping(self):
+ """Bundle notifications"""
+ models.Notification.notify(
+ self.local_user,
+ self.remote_user,
+ notification_type=models.Notification.FAVORITE,
+ )
+ self.assertEqual(models.Notification.objects.count(), 1)
+ notification = models.Notification.objects.get()
+ self.assertEqual(notification.related_users.count(), 1)
+
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
+ notification_type=models.Notification.FAVORITE,
+ )
+ self.assertEqual(models.Notification.objects.count(), 1)
+ notification.refresh_from_db()
+ self.assertEqual(notification.related_users.count(), 2)
+
+ def test_notify_remote(self):
+ """Don't create notifications for remote users"""
+ models.Notification.notify(
+ self.remote_user,
+ self.local_user,
+ notification_type=models.Notification.FAVORITE,
+ )
+ self.assertFalse(models.Notification.objects.exists())
+
+ def test_notify_self(self):
+ """Don't create notifications for yourself"""
+ models.Notification.notify(
+ self.local_user,
+ self.local_user,
+ notification_type=models.Notification.FAVORITE,
+ )
+ self.assertFalse(models.Notification.objects.exists())
+
+ @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
+ @patch("bookwyrm.lists_stream.remove_list_task.delay")
+ def test_notify_list_item_own_list(self, *_):
+ """Don't add list item notification for your own list"""
+ test_list = models.List.objects.create(user=self.local_user, name="hi")
+
+ models.ListItem.objects.create(
+ user=self.local_user, book=self.book, book_list=test_list, order=1
+ )
+ self.assertFalse(models.Notification.objects.exists())
+
+ @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
+ @patch("bookwyrm.lists_stream.remove_list_task.delay")
+ def test_notify_list_item_remote(self, *_):
+ """Don't add list item notification for a remote user"""
+ test_list = models.List.objects.create(user=self.remote_user, name="hi")
+
+ models.ListItem.objects.create(
+ user=self.local_user, book=self.book, book_list=test_list, order=1
+ )
+ self.assertFalse(models.Notification.objects.exists())
+
+ @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
+ @patch("bookwyrm.lists_stream.remove_list_task.delay")
+ def test_notify_list_item(self, *_):
+ """Add list item notification"""
+ test_list = models.List.objects.create(user=self.local_user, name="hi")
+ list_item = models.ListItem.objects.create(
+ user=self.remote_user, book=self.book, book_list=test_list, order=2
+ )
+ notification = models.Notification.objects.get()
+ self.assertEqual(notification.related_users.count(), 1)
+ self.assertEqual(notification.related_users.first(), self.remote_user)
+ self.assertEqual(notification.related_list_items.count(), 1)
+ self.assertEqual(notification.related_list_items.first(), list_item)
+
+ models.ListItem.objects.create(
+ user=self.remote_user, book=self.another_book, book_list=test_list, order=3
+ )
+ notification = models.Notification.objects.get()
+ self.assertEqual(notification.related_users.count(), 1)
+ self.assertEqual(notification.related_users.first(), self.remote_user)
+ self.assertEqual(notification.related_list_items.count(), 2)
+
+ def test_unnotify(self):
+ """Remove a notification"""
+ models.Notification.notify(
+ self.local_user,
+ self.remote_user,
+ notification_type=models.Notification.FAVORITE,
+ )
+ self.assertTrue(models.Notification.objects.exists())
+
+ models.Notification.unnotify(
+ self.local_user,
+ self.remote_user,
+ notification_type=models.Notification.FAVORITE,
+ )
+ self.assertFalse(models.Notification.objects.exists())
+
+ def test_unnotify_multiple_users(self):
+ """Remove a notification"""
+ models.Notification.notify(
+ self.local_user,
+ self.remote_user,
+ notification_type=models.Notification.FAVORITE,
+ )
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
+ notification_type=models.Notification.FAVORITE,
+ )
+ self.assertTrue(models.Notification.objects.exists())
+
+ models.Notification.unnotify(
+ self.local_user,
+ self.remote_user,
+ notification_type=models.Notification.FAVORITE,
+ )
+ self.assertTrue(models.Notification.objects.exists())
+
+ models.Notification.unnotify(
+ self.local_user,
+ self.another_user,
+ notification_type=models.Notification.FAVORITE,
+ )
+ self.assertFalse(models.Notification.objects.exists())
diff --git a/bookwyrm/tests/models/test_status_model.py b/bookwyrm/tests/models/test_status_model.py
index 2f1e8670f0..aae34b0a50 100644
--- a/bookwyrm/tests/models/test_status_model.py
+++ b/bookwyrm/tests/models/test_status_model.py
@@ -394,18 +394,6 @@ def test_boost(self, *_):
self.assertEqual(activity["type"], "Announce")
self.assertEqual(activity, boost.to_activity(pure=True))
- def test_notification(self, *_):
- """a simple model"""
- notification = models.Notification.objects.create(
- user=self.local_user, notification_type="FAVORITE"
- )
- self.assertFalse(notification.read)
-
- with self.assertRaises(IntegrityError):
- models.Notification.objects.create(
- user=self.local_user, notification_type="GLORB"
- )
-
# pylint: disable=unused-argument
def test_create_broadcast(self, one, two, broadcast_mock, *_):
"""should send out two verions of a status on create"""
diff --git a/bookwyrm/tests/test_utils.py b/bookwyrm/tests/test_utils.py
new file mode 100644
index 0000000000..1835489751
--- /dev/null
+++ b/bookwyrm/tests/test_utils.py
@@ -0,0 +1,13 @@
+""" test searching for books """
+import re
+from django.test import TestCase
+
+from bookwyrm.utils import regex
+
+
+class TestUtils(TestCase):
+ """utility functions"""
+
+ def test_regex(self):
+ """Regexes used throughout the app"""
+ self.assertTrue(re.match(regex.DOMAIN, "xn--69aa8bzb.xn--y9a3aq"))
diff --git a/bookwyrm/tests/views/admin/test_site.py b/bookwyrm/tests/views/admin/test_site.py
new file mode 100644
index 0000000000..6b228cf864
--- /dev/null
+++ b/bookwyrm/tests/views/admin/test_site.py
@@ -0,0 +1,80 @@
+""" test for app action functionality """
+from unittest.mock import patch
+from django.template.response import TemplateResponse
+from django.test import TestCase
+from django.test.client import RequestFactory
+
+from bookwyrm import forms, models, views
+from bookwyrm.tests.validate_html import validate_html
+
+
+class SiteSettingsViews(TestCase):
+ """Edit site settings"""
+
+ def setUp(self):
+ """we need basic test data and mocks"""
+ self.factory = RequestFactory()
+ 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",
+ )
+
+ self.site = models.SiteSettings.objects.create()
+
+ def test_site_get(self):
+ """there are so many views, this just makes sure it LOADS"""
+ view = views.Site.as_view()
+ request = self.factory.get("")
+ request.user = self.local_user
+ request.user.is_superuser = True
+ result = view(request)
+ self.assertIsInstance(result, TemplateResponse)
+ validate_html(result.render())
+ self.assertEqual(result.status_code, 200)
+
+ def test_site_post(self):
+ """there are so many views, this just makes sure it LOADS"""
+ view = views.Site.as_view()
+ form = forms.SiteForm()
+ form.data["name"] = "Name!"
+ form.data["instance_tagline"] = "hi"
+ form.data["instance_description"] = "blah"
+ form.data["registration_closed_text"] = "blah"
+ form.data["invite_request_text"] = "blah"
+ form.data["code_of_conduct"] = "blah"
+ form.data["privacy_policy"] = "blah"
+ request = self.factory.post("", form.data)
+ request.user = self.local_user
+ request.user.is_superuser = True
+
+ result = view(request)
+
+ self.assertIsInstance(result, TemplateResponse)
+ validate_html(result.render())
+ self.assertEqual(result.status_code, 200)
+
+ site = models.SiteSettings.objects.get()
+ self.assertEqual(site.name, "Name!")
+
+ def test_site_post_invalid(self):
+ """there are so many views, this just makes sure it LOADS"""
+ view = views.Site.as_view()
+ form = forms.SiteForm()
+ request = self.factory.post("", form.data)
+ request.user = self.local_user
+ request.user.is_superuser = True
+
+ result = view(request)
+
+ self.assertIsInstance(result, TemplateResponse)
+ validate_html(result.render())
+ self.assertEqual(result.status_code, 200)
+
+ self.site.refresh_from_db()
+ self.assertEqual(self.site.name, "BookWyrm")
diff --git a/bookwyrm/tests/views/test_group.py b/bookwyrm/tests/views/test_group.py
index e43b040ac2..b0a0c925a2 100644
--- a/bookwyrm/tests/views/test_group.py
+++ b/bookwyrm/tests/views/test_group.py
@@ -257,6 +257,29 @@ def test_remove_member_existing_member(self, _):
self.assertEqual(notification.related_group, self.testgroup)
self.assertEqual(notification.notification_type, "REMOVE")
+ def test_remove_member_remove_self(self, _):
+ """Leave a group"""
+ models.GroupMember.objects.create(
+ user=self.rat,
+ group=self.testgroup,
+ )
+ request = self.factory.post(
+ "",
+ {
+ "group": self.testgroup.id,
+ "user": self.rat.localname,
+ },
+ )
+ request.user = self.rat
+ result = views.remove_member(request)
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(models.GroupMember.objects.count(), 1)
+ self.assertEqual(models.GroupMember.objects.first().user, self.local_user)
+ notification = models.Notification.objects.get()
+ self.assertEqual(notification.user, self.local_user)
+ self.assertEqual(notification.related_group, self.testgroup)
+ self.assertEqual(notification.notification_type, "LEAVE")
+
def test_accept_membership(self, _):
"""accept an invite"""
models.GroupMemberInvitation.objects.create(
diff --git a/bookwyrm/tests/views/test_interaction.py b/bookwyrm/tests/views/test_interaction.py
index 1d729f9a51..74878df7d7 100644
--- a/bookwyrm/tests/views/test_interaction.py
+++ b/bookwyrm/tests/views/test_interaction.py
@@ -60,7 +60,7 @@ def test_favorite(self, *_):
notification = models.Notification.objects.get()
self.assertEqual(notification.notification_type, "FAVORITE")
self.assertEqual(notification.user, self.local_user)
- self.assertEqual(notification.related_user, self.remote_user)
+ self.assertEqual(notification.related_users.first(), self.remote_user)
def test_unfavorite(self, *_):
"""unfav a status"""
@@ -98,7 +98,7 @@ def test_boost(self, *_):
notification = models.Notification.objects.get()
self.assertEqual(notification.notification_type, "BOOST")
self.assertEqual(notification.user, self.local_user)
- self.assertEqual(notification.related_user, self.remote_user)
+ self.assertEqual(notification.related_users.first(), self.remote_user)
self.assertEqual(notification.related_status, status)
def test_self_boost(self, *_):
diff --git a/bookwyrm/tests/views/test_notifications.py b/bookwyrm/tests/views/test_notifications.py
index 2a5cf79840..8e5dfa2b51 100644
--- a/bookwyrm/tests/views/test_notifications.py
+++ b/bookwyrm/tests/views/test_notifications.py
@@ -25,10 +25,12 @@ def setUp(self):
local=True,
localname="mouse",
)
+ self.another_user = models.User.objects.create_user(
+ "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
+ )
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
self.status = models.Status.objects.create(
- content="hi",
- user=self.local_user,
+ content="hi", user=self.local_user
)
models.SiteSettings.objects.create()
@@ -42,27 +44,31 @@ def test_notifications_page_empty(self):
validate_html(result.render())
self.assertEqual(result.status_code, 200)
- def test_notifications_page_notifications(self):
+ def test_notifications_page_status_notifications(self):
"""there are so many views, this just makes sure it LOADS"""
- models.Notification.objects.create(
- user=self.local_user,
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
notification_type="FAVORITE",
related_status=self.status,
)
- models.Notification.objects.create(
- user=self.local_user,
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
notification_type="BOOST",
related_status=self.status,
)
- models.Notification.objects.create(
- user=self.local_user,
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
notification_type="MENTION",
related_status=self.status,
)
self.status.reply_parent = self.status
self.status.save(broadcast=False)
- models.Notification.objects.create(
- user=self.local_user,
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
notification_type="REPLY",
related_status=self.status,
)
@@ -74,6 +80,200 @@ def test_notifications_page_notifications(self):
validate_html(result.render())
self.assertEqual(result.status_code, 200)
+ def test_notifications_page_follow_request(self):
+ """import completed notification"""
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
+ notification_type="FOLLOW_REQUEST",
+ )
+ view = views.Notifications.as_view()
+ request = self.factory.get("")
+ request.user = self.local_user
+ result = view(request)
+ self.assertIsInstance(result, TemplateResponse)
+ validate_html(result.render())
+
+ def test_notifications_page_follows(self):
+ """import completed notification"""
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
+ notification_type="FOLLOW",
+ )
+ view = views.Notifications.as_view()
+ request = self.factory.get("")
+ request.user = self.local_user
+ result = view(request)
+ self.assertIsInstance(result, TemplateResponse)
+ validate_html(result.render())
+
+ def test_notifications_page_report(self):
+ """import completed notification"""
+ report = models.Report.objects.create(
+ user=self.another_user,
+ reporter=self.local_user,
+ )
+ notification = models.Notification.objects.create(
+ user=self.local_user,
+ notification_type="REPORT",
+ )
+ notification.related_reports.add(report)
+
+ view = views.Notifications.as_view()
+ request = self.factory.get("")
+ request.user = self.local_user
+ result = view(request)
+ self.assertIsInstance(result, TemplateResponse)
+ validate_html(result.render())
+
+ def test_notifications_page_import(self):
+ """import completed notification"""
+ import_job = models.ImportJob.objects.create(user=self.local_user, mappings={})
+ models.Notification.objects.create(
+ user=self.local_user, notification_type="IMPORT", related_import=import_job
+ )
+ view = views.Notifications.as_view()
+ request = self.factory.get("")
+ request.user = self.local_user
+ result = view(request)
+ self.assertIsInstance(result, TemplateResponse)
+ validate_html(result.render())
+ self.assertEqual(result.status_code, 200)
+
+ def test_notifications_page_list(self):
+ """Adding books to lists"""
+ book = models.Edition.objects.create(title="shape")
+ with patch(
+ "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
+ ), patch("bookwyrm.lists_stream.remove_list_task.delay"):
+ book_list = models.List.objects.create(user=self.local_user, name="hi")
+ item = models.ListItem.objects.create(
+ book=book, user=self.another_user, book_list=book_list, order=1
+ )
+ models.Notification.notify_list_item(self.local_user, item)
+ view = views.Notifications.as_view()
+ request = self.factory.get("")
+ request.user = self.local_user
+ result = view(request)
+ self.assertIsInstance(result, TemplateResponse)
+ validate_html(result.render())
+ self.assertEqual(result.status_code, 200)
+
+ def test_notifications_page_group_invite(self):
+ """group related notifications"""
+ group = models.Group.objects.create(user=self.another_user, name="group")
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
+ notification_type="INVITE",
+ related_group=group,
+ )
+ view = views.Notifications.as_view()
+ request = self.factory.get("")
+ request.user = self.local_user
+ result = view(request)
+ self.assertIsInstance(result, TemplateResponse)
+ validate_html(result.render())
+ self.assertEqual(result.status_code, 200)
+
+ def test_notifications_page_group_accept(self):
+ """group related notifications"""
+ group = models.Group.objects.create(user=self.another_user, name="group")
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
+ notification_type="ACCEPT",
+ related_group=group,
+ )
+ view = views.Notifications.as_view()
+ request = self.factory.get("")
+ request.user = self.local_user
+ result = view(request)
+ self.assertIsInstance(result, TemplateResponse)
+ validate_html(result.render())
+ self.assertEqual(result.status_code, 200)
+
+ def test_notifications_page_group_join(self):
+ """group related notifications"""
+ group = models.Group.objects.create(user=self.another_user, name="group")
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
+ notification_type="JOIN",
+ related_group=group,
+ )
+ view = views.Notifications.as_view()
+ request = self.factory.get("")
+ request.user = self.local_user
+ result = view(request)
+ self.assertIsInstance(result, TemplateResponse)
+ validate_html(result.render())
+ self.assertEqual(result.status_code, 200)
+
+ def test_notifications_page_group_leave(self):
+ """group related notifications"""
+ group = models.Group.objects.create(user=self.another_user, name="group")
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
+ notification_type="LEAVE",
+ related_group=group,
+ )
+ view = views.Notifications.as_view()
+ request = self.factory.get("")
+ request.user = self.local_user
+ result = view(request)
+ self.assertIsInstance(result, TemplateResponse)
+ validate_html(result.render())
+ self.assertEqual(result.status_code, 200)
+
+ def test_notifications_page_group_remove(self):
+ """group related notifications"""
+ group = models.Group.objects.create(user=self.another_user, name="group")
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
+ notification_type="REMOVE",
+ related_group=group,
+ )
+ view = views.Notifications.as_view()
+ request = self.factory.get("")
+ request.user = self.local_user
+ result = view(request)
+ self.assertIsInstance(result, TemplateResponse)
+ validate_html(result.render())
+ self.assertEqual(result.status_code, 200)
+
+ def test_notifications_page_group_changes(self):
+ """group related notifications"""
+ group = models.Group.objects.create(user=self.another_user, name="group")
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
+ notification_type="GROUP_PRIVACY",
+ related_group=group,
+ )
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
+ notification_type="GROUP_NAME",
+ related_group=group,
+ )
+ models.Notification.notify(
+ self.local_user,
+ self.another_user,
+ notification_type="GROUP_DESCRIPTION",
+ related_group=group,
+ )
+ view = views.Notifications.as_view()
+ request = self.factory.get("")
+ request.user = self.local_user
+ result = view(request)
+ self.assertIsInstance(result, TemplateResponse)
+ validate_html(result.render())
+ self.assertEqual(result.status_code, 200)
+
def test_clear_notifications(self):
"""erase notifications"""
models.Notification.objects.create(
diff --git a/bookwyrm/views/group.py b/bookwyrm/views/group.py
index 9c282e48fd..469f787d3e 100644
--- a/bookwyrm/views/group.py
+++ b/bookwyrm/views/group.py
@@ -59,11 +59,11 @@ def post(self, request, group_id):
model = apps.get_model("bookwyrm.Notification", require_ready=True)
for field in form.changed_data:
notification_type = (
- "GROUP_PRIVACY"
+ model.GROUP_PRIVACY
if field == "privacy"
- else "GROUP_NAME"
+ else model.GROUP_NAME
if field == "name"
- else "GROUP_DESCRIPTION"
+ else model.GROUP_DESCRIPTION
if field == "description"
else None
)
@@ -71,9 +71,9 @@ def post(self, request, group_id):
for membership in memberships:
member = membership.user
if member != request.user:
- model.objects.create(
- user=member,
- related_user=request.user,
+ model.notify(
+ member,
+ request.user,
related_group=user_group,
notification_type=notification_type,
)
@@ -244,24 +244,22 @@ def remove_member(request):
memberships = models.GroupMember.objects.filter(group=group)
model = apps.get_model("bookwyrm.Notification", require_ready=True)
- notification_type = "LEAVE" if user == request.user else "REMOVE"
+ notification_type = model.LEAVE if user == request.user else model.REMOVE
# let the other members know about it
for membership in memberships:
member = membership.user
if member != request.user:
- model.objects.create(
- user=member,
- related_user=user,
+ model.notify(
+ member,
+ user,
related_group=group,
notification_type=notification_type,
)
# let the user (now ex-member) know as well, if they were removed
- if notification_type == "REMOVE":
- model.objects.create(
- user=user,
- related_group=group,
- notification_type=notification_type,
+ if notification_type == model.REMOVE:
+ model.notify(
+ user, None, related_group=group, notification_type=notification_type
)
return redirect(group.local_path)
diff --git a/bookwyrm/views/landing/password.py b/bookwyrm/views/landing/password.py
index 90713e29d1..a7eb001b00 100644
--- a/bookwyrm/views/landing/password.py
+++ b/bookwyrm/views/landing/password.py
@@ -3,7 +3,6 @@
from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect
from django.template.response import TemplateResponse
-from django.utils.translation import gettext_lazy as _
from django.views import View
from bookwyrm import models
@@ -24,12 +23,13 @@ def get(self, request):
def post(self, request):
"""create a password reset token"""
email = request.POST.get("email")
+ data = {"sent_message": True, "email": email}
try:
user = models.User.viewer_aware_objects(request.user).get(
email=email, email__isnull=False
)
except models.User.DoesNotExist:
- data = {"error": _("No user with that email address was found.")}
+ # Showing an error message would leak whether or not this email is in use
return TemplateResponse(
request, "landing/password_reset_request.html", data
)
@@ -40,7 +40,6 @@ def post(self, request):
# create a new reset code
code = models.PasswordReset.objects.create(user=user)
password_reset_email(code)
- data = {"message": _(f"A password reset link was sent to {email}")}
return TemplateResponse(request, "landing/password_reset_request.html", data)
diff --git a/bookwyrm/views/notifications.py b/bookwyrm/views/notifications.py
index 0a7a62002d..e4549ba983 100644
--- a/bookwyrm/views/notifications.py
+++ b/bookwyrm/views/notifications.py
@@ -15,16 +15,17 @@ def get(self, request, notification_type=None):
"""people are interacting with you, get hyped"""
notifications = (
request.user.notification_set.all()
- .order_by("-created_date")
+ .order_by("-updated_date")
.select_related(
"related_status",
"related_status__reply_parent",
+ "related_group",
"related_import",
- "related_report",
- "related_user",
- "related_book",
- "related_list_item",
- "related_list_item__book",
+ )
+ .prefetch_related(
+ "related_reports",
+ "related_users",
+ "related_list_items",
)
)
if notification_type == "mentions":