Merge branch 'main' into tour

Also fixes conflict
This commit is contained in:
Hugh Rundle 2022-07-09 20:54:48 +10:00
commit ab5e4128e6
129 changed files with 6239 additions and 2325 deletions

View file

@ -55,5 +55,6 @@ jobs:
EMAIL_HOST_PASSWORD: ""
EMAIL_USE_TLS: true
ENABLE_PREVIEW_IMAGES: false
ENABLE_THUMBNAIL_GENERATION: true
run: |
pytest -n 3

View file

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

View file

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

View file

@ -10,3 +10,4 @@ from .landing import *
from .links import *
from .lists import *
from .status import *
from .user_admin import *

View file

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

View 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"]

View 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,
),
),
]

View file

@ -0,0 +1,90 @@
# Generated by Django 3.2.13 on 2022-07-05 00:49
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0150_readthrough_stopped_date"),
]
operations = [
migrations.RemoveField(
model_name="notification",
name="related_book",
),
migrations.AddField(
model_name="notification",
name="related_list_items",
field=models.ManyToManyField(
related_name="notifications", to="bookwyrm.ListItem"
),
),
migrations.AddField(
model_name="notification",
name="related_reports",
field=models.ManyToManyField(to="bookwyrm.Report"),
),
migrations.AddField(
model_name="notification",
name="related_users",
field=models.ManyToManyField(
related_name="notifications", to=settings.AUTH_USER_MODEL
),
),
migrations.AlterField(
model_name="notification",
name="related_list_item",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications_tmp",
to="bookwyrm.listitem",
),
),
migrations.AlterField(
model_name="notification",
name="related_report",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications_tmp",
to="bookwyrm.report",
),
),
migrations.RunSQL(
sql="""
INSERT INTO bookwyrm_notification_related_users (notification_id, user_id)
SELECT id, related_user_id
FROM bookwyrm_notification
WHERE bookwyrm_notification.related_user_id IS NOT NULL;
INSERT INTO bookwyrm_notification_related_list_items (notification_id, listitem_id)
SELECT id, related_list_item_id
FROM bookwyrm_notification
WHERE bookwyrm_notification.related_list_item_id IS NOT NULL;
INSERT INTO bookwyrm_notification_related_reports (notification_id, report_id)
SELECT id, related_report_id
FROM bookwyrm_notification
WHERE bookwyrm_notification.related_report_id IS NOT NULL;
""",
reverse_sql=migrations.RunSQL.noop,
),
migrations.RemoveField(
model_name="notification",
name="related_list_item",
),
migrations.RemoveField(
model_name="notification",
name="related_report",
),
migrations.RemoveField(
model_name="notification",
name="related_user",
),
]

View file

@ -0,0 +1,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,
),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 3.2.13 on 2022-07-05 03:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0151_auto_20220705_0049"),
]
operations = [
migrations.RemoveConstraint(
model_name="notification",
name="notification_type_valid",
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.13 on 2022-07-06 21:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0152_alter_report_user"),
("bookwyrm", "0152_remove_notification_notification_type_valid"),
]
operations = []

View file

@ -3,7 +3,7 @@ from functools import reduce
import operator
from django.apps import apps
from django.db import models
from django.db import models, transaction
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
@ -58,25 +58,20 @@ def automod_task():
return
reporter = AutoMod.objects.first().created_by
reports = automod_users(reporter) + automod_statuses(reporter)
if reports:
admins = User.objects.filter(
models.Q(user_permissions__name__in=["moderate_user", "moderate_post"])
| models.Q(is_superuser=True)
).all()
notification_model = apps.get_model(
"bookwyrm", "Notification", require_ready=True
)
if not reports:
return
admins = User.objects.filter(
models.Q(user_permissions__name__in=["moderate_user", "moderate_post"])
| models.Q(is_superuser=True)
).all()
notification_model = apps.get_model("bookwyrm", "Notification", require_ready=True)
with transaction.atomic():
for admin in admins:
notification_model.objects.bulk_create(
[
notification_model(
user=admin,
related_report=r,
notification_type="REPORT",
)
for r in reports
]
notification, _ = notification_model.objects.get_or_create(
user=admin, notification_type=notification_model.REPORT, read=False
)
notification.related_repors.add(reports)
def automod_users(reporter):

View file

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

View file

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

View file

@ -140,16 +140,6 @@ class GroupMemberInvitation(models.Model):
# make an invitation
super().save(*args, **kwargs)
# now send the invite
model = apps.get_model("bookwyrm.Notification", require_ready=True)
notification_type = "INVITE"
model.objects.create(
user=self.user,
related_user=self.group.user,
related_group=self.group,
notification_type=notification_type,
)
@transaction.atomic
def accept(self):
"""turn this request into the real deal"""
@ -157,25 +147,24 @@ class GroupMemberInvitation(models.Model):
model = apps.get_model("bookwyrm.Notification", require_ready=True)
# tell the group owner
model.objects.create(
user=self.group.user,
related_user=self.user,
model.notify(
self.group.user,
self.user,
related_group=self.group,
notification_type="ACCEPT",
notification_type=model.ACCEPT,
)
# let the other members know about it
for membership in self.group.memberships.all():
member = membership.user
if member not in (self.user, self.group.user):
model.objects.create(
user=member,
related_user=self.user,
model.notify(
member,
self.user,
related_group=self.group,
notification_type="JOIN",
notification_type=model.JOIN,
)
def reject(self):
"""generate a Reject for this membership request"""
self.delete()

View file

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

View file

@ -1,7 +1,6 @@
""" make a list of books!! """
import uuid
from django.apps import apps
from django.core.exceptions import PermissionDenied
from django.db import models
from django.db.models import Q
@ -151,34 +150,12 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
collection_field = "book_list"
def save(self, *args, **kwargs):
"""create a notification too"""
created = not bool(self.id)
"""Update the list's date"""
super().save(*args, **kwargs)
# tick the updated date on the parent list
self.book_list.updated_date = timezone.now()
self.book_list.save(broadcast=False, update_fields=["updated_date"])
list_owner = self.book_list.user
model = apps.get_model("bookwyrm.Notification", require_ready=True)
# create a notification if somoene ELSE added to a local user's list
if created and list_owner.local and list_owner != self.user:
model.objects.create(
user=list_owner,
related_user=self.user,
related_list_item=self,
notification_type="ADD",
)
if self.book_list.group:
for membership in self.book_list.group.memberships.all():
if membership.user != self.user:
model.objects.create(
user=membership.user,
related_user=self.user,
related_list_item=self,
notification_type="ADD",
)
def raise_not_deletable(self, viewer):
"""the associated user OR the list owner can delete"""
if self.book_list.user == viewer:

View file

@ -1,77 +1,123 @@
""" alert a user to activity """
from django.db import models
from django.db import models, transaction
from django.dispatch import receiver
from .base_model import BookWyrmModel
from . import Boost, Favorite, ImportJob, Report, Status, User
# pylint: disable=line-too-long
NotificationType = models.TextChoices(
"NotificationType",
"FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD REPORT INVITE ACCEPT JOIN LEAVE REMOVE GROUP_PRIVACY GROUP_NAME GROUP_DESCRIPTION",
)
from . import Boost, Favorite, GroupMemberInvitation, ImportJob, ListItem, Report
from . import Status, User, UserFollowRequest
class Notification(BookWyrmModel):
"""you've been tagged, liked, followed, etc"""
# Status interactions
FAVORITE = "FAVORITE"
BOOST = "BOOST"
REPLY = "REPLY"
MENTION = "MENTION"
TAG = "TAG"
# Relationships
FOLLOW = "FOLLOW"
FOLLOW_REQUEST = "FOLLOW_REQUEST"
# Imports
IMPORT = "IMPORT"
# List activity
ADD = "ADD"
# Admin
REPORT = "REPORT"
# Groups
INVITE = "INVITE"
ACCEPT = "ACCEPT"
JOIN = "JOIN"
LEAVE = "LEAVE"
REMOVE = "REMOVE"
GROUP_PRIVACY = "GROUP_PRIVACY"
GROUP_NAME = "GROUP_NAME"
GROUP_DESCRIPTION = "GROUP_DESCRIPTION"
# pylint: disable=line-too-long
NotificationType = models.TextChoices(
# there has got be a better way to do this
"NotificationType",
f"{FAVORITE} {REPLY} {MENTION} {TAG} {FOLLOW} {FOLLOW_REQUEST} {BOOST} {IMPORT} {ADD} {REPORT} {INVITE} {ACCEPT} {JOIN} {LEAVE} {REMOVE} {GROUP_PRIVACY} {GROUP_NAME} {GROUP_DESCRIPTION}",
)
user = models.ForeignKey("User", on_delete=models.CASCADE)
related_book = models.ForeignKey("Edition", on_delete=models.CASCADE, null=True)
related_user = models.ForeignKey(
"User", on_delete=models.CASCADE, null=True, related_name="related_user"
read = models.BooleanField(default=False)
notification_type = models.CharField(
max_length=255, choices=NotificationType.choices
)
related_users = models.ManyToManyField(
"User", symmetrical=False, related_name="notifications"
)
related_group = models.ForeignKey(
"Group", on_delete=models.CASCADE, null=True, related_name="notifications"
)
related_status = models.ForeignKey("Status", on_delete=models.CASCADE, null=True)
related_import = models.ForeignKey("ImportJob", on_delete=models.CASCADE, null=True)
related_list_item = models.ForeignKey(
"ListItem", on_delete=models.CASCADE, null=True
)
related_report = models.ForeignKey("Report", on_delete=models.CASCADE, null=True)
read = models.BooleanField(default=False)
notification_type = models.CharField(
max_length=255, choices=NotificationType.choices
related_list_items = models.ManyToManyField(
"ListItem", symmetrical=False, related_name="notifications"
)
related_reports = models.ManyToManyField("Report", symmetrical=False)
def save(self, *args, **kwargs):
"""save, but don't make dupes"""
# there's probably a better way to do this
if self.__class__.objects.filter(
user=self.user,
related_book=self.related_book,
related_user=self.related_user,
related_group=self.related_group,
related_status=self.related_status,
related_import=self.related_import,
related_list_item=self.related_list_item,
related_report=self.related_report,
notification_type=self.notification_type,
).exists():
@classmethod
@transaction.atomic
def notify(cls, user, related_user, **kwargs):
"""Create a notification"""
if related_user and (not user.local or user == related_user):
return
super().save(*args, **kwargs)
notification, _ = cls.objects.get_or_create(user=user, **kwargs)
if related_user:
notification.related_users.add(related_user)
notification.read = False
notification.save()
class Meta:
"""checks if notifcation is in enum list for valid types"""
constraints = [
models.CheckConstraint(
check=models.Q(notification_type__in=NotificationType.values),
name="notification_type_valid",
@classmethod
@transaction.atomic
def notify_list_item(cls, user, list_item):
"""Group the notifications around the list items, not the user"""
related_user = list_item.user
notification = cls.objects.filter(
user=user,
related_users=related_user,
related_list_items__book_list=list_item.book_list,
notification_type=Notification.ADD,
).first()
if not notification:
notification = cls.objects.create(
user=user, notification_type=Notification.ADD
)
]
notification.related_users.add(related_user)
notification.related_list_items.add(list_item)
notification.read = False
notification.save()
@classmethod
def unnotify(cls, user, related_user, **kwargs):
"""Remove a user from a notification and delete it if that was the only user"""
try:
notification = cls.objects.filter(user=user, **kwargs).get()
except Notification.DoesNotExist:
return
notification.related_users.remove(related_user)
if not notification.related_users.count():
notification.delete()
@receiver(models.signals.post_save, sender=Favorite)
# pylint: disable=unused-argument
def notify_on_fav(sender, instance, *args, **kwargs):
"""someone liked your content, you ARE loved"""
if not instance.status.user.local or instance.status.user == instance.user:
return
Notification.objects.create(
user=instance.status.user,
notification_type="FAVORITE",
related_user=instance.user,
Notification.notify(
instance.status.user,
instance.user,
related_status=instance.status,
notification_type=Notification.FAVORITE,
)
@ -81,15 +127,16 @@ def notify_on_unfav(sender, instance, *args, **kwargs):
"""oops, didn't like that after all"""
if not instance.status.user.local:
return
Notification.objects.filter(
user=instance.status.user,
related_user=instance.user,
Notification.unnotify(
instance.status.user,
instance.user,
related_status=instance.status,
notification_type="FAVORITE",
).delete()
notification_type=Notification.FAVORITE,
)
@receiver(models.signals.post_save)
@transaction.atomic
# pylint: disable=unused-argument
def notify_user_on_mention(sender, instance, *args, **kwargs):
"""creating and deleting statuses with @ mentions and replies"""
@ -105,22 +152,23 @@ def notify_user_on_mention(sender, instance, *args, **kwargs):
and instance.reply_parent.user != instance.user
and instance.reply_parent.user.local
):
Notification.objects.create(
user=instance.reply_parent.user,
notification_type="REPLY",
related_user=instance.user,
Notification.notify(
instance.reply_parent.user,
instance.user,
related_status=instance,
notification_type=Notification.REPLY,
)
for mention_user in instance.mention_users.all():
# avoid double-notifying about this status
if not mention_user.local or (
instance.reply_parent and mention_user == instance.reply_parent.user
):
continue
Notification.objects.create(
user=mention_user,
notification_type="MENTION",
related_user=instance.user,
Notification.notify(
mention_user,
instance.user,
notification_type=Notification.MENTION,
related_status=instance,
)
@ -135,11 +183,11 @@ def notify_user_on_boost(sender, instance, *args, **kwargs):
):
return
Notification.objects.create(
user=instance.boosted_status.user,
Notification.notify(
instance.boosted_status.user,
instance.user,
related_status=instance.boosted_status,
related_user=instance.user,
notification_type="BOOST",
notification_type=Notification.BOOST,
)
@ -147,12 +195,12 @@ def notify_user_on_boost(sender, instance, *args, **kwargs):
# pylint: disable=unused-argument
def notify_user_on_unboost(sender, instance, *args, **kwargs):
"""unboosting a status"""
Notification.objects.filter(
user=instance.boosted_status.user,
Notification.unnotify(
instance.boosted_status.user,
instance.user,
related_status=instance.boosted_status,
related_user=instance.user,
notification_type="BOOST",
).delete()
notification_type=Notification.BOOST,
)
@receiver(models.signals.post_save, sender=ImportJob)
@ -166,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,
)

View file

@ -1,5 +1,4 @@
""" defines relationships between users """
from django.apps import apps
from django.core.cache import cache
from django.db import models, transaction, IntegrityError
from django.db.models import Q
@ -148,14 +147,6 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
if not manually_approves:
self.accept()
model = apps.get_model("bookwyrm.Notification", require_ready=True)
notification_type = "FOLLOW_REQUEST" if manually_approves else "FOLLOW"
model.objects.create(
user=self.user_object,
related_user=self.user_subject,
notification_type=notification_type,
)
def get_accept_reject_id(self, status):
"""get id for sending an accept or reject of a local user"""
@ -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}",
]
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 %}

View file

@ -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 %}

View 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 %}

View file

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

View file

@ -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 %}

View file

@ -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 %}

View 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 %}

View file

@ -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 %}

View file

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

View file

@ -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 %}

View file

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

View file

@ -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 %}

View file

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

View file

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

View file

@ -112,9 +112,6 @@
{% with full=status.content|safe no_trim=status.content_warning itemprop="reviewBody" %}
{% include 'snippets/trimmed_text.html' %}
{% endwith %}
{% if status.progress %}
<div class="is-small is-italic has-text-right mr-3">{% trans "page" %} {{ status.progress }}</div>
{% endif %}
{% endif %}
{% if status.attachments.exists %}

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@ from bookwyrm import models, settings
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.lists_stream.remove_list_task.delay")
class List(TestCase):
"""some activitypub oddness ahead"""
@ -21,25 +22,15 @@ class List(TestCase):
work = models.Work.objects.create(title="hello")
self.book = models.Edition.objects.create(title="hi", parent_work=work)
def test_remote_id(self, _):
def test_remote_id(self, *_):
"""shelves use custom remote ids"""
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
), patch("bookwyrm.lists_stream.remove_list_task.delay"):
book_list = models.List.objects.create(
name="Test List", user=self.local_user
)
book_list = models.List.objects.create(name="Test List", user=self.local_user)
expected_id = f"https://{settings.DOMAIN}/list/{book_list.id}"
self.assertEqual(book_list.get_remote_id(), expected_id)
def test_to_activity(self, _):
def test_to_activity(self, *_):
"""jsonify it"""
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
), patch("bookwyrm.lists_stream.remove_list_task.delay"):
book_list = models.List.objects.create(
name="Test List", user=self.local_user
)
book_list = models.List.objects.create(name="Test List", user=self.local_user)
activity_json = book_list.to_activity()
self.assertIsInstance(activity_json, dict)
self.assertEqual(activity_json["id"], book_list.remote_id)
@ -48,14 +39,11 @@ class List(TestCase):
self.assertEqual(activity_json["name"], "Test List")
self.assertEqual(activity_json["owner"], self.local_user.remote_id)
def test_list_item(self, _):
def test_list_item(self, *_):
"""a list entry"""
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
), patch("bookwyrm.lists_stream.remove_list_task.delay"):
book_list = models.List.objects.create(
name="Test List", user=self.local_user, privacy="unlisted"
)
book_list = models.List.objects.create(
name="Test List", user=self.local_user, privacy="unlisted"
)
item = models.ListItem.objects.create(
book_list=book_list,
@ -68,14 +56,9 @@ class List(TestCase):
self.assertEqual(item.privacy, "unlisted")
self.assertEqual(item.recipients, [])
def test_list_item_pending(self, _):
def test_list_item_pending(self, *_):
"""a list entry"""
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
), patch("bookwyrm.lists_stream.remove_list_task.delay"):
book_list = models.List.objects.create(
name="Test List", user=self.local_user
)
book_list = models.List.objects.create(name="Test List", user=self.local_user)
item = models.ListItem.objects.create(
book_list=book_list,
@ -90,13 +73,8 @@ class List(TestCase):
self.assertEqual(item.privacy, "direct")
self.assertEqual(item.recipients, [])
def test_embed_key(self, _):
def test_embed_key(self, *_):
"""embed_key should never be empty"""
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
), patch("bookwyrm.lists_stream.remove_list_task.delay"):
book_list = models.List.objects.create(
name="Test List", user=self.local_user
)
book_list = models.List.objects.create(name="Test List", user=self.local_user)
self.assertIsInstance(book_list.embed_key, UUID)

View file

@ -0,0 +1,183 @@
""" testing models """
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models
class Notification(TestCase):
"""let people know things"""
def setUp(self): # pylint: disable=invalid-name
"""useful things for creating a notification"""
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
)
self.another_user = models.User.objects.create_user(
"rat", "rat@rat.rat", "ratword", local=True, localname="rat"
)
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=False,
remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox",
)
self.work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Test Book",
isbn_13="1234567890123",
remote_id="https://example.com/book/1",
parent_work=self.work,
)
self.another_book = models.Edition.objects.create(
title="Second Test Book",
parent_work=models.Work.objects.create(title="Test Work"),
)
def test_notification(self):
"""New notifications are unread"""
notification = models.Notification.objects.create(
user=self.local_user, notification_type=models.Notification.FAVORITE
)
self.assertFalse(notification.read)
def test_notify(self):
"""Create a notification"""
models.Notification.notify(
self.local_user,
self.remote_user,
notification_type=models.Notification.FAVORITE,
)
self.assertTrue(models.Notification.objects.exists())
def test_notify_grouping(self):
"""Bundle notifications"""
models.Notification.notify(
self.local_user,
self.remote_user,
notification_type=models.Notification.FAVORITE,
)
self.assertEqual(models.Notification.objects.count(), 1)
notification = models.Notification.objects.get()
self.assertEqual(notification.related_users.count(), 1)
models.Notification.notify(
self.local_user,
self.another_user,
notification_type=models.Notification.FAVORITE,
)
self.assertEqual(models.Notification.objects.count(), 1)
notification.refresh_from_db()
self.assertEqual(notification.related_users.count(), 2)
def test_notify_remote(self):
"""Don't create notifications for remote users"""
models.Notification.notify(
self.remote_user,
self.local_user,
notification_type=models.Notification.FAVORITE,
)
self.assertFalse(models.Notification.objects.exists())
def test_notify_self(self):
"""Don't create notifications for yourself"""
models.Notification.notify(
self.local_user,
self.local_user,
notification_type=models.Notification.FAVORITE,
)
self.assertFalse(models.Notification.objects.exists())
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.lists_stream.remove_list_task.delay")
def test_notify_list_item_own_list(self, *_):
"""Don't add list item notification for your own list"""
test_list = models.List.objects.create(user=self.local_user, name="hi")
models.ListItem.objects.create(
user=self.local_user, book=self.book, book_list=test_list, order=1
)
self.assertFalse(models.Notification.objects.exists())
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.lists_stream.remove_list_task.delay")
def test_notify_list_item_remote(self, *_):
"""Don't add list item notification for a remote user"""
test_list = models.List.objects.create(user=self.remote_user, name="hi")
models.ListItem.objects.create(
user=self.local_user, book=self.book, book_list=test_list, order=1
)
self.assertFalse(models.Notification.objects.exists())
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.lists_stream.remove_list_task.delay")
def test_notify_list_item(self, *_):
"""Add list item notification"""
test_list = models.List.objects.create(user=self.local_user, name="hi")
list_item = models.ListItem.objects.create(
user=self.remote_user, book=self.book, book_list=test_list, order=2
)
notification = models.Notification.objects.get()
self.assertEqual(notification.related_users.count(), 1)
self.assertEqual(notification.related_users.first(), self.remote_user)
self.assertEqual(notification.related_list_items.count(), 1)
self.assertEqual(notification.related_list_items.first(), list_item)
models.ListItem.objects.create(
user=self.remote_user, book=self.another_book, book_list=test_list, order=3
)
notification = models.Notification.objects.get()
self.assertEqual(notification.related_users.count(), 1)
self.assertEqual(notification.related_users.first(), self.remote_user)
self.assertEqual(notification.related_list_items.count(), 2)
def test_unnotify(self):
"""Remove a notification"""
models.Notification.notify(
self.local_user,
self.remote_user,
notification_type=models.Notification.FAVORITE,
)
self.assertTrue(models.Notification.objects.exists())
models.Notification.unnotify(
self.local_user,
self.remote_user,
notification_type=models.Notification.FAVORITE,
)
self.assertFalse(models.Notification.objects.exists())
def test_unnotify_multiple_users(self):
"""Remove a notification"""
models.Notification.notify(
self.local_user,
self.remote_user,
notification_type=models.Notification.FAVORITE,
)
models.Notification.notify(
self.local_user,
self.another_user,
notification_type=models.Notification.FAVORITE,
)
self.assertTrue(models.Notification.objects.exists())
models.Notification.unnotify(
self.local_user,
self.remote_user,
notification_type=models.Notification.FAVORITE,
)
self.assertTrue(models.Notification.objects.exists())
models.Notification.unnotify(
self.local_user,
self.another_user,
notification_type=models.Notification.FAVORITE,
)
self.assertFalse(models.Notification.objects.exists())

