mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-22 16:16:39 +00:00
Merge branch 'main' into tour
Also fixes conflict
This commit is contained in:
commit
ab5e4128e6
129 changed files with 6239 additions and 2325 deletions
1
.github/workflows/django-tests.yml
vendored
1
.github/workflows/django-tests.yml
vendored
|
@ -55,5 +55,6 @@ jobs:
|
|||
EMAIL_HOST_PASSWORD: ""
|
||||
EMAIL_USE_TLS: true
|
||||
ENABLE_PREVIEW_IMAGES: false
|
||||
ENABLE_THUMBNAIL_GENERATION: true
|
||||
run: |
|
||||
pytest -n 3
|
||||
|
|
|
@ -160,12 +160,13 @@ class Connector(AbstractConnector):
|
|||
|
||||
def create_edition_from_data(self, work, edition_data, instance=None):
|
||||
"""pass in the url as data and then call the version in abstract connector"""
|
||||
try:
|
||||
data = self.get_book_data(edition_data)
|
||||
except ConnectorException:
|
||||
# who, indeed, knows
|
||||
return
|
||||
super().create_edition_from_data(work, data, instance=instance)
|
||||
if isinstance(edition_data, str):
|
||||
try:
|
||||
edition_data = self.get_book_data(edition_data)
|
||||
except ConnectorException:
|
||||
# who, indeed, knows
|
||||
return
|
||||
super().create_edition_from_data(work, edition_data, instance=instance)
|
||||
|
||||
def get_cover_url(self, cover_blob, *_):
|
||||
"""format the relative cover url into an absolute one:
|
||||
|
|
|
@ -45,7 +45,8 @@ def moderation_report_email(report):
|
|||
"""a report was created"""
|
||||
data = email_data()
|
||||
data["reporter"] = report.reporter.localname or report.reporter.username
|
||||
data["reportee"] = report.user.localname or report.user.username
|
||||
if report.user:
|
||||
data["reportee"] = report.user.localname or report.user.username
|
||||
data["report_link"] = report.remote_id
|
||||
|
||||
for admin in models.User.objects.filter(
|
||||
|
|
|
@ -10,3 +10,4 @@ from .landing import *
|
|||
from .links import *
|
||||
from .lists import *
|
||||
from .status import *
|
||||
from .user_admin import *
|
||||
|
|
|
@ -4,12 +4,6 @@ from .custom_form import CustomForm
|
|||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class UserGroupForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["groups"]
|
||||
|
||||
|
||||
class GroupForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Group
|
||||
|
|
10
bookwyrm/forms/user_admin.py
Normal file
10
bookwyrm/forms/user_admin.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
""" using django model forms """
|
||||
from bookwyrm import models
|
||||
from .custom_form import CustomForm
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class UserGroupForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["groups"]
|
24
bookwyrm/migrations/0151_alter_report_user.py
Normal file
24
bookwyrm/migrations/0151_alter_report_user.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.2.13 on 2022-07-05 23:54
|
||||
|
||||
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.AlterField(
|
||||
model_name="report",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
90
bookwyrm/migrations/0151_auto_20220705_0049.py
Normal file
90
bookwyrm/migrations/0151_auto_20220705_0049.py
Normal 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",
|
||||
),
|
||||
]
|
25
bookwyrm/migrations/0152_alter_report_user.py
Normal file
25
bookwyrm/migrations/0152_alter_report_user.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.2.13 on 2022-07-06 19:16
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0151_alter_report_user"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="report",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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",
|
||||
),
|
||||
]
|
13
bookwyrm/migrations/0153_merge_20220706_2141.py
Normal file
13
bookwyrm/migrations/0153_merge_20220706_2141.py
Normal 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 = []
|
|
@ -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):
|
||||
|
|
|
@ -132,7 +132,7 @@ class BookWyrmModel(models.Model):
|
|||
return
|
||||
|
||||
# but generally moderators can delete other people's stuff
|
||||
if self.user == viewer or viewer.has_perm("moderate_post"):
|
||||
if self.user == viewer or viewer.has_perm("bookwyrm.moderate_post"):
|
||||
return
|
||||
|
||||
raise PermissionDenied()
|
||||
|
|
|
@ -16,7 +16,7 @@ from django.utils.encoding import filepath_to_uri
|
|||
|
||||
from bookwyrm import activitypub
|
||||
from bookwyrm.connectors import get_image
|
||||
from bookwyrm.sanitize_html import InputHtmlParser
|
||||
from bookwyrm.utils.sanitizer import clean
|
||||
from bookwyrm.settings import MEDIA_FULL_URL
|
||||
|
||||
|
||||
|
@ -497,9 +497,7 @@ class HtmlField(ActivitypubFieldMixin, models.TextField):
|
|||
def field_from_activity(self, value):
|
||||
if not value or value == MISSING:
|
||||
return None
|
||||
sanitizer = InputHtmlParser()
|
||||
sanitizer.feed(value)
|
||||
return sanitizer.get_output()
|
||||
return clean(value)
|
||||
|
||||
|
||||
class ArrayField(ActivitypubFieldMixin, DjangoArrayField):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -84,7 +84,7 @@ class LinkDomain(BookWyrmModel):
|
|||
)
|
||||
|
||||
def raise_not_editable(self, viewer):
|
||||
if viewer.has_perm("moderate_post"):
|
||||
if viewer.has_perm("bookwyrm.moderate_post"):
|
||||
return
|
||||
raise PermissionDenied()
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,23 +214,92 @@ 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):
|
||||
def notify_admins_on_report(sender, instance, created, *args, **kwargs):
|
||||
"""something is up, make sure the admins know"""
|
||||
if not created:
|
||||
# otherwise you'll get a notification when you resolve a report
|
||||
return
|
||||
|
||||
# moderators and superusers should be notified
|
||||
admins = User.objects.filter(
|
||||
models.Q(user_permissions__name__in=["moderate_user", "moderate_post"])
|
||||
| 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,
|
||||
)
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
@ -218,7 +209,7 @@ def clear_cache(user_subject, user_object):
|
|||
"""clear relationship cache"""
|
||||
cache.delete_many(
|
||||
[
|
||||
f"relationship-{user_subject.id}-{user_object.id}",
|
||||
f"relationship-{user_object.id}-{user_subject.id}",
|
||||
f"cached-relationship-{user_subject.id}-{user_object.id}",
|
||||
f"cached-relationship-{user_object.id}-{user_subject.id}",
|
||||
]
|
||||
)
|
||||
|
|
|
@ -11,7 +11,7 @@ class Report(BookWyrmModel):
|
|||
"User", related_name="reporter", on_delete=models.PROTECT
|
||||
)
|
||||
note = models.TextField(null=True, blank=True)
|
||||
user = models.ForeignKey("User", on_delete=models.PROTECT)
|
||||
user = models.ForeignKey("User", on_delete=models.PROTECT, null=True, blank=True)
|
||||
status = models.ForeignKey(
|
||||
"Status",
|
||||
null=True,
|
||||
|
|
|
@ -103,12 +103,25 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel):
|
|||
if not self.user:
|
||||
self.user = self.shelf.user
|
||||
if self.id and self.user.local:
|
||||
cache.delete(f"book-on-shelf-{self.book.id}-{self.shelf.id}")
|
||||
# remove all caches related to all editions of this book
|
||||
cache.delete_many(
|
||||
[
|
||||
f"book-on-shelf-{book.id}-{self.shelf.id}"
|
||||
for book in self.book.parent_work.editions.all()
|
||||
]
|
||||
)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
if self.id and self.user.local:
|
||||
cache.delete(f"book-on-shelf-{self.book.id}-{self.shelf.id}")
|
||||
cache.delete_many(
|
||||
[
|
||||
f"book-on-shelf-{book}-{self.shelf.id}"
|
||||
for book in self.book.parent_work.editions.values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
]
|
||||
)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
""" html parser to clean up incoming text from unknown sources """
|
||||
from html.parser import HTMLParser
|
||||
|
||||
|
||||
class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
|
||||
"""Removes any html that isn't allowed_tagsed from a block"""
|
||||
|
||||
def __init__(self):
|
||||
HTMLParser.__init__(self)
|
||||
self.allowed_tags = [
|
||||
"p",
|
||||
"blockquote",
|
||||
"br",
|
||||
"b",
|
||||
"i",
|
||||
"strong",
|
||||
"em",
|
||||
"pre",
|
||||
"a",
|
||||
"span",
|
||||
"ul",
|
||||
"ol",
|
||||
"li",
|
||||
]
|
||||
self.allowed_attrs = ["href", "rel", "src", "alt"]
|
||||
self.tag_stack = []
|
||||
self.output = []
|
||||
# if the html appears invalid, we just won't allow any at all
|
||||
self.allow_html = True
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
"""check if the tag is valid"""
|
||||
if self.allow_html and tag in self.allowed_tags:
|
||||
allowed_attrs = " ".join(
|
||||
f'{a}="{v}"' for a, v in attrs if a in self.allowed_attrs
|
||||
)
|
||||
reconstructed = f"<{tag}"
|
||||
if allowed_attrs:
|
||||
reconstructed += " " + allowed_attrs
|
||||
reconstructed += ">"
|
||||
self.output.append(("tag", reconstructed))
|
||||
self.tag_stack.append(tag)
|
||||
else:
|
||||
self.output.append(("data", ""))
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
"""keep the close tag"""
|
||||
if not self.allow_html or tag not in self.allowed_tags:
|
||||
self.output.append(("data", ""))
|
||||
return
|
||||
|
||||
if not self.tag_stack or self.tag_stack[-1] != tag:
|
||||
# the end tag doesn't match the most recent start tag
|
||||
self.allow_html = False
|
||||
self.output.append(("data", ""))
|
||||
return
|
||||
|
||||
self.tag_stack = self.tag_stack[:-1]
|
||||
self.output.append(("tag", f"</{tag}>"))
|
||||
|
||||
def handle_data(self, data):
|
||||
"""extract the answer, if we're in an answer tag"""
|
||||
self.output.append(("data", data))
|
||||
|
||||
def get_output(self):
|
||||
"""convert the output from a list of tuples to a string"""
|
||||
if self.tag_stack:
|
||||
self.allow_html = False
|
||||
if not self.allow_html:
|
||||
return "".join(v for (k, v) in self.output if k == "data")
|
||||
return "".join(v for (k, v) in self.output)
|
|
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
env = Env()
|
||||
env.read_env()
|
||||
DOMAIN = env("DOMAIN")
|
||||
VERSION = "0.4.0"
|
||||
VERSION = "0.4.2"
|
||||
|
||||
RELEASE_API = env(
|
||||
"RELEASE_API",
|
||||
|
|
|
@ -2,15 +2,13 @@
|
|||
from django.db import transaction
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.sanitize_html import InputHtmlParser
|
||||
from bookwyrm.utils import sanitizer
|
||||
|
||||
|
||||
def create_generated_note(user, content, mention_books=None, privacy="public"):
|
||||
"""a note created by the app about user activity"""
|
||||
# sanitize input html
|
||||
parser = InputHtmlParser()
|
||||
parser.feed(content)
|
||||
content = parser.get_output()
|
||||
content = sanitizer.clean(content)
|
||||
|
||||
with transaction.atomic():
|
||||
# create but don't save
|
||||
|
|
|
@ -42,7 +42,11 @@
|
|||
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer">{{ link.url }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if link.added_by %}
|
||||
<a href="{% url 'user-feed' link.added_by.id %}">{{ link.added_by.display_name }}</a>
|
||||
{% else %}
|
||||
<em>{% trans "Unknown user" %}</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ link.filelink.filetype }}
|
||||
|
@ -50,7 +54,7 @@
|
|||
<td>
|
||||
{{ link.domain.name }}
|
||||
<p>
|
||||
<a href="{% url 'report-link' link.added_by.id link.id %}">{% trans "Report spam" %}</a>
|
||||
<a href="{% url 'report-link' link.id %}">{% trans "Report spam" %}</a>
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
|
|
|
@ -19,7 +19,7 @@ Is that where you'd like to go?
|
|||
{% block modal-footer %}
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="is-flex-grow-1">
|
||||
<a href="{% url 'report-link' link.added_by.id link.id %}">{% trans "Report spam" %}</a>
|
||||
<a href="{% url 'report-link' link.id %}">{% trans "Report spam" %}</a>
|
||||
</div>
|
||||
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
|
|
|
@ -3,7 +3,19 @@
|
|||
|
||||
{% block content %}
|
||||
<p>
|
||||
{% blocktrans %}@{{ reporter }} has flagged behavior by @{{ reportee }} for moderation. {% endblocktrans %}
|
||||
{% if report_link %}
|
||||
|
||||
{% blocktrans trimmed %}
|
||||
@{{ reporter }} has flagged a link domain for moderation.
|
||||
{% endblocktrans %}
|
||||
|
||||
{% else %}
|
||||
|
||||
{% blocktrans trimmed %}
|
||||
@{{ reporter }} has flagged behavior by @{{ reportee }} for moderation.
|
||||
{% endblocktrans %}
|
||||
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% trans "View report" as text %}
|
||||
|
|
|
@ -2,7 +2,15 @@
|
|||
{% load i18n %}
|
||||
{% block content %}
|
||||
|
||||
{% blocktrans %}@{{ reporter}} has flagged behavior by @{{ reportee }} for moderation. {% endblocktrans %}
|
||||
{% if report_link %}
|
||||
{% blocktrans trimmed %}
|
||||
@{{ reporter }} has flagged a link domain for moderation.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
@{{ reporter }} has flagged behavior by @{{ reportee }} for moderation.
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
{% trans "View report" %}
|
||||
{{ report_link }}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -37,50 +37,40 @@
|
|||
</div>
|
||||
|
||||
<div class="columns block is-multiline">
|
||||
{% if reports %}
|
||||
<div class="column is-flex">
|
||||
<a href="{% url 'settings-reports' %}" class="notification is-warning is-block is-flex-grow-1">
|
||||
{% blocktrans trimmed count counter=reports with display_count=reports|intcomma %}
|
||||
{{ display_count }} open report
|
||||
{% plural %}
|
||||
{{ display_count }} open reports
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</div>
|
||||
{% if email_config_error %}
|
||||
{% include 'settings/dashboard/warnings/email_config.html' with warning_level="danger" fullwidth=True %}
|
||||
{% endif %}
|
||||
|
||||
{% if pending_domains %}
|
||||
<div class="column is-flex">
|
||||
<a href="{% url 'settings-link-domain' %}" class="notification is-primary is-block is-flex-grow-1">
|
||||
{% blocktrans trimmed count counter=pending_domains with display_count=pending_domains|intcomma %}
|
||||
{{ display_count }} domain needs review
|
||||
{% plural %}
|
||||
{{ display_count }} domains need review
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</div>
|
||||
{% if current_version %}
|
||||
{% include 'settings/dashboard/warnings/update_version.html' with warning_level="warning" fullwidth=True %}
|
||||
{% endif %}
|
||||
|
||||
{% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
|
||||
<div class="column is-flex">
|
||||
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success is-flex-grow-1">
|
||||
{% blocktrans trimmed count counter=invite_requests with display_count=invite_requests|intcomma %}
|
||||
{{ display_count }} invite request
|
||||
{% plural %}
|
||||
{{ display_count }} invite requests
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
{% if missing_privacy or missing_conduct %}
|
||||
<div class="column is-12 columns m-0 p-0">
|
||||
{% if missing_privacy %}
|
||||
{% include 'settings/dashboard/warnings/missing_privacy.html' with warning_level="danger" %}
|
||||
{% endif %}
|
||||
|
||||
{% if missing_conduct %}
|
||||
{% include 'settings/dashboard/warnings/missing_conduct.html' with warning_level="warning" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_version %}
|
||||
<div class="column is-flex">
|
||||
<a href="https://docs.joinbookwyrm.com/updating-your-instance.html" class="notification is-block is-warning is-flex-grow-1" target="_blank">
|
||||
{% blocktrans trimmed with current=current_version available=available_version %}
|
||||
An update is available! You're running v{{ current }} and the latest release is {{ available }}.
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</div>
|
||||
{% include 'settings/dashboard/warnings/update_version.html' with warning_level="warning" fullwidth=True %}
|
||||
{% endif %}
|
||||
|
||||
{% if reports %}
|
||||
{% include 'settings/dashboard/warnings/reports.html' with warning_level="warning" %}
|
||||
{% endif %}
|
||||
|
||||
{% if pending_domains %}
|
||||
{% include 'settings/dashboard/warnings/domain_review.html' with warning_level="primary" %}
|
||||
{% endif %}
|
||||
|
||||
{% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
|
||||
{% include 'settings/dashboard/warnings/invites.html' with warning_level="success" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
{% extends 'settings/dashboard/warnings/layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block warning_link %}{% url 'settings-link-domain' %}{% endblock %}
|
||||
|
||||
{% block warning_text %}
|
||||
|
||||
{% blocktrans trimmed count counter=pending_domains with display_count=pending_domains|intcomma %}
|
||||
{{ display_count }} domain needs review
|
||||
{% plural %}
|
||||
{{ display_count }} domains need review
|
||||
{% endblocktrans %}
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,13 @@
|
|||
{% extends 'settings/dashboard/warnings/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block warning_link %}https://docs.joinbookwyrm.com/install-prod.html{% endblock %}
|
||||
|
||||
{% block warning_text %}
|
||||
|
||||
{% blocktrans trimmed %}
|
||||
Your outgoing email address, <code>{{ email_sender }}</code>, may be misconfigured.
|
||||
{% endblocktrans %}
|
||||
{% trans "Check the <code>EMAIL_SENDER_NAME</code> and <code>EMAIL_SENDER_DOMAIN</code> in your <code>.env</code> file." %}
|
||||
|
||||
{% endblock %}
|
15
bookwyrm/templates/settings/dashboard/warnings/invites.html
Normal file
15
bookwyrm/templates/settings/dashboard/warnings/invites.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends 'settings/dashboard/warnings/layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block warning_link %}{% url 'settings-invite-requests' %}{% endblock %}
|
||||
|
||||
{% block warning_text %}
|
||||
|
||||
{% blocktrans trimmed count counter=invite_requests with display_count=invite_requests|intcomma %}
|
||||
{{ display_count }} invite request
|
||||
{% plural %}
|
||||
{{ display_count }} invite requests
|
||||
{% endblocktrans %}
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,5 @@
|
|||
<div class="column is-flex{% if fullwidth %} is-12{% endif %}">
|
||||
<a href="{% block warning_link %}{% endblock %}" class="notification is-{{ warning_level }} is-block is-flex-grow-1">
|
||||
{% block warning_text %}{% endblock %}
|
||||
</a>
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
{% extends 'settings/dashboard/warnings/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block warning_link %}{% url 'settings-site' %}#instance-info{% endblock %}
|
||||
|
||||
{% block warning_text %}
|
||||
|
||||
{% trans "Your instance is missing a code of conduct." %}
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,10 @@
|
|||
{% extends 'settings/dashboard/warnings/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block warning_link %}{% url 'settings-site' %}#instance-info{% endblock %}
|
||||
|
||||
{% block warning_text %}
|
||||
|
||||
{% trans "Your instance is missing a privacy policy." %}
|
||||
|
||||
{% endblock %}
|
15
bookwyrm/templates/settings/dashboard/warnings/reports.html
Normal file
15
bookwyrm/templates/settings/dashboard/warnings/reports.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends 'settings/dashboard/warnings/layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block warning_link %}{% url 'settings-reports' %}{% endblock %}
|
||||
|
||||
{% block warning_text %}
|
||||
|
||||
{% blocktrans trimmed count counter=reports with display_count=reports|intcomma %}
|
||||
{{ display_count }} open report
|
||||
{% plural %}
|
||||
{{ display_count }} open reports
|
||||
{% endblocktrans %}
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,12 @@
|
|||
{% extends 'settings/dashboard/warnings/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block warning_link %}https://docs.joinbookwyrm.com/updating.html{% endblock %}
|
||||
|
||||
{% block warning_text %}
|
||||
|
||||
{% blocktrans trimmed with current=current_version available=available_version %}
|
||||
An update is available! You're running v{{ current }} and the latest release is {{ available }}.
|
||||
{% endblocktrans %}
|
||||
|
||||
{% endblock %}
|
|
@ -56,7 +56,7 @@
|
|||
<dt class="is-pulled-left mr-5">{% trans "Users:" %}</dt>
|
||||
<dd>
|
||||
{{ users.count }}
|
||||
{% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
|
||||
{% if server.user_set.count %}(<a href="{% url 'settings-users' status="federated" %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
|
||||
</dd>
|
||||
|
||||
<dt class="is-pulled-left mr-5">{% trans "Reports:" %}</dt>
|
||||
|
|
|
@ -15,7 +15,11 @@
|
|||
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer">{{ link.url }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if link.added_by %}
|
||||
<a href="{% url 'settings-user' link.added_by.id %}">@{{ link.added_by|username }}</a>
|
||||
{% else %}
|
||||
<em>{% trans "Unknown user" %}</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if link.filelink.filetype %}
|
||||
|
|
|
@ -55,9 +55,11 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if report.user %}
|
||||
{% include 'settings/users/user_info.html' with user=report.user %}
|
||||
|
||||
{% include 'settings/users/user_moderation_actions.html' with user=report.user %}
|
||||
{% endif %}
|
||||
|
||||
<div class="block">
|
||||
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3>
|
||||
|
|
|
@ -9,9 +9,15 @@ Report #{{ report_id }}: Status posted by @{{ username }}
|
|||
|
||||
{% elif report.links.exists %}
|
||||
|
||||
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
|
||||
Report #{{ report_id }}: Link added by @{{ username }}
|
||||
{% endblocktrans %}
|
||||
{% if report.user %}
|
||||
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
|
||||
Report #{{ report_id }}: Link added by @{{ username }}
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with report_id=report.id %}
|
||||
Report #{{ report_id }}: Link domain
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
|
||||
|
|
|
@ -24,6 +24,10 @@
|
|||
<li {% if request.path in url %}class="is-active" aria-current="page"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Local users" %}</a>
|
||||
</li>
|
||||
{% url 'settings-users' status="deleted" as url %}
|
||||
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Deleted users" %}</a>
|
||||
</li>
|
||||
{% url 'settings-users' status="federated" as url %}
|
||||
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Federated community" %}</a>
|
||||
|
@ -36,7 +40,7 @@
|
|||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
{% url 'settings-users' as url %}
|
||||
<th>
|
||||
<th colspan="2">
|
||||
{% trans "Username" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="username" sort=sort text=text %}
|
||||
</th>
|
||||
|
@ -52,7 +56,7 @@
|
|||
{% trans "Status" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="is_active" sort=sort text=text %}
|
||||
</th>
|
||||
{% if status != "local" %}
|
||||
{% if status == "federated" %}
|
||||
<th>
|
||||
{% trans "Remote instance" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="federated_server__server_name" sort=sort text=text %}
|
||||
|
@ -61,7 +65,10 @@
|
|||
</tr>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td class="overflow-wrap-anywhere">
|
||||
<td class="pr-0">
|
||||
{% include 'snippets/avatar.html' with user=user %}
|
||||
</td>
|
||||
<td class="overflow-wrap-anywhere pl-1">
|
||||
<a href="{% url 'settings-user' user.id %}">{{ user|username }}</a>
|
||||
</td>
|
||||
<td>{{ user.created_date }}</td>
|
||||
|
@ -72,6 +79,12 @@
|
|||
<span class="icon icon-check"></span>
|
||||
</span>
|
||||
{% trans "Active" %}
|
||||
{% elif user.deactivation_reason == "moderator_deletion" or user.deactivation_reason == "self_deletion" %}
|
||||
<span class="tag is-danger" aria-hidden="true">
|
||||
<span class="icon icon-x"></span>
|
||||
</span>
|
||||
{% trans "Deleted" %}
|
||||
<span class="help">({{ user.get_deactivation_reason_display }})</span>
|
||||
{% else %}
|
||||
<span class="tag is-warning" aria-hidden="true">
|
||||
<span class="icon icon-x"></span>
|
||||
|
@ -80,7 +93,7 @@
|
|||
<span class="help">({{ user.get_deactivation_reason_display }})</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if status != "local" %}
|
||||
{% if status == "federated" %}
|
||||
<td>
|
||||
{% if user.federated_server %}
|
||||
<a href="{% url 'settings-federated-server' user.federated_server.id %}">{{ user.federated_server.server_name }}</a>
|
||||
|
|
|
@ -144,7 +144,7 @@
|
|||
{% blocktrans trimmed %}
|
||||
You can change your instance settings in the <code>.env</code> file on your server.
|
||||
{% endblocktrans %}
|
||||
<a href="https://docs.joinbookwyrm.com/installing-in-production.html" target="_blank">
|
||||
<a href="https://docs.joinbookwyrm.com/install-prod.html" target="_blank">
|
||||
{% trans "View installation instructions" %}
|
||||
</a>
|
||||
</p>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -42,7 +42,7 @@ def get_relationship(context, user_object):
|
|||
"""caches the relationship between the logged in user and another user"""
|
||||
user = context["request"].user
|
||||
return get_or_set(
|
||||
f"relationship-{user.id}-{user_object.id}",
|
||||
f"cached-relationship-{user.id}-{user_object.id}",
|
||||
get_relationship_name,
|
||||
user,
|
||||
user_object,
|
||||
|
@ -61,6 +61,6 @@ def get_relationship_name(user, user_object):
|
|||
types["is_blocked"] = True
|
||||
elif user_object in user.following.all():
|
||||
types["is_following"] = True
|
||||
elif user_object in user.follower_requests.all():
|
||||
elif user in user_object.follower_requests.all():
|
||||
types["is_follow_pending"] = True
|
||||
return types
|
||||
|
|
|
@ -17,7 +17,7 @@ def get_is_book_on_shelf(book, shelf):
|
|||
lambda b, s: s.books.filter(id=b.id).exists(),
|
||||
book,
|
||||
shelf,
|
||||
timeout=15552000,
|
||||
timeout=60 * 60, # just cache this for an hour
|
||||
)
|
||||
|
||||
|
||||
|
@ -68,7 +68,7 @@ def active_shelf(context, book):
|
|||
),
|
||||
user,
|
||||
book,
|
||||
timeout=15552000,
|
||||
timeout=60 * 60,
|
||||
) or {"book": book}
|
||||
|
||||
|
||||
|
@ -85,5 +85,5 @@ def latest_read_through(book, user):
|
|||
),
|
||||
user,
|
||||
book,
|
||||
timeout=15552000,
|
||||
timeout=60 * 60,
|
||||
)
|
||||
|
|
|
@ -53,7 +53,7 @@ def comparison_bool(str1, str2, reverse=False):
|
|||
|
||||
@register.filter(is_safe=True)
|
||||
def truncatepath(value, arg):
|
||||
"""Truncate a path by removing all directories except the first and truncating ."""
|
||||
"""Truncate a path by removing all directories except the first and truncating"""
|
||||
path = os.path.normpath(value.name)
|
||||
path_list = path.split(os.sep)
|
||||
try:
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
""" testing models """
|
||||
from dateutil.parser import parse
|
||||
from io import BytesIO
|
||||
import pathlib
|
||||
|
||||
from dateutil.parser import parse
|
||||
from PIL import Image
|
||||
from django.core.files.base import ContentFile
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
|
@ -96,3 +100,28 @@ class Book(TestCase):
|
|||
self.first_edition.description = "hi"
|
||||
self.first_edition.save()
|
||||
self.assertEqual(self.first_edition.edition_rank, 1)
|
||||
|
||||
def test_thumbnail_fields(self):
|
||||
"""Just hit them"""
|
||||
image_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../../static/images/default_avi.jpg"
|
||||
)
|
||||
image = Image.open(image_file)
|
||||
output = BytesIO()
|
||||
image.save(output, format=image.format)
|
||||
|
||||
book = models.Edition.objects.create(title="hello")
|
||||
book.cover.save("test.jpg", ContentFile(output.getvalue()))
|
||||
|
||||
self.assertIsNotNone(book.cover_bw_book_xsmall_webp.url)
|
||||
self.assertIsNotNone(book.cover_bw_book_xsmall_jpg.url)
|
||||
self.assertIsNotNone(book.cover_bw_book_small_webp.url)
|
||||
self.assertIsNotNone(book.cover_bw_book_small_jpg.url)
|
||||
self.assertIsNotNone(book.cover_bw_book_medium_webp.url)
|
||||
self.assertIsNotNone(book.cover_bw_book_medium_jpg.url)
|
||||
self.assertIsNotNone(book.cover_bw_book_large_webp.url)
|
||||
self.assertIsNotNone(book.cover_bw_book_large_jpg.url)
|
||||
self.assertIsNotNone(book.cover_bw_book_xlarge_webp.url)
|
||||
self.assertIsNotNone(book.cover_bw_book_xlarge_jpg.url)
|
||||
self.assertIsNotNone(book.cover_bw_book_xxlarge_webp.url)
|
||||
self.assertIsNotNone(book.cover_bw_book_xxlarge_jpg.url)
|
||||
|
|
|
@ -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)
|
||||
|
|
183
bookwyrm/tests/models/test_notification.py
Normal file
183
bookwyrm/tests/models/test_notification.py
Normal 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())
|
|
@ -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"""
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" style fixes and lookups for templates """
|
||||
from collections import namedtuple
|
||||
import re
|
||||
from unittest.mock import patch
|
||||
|
||||
|
@ -61,3 +62,18 @@ class UtilitiesTags(TestCase):
|
|||
self.assertEqual(utilities.get_title(self.book), "Test Book")
|
||||
book = models.Edition.objects.create(title="Oh", subtitle="oh my")
|
||||
self.assertEqual(utilities.get_title(book), "Oh: oh my")
|
||||
|
||||
def test_comparison_bool(self, *_):
|
||||
"""just a simple comparison"""
|
||||
self.assertTrue(utilities.comparison_bool("a", "a"))
|
||||
self.assertFalse(utilities.comparison_bool("a", "b"))
|
||||
|
||||
self.assertFalse(utilities.comparison_bool("a", "a", reverse=True))
|
||||
self.assertTrue(utilities.comparison_bool("a", "b", reverse=True))
|
||||
|
||||
def test_truncatepath(self, *_):
|
||||
"""truncate a path"""
|
||||
ValueMock = namedtuple("Value", ("name"))
|
||||
value = ValueMock("home/one/two/three/four")
|
||||
self.assertEqual(utilities.truncatepath(value, 2), "home/…ur")
|
||||
self.assertEqual(utilities.truncatepath(value, "a"), "four")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
""" make sure only valid html gets to the app """
|
||||
from django.test import TestCase
|
||||
|
||||
from bookwyrm.sanitize_html import InputHtmlParser
|
||||
from bookwyrm.utils.sanitizer import clean
|
||||
|
||||
|
||||
class Sanitizer(TestCase):
|
||||
|
@ -10,53 +10,39 @@ class Sanitizer(TestCase):
|
|||
def test_no_html(self):
|
||||
"""just text"""
|
||||
input_text = "no html "
|
||||
parser = InputHtmlParser()
|
||||
parser.feed(input_text)
|
||||
output = parser.get_output()
|
||||
output = clean(input_text)
|
||||
self.assertEqual(input_text, output)
|
||||
|
||||
def test_valid_html(self):
|
||||
"""leave the html untouched"""
|
||||
input_text = "<b>yes </b> <i>html</i>"
|
||||
parser = InputHtmlParser()
|
||||
parser.feed(input_text)
|
||||
output = parser.get_output()
|
||||
output = clean(input_text)
|
||||
self.assertEqual(input_text, output)
|
||||
|
||||
def test_valid_html_attrs(self):
|
||||
"""and don't remove useful attributes"""
|
||||
input_text = '<a href="fish.com">yes </a> <i>html</i>'
|
||||
parser = InputHtmlParser()
|
||||
parser.feed(input_text)
|
||||
output = parser.get_output()
|
||||
output = clean(input_text)
|
||||
self.assertEqual(input_text, output)
|
||||
|
||||
def test_valid_html_invalid_attrs(self):
|
||||
"""do remove un-approved attributes"""
|
||||
input_text = '<a href="fish.com" fish="hello">yes </a> <i>html</i>'
|
||||
parser = InputHtmlParser()
|
||||
parser.feed(input_text)
|
||||
output = parser.get_output()
|
||||
output = clean(input_text)
|
||||
self.assertEqual(output, '<a href="fish.com">yes </a> <i>html</i>')
|
||||
|
||||
def test_invalid_html(self):
|
||||
"""remove all html when the html is malformed"""
|
||||
"""don't allow malformed html"""
|
||||
input_text = "<b>yes <i>html</i>"
|
||||
parser = InputHtmlParser()
|
||||
parser.feed(input_text)
|
||||
output = parser.get_output()
|
||||
self.assertEqual("yes html", output)
|
||||
output = clean(input_text)
|
||||
self.assertEqual("<b>yes <i>html</i></b>", output)
|
||||
|
||||
input_text = "yes <i></b>html </i>"
|
||||
parser = InputHtmlParser()
|
||||
parser.feed(input_text)
|
||||
output = parser.get_output()
|
||||
self.assertEqual("yes html ", output)
|
||||
output = clean(input_text)
|
||||
self.assertEqual("yes <i>html </i>", output)
|
||||
|
||||
def test_disallowed_html(self):
|
||||
"""remove disallowed html but keep allowed html"""
|
||||
input_text = "<div> yes <i>html</i></div>"
|
||||
parser = InputHtmlParser()
|
||||
parser.feed(input_text)
|
||||
output = parser.get_output()
|
||||
output = clean(input_text)
|
||||
self.assertEqual(" yes <i>html</i>", output)
|
||||
|
|
13
bookwyrm/tests/test_utils.py
Normal file
13
bookwyrm/tests/test_utils.py
Normal 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"))
|
|
@ -1,11 +1,14 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django_celery_beat.models import PeriodicTask, IntervalSchedule
|
||||
|
||||
from bookwyrm import forms, models, views
|
||||
from bookwyrm.management.commands import initdb
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
|
@ -25,14 +28,52 @@ class AutomodViews(TestCase):
|
|||
local=True,
|
||||
localname="mouse",
|
||||
)
|
||||
initdb.init_groups()
|
||||
initdb.init_permissions()
|
||||
group = Group.objects.get(name="moderator")
|
||||
self.local_user.groups.set([group])
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_automod_rules_get(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
schedule = IntervalSchedule.objects.create(every=1, period="days")
|
||||
PeriodicTask.objects.create(
|
||||
interval=schedule,
|
||||
name="automod-task",
|
||||
task="bookwyrm.models.antispam.automod_task",
|
||||
)
|
||||
models.AutoMod.objects.create(created_by=self.local_user, string_match="hello")
|
||||
view = views.AutoMod.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_automod_rules_get_empty_with_schedule(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
schedule = IntervalSchedule.objects.create(every=1, period="days")
|
||||
PeriodicTask.objects.create(
|
||||
interval=schedule,
|
||||
name="automod-task",
|
||||
task="bookwyrm.models.antispam.automod_task",
|
||||
)
|
||||
view = views.AutoMod.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_automod_rules_get_empty_without_schedule(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.AutoMod.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
|
@ -45,14 +86,33 @@ class AutomodViews(TestCase):
|
|||
form.data["string_match"] = "hello"
|
||||
form.data["flag_users"] = True
|
||||
form.data["flag_statuses"] = False
|
||||
form.data["created_by"] = self.local_user
|
||||
form.data["created_by"] = self.local_user.id
|
||||
|
||||
view = views.AutoMod.as_view()
|
||||
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)
|
||||
|
||||
rule = models.AutoMod.objects.get()
|
||||
self.assertTrue(rule.flag_users)
|
||||
self.assertFalse(rule.flag_statuses)
|
||||
|
||||
def test_schedule_automod_task(self):
|
||||
"""Schedule the task"""
|
||||
self.assertFalse(IntervalSchedule.objects.exists())
|
||||
|
||||
form = forms.IntervalScheduleForm()
|
||||
form.data["every"] = 1
|
||||
form.data["period"] = "days"
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
response = views.schedule_automod_task(request)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
self.assertTrue(IntervalSchedule.objects.exists())
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.management.commands import initdb
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
|
@ -24,6 +27,10 @@ class DashboardViews(TestCase):
|
|||
local=True,
|
||||
localname="mouse",
|
||||
)
|
||||
initdb.init_groups()
|
||||
initdb.init_permissions()
|
||||
group = Group.objects.get(name="moderator")
|
||||
self.local_user.groups.set([group])
|
||||
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
|
@ -32,7 +39,7 @@ class DashboardViews(TestCase):
|
|||
view = views.Dashboard.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())
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.management.commands import initdb
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
|
@ -25,6 +27,10 @@ class EmailBlocklistViews(TestCase):
|
|||
local=True,
|
||||
localname="mouse",
|
||||
)
|
||||
initdb.init_groups()
|
||||
initdb.init_permissions()
|
||||
group = Group.objects.get(name="moderator")
|
||||
self.local_user.groups.set([group])
|
||||
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
|
@ -33,7 +39,6 @@ class EmailBlocklistViews(TestCase):
|
|||
view = views.EmailBlocklist.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request)
|
||||
|
||||
|
@ -46,7 +51,6 @@ class EmailBlocklistViews(TestCase):
|
|||
view = views.EmailBlocklist.as_view()
|
||||
request = self.factory.post("", {"domain": "gmail.com"})
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request)
|
||||
|
||||
|
@ -65,7 +69,6 @@ class EmailBlocklistViews(TestCase):
|
|||
view = views.EmailBlocklist.as_view()
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request, domain_id=domain.id)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
|
|
@ -3,12 +3,14 @@ import os
|
|||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
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.management.commands import initdb
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
|
@ -38,6 +40,10 @@ class FederationViews(TestCase):
|
|||
inbox="https://example.com/users/rat/inbox",
|
||||
outbox="https://example.com/users/rat/outbox",
|
||||
)
|
||||
initdb.init_groups()
|
||||
initdb.init_permissions()
|
||||
group = Group.objects.get(name="moderator")
|
||||
self.local_user.groups.set([group])
|
||||
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
|
@ -46,7 +52,7 @@ class FederationViews(TestCase):
|
|||
view = views.Federation.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())
|
||||
|
@ -58,7 +64,6 @@ class FederationViews(TestCase):
|
|||
view = views.FederatedServer.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request, server.id)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
|
@ -81,7 +86,6 @@ class FederationViews(TestCase):
|
|||
view = views.block_server
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
with patch("bookwyrm.suggested_users.bulk_remove_instance_task.delay") as mock:
|
||||
view(request, server.id)
|
||||
|
@ -121,7 +125,6 @@ class FederationViews(TestCase):
|
|||
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
with patch("bookwyrm.suggested_users.bulk_add_instance_task.delay") as mock:
|
||||
views.unblock_server(request, server.id)
|
||||
|
@ -147,7 +150,6 @@ class FederationViews(TestCase):
|
|||
view = views.AddFederatedServer.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
|
@ -164,7 +166,6 @@ class FederationViews(TestCase):
|
|||
view = views.AddFederatedServer.as_view()
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
view(request)
|
||||
server = models.FederatedServer.objects.get()
|
||||
|
@ -196,7 +197,6 @@ class FederationViews(TestCase):
|
|||
},
|
||||
)
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
view(request)
|
||||
server.refresh_from_db()
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
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.management.commands import initdb
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
|
@ -24,6 +27,10 @@ class IPBlocklistViews(TestCase):
|
|||
local=True,
|
||||
localname="mouse",
|
||||
)
|
||||
initdb.init_groups()
|
||||
initdb.init_permissions()
|
||||
group = Group.objects.get(name="moderator")
|
||||
self.local_user.groups.set([group])
|
||||
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
|
@ -32,7 +39,6 @@ class IPBlocklistViews(TestCase):
|
|||
view = views.IPBlocklist.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request)
|
||||
|
||||
|
@ -48,7 +54,6 @@ class IPBlocklistViews(TestCase):
|
|||
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request)
|
||||
|
||||
|
@ -67,7 +72,6 @@ class IPBlocklistViews(TestCase):
|
|||
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
view(request, block.id)
|
||||
self.assertFalse(models.IPBlocklist.objects.exists())
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.management.commands import initdb
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
|
@ -25,6 +27,11 @@ class LinkDomainViews(TestCase):
|
|||
local=True,
|
||||
localname="mouse",
|
||||
)
|
||||
initdb.init_groups()
|
||||
initdb.init_permissions()
|
||||
group = Group.objects.get(name="moderator")
|
||||
self.local_user.groups.set([group])
|
||||
|
||||
self.book = models.Edition.objects.create(title="hello")
|
||||
models.FileLink.objects.create(
|
||||
book=self.book,
|
||||
|
@ -39,7 +46,6 @@ class LinkDomainViews(TestCase):
|
|||
view = views.LinkDomain.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request, "pending")
|
||||
|
||||
|
@ -55,7 +61,6 @@ class LinkDomainViews(TestCase):
|
|||
view = views.LinkDomain.as_view()
|
||||
request = self.factory.post("", {"name": "ugh"})
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request, "pending", domain.id)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
@ -71,7 +76,6 @@ class LinkDomainViews(TestCase):
|
|||
view = views.update_domain_status
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request, domain.id, "approved")
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
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 import models, views
|
||||
from bookwyrm.management.commands import initdb
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
|
@ -33,6 +35,10 @@ class ReportViews(TestCase):
|
|||
local=True,
|
||||
localname="rat",
|
||||
)
|
||||
initdb.init_groups()
|
||||
initdb.init_permissions()
|
||||
group = Group.objects.get(name="moderator")
|
||||
self.local_user.groups.set([group])
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_reports_page(self):
|
||||
|
@ -40,7 +46,6 @@ class ReportViews(TestCase):
|
|||
view = views.ReportsAdmin.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
|
@ -52,7 +57,6 @@ class ReportViews(TestCase):
|
|||
view = views.ReportsAdmin.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
models.Report.objects.create(reporter=self.local_user, user=self.rat)
|
||||
|
||||
result = view(request)
|
||||
|
@ -65,7 +69,6 @@ class ReportViews(TestCase):
|
|||
view = views.ReportAdmin.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
|
||||
|
||||
result = view(request, report.id)
|
||||
|
@ -79,7 +82,6 @@ class ReportViews(TestCase):
|
|||
view = views.ReportAdmin.as_view()
|
||||
request = self.factory.post("", {"note": "hi"})
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
|
||||
|
||||
view(request, report.id)
|
||||
|
@ -89,59 +91,12 @@ class ReportViews(TestCase):
|
|||
self.assertEqual(comment.note, "hi")
|
||||
self.assertEqual(comment.report, report)
|
||||
|
||||
def test_report_modal_view(self):
|
||||
"""a user reports another user"""
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
result = views.Report.as_view()(request, self.local_user.id)
|
||||
|
||||
validate_html(result.render())
|
||||
|
||||
def test_make_report(self):
|
||||
"""a user reports another user"""
|
||||
form = forms.ReportForm()
|
||||
form.data["reporter"] = self.local_user.id
|
||||
form.data["user"] = self.rat.id
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
views.Report.as_view()(request)
|
||||
|
||||
report = models.Report.objects.get()
|
||||
self.assertEqual(report.reporter, self.local_user)
|
||||
self.assertEqual(report.user, self.rat)
|
||||
|
||||
def test_report_link(self):
|
||||
"""a user reports a link as spam"""
|
||||
book = models.Edition.objects.create(title="hi")
|
||||
link = models.FileLink.objects.create(
|
||||
book=book, added_by=self.local_user, url="https://skdjfs.sdf"
|
||||
)
|
||||
domain = link.domain
|
||||
domain.status = "approved"
|
||||
domain.save()
|
||||
|
||||
form = forms.ReportForm()
|
||||
form.data["reporter"] = self.local_user.id
|
||||
form.data["user"] = self.rat.id
|
||||
form.data["links"] = link.id
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
views.Report.as_view()(request)
|
||||
|
||||
report = models.Report.objects.get()
|
||||
domain.refresh_from_db()
|
||||
self.assertEqual(report.links.first().id, link.id)
|
||||
self.assertEqual(domain.status, "pending")
|
||||
|
||||
def test_resolve_report(self):
|
||||
"""toggle report resolution status"""
|
||||
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
|
||||
self.assertFalse(report.resolved)
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
# resolve
|
||||
views.resolve_report(request, report.id)
|
||||
|
@ -161,7 +116,6 @@ class ReportViews(TestCase):
|
|||
self.assertTrue(self.rat.is_active)
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
# de-activate
|
||||
views.suspend_user(request, self.rat.id)
|
||||
|
@ -180,7 +134,6 @@ class ReportViews(TestCase):
|
|||
self.assertTrue(self.rat.is_active)
|
||||
request = self.factory.post("", {"password": "password"})
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
# de-activate
|
||||
with patch(
|
||||
|
|
85
bookwyrm/tests/views/admin/test_site.py
Normal file
85
bookwyrm/tests/views/admin/test_site.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
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.management.commands import initdb
|
||||
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",
|
||||
)
|
||||
initdb.init_groups()
|
||||
initdb.init_permissions()
|
||||
group = Group.objects.get(name="admin")
|
||||
self.local_user.groups.set([group])
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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")
|
|
@ -7,6 +7,7 @@ from django.test import TestCase
|
|||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.management.commands import initdb
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
|
@ -26,6 +27,10 @@ class UserAdminViews(TestCase):
|
|||
local=True,
|
||||
localname="mouse",
|
||||
)
|
||||
initdb.init_groups()
|
||||
initdb.init_permissions()
|
||||
group = Group.objects.get(name="moderator")
|
||||
self.local_user.groups.set([group])
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_user_admin_list_page(self):
|
||||
|
@ -33,7 +38,7 @@ class UserAdminViews(TestCase):
|
|||
view = views.UserAdminList.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())
|
||||
|
@ -44,7 +49,6 @@ class UserAdminViews(TestCase):
|
|||
view = views.UserAdmin.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request, self.local_user.id)
|
||||
|
||||
|
@ -57,15 +61,14 @@ class UserAdminViews(TestCase):
|
|||
@patch("bookwyrm.suggested_users.remove_user_task.delay")
|
||||
def test_user_admin_page_post(self, *_):
|
||||
"""set the user's group"""
|
||||
group = Group.objects.create(name="editor")
|
||||
group = Group.objects.get(name="editor")
|
||||
self.assertEqual(
|
||||
list(self.local_user.groups.values_list("name", flat=True)), []
|
||||
list(self.local_user.groups.values_list("name", flat=True)), ["moderator"]
|
||||
)
|
||||
|
||||
view = views.UserAdmin.as_view()
|
||||
request = self.factory.post("", {"groups": [group.id]})
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||
result = view(request, self.local_user.id)
|
||||
|
|
|
@ -48,7 +48,7 @@ class EditBookViews(TestCase):
|
|||
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_edit_book_page(self):
|
||||
def test_edit_book_get(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.EditBook.as_view()
|
||||
request = self.factory.get("")
|
||||
|
@ -59,18 +59,7 @@ class EditBookViews(TestCase):
|
|||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_edit_book_create_page(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.CreateBook.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_edit_book(self):
|
||||
def test_edit_book_post(self):
|
||||
"""lets a user edit a book"""
|
||||
view = views.EditBook.as_view()
|
||||
self.local_user.groups.add(self.group)
|
||||
|
@ -86,6 +75,23 @@ class EditBookViews(TestCase):
|
|||
self.book.refresh_from_db()
|
||||
self.assertEqual(self.book.title, "New Title")
|
||||
|
||||
def test_edit_book_post_invalid(self):
|
||||
"""book form is invalid"""
|
||||
view = views.EditBook.as_view()
|
||||
self.local_user.groups.add(self.group)
|
||||
form = forms.EditionForm(instance=self.book)
|
||||
form.data["title"] = ""
|
||||
form.data["last_edited_by"] = self.local_user.id
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
result = view(request, self.book.id)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
# Title is unchanged
|
||||
self.book.refresh_from_db()
|
||||
self.assertEqual(self.book.title, "Example Edition")
|
||||
|
||||
def test_edit_book_add_author(self):
|
||||
"""lets a user edit a book with new authors"""
|
||||
view = views.EditBook.as_view()
|
||||
|
@ -234,3 +240,49 @@ class EditBookViews(TestCase):
|
|||
self.assertEqual(len(result["author_matches"]), 2)
|
||||
self.assertEqual(result["author_matches"][0]["name"], "Sappho")
|
||||
self.assertEqual(result["author_matches"][1]["name"], "Some Guy")
|
||||
|
||||
def test_create_book_get(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.CreateBook.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_create_book_post_existing_work(self):
|
||||
"""Adding an edition to an existing work"""
|
||||
author = models.Author.objects.create(name="Sappho")
|
||||
view = views.CreateBook.as_view()
|
||||
form = forms.EditionForm()
|
||||
form.data["title"] = "A Title"
|
||||
form.data["parent_work"] = self.work.id
|
||||
form.data["authors"] = [author.id]
|
||||
form.data["last_edited_by"] = self.local_user.id
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||
result = view(request)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
||||
new_edition = models.Edition.objects.get(title="A Title")
|
||||
self.assertEqual(new_edition.parent_work, self.work)
|
||||
self.assertEqual(new_edition.authors.first(), author)
|
||||
|
||||
def test_create_book_post_invalid(self):
|
||||
"""book form is invalid"""
|
||||
view = views.CreateBook.as_view()
|
||||
self.local_user.groups.add(self.group)
|
||||
form = forms.EditionForm(instance=self.book)
|
||||
form.data["title"] = ""
|
||||
form.data["last_edited_by"] = self.local_user.id
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
result = view(request)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -112,7 +112,17 @@ class ViewsHelpers(TestCase):
|
|||
request = self.factory.get("", {"q": "Test Book"}, HTTP_USER_AGENT=USER_AGENT)
|
||||
self.assertTrue(views.helpers.is_bookwyrm_request(request))
|
||||
|
||||
def test_existing_user(self, *_):
|
||||
def test_handle_remote_webfinger_invalid(self, *_):
|
||||
"""Various ways you can send a bad query"""
|
||||
# if there's no query, there's no result
|
||||
result = views.helpers.handle_remote_webfinger(None)
|
||||
self.assertIsNone(result)
|
||||
|
||||
# malformed user
|
||||
result = views.helpers.handle_remote_webfinger("noatsymbol")
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_handle_remote_webfinger_existing_user(self, *_):
|
||||
"""simple database lookup by username"""
|
||||
result = views.helpers.handle_remote_webfinger("@mouse@local.com")
|
||||
self.assertEqual(result, self.local_user)
|
||||
|
@ -124,7 +134,19 @@ class ViewsHelpers(TestCase):
|
|||
self.assertEqual(result, self.local_user)
|
||||
|
||||
@responses.activate
|
||||
def test_load_user(self, *_):
|
||||
def test_handle_remote_webfinger_load_user_invalid_result(self, *_):
|
||||
"""find a remote user using webfinger, but fail"""
|
||||
username = "mouse@example.com"
|
||||
responses.add(
|
||||
responses.GET,
|
||||
f"https://example.com/.well-known/webfinger?resource=acct:{username}",
|
||||
status=500,
|
||||
)
|
||||
result = views.helpers.handle_remote_webfinger("@mouse@example.com")
|
||||
self.assertIsNone(result)
|
||||
|
||||
@responses.activate
|
||||
def test_handle_remote_webfinger_load_user(self, *_):
|
||||
"""find a remote user using webfinger"""
|
||||
username = "mouse@example.com"
|
||||
wellknown = {
|
||||
|
@ -154,7 +176,7 @@ class ViewsHelpers(TestCase):
|
|||
self.assertIsInstance(result, models.User)
|
||||
self.assertEqual(result.username, "mouse@example.com")
|
||||
|
||||
def test_user_on_blocked_server(self, *_):
|
||||
def test_handler_remote_webfinger_user_on_blocked_server(self, *_):
|
||||
"""find a remote user using webfinger"""
|
||||
models.FederatedServer.objects.create(
|
||||
server_name="example.com", status="blocked"
|
||||
|
@ -163,6 +185,38 @@ class ViewsHelpers(TestCase):
|
|||
result = views.helpers.handle_remote_webfinger("@mouse@example.com")
|
||||
self.assertIsNone(result)
|
||||
|
||||
@responses.activate
|
||||
def test_subscribe_remote_webfinger(self, *_):
|
||||
"""remote subscribe templates"""
|
||||
query = "mouse@example.com"
|
||||
response = {
|
||||
"subject": f"acct:{query}",
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": "https://example.com/user/mouse",
|
||||
"template": "hi",
|
||||
},
|
||||
{
|
||||
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||
"type": "application/activity+json",
|
||||
"href": "https://example.com/user/mouse",
|
||||
"template": "hello",
|
||||
},
|
||||
],
|
||||
}
|
||||
responses.add(
|
||||
responses.GET,
|
||||
f"https://example.com/.well-known/webfinger?resource=acct:{query}",
|
||||
json=response,
|
||||
status=200,
|
||||
)
|
||||
template = views.helpers.subscribe_remote_webfinger(query)
|
||||
self.assertEqual(template, "hello")
|
||||
template = views.helpers.subscribe_remote_webfinger(f"@{query}")
|
||||
self.assertEqual(template, "hello")
|
||||
|
||||
def test_handle_reading_status_to_read(self, *_):
|
||||
"""posts shelve activities"""
|
||||
shelf = self.local_user.shelf_set.get(identifier="to-read")
|
||||
|
|
|
@ -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, *_):
|
||||
|
|
|
@ -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(
|
||||
|
|
109
bookwyrm/tests/views/test_report.py
Normal file
109
bookwyrm/tests/views/test_report.py
Normal file
|
@ -0,0 +1,109 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
|
||||
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 ReportViews(TestCase):
|
||||
"""every response to a get request, html or json"""
|
||||
|
||||
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.rat = models.User.objects.create_user(
|
||||
"rat@local.com",
|
||||
"rat@mouse.mouse",
|
||||
"password",
|
||||
local=True,
|
||||
localname="rat",
|
||||
)
|
||||
with patch(
|
||||
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
|
||||
), patch("bookwyrm.activitystreams.add_status_task.delay"):
|
||||
self.status = models.Status.objects.create(
|
||||
user=self.local_user,
|
||||
content="Test status",
|
||||
)
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_report_modal_view(self):
|
||||
"""a user reports another user"""
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
result = views.Report.as_view()(request, self.local_user.id)
|
||||
|
||||
validate_html(result.render())
|
||||
|
||||
def test_report_modal_view_with_status(self):
|
||||
"""a user reports another user"""
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
result = views.Report.as_view()(
|
||||
request, user_id=self.local_user.id, status_id=self.status.id
|
||||
)
|
||||
|
||||
validate_html(result.render())
|
||||
|
||||
def test_report_modal_view_with_link_domain(self):
|
||||
"""a user reports another user"""
|
||||
link = models.Link.objects.create(
|
||||
url="http://example.com/hi",
|
||||
added_by=self.local_user,
|
||||
)
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
result = views.Report.as_view()(request, link_id=link.id)
|
||||
|
||||
validate_html(result.render())
|
||||
|
||||
def test_make_report(self):
|
||||
"""a user reports another user"""
|
||||
form = forms.ReportForm()
|
||||
form.data["reporter"] = self.local_user.id
|
||||
form.data["user"] = self.rat.id
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
views.Report.as_view()(request)
|
||||
|
||||
report = models.Report.objects.get()
|
||||
self.assertEqual(report.reporter, self.local_user)
|
||||
self.assertEqual(report.user, self.rat)
|
||||
|
||||
def test_report_link(self):
|
||||
"""a user reports a link as spam"""
|
||||
book = models.Edition.objects.create(title="hi")
|
||||
link = models.FileLink.objects.create(
|
||||
book=book, added_by=self.local_user, url="https://skdjfs.sdf"
|
||||
)
|
||||
domain = link.domain
|
||||
domain.status = "approved"
|
||||
domain.save()
|
||||
|
||||
form = forms.ReportForm()
|
||||
form.data["reporter"] = self.local_user.id
|
||||
form.data["user"] = self.rat.id
|
||||
form.data["links"] = link.id
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
views.Report.as_view()(request)
|
||||
|
||||
report = models.Report.objects.get()
|
||||
domain.refresh_from_db()
|
||||
self.assertEqual(report.links.first().id, link.id)
|
||||
self.assertEqual(domain.status, "pending")
|
|
@ -126,7 +126,7 @@ urlpatterns = [
|
|||
r"^settings/users/?$", views.UserAdminList.as_view(), name="settings-users"
|
||||
),
|
||||
re_path(
|
||||
r"^settings/users/(?P<status>(local|federated))\/?$",
|
||||
r"^settings/users/(?P<status>(local|federated|deleted))\/?$",
|
||||
views.UserAdminList.as_view(),
|
||||
name="settings-users",
|
||||
),
|
||||
|
@ -287,7 +287,7 @@ urlpatterns = [
|
|||
name="report-status",
|
||||
),
|
||||
re_path(
|
||||
r"^report/(?P<user_id>\d+)/link/(?P<link_id>\d+)?$",
|
||||
r"^report/link/(?P<link_id>\d+)?$",
|
||||
views.Report.as_view(),
|
||||
name="report-link",
|
||||
),
|
||||
|
|
26
bookwyrm/utils/sanitizer.py
Normal file
26
bookwyrm/utils/sanitizer.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
"""Clean user-provided text"""
|
||||
import bleach
|
||||
|
||||
|
||||
def clean(input_text):
|
||||
"""Run through "bleach" """
|
||||
return bleach.clean(
|
||||
input_text,
|
||||
tags=[
|
||||
"p",
|
||||
"blockquote",
|
||||
"br",
|
||||
"b",
|
||||
"i",
|
||||
"strong",
|
||||
"em",
|
||||
"pre",
|
||||
"a",
|
||||
"span",
|
||||
"ul",
|
||||
"ol",
|
||||
"li",
|
||||
],
|
||||
attributes=["href", "rel", "src", "alt"],
|
||||
strip=True,
|
||||
)
|
|
@ -33,8 +33,7 @@ class AutoMod(View):
|
|||
def post(self, request):
|
||||
"""add rule"""
|
||||
form = forms.AutoModRuleForm(request.POST)
|
||||
success = form.is_valid()
|
||||
if success:
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
form = forms.AutoModRuleForm()
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
""" instance overview """
|
||||
from datetime import timedelta
|
||||
import re
|
||||
|
||||
from dateutil.parser import parse
|
||||
from packaging import version
|
||||
|
||||
|
@ -13,6 +15,7 @@ from django.views import View
|
|||
from bookwyrm import models, settings
|
||||
from bookwyrm.connectors.abstract_connector import get_data
|
||||
from bookwyrm.connectors.connector_manager import ConnectorException
|
||||
from bookwyrm.utils import regex
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
|
@ -26,91 +29,32 @@ class Dashboard(View):
|
|||
|
||||
def get(self, request):
|
||||
"""list of users"""
|
||||
interval = int(request.GET.get("days", 1))
|
||||
now = timezone.now()
|
||||
start = request.GET.get("start")
|
||||
if start:
|
||||
start = timezone.make_aware(parse(start))
|
||||
else:
|
||||
start = now - timedelta(days=6 * interval)
|
||||
data = get_charts_and_stats(request)
|
||||
|
||||
end = request.GET.get("end")
|
||||
end = timezone.make_aware(parse(end)) if end else now
|
||||
start = start.replace(hour=0, minute=0, second=0)
|
||||
# Make sure email looks properly configured
|
||||
email_config_error = re.findall(
|
||||
r"[\s\@]", settings.EMAIL_SENDER_DOMAIN
|
||||
) or not re.match(regex.DOMAIN, settings.EMAIL_SENDER_DOMAIN)
|
||||
|
||||
user_queryset = models.User.objects.filter(local=True)
|
||||
user_chart = Chart(
|
||||
queryset=user_queryset,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
Q(is_active=True) | Q(deactivation_date__gt=e),
|
||||
created_date__lte=e,
|
||||
).count(),
|
||||
"active": lambda q, s, e: q.filter(
|
||||
Q(is_active=True) | Q(deactivation_date__gt=e),
|
||||
created_date__lte=e,
|
||||
)
|
||||
.filter(
|
||||
last_active_date__gt=e - timedelta(days=31),
|
||||
)
|
||||
.count(),
|
||||
},
|
||||
data["email_config_error"] = email_config_error
|
||||
# pylint: disable=line-too-long
|
||||
data[
|
||||
"email_sender"
|
||||
] = f"{settings.EMAIL_SENDER_NAME}@{settings.EMAIL_SENDER_DOMAIN}"
|
||||
|
||||
site = models.SiteSettings.objects.get()
|
||||
# pylint: disable=protected-access
|
||||
data["missing_conduct"] = (
|
||||
not site.code_of_conduct
|
||||
or site.code_of_conduct
|
||||
== site._meta.get_field("code_of_conduct").get_default()
|
||||
)
|
||||
|
||||
status_queryset = models.Status.objects.filter(user__local=True, deleted=False)
|
||||
status_chart = Chart(
|
||||
queryset=status_queryset,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
created_date__gt=s,
|
||||
created_date__lte=e,
|
||||
).count()
|
||||
},
|
||||
data["missing_privacy"] = (
|
||||
not site.privacy_policy
|
||||
or site.privacy_policy
|
||||
== site._meta.get_field("privacy_policy").get_default()
|
||||
)
|
||||
|
||||
register_chart = Chart(
|
||||
queryset=user_queryset,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
created_date__gt=s,
|
||||
created_date__lte=e,
|
||||
).count()
|
||||
},
|
||||
)
|
||||
|
||||
works_chart = Chart(
|
||||
queryset=models.Work.objects,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
created_date__gt=s,
|
||||
created_date__lte=e,
|
||||
).count()
|
||||
},
|
||||
)
|
||||
|
||||
data = {
|
||||
"start": start.strftime("%Y-%m-%d"),
|
||||
"end": end.strftime("%Y-%m-%d"),
|
||||
"interval": interval,
|
||||
"users": user_queryset.filter(is_active=True).count(),
|
||||
"active_users": user_queryset.filter(
|
||||
is_active=True, last_active_date__gte=now - timedelta(days=31)
|
||||
).count(),
|
||||
"statuses": status_queryset.count(),
|
||||
"works": models.Work.objects.count(),
|
||||
"reports": models.Report.objects.filter(resolved=False).count(),
|
||||
"pending_domains": models.LinkDomain.objects.filter(
|
||||
status="pending"
|
||||
).count(),
|
||||
"invite_requests": models.InviteRequest.objects.filter(
|
||||
ignored=False, invite__isnull=True
|
||||
).count(),
|
||||
"user_stats": user_chart.get_chart(start, end, interval),
|
||||
"status_stats": status_chart.get_chart(start, end, interval),
|
||||
"register_stats": register_chart.get_chart(start, end, interval),
|
||||
"works_stats": works_chart.get_chart(start, end, interval),
|
||||
}
|
||||
|
||||
# check version
|
||||
try:
|
||||
release = get_data(settings.RELEASE_API, timeout=3)
|
||||
|
@ -126,6 +70,91 @@ class Dashboard(View):
|
|||
return TemplateResponse(request, "settings/dashboard/dashboard.html", data)
|
||||
|
||||
|
||||
def get_charts_and_stats(request):
|
||||
"""Defines the dashbaord charts"""
|
||||
interval = int(request.GET.get("days", 1))
|
||||
now = timezone.now()
|
||||
start = request.GET.get("start")
|
||||
if start:
|
||||
start = timezone.make_aware(parse(start))
|
||||
else:
|
||||
start = now - timedelta(days=6 * interval)
|
||||
|
||||
end = request.GET.get("end")
|
||||
end = timezone.make_aware(parse(end)) if end else now
|
||||
start = start.replace(hour=0, minute=0, second=0)
|
||||
|
||||
user_queryset = models.User.objects.filter(local=True)
|
||||
user_chart = Chart(
|
||||
queryset=user_queryset,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
Q(is_active=True) | Q(deactivation_date__gt=e),
|
||||
created_date__lte=e,
|
||||
).count(),
|
||||
"active": lambda q, s, e: q.filter(
|
||||
Q(is_active=True) | Q(deactivation_date__gt=e),
|
||||
created_date__lte=e,
|
||||
)
|
||||
.filter(
|
||||
last_active_date__gt=e - timedelta(days=31),
|
||||
)
|
||||
.count(),
|
||||
},
|
||||
)
|
||||
|
||||
status_queryset = models.Status.objects.filter(user__local=True, deleted=False)
|
||||
status_chart = Chart(
|
||||
queryset=status_queryset,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
created_date__gt=s,
|
||||
created_date__lte=e,
|
||||
).count()
|
||||
},
|
||||
)
|
||||
|
||||
register_chart = Chart(
|
||||
queryset=user_queryset,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
created_date__gt=s,
|
||||
created_date__lte=e,
|
||||
).count()
|
||||
},
|
||||
)
|
||||
|
||||
works_chart = Chart(
|
||||
queryset=models.Work.objects,
|
||||
queries={
|
||||
"total": lambda q, s, e: q.filter(
|
||||
created_date__gt=s,
|
||||
created_date__lte=e,
|
||||
).count()
|
||||
},
|
||||
)
|
||||
return {
|
||||
"start": start.strftime("%Y-%m-%d"),
|
||||
"end": end.strftime("%Y-%m-%d"),
|
||||
"interval": interval,
|
||||
"users": user_queryset.filter(is_active=True).count(),
|
||||
"active_users": user_queryset.filter(
|
||||
is_active=True, last_active_date__gte=now - timedelta(days=31)
|
||||
).count(),
|
||||
"statuses": status_queryset.count(),
|
||||
"works": models.Work.objects.count(),
|
||||
"reports": models.Report.objects.filter(resolved=False).count(),
|
||||
"pending_domains": models.LinkDomain.objects.filter(status="pending").count(),
|
||||
"invite_requests": models.InviteRequest.objects.filter(
|
||||
ignored=False, invite__isnull=True
|
||||
).count(),
|
||||
"user_stats": user_chart.get_chart(start, end, interval),
|
||||
"status_stats": status_chart.get_chart(start, end, interval),
|
||||
"register_stats": register_chart.get_chart(start, end, interval),
|
||||
"works_stats": works_chart.get_chart(start, end, interval),
|
||||
}
|
||||
|
||||
|
||||
class Chart:
|
||||
"""Data for a chart"""
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ class LinkDomain(View):
|
|||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permission_required("bookwyrm.moderate_user")
|
||||
def update_domain_status(request, domain_id, status):
|
||||
"""This domain seems fine"""
|
||||
domain = get_object_or_404(models.LinkDomain, id=domain_id)
|
||||
|
|
|
@ -83,7 +83,7 @@ class ReportAdmin(View):
|
|||
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm_moderate_user")
|
||||
@permission_required("bookwyrm.moderate_user")
|
||||
def suspend_user(_, user_id):
|
||||
"""mark an account as inactive"""
|
||||
user = get_object_or_404(models.User, id=user_id)
|
||||
|
@ -95,7 +95,7 @@ def suspend_user(_, user_id):
|
|||
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm_moderate_user")
|
||||
@permission_required("bookwyrm.moderate_user")
|
||||
def unsuspend_user(_, user_id):
|
||||
"""mark an account as inactive"""
|
||||
user = get_object_or_404(models.User, id=user_id)
|
||||
|
@ -107,7 +107,7 @@ def unsuspend_user(_, user_id):
|
|||
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm_moderate_user")
|
||||
@permission_required("bookwyrm.moderate_user")
|
||||
def moderator_delete_user(request, user_id):
|
||||
"""permanently delete a user"""
|
||||
user = get_object_or_404(models.User, id=user_id)
|
||||
|
@ -132,7 +132,7 @@ def moderator_delete_user(request, user_id):
|
|||
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm_moderate_post")
|
||||
@permission_required("bookwyrm.moderate_post")
|
||||
def resolve_report(_, report_id):
|
||||
"""mark a report as (un)resolved"""
|
||||
report = get_object_or_404(models.Report, id=report_id)
|
||||
|
|
|
@ -22,21 +22,25 @@ class UserAdminList(View):
|
|||
def get(self, request, status="local"):
|
||||
"""list of users"""
|
||||
filters = {}
|
||||
exclusions = {}
|
||||
if server := request.GET.get("server"):
|
||||
server = models.FederatedServer.objects.filter(server_name=server).first()
|
||||
filters["federated_server"] = server
|
||||
filters["federated_server__isnull"] = False
|
||||
|
||||
if username := request.GET.get("username"):
|
||||
filters["username__icontains"] = username
|
||||
scope = request.GET.get("scope")
|
||||
if scope and scope == "local":
|
||||
filters["local"] = True
|
||||
|
||||
if email := request.GET.get("email"):
|
||||
filters["email__endswith"] = email
|
||||
|
||||
filters["local"] = status == "local"
|
||||
filters["local"] = status in ["local", "deleted"]
|
||||
if status == "deleted":
|
||||
filters["deactivation_reason__icontains"] = "deletion"
|
||||
else:
|
||||
exclusions["deactivation_reason__icontains"] = "deletion"
|
||||
|
||||
users = models.User.objects.filter(**filters)
|
||||
users = models.User.objects.filter(**filters).exclude(**exclusions)
|
||||
|
||||
sort = request.GET.get("sort", "-created_date")
|
||||
sort_fields = [
|
||||
|
@ -62,7 +66,7 @@ class UserAdminList(View):
|
|||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.moderate_users", raise_exception=True),
|
||||
permission_required("bookwyrm.moderate_user", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class UserAdmin(View):
|
||||
|
@ -77,8 +81,13 @@ class UserAdmin(View):
|
|||
def post(self, request, user):
|
||||
"""update user group"""
|
||||
user = get_object_or_404(models.User, id=user)
|
||||
form = forms.UserGroupForm(request.POST, instance=user)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
|
||||
if request.POST.get("groups") == "":
|
||||
user.groups.set([])
|
||||
form = forms.UserGroupForm(instance=user)
|
||||
else:
|
||||
form = forms.UserGroupForm(request.POST, instance=user)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
data = {"user": user, "group_form": form}
|
||||
return TemplateResponse(request, "settings/users/user.html", data)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -148,13 +148,6 @@ def handle_reading_status(user, shelf, book, privacy):
|
|||
status.save()
|
||||
|
||||
|
||||
def is_blocked(viewer, user):
|
||||
"""is this viewer blocked by the user?"""
|
||||
if viewer.is_authenticated and viewer in user.blocks.all():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime:
|
||||
"""ensures that data is stored consistently in the UTC timezone"""
|
||||
if not date_str:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -52,9 +52,6 @@ class ReadingStatus(View):
|
|||
logger.exception("Invalid reading status type: %s", status)
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# invalidate related caches
|
||||
cache.delete(f"active_shelf-{request.user.id}-{book_id}")
|
||||
|
||||
desired_shelf = get_object_or_404(
|
||||
models.Shelf, identifier=identifier, user=request.user
|
||||
)
|
||||
|
@ -65,6 +62,14 @@ class ReadingStatus(View):
|
|||
.get(id=book_id)
|
||||
)
|
||||
|
||||
# invalidate related caches
|
||||
cache.delete_many(
|
||||
[
|
||||
f"active_shelf-{request.user.id}-{ed}"
|
||||
for ed in book.parent_work.editions.values_list("id", flat=True)
|
||||
]
|
||||
)
|
||||
|
||||
# gets the first shelf that indicates a reading status, or None
|
||||
shelves = [
|
||||
s
|
||||
|
|
|
@ -13,9 +13,13 @@ from bookwyrm import emailing, forms, models
|
|||
class Report(View):
|
||||
"""Make reports"""
|
||||
|
||||
def get(self, request, user_id, status_id=None, link_id=None):
|
||||
def get(self, request, user_id=None, status_id=None, link_id=None):
|
||||
"""static view of report modal"""
|
||||
data = {"user": get_object_or_404(models.User, id=user_id)}
|
||||
data = {"user": None}
|
||||
if user_id:
|
||||
# but normally we should have an error if the user is not found
|
||||
data["user"] = get_object_or_404(models.User, id=user_id)
|
||||
|
||||
if status_id:
|
||||
data["status"] = status_id
|
||||
if link_id:
|
||||
|
|
|
@ -16,9 +16,8 @@ from django.views.decorators.http import require_POST
|
|||
|
||||
from markdown import markdown
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.sanitize_html import InputHtmlParser
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.utils import regex
|
||||
from bookwyrm.utils import regex, sanitizer
|
||||
from .helpers import handle_remote_webfinger, is_api_request
|
||||
from .helpers import load_date_in_user_tz_as_utc
|
||||
|
||||
|
@ -268,6 +267,4 @@ def to_markdown(content):
|
|||
content = format_links(content)
|
||||
content = markdown(content)
|
||||
# sanitize resulting html
|
||||
sanitizer = InputHtmlParser()
|
||||
sanitizer.feed(content)
|
||||
return sanitizer.get_output()
|
||||
return sanitizer.clean(content)
|
||||
|
|
|
@ -60,6 +60,12 @@ class User(View):
|
|||
request.user,
|
||||
)
|
||||
.filter(user=user)
|
||||
.exclude(
|
||||
privacy="direct",
|
||||
review__isnull=True,
|
||||
comment__isnull=True,
|
||||
quotation__isnull=True,
|
||||
)
|
||||
.select_related(
|
||||
"user",
|
||||
"reply_parent",
|
||||
|
|
3
bw-dev
3
bw-dev
|
@ -103,6 +103,9 @@ case "$CMD" in
|
|||
pytest)
|
||||
runweb pytest --no-cov-on-fail "$@"
|
||||
;;
|
||||
pytest_coverage_report)
|
||||
runweb pytest -n 3 --cov-report term-missing "$@"
|
||||
;;
|
||||
collectstatic)
|
||||
runweb python manage.py collectstatic --no-input
|
||||
;;
|
||||
|
|
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue