mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-23 00:26:33 +00:00
Merge branch 'main' into django-3-2
This commit is contained in:
commit
0889c57b86
214 changed files with 4734 additions and 3265 deletions
|
@ -11,6 +11,7 @@ class Book(ActivityObject):
|
||||||
"""serializes an edition or work, abstract"""
|
"""serializes an edition or work, abstract"""
|
||||||
|
|
||||||
title: str
|
title: str
|
||||||
|
lastEditedBy: str = None
|
||||||
sortTitle: str = ""
|
sortTitle: str = ""
|
||||||
subtitle: str = ""
|
subtitle: str = ""
|
||||||
description: str = ""
|
description: str = ""
|
||||||
|
@ -64,6 +65,7 @@ class Author(ActivityObject):
|
||||||
"""author of a book"""
|
"""author of a book"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
|
lastEditedBy: str = None
|
||||||
born: str = None
|
born: str = None
|
||||||
died: str = None
|
died: str = None
|
||||||
aliases: List[str] = field(default_factory=lambda: [])
|
aliases: List[str] = field(default_factory=lambda: [])
|
||||||
|
|
|
@ -176,6 +176,7 @@ class Remove(Add):
|
||||||
def action(self):
|
def action(self):
|
||||||
"""find and remove the activity object"""
|
"""find and remove the activity object"""
|
||||||
obj = self.object.to_model(save=False, allow_create=False)
|
obj = self.object.to_model(save=False, allow_create=False)
|
||||||
|
if obj:
|
||||||
obj.delete()
|
obj.delete()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import datetime
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms import ModelForm, PasswordInput, widgets
|
from django.forms import ModelForm, PasswordInput, widgets, ChoiceField
|
||||||
from django.forms.widgets import Textarea
|
from django.forms.widgets import Textarea
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -150,12 +150,10 @@ class LimitedEditUserForm(CustomForm):
|
||||||
help_texts = {f: None for f in fields}
|
help_texts = {f: None for f in fields}
|
||||||
|
|
||||||
|
|
||||||
class TagForm(CustomForm):
|
class UserGroupForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Tag
|
model = models.User
|
||||||
fields = ["name"]
|
fields = ["groups"]
|
||||||
help_texts = {f: None for f in fields}
|
|
||||||
labels = {"name": "Add a tag"}
|
|
||||||
|
|
||||||
|
|
||||||
class CoverForm(CustomForm):
|
class CoverForm(CustomForm):
|
||||||
|
@ -287,3 +285,20 @@ class ServerForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.FederatedServer
|
model = models.FederatedServer
|
||||||
exclude = ["remote_id"]
|
exclude = ["remote_id"]
|
||||||
|
|
||||||
|
|
||||||
|
class SortListForm(forms.Form):
|
||||||
|
sort_by = ChoiceField(
|
||||||
|
choices=(
|
||||||
|
("order", _("List Order")),
|
||||||
|
("title", _("Book Title")),
|
||||||
|
("rating", _("Rating")),
|
||||||
|
),
|
||||||
|
label=_("Sort By"),
|
||||||
|
)
|
||||||
|
direction = ChoiceField(
|
||||||
|
choices=(
|
||||||
|
("ascending", _("Ascending")),
|
||||||
|
("descending", _("Descending")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
30
bookwyrm/migrations/0067_denullify_list_item_order.py
Normal file
30
bookwyrm/migrations/0067_denullify_list_item_order.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def forwards_func(apps, schema_editor):
|
||||||
|
# Set all values for ListItem.order
|
||||||
|
BookList = apps.get_model("bookwyrm", "List")
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
for book_list in BookList.objects.using(db_alias).all():
|
||||||
|
for i, item in enumerate(book_list.listitem_set.order_by("id"), 1):
|
||||||
|
item.order = i
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_func(apps, schema_editor):
|
||||||
|
# null all values for ListItem.order
|
||||||
|
BookList = apps.get_model("bookwyrm", "List")
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
for book_list in BookList.objects.using(db_alias).all():
|
||||||
|
for item in book_list.listitem_set.order_by("id"):
|
||||||
|
item.order = None
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0066_user_deactivation_reason"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(forwards_func, reverse_func)]
|
23
bookwyrm/migrations/0068_ordering_for_list_items.py
Normal file
23
bookwyrm/migrations/0068_ordering_for_list_items.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 3.1.6 on 2021-04-08 16:15
|
||||||
|
|
||||||
|
import bookwyrm.models.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0067_denullify_list_item_order"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="listitem",
|
||||||
|
name="order",
|
||||||
|
field=bookwyrm.models.fields.IntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="listitem",
|
||||||
|
unique_together={("order", "book_list"), ("book", "book_list")},
|
||||||
|
),
|
||||||
|
]
|
34
bookwyrm/migrations/0069_auto_20210422_1604.py
Normal file
34
bookwyrm/migrations/0069_auto_20210422_1604.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 3.1.8 on 2021-04-22 16:04
|
||||||
|
|
||||||
|
import bookwyrm.models.fields
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0068_ordering_for_list_items"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="author",
|
||||||
|
name="last_edited_by",
|
||||||
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="book",
|
||||||
|
name="last_edited_by",
|
||||||
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
35
bookwyrm/migrations/0070_auto_20210423_0121.py
Normal file
35
bookwyrm/migrations/0070_auto_20210423_0121.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 3.1.8 on 2021-04-23 01:21
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0069_auto_20210422_1604"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="usertag",
|
||||||
|
unique_together=None,
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="usertag",
|
||||||
|
name="book",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="usertag",
|
||||||
|
name="tag",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="usertag",
|
||||||
|
name="user",
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="Tag",
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="UserTag",
|
||||||
|
),
|
||||||
|
]
|
|
@ -17,8 +17,6 @@ from .favorite import Favorite
|
||||||
from .notification import Notification
|
from .notification import Notification
|
||||||
from .readthrough import ReadThrough, ProgressUpdate, ProgressMode
|
from .readthrough import ReadThrough, ProgressUpdate, ProgressMode
|
||||||
|
|
||||||
from .tag import Tag, UserTag
|
|
||||||
|
|
||||||
from .user import User, KeyPair, AnnualGoal
|
from .user import User, KeyPair, AnnualGoal
|
||||||
from .relationship import UserFollows, UserFollowRequest, UserBlocks
|
from .relationship import UserFollows, UserFollowRequest, UserBlocks
|
||||||
from .report import Report, ReportComment
|
from .report import Report, ReportComment
|
||||||
|
|
|
@ -148,14 +148,18 @@ class ActivitypubMixin:
|
||||||
mentions = self.recipients if hasattr(self, "recipients") else []
|
mentions = self.recipients if hasattr(self, "recipients") else []
|
||||||
|
|
||||||
# we always send activities to explicitly mentioned users' inboxes
|
# we always send activities to explicitly mentioned users' inboxes
|
||||||
recipients = [u.inbox for u in mentions or []]
|
recipients = [u.inbox for u in mentions or [] if not u.local]
|
||||||
|
|
||||||
# unless it's a dm, all the followers should receive the activity
|
# unless it's a dm, all the followers should receive the activity
|
||||||
if privacy != "direct":
|
if privacy != "direct":
|
||||||
# we will send this out to a subset of all remote users
|
# we will send this out to a subset of all remote users
|
||||||
queryset = user_model.viewer_aware_objects(user).filter(
|
queryset = (
|
||||||
|
user_model.viewer_aware_objects(user)
|
||||||
|
.filter(
|
||||||
local=False,
|
local=False,
|
||||||
)
|
)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
# filter users first by whether they're using the desired software
|
# filter users first by whether they're using the desired software
|
||||||
# this lets us send book updates only to other bw servers
|
# this lets us send book updates only to other bw servers
|
||||||
if software:
|
if software:
|
||||||
|
@ -175,7 +179,7 @@ class ActivitypubMixin:
|
||||||
"inbox", flat=True
|
"inbox", flat=True
|
||||||
)
|
)
|
||||||
recipients += list(shared_inboxes) + list(inboxes)
|
recipients += list(shared_inboxes) + list(inboxes)
|
||||||
return recipients
|
return list(set(recipients))
|
||||||
|
|
||||||
def to_activity_dataclass(self):
|
def to_activity_dataclass(self):
|
||||||
"""convert from a model to an activity"""
|
"""convert from a model to an activity"""
|
||||||
|
@ -200,7 +204,9 @@ class ObjectMixin(ActivitypubMixin):
|
||||||
created = created or not bool(self.id)
|
created = created or not bool(self.id)
|
||||||
# first off, we want to save normally no matter what
|
# first off, we want to save normally no matter what
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
if not broadcast:
|
if not broadcast or (
|
||||||
|
hasattr(self, "status_type") and self.status_type == "Announce"
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
# this will work for objects owned by a user (lists, shelves)
|
# this will work for objects owned by a user (lists, shelves)
|
||||||
|
|
|
@ -26,7 +26,11 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
|
||||||
max_length=255, blank=True, null=True, deduplication_field=True
|
max_length=255, blank=True, null=True, deduplication_field=True
|
||||||
)
|
)
|
||||||
|
|
||||||
last_edited_by = models.ForeignKey("User", on_delete=models.PROTECT, null=True)
|
last_edited_by = fields.ForeignKey(
|
||||||
|
"User",
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""can't initialize this model, that wouldn't make sense"""
|
"""can't initialize this model, that wouldn't make sense"""
|
||||||
|
@ -163,7 +167,7 @@ class Work(OrderedCollectionPageMixin, Book):
|
||||||
return self.to_ordered_collection(
|
return self.to_ordered_collection(
|
||||||
self.editions.order_by("-edition_rank").all(),
|
self.editions.order_by("-edition_rank").all(),
|
||||||
remote_id="%s/editions" % self.remote_id,
|
remote_id="%s/editions" % self.remote_id,
|
||||||
**kwargs
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
activity_serializer = activitypub.Work
|
activity_serializer = activitypub.Work
|
||||||
|
|
|
@ -275,9 +275,12 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
||||||
return [i.remote_id for i in value.all()]
|
return [i.remote_id for i in value.all()]
|
||||||
|
|
||||||
def field_from_activity(self, value):
|
def field_from_activity(self, value):
|
||||||
items = []
|
|
||||||
if value is None or value is MISSING:
|
if value is None or value is MISSING:
|
||||||
return []
|
return None
|
||||||
|
if not isinstance(value, list):
|
||||||
|
# If this is a link, we currently aren't doing anything with it
|
||||||
|
return None
|
||||||
|
items = []
|
||||||
for remote_id in value:
|
for remote_id in value:
|
||||||
try:
|
try:
|
||||||
validate_remote_id(remote_id)
|
validate_remote_id(remote_id)
|
||||||
|
|
|
@ -47,7 +47,7 @@ class List(OrderedCollectionMixin, BookWyrmModel):
|
||||||
@property
|
@property
|
||||||
def collection_queryset(self):
|
def collection_queryset(self):
|
||||||
"""list of books for this shelf, overrides OrderedCollectionMixin"""
|
"""list of books for this shelf, overrides OrderedCollectionMixin"""
|
||||||
return self.books.filter(listitem__approved=True).all().order_by("listitem")
|
return self.books.filter(listitem__approved=True).order_by("listitem")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""default sorting"""
|
"""default sorting"""
|
||||||
|
@ -67,7 +67,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
|
||||||
)
|
)
|
||||||
notes = fields.TextField(blank=True, null=True)
|
notes = fields.TextField(blank=True, null=True)
|
||||||
approved = models.BooleanField(default=True)
|
approved = models.BooleanField(default=True)
|
||||||
order = fields.IntegerField(blank=True, null=True)
|
order = fields.IntegerField()
|
||||||
endorsement = models.ManyToManyField("User", related_name="endorsers")
|
endorsement = models.ManyToManyField("User", related_name="endorsers")
|
||||||
|
|
||||||
activity_serializer = activitypub.ListItem
|
activity_serializer = activitypub.ListItem
|
||||||
|
@ -93,7 +93,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
""" an opinionated constraint! you can't put a book on a list twice """
|
# A book may only be placed into a list once, and each order in the list may be used only
|
||||||
|
# once
|
||||||
unique_together = ("book", "book_list")
|
unique_together = (("book", "book_list"), ("order", "book_list"))
|
||||||
ordering = ("-created_date",)
|
ordering = ("-created_date",)
|
||||||
|
|
|
@ -50,11 +50,10 @@ class UserRelationship(BookWyrmModel):
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_remote_id(self, status=None): # pylint: disable=arguments-differ
|
def get_remote_id(self):
|
||||||
"""use shelf identifier in remote_id"""
|
"""use shelf identifier in remote_id"""
|
||||||
status = status or "follows"
|
|
||||||
base_path = self.user_subject.remote_id
|
base_path = self.user_subject.remote_id
|
||||||
return "%s#%s/%d" % (base_path, status, self.id)
|
return "%s#follows/%d" % (base_path, self.id)
|
||||||
|
|
||||||
|
|
||||||
class UserFollows(ActivityMixin, UserRelationship):
|
class UserFollows(ActivityMixin, UserRelationship):
|
||||||
|
@ -102,12 +101,15 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
|
|
||||||
def save(self, *args, broadcast=True, **kwargs):
|
def save(self, *args, broadcast=True, **kwargs):
|
||||||
"""make sure the follow or block relationship doesn't already exist"""
|
"""make sure the follow or block relationship doesn't already exist"""
|
||||||
# don't create a request if a follow already exists
|
# if there's a request for a follow that already exists, accept it
|
||||||
|
# without changing the local database state
|
||||||
if UserFollows.objects.filter(
|
if UserFollows.objects.filter(
|
||||||
user_subject=self.user_subject,
|
user_subject=self.user_subject,
|
||||||
user_object=self.user_object,
|
user_object=self.user_object,
|
||||||
).exists():
|
).exists():
|
||||||
raise IntegrityError()
|
self.accept(broadcast_only=True)
|
||||||
|
return
|
||||||
|
|
||||||
# blocking in either direction is a no-go
|
# blocking in either direction is a no-go
|
||||||
if UserBlocks.objects.filter(
|
if UserBlocks.objects.filter(
|
||||||
Q(
|
Q(
|
||||||
|
@ -138,16 +140,25 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
notification_type=notification_type,
|
notification_type=notification_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
def accept(self):
|
def get_accept_reject_id(self, status):
|
||||||
|
"""get id for sending an accept or reject of a local user"""
|
||||||
|
|
||||||
|
base_path = self.user_object.remote_id
|
||||||
|
return "%s#%s/%d" % (base_path, status, self.id or 0)
|
||||||
|
|
||||||
|
def accept(self, broadcast_only=False):
|
||||||
"""turn this request into the real deal"""
|
"""turn this request into the real deal"""
|
||||||
user = self.user_object
|
user = self.user_object
|
||||||
if not self.user_subject.local:
|
if not self.user_subject.local:
|
||||||
activity = activitypub.Accept(
|
activity = activitypub.Accept(
|
||||||
id=self.get_remote_id(status="accepts"),
|
id=self.get_accept_reject_id(status="accepts"),
|
||||||
actor=self.user_object.remote_id,
|
actor=self.user_object.remote_id,
|
||||||
object=self.to_activity(),
|
object=self.to_activity(),
|
||||||
).serialize()
|
).serialize()
|
||||||
self.broadcast(activity, user)
|
self.broadcast(activity, user)
|
||||||
|
if broadcast_only:
|
||||||
|
return
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
UserFollows.from_request(self)
|
UserFollows.from_request(self)
|
||||||
self.delete()
|
self.delete()
|
||||||
|
@ -156,7 +167,7 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
"""generate a Reject for this follow request"""
|
"""generate a Reject for this follow request"""
|
||||||
if self.user_object.local:
|
if self.user_object.local:
|
||||||
activity = activitypub.Reject(
|
activity = activitypub.Reject(
|
||||||
id=self.get_remote_id(status="rejects"),
|
id=self.get_accept_reject_id(status="rejects"),
|
||||||
actor=self.user_object.remote_id,
|
actor=self.user_object.remote_id,
|
||||||
object=self.to_activity(),
|
object=self.to_activity(),
|
||||||
).serialize()
|
).serialize()
|
||||||
|
|
|
@ -48,7 +48,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||||
@property
|
@property
|
||||||
def collection_queryset(self):
|
def collection_queryset(self):
|
||||||
"""list of books for this shelf, overrides OrderedCollectionMixin"""
|
"""list of books for this shelf, overrides OrderedCollectionMixin"""
|
||||||
return self.books.all().order_by("shelfbook")
|
return self.books.order_by("shelfbook")
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""shelf identifier instead of id"""
|
"""shelf identifier instead of id"""
|
||||||
|
|
|
@ -351,6 +351,16 @@ class Boost(ActivityMixin, Status):
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""save and notify"""
|
"""save and notify"""
|
||||||
|
# This constraint can't work as it would cross tables.
|
||||||
|
# class Meta:
|
||||||
|
# unique_together = ('user', 'boosted_status')
|
||||||
|
if (
|
||||||
|
Boost.objects.filter(boosted_status=self.boosted_status, user=self.user)
|
||||||
|
.exclude(id=self.id)
|
||||||
|
.exists()
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
if not self.boosted_status.user.local or self.boosted_status.user == self.user:
|
if not self.boosted_status.user.local or self.boosted_status.user == self.user:
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
""" models for storing different kinds of Activities """
|
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
|
||||||
from bookwyrm.settings import DOMAIN
|
|
||||||
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
|
||||||
from .base_model import BookWyrmModel
|
|
||||||
from . import fields
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(OrderedCollectionMixin, BookWyrmModel):
|
|
||||||
""" freeform tags for books """
|
|
||||||
|
|
||||||
name = fields.CharField(max_length=100, unique=True)
|
|
||||||
identifier = models.CharField(max_length=100)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def books(self):
|
|
||||||
""" count of books associated with this tag """
|
|
||||||
edition_model = apps.get_model("bookwyrm.Edition", require_ready=True)
|
|
||||||
return (
|
|
||||||
edition_model.objects.filter(usertag__tag__identifier=self.identifier)
|
|
||||||
.order_by("-created_date")
|
|
||||||
.distinct()
|
|
||||||
)
|
|
||||||
|
|
||||||
collection_queryset = books
|
|
||||||
|
|
||||||
def get_remote_id(self):
|
|
||||||
""" tag should use identifier not id in remote_id """
|
|
||||||
base_path = "https://%s" % DOMAIN
|
|
||||||
return "%s/tag/%s" % (base_path, self.identifier)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
""" create a url-safe lookup key for the tag """
|
|
||||||
if not self.id:
|
|
||||||
# add identifiers to new tags
|
|
||||||
self.identifier = urllib.parse.quote_plus(self.name)
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class UserTag(CollectionItemMixin, BookWyrmModel):
|
|
||||||
""" an instance of a tag on a book by a user """
|
|
||||||
|
|
||||||
user = fields.ForeignKey(
|
|
||||||
"User", on_delete=models.PROTECT, activitypub_field="actor"
|
|
||||||
)
|
|
||||||
book = fields.ForeignKey(
|
|
||||||
"Edition", on_delete=models.PROTECT, activitypub_field="object"
|
|
||||||
)
|
|
||||||
tag = fields.ForeignKey("Tag", on_delete=models.PROTECT, activitypub_field="target")
|
|
||||||
|
|
||||||
activity_serializer = activitypub.Add
|
|
||||||
object_field = "book"
|
|
||||||
collection_field = "tag"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
""" unqiueness constraint """
|
|
||||||
|
|
||||||
unique_together = ("user", "book", "tag")
|
|
|
@ -153,7 +153,7 @@ LANGUAGES = [
|
||||||
("de-de", _("German")),
|
("de-de", _("German")),
|
||||||
("es", _("Spanish")),
|
("es", _("Spanish")),
|
||||||
("fr-fr", _("French")),
|
("fr-fr", _("French")),
|
||||||
("zh-cn", _("Simplified Chinese")),
|
("zh-hans", _("Simplified Chinese")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
scroll-padding-top: 20%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -30,6 +29,40 @@ body {
|
||||||
min-width: 75% !important;
|
min-width: 75% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Utilities not covered by Bulma
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
.is-sr-only-mobile {
|
||||||
|
border: none !important;
|
||||||
|
clip: rect(0, 0, 0, 0) !important;
|
||||||
|
height: 0.01em !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
position: absolute !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
width: 0.01em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-0-mobile {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.is-transparent {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.is-stretchable {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.is-stretchable .card-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/** Shelving
|
/** Shelving
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
|
@ -86,6 +119,13 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Stars
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
.stars {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/** Stars in a review form
|
/** Stars in a review form
|
||||||
*
|
*
|
||||||
* Specificity makes hovering taking over checked inputs.
|
* Specificity makes hovering taking over checked inputs.
|
||||||
|
@ -256,3 +296,53 @@ body {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Book preview table
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
.book-preview td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
table.is-mobile,
|
||||||
|
table.is-mobile tbody {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.is-mobile tr {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-top: 1px solid #dbdbdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.is-mobile td {
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1 0 100%;
|
||||||
|
order: 2;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.is-mobile td.book-preview-top-row {
|
||||||
|
order: 1;
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.is-mobile td[data-title]:not(:empty)::before {
|
||||||
|
content: attr(data-title);
|
||||||
|
display: block;
|
||||||
|
font-size: 0.75em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.is-mobile td:empty {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.is-mobile th,
|
||||||
|
table.is-mobile thead {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -67,31 +67,16 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<section class="content is-clipped">
|
<section class="is-clipped">
|
||||||
<dl>
|
{% with book=book %}
|
||||||
{% if book.isbn_13 %}
|
<div class="content">
|
||||||
<div class="is-flex is-justify-content-space-between is-align-items-center">
|
{% include 'book/publisher_info.html' %}
|
||||||
<dt>{% trans "ISBN:" %}</dt>
|
|
||||||
<dd itemprop="isbn">{{ book.isbn_13 }}</dd>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if book.oclc_number %}
|
<div class="my-3">
|
||||||
<div class="is-flex is-justify-content-space-between is-align-items-center">
|
{% include 'book/book_identifiers.html' %}
|
||||||
<dt>{% trans "OCLC Number:" %}</dt>
|
|
||||||
<dd>{{ book.oclc_number }}</dd>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endwith %}
|
||||||
|
|
||||||
{% if book.asin %}
|
|
||||||
<div class="is-flex is-justify-content-space-between is-align-items-center">
|
|
||||||
<dt>{% trans "ASIN:" %}</dt>
|
|
||||||
<dd>{{ book.asin }}</dd>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
{% include 'book/publisher_info.html' with book=book %}
|
|
||||||
|
|
||||||
{% if book.openlibrary_key %}
|
{% if book.openlibrary_key %}
|
||||||
<p><a href="https://openlibrary.org/books/{{ book.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a></p>
|
<p><a href="https://openlibrary.org/books/{{ book.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a></p>
|
||||||
|
@ -261,7 +246,36 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block" id="reviews">
|
<div class="block" id="reviews">
|
||||||
{% for review in reviews %}
|
{% if request.user.is_authenticated %}
|
||||||
|
<nav class="tabs">
|
||||||
|
<ul>
|
||||||
|
{% url 'book' book.id as tab_url %}
|
||||||
|
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||||
|
<a href="{{ tab_url }}">{% trans "Reviews" %} ({{ review_count }})</a>
|
||||||
|
</li>
|
||||||
|
{% if user_statuses.review_count %}
|
||||||
|
{% url 'book-user-statuses' book.id 'review' as tab_url %}
|
||||||
|
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||||
|
<a href="{{ tab_url }}">{% trans "Your reviews" %} ({{ user_statuses.review_count }})</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if user_statuses.comment_count %}
|
||||||
|
{% url 'book-user-statuses' book.id 'comment' as tab_url %}
|
||||||
|
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||||
|
<a href="{{ tab_url }}">{% trans "Your comments" %} ({{ user_statuses.comment_count }})</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if user_statuses.quotation_count %}
|
||||||
|
{% url 'book-user-statuses' book.id 'quote' as tab_url %}
|
||||||
|
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||||
|
<a href="{{ tab_url }}">{% trans "Your quotes" %} ({{ user_statuses.quotation_count }})</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for review in statuses %}
|
||||||
<div
|
<div
|
||||||
class="block"
|
class="block"
|
||||||
itemprop="review"
|
itemprop="review"
|
||||||
|
@ -302,7 +316,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{% include 'snippets/pagination.html' with page=reviews path=book.local_path anchor="#reviews" %}
|
{% include 'snippets/pagination.html' with page=statuses path=request.path anchor="#reviews" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
27
bookwyrm/templates/book/book_identifiers.html
Normal file
27
bookwyrm/templates/book/book_identifiers.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% spaceless %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
{% if book.isbn_13 %}
|
||||||
|
<div class="is-flex">
|
||||||
|
<dt class="mr-1">{% trans "ISBN:" %}</dt>
|
||||||
|
<dd itemprop="isbn">{{ book.isbn_13 }}</dd>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if book.oclc_number %}
|
||||||
|
<div class="is-flex">
|
||||||
|
<dt class="mr-1">{% trans "OCLC Number:" %}</dt>
|
||||||
|
<dd>{{ book.oclc_number }}</dd>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if book.asin %}
|
||||||
|
<div class="is-flex">
|
||||||
|
<dt class="mr-1">{% trans "ASIN:" %}</dt>
|
||||||
|
<dd>{{ book.asin }}</dd>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</dl>
|
||||||
|
{% endspaceless %}
|
|
@ -109,7 +109,10 @@
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<p class="mb-2"><label class="label" for="id_series">{% trans "Series:" %}</label> {{ form.series }} </p>
|
<p class="mb-2">
|
||||||
|
<label class="label" for="id_series">{% trans "Series:" %}</label>
|
||||||
|
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}">
|
||||||
|
</p>
|
||||||
{% for error in form.series.errors %}
|
{% for error in form.series.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -25,7 +25,18 @@
|
||||||
{{ book.title }}
|
{{ book.title }}
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
{% include 'book/publisher_info.html' with book=book %}
|
|
||||||
|
{% with book=book %}
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
<div class="column is-half">
|
||||||
|
{% include 'book/publisher_info.html' %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-half ">
|
||||||
|
{% include 'book/book_identifiers.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True %}
|
{% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True %}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{% with format=book.physical_format pages=book.pages %}
|
{% with format=book.physical_format pages=book.pages %}
|
||||||
|
@ -39,7 +40,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{% with date=book.published_date|date:'M jS Y' publisher=book.publishers|join:', ' %}
|
{% with date=book.published_date|naturalday publisher=book.publishers|join:', ' %}
|
||||||
{% if date or book.first_published_date %}
|
{% if date or book.first_published_date %}
|
||||||
<meta
|
<meta
|
||||||
itemprop="datePublished"
|
itemprop="datePublished"
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<ul
|
<ul
|
||||||
id="menu-options-{{ uuid }}"
|
id="menu-options-{{ uuid }}"
|
||||||
class="dropdown-content"
|
class="dropdown-content p-0 is-clipped"
|
||||||
role="menu"
|
role="menu"
|
||||||
>
|
>
|
||||||
{% block dropdown-list %}{% endblock %}
|
{% block dropdown-list %}{% endblock %}
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<div class="column is-one-third">
|
<div class="column is-one-third">
|
||||||
<div class="card block">
|
<div class="card is-stretchable">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<a href="{{ user.local_path }}" class="media-left">
|
<a href="{{ user.local_path }}" class="media-left">
|
||||||
|
@ -56,13 +56,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div>
|
||||||
{% if user.summary %}
|
{% if user.summary %}
|
||||||
{{ user.summary | to_markdown | safe | truncatechars_html:40 }}
|
{{ user.summary | to_markdown | safe | truncatechars_html:40 }}
|
||||||
{% else %} {% endif %}
|
{% else %} {% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="card-footer content">
|
<footer class="card-footer">
|
||||||
{% if user != request.user %}
|
{% if user != request.user %}
|
||||||
{% if user.mutuals %}
|
{% if user.mutuals %}
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="{% get_lang %}">
|
||||||
<head>
|
<head>
|
||||||
<title>{% block title %}BookWyrm{% endblock %} | {{ site.name }}</title>
|
<title>{% block title %}BookWyrm{% endblock %} | {{ site.name }}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
|
|
||||||
<div class="columns mt-3">
|
<div class="columns mt-3">
|
||||||
<section class="column is-three-quarters">
|
<section class="column is-three-quarters">
|
||||||
{% if not items.exists %}
|
{% if not items.object_list.exists %}
|
||||||
<p>{% trans "This list is currently empty" %}</p>
|
<p>{% trans "This list is currently empty" %}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<ol>
|
<ol start="{{ items.start_index }}">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<li class="block pb-3">
|
<li class="block pb-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
@ -30,11 +30,27 @@
|
||||||
{% include 'snippets/shelve_button/shelve_button.html' with book=item.book %}
|
{% include 'snippets/shelve_button/shelve_button.html' with book=item.book %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer has-background-white-bis">
|
<div class="card-footer has-background-white-bis is-align-items-baseline">
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
|
<div>
|
||||||
<p>{% blocktrans with username=item.user.display_name user_path=user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p>
|
<p>{% blocktrans with username=item.user.display_name user_path=user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %}
|
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %}
|
||||||
|
<div class="card-footer-item">
|
||||||
|
<form name="set-position" method="post" action="{% url 'list-set-book-position' item.id %}">
|
||||||
|
<div class="field has-addons mb-0">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="control">
|
||||||
|
<input id="input-list-position" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}">
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button type="submit" class="button is-info is-small is-tablet">{% trans "Set" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label for="input-list-position" class="help">{% trans "List position" %}</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item">
|
<form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="item" value="{{ item.id }}">
|
<input type="hidden" name="item" value="{{ item.id }}">
|
||||||
|
@ -47,10 +63,27 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% include "snippets/pagination.html" with page=items %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% if request.user.is_authenticated and not list.curation == 'closed' or request.user == list.user %}
|
|
||||||
<section class="column is-one-quarter content">
|
<section class="column is-one-quarter content">
|
||||||
|
<h2>{% trans "Sort List" %}</h2>
|
||||||
|
<form name="sort" action="{% url 'list' list.id %}" method="GET" class="block">
|
||||||
|
<label class="label" for="id_sort_by">{% trans "Sort By" %}</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
{{ sort_form.sort_by }}
|
||||||
|
</div>
|
||||||
|
<label class="label" for="id_direction">{% trans "Direction" %}</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
{{ sort_form.direction }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="button is-primary is-fullwidth" type="submit">
|
||||||
|
{% trans "Sort List" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% if request.user.is_authenticated and not list.curation == 'closed' or request.user == list.user %}
|
||||||
<h2>{% if list.curation == 'open' or request.user == list.user %}{% trans "Add Books" %}{% else %}{% trans "Suggest Books" %}{% endif %}</h2>
|
<h2>{% if list.curation == 'open' or request.user == list.user %}{% trans "Add Books" %}{% else %}{% trans "Suggest Books" %}{% endif %}</h2>
|
||||||
<form name="search" action="{% url 'list' list.id %}" method="GET" class="block">
|
<form name="search" action="{% url 'list' list.id %}" method="GET" class="block">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
|
@ -93,7 +126,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</section>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -15,76 +15,9 @@
|
||||||
{% include 'moderation/report_preview.html' with report=report %}
|
{% include 'moderation/report_preview.html' with report=report %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block columns">
|
{% include 'user_admin/user_info.html' with user=report.user %}
|
||||||
<div class="column is-flex is-flex-direction-column">
|
|
||||||
<h4 class="title is-4">{% trans "User details" %}</h4>
|
|
||||||
<div class="box is-flex-grow-1">
|
|
||||||
{% include 'user/user_preview.html' with user=report.user %}
|
|
||||||
{% if report.user.summary %}
|
|
||||||
<div class="box content has-background-white-ter is-shadowless">
|
|
||||||
{{ report.user.summary | to_markdown | safe }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p class="mt-2"><a href="{{ report.user.local_path }}">{% trans "View user profile" %}</a></p>
|
{% include 'user_admin/user_moderation_actions.html' with user=report.user %}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if not report.user.local %}
|
|
||||||
{% with server=report.user.federated_server %}
|
|
||||||
<div class="column is-half is-flex is-flex-direction-column">
|
|
||||||
<h4 class="title is-4">{% trans "Instance details" %}</h4>
|
|
||||||
<div class="box content is-flex-grow-1">
|
|
||||||
{% if server %}
|
|
||||||
<h5>{{ server.server_name }}</h5>
|
|
||||||
<dl>
|
|
||||||
<div class="is-flex">
|
|
||||||
<dt>{% trans "Software:" %}</dt>
|
|
||||||
<dd>{{ server.application_type }}</dd>
|
|
||||||
</div>
|
|
||||||
<div class="is-flex">
|
|
||||||
<dt>{% trans "Version:" %}</dt>
|
|
||||||
<dd>{{ server.application_version }}</dd>
|
|
||||||
</div>
|
|
||||||
<div class="is-flex">
|
|
||||||
<dt>{% trans "Status:" %}</dt>
|
|
||||||
<dd>{{ server.status }}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
{% if server.notes %}
|
|
||||||
<h5>{% trans "Notes" %}</h5>
|
|
||||||
<div class="box content has-background-white-ter is-shadowless">
|
|
||||||
{{ server.notes }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<a href="{% url 'settings-federated-server' server.id %}">{% trans "View instance" %}</a>
|
|
||||||
</p>
|
|
||||||
{% else %}
|
|
||||||
<em>{% trans "Not set" %}</em>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="block content">
|
|
||||||
<h3>{% trans "Actions" %}</h3>
|
|
||||||
<div class="is-flex">
|
|
||||||
<p class="mr-1">
|
|
||||||
<a class="button" href="{% url 'direct-messages-user' report.user.username %}">{% trans "Send direct message" %}</a>
|
|
||||||
</p>
|
|
||||||
<form name="deactivate" method="post" action="{% url 'settings-report-deactivate' report.id %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% if report.user.is_active %}
|
|
||||||
<button type="submit" class="button is-danger is-light">{% trans "Deactivate user" %}</button>
|
|
||||||
{% else %}
|
|
||||||
<button class="button">{% trans "Reactivate user" %}</button>
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3>
|
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3>
|
||||||
|
@ -118,7 +51,7 @@
|
||||||
{% for status in report.statuses.select_subclasses.all %}
|
{% for status in report.statuses.select_subclasses.all %}
|
||||||
<li>
|
<li>
|
||||||
{% if status.deleted %}
|
{% if status.deleted %}
|
||||||
<em>{% trans "Statuses has been deleted" %}</em>
|
<em>{% trans "Status has been deleted" %}</em>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include 'snippets/status/status.html' with status=status moderation_mode=True %}
|
{% include 'snippets/status/status.html' with status=status moderation_mode=True %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'settings/user_admin_filters.html' %}
|
{% include 'user_admin/user_admin_filters.html' %}
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{% if not reports %}
|
{% if not reports %}
|
||||||
|
|
|
@ -123,7 +123,7 @@
|
||||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow {% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %}has-text-black{% else %}has-text-grey-dark{% endif %}">
|
<div class="column is-narrow {% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %}has-text-black{% else %}has-text-grey-dark{% endif %}">
|
||||||
{{ related_status.published_date | post_date }}
|
{{ related_status.published_date|timesince }}
|
||||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
{% if book.authors %}
|
{% if book.authors %}
|
||||||
{% blocktrans with path=book.local_path title=book.title %}<a href="{{ path }}">{{ title }}</a> by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %}
|
{% blocktrans with path=book.local_path title=book|title %}<a href="{{ path }}">{{ title }}</a> by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ book.local_path }}">{{ book.title }}</a>
|
<a href="{{ book.local_path }}">{{ book|title }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -4,18 +4,16 @@
|
||||||
{% with status.id|uuid as uuid %}
|
{% with status.id|uuid as uuid %}
|
||||||
<form name="boost" action="/boost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} {% if request.user|boosted:status %}is-hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
|
<form name="boost" action="/boost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} {% if request.user|boosted:status %}is-hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="button is-small" type="submit" {% if not status.boostable %}disabled{% endif %}>
|
<button class="button is-small is-light is-transparent" type="submit" {% if not status.boostable %}disabled{% endif %}>
|
||||||
<span class="icon icon-boost" title="{% trans 'Boost status' %}">
|
<span class="icon icon-boost m-0-mobile" title="{% trans 'Boost' %}"></span>
|
||||||
<span class="is-sr-only">{% trans "Boost status" %}</span>
|
<span class="is-sr-only-mobile">{% trans "Boost" %}</span>
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<form name="unboost" action="/unboost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} active {% if not request.user|boosted:status %}is-hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
|
<form name="unboost" action="/unboost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} active {% if not request.user|boosted:status %}is-hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="button is-small is-primary" type="submit">
|
<button class="button is-small is-light is-transparent" type="submit">
|
||||||
<span class="icon icon-boost" title="{% trans 'Un-boost status' %}">
|
<span class="icon icon-boost has-text-primary m-0-mobile" title="{% trans 'Un-boost' %}"></span>
|
||||||
<span class="is-sr-only">{% trans "Un-boost status" %}</span>
|
<span class="is-sr-only-mobile">{% trans "Un-boost" %}</span>
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
|
@ -6,14 +6,16 @@
|
||||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||||
<input type="hidden" name="reply_parent" value="{% firstof draft.reply_parent.id reply_parent.id %}">
|
<input type="hidden" name="reply_parent" value="{% firstof draft.reply_parent.id reply_parent.id %}">
|
||||||
{% if type == 'review' %}
|
{% if type == 'review' %}
|
||||||
<div class="control">
|
<div class="field">
|
||||||
<label class="label" for="id_name_{{ book.id }}_{{ type }}">{% trans "Title" %}:</label>
|
<label class="label" for="id_name_{{ book.id }}_{{ type }}">{% trans "Title" %}:</label>
|
||||||
|
<div class="control">
|
||||||
<input type="text" name="name" maxlength="255" class="input" required="" id="id_name_{{ book.id }}_{{ type }}" placeholder="My {{ type }} of '{{ book.title }}'" value="{% firstof draft.name ''%}">
|
<input type="text" name="name" maxlength="255" class="input" required="" id="id_name_{{ book.id }}_{{ type }}" placeholder="My {{ type }} of '{{ book.title }}'" value="{% firstof draft.name ''%}">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="control">
|
<div class="field">
|
||||||
{% if type != 'reply' and type != 'direct' %}
|
{% if type != 'reply' and type != 'direct' %}
|
||||||
<label class="label" for="id_{% if type == 'quotation' %}quote{% else %}content{% endif %}_{{ book.id }}_{{ type }}">
|
<label class="label{% if type == 'review' %} mb-0{% endif %}" for="id_{% if type == 'quotation' %}quote{% else %}content{% endif %}_{{ book.id }}_{{ type }}">
|
||||||
{% if type == 'comment' %}
|
{% if type == 'comment' %}
|
||||||
{% trans "Comment:" %}
|
{% trans "Comment:" %}
|
||||||
{% elif type == 'quotation' %}
|
{% elif type == 'quotation' %}
|
||||||
|
@ -25,28 +27,37 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if type == 'review' %}
|
{% if type == 'review' %}
|
||||||
<fieldset>
|
<fieldset class="mb-1">
|
||||||
<legend class="is-sr-only">{% trans "Rating" %}</legend>
|
<legend class="is-sr-only">{% trans "Rating" %}</legend>
|
||||||
|
|
||||||
{% include 'snippets/form_rate_stars.html' with book=book type=type|default:'summary' default_rating=draft.rating %}
|
{% include 'snippets/form_rate_stars.html' with book=book type=type|default:'summary' default_rating=draft.rating %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="control">
|
||||||
{% if type == 'quotation' %}
|
{% if type == 'quotation' %}
|
||||||
<textarea name="quote" class="textarea" id="id_quote_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required>{{ draft.quote|default:'' }}</textarea>
|
<textarea name="quote" class="textarea" id="id_quote_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required>{{ draft.quote|default:'' }}</textarea>
|
||||||
|
{% elif type == 'reply' %}
|
||||||
|
{% include 'snippets/content_warning_field.html' with parent_status=status %}
|
||||||
|
<textarea name="content" class="textarea" id="id_content_{{ type }}-{{ book.id }}{{reply_parent.id}}" placeholder="{{ placeholder }}" aria-label="{% trans 'Reply' %}" required>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mentions %}@{{ mentions|username }} {% endif %}{{ draft.content|default:'' }}</textarea>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include 'snippets/content_warning_field.html' with parent_status=status %}
|
{% include 'snippets/content_warning_field.html' with parent_status=status %}
|
||||||
<textarea name="content" class="textarea" id="id_content_{{ type }}-{{ book.id }}{{reply_parent.id}}" placeholder="{{ placeholder }}" {% if type == 'reply' %} aria-label="Reply"{% endif %} required>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mentions %}@{{ mentions|username }} {% endif %}{{ draft.content|default:'' }}</textarea>
|
<textarea name="content" class="textarea" id="id_content_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required>{{ draft.content|default:'' }}</textarea>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Supplemental fields #}
|
||||||
{% if type == 'quotation' %}
|
{% if type == 'quotation' %}
|
||||||
<div class="control">
|
<div class="field">
|
||||||
<label class="label" for="id_content_quote-{{ book.id }}">{% trans "Comment" %}:</label>
|
<label class="label" for="id_content_quote-{{ book.id }}">{% trans "Comment" %}:</label>
|
||||||
{% include 'snippets/content_warning_field.html' with parent_status=status %}
|
{% include 'snippets/content_warning_field.html' with parent_status=status %}
|
||||||
<textarea name="content" class="textarea is-small" id="id_content_quote-{{ book.id }}">{{ draft.content|default:'' }}</textarea>
|
<div class="control">
|
||||||
|
<textarea name="content" class="textarea" rows="3" id="id_content_quote-{{ book.id }}">{{ draft.content|default:'' }}</textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% elif type == 'comment' %}
|
{% elif type == 'comment' %}
|
||||||
<div class="control">
|
<div>
|
||||||
{% active_shelf book as active_shelf %}
|
{% active_shelf book as active_shelf %}
|
||||||
{% if active_shelf.shelf.identifier == 'reading' and book.latest_readthrough %}
|
{% if active_shelf.shelf.identifier == 'reading' and book.latest_readthrough %}
|
||||||
|
|
||||||
|
@ -58,13 +69,15 @@
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input aria-label="{% if draft.progress_mode == 'PG' or readthrough.progress_mode == 'PG' %}Current page{% else %}Percent read{% endif %}" class="input" type="number" min="0" name="progress" size="3" value="{% firstof draft.progress readthrough.progress '' %}" id="progress-{{ uuid }}">
|
<input aria-label="{% if draft.progress_mode == 'PG' or readthrough.progress_mode == 'PG' %}Current page{% else %}Percent read{% endif %}" class="input" type="number" min="0" name="progress" size="3" value="{% firstof draft.progress readthrough.progress '' %}" id="progress-{{ uuid }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="control select">
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
<select name="progress_mode" aria-label="Progress mode">
|
<select name="progress_mode" aria-label="Progress mode">
|
||||||
<option value="PG" {% if draft.progress_mode == 'PG' or readthrough.progress_mode == 'PG' %}selected{% endif %}>{% trans "pages" %}</option>
|
<option value="PG" {% if draft.progress_mode == 'PG' or readthrough.progress_mode == 'PG' %}selected{% endif %}>{% trans "pages" %}</option>
|
||||||
<option value="PCT" {% if draft.progress_mode == 'PCT' or readthrough.progress_mode == 'PCT' %}selected{% endif %}>{% trans "percent" %}</option>
|
<option value="PCT" {% if draft.progress_mode == 'PCT' or readthrough.progress_mode == 'PCT' %}selected{% endif %}>{% trans "percent" %}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% if readthrough.progress_mode == 'PG' and book.pages %}
|
{% if readthrough.progress_mode == 'PG' and book.pages %}
|
||||||
<p class="help">{% blocktrans with pages=book.pages %}of {{ pages }} pages{% endblocktrans %}</p>
|
<p class="help">{% blocktrans with pages=book.pages %}of {{ pages }} pages{% endblocktrans %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -73,9 +86,12 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input type="checkbox" class="is-hidden" name="sensitive" id="id_show_spoilers-{{ uuid }}" {% if draft.content_warning or status.content_warning %}checked{% endif %} aria-hidden="true">
|
|
||||||
|
|
||||||
{# bottom bar #}
|
{# bottom bar #}
|
||||||
<div class="columns pt-1">
|
<input type="checkbox" class="is-hidden" name="sensitive" id="id_show_spoilers-{{ uuid }}" {% if draft.content_warning or status.content_warning %}checked{% endif %} aria-hidden="true">
|
||||||
|
|
||||||
|
<div class="columns mt-1">
|
||||||
<div class="field has-addons column">
|
<div class="field has-addons column">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{% trans "Include spoiler alert" as button_text %}
|
{% trans "Include spoiler alert" as button_text %}
|
||||||
|
|
|
@ -3,18 +3,17 @@
|
||||||
{% with status.id|uuid as uuid %}
|
{% with status.id|uuid as uuid %}
|
||||||
<form name="favorite" action="/favorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} {% if request.user|liked:status %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
|
<form name="favorite" action="/favorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} {% if request.user|liked:status %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="button is-small" type="submit">
|
<button class="button is-small is-light is-transparent" type="submit">
|
||||||
<span class="icon icon-heart" title="{% trans 'Like status' %}">
|
<span class="icon icon-heart m-0-mobile" title="{% trans 'Like' %}">
|
||||||
<span class="is-sr-only">{% trans "Like status" %}</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
<span class="is-sr-only-mobile">{% trans "Like" %}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} active {% if not request.user|liked:status %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
|
<form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} active {% if not request.user|liked:status %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="button is-primary is-small" type="submit">
|
<button class="button is-light is-transparent is-small" type="submit">
|
||||||
<span class="icon icon-heart" title="{% trans 'Un-like status' %}">
|
<span class="icon icon-heart has-text-primary m-0-mobile" title="{% trans 'Un-like' %}"></span>
|
||||||
<span class="is-sr-only">{% trans "Un-like status" %}</span>
|
<span class="is-sr-only-mobile">{% trans "Un-like" %}</span>
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% if rating %}
|
{% if rating %}
|
||||||
|
|
||||||
{% blocktrans with book_title=book.title display_rating=rating|floatformat:"0" review_title=name count counter=rating %}Review of "{{ book_title }}" ({{ display_rating }} star): {{ review_title }}{% plural %}Review of "{{ book_title }}" ({{ display_rating }} stars): {{ review_title }}{% endblocktrans %}
|
{% blocktrans with book_title=book.title|safe display_rating=rating|floatformat:"0" review_title=name|safe count counter=rating %}Review of "{{ book_title }}" ({{ display_rating }} star): {{ review_title }}{% plural %}Review of "{{ book_title }}" ({{ display_rating }} stars): {{ review_title }}{% endblocktrans %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
{% blocktrans with book_title=book.title review_title=name %}Review of "{{ book_title }}": {{ review_title }}{% endblocktrans %}
|
{% blocktrans with book_title=book.title|safe review_title=name|safe %}Review of "{{ book_title }}": {{ review_title }}{% endblocktrans %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -7,23 +7,23 @@
|
||||||
|
|
||||||
{% block dropdown-list %}
|
{% block dropdown-list %}
|
||||||
{% for shelf in request.user.shelf_set.all %}
|
{% for shelf in request.user.shelf_set.all %}
|
||||||
<li role="menuitem">
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
<form class="dropdown-item pt-0 pb-0" name="shelve" action="/shelve/" method="post">
|
<form name="shelve" action="/shelve/" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
<input type="hidden" name="change-shelf-from" value="{{ current.identifier }}">
|
<input type="hidden" name="change-shelf-from" value="{{ current.identifier }}">
|
||||||
<input type="hidden" name="shelf" value="{{ shelf.identifier }}">
|
<input type="hidden" name="shelf" value="{{ shelf.identifier }}">
|
||||||
<button class="button is-fullwidth is-small shelf-option" type="submit" {% if shelf.identifier == current.identifier %}disabled{% endif %}><span>{{ shelf.name }}</span></button>
|
<button class="button is-fullwidth is-small shelf-option is-radiusless is-white" type="submit" {% if shelf.identifier == current.identifier %}disabled{% endif %}><span>{{ shelf.name }}</span></button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li class="navbar-divider" role="presentation"></li>
|
<li class="navbar-divider" role="separator"></li>
|
||||||
<li>
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
<form class="dropdown-item pt-0 pb-0" name="shelve" action="/unshelve/" method="post">
|
<form name="shelve" action="/unshelve/" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
<input type="hidden" name="shelf" value="{{ current.id }}">
|
<input type="hidden" name="shelf" value="{{ current.id }}">
|
||||||
<button class="button is-fullwidth is-small is-danger is-light" type="submit">{% trans "Remove" %}</button>
|
<button class="button is-fullwidth is-small is-radiusless is-danger is-light" type="submit">{% trans "Remove" %}</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -7,5 +7,5 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block dropdown-list %}
|
{% block dropdown-list %}
|
||||||
{% include 'snippets/shelve_button/shelve_button_options.html' with active_shelf=active_shelf shelves=request.user.shelf_set.all dropdown=True class="shelf-option is-fullwidth is-small" %}
|
{% include 'snippets/shelve_button/shelve_button_options.html' with active_shelf=active_shelf shelves=request.user.shelf_set.all dropdown=True class="shelf-option is-fullwidth is-small is-radiusless is-white" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% for shelf in shelves %}
|
{% for shelf in shelves %}
|
||||||
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
|
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
|
||||||
{% if dropdown %}<li role="menuitem">{% endif %}
|
{% if dropdown %}<li role="menuitem" class="dropdown-item p-0">{% endif %}
|
||||||
<div class="{% if dropdown %}dropdown-item pt-0 pb-0{% elif active_shelf.shelf.identifier|next_shelf != shelf.identifier %}is-hidden{% endif %}">
|
<div class="{% if not dropdown and active_shelf.shelf.identifier|next_shelf != shelf.identifier %}is-hidden{% endif %}">
|
||||||
{% if shelf.identifier == 'reading' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
|
{% if shelf.identifier == 'reading' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
|
||||||
{% trans "Start reading" as button_text %}
|
{% trans "Start reading" as button_text %}
|
||||||
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="start-reading" controls_uid=button_uuid focus="modal-title-start-reading" disabled=is_current %}
|
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="start-reading" controls_uid=button_uuid focus="modal-title-start-reading" disabled=is_current %}
|
||||||
|
@ -30,24 +30,20 @@
|
||||||
{% if dropdown %}
|
{% if dropdown %}
|
||||||
|
|
||||||
{% if readthrough and active_shelf.shelf.identifier != 'read' %}
|
{% if readthrough and active_shelf.shelf.identifier != 'read' %}
|
||||||
<li role="menuitem">
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
<div class="dropdown-item pt-0 pb-0">
|
|
||||||
{% trans "Update progress" as button_text %}
|
{% trans "Update progress" as button_text %}
|
||||||
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="progress-update" controls_uid=button_uuid focus="modal-title-progress-update" %}
|
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="progress-update" controls_uid=button_uuid focus="modal-title-progress-update" %}
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if active_shelf.shelf %}
|
{% if active_shelf.shelf %}
|
||||||
<li role="menuitem">
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
<div class="dropdown-item pt-0 pb-0">
|
|
||||||
<form name="shelve" action="/unshelve/" method="post">
|
<form name="shelve" action="/unshelve/" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
||||||
<input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}">
|
<input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}">
|
||||||
<button class="button is-fullwidth is-small is-danger is-light" type="submit">{% blocktrans with name=active_shelf.shelf.name %}Remove from {{ name }}{% endblocktrans %}</button>
|
<button class="button is-fullwidth is-small{% if dropdown %} is-radiusless{% endif %} is-danger is-light" type="submit">{% blocktrans with name=active_shelf.shelf.name %}Remove from {{ name }}{% endblocktrans %}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<p class="stars">
|
<span class="stars">
|
||||||
<span class="is-sr-only">
|
<span class="is-sr-only">
|
||||||
{% if rating %}
|
{% if rating %}
|
||||||
{% blocktranslate trimmed with rating=rating|floatformat count counter=rating|length %}
|
{% blocktranslate trimmed with rating=rating|floatformat count counter=rating|length %}
|
||||||
|
@ -23,5 +23,5 @@
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></span>
|
></span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</span>
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-narrow">
|
|
||||||
<div>
|
|
||||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
|
|
||||||
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
|
|
||||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<h3 class="title is-6">{% include 'snippets/book_titleby.html' with book=book %}</h3>
|
|
||||||
{% include 'snippets/trimmed_text.html' with full=book|book_description %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
135
bookwyrm/templates/snippets/status/content_status.html
Normal file
135
bookwyrm/templates/snippets/status/content_status.html
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% with status_type=status.status_type %}
|
||||||
|
<div
|
||||||
|
class="block"
|
||||||
|
{% if status_type == "Review" %}
|
||||||
|
itemprop="rating"
|
||||||
|
itemtype="https://schema.org/Rating"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
{% if not hide_book %}
|
||||||
|
{% with book=status.book|default:status.mention_books.first %}
|
||||||
|
{% if book %}
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
|
||||||
|
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
|
||||||
|
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||||
|
</div>
|
||||||
|
<div class="column is-hidden-tablet">
|
||||||
|
<p>{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:15 }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<article class="column">
|
||||||
|
{% if status_type == 'Review' %}
|
||||||
|
<header class="mb-2">
|
||||||
|
<h3
|
||||||
|
class="title is-5 has-subtitle"
|
||||||
|
dir="auto"
|
||||||
|
itemprop="name"
|
||||||
|
>
|
||||||
|
{{ status.name|escape }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<h4 class="subtitle is-6">
|
||||||
|
<span
|
||||||
|
class="is-hidden"
|
||||||
|
{% if status_type == "Review" %}
|
||||||
|
itemprop="reviewRating"
|
||||||
|
itemscope
|
||||||
|
itemtype="https://schema.org/Rating"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
|
||||||
|
|
||||||
|
{# @todo Is it possible to not hard-code the value? #}
|
||||||
|
<meta itemprop="bestRating" content="5">
|
||||||
|
</span>
|
||||||
|
{% include 'snippets/stars.html' with rating=status.rating %}
|
||||||
|
</h4>
|
||||||
|
</header>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if status.content_warning %}
|
||||||
|
<div>
|
||||||
|
<p>{{ status.content_warning }}</p>
|
||||||
|
|
||||||
|
{% trans "Show more" as button_text %}
|
||||||
|
|
||||||
|
{% with text=button_text class="is-small" controls_text="show-status-cw" controls_uid=status.id %}
|
||||||
|
{% include 'snippets/toggle/open_button.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div
|
||||||
|
{% if status.content_warning %}
|
||||||
|
id="show-status-cw-{{ status.id }}"
|
||||||
|
class="is-hidden"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
{% if status.content_warning %}
|
||||||
|
{% trans "Show less" as button_text %}
|
||||||
|
|
||||||
|
{% with text=button_text class="is-small" controls_text="show-status-cw" controls_uid=status.id %}
|
||||||
|
{% include 'snippets/toggle/close_button.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if status.quote %}
|
||||||
|
<div class="quote block">
|
||||||
|
<blockquote dir="auto" class="content mb-2">{{ status.quote | safe }}</blockquote>
|
||||||
|
|
||||||
|
<p> — {% include 'snippets/book_titleby.html' with book=status.book %}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if status.content and status_type != 'GeneratedNote' and status_type != 'Announce' %}
|
||||||
|
{% with full=status.content|safe no_trim=status.content_warning itemprop="reviewBody" %}
|
||||||
|
{% include 'snippets/trimmed_text.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if status.attachments.exists %}
|
||||||
|
<div class="block">
|
||||||
|
<div class="columns">
|
||||||
|
{% for attachment in status.attachments.all %}
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<figure class="image is-128x128">
|
||||||
|
<a
|
||||||
|
href="/images/{{ attachment.image }}"
|
||||||
|
target="_blank"
|
||||||
|
aria-label="{% trans 'Open image in new window' %}"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/{{ attachment.image }}"
|
||||||
|
|
||||||
|
{% if attachment.caption %}
|
||||||
|
alt="{{ attachment.caption }}"
|
||||||
|
title="{{ attachment.caption }}"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
|
|
23
bookwyrm/templates/snippets/status/generated_status.html
Normal file
23
bookwyrm/templates/snippets/status/generated_status.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{% spaceless %}
|
||||||
|
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% if not hide_book %}
|
||||||
|
{% with book=status.book|default:status.mention_books.first %}
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<div>
|
||||||
|
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<h3 class="title is-6 mb-1">{% include 'snippets/book_titleby.html' with book=book %}</h3>
|
||||||
|
<p>{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}</p>
|
||||||
|
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endspaceless %}
|
76
bookwyrm/templates/snippets/status/layout.html
Normal file
76
bookwyrm/templates/snippets/status/layout.html
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
{% extends 'components/card.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
|
{% block card-header %}
|
||||||
|
<div class="card-header-title has-background-white-ter is-block">
|
||||||
|
{% include 'snippets/status/status_header.html' with status=status %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block card-content %}{% endblock %}
|
||||||
|
|
||||||
|
{% block card-footer %}
|
||||||
|
{% if moderation_mode and perms.bookwyrm.moderate_post %}
|
||||||
|
<div class="card-footer-item">
|
||||||
|
|
||||||
|
{# moderation options #}
|
||||||
|
<form name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="button is-danger is-light" type="submit">
|
||||||
|
{% trans "Delete status" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% elif no_interact %}
|
||||||
|
{# nothing here #}
|
||||||
|
{% elif request.user.is_authenticated %}
|
||||||
|
<div class="card-footer-item">
|
||||||
|
{% trans "Reply" as button_text %}
|
||||||
|
{% include 'snippets/toggle/toggle_button.html' with controls_text="show-comment" controls_uid=status.id text=button_text icon_with_text="comment" class="is-small is-light is-transparent toggle-button" focus="id_content_reply" %}
|
||||||
|
</div>
|
||||||
|
<div class="card-footer-item">
|
||||||
|
{% include 'snippets/boost_button.html' with status=status %}
|
||||||
|
</div>
|
||||||
|
<div class="card-footer-item">
|
||||||
|
{% include 'snippets/fav_button.html' with status=status %}
|
||||||
|
</div>
|
||||||
|
{% if not moderation_mode %}
|
||||||
|
<div class="card-footer-item">
|
||||||
|
{% include 'snippets/status/status_options.html' with class="is-small is-light is-transparent" right=True %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="card-footer-item">
|
||||||
|
<a href="/login">
|
||||||
|
<span class="icon icon-comment is-small" title="{% trans 'Reply' %}">
|
||||||
|
<span class="is-sr-only">{% trans "Reply" %}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="icon icon-boost is-small ml-4" title="{% trans 'Boost status' %}">
|
||||||
|
<span class="is-sr-only">{% trans "Boost status" %}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="icon icon-heart is-small ml-4" title="{% trans 'Like status' %}">
|
||||||
|
<span class="is-sr-only">{% trans "Like status" %}</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block card-bonus %}
|
||||||
|
{% if request.user.is_authenticated and not moderation_mode %}
|
||||||
|
{% with status.id|uuid as uuid %}
|
||||||
|
<section class="is-hidden" id="show-comment-{{ status.id }}">
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="card-footer-item">
|
||||||
|
{% include 'snippets/create_status_form.html' with reply_parent=status type="reply" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
|
@ -1,90 +1,14 @@
|
||||||
{% extends 'components/card.html' %}
|
{% extends 'snippets/status/layout.html' %}
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
{% load humanize %}
|
|
||||||
|
|
||||||
{% block card-header %}
|
|
||||||
<h3 class="card-header-title has-background-white-ter is-block">
|
|
||||||
{% include 'snippets/status/status_header.html' with status=status %}
|
|
||||||
</h3>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block card-content %}
|
{% block card-content %}
|
||||||
{% include 'snippets/status/status_content.html' with status=status %}
|
{% with status_type=status.status_type %}
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block card-footer %}
|
|
||||||
<div class="card-footer-item">
|
|
||||||
{% if moderation_mode and perms.bookwyrm.moderate_post %}
|
|
||||||
|
|
||||||
{# moderation options #}
|
|
||||||
<form class="dropdown-item pt-0 pb-0" name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button class="button is-danger is-light" type="submit">
|
|
||||||
{% trans "Delete status" %}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% elif no_interact %}
|
|
||||||
{# nothing here #}
|
|
||||||
{% elif request.user.is_authenticated %}
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control">
|
|
||||||
{% trans "Reply" as button_text %}
|
|
||||||
{% include 'snippets/toggle/toggle_button.html' with controls_text="show-comment" controls_uid=status.id text=button_text icon="comment" class="is-small toggle-button" focus="id_content_reply" %}
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
{% include 'snippets/boost_button.html' with status=status %}
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
{% include 'snippets/fav_button.html' with status=status %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{% if status_type == 'GeneratedNote' or status_type == 'Rating' %}
|
||||||
|
{% include 'snippets/status/generated_status.html' with status=status %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/login">
|
{% include 'snippets/status/content_status.html' with status=status %}
|
||||||
<span class="icon icon-comment" title="{% trans 'Reply' %}">
|
|
||||||
<span class="is-sr-only">{% trans "Reply" %}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="icon icon-boost" title="{% trans 'Boost status' %}">
|
|
||||||
<span class="is-sr-only">{% trans "Boost status" %}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="icon icon-heart" title="{% trans 'Like status' %}">
|
|
||||||
<span class="is-sr-only">{% trans "Like status" %}</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-footer-item">
|
|
||||||
{% include 'snippets/privacy-icons.html' with item=status %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-footer-item">
|
|
||||||
<a href="{{ status.remote_id }}">{{ status.published_date | post_date }}</a>
|
|
||||||
</div>
|
|
||||||
{% if not moderation_mode %}
|
|
||||||
<div class="card-footer-item">
|
|
||||||
{% include 'snippets/status/status_options.html' with class="is-small" right=True %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block card-bonus %}
|
|
||||||
{% if request.user.is_authenticated and not moderation_mode %}
|
|
||||||
{% with status.id|uuid as uuid %}
|
|
||||||
<section class="is-hidden" id="show-comment-{{ status.id }}">
|
|
||||||
<div class="card-footer">
|
|
||||||
<div class="card-footer-item">
|
|
||||||
{% include 'snippets/create_status_form.html' with reply_parent=status type="reply" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -1,137 +0,0 @@
|
||||||
{% spaceless %}
|
|
||||||
|
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% with status_type=status.status_type %}
|
|
||||||
<div
|
|
||||||
class="block"
|
|
||||||
|
|
||||||
{% if status_type == 'Review' %}
|
|
||||||
{% firstof "reviewBody" as body_prop %}
|
|
||||||
{% firstof 'itemprop="reviewRating" itemscope itemtype="https://schema.org/Rating"' as rating_type %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if status_type == 'Rating' %}
|
|
||||||
itemprop="rating"
|
|
||||||
itemtype="https://schema.org/Rating"
|
|
||||||
{% endif %}
|
|
||||||
>
|
|
||||||
{% if status_type == 'Review' or status_type == 'Rating' %}
|
|
||||||
<div>
|
|
||||||
{% if status.name %}
|
|
||||||
<h3
|
|
||||||
class="title is-5 has-subtitle"
|
|
||||||
dir="auto"
|
|
||||||
itemprop="name"
|
|
||||||
>
|
|
||||||
{{ status.name|escape }}
|
|
||||||
</h3>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="is-sr-only"
|
|
||||||
{{ rating_type }}
|
|
||||||
>
|
|
||||||
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
|
|
||||||
|
|
||||||
{% if status_type == 'Rating' %}
|
|
||||||
{# @todo Is it possible to not hard-code the value? #}
|
|
||||||
<meta itemprop="bestRating" content="5">
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{% include 'snippets/stars.html' with rating=status.rating %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if status.content_warning %}
|
|
||||||
<div>
|
|
||||||
<p>{{ status.content_warning }}</p>
|
|
||||||
|
|
||||||
{% trans "Show more" as button_text %}
|
|
||||||
|
|
||||||
{% with text=button_text class="is-small" controls_text="show-status-cw" controls_uid=status.id %}
|
|
||||||
{% include 'snippets/toggle/open_button.html' %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div
|
|
||||||
{% if status.content_warning %}
|
|
||||||
id="show-status-cw-{{ status.id }}"
|
|
||||||
class="is-hidden"
|
|
||||||
{% endif %}
|
|
||||||
>
|
|
||||||
{% if status.content_warning %}
|
|
||||||
{% trans "Show less" as button_text %}
|
|
||||||
|
|
||||||
{% with text=button_text class="is-small" controls_text="show-status-cw" controls_uid=status.id %}
|
|
||||||
{% include 'snippets/toggle/close_button.html' %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if status.quote %}
|
|
||||||
<div class="quote block">
|
|
||||||
<blockquote dir="auto" class="content mb-2">{{ status.quote | safe }}</blockquote>
|
|
||||||
|
|
||||||
<p> — {% include 'snippets/book_titleby.html' with book=status.book %}</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if status.content and status_type != 'GeneratedNote' and status_type != 'Announce' %}
|
|
||||||
{% with full=status.content|safe no_trim=status.content_warning itemprop=body_prop %}
|
|
||||||
{% include 'snippets/trimmed_text.html' %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if status.attachments.exists %}
|
|
||||||
<div class="block">
|
|
||||||
<div class="columns">
|
|
||||||
{% for attachment in status.attachments.all %}
|
|
||||||
<div class="column is-narrow">
|
|
||||||
<figure class="image is-128x128">
|
|
||||||
<a
|
|
||||||
href="/images/{{ attachment.image }}"
|
|
||||||
target="_blank"
|
|
||||||
aria-label="{% trans 'Open image in new window' %}"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/images/{{ attachment.image }}"
|
|
||||||
|
|
||||||
{% if attachment.caption %}
|
|
||||||
alt="{{ attachment.caption }}"
|
|
||||||
title="{{ attachment.caption }}"
|
|
||||||
{% endif %}
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if not hide_book %}
|
|
||||||
{% if status.book or status.mention_books.count %}
|
|
||||||
<div
|
|
||||||
{% if status_type != 'GeneratedNote' %}
|
|
||||||
class="box has-background-white-bis"
|
|
||||||
{% endif %}
|
|
||||||
>
|
|
||||||
{% if status.book %}
|
|
||||||
{% with book=status.book %}
|
|
||||||
{% include 'snippets/status/book_preview.html' %}
|
|
||||||
{% endwith %}
|
|
||||||
{% elif status.mention_books.count %}
|
|
||||||
{% with book=status.mention_books.first %}
|
|
||||||
{% include 'snippets/status/book_preview.html' %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endspaceless %}
|
|
|
@ -1,16 +1,29 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
|
<div class="media">
|
||||||
|
<figure class="media-left" aria-hidden="true">
|
||||||
|
<a class="image is-48x48" href="{{ status.user.local_path }}">
|
||||||
|
{% include 'snippets/avatar.html' with user=status.user ariaHide="true" medium="true" %}
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<div class="media-content">
|
||||||
|
<h3 class="has-text-weight-bold">
|
||||||
<span
|
<span
|
||||||
itemprop="author"
|
itemprop="author"
|
||||||
itemscope
|
itemscope
|
||||||
itemtype="https://schema.org/Person"
|
itemtype="https://schema.org/Person"
|
||||||
>
|
>
|
||||||
|
{% if status.user.avatar %}
|
||||||
|
<meta itemprop="image" content="/images/{{ status.user.avatar }}">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="{{ status.user.local_path }}"
|
href="{{ status.user.local_path }}"
|
||||||
itemprop="url"
|
itemprop="url"
|
||||||
>
|
>
|
||||||
{% include 'snippets/avatar.html' with user=status.user ariaHide="true" %}
|
|
||||||
|
|
||||||
<span itemprop="name">{{ status.user.display_name }}</span>
|
<span itemprop="name">{{ status.user.display_name }}</span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
@ -28,26 +41,68 @@
|
||||||
{% elif status.reply_parent %}
|
{% elif status.reply_parent %}
|
||||||
{% with parent_status=status|parent %}
|
{% with parent_status=status|parent %}
|
||||||
|
|
||||||
{% if parent_status.status_type == 'Review' %}
|
|
||||||
{% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">review</a>{% endblocktrans %}
|
|
||||||
{% elif parent_status.status_type == 'Comment' %}
|
|
||||||
{% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">comment</a>{% endblocktrans %}
|
|
||||||
{% elif parent_status.status_type == 'Quotation' %}
|
|
||||||
{% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">quote</a>{% endblocktrans %}
|
|
||||||
{% else %}
|
|
||||||
{% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">status</a>{% endblocktrans %}
|
{% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">status</a>{% endblocktrans %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if status.book %}
|
{% if status.book %}
|
||||||
<a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>
|
{% if status.status_type == 'GeneratedNote' or status.status_type == 'Rating' %}
|
||||||
|
<a href="/book/{{ status.book.id }}">{{ status.book|title }}</a>{% if status.status_type == 'Rating' %}:
|
||||||
|
<span
|
||||||
|
itemprop="reviewRating"
|
||||||
|
itemscope
|
||||||
|
itemtype="https://schema.org/Rating"
|
||||||
|
>
|
||||||
|
<span class="is-hidden" {{ rating_type }}>
|
||||||
|
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
|
||||||
|
|
||||||
|
{% if status.book %}
|
||||||
|
{% if status.status_type == 'GeneratedNote' or status.status_type == 'Rating' %}
|
||||||
|
<a href="/book/{{ status.book.id }}">{{ status.book|title }}</a>{% if status.status_type == 'Rating' %}:
|
||||||
|
<span
|
||||||
|
itemprop="reviewRating"
|
||||||
|
itemscope
|
||||||
|
itemtype="https://schema.org/Rating"
|
||||||
|
>
|
||||||
|
<span class="is-hidden" {{ rating_type }}>
|
||||||
|
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
|
||||||
|
|
||||||
|
{# @todo Is it possible to not hard-code the value? #}
|
||||||
|
<meta itemprop="bestRating" content="5">
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% include 'snippets/stars.html' with rating=status.rating %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% include 'snippets/book_titleby.html' with book=status.book %}
|
||||||
|
{% endif %}
|
||||||
{% elif status.mention_books %}
|
{% elif status.mention_books %}
|
||||||
<a href="/book/{{ status.mention_books.first.id }}">{{ status.mention_books.first.title }}</a>
|
<a href="/book/{{ status.mention_books.first.id }}">
|
||||||
|
{{ status.mention_books.first.title }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% include 'snippets/stars.html' with rating=status.rating %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% include 'snippets/book_titleby.html' with book=status.book %}
|
||||||
|
{% endif %}
|
||||||
|
{% elif status.mention_books %}
|
||||||
|
<a href="/book/{{ status.mention_books.first.id }}">{{ status.mention_books.first|title }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
</h3>
|
||||||
|
<p class="is-size-7 is-flex is-align-items-center">
|
||||||
|
<a href="{{ status.remote_id }}">{{ status.published_date|timesince }}</a>
|
||||||
{% if status.progress %}
|
{% if status.progress %}
|
||||||
<p class="help">
|
<span class="ml-1">
|
||||||
({% if status.progress_mode == 'PG' %}{% include 'snippets/page_text.html' with page=status.progress total_pages=status.book.pages %}{% else %}{{ status.progress }}%{% endif %})
|
{% if status.progress_mode == 'PG' %}
|
||||||
</p>
|
({% include 'snippets/page_text.html' with page=status.progress total_pages=status.book.pages %})
|
||||||
|
{% else %}
|
||||||
|
({{ status.progress }}%)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% include 'snippets/privacy-icons.html' with item=status %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
@ -3,27 +3,26 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
|
|
||||||
{% block dropdown-trigger %}
|
{% block dropdown-trigger %}
|
||||||
<span class="icon icon-dots-three">
|
<span class="icon icon-dots-three m-0-mobile"></span>
|
||||||
<span class="is-sr-only">{% trans "More options" %}</span>
|
<span class="is-sr-only-mobile">{% trans "More options" %}</span>
|
||||||
</span>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block dropdown-list %}
|
{% block dropdown-list %}
|
||||||
{% if status.user == request.user %}
|
{% if status.user == request.user %}
|
||||||
{# things you can do to your own statuses #}
|
{# things you can do to your own statuses #}
|
||||||
<li role="menuitem">
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
<form class="dropdown-item pt-0 pb-0" name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
|
<form name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="button is-danger is-light is-fullwidth is-small" type="submit">
|
<button class="button is-radiusless is-danger is-light is-fullwidth is-small" type="submit">
|
||||||
{% trans "Delete status" %}
|
{% trans "Delete status" %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
{% if status.status_type != 'GeneratedNote' and status.status_type != 'Rating' %}
|
{% if status.status_type != 'GeneratedNote' and status.status_type != 'Rating' %}
|
||||||
<li role="menuitem">
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
<form class="dropdown-item pt-0 pb-0" name="delete-{{status.id}}" action="{% url 'redraft' status.id %}" method="post">
|
<form class="" name="delete-{{status.id}}" action="{% url 'redraft' status.id %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="button is-danger is-light is-fullwidth is-small" type="submit">
|
<button class="button is-radiusless is-danger is-light is-fullwidth is-small" type="submit">
|
||||||
{% trans "Delete & re-draft" %}
|
{% trans "Delete & re-draft" %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -31,13 +30,15 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{# things you can do to other people's statuses #}
|
{# things you can do to other people's statuses #}
|
||||||
<li role="menuitem">
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
<a href="{% url 'direct-messages-user' status.user|username %}" class="button is-small is-fullwidth">{% trans "Send direct message" %}</a>
|
<a href="{% url 'direct-messages-user' status.user|username %}" class="button is-small is-white is-radiusless is-fullwidth">
|
||||||
|
{% trans "Send direct message" %}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="menuitem">
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
{% include 'snippets/report_button.html' with user=status.user status=status %}
|
{% include 'snippets/report_button.html' with user=status.user status=status %}
|
||||||
</li>
|
</li>
|
||||||
<li role="menuitem">
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
{% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %}
|
{% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %}
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
{% load i18n %}
|
|
||||||
<div class="control">
|
|
||||||
<form name="tag" action="/{% if tag.tag.identifier in user_tags %}untag{% else %}tag{% endif %}/" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
|
||||||
<input type="hidden" name="name" value="{{ tag.tag.name }}">
|
|
||||||
|
|
||||||
<div class="tags has-addons">
|
|
||||||
<a class="tag" href="{{ tag.tag.local_path }}">
|
|
||||||
{{ tag.tag.name }}
|
|
||||||
</a>
|
|
||||||
{% if tag.tag.identifier in user_tags %}
|
|
||||||
<button class="tag is-delete" type="submit">
|
|
||||||
<span class="is-sr-only">{% trans "Remove tag" %}</span>
|
|
||||||
</button>
|
|
||||||
{% else %}
|
|
||||||
<button class="tag" type="submit">+
|
|
||||||
<span class="is-sr-only">{% trans "Add tag" %}</span>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
|
@ -10,9 +10,12 @@
|
||||||
>
|
>
|
||||||
|
|
||||||
{% if icon %}
|
{% if icon %}
|
||||||
<span class="icon icon-{{ icon }}" title="{{ text }}">
|
<span class="icon icon-{{ icon }} m-0-mobile" title="{{ text }}">
|
||||||
<span class="is-sr-only">{{ text }}</span>
|
<span class="is-sr-only">{{ text }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
{% elif icon_with_text %}
|
||||||
|
<span class="icon icon-{{ icon_with_text }} m-0-mobile" title="{{ text }}"></span>
|
||||||
|
<span class="is-sr-only-mobile">{{ text }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span>{{ text }}</span>
|
<span>{{ text }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
{% spaceless %}
|
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% with 0|uuid as uuid %}
|
{% with 0|uuid as uuid %}
|
||||||
{% if full %}
|
{% if full %}
|
||||||
{% with full|to_markdown|safe as full %}
|
{% with full|to_markdown|safe as full %}
|
||||||
{% with full|to_markdown|safe|truncatewords_html:60 as trimmed %}
|
{% with full|to_markdown|safe|truncatewords_html:150 as trimmed %}
|
||||||
{% if not no_trim and trimmed != full %}
|
{% if not no_trim and trimmed != full %}
|
||||||
<div id="hide-full-{{ uuid }}">
|
<div id="hide-full-{{ uuid }}">
|
||||||
<div class="content" id="trimmed-{{ uuid }}">
|
<div class="content" id="trimmed-{{ uuid }}">
|
||||||
|
@ -46,4 +45,3 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endspaceless %}
|
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
{% extends 'layout.html' %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
|
|
||||||
{% block title %}{{ tag.name }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="block">
|
|
||||||
<h1 class="title">{% blocktrans %}Books tagged "{{ tag.name }}"{% endblocktrans %}</h1>
|
|
||||||
{% include 'snippets/book_tiles.html' with books=books.all %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
|
@ -68,10 +68,9 @@
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div>
|
<div>
|
||||||
{% if books|length > 0 %}
|
{% if books|length > 0 %}
|
||||||
<div class="scroll-x">
|
<table class="table is-striped is-fullwidth is-mobile">
|
||||||
<table class="table is-striped is-fullwidth">
|
<thead>
|
||||||
|
<tr>
|
||||||
<tr class="book-preview">
|
|
||||||
<th>{% trans "Cover" %}</th>
|
<th>{% trans "Cover" %}</th>
|
||||||
<th>{% trans "Title" %}</th>
|
<th>{% trans "Title" %}</th>
|
||||||
<th>{% trans "Author" %}</th>
|
<th>{% trans "Author" %}</th>
|
||||||
|
@ -83,34 +82,37 @@
|
||||||
<th aria-hidden="true"></th>
|
<th aria-hidden="true"></th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{% for book in books %}
|
{% for book in books %}
|
||||||
|
{% spaceless %}
|
||||||
<tr class="book-preview">
|
<tr class="book-preview">
|
||||||
<td>
|
<td class="book-preview-top-row">
|
||||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
|
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td data-title="{% trans "Title" %}">
|
||||||
<a href="{{ book.local_path }}">{{ book.title }}</a>
|
<a href="{{ book.local_path }}">{{ book.title }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td data-title="{% trans "Author" %}">
|
||||||
{% include 'snippets/authors.html' %}
|
{% include 'snippets/authors.html' %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td data-title="{% trans "Shelved" %}">
|
||||||
{{ book.created_date | naturalday }}
|
{{ book.created_date | naturalday }}
|
||||||
</td>
|
</td>
|
||||||
{% latest_read_through book user as read_through %}
|
{% latest_read_through book user as read_through %}
|
||||||
<td>
|
<td data-title="{% trans "Started" %}">
|
||||||
{{ read_through.start_date | naturalday |default_if_none:""}}
|
{{ read_through.start_date | naturalday |default_if_none:""}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td data-title="{% trans "Finished" %}">
|
||||||
{{ read_through.finish_date | naturalday |default_if_none:""}}
|
{{ read_through.finish_date | naturalday |default_if_none:""}}
|
||||||
</td>
|
</td>
|
||||||
{% if ratings %}
|
{% if ratings %}
|
||||||
<td>
|
<td data-title="{% trans "Rating" %}">
|
||||||
{% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %}
|
{% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if shelf.user == request.user %}
|
{% if shelf.user == request.user %}
|
||||||
<td>
|
<td class="book-preview-top-row has-text-right">
|
||||||
{% with right=True %}
|
{% with right=True %}
|
||||||
{% if not shelf.id %}
|
{% if not shelf.id %}
|
||||||
{% active_shelf book as current %}
|
{% active_shelf book as current %}
|
||||||
|
@ -122,9 +124,10 @@
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endspaceless %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{% trans "This shelf is empty." %}</p>
|
<p>{% trans "This shelf is empty." %}</p>
|
||||||
{% if shelf.id and shelf.editable %}
|
{% if shelf.id and shelf.editable %}
|
||||||
|
|
19
bookwyrm/templates/user_admin/user.html
Normal file
19
bookwyrm/templates/user_admin/user.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends 'settings/admin_layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
|
{% block title %}{{ user.username }}{% endblock %}
|
||||||
|
{% block header %}{{ user.username }}{% endblock %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
<div class="block">
|
||||||
|
<a href="{% url 'settings-users' %}">{% trans "Back to users" %}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include 'user_admin/user_info.html' with user=user %}
|
||||||
|
|
||||||
|
{% include 'user_admin/user_moderation_actions.html' with user=user %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
{% include 'settings/user_admin_filters.html' %}
|
{% include 'user_admin/user_admin_filters.html' %}
|
||||||
|
|
||||||
<table class="table is-striped">
|
<table class="table is-striped">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ user.username }}</td>
|
<td><a href="{% url 'settings-user' user.id %}">{{ user.username }}</a></td>
|
||||||
<td>{{ user.created_date }}</td>
|
<td>{{ user.created_date }}</td>
|
||||||
<td>{{ user.last_active_date }}</td>
|
<td>{{ user.last_active_date }}</td>
|
||||||
<td>{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}</td>
|
<td>{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}</td>
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||||
|
|
||||||
{% block filter_fields %}
|
{% block filter_fields %}
|
||||||
{% include 'settings/server_filter.html' %}
|
{% include 'user_admin/server_filter.html' %}
|
||||||
{% include 'settings/username_filter.html' %}
|
{% include 'user_admin/username_filter.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
56
bookwyrm/templates/user_admin/user_info.html
Normal file
56
bookwyrm/templates/user_admin/user_info.html
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
|
<div class="block columns">
|
||||||
|
<div class="column is-flex is-flex-direction-column">
|
||||||
|
<h4 class="title is-4">{% trans "User details" %}</h4>
|
||||||
|
<div class="box is-flex-grow-1">
|
||||||
|
{% include 'user/user_preview.html' with user=user %}
|
||||||
|
{% if user.summary %}
|
||||||
|
<div class="box content has-background-white-ter is-shadowless">
|
||||||
|
{{ user.summary | to_markdown | safe }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="mt-2"><a href="{{ user.local_path }}">{% trans "View user profile" %}</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if not user.local %}
|
||||||
|
{% with server=user.federated_server %}
|
||||||
|
<div class="column is-half is-flex is-flex-direction-column">
|
||||||
|
<h4 class="title is-4">{% trans "Instance details" %}</h4>
|
||||||
|
<div class="box content is-flex-grow-1">
|
||||||
|
{% if server %}
|
||||||
|
<h5>{{ server.server_name }}</h5>
|
||||||
|
<dl>
|
||||||
|
<div class="is-flex">
|
||||||
|
<dt>{% trans "Software:" %}</dt>
|
||||||
|
<dd>{{ server.application_type }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="is-flex">
|
||||||
|
<dt>{% trans "Version:" %}</dt>
|
||||||
|
<dd>{{ server.application_version }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="is-flex">
|
||||||
|
<dt>{% trans "Status:" %}</dt>
|
||||||
|
<dd>{{ server.status }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
{% if server.notes %}
|
||||||
|
<h5>{% trans "Notes" %}</h5>
|
||||||
|
<div class="box content has-background-white-ter is-shadowless">
|
||||||
|
{{ server.notes }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'settings-federated-server' server.id %}">{% trans "View instance" %}</a>
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<em>{% trans "Not set" %}</em>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
42
bookwyrm/templates/user_admin/user_moderation_actions.html
Normal file
42
bookwyrm/templates/user_admin/user_moderation_actions.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<div class="block content">
|
||||||
|
<h3>{% trans "Actions" %}</h3>
|
||||||
|
<div class="is-flex">
|
||||||
|
<p class="mr-1">
|
||||||
|
<a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a>
|
||||||
|
</p>
|
||||||
|
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if user.is_active %}
|
||||||
|
<button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button>
|
||||||
|
{% else %}
|
||||||
|
<button class="button">{% trans "Un-suspend user" %}</button>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% if user.local %}
|
||||||
|
<div>
|
||||||
|
<form name="permission" method="post" action="{% url 'settings-user' user.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<label class="label" for="id_user_group">{% trans "Access level:" %}</label>
|
||||||
|
{% if group_form.non_field_errors %}
|
||||||
|
{{ group_form.non_field_errors }}
|
||||||
|
{% endif %}
|
||||||
|
{% with group=user.groups.first %}
|
||||||
|
<div class="select">
|
||||||
|
<select name="groups" id="id_user_group">
|
||||||
|
{% for value, name in group_form.fields.groups.choices %}
|
||||||
|
<option value="{{ value }}" {% if name == group.name %}selected{% endif %}>{{ name|title }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
<option value="" {% if not group %}selected{% endif %}>User</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% for error in group_form.groups.errors %}
|
||||||
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
<button class="button">{% trans "Save" %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
|
@ -1,11 +1,8 @@
|
||||||
""" template filters """
|
""" template filters """
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from django import template, utils
|
||||||
from django import template
|
|
||||||
from django.db.models import Avg
|
from django.db.models import Avg
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from bookwyrm import models, views
|
from bookwyrm import models, views
|
||||||
from bookwyrm.views.status import to_markdown
|
from bookwyrm.views.status import to_markdown
|
||||||
|
@ -62,14 +59,10 @@ def get_notification_count(user):
|
||||||
def get_replies(status):
|
def get_replies(status):
|
||||||
"""get all direct replies to a status"""
|
"""get all direct replies to a status"""
|
||||||
# TODO: this limit could cause problems
|
# TODO: this limit could cause problems
|
||||||
return (
|
return models.Status.objects.filter(
|
||||||
models.Status.objects.filter(
|
|
||||||
reply_parent=status,
|
reply_parent=status,
|
||||||
deleted=False,
|
deleted=False,
|
||||||
)
|
).select_subclasses()[:10]
|
||||||
.select_subclasses()
|
|
||||||
.all()[:10]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="parent")
|
@register.filter(name="parent")
|
||||||
|
@ -133,28 +126,6 @@ def get_uuid(identifier):
|
||||||
return "%s%s" % (identifier, uuid4())
|
return "%s%s" % (identifier, uuid4())
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="post_date")
|
|
||||||
def time_since(date):
|
|
||||||
""" concise time ago function """
|
|
||||||
if not isinstance(date, datetime):
|
|
||||||
return ""
|
|
||||||
now = timezone.now()
|
|
||||||
|
|
||||||
if date < (now - relativedelta(weeks=1)):
|
|
||||||
formatter = "%b %-d"
|
|
||||||
if date.year != now.year:
|
|
||||||
formatter += " %Y"
|
|
||||||
return date.strftime(formatter)
|
|
||||||
delta = relativedelta(now, date)
|
|
||||||
if delta.days:
|
|
||||||
return "%dd" % delta.days
|
|
||||||
if delta.hours:
|
|
||||||
return "%dh" % delta.hours
|
|
||||||
if delta.minutes:
|
|
||||||
return "%dm" % delta.minutes
|
|
||||||
return "%ds" % delta.seconds
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="to_markdown")
|
@register.filter(name="to_markdown")
|
||||||
def get_markdown(content):
|
def get_markdown(content):
|
||||||
"""convert markdown to html"""
|
"""convert markdown to html"""
|
||||||
|
@ -197,6 +168,17 @@ def get_next_shelf(current_shelf):
|
||||||
return "to-read"
|
return "to-read"
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="title")
|
||||||
|
def get_title(book):
|
||||||
|
"""display the subtitle if the title is short"""
|
||||||
|
if not book:
|
||||||
|
return ""
|
||||||
|
title = book.title
|
||||||
|
if len(title) < 6 and book.subtitle:
|
||||||
|
title = "{:s}: {:s}".format(title, book.subtitle)
|
||||||
|
return title
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=False)
|
@register.simple_tag(takes_context=False)
|
||||||
def related_status(notification):
|
def related_status(notification):
|
||||||
"""for notifications"""
|
"""for notifications"""
|
||||||
|
@ -246,3 +228,10 @@ def active_read_through(book, user):
|
||||||
def comparison_bool(str1, str2):
|
def comparison_bool(str1, str2):
|
||||||
"""idk why I need to write a tag for this, it reutrns a bool"""
|
"""idk why I need to write a tag for this, it reutrns a bool"""
|
||||||
return str1 == str2
|
return str1 == str2
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=False)
|
||||||
|
def get_lang():
|
||||||
|
"""get current language, strip to the first two letters"""
|
||||||
|
language = utils.translation.get_language()
|
||||||
|
return language[0 : language.find("-")]
|
||||||
|
|
39
bookwyrm/tests/data/ap_user_rat.json
Normal file
39
bookwyrm/tests/data/ap_user_rat.json
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
{
|
||||||
|
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||||
|
"schema": "http://schema.org#",
|
||||||
|
"PropertyValue": "schema:PropertyValue",
|
||||||
|
"value": "schema:value"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "https://example.com/users/rat",
|
||||||
|
"type": "Person",
|
||||||
|
"preferredUsername": "rat",
|
||||||
|
"name": "RAT???",
|
||||||
|
"inbox": "https://example.com/users/rat/inbox",
|
||||||
|
"outbox": "https://example.com/users/rat/outbox",
|
||||||
|
"followers": "https://example.com/users/rat/followers",
|
||||||
|
"following": "https://example.com/users/rat/following",
|
||||||
|
"summary": "",
|
||||||
|
"publicKey": {
|
||||||
|
"id": "https://example.com/users/rat/#main-key",
|
||||||
|
"owner": "https://example.com/users/rat",
|
||||||
|
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6QisDrjOQvkRo/MqNmSYPwqtt\nCxg/8rCW+9jKbFUKvqjTeKVotEE85122v/DCvobCCdfQuYIFdVMk+dB1xJ0iPGPg\nyU79QHY22NdV9mFKA2qtXVVxb5cxpA4PlwOHM6PM/k8B+H09OUrop2aPUAYwy+vg\n+MXyz8bAXrIS1kq6fQIDAQAB\n-----END PUBLIC KEY-----"
|
||||||
|
},
|
||||||
|
"endpoints": {
|
||||||
|
"sharedInbox": "https://example.com/inbox"
|
||||||
|
},
|
||||||
|
"bookwyrmUser": true,
|
||||||
|
"manuallyApprovesFollowers": false,
|
||||||
|
"discoverable": true,
|
||||||
|
"devices": "https://friend.camp/users/tripofmice/collections/devices",
|
||||||
|
"tag": [],
|
||||||
|
"icon": {
|
||||||
|
"type": "Image",
|
||||||
|
"mediaType": "image/png",
|
||||||
|
"url": "https://example.com/images/avatars/AL-2-crop-50.png"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"id": "https://bookwyrm.social/book/5989",
|
"id": "https://bookwyrm.social/book/5989",
|
||||||
|
"lastEditedBy": "https://example.com/users/rat",
|
||||||
"type": "Edition",
|
"type": "Edition",
|
||||||
"authors": [
|
"authors": [
|
||||||
"https://bookwyrm.social/author/417"
|
"https://bookwyrm.social/author/417"
|
||||||
|
|
|
@ -155,8 +155,8 @@ class ActivitypubMixins(TestCase):
|
||||||
|
|
||||||
recipients = ActivitypubMixin.get_recipients(mock_self)
|
recipients = ActivitypubMixin.get_recipients(mock_self)
|
||||||
self.assertEqual(len(recipients), 2)
|
self.assertEqual(len(recipients), 2)
|
||||||
self.assertEqual(recipients[0], another_remote_user.inbox)
|
self.assertTrue(another_remote_user.inbox in recipients)
|
||||||
self.assertEqual(recipients[1], self.remote_user.inbox)
|
self.assertTrue(self.remote_user.inbox in recipients)
|
||||||
|
|
||||||
def test_get_recipients_direct(self, _):
|
def test_get_recipients_direct(self, _):
|
||||||
"""determines the recipients for a user's object broadcast"""
|
"""determines the recipients for a user's object broadcast"""
|
||||||
|
|
|
@ -51,6 +51,7 @@ class List(TestCase):
|
||||||
book_list=book_list,
|
book_list=book_list,
|
||||||
book=self.book,
|
book=self.book,
|
||||||
user=self.local_user,
|
user=self.local_user,
|
||||||
|
order=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(item.approved)
|
self.assertTrue(item.approved)
|
||||||
|
@ -65,7 +66,11 @@ class List(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
item = models.ListItem.objects.create(
|
item = models.ListItem.objects.create(
|
||||||
book_list=book_list, book=self.book, user=self.local_user, approved=False
|
book_list=book_list,
|
||||||
|
book=self.book,
|
||||||
|
user=self.local_user,
|
||||||
|
approved=False,
|
||||||
|
order=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertFalse(item.approved)
|
self.assertFalse(item.approved)
|
||||||
|
|
|
@ -269,7 +269,7 @@ class Status(TestCase):
|
||||||
def test_review_to_pure_activity(self, *_):
|
def test_review_to_pure_activity(self, *_):
|
||||||
"""subclass of the base model version with a "pure" serializer"""
|
"""subclass of the base model version with a "pure" serializer"""
|
||||||
status = models.Review.objects.create(
|
status = models.Review.objects.create(
|
||||||
name="Review name",
|
name="Review's name",
|
||||||
content="test content",
|
content="test content",
|
||||||
rating=3.0,
|
rating=3.0,
|
||||||
user=self.local_user,
|
user=self.local_user,
|
||||||
|
@ -280,7 +280,7 @@ class Status(TestCase):
|
||||||
self.assertEqual(activity["type"], "Article")
|
self.assertEqual(activity["type"], "Article")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
activity["name"],
|
activity["name"],
|
||||||
'Review of "%s" (3 stars): Review name' % self.book.title,
|
'Review of "%s" (3 stars): Review\'s name' % self.book.title,
|
||||||
)
|
)
|
||||||
self.assertEqual(activity["content"], "test content")
|
self.assertEqual(activity["content"], "test content")
|
||||||
self.assertEqual(activity["attachment"][0].type, "Document")
|
self.assertEqual(activity["attachment"][0].type, "Document")
|
||||||
|
|
|
@ -181,36 +181,6 @@ class TemplateTags(TestCase):
|
||||||
uuid = bookwyrm_tags.get_uuid("hi")
|
uuid = bookwyrm_tags.get_uuid("hi")
|
||||||
self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid))
|
self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid))
|
||||||
|
|
||||||
def test_time_since(self, _):
|
|
||||||
""" ultraconcise timestamps """
|
|
||||||
self.assertEqual(bookwyrm_tags.time_since("bleh"), "")
|
|
||||||
|
|
||||||
now = timezone.now()
|
|
||||||
self.assertEqual(bookwyrm_tags.time_since(now), "0s")
|
|
||||||
|
|
||||||
seconds_ago = now - relativedelta(seconds=4)
|
|
||||||
self.assertEqual(bookwyrm_tags.time_since(seconds_ago), "4s")
|
|
||||||
|
|
||||||
minutes_ago = now - relativedelta(minutes=8)
|
|
||||||
self.assertEqual(bookwyrm_tags.time_since(minutes_ago), "8m")
|
|
||||||
|
|
||||||
hours_ago = now - relativedelta(hours=9)
|
|
||||||
self.assertEqual(bookwyrm_tags.time_since(hours_ago), "9h")
|
|
||||||
|
|
||||||
days_ago = now - relativedelta(days=3)
|
|
||||||
self.assertEqual(bookwyrm_tags.time_since(days_ago), "3d")
|
|
||||||
|
|
||||||
# I am not going to figure out how to mock dates tonight.
|
|
||||||
months_ago = now - relativedelta(months=5)
|
|
||||||
self.assertTrue(
|
|
||||||
re.match(r"[A-Z][a-z]{2} \d?\d", bookwyrm_tags.time_since(months_ago))
|
|
||||||
)
|
|
||||||
|
|
||||||
years_ago = now - relativedelta(years=10)
|
|
||||||
self.assertTrue(
|
|
||||||
re.match(r"[A-Z][a-z]{2} \d?\d \d{4}", bookwyrm_tags.time_since(years_ago))
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_markdown(self, _):
|
def test_get_markdown(self, _):
|
||||||
"""mardown format data"""
|
"""mardown format data"""
|
||||||
result = bookwyrm_tags.get_markdown("_hi_")
|
result = bookwyrm_tags.get_markdown("_hi_")
|
||||||
|
|
|
@ -94,6 +94,7 @@ class InboxAdd(TestCase):
|
||||||
"type": "ListItem",
|
"type": "ListItem",
|
||||||
"book": self.book.remote_id,
|
"book": self.book.remote_id,
|
||||||
"id": "https://bookwyrm.social/listbook/6189",
|
"id": "https://bookwyrm.social/listbook/6189",
|
||||||
|
"order": 1,
|
||||||
},
|
},
|
||||||
"target": "https://bookwyrm.social/user/mouse/list/to-read",
|
"target": "https://bookwyrm.social/user/mouse/list/to-read",
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
|
|
@ -51,7 +51,7 @@ class InboxActivities(TestCase):
|
||||||
models.SiteSettings.objects.create()
|
models.SiteSettings.objects.create()
|
||||||
|
|
||||||
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
|
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
|
||||||
def test_handle_boost(self, _):
|
def test_boost(self, redis_mock):
|
||||||
"""boost a status"""
|
"""boost a status"""
|
||||||
self.assertEqual(models.Notification.objects.count(), 0)
|
self.assertEqual(models.Notification.objects.count(), 0)
|
||||||
activity = {
|
activity = {
|
||||||
|
@ -66,16 +66,23 @@ class InboxActivities(TestCase):
|
||||||
with patch("bookwyrm.models.status.Status.ignore_activity") as discarder:
|
with patch("bookwyrm.models.status.Status.ignore_activity") as discarder:
|
||||||
discarder.return_value = False
|
discarder.return_value = False
|
||||||
views.inbox.activity_task(activity)
|
views.inbox.activity_task(activity)
|
||||||
|
|
||||||
|
# boost added to redis activitystreams
|
||||||
|
self.assertTrue(redis_mock.called)
|
||||||
|
|
||||||
|
# boost created of correct status
|
||||||
boost = models.Boost.objects.get()
|
boost = models.Boost.objects.get()
|
||||||
self.assertEqual(boost.boosted_status, self.status)
|
self.assertEqual(boost.boosted_status, self.status)
|
||||||
|
|
||||||
|
# notification sent to original poster
|
||||||
notification = models.Notification.objects.get()
|
notification = models.Notification.objects.get()
|
||||||
self.assertEqual(notification.user, self.local_user)
|
self.assertEqual(notification.user, self.local_user)
|
||||||
self.assertEqual(notification.related_status, self.status)
|
self.assertEqual(notification.related_status, self.status)
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
|
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
|
||||||
def test_handle_boost_remote_status(self, redis_mock):
|
def test_boost_remote_status(self, redis_mock):
|
||||||
""" boost a status """
|
"""boost a status from a remote server"""
|
||||||
work = models.Work.objects.create(title="work title")
|
work = models.Work.objects.create(title="work title")
|
||||||
book = models.Edition.objects.create(
|
book = models.Edition.objects.create(
|
||||||
title="Test",
|
title="Test",
|
||||||
|
@ -123,7 +130,7 @@ class InboxActivities(TestCase):
|
||||||
self.assertEqual(boost.boosted_status.comment.book, book)
|
self.assertEqual(boost.boosted_status.comment.book, book)
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_handle_discarded_boost(self):
|
def test_discarded_boost(self):
|
||||||
"""test a boost of a mastodon status that will be discarded"""
|
"""test a boost of a mastodon status that will be discarded"""
|
||||||
status = models.Status(
|
status = models.Status(
|
||||||
content="hi",
|
content="hi",
|
||||||
|
@ -146,7 +153,7 @@ class InboxActivities(TestCase):
|
||||||
views.inbox.activity_task(activity)
|
views.inbox.activity_task(activity)
|
||||||
self.assertEqual(models.Boost.objects.count(), 0)
|
self.assertEqual(models.Boost.objects.count(), 0)
|
||||||
|
|
||||||
def test_handle_unboost(self):
|
def test_unboost(self):
|
||||||
"""undo a boost"""
|
"""undo a boost"""
|
||||||
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
|
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
|
||||||
boost = models.Boost.objects.create(
|
boost = models.Boost.objects.create(
|
||||||
|
@ -175,7 +182,7 @@ class InboxActivities(TestCase):
|
||||||
self.assertTrue(redis_mock.called)
|
self.assertTrue(redis_mock.called)
|
||||||
self.assertFalse(models.Boost.objects.exists())
|
self.assertFalse(models.Boost.objects.exists())
|
||||||
|
|
||||||
def test_handle_unboost_unknown_boost(self):
|
def test_unboost_unknown_boost(self):
|
||||||
"""undo a boost"""
|
"""undo a boost"""
|
||||||
activity = {
|
activity = {
|
||||||
"type": "Undo",
|
"type": "Undo",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
""" tests incoming activities"""
|
""" tests incoming activities"""
|
||||||
|
import json
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
@ -34,7 +35,7 @@ class InboxRelationships(TestCase):
|
||||||
|
|
||||||
models.SiteSettings.objects.create()
|
models.SiteSettings.objects.create()
|
||||||
|
|
||||||
def test_handle_follow(self):
|
def test_follow(self):
|
||||||
"""remote user wants to follow local user"""
|
"""remote user wants to follow local user"""
|
||||||
activity = {
|
activity = {
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
@ -48,6 +49,8 @@ class InboxRelationships(TestCase):
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
|
||||||
views.inbox.activity_task(activity)
|
views.inbox.activity_task(activity)
|
||||||
self.assertEqual(mock.call_count, 1)
|
self.assertEqual(mock.call_count, 1)
|
||||||
|
response_activity = json.loads(mock.call_args[0][1])
|
||||||
|
self.assertEqual(response_activity["type"], "Accept")
|
||||||
|
|
||||||
# notification created
|
# notification created
|
||||||
notification = models.Notification.objects.get()
|
notification = models.Notification.objects.get()
|
||||||
|
@ -61,7 +64,34 @@ class InboxRelationships(TestCase):
|
||||||
follow = models.UserFollows.objects.get(user_object=self.local_user)
|
follow = models.UserFollows.objects.get(user_object=self.local_user)
|
||||||
self.assertEqual(follow.user_subject, self.remote_user)
|
self.assertEqual(follow.user_subject, self.remote_user)
|
||||||
|
|
||||||
def test_handle_follow_manually_approved(self):
|
def test_follow_duplicate(self):
|
||||||
|
"""remote user wants to follow local user twice"""
|
||||||
|
activity = {
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id": "https://example.com/users/rat/follows/123",
|
||||||
|
"type": "Follow",
|
||||||
|
"actor": "https://example.com/users/rat",
|
||||||
|
"object": "https://example.com/user/mouse",
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
views.inbox.activity_task(activity)
|
||||||
|
|
||||||
|
# the follow relationship should exist
|
||||||
|
follow = models.UserFollows.objects.get(user_object=self.local_user)
|
||||||
|
self.assertEqual(follow.user_subject, self.remote_user)
|
||||||
|
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
|
||||||
|
views.inbox.activity_task(activity)
|
||||||
|
self.assertEqual(mock.call_count, 1)
|
||||||
|
response_activity = json.loads(mock.call_args[0][1])
|
||||||
|
self.assertEqual(response_activity["type"], "Accept")
|
||||||
|
|
||||||
|
# the follow relationship should STILL exist
|
||||||
|
follow = models.UserFollows.objects.get(user_object=self.local_user)
|
||||||
|
self.assertEqual(follow.user_subject, self.remote_user)
|
||||||
|
|
||||||
|
def test_follow_manually_approved(self):
|
||||||
"""needs approval before following"""
|
"""needs approval before following"""
|
||||||
activity = {
|
activity = {
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
@ -91,7 +121,7 @@ class InboxRelationships(TestCase):
|
||||||
follow = models.UserFollows.objects.all()
|
follow = models.UserFollows.objects.all()
|
||||||
self.assertEqual(list(follow), [])
|
self.assertEqual(list(follow), [])
|
||||||
|
|
||||||
def test_handle_undo_follow_request(self):
|
def test_undo_follow_request(self):
|
||||||
"""the requester cancels a follow request"""
|
"""the requester cancels a follow request"""
|
||||||
self.local_user.manually_approves_followers = True
|
self.local_user.manually_approves_followers = True
|
||||||
self.local_user.save(broadcast=False)
|
self.local_user.save(broadcast=False)
|
||||||
|
@ -121,7 +151,7 @@ class InboxRelationships(TestCase):
|
||||||
|
|
||||||
self.assertFalse(self.local_user.follower_requests.exists())
|
self.assertFalse(self.local_user.follower_requests.exists())
|
||||||
|
|
||||||
def test_handle_unfollow(self):
|
def test_unfollow(self):
|
||||||
"""remove a relationship"""
|
"""remove a relationship"""
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
rel = models.UserFollows.objects.create(
|
rel = models.UserFollows.objects.create(
|
||||||
|
@ -146,7 +176,7 @@ class InboxRelationships(TestCase):
|
||||||
views.inbox.activity_task(activity)
|
views.inbox.activity_task(activity)
|
||||||
self.assertIsNone(self.local_user.followers.first())
|
self.assertIsNone(self.local_user.followers.first())
|
||||||
|
|
||||||
def test_handle_follow_accept(self):
|
def test_follow_accept(self):
|
||||||
"""a remote user approved a follow request from local"""
|
"""a remote user approved a follow request from local"""
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
rel = models.UserFollowRequest.objects.create(
|
rel = models.UserFollowRequest.objects.create(
|
||||||
|
@ -177,7 +207,7 @@ class InboxRelationships(TestCase):
|
||||||
self.assertEqual(follows.count(), 1)
|
self.assertEqual(follows.count(), 1)
|
||||||
self.assertEqual(follows.first(), self.local_user)
|
self.assertEqual(follows.first(), self.local_user)
|
||||||
|
|
||||||
def test_handle_follow_reject(self):
|
def test_follow_reject(self):
|
||||||
"""turn down a follow request"""
|
"""turn down a follow request"""
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
rel = models.UserFollowRequest.objects.create(
|
rel = models.UserFollowRequest.objects.create(
|
||||||
|
|
|
@ -80,6 +80,7 @@ class InboxRemove(TestCase):
|
||||||
user=self.local_user,
|
user=self.local_user,
|
||||||
book=self.book,
|
book=self.book,
|
||||||
book_list=booklist,
|
book_list=booklist,
|
||||||
|
order=1,
|
||||||
)
|
)
|
||||||
self.assertEqual(booklist.books.count(), 1)
|
self.assertEqual(booklist.books.count(), 1)
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,16 @@ class InboxUpdate(TestCase):
|
||||||
)
|
)
|
||||||
self.local_user.remote_id = "https://example.com/user/mouse"
|
self.local_user.remote_id = "https://example.com/user/mouse"
|
||||||
self.local_user.save(broadcast=False)
|
self.local_user.save(broadcast=False)
|
||||||
|
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.create_json = {
|
self.create_json = {
|
||||||
"id": "hi",
|
"id": "hi",
|
||||||
|
@ -34,7 +44,7 @@ class InboxUpdate(TestCase):
|
||||||
}
|
}
|
||||||
models.SiteSettings.objects.create()
|
models.SiteSettings.objects.create()
|
||||||
|
|
||||||
def test_handle_update_list(self):
|
def test_update_list(self):
|
||||||
"""a new list"""
|
"""a new list"""
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
book_list = models.List.objects.create(
|
book_list = models.List.objects.create(
|
||||||
|
@ -68,16 +78,24 @@ class InboxUpdate(TestCase):
|
||||||
self.assertEqual(book_list.description, "summary text")
|
self.assertEqual(book_list.description, "summary text")
|
||||||
self.assertEqual(book_list.remote_id, "https://example.com/list/22")
|
self.assertEqual(book_list.remote_id, "https://example.com/list/22")
|
||||||
|
|
||||||
def test_handle_update_user(self):
|
def test_update_user(self):
|
||||||
"""update an existing user"""
|
"""update an existing user"""
|
||||||
# we only do this with remote users
|
models.UserFollows.objects.create(
|
||||||
self.local_user.local = False
|
user_subject=self.local_user,
|
||||||
self.local_user.save()
|
user_object=self.remote_user,
|
||||||
|
)
|
||||||
|
models.UserFollows.objects.create(
|
||||||
|
user_subject=self.remote_user,
|
||||||
|
user_object=self.local_user,
|
||||||
|
)
|
||||||
|
self.assertTrue(self.remote_user in self.local_user.followers.all())
|
||||||
|
self.assertTrue(self.local_user in self.remote_user.followers.all())
|
||||||
|
|
||||||
datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_user.json")
|
datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_user_rat.json")
|
||||||
userdata = json.loads(datafile.read_bytes())
|
userdata = json.loads(datafile.read_bytes())
|
||||||
del userdata["icon"]
|
del userdata["icon"]
|
||||||
self.assertIsNone(self.local_user.name)
|
self.assertIsNone(self.remote_user.name)
|
||||||
|
self.assertFalse(self.remote_user.discoverable)
|
||||||
views.inbox.activity_task(
|
views.inbox.activity_task(
|
||||||
{
|
{
|
||||||
"type": "Update",
|
"type": "Update",
|
||||||
|
@ -88,13 +106,16 @@ class InboxUpdate(TestCase):
|
||||||
"object": userdata,
|
"object": userdata,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
user = models.User.objects.get(id=self.local_user.id)
|
user = models.User.objects.get(id=self.remote_user.id)
|
||||||
self.assertEqual(user.name, "MOUSE?? MOUSE!!")
|
self.assertEqual(user.name, "RAT???")
|
||||||
self.assertEqual(user.username, "mouse@example.com")
|
self.assertEqual(user.username, "rat@example.com")
|
||||||
self.assertEqual(user.localname, "mouse")
|
|
||||||
self.assertTrue(user.discoverable)
|
self.assertTrue(user.discoverable)
|
||||||
|
|
||||||
def test_handle_update_edition(self):
|
# make sure relationships aren't disrupted
|
||||||
|
self.assertTrue(self.remote_user in self.local_user.followers.all())
|
||||||
|
self.assertTrue(self.local_user in self.remote_user.followers.all())
|
||||||
|
|
||||||
|
def test_update_edition(self):
|
||||||
"""update an existing edition"""
|
"""update an existing edition"""
|
||||||
datafile = pathlib.Path(__file__).parent.joinpath("../../data/bw_edition.json")
|
datafile = pathlib.Path(__file__).parent.joinpath("../../data/bw_edition.json")
|
||||||
bookdata = json.loads(datafile.read_bytes())
|
bookdata = json.loads(datafile.read_bytes())
|
||||||
|
@ -122,8 +143,9 @@ class InboxUpdate(TestCase):
|
||||||
)
|
)
|
||||||
book = models.Edition.objects.get(id=book.id)
|
book = models.Edition.objects.get(id=book.id)
|
||||||
self.assertEqual(book.title, "Piranesi")
|
self.assertEqual(book.title, "Piranesi")
|
||||||
|
self.assertEqual(book.last_edited_by, self.remote_user)
|
||||||
|
|
||||||
def test_handle_update_work(self):
|
def test_update_work(self):
|
||||||
"""update an existing edition"""
|
"""update an existing edition"""
|
||||||
datafile = pathlib.Path(__file__).parent.joinpath("../../data/bw_work.json")
|
datafile = pathlib.Path(__file__).parent.joinpath("../../data/bw_work.json")
|
||||||
bookdata = json.loads(datafile.read_bytes())
|
bookdata = json.loads(datafile.read_bytes())
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
""" test for app action functionality """
|
""" test for app action functionality """
|
||||||
|
import json
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
@ -39,7 +40,7 @@ class InteractionViews(TestCase):
|
||||||
parent_work=work,
|
parent_work=work,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_handle_favorite(self, _):
|
def test_favorite(self, _):
|
||||||
"""create and broadcast faving a status"""
|
"""create and broadcast faving a status"""
|
||||||
view = views.Favorite.as_view()
|
view = views.Favorite.as_view()
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
|
@ -57,7 +58,7 @@ class InteractionViews(TestCase):
|
||||||
self.assertEqual(notification.user, self.local_user)
|
self.assertEqual(notification.user, self.local_user)
|
||||||
self.assertEqual(notification.related_user, self.remote_user)
|
self.assertEqual(notification.related_user, self.remote_user)
|
||||||
|
|
||||||
def test_handle_unfavorite(self, _):
|
def test_unfavorite(self, _):
|
||||||
"""unfav a status"""
|
"""unfav a status"""
|
||||||
view = views.Unfavorite.as_view()
|
view = views.Unfavorite.as_view()
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
|
@ -74,7 +75,7 @@ class InteractionViews(TestCase):
|
||||||
self.assertEqual(models.Favorite.objects.count(), 0)
|
self.assertEqual(models.Favorite.objects.count(), 0)
|
||||||
self.assertEqual(models.Notification.objects.count(), 0)
|
self.assertEqual(models.Notification.objects.count(), 0)
|
||||||
|
|
||||||
def test_handle_boost(self, _):
|
def test_boost(self, _):
|
||||||
"""boost a status"""
|
"""boost a status"""
|
||||||
view = views.Boost.as_view()
|
view = views.Boost.as_view()
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
|
@ -85,6 +86,7 @@ class InteractionViews(TestCase):
|
||||||
view(request, status.id)
|
view(request, status.id)
|
||||||
|
|
||||||
boost = models.Boost.objects.get()
|
boost = models.Boost.objects.get()
|
||||||
|
|
||||||
self.assertEqual(boost.boosted_status, status)
|
self.assertEqual(boost.boosted_status, status)
|
||||||
self.assertEqual(boost.user, self.remote_user)
|
self.assertEqual(boost.user, self.remote_user)
|
||||||
self.assertEqual(boost.privacy, "public")
|
self.assertEqual(boost.privacy, "public")
|
||||||
|
@ -95,7 +97,7 @@ class InteractionViews(TestCase):
|
||||||
self.assertEqual(notification.related_user, self.remote_user)
|
self.assertEqual(notification.related_user, self.remote_user)
|
||||||
self.assertEqual(notification.related_status, status)
|
self.assertEqual(notification.related_status, status)
|
||||||
|
|
||||||
def test_handle_self_boost(self, _):
|
def test_self_boost(self, _):
|
||||||
"""boost your own status"""
|
"""boost your own status"""
|
||||||
view = views.Boost.as_view()
|
view = views.Boost.as_view()
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
|
@ -103,8 +105,15 @@ class InteractionViews(TestCase):
|
||||||
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
|
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
|
||||||
status = models.Status.objects.create(user=self.local_user, content="hi")
|
status = models.Status.objects.create(user=self.local_user, content="hi")
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"bookwyrm.models.activitypub_mixin.broadcast_task.delay"
|
||||||
|
) as broadcast_mock:
|
||||||
view(request, status.id)
|
view(request, status.id)
|
||||||
|
|
||||||
|
self.assertEqual(broadcast_mock.call_count, 1)
|
||||||
|
activity = json.loads(broadcast_mock.call_args[0][1])
|
||||||
|
self.assertEqual(activity["type"], "Announce")
|
||||||
|
|
||||||
boost = models.Boost.objects.get()
|
boost = models.Boost.objects.get()
|
||||||
self.assertEqual(boost.boosted_status, status)
|
self.assertEqual(boost.boosted_status, status)
|
||||||
self.assertEqual(boost.user, self.local_user)
|
self.assertEqual(boost.user, self.local_user)
|
||||||
|
@ -112,7 +121,7 @@ class InteractionViews(TestCase):
|
||||||
|
|
||||||
self.assertFalse(models.Notification.objects.exists())
|
self.assertFalse(models.Notification.objects.exists())
|
||||||
|
|
||||||
def test_handle_boost_unlisted(self, _):
|
def test_boost_unlisted(self, _):
|
||||||
"""boost a status"""
|
"""boost a status"""
|
||||||
view = views.Boost.as_view()
|
view = views.Boost.as_view()
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
|
@ -127,7 +136,7 @@ class InteractionViews(TestCase):
|
||||||
boost = models.Boost.objects.get()
|
boost = models.Boost.objects.get()
|
||||||
self.assertEqual(boost.privacy, "unlisted")
|
self.assertEqual(boost.privacy, "unlisted")
|
||||||
|
|
||||||
def test_handle_boost_private(self, _):
|
def test_boost_private(self, _):
|
||||||
"""boost a status"""
|
"""boost a status"""
|
||||||
view = views.Boost.as_view()
|
view = views.Boost.as_view()
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
|
@ -140,7 +149,7 @@ class InteractionViews(TestCase):
|
||||||
view(request, status.id)
|
view(request, status.id)
|
||||||
self.assertFalse(models.Boost.objects.exists())
|
self.assertFalse(models.Boost.objects.exists())
|
||||||
|
|
||||||
def test_handle_boost_twice(self, _):
|
def test_boost_twice(self, _):
|
||||||
"""boost a status"""
|
"""boost a status"""
|
||||||
view = views.Boost.as_view()
|
view = views.Boost.as_view()
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
|
@ -152,7 +161,7 @@ class InteractionViews(TestCase):
|
||||||
view(request, status.id)
|
view(request, status.id)
|
||||||
self.assertEqual(models.Boost.objects.count(), 1)
|
self.assertEqual(models.Boost.objects.count(), 1)
|
||||||
|
|
||||||
def test_handle_unboost(self, _):
|
def test_unboost(self, _):
|
||||||
"""undo a boost"""
|
"""undo a boost"""
|
||||||
view = views.Unboost.as_view()
|
view = views.Unboost.as_view()
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
|
|
|
@ -39,6 +39,25 @@ class ListViews(TestCase):
|
||||||
remote_id="https://example.com/book/1",
|
remote_id="https://example.com/book/1",
|
||||||
parent_work=work,
|
parent_work=work,
|
||||||
)
|
)
|
||||||
|
work_two = models.Work.objects.create(title="Labori")
|
||||||
|
self.book_two = models.Edition.objects.create(
|
||||||
|
title="Example Edition 2",
|
||||||
|
remote_id="https://example.com/book/2",
|
||||||
|
parent_work=work_two,
|
||||||
|
)
|
||||||
|
work_three = models.Work.objects.create(title="Trabajar")
|
||||||
|
self.book_three = models.Edition.objects.create(
|
||||||
|
title="Example Edition 3",
|
||||||
|
remote_id="https://example.com/book/3",
|
||||||
|
parent_work=work_three,
|
||||||
|
)
|
||||||
|
work_four = models.Work.objects.create(title="Travailler")
|
||||||
|
self.book_four = models.Edition.objects.create(
|
||||||
|
title="Example Edition 4",
|
||||||
|
remote_id="https://example.com/book/4",
|
||||||
|
parent_work=work_four,
|
||||||
|
)
|
||||||
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
self.list = models.List.objects.create(
|
self.list = models.List.objects.create(
|
||||||
name="Test List", user=self.local_user
|
name="Test List", user=self.local_user
|
||||||
|
@ -194,6 +213,7 @@ class ListViews(TestCase):
|
||||||
user=self.local_user,
|
user=self.local_user,
|
||||||
book=self.book,
|
book=self.book,
|
||||||
approved=False,
|
approved=False,
|
||||||
|
order=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
request = self.factory.post(
|
request = self.factory.post(
|
||||||
|
@ -208,7 +228,7 @@ class ListViews(TestCase):
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
|
||||||
view(request, self.list.id)
|
view(request, self.list.id)
|
||||||
|
|
||||||
self.assertEqual(mock.call_count, 1)
|
self.assertEqual(mock.call_count, 2)
|
||||||
activity = json.loads(mock.call_args[0][1])
|
activity = json.loads(mock.call_args[0][1])
|
||||||
self.assertEqual(activity["type"], "Add")
|
self.assertEqual(activity["type"], "Add")
|
||||||
self.assertEqual(activity["actor"], self.local_user.remote_id)
|
self.assertEqual(activity["actor"], self.local_user.remote_id)
|
||||||
|
@ -228,6 +248,7 @@ class ListViews(TestCase):
|
||||||
user=self.local_user,
|
user=self.local_user,
|
||||||
book=self.book,
|
book=self.book,
|
||||||
approved=False,
|
approved=False,
|
||||||
|
order=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
request = self.factory.post(
|
request = self.factory.post(
|
||||||
|
@ -268,6 +289,261 @@ class ListViews(TestCase):
|
||||||
self.assertEqual(item.user, self.local_user)
|
self.assertEqual(item.user, self.local_user)
|
||||||
self.assertTrue(item.approved)
|
self.assertTrue(item.approved)
|
||||||
|
|
||||||
|
def test_add_two_books(self):
|
||||||
|
"""
|
||||||
|
Putting two books on the list. The first should have an order value of
|
||||||
|
1 and the second should have an order value of 2.
|
||||||
|
"""
|
||||||
|
request_one = self.factory.post(
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
"book": self.book.id,
|
||||||
|
"list": self.list.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
request_one.user = self.local_user
|
||||||
|
|
||||||
|
request_two = self.factory.post(
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
"book": self.book_two.id,
|
||||||
|
"list": self.list.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
request_two.user = self.local_user
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
views.list.add_book(request_one)
|
||||||
|
views.list.add_book(request_two)
|
||||||
|
|
||||||
|
items = self.list.listitem_set.order_by("order").all()
|
||||||
|
self.assertEqual(items[0].book, self.book)
|
||||||
|
self.assertEqual(items[1].book, self.book_two)
|
||||||
|
self.assertEqual(items[0].order, 1)
|
||||||
|
self.assertEqual(items[1].order, 2)
|
||||||
|
|
||||||
|
def test_add_three_books_and_remove_second(self):
|
||||||
|
"""
|
||||||
|
Put three books on a list and then remove the one in the middle. The
|
||||||
|
ordering of the list should adjust to not have a gap.
|
||||||
|
"""
|
||||||
|
request_one = self.factory.post(
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
"book": self.book.id,
|
||||||
|
"list": self.list.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
request_one.user = self.local_user
|
||||||
|
|
||||||
|
request_two = self.factory.post(
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
"book": self.book_two.id,
|
||||||
|
"list": self.list.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
request_two.user = self.local_user
|
||||||
|
|
||||||
|
request_three = self.factory.post(
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
"book": self.book_three.id,
|
||||||
|
"list": self.list.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
request_three.user = self.local_user
|
||||||
|
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
views.list.add_book(request_one)
|
||||||
|
views.list.add_book(request_two)
|
||||||
|
views.list.add_book(request_three)
|
||||||
|
|
||||||
|
items = self.list.listitem_set.order_by("order").all()
|
||||||
|
self.assertEqual(items[0].book, self.book)
|
||||||
|
self.assertEqual(items[1].book, self.book_two)
|
||||||
|
self.assertEqual(items[2].book, self.book_three)
|
||||||
|
self.assertEqual(items[0].order, 1)
|
||||||
|
self.assertEqual(items[1].order, 2)
|
||||||
|
self.assertEqual(items[2].order, 3)
|
||||||
|
|
||||||
|
remove_request = self.factory.post("", {"item": items[1].id})
|
||||||
|
remove_request.user = self.local_user
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
views.list.remove_book(remove_request, self.list.id)
|
||||||
|
items = self.list.listitem_set.order_by("order").all()
|
||||||
|
self.assertEqual(items[0].book, self.book)
|
||||||
|
self.assertEqual(items[1].book, self.book_three)
|
||||||
|
self.assertEqual(items[0].order, 1)
|
||||||
|
self.assertEqual(items[1].order, 2)
|
||||||
|
|
||||||
|
def test_adding_book_with_a_pending_book(self):
|
||||||
|
"""
|
||||||
|
When a list contains any pending books, the pending books should have
|
||||||
|
be at the end of the list by order. If a book is added while a book is
|
||||||
|
pending, its order should precede the pending books.
|
||||||
|
"""
|
||||||
|
request = self.factory.post(
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
"book": self.book_three.id,
|
||||||
|
"list": self.list.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
models.ListItem.objects.create(
|
||||||
|
book_list=self.list,
|
||||||
|
user=self.local_user,
|
||||||
|
book=self.book,
|
||||||
|
approved=True,
|
||||||
|
order=1,
|
||||||
|
)
|
||||||
|
models.ListItem.objects.create(
|
||||||
|
book_list=self.list,
|
||||||
|
user=self.rat,
|
||||||
|
book=self.book_two,
|
||||||
|
approved=False,
|
||||||
|
order=2,
|
||||||
|
)
|
||||||
|
views.list.add_book(request)
|
||||||
|
|
||||||
|
items = self.list.listitem_set.order_by("order").all()
|
||||||
|
self.assertEqual(items[0].book, self.book)
|
||||||
|
self.assertEqual(items[0].order, 1)
|
||||||
|
self.assertTrue(items[0].approved)
|
||||||
|
|
||||||
|
self.assertEqual(items[1].book, self.book_three)
|
||||||
|
self.assertEqual(items[1].order, 2)
|
||||||
|
self.assertTrue(items[1].approved)
|
||||||
|
|
||||||
|
self.assertEqual(items[2].book, self.book_two)
|
||||||
|
self.assertEqual(items[2].order, 3)
|
||||||
|
self.assertFalse(items[2].approved)
|
||||||
|
|
||||||
|
def test_approving_one_pending_book_from_multiple(self):
|
||||||
|
"""
|
||||||
|
When a list contains any pending books, the pending books should have
|
||||||
|
be at the end of the list by order. If a pending book is approved, then
|
||||||
|
its order should be at the end of the approved books and before the
|
||||||
|
remaining pending books.
|
||||||
|
"""
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
models.ListItem.objects.create(
|
||||||
|
book_list=self.list,
|
||||||
|
user=self.local_user,
|
||||||
|
book=self.book,
|
||||||
|
approved=True,
|
||||||
|
order=1,
|
||||||
|
)
|
||||||
|
models.ListItem.objects.create(
|
||||||
|
book_list=self.list,
|
||||||
|
user=self.local_user,
|
||||||
|
book=self.book_two,
|
||||||
|
approved=True,
|
||||||
|
order=2,
|
||||||
|
)
|
||||||
|
models.ListItem.objects.create(
|
||||||
|
book_list=self.list,
|
||||||
|
user=self.rat,
|
||||||
|
book=self.book_three,
|
||||||
|
approved=False,
|
||||||
|
order=3,
|
||||||
|
)
|
||||||
|
to_be_approved = models.ListItem.objects.create(
|
||||||
|
book_list=self.list,
|
||||||
|
user=self.rat,
|
||||||
|
book=self.book_four,
|
||||||
|
approved=False,
|
||||||
|
order=4,
|
||||||
|
)
|
||||||
|
|
||||||
|
view = views.Curate.as_view()
|
||||||
|
request = self.factory.post(
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
"item": to_be_approved.id,
|
||||||
|
"approved": "true",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
view(request, self.list.id)
|
||||||
|
|
||||||
|
items = self.list.listitem_set.order_by("order").all()
|
||||||
|
self.assertEqual(items[0].book, self.book)
|
||||||
|
self.assertEqual(items[0].order, 1)
|
||||||
|
self.assertTrue(items[0].approved)
|
||||||
|
|
||||||
|
self.assertEqual(items[1].book, self.book_two)
|
||||||
|
self.assertEqual(items[1].order, 2)
|
||||||
|
self.assertTrue(items[1].approved)
|
||||||
|
|
||||||
|
self.assertEqual(items[2].book, self.book_four)
|
||||||
|
self.assertEqual(items[2].order, 3)
|
||||||
|
self.assertTrue(items[2].approved)
|
||||||
|
|
||||||
|
self.assertEqual(items[3].book, self.book_three)
|
||||||
|
self.assertEqual(items[3].order, 4)
|
||||||
|
self.assertFalse(items[3].approved)
|
||||||
|
|
||||||
|
def test_add_three_books_and_move_last_to_first(self):
|
||||||
|
"""
|
||||||
|
Put three books on the list and move the last book to the first
|
||||||
|
position.
|
||||||
|
"""
|
||||||
|
request_one = self.factory.post(
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
"book": self.book.id,
|
||||||
|
"list": self.list.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
request_one.user = self.local_user
|
||||||
|
|
||||||
|
request_two = self.factory.post(
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
"book": self.book_two.id,
|
||||||
|
"list": self.list.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
request_two.user = self.local_user
|
||||||
|
|
||||||
|
request_three = self.factory.post(
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
"book": self.book_three.id,
|
||||||
|
"list": self.list.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
request_three.user = self.local_user
|
||||||
|
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
views.list.add_book(request_one)
|
||||||
|
views.list.add_book(request_two)
|
||||||
|
views.list.add_book(request_three)
|
||||||
|
|
||||||
|
items = self.list.listitem_set.order_by("order").all()
|
||||||
|
self.assertEqual(items[0].book, self.book)
|
||||||
|
self.assertEqual(items[1].book, self.book_two)
|
||||||
|
self.assertEqual(items[2].book, self.book_three)
|
||||||
|
self.assertEqual(items[0].order, 1)
|
||||||
|
self.assertEqual(items[1].order, 2)
|
||||||
|
self.assertEqual(items[2].order, 3)
|
||||||
|
|
||||||
|
set_position_request = self.factory.post("", {"position": 1})
|
||||||
|
set_position_request.user = self.local_user
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
views.list.set_book_position(set_position_request, items[2].id)
|
||||||
|
items = self.list.listitem_set.order_by("order").all()
|
||||||
|
self.assertEqual(items[0].book, self.book_three)
|
||||||
|
self.assertEqual(items[1].book, self.book)
|
||||||
|
self.assertEqual(items[2].book, self.book_two)
|
||||||
|
self.assertEqual(items[0].order, 1)
|
||||||
|
self.assertEqual(items[1].order, 2)
|
||||||
|
self.assertEqual(items[2].order, 3)
|
||||||
|
|
||||||
def test_add_book_outsider(self):
|
def test_add_book_outsider(self):
|
||||||
"""put a book on a list"""
|
"""put a book on a list"""
|
||||||
self.list.curation = "open"
|
self.list.curation = "open"
|
||||||
|
@ -358,6 +634,7 @@ class ListViews(TestCase):
|
||||||
book_list=self.list,
|
book_list=self.list,
|
||||||
user=self.local_user,
|
user=self.local_user,
|
||||||
book=self.book,
|
book=self.book,
|
||||||
|
order=1,
|
||||||
)
|
)
|
||||||
self.assertTrue(self.list.listitem_set.exists())
|
self.assertTrue(self.list.listitem_set.exists())
|
||||||
|
|
||||||
|
@ -377,9 +654,7 @@ class ListViews(TestCase):
|
||||||
"""take an item off a list"""
|
"""take an item off a list"""
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
item = models.ListItem.objects.create(
|
item = models.ListItem.objects.create(
|
||||||
book_list=self.list,
|
book_list=self.list, user=self.local_user, book=self.book, order=1
|
||||||
user=self.local_user,
|
|
||||||
book=self.book,
|
|
||||||
)
|
)
|
||||||
self.assertTrue(self.list.listitem_set.exists())
|
self.assertTrue(self.list.listitem_set.exists())
|
||||||
request = self.factory.post(
|
request = self.factory.post(
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
""" test for app action functionality """
|
""" test for app action functionality """
|
||||||
from unittest.mock import patch
|
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
@ -115,22 +114,19 @@ class ReportViews(TestCase):
|
||||||
report.refresh_from_db()
|
report.refresh_from_db()
|
||||||
self.assertFalse(report.resolved)
|
self.assertFalse(report.resolved)
|
||||||
|
|
||||||
def test_deactivate_user(self):
|
def test_suspend_user(self):
|
||||||
"""toggle whether a user is able to log in"""
|
"""toggle whether a user is able to log in"""
|
||||||
self.assertTrue(self.rat.is_active)
|
self.assertTrue(self.rat.is_active)
|
||||||
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
|
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
request.user.is_superuser = True
|
request.user.is_superuser = True
|
||||||
|
|
||||||
# de-activate
|
# de-activate
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
views.suspend_user(request, self.rat.id)
|
||||||
views.deactivate_user(request, report.id)
|
|
||||||
self.rat.refresh_from_db()
|
self.rat.refresh_from_db()
|
||||||
self.assertFalse(self.rat.is_active)
|
self.assertFalse(self.rat.is_active)
|
||||||
|
|
||||||
# re-activate
|
# re-activate
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
views.suspend_user(request, self.rat.id)
|
||||||
views.deactivate_user(request, report.id)
|
|
||||||
self.rat.refresh_from_db()
|
self.rat.refresh_from_db()
|
||||||
self.assertTrue(self.rat.is_active)
|
self.assertTrue(self.rat.is_active)
|
||||||
|
|
|
@ -1,119 +0,0 @@
|
||||||
""" test for app action functionality """
|
|
||||||
from unittest.mock import patch
|
|
||||||
from django.contrib.auth.models import Group, Permission
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.template.response import TemplateResponse
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.test.client import RequestFactory
|
|
||||||
|
|
||||||
from bookwyrm import models, views
|
|
||||||
from bookwyrm.activitypub import ActivitypubResponse
|
|
||||||
|
|
||||||
|
|
||||||
class TagViews(TestCase):
|
|
||||||
""" tag views"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
""" we need basic test data and mocks """
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
self.local_user = models.User.objects.create_user(
|
|
||||||
"mouse@local.com",
|
|
||||||
"mouse@mouse.com",
|
|
||||||
"mouseword",
|
|
||||||
local=True,
|
|
||||||
localname="mouse",
|
|
||||||
remote_id="https://example.com/users/mouse",
|
|
||||||
)
|
|
||||||
self.group = Group.objects.create(name="editor")
|
|
||||||
self.group.permissions.add(
|
|
||||||
Permission.objects.create(
|
|
||||||
name="edit_book",
|
|
||||||
codename="edit_book",
|
|
||||||
content_type=ContentType.objects.get_for_model(models.User),
|
|
||||||
).id
|
|
||||||
)
|
|
||||||
self.work = models.Work.objects.create(title="Test Work")
|
|
||||||
self.book = models.Edition.objects.create(
|
|
||||||
title="Example Edition",
|
|
||||||
remote_id="https://example.com/book/1",
|
|
||||||
parent_work=self.work,
|
|
||||||
)
|
|
||||||
models.SiteSettings.objects.create()
|
|
||||||
|
|
||||||
def test_tag_page(self):
|
|
||||||
""" there are so many views, this just makes sure it LOADS """
|
|
||||||
view = views.Tag.as_view()
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
tag = models.Tag.objects.create(name="hi there")
|
|
||||||
models.UserTag.objects.create(tag=tag, user=self.local_user, book=self.book)
|
|
||||||
request = self.factory.get("")
|
|
||||||
with patch("bookwyrm.views.tag.is_api_request") as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = view(request, tag.identifier)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
result.render()
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
request = self.factory.get("")
|
|
||||||
with patch("bookwyrm.views.tag.is_api_request") as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = view(request, tag.identifier)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
def test_tag_page_activitypub_page(self):
|
|
||||||
""" there are so many views, this just makes sure it LOADS """
|
|
||||||
view = views.Tag.as_view()
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
tag = models.Tag.objects.create(name="hi there")
|
|
||||||
models.UserTag.objects.create(tag=tag, user=self.local_user, book=self.book)
|
|
||||||
request = self.factory.get("", {"page": 1})
|
|
||||||
with patch("bookwyrm.views.tag.is_api_request") as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = view(request, tag.identifier)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
def test_tag(self):
|
|
||||||
""" add a tag to a book """
|
|
||||||
view = views.AddTag.as_view()
|
|
||||||
request = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"name": "A Tag!?",
|
|
||||||
"book": self.book.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request.user = self.local_user
|
|
||||||
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
view(request)
|
|
||||||
|
|
||||||
tag = models.Tag.objects.get()
|
|
||||||
user_tag = models.UserTag.objects.get()
|
|
||||||
self.assertEqual(tag.name, "A Tag!?")
|
|
||||||
self.assertEqual(tag.identifier, "A+Tag%21%3F")
|
|
||||||
self.assertEqual(user_tag.user, self.local_user)
|
|
||||||
self.assertEqual(user_tag.book, self.book)
|
|
||||||
|
|
||||||
def test_untag(self):
|
|
||||||
""" remove a tag from a book """
|
|
||||||
view = views.RemoveTag.as_view()
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
tag = models.Tag.objects.create(name="A Tag!?")
|
|
||||||
models.UserTag.objects.create(user=self.local_user, book=self.book, tag=tag)
|
|
||||||
request = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"user": self.local_user.id,
|
|
||||||
"book": self.book.id,
|
|
||||||
"name": tag.name,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request.user = self.local_user
|
|
||||||
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
view(request)
|
|
||||||
|
|
||||||
self.assertTrue(models.Tag.objects.filter(name="A Tag!?").exists())
|
|
||||||
self.assertFalse(models.UserTag.objects.exists())
|
|
|
@ -30,6 +30,14 @@ class UserViews(TestCase):
|
||||||
self.rat = models.User.objects.create_user(
|
self.rat = models.User.objects.create_user(
|
||||||
"rat@local.com", "rat@rat.rat", "password", local=True, localname="rat"
|
"rat@local.com", "rat@rat.rat", "password", local=True, localname="rat"
|
||||||
)
|
)
|
||||||
|
self.book = models.Edition.objects.create(title="test")
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
models.ShelfBook.objects.create(
|
||||||
|
book=self.book,
|
||||||
|
user=self.local_user,
|
||||||
|
shelf=self.local_user.shelf_set.first(),
|
||||||
|
)
|
||||||
|
|
||||||
models.SiteSettings.objects.create()
|
models.SiteSettings.objects.create()
|
||||||
self.anonymous_user = AnonymousUser
|
self.anonymous_user = AnonymousUser
|
||||||
self.anonymous_user.is_authenticated = False
|
self.anonymous_user.is_authenticated = False
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
""" test for app action functionality """
|
""" 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.template.response import TemplateResponse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
@ -21,9 +23,9 @@ class UserAdminViews(TestCase):
|
||||||
)
|
)
|
||||||
models.SiteSettings.objects.create()
|
models.SiteSettings.objects.create()
|
||||||
|
|
||||||
def test_user_admin_page(self):
|
def test_user_admin_list_page(self):
|
||||||
"""there are so many views, this just makes sure it LOADS"""
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
view = views.UserAdmin.as_view()
|
view = views.UserAdminList.as_view()
|
||||||
request = self.factory.get("")
|
request = self.factory.get("")
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
request.user.is_superuser = True
|
request.user.is_superuser = True
|
||||||
|
@ -31,3 +33,38 @@ class UserAdminViews(TestCase):
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
result.render()
|
result.render()
|
||||||
self.assertEqual(result.status_code, 200)
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_user_admin_page(self):
|
||||||
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
result.render()
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_user_admin_page_post(self):
|
||||||
|
"""set the user's group"""
|
||||||
|
group = Group.objects.create(name="editor")
|
||||||
|
self.assertEqual(
|
||||||
|
list(self.local_user.groups.values_list("name", flat=True)), []
|
||||||
|
)
|
||||||
|
|
||||||
|
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.delay"):
|
||||||
|
result = view(request, self.local_user.id)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
result.render()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
list(self.local_user.groups.values_list("name", flat=True)), ["editor"]
|
||||||
|
)
|
||||||
|
|
|
@ -51,13 +51,20 @@ urlpatterns = [
|
||||||
r"^password-reset/(?P<code>[A-Za-z0-9]+)/?$", views.PasswordReset.as_view()
|
r"^password-reset/(?P<code>[A-Za-z0-9]+)/?$", views.PasswordReset.as_view()
|
||||||
),
|
),
|
||||||
# admin
|
# admin
|
||||||
re_path(r"^settings/site-settings", views.Site.as_view(), name="settings-site"),
|
re_path(r"^settings/site-settings/?$", views.Site.as_view(), name="settings-site"),
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/email-preview",
|
r"^settings/email-preview/?$",
|
||||||
views.site.email_preview,
|
views.site.email_preview,
|
||||||
name="settings-email-preview",
|
name="settings-email-preview",
|
||||||
),
|
),
|
||||||
re_path(r"^settings/users", views.UserAdmin.as_view(), name="settings-users"),
|
re_path(
|
||||||
|
r"^settings/users/?$", views.UserAdminList.as_view(), name="settings-users"
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^settings/users/(?P<user>\d+)/?$",
|
||||||
|
views.UserAdmin.as_view(),
|
||||||
|
name="settings-user",
|
||||||
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/federation/?$",
|
r"^settings/federation/?$",
|
||||||
views.Federation.as_view(),
|
views.Federation.as_view(),
|
||||||
|
@ -113,9 +120,9 @@ urlpatterns = [
|
||||||
name="settings-report",
|
name="settings-report",
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/reports/(?P<report_id>\d+)/deactivate/?$",
|
r"^settings/reports/(?P<user_id>\d+)/suspend/?$",
|
||||||
views.deactivate_user,
|
views.suspend_user,
|
||||||
name="settings-report-deactivate",
|
name="settings-report-suspend",
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/reports/(?P<report_id>\d+)/resolve/?$",
|
r"^settings/reports/(?P<report_id>\d+)/resolve/?$",
|
||||||
|
@ -184,6 +191,11 @@ urlpatterns = [
|
||||||
views.list.remove_book,
|
views.list.remove_book,
|
||||||
name="list-remove-book",
|
name="list-remove-book",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
r"^list-item/(?P<list_item_id>\d+)/set-position$",
|
||||||
|
views.list.set_book_position,
|
||||||
|
name="list-set-book-position",
|
||||||
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^list/(?P<list_id>\d+)/curate/?$", views.Curate.as_view(), name="list-curate"
|
r"^list/(?P<list_id>\d+)/curate/?$", views.Curate.as_view(), name="list-curate"
|
||||||
),
|
),
|
||||||
|
@ -248,7 +260,12 @@ urlpatterns = [
|
||||||
re_path(r"^boost/(?P<status_id>\d+)/?$", views.Boost.as_view()),
|
re_path(r"^boost/(?P<status_id>\d+)/?$", views.Boost.as_view()),
|
||||||
re_path(r"^unboost/(?P<status_id>\d+)/?$", views.Unboost.as_view()),
|
re_path(r"^unboost/(?P<status_id>\d+)/?$", views.Unboost.as_view()),
|
||||||
# books
|
# books
|
||||||
re_path(r"%s(.json)?/?$" % book_path, views.Book.as_view()),
|
re_path(r"%s(.json)?/?$" % book_path, views.Book.as_view(), name="book"),
|
||||||
|
re_path(
|
||||||
|
r"%s/(?P<user_statuses>review|comment|quote)/?$" % book_path,
|
||||||
|
views.Book.as_view(),
|
||||||
|
name="book-user-statuses",
|
||||||
|
),
|
||||||
re_path(r"%s/edit/?$" % book_path, views.EditBook.as_view()),
|
re_path(r"%s/edit/?$" % book_path, views.EditBook.as_view()),
|
||||||
re_path(r"%s/confirm/?$" % book_path, views.ConfirmEditBook.as_view()),
|
re_path(r"%s/confirm/?$" % book_path, views.ConfirmEditBook.as_view()),
|
||||||
re_path(r"^create-book/?$", views.EditBook.as_view()),
|
re_path(r"^create-book/?$", views.EditBook.as_view()),
|
||||||
|
@ -265,11 +282,6 @@ urlpatterns = [
|
||||||
# author
|
# author
|
||||||
re_path(r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view()),
|
re_path(r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view()),
|
||||||
re_path(r"^author/(?P<author_id>\d+)/edit/?$", views.EditAuthor.as_view()),
|
re_path(r"^author/(?P<author_id>\d+)/edit/?$", views.EditAuthor.as_view()),
|
||||||
# tags
|
|
||||||
re_path(r"^tag/(?P<tag_id>.+)\.json/?$", views.Tag.as_view()),
|
|
||||||
re_path(r"^tag/(?P<tag_id>.+)/?$", views.Tag.as_view()),
|
|
||||||
re_path(r"^tag/?$", views.AddTag.as_view()),
|
|
||||||
re_path(r"^untag/?$", views.RemoveTag.as_view()),
|
|
||||||
# reading progress
|
# reading progress
|
||||||
re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"),
|
re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"),
|
||||||
re_path(r"^delete-readthrough/?$", views.delete_readthrough),
|
re_path(r"^delete-readthrough/?$", views.delete_readthrough),
|
||||||
|
|
|
@ -25,7 +25,7 @@ from .notifications import Notifications
|
||||||
from .outbox import Outbox
|
from .outbox import Outbox
|
||||||
from .reading import edit_readthrough, create_readthrough, delete_readthrough
|
from .reading import edit_readthrough, create_readthrough, delete_readthrough
|
||||||
from .reading import start_reading, finish_reading, delete_progressupdate
|
from .reading import start_reading, finish_reading, delete_progressupdate
|
||||||
from .reports import Report, Reports, make_report, resolve_report, deactivate_user
|
from .reports import Report, Reports, make_report, resolve_report, suspend_user
|
||||||
from .rss_feed import RssFeed
|
from .rss_feed import RssFeed
|
||||||
from .password import PasswordResetRequest, PasswordReset, ChangePassword
|
from .password import PasswordResetRequest, PasswordReset, ChangePassword
|
||||||
from .search import Search
|
from .search import Search
|
||||||
|
@ -34,8 +34,7 @@ from .shelf import create_shelf, delete_shelf
|
||||||
from .shelf import shelve, unshelve
|
from .shelf import shelve, unshelve
|
||||||
from .site import Site
|
from .site import Site
|
||||||
from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
|
from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
|
||||||
from .tag import Tag, AddTag, RemoveTag
|
|
||||||
from .updates import get_notification_count, get_unread_status_count
|
from .updates import get_notification_count, get_unread_status_count
|
||||||
from .user import User, EditUser, Followers, Following
|
from .user import User, EditUser, Followers, Following
|
||||||
from .user_admin import UserAdmin
|
from .user_admin import UserAdmin, UserAdminList
|
||||||
from .wellknown import *
|
from .wellknown import *
|
||||||
|
|
|
@ -28,13 +28,8 @@ from .helpers import is_api_request, get_edition, privacy_filter
|
||||||
class Book(View):
|
class Book(View):
|
||||||
"""a book! this is the stuff"""
|
"""a book! this is the stuff"""
|
||||||
|
|
||||||
def get(self, request, book_id):
|
def get(self, request, book_id, user_statuses=False):
|
||||||
"""info about a book"""
|
"""info about a book"""
|
||||||
try:
|
|
||||||
page = int(request.GET.get("page", 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
book = models.Book.objects.select_subclasses().get(id=book_id)
|
||||||
except models.Book.DoesNotExist:
|
except models.Book.DoesNotExist:
|
||||||
|
@ -45,29 +40,41 @@ class Book(View):
|
||||||
|
|
||||||
if isinstance(book, models.Work):
|
if isinstance(book, models.Work):
|
||||||
book = book.get_default_edition()
|
book = book.get_default_edition()
|
||||||
if not book:
|
if not book or not book.parent_work:
|
||||||
return HttpResponseNotFound()
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
work = book.parent_work
|
work = book.parent_work
|
||||||
if not work:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
# all reviews for the book
|
# all reviews for the book
|
||||||
reviews = models.Review.objects.filter(book__in=work.editions.all())
|
reviews = privacy_filter(
|
||||||
reviews = privacy_filter(request.user, reviews)
|
request.user, models.Review.objects.filter(book__in=work.editions.all())
|
||||||
|
)
|
||||||
|
|
||||||
# the reviews to show
|
# the reviews to show
|
||||||
paginated = Paginator(
|
if user_statuses and request.user.is_authenticated:
|
||||||
reviews.exclude(Q(content__isnull=True) | Q(content="")), PAGE_LENGTH
|
if user_statuses == "review":
|
||||||
)
|
queryset = book.review_set
|
||||||
reviews_page = paginated.get_page(page)
|
elif user_statuses == "comment":
|
||||||
|
queryset = book.comment_set
|
||||||
|
else:
|
||||||
|
queryset = book.quotation_set
|
||||||
|
queryset = queryset.filter(user=request.user)
|
||||||
|
else:
|
||||||
|
queryset = reviews.exclude(Q(content__isnull=True) | Q(content=""))
|
||||||
|
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"book": book,
|
||||||
|
"statuses": paginated.get_page(request.GET.get("page")),
|
||||||
|
"review_count": reviews.count(),
|
||||||
|
"ratings": reviews.filter(Q(content__isnull=True) | Q(content="")),
|
||||||
|
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
|
||||||
|
"lists": privacy_filter(
|
||||||
|
request.user, book.list_set.filter(listitem__approved=True)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
user_tags = readthroughs = user_shelves = other_edition_shelves = []
|
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
user_tags = models.UserTag.objects.filter(
|
|
||||||
book=book, user=request.user
|
|
||||||
).values_list("tag__identifier", flat=True)
|
|
||||||
|
|
||||||
readthroughs = models.ReadThrough.objects.filter(
|
readthroughs = models.ReadThrough.objects.filter(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
book=book,
|
book=book,
|
||||||
|
@ -77,31 +84,24 @@ class Book(View):
|
||||||
readthrough.progress_updates = (
|
readthrough.progress_updates = (
|
||||||
readthrough.progressupdate_set.all().order_by("-updated_date")
|
readthrough.progressupdate_set.all().order_by("-updated_date")
|
||||||
)
|
)
|
||||||
|
data["readthroughs"] = readthroughs
|
||||||
|
|
||||||
user_shelves = models.ShelfBook.objects.filter(user=request.user, book=book)
|
data["user_shelves"] = models.ShelfBook.objects.filter(
|
||||||
|
user=request.user, book=book
|
||||||
|
)
|
||||||
|
|
||||||
other_edition_shelves = models.ShelfBook.objects.filter(
|
data["other_edition_shelves"] = models.ShelfBook.objects.filter(
|
||||||
~Q(book=book),
|
~Q(book=book),
|
||||||
user=request.user,
|
user=request.user,
|
||||||
book__parent_work=book.parent_work,
|
book__parent_work=book.parent_work,
|
||||||
)
|
)
|
||||||
|
|
||||||
data = {
|
data["user_statuses"] = {
|
||||||
"book": book,
|
"review_count": book.review_set.filter(user=request.user).count(),
|
||||||
"reviews": reviews_page,
|
"comment_count": book.comment_set.filter(user=request.user).count(),
|
||||||
"review_count": reviews.count(),
|
"quotation_count": book.quotation_set.filter(user=request.user).count(),
|
||||||
"ratings": reviews.filter(Q(content__isnull=True) | Q(content="")),
|
|
||||||
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
|
|
||||||
"tags": models.UserTag.objects.filter(book=book),
|
|
||||||
"lists": privacy_filter(
|
|
||||||
request.user, book.list_set.filter(listitem__approved=True)
|
|
||||||
),
|
|
||||||
"user_tags": user_tags,
|
|
||||||
"user_shelves": user_shelves,
|
|
||||||
"other_edition_shelves": other_edition_shelves,
|
|
||||||
"readthroughs": readthroughs,
|
|
||||||
"path": "/book/%s" % book_id,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return TemplateResponse(request, "book/book.html", data)
|
return TemplateResponse(request, "book/book.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
@ -266,11 +266,6 @@ class Editions(View):
|
||||||
"""list of editions of a book"""
|
"""list of editions of a book"""
|
||||||
work = get_object_or_404(models.Work, id=book_id)
|
work = get_object_or_404(models.Work, id=book_id)
|
||||||
|
|
||||||
try:
|
|
||||||
page = int(request.GET.get("page", 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
if is_api_request(request):
|
if is_api_request(request):
|
||||||
return ActivitypubResponse(work.to_edition_list(**request.GET))
|
return ActivitypubResponse(work.to_edition_list(**request.GET))
|
||||||
filters = {}
|
filters = {}
|
||||||
|
@ -280,12 +275,12 @@ class Editions(View):
|
||||||
if request.GET.get("format"):
|
if request.GET.get("format"):
|
||||||
filters["physical_format__iexact"] = request.GET.get("format")
|
filters["physical_format__iexact"] = request.GET.get("format")
|
||||||
|
|
||||||
editions = work.editions.order_by("-edition_rank").all()
|
editions = work.editions.order_by("-edition_rank")
|
||||||
languages = set(sum([e.languages for e in editions], []))
|
languages = set(sum([e.languages for e in editions], []))
|
||||||
|
|
||||||
paginated = Paginator(editions.filter(**filters).all(), PAGE_LENGTH)
|
paginated = Paginator(editions.filter(**filters), PAGE_LENGTH)
|
||||||
data = {
|
data = {
|
||||||
"editions": paginated.get_page(page),
|
"editions": paginated.get_page(request.GET.get("page")),
|
||||||
"work": work,
|
"work": work,
|
||||||
"languages": languages,
|
"languages": languages,
|
||||||
"formats": set(
|
"formats": set(
|
||||||
|
|
|
@ -15,12 +15,6 @@ class Directory(View):
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""lets see your cute faces"""
|
"""lets see your cute faces"""
|
||||||
try:
|
|
||||||
page = int(request.GET.get("page", 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
# filters
|
|
||||||
filters = {}
|
filters = {}
|
||||||
software = request.GET.get("software")
|
software = request.GET.get("software")
|
||||||
if not software or software == "bookwyrm":
|
if not software or software == "bookwyrm":
|
||||||
|
@ -39,7 +33,7 @@ class Directory(View):
|
||||||
paginated = Paginator(users, 12)
|
paginated = Paginator(users, 12)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"users": paginated.get_page(page),
|
"users": paginated.get_page(request.GET.get("page")),
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, "directory/directory.html", data)
|
return TemplateResponse(request, "directory/directory.html", data)
|
||||||
|
|
||||||
|
|
|
@ -24,11 +24,6 @@ class Federation(View):
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""list of servers"""
|
"""list of servers"""
|
||||||
try:
|
|
||||||
page = int(request.GET.get("page", 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
servers = models.FederatedServer.objects
|
servers = models.FederatedServer.objects
|
||||||
|
|
||||||
sort = request.GET.get("sort")
|
sort = request.GET.get("sort")
|
||||||
|
@ -40,7 +35,7 @@ class Federation(View):
|
||||||
paginated = Paginator(servers, PAGE_LENGTH)
|
paginated = Paginator(servers, PAGE_LENGTH)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"servers": paginated.get_page(page),
|
"servers": paginated.get_page(request.GET.get("page")),
|
||||||
"sort": sort,
|
"sort": sort,
|
||||||
"form": forms.ServerForm(),
|
"form": forms.ServerForm(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,11 +22,6 @@ class Feed(View):
|
||||||
|
|
||||||
def get(self, request, tab):
|
def get(self, request, tab):
|
||||||
"""user's homepage with activity feed"""
|
"""user's homepage with activity feed"""
|
||||||
try:
|
|
||||||
page = int(request.GET.get("page", 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
if not tab in STREAMS:
|
if not tab in STREAMS:
|
||||||
tab = "home"
|
tab = "home"
|
||||||
|
|
||||||
|
@ -39,7 +34,7 @@ class Feed(View):
|
||||||
**feed_page_data(request.user),
|
**feed_page_data(request.user),
|
||||||
**{
|
**{
|
||||||
"user": request.user,
|
"user": request.user,
|
||||||
"activities": paginated.get_page(page),
|
"activities": paginated.get_page(request.GET.get("page")),
|
||||||
"suggested_users": suggested_users,
|
"suggested_users": suggested_users,
|
||||||
"tab": tab,
|
"tab": tab,
|
||||||
"goal_form": forms.GoalForm(),
|
"goal_form": forms.GoalForm(),
|
||||||
|
@ -55,11 +50,6 @@ class DirectMessage(View):
|
||||||
|
|
||||||
def get(self, request, username=None):
|
def get(self, request, username=None):
|
||||||
"""like a feed but for dms only"""
|
"""like a feed but for dms only"""
|
||||||
try:
|
|
||||||
page = int(request.GET.get("page", 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
# remove fancy subclasses of status, keep just good ol' notes
|
# remove fancy subclasses of status, keep just good ol' notes
|
||||||
queryset = models.Status.objects.filter(
|
queryset = models.Status.objects.filter(
|
||||||
review__isnull=True,
|
review__isnull=True,
|
||||||
|
@ -82,13 +72,12 @@ class DirectMessage(View):
|
||||||
).order_by("-published_date")
|
).order_by("-published_date")
|
||||||
|
|
||||||
paginated = Paginator(activities, PAGE_LENGTH)
|
paginated = Paginator(activities, PAGE_LENGTH)
|
||||||
activity_page = paginated.get_page(page)
|
|
||||||
data = {
|
data = {
|
||||||
**feed_page_data(request.user),
|
**feed_page_data(request.user),
|
||||||
**{
|
**{
|
||||||
"user": request.user,
|
"user": request.user,
|
||||||
"partner": user,
|
"partner": user,
|
||||||
"activities": activity_page,
|
"activities": paginated.get_page(request.GET.get("page")),
|
||||||
"path": "/direct-messages",
|
"path": "/direct-messages",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -174,7 +163,7 @@ def get_suggested_books(user, max_books=5):
|
||||||
)
|
)
|
||||||
shelf = user.shelf_set.get(identifier=preset)
|
shelf = user.shelf_set.get(identifier=preset)
|
||||||
|
|
||||||
shelf_books = shelf.shelfbook_set.order_by("-updated_date").all()[:limit]
|
shelf_books = shelf.shelfbook_set.order_by("-updated_date")[:limit]
|
||||||
if not shelf_books:
|
if not shelf_books:
|
||||||
continue
|
continue
|
||||||
shelf_preview = {
|
shelf_preview = {
|
||||||
|
|
|
@ -30,11 +30,6 @@ class ManageInvites(View):
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""invite management page"""
|
"""invite management page"""
|
||||||
try:
|
|
||||||
page = int(request.GET.get("page", 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
paginated = Paginator(
|
paginated = Paginator(
|
||||||
models.SiteInvite.objects.filter(user=request.user).order_by(
|
models.SiteInvite.objects.filter(user=request.user).order_by(
|
||||||
"-created_date"
|
"-created_date"
|
||||||
|
@ -43,7 +38,7 @@ class ManageInvites(View):
|
||||||
)
|
)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"invites": paginated.get_page(page),
|
"invites": paginated.get_page(request.GET.get("page")),
|
||||||
"form": forms.CreateInviteForm(),
|
"form": forms.CreateInviteForm(),
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, "settings/manage_invites.html", data)
|
return TemplateResponse(request, "settings/manage_invites.html", data)
|
||||||
|
@ -93,11 +88,6 @@ class ManageInviteRequests(View):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""view a list of requests"""
|
"""view a list of requests"""
|
||||||
ignored = request.GET.get("ignored", False)
|
ignored = request.GET.get("ignored", False)
|
||||||
try:
|
|
||||||
page = int(request.GET.get("page", 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
sort = request.GET.get("sort")
|
sort = request.GET.get("sort")
|
||||||
sort_fields = [
|
sort_fields = [
|
||||||
"created_date",
|
"created_date",
|
||||||
|
@ -136,7 +126,7 @@ class ManageInviteRequests(View):
|
||||||
data = {
|
data = {
|
||||||
"ignored": ignored,
|
"ignored": ignored,
|
||||||
"count": paginated.count,
|
"count": paginated.count,
|
||||||
"requests": paginated.get_page(page),
|
"requests": paginated.get_page(request.GET.get("page")),
|
||||||
"sort": sort,
|
"sort": sort,
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, "settings/manage_invite_requests.html", data)
|
return TemplateResponse(request, "settings/manage_invite_requests.html", data)
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
""" book list views"""
|
""" book list views"""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError, transaction
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Avg, Count, Q, Max
|
||||||
from django.http import HttpResponseNotFound, HttpResponseBadRequest
|
from django.db.models.functions import Coalesce
|
||||||
|
from django.http import HttpResponseNotFound, HttpResponseBadRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
@ -16,17 +19,13 @@ from bookwyrm.connectors import connector_manager
|
||||||
from .helpers import is_api_request, privacy_filter
|
from .helpers import is_api_request, privacy_filter
|
||||||
from .helpers import get_user_from_username
|
from .helpers import get_user_from_username
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
class Lists(View):
|
class Lists(View):
|
||||||
"""book list page"""
|
"""book list page"""
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""display a book list"""
|
"""display a book list"""
|
||||||
try:
|
|
||||||
page = int(request.GET.get("page", 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
# hide lists with no approved books
|
# hide lists with no approved books
|
||||||
lists = (
|
lists = (
|
||||||
models.List.objects.annotate(
|
models.List.objects.annotate(
|
||||||
|
@ -35,7 +34,6 @@ class Lists(View):
|
||||||
.filter(item_count__gt=0)
|
.filter(item_count__gt=0)
|
||||||
.order_by("-updated_date")
|
.order_by("-updated_date")
|
||||||
.distinct()
|
.distinct()
|
||||||
.all()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
lists = privacy_filter(
|
lists = privacy_filter(
|
||||||
|
@ -44,7 +42,7 @@ class Lists(View):
|
||||||
|
|
||||||
paginated = Paginator(lists, 12)
|
paginated = Paginator(lists, 12)
|
||||||
data = {
|
data = {
|
||||||
"lists": paginated.get_page(page),
|
"lists": paginated.get_page(request.GET.get("page")),
|
||||||
"list_form": forms.ListForm(),
|
"list_form": forms.ListForm(),
|
||||||
"path": "/list",
|
"path": "/list",
|
||||||
}
|
}
|
||||||
|
@ -67,19 +65,15 @@ class UserLists(View):
|
||||||
|
|
||||||
def get(self, request, username):
|
def get(self, request, username):
|
||||||
"""display a book list"""
|
"""display a book list"""
|
||||||
try:
|
|
||||||
page = int(request.GET.get("page", 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
user = get_user_from_username(request.user, username)
|
user = get_user_from_username(request.user, username)
|
||||||
lists = models.List.objects.filter(user=user).all()
|
lists = models.List.objects.filter(user=user)
|
||||||
lists = privacy_filter(request.user, lists)
|
lists = privacy_filter(request.user, lists)
|
||||||
paginated = Paginator(lists, 12)
|
paginated = Paginator(lists, 12)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"user": user,
|
"user": user,
|
||||||
"is_self": request.user.id == user.id,
|
"is_self": request.user.id == user.id,
|
||||||
"lists": paginated.get_page(page),
|
"lists": paginated.get_page(request.GET.get("page")),
|
||||||
"list_form": forms.ListForm(),
|
"list_form": forms.ListForm(),
|
||||||
"path": user.local_path + "/lists",
|
"path": user.local_path + "/lists",
|
||||||
}
|
}
|
||||||
|
@ -100,6 +94,45 @@ class List(View):
|
||||||
|
|
||||||
query = request.GET.get("q")
|
query = request.GET.get("q")
|
||||||
suggestions = None
|
suggestions = None
|
||||||
|
|
||||||
|
# sort_by shall be "order" unless a valid alternative is given
|
||||||
|
sort_by = request.GET.get("sort_by", "order")
|
||||||
|
if sort_by not in ("order", "title", "rating"):
|
||||||
|
sort_by = "order"
|
||||||
|
|
||||||
|
# direction shall be "ascending" unless a valid alternative is given
|
||||||
|
direction = request.GET.get("direction", "ascending")
|
||||||
|
if direction not in ("ascending", "descending"):
|
||||||
|
direction = "ascending"
|
||||||
|
|
||||||
|
internal_sort_by = {
|
||||||
|
"order": "order",
|
||||||
|
"title": "book__title",
|
||||||
|
"rating": "average_rating",
|
||||||
|
}
|
||||||
|
directional_sort_by = internal_sort_by[sort_by]
|
||||||
|
if direction == "descending":
|
||||||
|
directional_sort_by = "-" + directional_sort_by
|
||||||
|
|
||||||
|
if sort_by == "order":
|
||||||
|
items = book_list.listitem_set.filter(approved=True).order_by(
|
||||||
|
directional_sort_by
|
||||||
|
)
|
||||||
|
elif sort_by == "title":
|
||||||
|
items = book_list.listitem_set.filter(approved=True).order_by(
|
||||||
|
directional_sort_by
|
||||||
|
)
|
||||||
|
elif sort_by == "rating":
|
||||||
|
items = (
|
||||||
|
book_list.listitem_set.annotate(
|
||||||
|
average_rating=Avg(Coalesce("book__review__rating", 0))
|
||||||
|
)
|
||||||
|
.filter(approved=True)
|
||||||
|
.order_by(directional_sort_by)
|
||||||
|
)
|
||||||
|
|
||||||
|
paginated = Paginator(items, 12)
|
||||||
|
|
||||||
if query and request.user.is_authenticated:
|
if query and request.user.is_authenticated:
|
||||||
# search for books
|
# search for books
|
||||||
suggestions = connector_manager.local_search(query, raw=True)
|
suggestions = connector_manager.local_search(query, raw=True)
|
||||||
|
@ -119,11 +152,14 @@ class List(View):
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"list": book_list,
|
"list": book_list,
|
||||||
"items": book_list.listitem_set.filter(approved=True),
|
"items": paginated.get_page(request.GET.get("page")),
|
||||||
"pending_count": book_list.listitem_set.filter(approved=False).count(),
|
"pending_count": book_list.listitem_set.filter(approved=False).count(),
|
||||||
"suggested_books": suggestions,
|
"suggested_books": suggestions,
|
||||||
"list_form": forms.ListForm(instance=book_list),
|
"list_form": forms.ListForm(instance=book_list),
|
||||||
"query": query or "",
|
"query": query or "",
|
||||||
|
"sort_form": forms.SortListForm(
|
||||||
|
{"direction": direction, "sort_by": sort_by}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, "lists/list.html", data)
|
return TemplateResponse(request, "lists/list.html", data)
|
||||||
|
|
||||||
|
@ -165,10 +201,22 @@ class Curate(View):
|
||||||
suggestion = get_object_or_404(models.ListItem, id=request.POST.get("item"))
|
suggestion = get_object_or_404(models.ListItem, id=request.POST.get("item"))
|
||||||
approved = request.POST.get("approved") == "true"
|
approved = request.POST.get("approved") == "true"
|
||||||
if approved:
|
if approved:
|
||||||
|
# update the book and set it to be the last in the order of approved books,
|
||||||
|
# before any pending books
|
||||||
suggestion.approved = True
|
suggestion.approved = True
|
||||||
|
order_max = (
|
||||||
|
book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[
|
||||||
|
"order__max"
|
||||||
|
]
|
||||||
|
or 0
|
||||||
|
) + 1
|
||||||
|
suggestion.order = order_max
|
||||||
|
increment_order_in_reverse(book_list.id, order_max)
|
||||||
suggestion.save()
|
suggestion.save()
|
||||||
else:
|
else:
|
||||||
|
deleted_order = suggestion.order
|
||||||
suggestion.delete(broadcast=False)
|
suggestion.delete(broadcast=False)
|
||||||
|
normalize_book_list_ordering(book_list.id, start=deleted_order)
|
||||||
return redirect("list-curate", book_list.id)
|
return redirect("list-curate", book_list.id)
|
||||||
|
|
||||||
|
|
||||||
|
@ -183,19 +231,30 @@ def add_book(request):
|
||||||
# do you have permission to add to the list?
|
# do you have permission to add to the list?
|
||||||
try:
|
try:
|
||||||
if request.user == book_list.user or book_list.curation == "open":
|
if request.user == book_list.user or book_list.curation == "open":
|
||||||
# go ahead and add it
|
# add the book at the latest order of approved books, before pending books
|
||||||
|
order_max = (
|
||||||
|
book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[
|
||||||
|
"order__max"
|
||||||
|
]
|
||||||
|
) or 0
|
||||||
|
increment_order_in_reverse(book_list.id, order_max + 1)
|
||||||
models.ListItem.objects.create(
|
models.ListItem.objects.create(
|
||||||
book=book,
|
book=book,
|
||||||
book_list=book_list,
|
book_list=book_list,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
order=order_max + 1,
|
||||||
)
|
)
|
||||||
elif book_list.curation == "curated":
|
elif book_list.curation == "curated":
|
||||||
# make a pending entry
|
# make a pending entry at the end of the list
|
||||||
|
order_max = (
|
||||||
|
book_list.listitem_set.aggregate(Max("order"))["order__max"]
|
||||||
|
) or 0
|
||||||
models.ListItem.objects.create(
|
models.ListItem.objects.create(
|
||||||
approved=False,
|
approved=False,
|
||||||
book=book,
|
book=book,
|
||||||
book_list=book_list,
|
book_list=book_list,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
order=order_max + 1,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# you can't add to this list, what were you THINKING
|
# you can't add to this list, what were you THINKING
|
||||||
|
@ -209,12 +268,113 @@ def add_book(request):
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def remove_book(request, list_id):
|
def remove_book(request, list_id):
|
||||||
""" put a book on a list """
|
"""remove a book from a list"""
|
||||||
|
with transaction.atomic():
|
||||||
book_list = get_object_or_404(models.List, id=list_id)
|
book_list = get_object_or_404(models.List, id=list_id)
|
||||||
item = get_object_or_404(models.ListItem, id=request.POST.get("item"))
|
item = get_object_or_404(models.ListItem, id=request.POST.get("item"))
|
||||||
|
|
||||||
if not book_list.user == request.user and not item.user == request.user:
|
if not book_list.user == request.user and not item.user == request.user:
|
||||||
return HttpResponseNotFound()
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
deleted_order = item.order
|
||||||
item.delete()
|
item.delete()
|
||||||
|
normalize_book_list_ordering(book_list.id, start=deleted_order)
|
||||||
return redirect("list", list_id)
|
return redirect("list", list_id)
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def set_book_position(request, list_item_id):
|
||||||
|
"""
|
||||||
|
Action for when the list user manually specifies a list position, takes
|
||||||
|
special care with the unique ordering per list.
|
||||||
|
"""
|
||||||
|
with transaction.atomic():
|
||||||
|
list_item = get_object_or_404(models.ListItem, id=list_item_id)
|
||||||
|
try:
|
||||||
|
int_position = int(request.POST.get("position"))
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
"bad value for position. should be an integer"
|
||||||
|
)
|
||||||
|
|
||||||
|
if int_position < 1:
|
||||||
|
return HttpResponseBadRequest("position cannot be less than 1")
|
||||||
|
|
||||||
|
book_list = list_item.book_list
|
||||||
|
|
||||||
|
# the max position to which a book may be set is the highest order for
|
||||||
|
# books which are approved
|
||||||
|
order_max = book_list.listitem_set.filter(approved=True).aggregate(
|
||||||
|
Max("order")
|
||||||
|
)["order__max"]
|
||||||
|
|
||||||
|
if int_position > order_max:
|
||||||
|
int_position = order_max
|
||||||
|
|
||||||
|
if request.user not in (book_list.user, list_item.user):
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
original_order = list_item.order
|
||||||
|
if original_order == int_position:
|
||||||
|
return HttpResponse(status=204)
|
||||||
|
if original_order > int_position:
|
||||||
|
list_item.order = -1
|
||||||
|
list_item.save()
|
||||||
|
increment_order_in_reverse(book_list.id, int_position, original_order)
|
||||||
|
else:
|
||||||
|
list_item.order = -1
|
||||||
|
list_item.save()
|
||||||
|
decrement_order(book_list.id, original_order, int_position)
|
||||||
|
|
||||||
|
list_item.order = int_position
|
||||||
|
list_item.save()
|
||||||
|
|
||||||
|
return redirect("list", book_list.id)
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def increment_order_in_reverse(
|
||||||
|
book_list_id: int, start: int, end: Optional[int] = None
|
||||||
|
):
|
||||||
|
"""increase the order nu,ber for every item in a list"""
|
||||||
|
try:
|
||||||
|
book_list = models.List.objects.get(id=book_list_id)
|
||||||
|
except models.List.DoesNotExist:
|
||||||
|
return
|
||||||
|
items = book_list.listitem_set.filter(order__gte=start)
|
||||||
|
if end is not None:
|
||||||
|
items = items.filter(order__lt=end)
|
||||||
|
items = items.order_by("-order")
|
||||||
|
for item in items:
|
||||||
|
item.order += 1
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def decrement_order(book_list_id, start, end):
|
||||||
|
"""decrement the order value for every item in a list"""
|
||||||
|
try:
|
||||||
|
book_list = models.List.objects.get(id=book_list_id)
|
||||||
|
except models.List.DoesNotExist:
|
||||||
|
return
|
||||||
|
items = book_list.listitem_set.filter(order__gt=start, order__lte=end).order_by(
|
||||||
|
"order"
|
||||||
|
)
|
||||||
|
for item in items:
|
||||||
|
item.order -= 1
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def normalize_book_list_ordering(book_list_id, start=0, add_offset=0):
|
||||||
|
"""gives each book in a list the proper sequential order number"""
|
||||||
|
try:
|
||||||
|
book_list = models.List.objects.get(id=book_list_id)
|
||||||
|
except models.List.DoesNotExist:
|
||||||
|
return
|
||||||
|
items = book_list.listitem_set.filter(order__gt=start).order_by("order")
|
||||||
|
for i, item in enumerate(items, start):
|
||||||
|
effective_order = i + add_offset
|
||||||
|
if item.order != effective_order:
|
||||||
|
item.order = effective_order
|
||||||
|
item.save()
|
||||||
|
|
|
@ -145,6 +145,7 @@ def create_readthrough(request):
|
||||||
|
|
||||||
|
|
||||||
def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime:
|
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"""
|
||||||
user_tz = dateutil.tz.gettz(user.preferred_timezone)
|
user_tz = dateutil.tz.gettz(user.preferred_timezone)
|
||||||
start_date = dateutil.parser.parse(date_str, ignoretz=True)
|
start_date = dateutil.parser.parse(date_str, ignoretz=True)
|
||||||
return start_date.replace(tzinfo=user_tz).astimezone(dateutil.tz.UTC)
|
return start_date.replace(tzinfo=user_tz).astimezone(dateutil.tz.UTC)
|
||||||
|
|
|
@ -74,12 +74,13 @@ class Report(View):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("bookwyrm_moderate_user")
|
@permission_required("bookwyrm_moderate_user")
|
||||||
def deactivate_user(_, report_id):
|
def suspend_user(_, user_id):
|
||||||
"""mark an account as inactive"""
|
"""mark an account as inactive"""
|
||||||
report = get_object_or_404(models.Report, id=report_id)
|
user = get_object_or_404(models.User, id=user_id)
|
||||||
report.user.is_active = not report.user.is_active
|
user.is_active = not user.is_active
|
||||||
report.user.save()
|
# this isn't a full deletion, so we don't want to tell the world
|
||||||
return redirect("settings-report", report.id)
|
user.save(broadcast=False)
|
||||||
|
return redirect("settings-user", user.id)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|
|
@ -30,11 +30,6 @@ class Shelf(View):
|
||||||
except models.User.DoesNotExist:
|
except models.User.DoesNotExist:
|
||||||
return HttpResponseNotFound()
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
try:
|
|
||||||
page = int(request.GET.get("page", 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
shelves = privacy_filter(request.user, user.shelf_set)
|
shelves = privacy_filter(request.user, user.shelf_set)
|
||||||
|
|
||||||
# get the shelf and make sure the logged in user should be able to see it
|
# get the shelf and make sure the logged in user should be able to see it
|
||||||
|
@ -61,7 +56,7 @@ class Shelf(View):
|
||||||
return ActivitypubResponse(shelf.to_activity(**request.GET))
|
return ActivitypubResponse(shelf.to_activity(**request.GET))
|
||||||
|
|
||||||
paginated = Paginator(
|
paginated = Paginator(
|
||||||
shelf.books.order_by("-updated_date").all(),
|
shelf.books.order_by("-updated_date"),
|
||||||
PAGE_LENGTH,
|
PAGE_LENGTH,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -70,7 +65,7 @@ class Shelf(View):
|
||||||
"is_self": is_self,
|
"is_self": is_self,
|
||||||
"shelves": shelves.all(),
|
"shelves": shelves.all(),
|
||||||
"shelf": shelf,
|
"shelf": shelf,
|
||||||
"books": paginated.get_page(page),
|
"books": paginated.get_page(request.GET.get("page")),
|
||||||
}
|
}
|
||||||
|
|
||||||
return TemplateResponse(request, "user/shelf.html", data)
|
return TemplateResponse(request, "user/shelf.html", data)
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
""" tagging views"""
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
|
||||||
from django.template.response import TemplateResponse
|
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
from django.views import View
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
from bookwyrm.activitypub import ActivitypubResponse
|
|
||||||
from .helpers import is_api_request
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable= no-self-use
|
|
||||||
class Tag(View):
|
|
||||||
""" tag page """
|
|
||||||
|
|
||||||
def get(self, request, tag_id):
|
|
||||||
""" see books related to a tag """
|
|
||||||
tag_obj = get_object_or_404(models.Tag, identifier=tag_id)
|
|
||||||
|
|
||||||
if is_api_request(request):
|
|
||||||
return ActivitypubResponse(tag_obj.to_activity(**request.GET))
|
|
||||||
|
|
||||||
books = models.Edition.objects.filter(
|
|
||||||
usertag__tag__identifier=tag_id
|
|
||||||
).distinct()
|
|
||||||
data = {
|
|
||||||
"books": books,
|
|
||||||
"tag": tag_obj,
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, "tag.html", data)
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(login_required, name="dispatch")
|
|
||||||
class AddTag(View):
|
|
||||||
""" add a tag to a book """
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
""" tag a book """
|
|
||||||
# I'm not using a form here because sometimes "name" is sent as a hidden
|
|
||||||
# field which doesn't validate
|
|
||||||
name = request.POST.get("name")
|
|
||||||
book_id = request.POST.get("book")
|
|
||||||
book = get_object_or_404(models.Edition, id=book_id)
|
|
||||||
tag_obj, _ = models.Tag.objects.get_or_create(
|
|
||||||
name=name,
|
|
||||||
)
|
|
||||||
models.UserTag.objects.get_or_create(
|
|
||||||
user=request.user,
|
|
||||||
book=book,
|
|
||||||
tag=tag_obj,
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect("/book/%s" % book_id)
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(login_required, name="dispatch")
|
|
||||||
class RemoveTag(View):
|
|
||||||
""" remove a user's tag from a book """
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
""" untag a book """
|
|
||||||
name = request.POST.get("name")
|
|
||||||
tag_obj = get_object_or_404(models.Tag, name=name)
|
|
||||||
book_id = request.POST.get("book")
|
|
||||||
book = get_object_or_404(models.Edition, id=book_id)
|
|
||||||
|
|
||||||
user_tag = get_object_or_404(
|
|
||||||
models.UserTag, tag=tag_obj, book=book, user=request.user
|
|
||||||
)
|
|
||||||
user_tag.delete()
|
|
||||||
|
|
||||||
return redirect("/book/%s" % book_id)
|
|
|
@ -40,11 +40,6 @@ class User(View):
|
||||||
return ActivitypubResponse(user.to_activity())
|
return ActivitypubResponse(user.to_activity())
|
||||||
# otherwise we're at a UI view
|
# otherwise we're at a UI view
|
||||||
|
|
||||||
try:
|
|
||||||
page = int(request.GET.get("page", 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
shelf_preview = []
|
shelf_preview = []
|
||||||
|
|
||||||
# only show other shelves that should be visible
|
# only show other shelves that should be visible
|
||||||
|
@ -87,7 +82,7 @@ class User(View):
|
||||||
"is_self": is_self,
|
"is_self": is_self,
|
||||||
"shelves": shelf_preview,
|
"shelves": shelf_preview,
|
||||||
"shelf_count": shelves.count(),
|
"shelf_count": shelves.count(),
|
||||||
"activities": paginated.get_page(page),
|
"activities": paginated.get_page(request.GET.get("page", 1)),
|
||||||
"goal": goal,
|
"goal": goal,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
""" manage user """
|
""" manage user """
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import forms, models
|
||||||
from bookwyrm.settings import PAGE_LENGTH
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,16 +16,11 @@ from bookwyrm.settings import PAGE_LENGTH
|
||||||
permission_required("bookwyrm.moderate_users", raise_exception=True),
|
permission_required("bookwyrm.moderate_users", raise_exception=True),
|
||||||
name="dispatch",
|
name="dispatch",
|
||||||
)
|
)
|
||||||
class UserAdmin(View):
|
class UserAdminList(View):
|
||||||
"""admin view of users on this server"""
|
"""admin view of users on this server"""
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""list of users"""
|
"""list of users"""
|
||||||
try:
|
|
||||||
page = int(request.GET.get("page", 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
filters = {}
|
filters = {}
|
||||||
server = request.GET.get("server")
|
server = request.GET.get("server")
|
||||||
if server:
|
if server:
|
||||||
|
@ -50,8 +46,32 @@ class UserAdmin(View):
|
||||||
|
|
||||||
paginated = Paginator(users, PAGE_LENGTH)
|
paginated = Paginator(users, PAGE_LENGTH)
|
||||||
data = {
|
data = {
|
||||||
"users": paginated.get_page(page),
|
"users": paginated.get_page(request.GET.get("page")),
|
||||||
"sort": sort,
|
"sort": sort,
|
||||||
"server": server,
|
"server": server,
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, "settings/user_admin.html", data)
|
return TemplateResponse(request, "user_admin/user_admin.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name="dispatch")
|
||||||
|
@method_decorator(
|
||||||
|
permission_required("bookwyrm.moderate_users", raise_exception=True),
|
||||||
|
name="dispatch",
|
||||||
|
)
|
||||||
|
class UserAdmin(View):
|
||||||
|
"""moderate an individual user"""
|
||||||
|
|
||||||
|
def get(self, request, user):
|
||||||
|
"""user view"""
|
||||||
|
user = get_object_or_404(models.User, id=user)
|
||||||
|
data = {"user": user, "group_form": forms.UserGroupForm()}
|
||||||
|
return TemplateResponse(request, "user_admin/user.html", data)
|
||||||
|
|
||||||
|
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()
|
||||||
|
data = {"user": user, "group_form": form}
|
||||||
|
return TemplateResponse(request, "user_admin/user.html", data)
|
||||||
|
|
4
bw-dev
4
bw-dev
|
@ -90,10 +90,10 @@ case "$CMD" in
|
||||||
runweb python manage.py collectstatic --no-input
|
runweb python manage.py collectstatic --no-input
|
||||||
;;
|
;;
|
||||||
makemessages)
|
makemessages)
|
||||||
runweb django-admin makemessages --no-wrap --ignore=venv3 $@
|
runweb django-admin makemessages --no-wrap --ignore=venv $@
|
||||||
;;
|
;;
|
||||||
compilemessages)
|
compilemessages)
|
||||||
runweb django-admin compilemessages --ignore venv3 $@
|
runweb django-admin compilemessages --ignore venv $@
|
||||||
;;
|
;;
|
||||||
build)
|
build)
|
||||||
docker-compose build
|
docker-compose build
|
||||||
|
|
1
fr-dev
1
fr-dev
|
@ -1 +0,0 @@
|
||||||
bw-dev
|
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
@ -15,7 +15,7 @@ django-rename-app==0.1.2
|
||||||
pytz>=2021.1
|
pytz>=2021.1
|
||||||
|
|
||||||
# Dev
|
# Dev
|
||||||
black==20.8b1
|
black==21.4b0
|
||||||
coverage==5.1
|
coverage==5.1
|
||||||
pytest-django==4.1.0
|
pytest-django==4.1.0
|
||||||
pytest==6.1.2
|
pytest==6.1.2
|
||||||
|
|
Loading…
Reference in a new issue