View file

@ -394,18 +394,6 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Announce")
self.assertEqual(activity, boost.to_activity(pure=True))
def test_notification(self, *_):
"""a simple model"""
notification = models.Notification.objects.create(
user=self.local_user, notification_type="FAVORITE"
)
self.assertFalse(notification.read)
with self.assertRaises(IntegrityError):
models.Notification.objects.create(
user=self.local_user, notification_type="GLORB"
)
# pylint: disable=unused-argument
def test_create_broadcast(self, one, two, broadcast_mock, *_):
"""should send out two verions of a status on create"""

View file

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

View file

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

View file

@ -0,0 +1,13 @@
""" test searching for books """
import re
from django.test import TestCase
from bookwyrm.utils import regex
class TestUtils(TestCase):
"""utility functions"""
def test_regex(self):
"""Regexes used throughout the app"""
self.assertTrue(re.match(regex.DOMAIN, "xn--69aa8bzb.xn--y9a3aq"))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -257,6 +257,29 @@ class GroupViews(TestCase):
self.assertEqual(notification.related_group, self.testgroup)
self.assertEqual(notification.notification_type, "REMOVE")
def test_remove_member_remove_self(self, _):
"""Leave a group"""
models.GroupMember.objects.create(
user=self.rat,
group=self.testgroup,
)
request = self.factory.post(
"",
{
"group": self.testgroup.id,
"user": self.rat.localname,
},
)
request.user = self.rat
result = views.remove_member(request)
self.assertEqual(result.status_code, 302)
self.assertEqual(models.GroupMember.objects.count(), 1)
self.assertEqual(models.GroupMember.objects.first().user, self.local_user)
notification = models.Notification.objects.get()
self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.related_group, self.testgroup)
self.assertEqual(notification.notification_type, "LEAVE")
def test_accept_membership(self, _):
"""accept an invite"""
models.GroupMemberInvitation.objects.create(

View file

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

View file

@ -60,7 +60,7 @@ class InteractionViews(TestCase):
notification = models.Notification.objects.get()
self.assertEqual(notification.notification_type, "FAVORITE")
self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.related_user, self.remote_user)
self.assertEqual(notification.related_users.first(), self.remote_user)
def test_unfavorite(self, *_):
"""unfav a status"""
@ -98,7 +98,7 @@ class InteractionViews(TestCase):
notification = models.Notification.objects.get()
self.assertEqual(notification.notification_type, "BOOST")
self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.related_user, self.remote_user)
self.assertEqual(notification.related_users.first(), self.remote_user)
self.assertEqual(notification.related_status, status)
def test_self_boost(self, *_):

