Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2022-07-07 09:34:57 -07:00
commit 2d2d0194a6
35 changed files with 1271 additions and 304 deletions

View file

@ -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",
),
]

View file

@ -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",
),
]

View file

@ -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 = []

View file

@ -3,7 +3,7 @@ from functools import reduce
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):

View file

@ -140,16 +140,6 @@ class GroupMemberInvitation(models.Model):
# 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 @@ class GroupMemberInvitation(models.Model):
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()

View file

@ -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:

View file

@ -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)
notification, _ = cls.objects.get_or_create(user=user, **kwargs)
if related_user:
notification.related_users.add(related_user)
notification.read = False
notification.save()
class Meta:
"""checks if notifcation is in enum list for valid types"""
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,
)

View file

@ -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 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
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"""

View file

@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
env = Env()
env.read_env()
DOMAIN = env("DOMAIN")
VERSION = "0.4.1"
VERSION = "0.4.2"
RELEASE_API = env(
"RELEASE_API",

View file

@ -9,7 +9,13 @@
<div class="block">
<h1 class="title">{% trans "Reset Password" %}</h1>
{% if message %}<p class="notification is-primary">{{ message }}</p>{% endif %}
{% if sent_message %}
<p class="notification is-primary">
{% blocktrans trimmed %}
A password reset link will be sent to <strong>{{ email }}</strong> if there is an account using that email address.
{% endblocktrans %}
</p>
{% endif %}
<p>{% trans "A link to reset your password will be sent to your email address" %}</p>
<form name="password-reset" method="post" action="/password-reset">

View file

@ -13,8 +13,34 @@
{% block description %}
{% if other_user_count == 0 %}
{% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %}
accepted your invitation to join group "<a href="{{ group_path }}">{{ group_name }}</a>"
<a href="{{ related_user_link }}">{{ related_user }}</a>
accepted your invitation to join group
"<a href="{{ group_path }}">{{ group_name }}</a>"
{% endblocktrans %}
{% elif other_user_count == 1 %}
{% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
and
<a href="{{ second_user_link }}">{{ second_user }}</a>
accepted your invitation to join group
"<a href="{{ group_path }}">{{ group_name }}</a>"
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
and
{{ other_user_display_count }} others
accepted your invitation to join group
"<a href="{{ group_path }}">{{ group_name }}</a>"
{% endblocktrans %}
{% endif %}
{% endblock %}

View file

@ -1,14 +1,16 @@
{% extends 'notifications/items/layout.html' %}
{% load i18n %}
{% load utilities %}
{% load humanize %}
{% block primary_link %}{% spaceless %}
{% if notification.related_list_item.approved %}
{{ notification.related_list_item.book_list.local_path }}
{% with related_list=notification.related_list_items.first.book_list %}
{% if related_list.curation != "curated" %}
{{ related_list.local_path }}
{% else %}
{% url 'list-curate' notification.related_list_item.book_list.id %}
{% url 'list-curate' related_list.id %}
{% endif %}
{% endwith %}
{% endspaceless %}{% endblock %}
{% block icon %}
@ -16,25 +18,89 @@
{% endblock %}
{% block description %}
{% with book_path=notification.related_list_item.book.local_path %}
{% with book_title=notification.related_list_item.book|book_title %}
{% with list_name=notification.related_list_item.book_list.name %}
{% with related_list=notification.related_list_items.first.book_list %}
{% with book_path=notification.related_list_items.first.book.local_path %}
{% with book_title=notification.related_list_items.first.book|book_title %}
{% with second_book_path=notification.related_list_items.all.1.book.local_path %}
{% with second_book_title=notification.related_list_items.all.1.book|book_title %}
{% with list_name=related_list.name %}
{% if notification.related_list_item.approved %}
{% blocktrans trimmed with list_path=notification.related_list_item.book_list.local_path %}
{% url 'list' related_list.id as list_path %}
{% url 'list-curate' related_list.id as list_curate_path %}
added <em><a href="{{ book_path }}">{{ book_title }}</a></em> to your list "<a href="{{ list_path }}">{{ list_name }}</a>"
{% endblocktrans %}
{% if notification.related_list_items.count == 1 %}
{% if related_list.curation != "curated" %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
added <em><a href="{{ book_path }}">{{ book_title }}</a></em>
to your list "<a href="{{ list_path }}">{{ list_name }}</a>"
{% endblocktrans %}
{% else %}
{% url 'list-curate' notification.related_list_item.book_list.id as list_path %}
{% blocktrans trimmed with list_path=list_path %}
suggested adding <em><a href="{{ book_path }}">{{ book_title }}</a></em> to your list "<a href="{{ list_path }}">{{ list_name }}</a>"
{% endblocktrans %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
suggested adding <em><a href="{{ book_path }}">{{ book_title }}</a></em>
to your list "<a href="{{ list_curate_path }}">{{ list_name }}</a>"
{% endblocktrans %}
{% endif %}
{% elif notification.related_list_items.count == 2 %}
{% if related_list.curation != "curated" %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
added <em><a href="{{ book_path }}">{{ book_title }}</a></em>
and <em><a href="{{ second_book_path }}">{{ second_book_title }}</a></em>
to your list "<a href="{{ list_path }}">{{ list_name }}</a>"
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
suggested adding <em><a href="{{ book_path }}">{{ book_title }}</a></em>
and <em><a href="{{ second_book_path }}">{{ second_book_title }}</a></em>
to your list "<a href="{{ list_curate_path }}">{{ list_name }}</a>"
{% endblocktrans %}
{% endif %}
{% else %}
{% with count=notification.related_list_items.count|add:"-2" %}
{% with display_count=count|intcomma %}
{% if related_list.curation != "curated" %}
{% blocktrans trimmed count counter=count %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
added <em><a href="{{ book_path }}">{{ book_title }}</a></em>,
<em><a href="{{ second_book_path }}">{{ second_book_title }}</a></em>,
and {{ display_count }} other book
to your list "<a href="{{ list_path }}">{{ list_name }}</a>"
{% plural %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
added <em><a href="{{ book_path }}">{{ book_title }}</a></em>,
<em><a href="{{ second_book_path }}">{{ second_book_title }}</a></em>,
and {{ display_count }} other books
to your list "<a href="{{ list_path }}">{{ list_name }}</a>"
{% endblocktrans %}
{% else %}
{% blocktrans trimmed count counter=count %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
suggested adding <em><a href="{{ book_path }}">{{ book_title }}</a></em>,
<em><a href="{{ second_book_path }}">{{ second_book_title }}</a></em>,
and {{ display_count }} other book
to your list "<a href="{{ list_curate_path }}">{{ list_name }}</a>"
{% plural %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
suggested adding <em><a href="{{ book_path }}">{{ book_title }}</a></em>,
<em><a href="{{ second_book_path }}">{{ second_book_title }}</a></em>,
and {{ display_count }} other books
to your list "<a href="{{ list_curate_path }}">{{ list_name }}</a>"
{% endblocktrans %}
{% endif %}
{% endwith %}
{% endwith %}
{% endif %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}

View file

@ -16,29 +16,97 @@
{% with related_status.local_path as related_path %}
{% if related_status.status_type == 'Review' %}
{% blocktrans trimmed %}
{% if other_user_count == 0 %}
boosted your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> boosted your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% endblocktrans %}
{% elif other_user_count == 1 %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
and
<a href="{{ second_user_link }}">{{ second_user }}</a>
boosted your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> and {{ other_user_display_count }} others boosted your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% endif %}
{% elif related_status.status_type == 'Comment' %}
{% blocktrans trimmed %}
{% if other_user_count == 0 %}
boosted your <a href="{{ related_path }}">comment on<em>{{ book_title }}</em></a>
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> boosted your <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% endblocktrans %}
{% elif other_user_count == 1 %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
and
<a href="{{ second_user_link }}">{{ second_user }}</a>
boosted your <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> and {{ other_user_display_count }} others boosted your <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% endif %}
{% elif related_status.status_type == 'Quotation' %}
{% blocktrans trimmed %}
{% if other_user_count == 0 %}
boosted your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> boosted your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% endblocktrans %}
{% elif other_user_count == 1 %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
and
<a href="{{ second_user_link }}">{{ second_user }}</a>
boosted your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> and {{ other_user_display_count }} others boosted your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% endif %}
{% else %}
{% blocktrans trimmed %}
{% if other_user_count == 0 %}
boosted your <a href="{{ related_path }}">status</a>
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> boosted your <a href="{{ related_path }}">status</a>
{% endblocktrans %}
{% endblocktrans %}
{% elif other_user_count == 1 %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
and
<a href="{{ second_user_link }}">{{ second_user }}</a>
boosted your <a href="{{ related_path }}">status</a>
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> and {{ other_user_display_count }} others boosted your <a href="{{ related_path }}">status</a>
{% endblocktrans %}
{% endif %}
{% endif %}
{% endwith %}

View file

@ -16,29 +16,98 @@
{% with related_status.local_path as related_path %}
{% if related_status.status_type == 'Review' %}
{% blocktrans trimmed %}
{% if other_user_count == 0 %}
liked your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> liked your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% endblocktrans %}
{% elif other_user_count == 1 %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
and
<a href="{{ second_user_link }}">{{ second_user }}</a>
liked your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> and {{ other_user_display_count }} others liked your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% endif %}
{% elif related_status.status_type == 'Comment' %}
{% blocktrans trimmed %}
{% if other_user_count == 0 %}
liked your <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> liked your <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% endblocktrans %}
{% elif other_user_count == 1 %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
and
<a href="{{ second_user_link }}">{{ second_user }}</a>
liked your <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> and {{ other_user_display_count }} others liked your <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% endif %}
{% elif related_status.status_type == 'Quotation' %}
{% blocktrans trimmed %}
{% if other_user_count == 0 %}
liked your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> liked your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% endblocktrans %}
{% elif other_user_count == 1 %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
and
<a href="{{ second_user_link }}">{{ second_user }}</a>
liked your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> and {{ other_user_display_count }} others liked your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% endif %}
{% else %}
{% blocktrans trimmed %}
{% if other_user_count == 0 %}
liked your <a href="{{ related_path }}">status</a>
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> liked your <a href="{{ related_path }}">status</a>
{% endblocktrans %}
{% elif other_user_count == 1 %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
and
<a href="{{ second_user_link }}">{{ second_user }}</a>
liked your <a href="{{ related_path }}">status</a>
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> and {{ other_user_display_count }} others liked your <a href="{{ related_path }}">status</a>
{% endblocktrans %}
{% endif %}
{% endblocktrans %}
{% endif %}
{% endwith %}

View file

@ -4,7 +4,7 @@
{% load utilities %}
{% block primary_link %}{% spaceless %}
{{ notification.related_user.local_path }}
{% url 'user-followers' request.user.localname %}
{% endspaceless %}{% endblock %}
{% block icon %}
@ -12,6 +12,19 @@
{% endblock %}
{% block description %}
{% trans "followed you" %}
{% include 'snippets/follow_button.html' with user=notification.related_user %}
{% if related_user_count == 1 %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> followed you
{% endblocktrans %}
{% elif related_user_count == 2 %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> and
<a href="{{ second_user_link }}">{{ second_user }}</a> followed you
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> and {{ other_user_display_count }} others followed you
{% endblocktrans %}
{% endif %}
{% endblock %}

View file

@ -3,13 +3,19 @@
{% load i18n %}
{% load utilities %}
{% block primary_link %}{% spaceless %}
{% url 'user-followers' request.user.localname %}
{% endspaceless %}{% endblock %}
{% block icon %}
<span class="icon icon-local"></span>
{% endblock %}
{% block description %}
{% trans "sent you a follow request" %}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a> sent you a follow request
{% endblocktrans %}
<div class="row shrink">
{% include 'snippets/follow_request_buttons.html' with user=notification.related_user %}
{% include 'snippets/follow_request_buttons.html' with user=notification.related_users.first %}
</div>
{% endblock %}

View file

@ -12,11 +12,15 @@
{% endblock %}
{% block description %}
{% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %}
invited you to join the group "<a href="{{ group_path }}">{{ group_name }}</a>"
<a href="{{ related_user_link }}">{{ related_user }}</a>
invited you to join the group
"<a href="{{ group_path }}">{{ group_name }}</a>"
{% endblocktrans %}
<div class="row shrink">
{% include 'snippets/join_invitation_buttons.html' with group=notification.related_group %}
</div>
{% endblock %}
{% endblock %}

View file

@ -1,5 +1,9 @@
{% load notification_page_tags %}
{% load humanize %}
{% related_status notification as related_status %}
{% with related_users=notification.related_users.all.distinct %}
{% with related_user_count=notification.related_users.count %}
<div class="notification {% if notification.id in unread %}has-background-primary{% endif %}">
<div class="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-more-muted{% endif %}">
<div class="column is-narrow is-size-3">
@ -9,14 +13,43 @@
</div>
<div class="column is-clipped">
{% if related_user_count > 1 %}
<div class="block">
<ul class="is-flex">
{% for user in related_users|slice:10 %}
<li class="mr-2">
<a href="{{ user.local_path }}">
{% include 'snippets/avatar.html' with user=user %}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="block content">
<p>
{% if notification.related_user %}
<a href="{{ notification.related_user.local_path }}">{% include 'snippets/avatar.html' with user=notification.related_user %}
{{ notification.related_user.display_name }}</a>
{% endif %}
{% if related_user_count == 1 %}
{% with user=related_users.first %}
{% spaceless %}
<a href="{{ user.local_path }}" class="mr-2">
{% include 'snippets/avatar.html' with user=user %}
</a>
{% endspaceless %}
{% endwith %}
{% endif %}
{% with related_user=related_users.first.display_name %}
{% with related_user_link=related_users.first.local_path %}
{% with second_user=related_users.1.display_name %}
{% with second_user_link=related_users.1.local_path %}
{% with other_user_count=related_user_count|add:"-1" %}
{% with other_user_display_count=other_user_count|intcomma %}
{% block description %}{% endblock %}
</p>
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
</div>
{% if related_status %}
@ -27,4 +60,5 @@
</div>
</div>
</div>
{% endwith %}
{% endwith %}

View file

@ -13,8 +13,34 @@
{% block description %}
{% if other_user_count == 0 %}
{% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %}
has left your group "<a href="{{ group_path }}">{{ group_name }}</a>"
<a href="{{ related_user_link }}">{{ related_user }}</a>
has left your group
"<a href="{{ group_path }}">{{ group_name }}</a>"
{% endblocktrans %}
{% elif other_user_count == 1 %}
{% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
and
<a href="{{ second_user_link }}">{{ second_user }}</a>
have left your group
"<a href="{{ group_path }}">{{ group_name }}</a>"
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with group_name=notification.related_group.name group_path=notification.related_group.local_path %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
and
{{ other_user_display_count }} others
have left your group
"<a href="{{ group_path }}">{{ group_name }}</a>"
{% endblocktrans %}
{% endif %}
{% endblock %}

View file

@ -19,25 +19,25 @@
{% if related_status.status_type == 'Review' %}
{% blocktrans trimmed %}
mentioned you in a <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
<a href="{{ related_user_link }}">{{ related_user }}</a> mentioned you in a <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% elif related_status.status_type == 'Comment' %}
{% blocktrans trimmed %}
mentioned you in a <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>
<a href="{{ related_user_link }}">{{ related_user }}</a> mentioned you in a <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% elif related_status.status_type == 'Quotation' %}
{% blocktrans trimmed %}
mentioned you in a <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
<a href="{{ related_user_link }}">{{ related_user }}</a> mentioned you in a <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
mentioned you in a <a href="{{ related_path }}">status</a>
<a href="{{ related_user_link }}">{{ related_user }}</a> mentioned you in a <a href="{{ related_path }}">status</a>
{% endblocktrans %}
{% endif %}

View file

@ -20,25 +20,25 @@
{% if related_status.reply_parent.status_type == 'Review' %}
{% blocktrans trimmed %}
<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">review of <em>{{ book_title }}</em></a>
<a href="{{ related_user_link }}">{{ related_user }}</a> <a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">review of <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% elif related_status.reply_parent.status_type == 'Comment' %}
{% blocktrans trimmed %}
<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">comment on <em>{{ book_title }}</em></a>
<a href="{{ related_user_link }}">{{ related_user }}</a> <a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">comment on <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% elif related_status.reply_parent.status_type == 'Quotation' %}
{% blocktrans trimmed %}
<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">quote from <em>{{ book_title }}</em></a>
<a href="{{ related_user_link }}">{{ related_user }}</a> <a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">quote from <em>{{ book_title }}</em></a>
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">status</a>
<a href="{{ related_user_link }}">{{ related_user }}</a> <a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">status</a>
{% endblocktrans %}
{% endif %}

View file

@ -1,9 +1,9 @@
{% extends 'notifications/items/layout.html' %}
{% load humanize %}
{% load i18n %}
{% block primary_link %}{% spaceless %}
{% url 'settings-report' notification.related_report.id %}
{% url 'settings-reports' %}
{% endspaceless %}{% endblock %}
{% block icon %}
@ -11,6 +11,10 @@
{% endblock %}
{% block description %}
{% url 'settings-report' notification.related_report.id as path %}
{% blocktrans %}A new <a href="{{ path }}">report</a> needs moderation.{% endblocktrans %}
{% url 'settings-reports' as path %}
{% blocktrans trimmed count counter=notification.related_reports.count with display_count=notification.related_reports.count|intcomma %}
A new <a href="{{ path }}">report</a> needs moderation
{% plural %}
{{ display_count }} new <a href="{{ path }}">reports</a> need moderation
{% endblocktrans %}
{% endblock %}

View file

@ -16,11 +16,11 @@
<select>
</select>
</div>
<div id="barcode-status" class="block">
<div class="grant-access is-hidden">
<span class="icon icon-lock"></span>
<span class="is-size-5">{% trans "Requesting camera..." %}</span></br>
<span class="is-size-5">{% trans "Requesting camera..." %}</span><br/>
<span>{% trans "Grant access to the camera to scan a book's barcode." %}</span>
</div>
<div class="access-denied is-hidden">

View file

@ -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 %}
<div class="is-small is-italic has-text-right mr-3">{% trans "page" %} {{ status.progress }}</div>
{% endif %}
{% endif %}
{% if status.attachments.exists %}

View file

@ -7,6 +7,7 @@ from bookwyrm import models, settings
@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 @@ class List(TestCase):
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 @@ class List(TestCase):
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 @@ class List(TestCase):
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 @@ class List(TestCase):
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)

View file

@ -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())

View file

@ -394,18 +394,6 @@ class Status(TestCase):
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"""

View file

@ -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"))

View file

@ -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")

View file

@ -257,6 +257,29 @@ class GroupViews(TestCase):
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(

View file

@ -60,7 +60,7 @@ class InteractionViews(TestCase):
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 @@ class InteractionViews(TestCase):
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, *_):

View file

@ -25,10 +25,12 @@ class NotificationViews(TestCase):
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 @@ class NotificationViews(TestCase):
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 @@ class NotificationViews(TestCase):
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(

View file

@ -59,11 +59,11 @@ class Group(View):
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 @@ class Group(View):
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)

View file

@ -3,7 +3,6 @@ from django.contrib.auth import login
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 @@ class PasswordResetRequest(View):
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 @@ class PasswordResetRequest(View):
# 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)

View file

@ -15,16 +15,17 @@ class Notifications(View):
"""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":