View file

@ -25,10 +25,12 @@ class NotificationViews(TestCase):
local=True,
localname="mouse",
)
self.another_user = models.User.objects.create_user(
"rat", "rat@rat.rat", "ratword", local=True, localname="rat"
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
self.status = models.Status.objects.create(
content="hi",
user=self.local_user,
content="hi", user=self.local_user
)
models.SiteSettings.objects.create()
@ -42,27 +44,31 @@ class NotificationViews(TestCase):
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_notifications_page_notifications(self):
def test_notifications_page_status_notifications(self):
"""there are so many views, this just makes sure it LOADS"""
models.Notification.objects.create(
user=self.local_user,
models.Notification.notify(
self.local_user,
self.another_user,
notification_type="FAVORITE",
related_status=self.status,
)
models.Notification.objects.create(
user=self.local_user,
models.Notification.notify(
self.local_user,
self.another_user,
notification_type="BOOST",
related_status=self.status,
)
models.Notification.objects.create(
user=self.local_user,
models.Notification.notify(
self.local_user,
self.another_user,
notification_type="MENTION",
related_status=self.status,
)
self.status.reply_parent = self.status
self.status.save(broadcast=False)
models.Notification.objects.create(
user=self.local_user,
models.Notification.notify(
self.local_user,
self.another_user,
notification_type="REPLY",
related_status=self.status,
)
@ -74,6 +80,200 @@ class NotificationViews(TestCase):
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_notifications_page_follow_request(self):
"""import completed notification"""
models.Notification.notify(
self.local_user,
self.another_user,
notification_type="FOLLOW_REQUEST",
)
view = views.Notifications.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
def test_notifications_page_follows(self):
"""import completed notification"""
models.Notification.notify(
self.local_user,
self.another_user,
notification_type="FOLLOW",
)
view = views.Notifications.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
def test_notifications_page_report(self):
"""import completed notification"""
report = models.Report.objects.create(
user=self.another_user,
reporter=self.local_user,
)
notification = models.Notification.objects.create(
user=self.local_user,
notification_type="REPORT",
)
notification.related_reports.add(report)
view = views.Notifications.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
def test_notifications_page_import(self):
"""import completed notification"""
import_job = models.ImportJob.objects.create(user=self.local_user, mappings={})
models.Notification.objects.create(
user=self.local_user, notification_type="IMPORT", related_import=import_job
)
view = views.Notifications.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_notifications_page_list(self):
"""Adding books to lists"""
book = models.Edition.objects.create(title="shape")
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
), patch("bookwyrm.lists_stream.remove_list_task.delay"):
book_list = models.List.objects.create(user=self.local_user, name="hi")
item = models.ListItem.objects.create(
book=book, user=self.another_user, book_list=book_list, order=1
)
models.Notification.notify_list_item(self.local_user, item)
view = views.Notifications.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_notifications_page_group_invite(self):
"""group related notifications"""
group = models.Group.objects.create(user=self.another_user, name="group")
models.Notification.notify(
self.local_user,
self.another_user,
notification_type="INVITE",
related_group=group,
)
view = views.Notifications.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_notifications_page_group_accept(self):
"""group related notifications"""
group = models.Group.objects.create(user=self.another_user, name="group")
models.Notification.notify(
self.local_user,
self.another_user,
notification_type="ACCEPT",
related_group=group,
)
view = views.Notifications.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_notifications_page_group_join(self):
"""group related notifications"""
group = models.Group.objects.create(user=self.another_user, name="group")
models.Notification.notify(
self.local_user,
self.another_user,
notification_type="JOIN",
related_group=group,
)
view = views.Notifications.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_notifications_page_group_leave(self):
"""group related notifications"""
group = models.Group.objects.create(user=self.another_user, name="group")
models.Notification.notify(
self.local_user,
self.another_user,
notification_type="LEAVE",
related_group=group,
)
view = views.Notifications.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_notifications_page_group_remove(self):
"""group related notifications"""
group = models.Group.objects.create(user=self.another_user, name="group")
models.Notification.notify(
self.local_user,
self.another_user,
notification_type="REMOVE",
related_group=group,
)
view = views.Notifications.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_notifications_page_group_changes(self):
"""group related notifications"""
group = models.Group.objects.create(user=self.another_user, name="group")
models.Notification.notify(
self.local_user,
self.another_user,
notification_type="GROUP_PRIVACY",
related_group=group,
)
models.Notification.notify(
self.local_user,
self.another_user,
notification_type="GROUP_NAME",
related_group=group,
)
models.Notification.notify(
self.local_user,
self.another_user,
notification_type="GROUP_DESCRIPTION",
related_group=group,
)
view = views.Notifications.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_clear_notifications(self):
"""erase notifications"""
models.Notification.objects.create(

View file

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

View file

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

View 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,
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -59,11 +59,11 @@ class Group(View):
model = apps.get_model("bookwyrm.Notification", require_ready=True)
for field in form.changed_data:
notification_type = (
"GROUP_PRIVACY"
model.GROUP_PRIVACY
if field == "privacy"
else "GROUP_NAME"
else model.GROUP_NAME
if field == "name"
else "GROUP_DESCRIPTION"
else model.GROUP_DESCRIPTION
if field == "description"
else None
)
@ -71,9 +71,9 @@ class Group(View):
for membership in memberships:
member = membership.user
if member != request.user:
model.objects.create(
user=member,
related_user=request.user,
model.notify(
member,
request.user,
related_group=user_group,
notification_type=notification_type,
)
@ -244,24 +244,22 @@ def remove_member(request):
memberships = models.GroupMember.objects.filter(group=group)
model = apps.get_model("bookwyrm.Notification", require_ready=True)
notification_type = "LEAVE" if user == request.user else "REMOVE"
notification_type = model.LEAVE if user == request.user else model.REMOVE
# let the other members know about it
for membership in memberships:
member = membership.user
if member != request.user:
model.objects.create(
user=member,
related_user=user,
model.notify(
member,
user,
related_group=group,
notification_type=notification_type,
)
# let the user (now ex-member) know as well, if they were removed
if notification_type == "REMOVE":
model.objects.create(
user=user,
related_group=group,
notification_type=notification_type,
if notification_type == model.REMOVE:
model.notify(
user, None, related_group=group, notification_type=notification_type
)
return redirect(group.local_path)

View file

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

View file

@ -3,7 +3,6 @@ from django.contrib.auth import login
from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from django.views import View
from bookwyrm import models
@ -24,12 +23,13 @@ class PasswordResetRequest(View):
def post(self, request):
"""create a password reset token"""
email = request.POST.get("email")
data = {"sent_message": True, "email": email}
try:
user = models.User.viewer_aware_objects(request.user).get(
email=email, email__isnull=False
)
except models.User.DoesNotExist:
data = {"error": _("No user with that email address was found.")}
# Showing an error message would leak whether or not this email is in use
return TemplateResponse(
request, "landing/password_reset_request.html", data
)
@ -40,7 +40,6 @@ class PasswordResetRequest(View):
# create a new reset code
code = models.PasswordReset.objects.create(user=user)
password_reset_email(code)
data = {"message": _(f"A password reset link was sent to {email}")}
return TemplateResponse(request, "landing/password_reset_request.html", data)

View file

@ -15,16 +15,17 @@ class Notifications(View):
"""people are interacting with you, get hyped"""
notifications = (
request.user.notification_set.all()
.order_by("-created_date")
.order_by("-updated_date")
.select_related(
"related_status",
"related_status__reply_parent",
"related_group",
"related_import",
"related_report",
"related_user",
"related_book",
"related_list_item",
"related_list_item__book",
)
.prefetch_related(
"related_reports",
"related_users",
"related_list_items",
)
)
if notification_type == "mentions":

View file

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

View file

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

View file

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

View file

@ -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
View file

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