2
0
Fork 0

Merge branch 'production' into nix

Patch celerywyrm settings to accept Redis that isn't over docker
This commit is contained in:
D Anzorge 2021-09-16 20:36:46 +02:00
commit 899805cc87
224 changed files with 7950 additions and 3113 deletions

View file

@ -213,7 +213,7 @@ class ActivityObject:
return data return data
@app.task @app.task(queue="medium_priority")
@transaction.atomic @transaction.atomic
def set_related_field( def set_related_field(
model_name, origin_model_name, related_field_name, related_remote_id, data model_name, origin_model_name, related_field_name, related_remote_id, data

View file

@ -70,6 +70,8 @@ class Quotation(Comment):
"""a quote and commentary on a book""" """a quote and commentary on a book"""
quote: str quote: str
position: int = None
positionMode: str = None
type: str = "Quotation" type: str = "Quotation"

View file

@ -1,6 +1,9 @@
""" access the activity streams stored in redis """ """ access the activity streams stored in redis """
from datetime import timedelta
from django.dispatch import receiver from django.dispatch import receiver
from django.db import transaction
from django.db.models import signals, Q from django.db.models import signals, Q
from django.utils import timezone
from bookwyrm import models from bookwyrm import models
from bookwyrm.redis_store import RedisStore, r from bookwyrm.redis_store import RedisStore, r
@ -258,38 +261,31 @@ def add_status_on_create(sender, instance, created, *args, **kwargs):
return return
if instance.deleted: if instance.deleted:
for stream in streams.values(): remove_status_task.delay(instance.id)
stream.remove_object_from_related_stores(instance)
return return
for stream in streams.values(): # when creating new things, gotta wait on the transaction
stream.add_status(instance, increment_unread=created) transaction.on_commit(
lambda: add_status_on_create_command(sender, instance, created)
if sender != models.Boost:
return
# remove the original post and other, earlier boosts
boosted = instance.boost.boosted_status
old_versions = models.Boost.objects.filter(
boosted_status__id=boosted.id,
created_date__lt=instance.created_date,
) )
for stream in streams.values():
audience = stream.get_stores_for_object(instance)
stream.remove_object_from_related_stores(boosted, stores=audience) def add_status_on_create_command(sender, instance, created):
for status in old_versions: """runs this code only after the database commit completes"""
stream.remove_object_from_related_stores(status, stores=audience) add_status_task.delay(instance.id, increment_unread=created)
if sender == models.Boost:
handle_boost_task.delay(instance.id)
@receiver(signals.post_delete, sender=models.Boost) @receiver(signals.post_delete, sender=models.Boost)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def remove_boost_on_delete(sender, instance, *args, **kwargs): def remove_boost_on_delete(sender, instance, *args, **kwargs):
"""boosts are deleted""" """boosts are deleted"""
# we're only interested in new statuses # remove the boost
for stream in streams.values(): remove_status_task.delay(instance.id)
# remove the boost # re-add the original status
stream.remove_object_from_related_stores(instance) add_status_task.delay(instance.boosted_status.id)
# re-add the original status
stream.add_status(instance.boosted_status)
@receiver(signals.post_save, sender=models.UserFollows) @receiver(signals.post_save, sender=models.UserFollows)
@ -298,7 +294,9 @@ def add_statuses_on_follow(sender, instance, created, *args, **kwargs):
"""add a newly followed user's statuses to feeds""" """add a newly followed user's statuses to feeds"""
if not created or not instance.user_subject.local: if not created or not instance.user_subject.local:
return return
HomeStream().add_user_statuses(instance.user_subject, instance.user_object) add_user_statuses_task.delay(
instance.user_subject.id, instance.user_object.id, stream_list=["home"]
)
@receiver(signals.post_delete, sender=models.UserFollows) @receiver(signals.post_delete, sender=models.UserFollows)
@ -307,7 +305,9 @@ def remove_statuses_on_unfollow(sender, instance, *args, **kwargs):
"""remove statuses from a feed on unfollow""" """remove statuses from a feed on unfollow"""
if not instance.user_subject.local: if not instance.user_subject.local:
return return
HomeStream().remove_user_statuses(instance.user_subject, instance.user_object) remove_user_statuses_task.delay(
instance.user_subject.id, instance.user_object.id, stream_list=["home"]
)
@receiver(signals.post_save, sender=models.UserBlocks) @receiver(signals.post_save, sender=models.UserBlocks)
@ -316,13 +316,15 @@ def remove_statuses_on_block(sender, instance, *args, **kwargs):
"""remove statuses from all feeds on block""" """remove statuses from all feeds on block"""
# blocks apply ot all feeds # blocks apply ot all feeds
if instance.user_subject.local: if instance.user_subject.local:
for stream in streams.values(): remove_user_statuses_task.delay(
stream.remove_user_statuses(instance.user_subject, instance.user_object) instance.user_subject.id, instance.user_object.id
)
# and in both directions # and in both directions
if instance.user_object.local: if instance.user_object.local:
for stream in streams.values(): remove_user_statuses_task.delay(
stream.remove_user_statuses(instance.user_object, instance.user_subject) instance.user_object.id, instance.user_subject.id
)
@receiver(signals.post_delete, sender=models.UserBlocks) @receiver(signals.post_delete, sender=models.UserBlocks)
@ -330,15 +332,22 @@ def remove_statuses_on_block(sender, instance, *args, **kwargs):
def add_statuses_on_unblock(sender, instance, *args, **kwargs): def add_statuses_on_unblock(sender, instance, *args, **kwargs):
"""remove statuses from all feeds on block""" """remove statuses from all feeds on block"""
public_streams = [v for (k, v) in streams.items() if k != "home"] public_streams = [v for (k, v) in streams.items() if k != "home"]
# add statuses back to streams with statuses from anyone # add statuses back to streams with statuses from anyone
if instance.user_subject.local: if instance.user_subject.local:
for stream in public_streams: add_user_statuses_task.delay(
stream.add_user_statuses(instance.user_subject, instance.user_object) instance.user_subject.id,
instance.user_object.id,
stream_list=public_streams,
)
# add statuses back to streams with statuses from anyone # add statuses back to streams with statuses from anyone
if instance.user_object.local: if instance.user_object.local:
for stream in public_streams: add_user_statuses_task.delay(
stream.add_user_statuses(instance.user_object, instance.user_subject) instance.user_object.id,
instance.user_subject.id,
stream_list=public_streams,
)
@receiver(signals.post_save, sender=models.User) @receiver(signals.post_save, sender=models.User)
@ -348,8 +357,8 @@ def populate_streams_on_account_create(sender, instance, created, *args, **kwarg
if not created or not instance.local: if not created or not instance.local:
return return
for stream in streams.values(): for stream in streams:
stream.populate_streams(instance) populate_stream_task.delay(stream, instance.id)
@receiver(signals.pre_save, sender=models.ShelfBook) @receiver(signals.pre_save, sender=models.ShelfBook)
@ -358,20 +367,14 @@ def add_statuses_on_shelve(sender, instance, *args, **kwargs):
"""update books stream when user shelves a book""" """update books stream when user shelves a book"""
if not instance.user.local: if not instance.user.local:
return return
book = None book = instance.book
if hasattr(instance, "book"):
book = instance.book
elif instance.mention_books.exists():
book = instance.mention_books.first()
if not book:
return
# check if the book is already on the user's shelves # check if the book is already on the user's shelves
editions = book.parent_work.editions.all() editions = book.parent_work.editions.all()
if models.ShelfBook.objects.filter(user=instance.user, book__in=editions).exists(): if models.ShelfBook.objects.filter(user=instance.user, book__in=editions).exists():
return return
BooksStream().add_book_statuses(instance.user, book) add_book_statuses_task.delay(instance.user.id, book.id)
@receiver(signals.post_delete, sender=models.ShelfBook) @receiver(signals.post_delete, sender=models.ShelfBook)
@ -381,24 +384,101 @@ def remove_statuses_on_unshelve(sender, instance, *args, **kwargs):
if not instance.user.local: if not instance.user.local:
return return
book = None book = instance.book
if hasattr(instance, "book"):
book = instance.book
elif instance.mention_books.exists():
book = instance.mention_books.first()
if not book:
return
# check if the book is actually unshelved, not just moved # check if the book is actually unshelved, not just moved
editions = book.parent_work.editions.all() editions = book.parent_work.editions.all()
if models.ShelfBook.objects.filter(user=instance.user, book__in=editions).exists(): if models.ShelfBook.objects.filter(user=instance.user, book__in=editions).exists():
return return
BooksStream().remove_book_statuses(instance.user, instance.book) remove_book_statuses_task.delay(instance.user.id, book.id)
@app.task # ---- TASKS
@app.task(queue="low_priority")
def add_book_statuses_task(user_id, book_id):
"""add statuses related to a book on shelve"""
user = models.User.objects.get(id=user_id)
book = models.Edition.objects.get(id=book_id)
BooksStream().add_book_statuses(user, book)
@app.task(queue="low_priority")
def remove_book_statuses_task(user_id, book_id):
"""remove statuses about a book from a user's books feed"""
user = models.User.objects.get(id=user_id)
book = models.Edition.objects.get(id=book_id)
BooksStream().remove_book_statuses(user, book)
@app.task(queue="medium_priority")
def populate_stream_task(stream, user_id): def populate_stream_task(stream, user_id):
"""background task for populating an empty activitystream""" """background task for populating an empty activitystream"""
user = models.User.objects.get(id=user_id) user = models.User.objects.get(id=user_id)
stream = streams[stream] stream = streams[stream]
stream.populate_streams(user) stream.populate_streams(user)
@app.task(queue="medium_priority")
def remove_status_task(status_ids):
"""remove a status from any stream it might be in"""
# this can take an id or a list of ids
if not isinstance(status_ids, list):
status_ids = [status_ids]
statuses = models.Status.objects.filter(id__in=status_ids)
for stream in streams.values():
for status in statuses:
stream.remove_object_from_related_stores(status)
@app.task(queue="high_priority")
def add_status_task(status_id, increment_unread=False):
"""add a status to any stream it should be in"""
status = models.Status.objects.get(id=status_id)
# we don't want to tick the unread count for csv import statuses, idk how better
# to check than just to see if the states is more than a few days old
if status.created_date < timezone.now() - timedelta(days=2):
increment_unread = False
for stream in streams.values():
stream.add_status(status, increment_unread=increment_unread)
@app.task(queue="medium_priority")
def remove_user_statuses_task(viewer_id, user_id, stream_list=None):
"""remove all statuses by a user from a viewer's stream"""
stream_list = [streams[s] for s in stream_list] if stream_list else streams.values()
viewer = models.User.objects.get(id=viewer_id)
user = models.User.objects.get(id=user_id)
for stream in stream_list:
stream.remove_user_statuses(viewer, user)
@app.task(queue="medium_priority")
def add_user_statuses_task(viewer_id, user_id, stream_list=None):
"""add all statuses by a user to a viewer's stream"""
stream_list = [streams[s] for s in stream_list] if stream_list else streams.values()
viewer = models.User.objects.get(id=viewer_id)
user = models.User.objects.get(id=user_id)
for stream in stream_list:
stream.add_user_statuses(viewer, user)
@app.task(queue="medium_priority")
def handle_boost_task(boost_id):
"""remove the original post and other, earlier boosts"""
instance = models.Status.objects.get(id=boost_id)
boosted = instance.boost.boosted_status
old_versions = models.Boost.objects.filter(
boosted_status__id=boosted.id,
created_date__lt=instance.created_date,
)
for stream in streams.values():
audience = stream.get_stores_for_object(instance)
stream.remove_object_from_related_stores(boosted, stores=audience)
for status in old_versions:
stream.remove_object_from_related_stores(status, stores=audience)

View file

@ -119,7 +119,7 @@ def get_or_create_connector(remote_id):
return load_connector(connector_info) return load_connector(connector_info)
@app.task @app.task(queue="low_priority")
def load_more_data(connector_id, book_id): def load_more_data(connector_id, book_id):
"""background the work of getting all 10,000 editions of LoTR""" """background the work of getting all 10,000 editions of LoTR"""
connector_info = models.Connector.objects.get(id=connector_id) connector_info = models.Connector.objects.get(id=connector_id)

View file

@ -15,4 +15,5 @@ def site_settings(request): # pylint: disable=unused-argument
"media_full_url": settings.MEDIA_FULL_URL, "media_full_url": settings.MEDIA_FULL_URL,
"preview_images_enabled": settings.ENABLE_PREVIEW_IMAGES, "preview_images_enabled": settings.ENABLE_PREVIEW_IMAGES,
"request_protocol": request_protocol, "request_protocol": request_protocol,
"js_cache": settings.JS_CACHE,
} }

View file

@ -64,7 +64,7 @@ def format_email(email_name, data):
return (subject, html_content, text_content) return (subject, html_content, text_content)
@app.task @app.task(queue="high_priority")
def send_email(recipient, subject, html_content, text_content): def send_email(recipient, subject, html_content, text_content):
"""use a task to send the email""" """use a task to send the email"""
email = EmailMultiAlternatives( email = EmailMultiAlternatives(

View file

@ -101,6 +101,8 @@ class QuotationForm(CustomForm):
"content_warning", "content_warning",
"sensitive", "sensitive",
"privacy", "privacy",
"position",
"position_mode",
] ]
@ -123,6 +125,12 @@ class StatusForm(CustomForm):
fields = ["user", "content", "content_warning", "sensitive", "privacy"] fields = ["user", "content", "content_warning", "sensitive", "privacy"]
class DirectForm(CustomForm):
class Meta:
model = models.Status
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
class EditUserForm(CustomForm): class EditUserForm(CustomForm):
class Meta: class Meta:
model = models.User model = models.User
@ -132,6 +140,7 @@ class EditUserForm(CustomForm):
"email", "email",
"summary", "summary",
"show_goal", "show_goal",
"show_suggested_users",
"manually_approves_followers", "manually_approves_followers",
"default_post_privacy", "default_post_privacy",
"discoverable", "discoverable",
@ -296,6 +305,12 @@ class ReportForm(CustomForm):
fields = ["user", "reporter", "statuses", "note"] fields = ["user", "reporter", "statuses", "note"]
class EmailBlocklistForm(CustomForm):
class Meta:
model = models.EmailBlocklist
fields = ["domain"]
class ServerForm(CustomForm): class ServerForm(CustomForm):
class Meta: class Meta:
model = models.FederatedServer model = models.FederatedServer

View file

@ -3,6 +3,7 @@ import csv
import logging import logging
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from bookwyrm import models from bookwyrm import models
from bookwyrm.models import ImportJob, ImportItem from bookwyrm.models import ImportJob, ImportItem
@ -61,7 +62,7 @@ class Importer:
job.save() job.save()
@app.task @app.task(queue="low_priority")
def import_data(source, job_id): def import_data(source, job_id):
"""does the actual lookup work in a celery task""" """does the actual lookup work in a celery task"""
job = ImportJob.objects.get(id=job_id) job = ImportJob.objects.get(id=job_id)
@ -71,19 +72,20 @@ def import_data(source, job_id):
item.resolve() item.resolve()
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
logger.exception(err) logger.exception(err)
item.fail_reason = "Error loading book" item.fail_reason = _("Error loading book")
item.save() item.save()
continue continue
if item.book: if item.book or item.book_guess:
item.save() item.save()
if item.book:
# shelves book and handles reviews # shelves book and handles reviews
handle_imported_book( handle_imported_book(
source, job.user, item, job.include_reviews, job.privacy source, job.user, item, job.include_reviews, job.privacy
) )
else: else:
item.fail_reason = "Could not find a match for book" item.fail_reason = _("Could not find a match for book")
item.save() item.save()
finally: finally:
job.complete = True job.complete = True

View file

@ -0,0 +1,34 @@
# Generated by Django 3.2.4 on 2021-09-05 22:33
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0087_merge_0086_auto_20210827_1727_0086_auto_20210828_1724"),
]
operations = [
migrations.AddField(
model_name="quotation",
name="position",
field=models.IntegerField(
blank=True,
null=True,
validators=[django.core.validators.MinValueValidator(0)],
),
),
migrations.AddField(
model_name="quotation",
name="position_mode",
field=models.CharField(
blank=True,
choices=[("PG", "page"), ("PCT", "percent")],
default="PG",
max_length=3,
null=True,
),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-09-08 16:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0088_auto_20210905_2233"),
]
operations = [
migrations.AddField(
model_name="user",
name="show_suggested_users",
field=models.BooleanField(default=True),
),
]

View file

@ -0,0 +1,45 @@
# Generated by Django 3.2.4 on 2021-09-08 23:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0089_user_show_suggested_users"),
]
operations = [
migrations.AlterField(
model_name="connector",
name="deactivation_reason",
field=models.CharField(
blank=True,
choices=[
("pending", "Pending"),
("self_deletion", "Self Deletion"),
("moderator_suspension", "Moderator Suspension"),
("moderator_deletion", "Moderator Deletion"),
("domain_block", "Domain Block"),
],
max_length=255,
null=True,
),
),
migrations.AlterField(
model_name="user",
name="deactivation_reason",
field=models.CharField(
blank=True,
choices=[
("pending", "Pending"),
("self_deletion", "Self Deletion"),
("moderator_suspension", "Moderator Suspension"),
("moderator_deletion", "Moderator Deletion"),
("domain_block", "Domain Block"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 3.2.4 on 2021-09-08 22:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0089_user_show_suggested_users"),
]
operations = [
migrations.CreateModel(
name="EmailBlocklist",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_date", models.DateTimeField(auto_now_add=True)),
("domain", models.CharField(max_length=255, unique=True)),
],
options={
"ordering": ("-created_date",),
},
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.4 on 2021-09-09 00:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0090_auto_20210908_2346"),
("bookwyrm", "0090_emailblocklist"),
]
operations = []

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-09-10 18:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0091_merge_0090_auto_20210908_2346_0090_emailblocklist"),
]
operations = [
migrations.AddField(
model_name="sitesettings",
name="instance_short_description",
field=models.TextField(blank=True, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-09-10 19:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0092_sitesettings_instance_short_description"),
]
operations = [
migrations.AlterField(
model_name="sitesettings",
name="instance_short_description",
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View file

@ -0,0 +1,39 @@
# Generated by Django 3.2.4 on 2021-09-11 15:50
from django.db import migrations, models
from django.db.models import F, Value, CharField
def set_deactivate_date(apps, schema_editor):
"""best-guess for deactivation date"""
db_alias = schema_editor.connection.alias
apps.get_model("bookwyrm", "User").objects.using(db_alias).filter(
is_active=False
).update(deactivation_date=models.F("last_active_date"))
def reverse_func(apps, schema_editor):
"""noop"""
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0093_alter_sitesettings_instance_short_description"),
]
operations = [
migrations.AddField(
model_name="user",
name="deactivation_date",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name="user",
name="saved_lists",
field=models.ManyToManyField(
blank=True, related_name="saved_lists", to="bookwyrm.List"
),
),
migrations.RunPython(set_deactivate_date, reverse_func),
]

View file

@ -0,0 +1,25 @@
# Generated by Django 3.2.4 on 2021-09-11 14:22
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0093_alter_sitesettings_instance_short_description"),
]
operations = [
migrations.AddField(
model_name="importitem",
name="book_guess",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="book_guess",
to="bookwyrm.book",
),
),
]

View file

@ -0,0 +1,45 @@
# Generated by Django 3.2.4 on 2021-09-11 20:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0094_auto_20210911_1550"),
]
operations = [
migrations.AlterField(
model_name="connector",
name="deactivation_reason",
field=models.CharField(
blank=True,
choices=[
("pending", "Pending"),
("self_deletion", "Self deletion"),
("moderator_suspension", "Moderator suspension"),
("moderator_deletion", "Moderator deletion"),
("domain_block", "Domain block"),
],
max_length=255,
null=True,
),
),
migrations.AlterField(
model_name="user",
name="deactivation_reason",
field=models.CharField(
blank=True,
choices=[
("pending", "Pending"),
("self_deletion", "Self deletion"),
("moderator_suspension", "Moderator suspension"),
("moderator_deletion", "Moderator deletion"),
("domain_block", "Domain block"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.4 on 2021-09-11 21:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0094_auto_20210911_1550"),
("bookwyrm", "0094_importitem_book_guess"),
]
operations = []

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.4 on 2021-09-12 00:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0095_auto_20210911_2053"),
("bookwyrm", "0095_merge_20210911_2143"),
]
operations = []

View file

@ -24,7 +24,9 @@ from .federated_server import FederatedServer
from .import_job import ImportJob, ImportItem from .import_job import ImportJob, ImportItem
from .site import SiteSettings, SiteInvite, PasswordReset, InviteRequest from .site import SiteSettings, SiteInvite
from .site import PasswordReset, InviteRequest
from .site import EmailBlocklist
from .announcement import Announcement from .announcement import Announcement
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass) cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)

View file

@ -362,6 +362,13 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin):
self.collection_queryset, **kwargs self.collection_queryset, **kwargs
).serialize() ).serialize()
def delete(self, *args, broadcast=True, **kwargs):
"""Delete the object"""
activity = self.to_delete_activity(self.user)
super().delete(*args, **kwargs)
if self.user.local and broadcast:
self.broadcast(activity, self.user)
class CollectionItemMixin(ActivitypubMixin): class CollectionItemMixin(ActivitypubMixin):
"""for items that are part of an (Ordered)Collection""" """for items that are part of an (Ordered)Collection"""
@ -495,7 +502,7 @@ def unfurl_related_field(related_field, sort_field=None):
return related_field.remote_id return related_field.remote_id
@app.task @app.task(queue="medium_priority")
def broadcast_task(sender_id, activity, recipients): def broadcast_task(sender_id, activity, recipients):
"""the celery task for broadcast""" """the celery task for broadcast"""
user_model = apps.get_model("bookwyrm.User", require_ready=True) user_model = apps.get_model("bookwyrm.User", require_ready=True)

View file

@ -3,20 +3,19 @@ import base64
from Crypto import Random from Crypto import Random
from django.db import models from django.db import models
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
from .fields import RemoteIdField from .fields import RemoteIdField
DeactivationReason = models.TextChoices( DeactivationReason = [
"DeactivationReason", ("pending", _("Pending")),
[ ("self_deletion", _("Self deletion")),
"pending", ("moderator_suspension", _("Moderator suspension")),
"self_deletion", ("moderator_deletion", _("Moderator deletion")),
"moderator_deletion", ("domain_block", _("Domain block")),
"domain_block", ]
],
)
def new_access_code(): def new_access_code():

View file

@ -4,6 +4,7 @@ import re
from django.contrib.postgres.search import SearchVectorField from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.indexes import GinIndex
from django.db import models from django.db import models
from django.db import transaction
from django.dispatch import receiver from django.dispatch import receiver
from model_utils import FieldTracker from model_utils import FieldTracker
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
@ -361,4 +362,6 @@ def preview_image(instance, *args, **kwargs):
changed_fields = instance.field_tracker.changed() changed_fields = instance.field_tracker.changed()
if len(changed_fields) > 0: if len(changed_fields) > 0:
generate_edition_preview_image_task.delay(instance.id) transaction.on_commit(
lambda: generate_edition_preview_image_task.delay(instance.id)
)

View file

@ -19,7 +19,7 @@ class Connector(BookWyrmModel):
api_key = models.CharField(max_length=255, null=True, blank=True) api_key = models.CharField(max_length=255, null=True, blank=True)
active = models.BooleanField(default=True) active = models.BooleanField(default=True)
deactivation_reason = models.CharField( deactivation_reason = models.CharField(
max_length=255, choices=DeactivationReason.choices, null=True, blank=True max_length=255, choices=DeactivationReason, null=True, blank=True
) )
base_url = models.CharField(max_length=255) base_url = models.CharField(max_length=255)

View file

@ -1,16 +1,16 @@
""" connections to external ActivityPub servers """ """ connections to external ActivityPub servers """
from urllib.parse import urlparse from urllib.parse import urlparse
from django.apps import apps from django.apps import apps
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
FederationStatus = models.TextChoices( FederationStatus = [
"Status", ("federated", _("Federated")),
[ ("blocked", _("Blocked")),
"federated", ]
"blocked",
],
)
class FederatedServer(BookWyrmModel): class FederatedServer(BookWyrmModel):
@ -18,7 +18,7 @@ class FederatedServer(BookWyrmModel):
server_name = models.CharField(max_length=255, unique=True) server_name = models.CharField(max_length=255, unique=True)
status = models.CharField( status = models.CharField(
max_length=255, default="federated", choices=FederationStatus.choices max_length=255, default="federated", choices=FederationStatus
) )
# is it mastodon, bookwyrm, etc # is it mastodon, bookwyrm, etc
application_type = models.CharField(max_length=255, null=True, blank=True) application_type = models.CharField(max_length=255, null=True, blank=True)
@ -28,7 +28,7 @@ class FederatedServer(BookWyrmModel):
def block(self): def block(self):
"""block a server""" """block a server"""
self.status = "blocked" self.status = "blocked"
self.save() self.save(update_fields=["status"])
# deactivate all associated users # deactivate all associated users
self.user_set.filter(is_active=True).update( self.user_set.filter(is_active=True).update(
@ -45,7 +45,7 @@ class FederatedServer(BookWyrmModel):
def unblock(self): def unblock(self):
"""unblock a server""" """unblock a server"""
self.status = "federated" self.status = "federated"
self.save() self.save(update_fields=["status"])
self.user_set.filter(deactivation_reason="domain_block").update( self.user_set.filter(deactivation_reason="domain_block").update(
is_active=True, deactivation_reason=None is_active=True, deactivation_reason=None

View file

@ -71,6 +71,13 @@ class ImportItem(models.Model):
index = models.IntegerField() index = models.IntegerField()
data = models.JSONField() data = models.JSONField()
book = models.ForeignKey(Book, on_delete=models.SET_NULL, null=True, blank=True) book = models.ForeignKey(Book, on_delete=models.SET_NULL, null=True, blank=True)
book_guess = models.ForeignKey(
Book,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="book_guess",
)
fail_reason = models.TextField(null=True) fail_reason = models.TextField(null=True)
def resolve(self): def resolve(self):
@ -78,9 +85,13 @@ class ImportItem(models.Model):
if self.isbn: if self.isbn:
self.book = self.get_book_from_isbn() self.book = self.get_book_from_isbn()
else: else:
# don't fall back on title/author search is isbn is present. # don't fall back on title/author search if isbn is present.
# you're too likely to mismatch # you're too likely to mismatch
self.book = self.get_book_from_title_author() book, confidence = self.get_book_from_title_author()
if confidence > 0.999:
self.book = book
else:
self.book_guess = book
def get_book_from_isbn(self): def get_book_from_isbn(self):
"""search by isbn""" """search by isbn"""
@ -96,12 +107,15 @@ class ImportItem(models.Model):
"""search by title and author""" """search by title and author"""
search_term = construct_search_term(self.title, self.author) search_term = construct_search_term(self.title, self.author)
search_result = connector_manager.first_search_result( search_result = connector_manager.first_search_result(
search_term, min_confidence=0.999 search_term, min_confidence=0.1
) )
if search_result: if search_result:
# raises ConnectorException # raises ConnectorException
return search_result.connector.get_or_create_book(search_result.key) return (
return None search_result.connector.get_or_create_book(search_result.key),
search_result.confidence,
)
return None, 0
@property @property
def title(self): def title(self):
@ -174,6 +188,7 @@ class ImportItem(models.Model):
if start_date and start_date is not None and not self.date_read: if start_date and start_date is not None and not self.date_read:
return [ReadThrough(start_date=start_date)] return [ReadThrough(start_date=start_date)]
if self.date_read: if self.date_read:
start_date = start_date if start_date < self.date_read else None
return [ return [
ReadThrough( ReadThrough(
start_date=start_date, start_date=start_date,

View file

@ -20,6 +20,7 @@ class SiteSettings(models.Model):
max_length=150, default="Social Reading and Reviewing" max_length=150, default="Social Reading and Reviewing"
) )
instance_description = models.TextField(default="This instance has no description.") instance_description = models.TextField(default="This instance has no description.")
instance_short_description = models.CharField(max_length=255, blank=True, null=True)
# about page # about page
registration_closed_text = models.TextField( registration_closed_text = models.TextField(
@ -123,6 +124,23 @@ class PasswordReset(models.Model):
return "https://{}/password-reset/{}".format(DOMAIN, self.code) return "https://{}/password-reset/{}".format(DOMAIN, self.code)
class EmailBlocklist(models.Model):
"""blocked email addresses"""
created_date = models.DateTimeField(auto_now_add=True)
domain = models.CharField(max_length=255, unique=True)
class Meta:
"""default sorting"""
ordering = ("-created_date",)
@property
def users(self):
"""find the users associated with this address"""
return User.objects.filter(email__endswith=f"@{self.domain}")
# pylint: disable=unused-argument # pylint: disable=unused-argument
@receiver(models.signals.post_save, sender=SiteSettings) @receiver(models.signals.post_save, sender=SiteSettings)
def preview_image(instance, *args, **kwargs): def preview_image(instance, *args, **kwargs):

View file

@ -290,6 +290,16 @@ class Quotation(BookStatus):
"""like a review but without a rating and transient""" """like a review but without a rating and transient"""
quote = fields.HtmlField() quote = fields.HtmlField()
position = models.IntegerField(
validators=[MinValueValidator(0)], null=True, blank=True
)
position_mode = models.CharField(
max_length=3,
choices=ProgressMode.choices,
default=ProgressMode.PAGE,
null=True,
blank=True,
)
@property @property
def pure_content(self): def pure_content(self):

View file

@ -105,7 +105,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
related_name="blocked_by", related_name="blocked_by",
) )
saved_lists = models.ManyToManyField( saved_lists = models.ManyToManyField(
"List", symmetrical=False, related_name="saved_lists" "List", symmetrical=False, related_name="saved_lists", blank=True
) )
favorites = models.ManyToManyField( favorites = models.ManyToManyField(
"Status", "Status",
@ -122,16 +122,21 @@ class User(OrderedCollectionPageMixin, AbstractUser):
updated_date = models.DateTimeField(auto_now=True) updated_date = models.DateTimeField(auto_now=True)
last_active_date = models.DateTimeField(default=timezone.now) last_active_date = models.DateTimeField(default=timezone.now)
manually_approves_followers = fields.BooleanField(default=False) manually_approves_followers = fields.BooleanField(default=False)
# options to turn features on and off
show_goal = models.BooleanField(default=True) show_goal = models.BooleanField(default=True)
show_suggested_users = models.BooleanField(default=True)
discoverable = fields.BooleanField(default=False) discoverable = fields.BooleanField(default=False)
preferred_timezone = models.CharField( preferred_timezone = models.CharField(
choices=[(str(tz), str(tz)) for tz in pytz.all_timezones], choices=[(str(tz), str(tz)) for tz in pytz.all_timezones],
default=str(pytz.utc), default=str(pytz.utc),
max_length=255, max_length=255,
) )
deactivation_reason = models.CharField( deactivation_reason = models.CharField(
max_length=255, choices=DeactivationReason.choices, null=True, blank=True max_length=255, choices=DeactivationReason, null=True, blank=True
) )
deactivation_date = models.DateTimeField(null=True, blank=True)
confirmation_code = models.CharField(max_length=32, default=new_access_code) confirmation_code = models.CharField(max_length=32, default=new_access_code)
name_field = "username" name_field = "username"
@ -265,6 +270,11 @@ class User(OrderedCollectionPageMixin, AbstractUser):
# this user already exists, no need to populate fields # this user already exists, no need to populate fields
if not created: if not created:
if self.is_active:
self.deactivation_date = None
elif not self.deactivation_date:
self.deactivation_date = timezone.now()
super().save(*args, **kwargs) super().save(*args, **kwargs)
return return
@ -274,30 +284,46 @@ class User(OrderedCollectionPageMixin, AbstractUser):
transaction.on_commit(lambda: set_remote_server.delay(self.id)) transaction.on_commit(lambda: set_remote_server.delay(self.id))
return return
# populate fields for local users with transaction.atomic():
link = site_link() # populate fields for local users
self.remote_id = f"{link}/user/{self.localname}" link = site_link()
self.followers_url = f"{self.remote_id}/followers" self.remote_id = f"{link}/user/{self.localname}"
self.inbox = f"{self.remote_id}/inbox" self.followers_url = f"{self.remote_id}/followers"
self.shared_inbox = f"{link}/inbox" self.inbox = f"{self.remote_id}/inbox"
self.outbox = f"{self.remote_id}/outbox" self.shared_inbox = f"{link}/inbox"
self.outbox = f"{self.remote_id}/outbox"
# an id needs to be set before we can proceed with related models # an id needs to be set before we can proceed with related models
super().save(*args, **kwargs)
# make users editors by default
try:
self.groups.add(Group.objects.get(name="editor"))
except Group.DoesNotExist:
# this should only happen in tests
pass
# create keys and shelves for new local users
self.key_pair = KeyPair.objects.create(
remote_id=f"{self.remote_id}/#main-key"
)
self.save(broadcast=False, update_fields=["key_pair"])
self.create_shelves()
def delete(self, *args, **kwargs):
"""deactivate rather than delete a user"""
self.is_active = False
# skip the logic in this class's save()
super().save(*args, **kwargs) super().save(*args, **kwargs)
# make users editors by default @property
try: def local_path(self):
self.groups.add(Group.objects.get(name="editor")) """this model doesn't inherit bookwyrm model, so here we are"""
except Group.DoesNotExist: return "/user/%s" % (self.localname or self.username)
# this should only happen in tests
pass
# create keys and shelves for new local users
self.key_pair = KeyPair.objects.create(
remote_id="%s/#main-key" % self.remote_id
)
self.save(broadcast=False, update_fields=["key_pair"])
def create_shelves(self):
"""default shelves for a new user"""
shelves = [ shelves = [
{ {
"name": "To Read", "name": "To Read",
@ -321,17 +347,6 @@ class User(OrderedCollectionPageMixin, AbstractUser):
editable=False, editable=False,
).save(broadcast=False) ).save(broadcast=False)
def delete(self, *args, **kwargs):
"""deactivate rather than delete a user"""
self.is_active = False
# skip the logic in this class's save()
super().save(*args, **kwargs)
@property
def local_path(self):
"""this model doesn't inherit bookwyrm model, so here we are"""
return "/user/%s" % (self.localname or self.username)
class KeyPair(ActivitypubMixin, BookWyrmModel): class KeyPair(ActivitypubMixin, BookWyrmModel):
"""public and private keys for a user""" """public and private keys for a user"""
@ -420,7 +435,7 @@ class AnnualGoal(BookWyrmModel):
} }
@app.task @app.task(queue="low_priority")
def set_remote_server(user_id): def set_remote_server(user_id):
"""figure out the user's remote server in the background""" """figure out the user's remote server in the background"""
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
@ -459,7 +474,7 @@ def get_or_create_remote_server(domain):
return server return server
@app.task @app.task(queue="low_priority")
def get_remote_reviews(outbox): def get_remote_reviews(outbox):
"""ingest reviews by a new remote bookwyrm user""" """ingest reviews by a new remote bookwyrm user"""
outbox_page = outbox + "?page=true&type=Review" outbox_page = outbox + "?page=true&type=Review"

View file

@ -352,7 +352,7 @@ def save_and_cleanup(image, instance=None):
# pylint: disable=invalid-name # pylint: disable=invalid-name
@app.task @app.task(queue="low_priority")
def generate_site_preview_image_task(): def generate_site_preview_image_task():
"""generate preview_image for the website""" """generate preview_image for the website"""
if not settings.ENABLE_PREVIEW_IMAGES: if not settings.ENABLE_PREVIEW_IMAGES:
@ -377,7 +377,7 @@ def generate_site_preview_image_task():
# pylint: disable=invalid-name # pylint: disable=invalid-name
@app.task @app.task(queue="low_priority")
def generate_edition_preview_image_task(book_id): def generate_edition_preview_image_task(book_id):
"""generate preview_image for a book""" """generate preview_image for a book"""
if not settings.ENABLE_PREVIEW_IMAGES: if not settings.ENABLE_PREVIEW_IMAGES:
@ -402,7 +402,7 @@ def generate_edition_preview_image_task(book_id):
save_and_cleanup(image, instance=book) save_and_cleanup(image, instance=book)
@app.task @app.task(queue="low_priority")
def generate_user_preview_image_task(user_id): def generate_user_preview_image_task(user_id):
"""generate preview_image for a book""" """generate preview_image for a book"""
if not settings.ENABLE_PREVIEW_IMAGES: if not settings.ENABLE_PREVIEW_IMAGES:

View file

@ -13,12 +13,7 @@ VERSION = "0.0.1"
PAGE_LENGTH = env("PAGE_LENGTH", 15) PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English") DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
# celery JS_CACHE = "e5832a26"
CELERY_BROKER = env("CELERY_BROKER")
CELERY_RESULT_BACKEND = env("CELERY_RESULT_BACKEND")
CELERY_ACCEPT_CONTENT = ["application/json"]
CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json"
# email # email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend") EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")

View file

@ -89,6 +89,32 @@ body {
display: inline !important; display: inline !important;
} }
input[type=file]::file-selector-button {
-moz-appearance: none;
-webkit-appearance: none;
background-color: #fff;
border-radius: 4px;
border: 1px solid #dbdbdb;
box-shadow: none;
color: #363636;
cursor: pointer;
font-size: 1rem;
height: 2.5em;
justify-content: center;
line-height: 1.5;
padding-bottom: calc(0.5em - 1px);
padding-left: 1em;
padding-right: 1em;
padding-top: calc(0.5em - 1px);
text-align: center;
white-space: nowrap;
}
input[type=file]::file-selector-button:hover {
border-color: #b5b5b5;
color: #363636;
}
/** Shelving /** Shelving
******************************************************************************/ ******************************************************************************/
@ -96,7 +122,7 @@ body {
@see https://www.youtube.com/watch?v=9xXBYcWgCHA */ @see https://www.youtube.com/watch?v=9xXBYcWgCHA */
.shelf-option:disabled > *::after { .shelf-option:disabled > *::after {
font-family: "icomoon"; /* stylelint-disable font-family-no-missing-generic-family-keyword */ font-family: "icomoon"; /* stylelint-disable font-family-no-missing-generic-family-keyword */
content: "\e918"; content: "\e919"; /* icon-check */
margin-left: 0.5em; margin-left: 0.5em;
} }
@ -167,21 +193,36 @@ body {
/* All stars are visually filled by default. */ /* All stars are visually filled by default. */
.form-rate-stars .icon::before { .form-rate-stars .icon::before {
content: '\e9d9'; content: '\e9d9'; /* icon-star-full */
}
/* Icons directly following half star inputs are marked as half */
.form-rate-stars input.half:checked ~ .icon::before {
content: '\e9d8'; /* icon-star-half */
}
/* stylelint-disable no-descending-specificity */
.form-rate-stars input.half:checked + input + .icon:hover::before {
content: '\e9d8' !important; /* icon-star-half */
}
/* Icons directly following half check inputs that follow the checked input are emptied. */
.form-rate-stars input.half:checked + input + .icon ~ .icon::before {
content: '\e9d7'; /* icon-star-empty */
} }
/* Icons directly following inputs that follow the checked input are emptied. */ /* Icons directly following inputs that follow the checked input are emptied. */
.form-rate-stars input:checked ~ input + .icon::before { .form-rate-stars input:checked ~ input + .icon::before {
content: '\e9d7'; content: '\e9d7'; /* icon-star-empty */
} }
/* When a label is hovered, repeat the fill-all-then-empty-following pattern. */ /* When a label is hovered, repeat the fill-all-then-empty-following pattern. */
.form-rate-stars:hover .icon.icon::before { .form-rate-stars:hover .icon.icon::before {
content: '\e9d9'; content: '\e9d9' !important; /* icon-star-full */
} }
.form-rate-stars .icon:hover ~ .icon::before { .form-rate-stars .icon:hover ~ .icon::before {
content: '\e9d7'; content: '\e9d7' !important; /* icon-star-empty */
} }
/** Book covers /** Book covers
@ -292,17 +333,52 @@ body {
} }
.quote > blockquote::before { .quote > blockquote::before {
content: "\e906"; content: "\e907"; /* icon-quote-open */
top: 0; top: 0;
left: 0; left: 0;
} }
.quote > blockquote::after { .quote > blockquote::after {
content: "\e905"; content: "\e906"; /* icon-quote-close */
right: 0; right: 0;
} }
/* States /** Animations and transitions
******************************************************************************/
@keyframes turning {
from { transform: rotateZ(0deg); }
to { transform: rotateZ(360deg); }
}
.is-processing .icon-spinner::before {
animation: turning 1.5s infinite linear;
}
.icon-spinner {
display: none;
}
.is-processing .icon-spinner {
display: flex;
}
@media (prefers-reduced-motion: reduce) {
.is-processing .icon::before {
transition-duration: 0.001ms !important;
}
}
/** Transient notification
******************************************************************************/
#live-messages {
position: fixed;
bottom: 1em;
right: 1em;
}
/** States
******************************************************************************/ ******************************************************************************/
/* "disabled" for non-buttons */ /* "disabled" for non-buttons */

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -1,10 +1,10 @@
@font-face { @font-face {
font-family: 'icomoon'; font-family: 'icomoon';
src: url('../fonts/icomoon.eot?19nagi'); src: url('../fonts/icomoon.eot?36x4a3');
src: url('../fonts/icomoon.eot?19nagi#iefix') format('embedded-opentype'), src: url('../fonts/icomoon.eot?36x4a3#iefix') format('embedded-opentype'),
url('../fonts/icomoon.ttf?19nagi') format('truetype'), url('../fonts/icomoon.ttf?36x4a3') format('truetype'),
url('../fonts/icomoon.woff?19nagi') format('woff'), url('../fonts/icomoon.woff?36x4a3') format('woff'),
url('../fonts/icomoon.svg?19nagi#icomoon') format('svg'); url('../fonts/icomoon.svg?36x4a3#icomoon') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: block; font-display: block;
@ -25,6 +25,90 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-book:before {
content: "\e901";
}
.icon-envelope:before {
content: "\e902";
}
.icon-arrow-right:before {
content: "\e903";
}
.icon-bell:before {
content: "\e904";
}
.icon-x:before {
content: "\e905";
}
.icon-quote-close:before {
content: "\e906";
}
.icon-quote-open:before {
content: "\e907";
}
.icon-image:before {
content: "\e908";
}
.icon-pencil:before {
content: "\e909";
}
.icon-list:before {
content: "\e90a";
}
.icon-unlock:before {
content: "\e90b";
}
.icon-globe:before {
content: "\e90c";
}
.icon-lock:before {
content: "\e90d";
}
.icon-chain-broken:before {
content: "\e90e";
}
.icon-chain:before {
content: "\e90f";
}
.icon-comments:before {
content: "\e910";
}
.icon-comment:before {
content: "\e911";
}
.icon-boost:before {
content: "\e912";
}
.icon-arrow-left:before {
content: "\e913";
}
.icon-arrow-up:before {
content: "\e914";
}
.icon-arrow-down:before {
content: "\e915";
}
.icon-local:before {
content: "\e917";
}
.icon-dots-three:before {
content: "\e918";
}
.icon-check:before {
content: "\e919";
}
.icon-dots-three-vertical:before {
content: "\e91a";
}
.icon-bookmark:before {
content: "\e91b";
}
.icon-warning:before {
content: "\e91c";
}
.icon-rss:before {
content: "\e91d";
}
.icon-graphic-heart:before { .icon-graphic-heart:before {
content: "\e91e"; content: "\e91e";
} }
@ -34,102 +118,6 @@
.icon-graphic-banknote:before { .icon-graphic-banknote:before {
content: "\e920"; content: "\e920";
} }
.icon-warning:before {
content: "\e91b";
}
.icon-book:before {
content: "\e900";
}
.icon-bookmark:before {
content: "\e91a";
}
.icon-rss:before {
content: "\e91d";
}
.icon-envelope:before {
content: "\e901";
}
.icon-arrow-right:before {
content: "\e902";
}
.icon-bell:before {
content: "\e903";
}
.icon-x:before {
content: "\e904";
}
.icon-quote-close:before {
content: "\e905";
}
.icon-quote-open:before {
content: "\e906";
}
.icon-image:before {
content: "\e907";
}
.icon-pencil:before {
content: "\e908";
}
.icon-list:before {
content: "\e909";
}
.icon-unlock:before {
content: "\e90a";
}
.icon-unlisted:before {
content: "\e90a";
}
.icon-globe:before {
content: "\e90b";
}
.icon-public:before {
content: "\e90b";
}
.icon-lock:before {
content: "\e90c";
}
.icon-followers:before {
content: "\e90c";
}
.icon-chain-broken:before {
content: "\e90d";
}
.icon-chain:before {
content: "\e90e";
}
.icon-comments:before {
content: "\e90f";
}
.icon-comment:before {
content: "\e910";
}
.icon-boost:before {
content: "\e911";
}
.icon-arrow-left:before {
content: "\e912";
}
.icon-arrow-up:before {
content: "\e913";
}
.icon-arrow-down:before {
content: "\e914";
}
.icon-home:before {
content: "\e915";
}
.icon-local:before {
content: "\e916";
}
.icon-dots-three:before {
content: "\e917";
}
.icon-check:before {
content: "\e918";
}
.icon-dots-three-vertical:before {
content: "\e919";
}
.icon-search:before { .icon-search:before {
content: "\e986"; content: "\e986";
} }
@ -148,3 +136,9 @@
.icon-plus:before { .icon-plus:before {
content: "\ea0a"; content: "\ea0a";
} }
.icon-question-circle:before {
content: "\e900";
}
.icon-spinner:before {
content: "\e97a";
}

View file

@ -301,7 +301,10 @@ let BookWyrm = new class {
ajaxPost(form) { ajaxPost(form) {
return fetch(form.action, { return fetch(form.action, {
method : "POST", method : "POST",
body: new FormData(form) body: new FormData(form),
headers: {
'Accept': 'application/json',
}
}); });
} }

View file

@ -0,0 +1,236 @@
/* exported StatusCache */
/* globals BookWyrm */
let StatusCache = new class {
constructor() {
document.querySelectorAll('[data-cache-draft]')
.forEach(t => t.addEventListener('change', this.updateDraft.bind(this)));
document.querySelectorAll('[data-cache-draft]')
.forEach(t => this.populateDraft(t));
document.querySelectorAll('.submit-status')
.forEach(button => button.addEventListener(
'submit',
this.submitStatus.bind(this))
);
document.querySelectorAll('.form-rate-stars label.icon')
.forEach(button => button.addEventListener('click', this.toggleStar.bind(this)));
}
/**
* Update localStorage copy of drafted status
*
* @param {Event} event
* @return {undefined}
*/
updateDraft(event) {
// Used in set reading goal
let key = event.target.dataset.cacheDraft;
let value = event.target.value;
if (!value) {
window.localStorage.removeItem(key);
return;
}
window.localStorage.setItem(key, value);
}
/**
* Toggle display of a DOM node based on its value in the localStorage.
*
* @param {object} node - DOM node to toggle.
* @return {undefined}
*/
populateDraft(node) {
// Used in set reading goal
let key = node.dataset.cacheDraft;
let value = window.localStorage.getItem(key);
if (!value) {
return;
}
node.value = value;
}
/**
* Post a status with ajax
*
* @param {} event
* @return {undefined}
*/
submitStatus(event) {
const form = event.currentTarget;
let trigger = event.submitter;
// Safari doesn't understand "submitter"
if (!trigger) {
trigger = event.currentTarget.querySelector("button[type=submit]");
}
// This allows the form to submit in the old fashioned way if there's a problem
if (!trigger || !form) {
return;
}
event.preventDefault();
BookWyrm.addRemoveClass(form, 'is-processing', true);
trigger.setAttribute('disabled', null);
BookWyrm.ajaxPost(form).finally(() => {
// Change icon to remove ongoing activity on the current UI.
// Enable back the element used to submit the form.
BookWyrm.addRemoveClass(form, 'is-processing', false);
trigger.removeAttribute('disabled');
})
.then(response => {
if (!response.ok) {
throw new Error();
}
this.submitStatusSuccess(form);
})
.catch(error => {
console.warn(error);
this.announceMessage('status-error-message');
});
}
/**
* Show a message in the live region
*
* @param {String} the id of the message dom element
* @return {undefined}
*/
announceMessage(message_id) {
const element = document.getElementById(message_id);
let copy = element.cloneNode(true);
copy.id = null;
element.insertAdjacentElement('beforebegin', copy);
BookWyrm.addRemoveClass(copy, 'is-hidden', false);
setTimeout(function() {
copy.remove();
}, 10000, copy);
}
/**
* Success state for a posted status
*
* @param {Object} the html form that was submitted
* @return {undefined}
*/
submitStatusSuccess(form) {
// Clear form data
form.reset();
// Clear localstorage
form.querySelectorAll('[data-cache-draft]')
.forEach(node => window.localStorage.removeItem(node.dataset.cacheDraft));
// Close modals
let modal = form.closest(".modal.is-active");
if (modal) {
modal.getElementsByClassName("modal-close")[0].click();
// Update shelve buttons
document.querySelectorAll("[data-shelve-button-book='" + form.book.value +"']")
.forEach(button => this.cycleShelveButtons(button, form.reading_status.value));
return;
}
// Close reply panel
let reply = form.closest(".reply-panel");
if (reply) {
document.querySelector("[data-controls=" + reply.id + "]").click();
}
this.announceMessage('status-success-message');
}
/**
* Change which buttons are available for a shelf
*
* @param {Object} html button dom element
* @param {String} the identifier of the selected shelf
* @return {undefined}
*/
cycleShelveButtons(button, identifier) {
// Pressed button
let shelf = button.querySelector("[data-shelf-identifier='" + identifier + "']");
let next_identifier = shelf.dataset.shelfNext;
// Set all buttons to hidden
button.querySelectorAll("[data-shelf-identifier]")
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", true));
// Button that should be visible now
let next = button.querySelector("[data-shelf-identifier=" + next_identifier + "]");
// Show the desired button
BookWyrm.addRemoveClass(next, "is-hidden", false);
// ------ update the dropdown buttons
// Remove existing hidden class
button.querySelectorAll("[data-shelf-dropdown-identifier]")
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", false));
// Remove existing disabled states
button.querySelectorAll("[data-shelf-dropdown-identifier] button")
.forEach(item => item.disabled = false);
next_identifier = next_identifier == 'complete' ? 'read' : next_identifier;
// Disable the current state
button.querySelector(
"[data-shelf-dropdown-identifier=" + identifier + "] button"
).disabled = true;
let main_button = button.querySelector(
"[data-shelf-dropdown-identifier=" + next_identifier + "]"
);
// Hide the option that's shown as the main button
BookWyrm.addRemoveClass(main_button, "is-hidden", true);
// Just hide the other two menu options, idk what to do with them
button.querySelectorAll("[data-extra-options]")
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", true));
// Close menu
let menu = button.querySelector(".dropdown-trigger[aria-expanded=true]");
if (menu) {
menu.click();
}
}
/**
* Reveal half-stars
*
* @param {Event} event
* @return {undefined}
*/
toggleStar(event) {
const label = event.currentTarget;
let wholeStar = document.getElementById(label.getAttribute("for"));
if (wholeStar.checked) {
event.preventDefault();
let halfStar = document.getElementById(label.dataset.forHalf);
wholeStar.checked = null;
halfStar.checked = "checked";
}
}
}();

View file

@ -86,10 +86,12 @@ class SuggestedUsers(RedisStore):
values = self.get_store(self.store_id(user), withscores=True) values = self.get_store(self.store_id(user), withscores=True)
results = [] results = []
# annotate users with mutuals and shared book counts # annotate users with mutuals and shared book counts
for user_id, rank in values[:5]: for user_id, rank in values:
counts = self.get_counts_from_rank(rank) counts = self.get_counts_from_rank(rank)
try: try:
user = models.User.objects.get(id=user_id) user = models.User.objects.get(
id=user_id, is_active=True, bookwyrm_user=True
)
except models.User.DoesNotExist as err: except models.User.DoesNotExist as err:
# if this happens, the suggestions are janked way up # if this happens, the suggestions are janked way up
logger.exception(err) logger.exception(err)
@ -97,6 +99,8 @@ class SuggestedUsers(RedisStore):
user.mutuals = counts["mutuals"] user.mutuals = counts["mutuals"]
# user.shared_books = counts["shared_books"] # user.shared_books = counts["shared_books"]
results.append(user) results.append(user)
if len(results) >= 5:
break
return results return results
@ -178,13 +182,21 @@ def update_suggestions_on_unfollow(sender, instance, **kwargs):
@receiver(signals.post_save, sender=models.User) @receiver(signals.post_save, sender=models.User)
# pylint: disable=unused-argument, too-many-arguments # pylint: disable=unused-argument, too-many-arguments
def add_new_user(sender, instance, created, update_fields=None, **kwargs): def update_user(sender, instance, created, update_fields=None, **kwargs):
"""a new user, wow how cool""" """an updated user, neat"""
# a new user is found, create suggestions for them # a new user is found, create suggestions for them
if created and instance.local: if created and instance.local:
rerank_suggestions_task.delay(instance.id) rerank_suggestions_task.delay(instance.id)
if update_fields and not "discoverable" in update_fields: # we know what fields were updated and discoverability didn't change
if not instance.bookwyrm_user or (
update_fields and not "discoverable" in update_fields
):
return
# deleted the user
if not created and not instance.is_active:
remove_user_task.delay(instance.id)
return return
# this happens on every save, not just when discoverability changes, annoyingly # this happens on every save, not just when discoverability changes, annoyingly
@ -194,28 +206,61 @@ def add_new_user(sender, instance, created, update_fields=None, **kwargs):
remove_user_task.delay(instance.id) remove_user_task.delay(instance.id)
@app.task @receiver(signals.post_save, sender=models.FederatedServer)
def domain_level_update(sender, instance, created, update_fields=None, **kwargs):
"""remove users on a domain block"""
if (
not update_fields
or "status" not in update_fields
or instance.application_type != "bookwyrm"
):
return
if instance.status == "blocked":
bulk_remove_instance_task.delay(instance.id)
return
bulk_add_instance_task.delay(instance.id)
# ------------------- TASKS
@app.task(queue="low_priority")
def rerank_suggestions_task(user_id): def rerank_suggestions_task(user_id):
"""do the hard work in celery""" """do the hard work in celery"""
suggested_users.rerank_user_suggestions(user_id) suggested_users.rerank_user_suggestions(user_id)
@app.task @app.task(queue="low_priority")
def rerank_user_task(user_id, update_only=False): def rerank_user_task(user_id, update_only=False):
"""do the hard work in celery""" """do the hard work in celery"""
user = models.User.objects.get(id=user_id) user = models.User.objects.get(id=user_id)
suggested_users.rerank_obj(user, update_only=update_only) suggested_users.rerank_obj(user, update_only=update_only)
@app.task @app.task(queue="low_priority")
def remove_user_task(user_id): def remove_user_task(user_id):
"""do the hard work in celery""" """do the hard work in celery"""
user = models.User.objects.get(id=user_id) user = models.User.objects.get(id=user_id)
suggested_users.remove_object_from_related_stores(user) suggested_users.remove_object_from_related_stores(user)
@app.task @app.task(queue="medium_priority")
def remove_suggestion_task(user_id, suggested_user_id): def remove_suggestion_task(user_id, suggested_user_id):
"""remove a specific user from a specific user's suggestions""" """remove a specific user from a specific user's suggestions"""
suggested_user = models.User.objects.get(id=suggested_user_id) suggested_user = models.User.objects.get(id=suggested_user_id)
suggested_users.remove_suggestion(user_id, suggested_user) suggested_users.remove_suggestion(user_id, suggested_user)
@app.task(queue="low_priority")
def bulk_remove_instance_task(instance_id):
"""remove a bunch of users from recs"""
for user in models.User.objects.filter(federated_server__id=instance_id):
suggested_users.remove_object_from_related_stores(user)
@app.task(queue="low_priority")
def bulk_add_instance_task(instance_id):
"""remove a bunch of users from recs"""
for user in models.User.objects.filter(federated_server__id=instance_id):
suggested_users.rerank_obj(user, update_only=False)

View file

@ -2,10 +2,10 @@
import os import os
from celery import Celery from celery import Celery
from bookwyrm import settings from celerywyrm import settings
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "celerywyrm.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "celerywyrm.settings")
app = Celery( app = Celery(
"tasks", broker=settings.CELERY_BROKER, backend=settings.CELERY_RESULT_BACKEND "tasks", broker=settings.CELERY_BROKER_URL, backend=settings.CELERY_RESULT_BACKEND
) )

View file

@ -4,7 +4,6 @@
{% load humanize %} {% load humanize %}
{% load utilities %} {% load utilities %}
{% load static %} {% load static %}
{% load layout %}
{% block title %}{{ book|book_title }}{% endblock %} {% block title %}{{ book|book_title }}{% endblock %}
@ -43,7 +42,7 @@
</p> </p>
{% endif %} {% endif %}
{% if book.authors %} {% if book.authors.exists %}
<div class="subtitle"> <div class="subtitle">
{% trans "by" %} {% include 'snippets/authors.html' with book=book %} {% trans "by" %} {% include 'snippets/authors.html' with book=book %}
</div> </div>
@ -326,5 +325,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{% static "js/vendor/tabs.js" %}"></script> <script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script>
{% endblock %} {% endblock %}

View file

@ -14,7 +14,11 @@
<h2 class="modal-card-title is-flex-shrink-1" id="modal_card_title_{{ controls_text }}_{{ controls_uid }}"> <h2 class="modal-card-title is-flex-shrink-1" id="modal_card_title_{{ controls_text }}_{{ controls_uid }}">
{% block modal-title %}{% endblock %} {% block modal-title %}{% endblock %}
</h2> </h2>
{% include 'snippets/toggle/toggle_button.html' with label=label class="delete" nonbutton=True %} {% if static %}
<a href="/" class="delete">{{ label }}</a>
{% else %}
{% include 'snippets/toggle/toggle_button.html' with label=label class="delete" nonbutton=True %}
{% endif %}
</header> </header>
{% block modal-form-open %}{% endblock %} {% block modal-form-open %}{% endblock %}
{% if not no_body %} {% if not no_body %}
@ -27,6 +31,10 @@
</footer> </footer>
{% block modal-form-close %}{% endblock %} {% block modal-form-close %}{% endblock %}
</div> </div>
{% include 'snippets/toggle/toggle_button.html' with label=label class="modal-close is-large" nonbutton=True %} {% if static %}
<a href="/" class="modal-close is-large">{{ label }}</a>
{% else %}
{% include 'snippets/toggle/toggle_button.html' with label=label class="modal-close is-large" nonbutton=True %}
{% endif %}
</div> </div>

View file

@ -0,0 +1,11 @@
{% load i18n %}
{% trans "Help" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text class="ml-3 is-rounded is-small is-white p-0 pb-1" icon="question-circle is-size-6" controls_text=controls_text controls_uid=controls_uid %}
<aside class="notification is-hidden transition-y is-pulled-left mb-2" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}">
{% trans "Close" as button_text %}
{% include 'snippets/toggle/close_button.html' with label=button_text class="delete" nonbutton=True controls_text=controls_text controls_uid=controls_uid %}
{% block tooltip_content %}{% endblock %}
</aside>

View file

@ -22,7 +22,7 @@
{% blocktrans with tab_key=tab.key %}load <span data-poll="stream/{{ tab_key }}">0</span> unread status(es){% endblocktrans %} {% blocktrans with tab_key=tab.key %}load <span data-poll="stream/{{ tab_key }}">0</span> unread status(es){% endblocktrans %}
</a> </a>
{% if request.user.show_goal and not goal and tab.key == streams.first.key %} {% if request.user.show_goal and not goal and tab.key == 'home' %}
{% now 'Y' as year %} {% now 'Y' as year %}
<section class="block"> <section class="block">
{% include 'snippets/goal_card.html' with year=year %} {% include 'snippets/goal_card.html' with year=year %}
@ -37,7 +37,7 @@
<div class="block content"> <div class="block content">
<p>{% trans "There aren't any activities right now! Try following a user to get started" %}</p> <p>{% trans "There aren't any activities right now! Try following a user to get started" %}</p>
{% if suggested_users %} {% if request.user.show_suggested_users and suggested_users %}
{# suggested users for when things are very lonely #} {# suggested users for when things are very lonely #}
{% include 'feed/suggested_users.html' with suggested_users=suggested_users %} {% include 'feed/suggested_users.html' with suggested_users=suggested_users %}
{% endif %} {% endif %}
@ -46,7 +46,7 @@
{% for activity in activities %} {% for activity in activities %}
{% if not activities.number > 1 and forloop.counter0 == 2 and suggested_users %} {% if request.user.show_suggested_users and not activities.number > 1 and forloop.counter0 == 2 and suggested_users %}
{# suggested users on the first page, two statuses down #} {# suggested users on the first page, two statuses down #}
{% include 'feed/suggested_users.html' with suggested_users=suggested_users %} {% include 'feed/suggested_users.html' with suggested_users=suggested_users %}
{% endif %} {% endif %}

View file

@ -106,5 +106,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{% static "js/vendor/tabs.js" %}"></script> <script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script>
{% endblock %} {% endblock %}

View file

@ -1,6 +1,15 @@
{% load i18n %} {% load i18n %}
<section class="block"> <section class="block">
<h2 class="title is-5">{% trans "Who to follow" %}</h2> <header class="columns">
<div class="column">
<h2 class="title is-5">{% trans "Who to follow" %}</h2>
</div>
<form class="column is-narrow" action="{% url 'hide-suggestions' %}" method="POST">
{% csrf_token %}
{% trans "Don't show suggested users" as button_text %}
<button type="submit" class="delete" title="{{ button_text }}">{{ button_text }}</button>
</form>
</header>
{% include 'snippets/suggested_users.html' with suggested_users=suggested_users %} {% include 'snippets/suggested_users.html' with suggested_users=suggested_users %}
<a class="help" href="{% url 'directory' %}">{% trans "View directory" %} <span class="icon icon-arrow-right"></span></a> <a class="help" href="{% url 'directory' %}">{% trans "View directory" %} <span class="icon icon-arrow-right"></span></a>
</section> </section>

View file

@ -12,9 +12,14 @@
<div class="columns"> <div class="columns">
<div class="column is-half"> <div class="column is-half">
<label class="label" for="source">
{% trans "Data source:" %} <div class="field">
</label> <label class="label is-pulled-left" for="source">
{% trans "Data source:" %}
</label>
{% include 'import/tooltip.html' with controls_text="goodreads-tooltip" %}
</div>
<div class="select block"> <div class="select block">
<select name="source" id="source"> <select name="source" id="source">
<option value="GoodReads" {% if current == 'GoodReads' %}selected{% endif %}> <option value="GoodReads" {% if current == 'GoodReads' %}selected{% endif %}>

View file

@ -40,7 +40,7 @@
<div class="block"> <div class="block">
<h2 class="title is-4">{% trans "Failed to load" %}</h2> <h2 class="title is-4">{% trans "Failed to load" %}</h2>
{% if not job.retry %} {% if not job.retry %}
<form name="retry" action="/import/{{ job.id }}" method="post"> <form name="retry" action="/import/{{ job.id }}" method="post" class="box">
{% csrf_token %} {% csrf_token %}
{% with failed_count=failed_items|length %} {% with failed_count=failed_items|length %}
@ -156,5 +156,5 @@
{% endspaceless %}{% endblock %} {% endspaceless %}{% endblock %}
{% block scripts %} {% block scripts %}
<script src="{% static "js/check_all.js" %}"></script> <script src="{% static "js/check_all.js" %}?v={{ js_cache }}"></script>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,8 @@
{% extends 'components/tooltip.html' %}
{% load i18n %}
{% block tooltip_content %}
{% trans 'You can download your GoodReads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener">Import/Export page</a> of your GoodReads account.' %}
{% endblock %}

View file

@ -121,7 +121,7 @@
{% endif %} {% endif %}
{% if perms.bookwyrm.moderate_user %} {% if perms.bookwyrm.moderate_user %}
<li> <li>
<a href="{% url 'settings-users' %}" class="navbar-item"> <a href="{% url 'settings-dashboard' %}" class="navbar-item">
{% trans 'Admin' %} {% trans 'Admin' %}
</a> </a>
</li> </li>
@ -210,6 +210,11 @@
</div> </div>
</div> </div>
<div role="region" aria-live="polite" id="live-messages">
<p id="status-success-message" class="live-message is-sr-only is-hidden">{% trans "Successfully posted status" %}</p>
<p id="status-error-message" class="live-message notification is-danger p-3 pr-5 pl-5 is-hidden">{% trans "Error posting status" %}</p>
</div>
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">
<div class="columns"> <div class="columns">
@ -249,8 +254,11 @@
<script> <script>
var csrf_token = '{{ csrf_token }}'; var csrf_token = '{{ csrf_token }}';
</script> </script>
<script src="{% static "js/bookwyrm.js" %}"></script>
<script src="{% static "js/localstorage.js" %}"></script> <script src="{% static "js/bookwyrm.js" %}?v={{ js_cache }}"></script>
<script src="{% static "js/localstorage.js" %}?v={{ js_cache }}"></script>
<script src="{% static "js/status_cache.js" %}?v={{ js_cache }}"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View file

@ -1,4 +1,4 @@
{% extends 'lists/list_layout.html' %} {% extends 'lists/layout.html' %}
{% load i18n %} {% load i18n %}
{% block panel %} {% block panel %}

View file

@ -0,0 +1,21 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% block modal-title %}{% trans "Delete this list?" %}{% endblock %}
{% block modal-body %}
{% trans "This action cannot be un-done" %}
{% endblock %}
{% block modal-footer %}
<form name="delete-list-{{ list.id }}" action="{% url 'delete-list' list.id %}" method="POST">
{% csrf_token %}
<input type="hidden" name="id" value="{{ list.id }}">
<button class="button is-danger" type="submit">
{% trans "Delete" %}
</button>
{% trans "Cancel" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="delete_list" controls_uid=list.id %}
</form>
{% endblock %}

View file

@ -9,4 +9,5 @@
<form name="edit-list" method="post" action="{% url 'list' list.id %}"> <form name="edit-list" method="post" action="{% url 'list' list.id %}">
{% include 'lists/form.html' %} {% include 'lists/form.html' %}
</form> </form>
{% include "lists/delete_list_modal.html" with controls_text="delete_list" controls_uid=list.id %}
{% endblock %} {% endblock %}

View file

@ -3,7 +3,7 @@
<input type="hidden" name="user" value="{{ request.user.id }}"> <input type="hidden" name="user" value="{{ request.user.id }}">
<div class="columns"> <div class="columns">
<div class="column"> <div class="column is-two-thirds">
<div class="field"> <div class="field">
<label class="label" for="id_name">{% trans "Name:" %}</label> <label class="label" for="id_name">{% trans "Name:" %}</label>
{{ list_form.name }} {{ list_form.name }}
@ -34,12 +34,19 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
<div class="field has-addons"> <div class="columns is-mobile">
<div class="control"> <div class="column">
{% include 'snippets/privacy_select.html' with current=list.privacy %} <div class="field has-addons">
<div class="control">
{% include 'snippets/privacy_select.html' with current=list.privacy %}
</div>
<div class="control">
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
</div>
</div>
</div> </div>
<div class="control"> <div class="column is-narrow">
<button type="submit" class="button is-primary">{% trans "Save" %}</button> {% trans "Delete list" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with class="is-danger" text=button_text icon_with_text="x" controls_text="delete_list" controls_uid=list.id focus="modal_title_delete_list" %}
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
{% extends 'lists/list_layout.html' %} {% extends 'lists/layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load markdown %} {% load markdown %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %} {% block title %}

View file

@ -43,9 +43,19 @@
</div> </div>
<div class="block"> <div class="block">
<label class="checkbox label" for="id_show_goal"> <label class="checkbox label" for="id_show_goal">
{% trans "Show set reading goal prompt in feed:" %} {% trans "Show reading goal prompt in feed:" %}
{{ form.show_goal }} {{ form.show_goal }}
</label> </label>
<label class="checkbox label" for="id_show_goal">
{% trans "Show suggested users:" %}
{{ form.show_suggested_users }}
</label>
<label class="checkbox label" for="id_discoverable">
{% trans "Show this account in suggested users:" %}
{{ form.discoverable }}
</label>
{% url 'directory' as path %}
<p class="help">{% blocktrans %}Your account will show up in the <a href="{{ path }}">directory</a>, and may be recommended to other BookWyrm users.{% endblocktrans %}</p>
</div> </div>
<div class="block"> <div class="block">
<label class="checkbox label" for="id_manually_approves_followers"> <label class="checkbox label" for="id_manually_approves_followers">
@ -61,14 +71,6 @@
{{ form.default_post_privacy }} {{ form.default_post_privacy }}
</div> </div>
</div> </div>
<div class="block">
<label class="checkbox label" for="id_discoverable">
{% trans "Show this account in suggested users:" %}
{{ form.discoverable }}
</label>
{% url 'directory' as path %}
<p class="help">{% blocktrans %}Your account will show up in the <a href="{{ path }}">directory</a>, and may be recommended to other BookWyrm users.{% endblocktrans %}</p>
</div>
<div class="block"> <div class="block">
<label class="label" for="id_preferred_timezone">{% trans "Preferred Timezone: " %}</label> <label class="label" for="id_preferred_timezone">{% trans "Preferred Timezone: " %}</label>
<div class="select"> <div class="select">

View file

@ -9,6 +9,6 @@ Finish "{{ book_title }}"
{% block content %} {% block content %}
{% include "snippets/reading_modals/finish_reading_modal.html" with book=book active=True %} {% include "snippets/reading_modals/finish_reading_modal.html" with book=book active=True static=True %}
{% endblock %} {% endblock %}

View file

@ -9,6 +9,6 @@ Start "{{ book_title }}"
{% block content %} {% block content %}
{% include "snippets/reading_modals/start_reading_modal.html" with book=book active=True %} {% include "snippets/reading_modals/start_reading_modal.html" with book=book active=True static=True %}
{% endblock %} {% endblock %}

View file

@ -9,6 +9,6 @@ Want to Read "{{ book_title }}"
{% block content %} {% block content %}
{% include "snippets/reading_modals/want_to_read_modal.html" with book=book active=True %} {% include "snippets/reading_modals/want_to_read_modal.html" with book=book active=True static=True %}
{% endblock %} {% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %}{% load humanize %} {% load i18n %}{% load humanize %}
{% block title %}{% trans "Announcement" %} - {{ announcement.preview }}{% endblock %} {% block title %}{% trans "Announcement" %} - {{ announcement.preview }}{% endblock %}

View file

@ -13,21 +13,21 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}"> <input type="hidden" name="user" value="{{ request.user.id }}">
<p> <p>
<label class="label" for="id_preview">Preview:</label> <label class="label" for="id_preview">{% trans "Preview:" %}</label>
{{ form.preview }} {{ form.preview }}
{% for error in form.preview.errors %} {% for error in form.preview.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</p> </p>
<p> <p>
<label class="label" for="id_content">Content:</label> <label class="label" for="id_content">{% trans "Content:" %}</label>
{{ form.content }} {{ form.content }}
{% for error in form.content.errors %} {% for error in form.content.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</p> </p>
<p> <p>
<label class="label" for="id_event_date">Event date:</label> <label class="label" for="id_event_date">{% trans "Event date:" %}</label>
<input type="date" name="event_date" value="{{ form.event_date.value|date:'Y-m-d' }}" class="input" id="id_event_date"> <input type="date" name="event_date" value="{{ form.event_date.value|date:'Y-m-d' }}" class="input" id="id_event_date">
{% for error in form.event_date.errors %} {% for error in form.event_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
@ -37,7 +37,7 @@
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<p> <p>
<label class="label" for="id_start_date">Start date:</label> <label class="label" for="id_start_date">{% trans "Start date:" %}</label>
<input type="date" name="start_date" class="input" value="{{ form.start_date.value|date:'Y-m-d' }}" id="id_start_date"> <input type="date" name="start_date" class="input" value="{{ form.start_date.value|date:'Y-m-d' }}" id="id_start_date">
{% for error in form.start_date.errors %} {% for error in form.start_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
@ -46,7 +46,7 @@
</div> </div>
<div class="column"> <div class="column">
<p> <p>
<label class="label" for="id_end_date">End date:</label> <label class="label" for="id_end_date">{% trans "End date:" %}</label>
<input type="date" name="end_date" class="input" id="id_end_date" value="{{ form.end_date.value|date:'Y-m-d' }}"> <input type="date" name="end_date" class="input" id="id_end_date" value="{{ form.end_date.value|date:'Y-m-d' }}">
{% for error in form.end_date.errors %} {% for error in form.end_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
@ -55,7 +55,7 @@
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
<p> <p>
<label class="label" for="id_active">Active:</label> <label class="label" for="id_active">{% trans "Active:" %}</label>
{{ form.active }} {{ form.active }}
{% for error in form.active.errors %} {% for error in form.active.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %}{% load humanize %} {% load i18n %}{% load humanize %}
{% block title %}{% trans "Announcements" %}{% endblock %} {% block title %}{% trans "Announcements" %}{% endblock %}
@ -14,40 +14,46 @@
{% include 'settings/announcement_form.html' with controls_text="create_announcement" %} {% include 'settings/announcement_form.html' with controls_text="create_announcement" %}
</form> </form>
<table class="table is-striped"> <div class="block">
<tr> <table class="table is-striped">
<th> <tr>
{% url 'settings-announcements' as url %} <th>
{% trans "Date added" as text %} {% url 'settings-announcements' as url %}
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %} {% trans "Date added" as text %}
</th> {% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
<th> </th>
{% trans "Preview" as text %} <th>
{% include 'snippets/table-sort-header.html' with field="preview" sort=sort text=text %} {% trans "Preview" as text %}
</th> {% include 'snippets/table-sort-header.html' with field="preview" sort=sort text=text %}
<th> </th>
{% trans "Start date" as text %} <th>
{% include 'snippets/table-sort-header.html' with field="start_date" sort=sort text=text %} {% trans "Start date" as text %}
</th> {% include 'snippets/table-sort-header.html' with field="start_date" sort=sort text=text %}
<th> </th>
{% trans "End date" as text %} <th>
{% include 'snippets/table-sort-header.html' with field="end_date" sort=sort text=text %} {% trans "End date" as text %}
</th> {% include 'snippets/table-sort-header.html' with field="end_date" sort=sort text=text %}
<th> </th>
{% trans "Status" as text %} <th>
{% include 'snippets/table-sort-header.html' with field="active" sort=sort text=text %} {% trans "Status" as text %}
</th> {% include 'snippets/table-sort-header.html' with field="active" sort=sort text=text %}
</tr> </th>
{% for announcement in announcements %} </tr>
<tr> {% for announcement in announcements %}
<td>{{ announcement.created_date|naturalday }}</td> <tr>
<td><a href="{% url 'settings-announcements' announcement.id %}">{{ announcement.preview }}</a></td> <td>{{ announcement.created_date|naturalday }}</td>
<td>{{ announcement.start_date|naturaltime|default:'' }}</td> <td><a href="{% url 'settings-announcements' announcement.id %}">{{ announcement.preview }}</a></td>
<td>{{ announcement.end_date|naturaltime|default:'' }}</td> <td>{{ announcement.start_date|naturaltime|default:'' }}</td>
<td>{% if announcement.active %}{% trans "active" %}{% else %}{% trans "inactive" %}{% endif %}</td> <td>{{ announcement.end_date|naturaltime|default:'' }}</td>
</tr> <td>{% if announcement.active %}{% trans "active" %}{% else %}{% trans "inactive" %}{% endif %}</td>
{% endfor %} </tr>
</table> {% endfor %}
</table>
{% if not announcements %}
<p><em>{% trans "No announcements found." %}</em></p>
{% endif %}
</div>
{% include 'snippets/pagination.html' with page=announcements path=request.path %} {% include 'snippets/pagination.html' with page=announcements path=request.path %}
{% endblock %} {% endblock %}

View file

@ -0,0 +1,120 @@
{% extends 'settings/layout.html' %}
{% load i18n %}
{% load humanize %}
{% load static %}
{% block title %}{% trans "Dashboard" %}{% endblock %}
{% block header %}{% trans "Dashboard" %}{% endblock %}
{% block panel %}
<div class="columns block has-text-centered">
<div class="column is-3">
<div class="notification">
<h3>{% trans "Total users" %}</h3>
<p class="title is-5">{{ users|intcomma }}</p>
</div>
</div>
<div class="column is-3">
<div class="notification">
<h3>{% trans "Active this month" %}</h3>
<p class="title is-5">{{ active_users|intcomma }}</p>
</div>
</div>
<div class="column is-3">
<div class="notification">
<h3>{% trans "Statuses" %}</h3>
<p class="title is-5">{{ statuses|intcomma }}</p>
</div>
</div>
<div class="column is-3">
<div class="notification">
<h3>{% trans "Works" %}</h3>
<p class="title is-5">{{ works|intcomma }}</p>
</div>
</div>
</div>
<div class="columns block is-multiline">
{% if reports %}
<div class="column">
<a href="{% url 'settings-reports' %}" class="notification is-warning is-block">
{% blocktrans trimmed count counter=reports with display_count=reports|intcomma %}
{{ display_count }} open report
{% plural %}
{{ display_count }} open reports
{% endblocktrans %}
</a>
</div>
{% endif %}
{% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
<div class="column">
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success is-light">
{% blocktrans trimmed count counter=invite_requests with display_count=invite_requests|intcomma %}
{{ display_count }} invite request
{% plural %}
{{ display_count }} invite requests
{% endblocktrans %}
</a>
</div>
{% endif %}
</div>
<div class="block content">
<h2>{% trans "Instance Activity" %}</h2>
<form method="get" action="{% url 'settings-dashboard' %}" class="notification has-background-white-bis">
<div class="is-flex is-align-items-flex-end">
<div class="ml-1 mr-1">
<label class="label">
{% trans "Start date:" %}
<input class="input" type="date" name="start" value="{{ start }}">
</label>
</div>
<div class="ml-1 mr-1">
<label class="label">
{% trans "End date:" %}
<input class="input" type="date" name="end" value="{{ end }}">
</label>
</div>
<div class="ml-1 mr-1">
<label class="label">
{% trans "Interval:" %}
<div class="select">
<select name="days">
<option value="1" {% if interval == 1 %}selected{% endif %}>{% trans "Days" %}</option>
<option value="7" {% if interval == 7 %}selected{% endif %}>{% trans "Weeks" %}</option>
</select>
</div>
</label>
</div>
<div class="ml-1 mr-1">
<button class="button is-link" type="submit">{% trans "Submit" %}</button>
</div>
</div>
</form>
<div class="columns">
<div class="column">
<h3>{% trans "User signup activity" %}</h3>
<div class="box">
<canvas id="user_stats"></canvas>
</div>
</div>
<div class="column">
<h3>{% trans "Status activity" %}</h3>
<div class="box">
<canvas id="status_stats"></canvas>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
{% include 'settings/dashboard_user_chart.html' %}
{% include 'settings/dashboard_status_chart.html' %}
{% endblock %}

View file

@ -0,0 +1,26 @@
{% load i18n %}
<script>
const status_labels = [{% for label in status_stats.labels %}"{{ label }}",{% endfor %}];
const status_data = {
labels: status_labels,
datasets: [{
label: '{% trans "Statuses posted" %}',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: {{ status_stats.total }},
}]
};
// === include 'setup' then 'config' above ===
const status_config = {
type: 'bar',
data: status_data,
options: {}
};
var statusStats = new Chart(
document.getElementById('status_stats'),
status_config
);
</script>

View file

@ -0,0 +1,29 @@
{% load i18n %}
<script>
const labels = [{% for label in user_stats.labels %}"{{ label }}",{% endfor %}];
const data = {
labels: labels,
datasets: [{
label: '{% trans "Total" %}',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: {{ user_stats.total }},
}, {
label: '{% trans "Active this month" %}',
backgroundColor: 'rgb(75, 192, 192)',
borderColor: 'rgb(75, 192, 192)',
data: {{ user_stats.active }},
}]
};
const config = {
type: 'line',
data: data,
options: {}
};
var userStats = new Chart(
document.getElementById('user_stats'),
config
);
</script>

View file

@ -0,0 +1,30 @@
{% extends 'components/inline_form.html' %}
{% load i18n %}
{% block header %}
{% trans "Add domain" %}
{% endblock %}
{% block form %}
<form name="add-domain" method="post" action="{% url 'settings-email-blocks' %}">
{% csrf_token %}
<label class="label" for="id_event_date">{% trans "Domain:" %}</label>
<div class="field has-addons">
<div class="control">
<div class="button is-disabled">@</div>
</div>
<div class="control">
{{ form.domain }}
</div>
</div>
{% for error in form.domain.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<div class="field">
<div class="control">
<button type="submit" class="button is-primary">{% trans "Add" %}</button>
</div>
</div>
</form>
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Add instance" %}{% endblock %} {% block title %}{% trans "Add instance" %}{% endblock %}

View file

@ -0,0 +1,61 @@
{% extends 'settings/layout.html' %}
{% load i18n %}
{% load humanize %}
{% block title %}{% trans "Email Blocklist" %}{% endblock %}
{% block header %}{% trans "Email Blocklist" %}{% endblock %}
{% block edit-button %}
{% trans "Add domain" as button_text %}
{% include 'snippets/toggle/open_button.html' with controls_text="add_domain" icon_with_text="plus" text=button_text focus="add_domain_header" %}
{% endblock %}
{% block panel %}
{% include 'settings/domain_form.html' with controls_text="add_domain" class="block" %}
<p class="notification block">
{% trans "When someone tries to register with an email from this domain, no account will be created. The registration process will appear to have worked." %}
</p>
<table class="table is-striped is-fullwidth">
<tr>
{% url 'settings-federation' as url %}
<th>
{% trans "Domain" %}
</th>
<th>{% trans "Users" %}</th>
<th>
{% trans "Options" %}
</th>
</tr>
{% for domain in domains %}
<tr>
<td>{{ domain.domain }}</td>
<td>
<a href="{% url 'settings-users' %}?email=@{{ domain.domain }}">
{% with user_count=domain.users.count %}
{% blocktrans trimmed count conter=user_count with display_count=user_count|intcomma %}
{{ display_count }} user
{% plural %}
{{ display_count }} users
{% endblocktrans %}
{% endwith %}
</a>
</td>
<td>
<form name="remove-{{ domain.id }}" action="{% url 'settings-email-blocks-delete' domain.id %}" method="post">
{% csrf_token %}
{% trans "Delete" as button_text %}
<button class="button" type="submit">
<span class="icon icon-x" title="{{ button_text }}" aria-hidden="true"></span>
<span class="is-hidden-mobile">{{ button_text }}</span>
</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% load markdown %} {% load markdown %}
@ -29,7 +29,7 @@
</div> </div>
<div class="is-flex"> <div class="is-flex">
<dt>{% trans "Status:" %}</dt> <dt>{% trans "Status:" %}</dt>
<dd>{{ server.status }}</dd> <dd>{{ server.get_status_display }}</dd>
</div> </div>
</dl> </dl>
</div> </div>

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Federated Instances" %}{% endblock %} {% block title %}{% trans "Federated Instances" %}{% endblock %}
@ -12,6 +12,19 @@
{% endblock %} {% endblock %}
{% block panel %} {% block panel %}
<div class="tabs">
<ul>
{% url 'settings-federation' status='federated' as url %}
<li {% if request.path in url %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Federated" %}</a>
</li>
{% url 'settings-federation' status='blocked' as url %}
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Blocked" %}</a>
</li>
</ul>
</div>
<table class="table is-striped"> <table class="table is-striped">
<tr> <tr>
{% url 'settings-federation' as url %} {% url 'settings-federation' as url %}
@ -20,21 +33,30 @@
{% include 'snippets/table-sort-header.html' with field="server_name" sort=sort text=text %} {% include 'snippets/table-sort-header.html' with field="server_name" sort=sort text=text %}
</th> </th>
<th> <th>
{% trans "Date federated" as text %} {% trans "Date added" as text %}
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %} {% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
</th> </th>
<th> <th>
{% trans "Software" as text %} {% trans "Software" as text %}
{% include 'snippets/table-sort-header.html' with field="application_type" sort=sort text=text %} {% include 'snippets/table-sort-header.html' with field="application_type" sort=sort text=text %}
</th> </th>
<th>
{% trans "Users" %}
</th>
<th>{% trans "Status" %}</th> <th>{% trans "Status" %}</th>
</tr> </tr>
{% for server in servers %} {% for server in servers %}
<tr> <tr>
<td><a href="{% url 'settings-federated-server' server.id %}">{{ server.server_name }}</a></td> <td><a href="{% url 'settings-federated-server' server.id %}">{{ server.server_name }}</a></td>
<td>{{ server.created_date }}</td> <td>{{ server.created_date }}</td>
<td>{{ server.application_type }} ({{ server.application_version }})</td> <td>
<td>{{ server.status }}</td> {% if server.application_type %}
{{ server.application_type }}
{% if server.application_version %}({{ server.application_version }}){% endif %}
{% endif %}
</td>
<td>{{ server.user_set.count }}</td>
<td>{{ server.get_status_display }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -18,6 +18,13 @@
<div class="block columns"> <div class="block columns">
<nav class="menu column is-one-quarter"> <nav class="menu column is-one-quarter">
<h2 class="menu-label">
{% url 'settings-dashboard' as url %}
<a
href="{{ url }}"
{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}
>{% trans "Dashboard" %}</a>
</h2>
{% if perms.bookwyrm.create_invites %} {% if perms.bookwyrm.create_invites %}
<h2 class="menu-label">{% trans "Manage Users" %}</h2> <h2 class="menu-label">{% trans "Manage Users" %}</h2>
<ul class="menu-list"> <ul class="menu-list">
@ -32,12 +39,6 @@
{% url 'settings-invites' as alt_url %} {% url 'settings-invites' as alt_url %}
<a href="{{ url }}"{% if url in request.path or request.path in alt_url %} class="is-active" aria-selected="true"{% endif %}>{% trans "Invites" %}</a> <a href="{{ url }}"{% if url in request.path or request.path in alt_url %} class="is-active" aria-selected="true"{% endif %}>{% trans "Invites" %}</a>
</li> </li>
{% if perms.bookwyrm.moderate_user %}
<li>
{% url 'settings-reports' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Reports" %}</a>
</li>
{% endif %}
{% if perms.bookwyrm.control_federation %} {% if perms.bookwyrm.control_federation %}
<li> <li>
{% url 'settings-federation' as url %} {% url 'settings-federation' as url %}
@ -46,6 +47,19 @@
{% endif %} {% endif %}
</ul> </ul>
{% endif %} {% endif %}
{% if perms.bookwyrm.moderate_user %}
<h2 class="menu-label">{% trans "Moderation" %}</h2>
<ul class="menu-list">
<li>
{% url 'settings-reports' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Reports" %}</a>
</li>
<li>
{% url 'settings-email-blocks' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Email Blocklist" %}</a>
</li>
</ul>
{% endif %}
{% if perms.bookwyrm.edit_instance_settings %} {% if perms.bookwyrm.edit_instance_settings %}
<h2 class="menu-label">{% trans "Instance Settings" %}</h2> <h2 class="menu-label">{% trans "Instance Settings" %}</h2>
<ul class="menu-list"> <ul class="menu-list">

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
{% block header %}{% trans "Invite Requests" %}{% endblock %} {% block header %}{% trans "Invite Requests" %}{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% block header %}{% trans "Invites" %}{% endblock %} {% block header %}{% trans "Invites" %}{% endblock %}
{% load humanize %} {% load humanize %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Add instance" %}{% endblock %} {% block title %}{% trans "Add instance" %}{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Site Settings" %}{% endblock %} {% block title %}{% trans "Site Settings" %}{% endblock %}
@ -23,6 +23,11 @@
<label class="label" for="id_instance_description">{% trans "Instance description:" %}</label> <label class="label" for="id_instance_description">{% trans "Instance description:" %}</label>
{{ site_form.instance_description }} {{ site_form.instance_description }}
</div> </div>
<div class="field">
<label class="label mb-0" for="id_short_description">{% trans "Short description:" %}</label>
<p class="help">{% trans "Used when the instance is previewed on joinbookwyrm.com. Does not support html or markdown." %}</p>
{{ site_form.instance_short_description }}
</div>
<div class="field"> <div class="field">
<label class="label" for="id_code_of_conduct">{% trans "Code of conduct:" %}</label> <label class="label" for="id_code_of_conduct">{% trans "Code of conduct:" %}</label>
{{ site_form.code_of_conduct }} {{ site_form.code_of_conduct }}

View file

@ -3,7 +3,7 @@
<div class="columns"> <div class="columns">
<div class="column is-narrow is-hidden-mobile"> <div class="column is-narrow is-hidden-mobile">
<figure class="block is-w-xl"> <figure class="block is-w-xl">
<img src="{% if site.logo %}/images/{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}" alt="BookWyrm logo"> <img src="{% if site.logo %}{% get_media_prefix %}{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}" alt="BookWyrm logo">
</figure> </figure>
</div> </div>
<div class="content"> <div class="content">

View file

@ -2,7 +2,7 @@
{% load utilities %} {% load utilities %}
{% spaceless %} {% spaceless %}
{% if book.authors %} {% if book.authors.exists %}
{% blocktrans trimmed with path=book.local_path title=book|book_title %} {% blocktrans trimmed with path=book.local_path title=book|book_title %}
<a href="{{ path }}">{{ title }}</a> by <a href="{{ path }}">{{ title }}</a> by
{% endblocktrans %} {% endblocktrans %}

View file

@ -26,13 +26,37 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
<label class="label" for="progress_{{ uuid }}">{% trans "Progress:" %}</label> <label class="label" for="progress_{{ uuid }}">{% trans "Progress:" %}</label>
<div class="field has-addons mb-0"> <div class="field has-addons mb-0">
<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 }}"
data-cache-draft="id_progress_comment_{{ book.id }}"
>
</div> </div>
<div class="control"> <div class="control">
<div class="select"> <div class="select">
<select name="progress_mode" aria-label="Progress mode"> <select
<option value="PG" {% if draft.progress_mode == 'PG' or readthrough.progress_mode == 'PG' %}selected{% endif %}>{% trans "pages" %}</option> name="progress_mode"
<option value="PCT" {% if draft.progress_mode == 'PCT' or readthrough.progress_mode == 'PCT' %}selected{% endif %}>{% trans "percent" %}</option> aria-label="Progress mode"
data-cache-draft="id_progress_mode_comment_{{ book.id }}"
>
<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>
</select> </select>
</div> </div>
</div> </div>

View file

@ -10,10 +10,11 @@ draft: an existing Status object that is providing default values for input fiel
{% endcomment %} {% endcomment %}
<textarea <textarea
name="content" name="content"
class="textarea" class="textarea save-draft"
data-cache-draft="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
id="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}" id="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
placeholder="{{ placeholder }}" placeholder="{{ placeholder }}"
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}" aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}"
{% if not optional and type != "quotation" %}required{% endif %} {% if not optional and type != "quotation" and type != "review" %}required{% endif %}
>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mention %}@{{ mention|username }} {% endif %}{{ draft.content|default:'' }}</textarea> >{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mention %}@{{ mention|username }} {% endif %}{{ draft.content|default:'' }}</textarea>

View file

@ -8,5 +8,7 @@
class="input" class="input"
id="id_content_warning_{{ uuid }}" id="id_content_warning_{{ uuid }}"
placeholder="{% trans 'Spoilers ahead!' %}" placeholder="{% trans 'Spoilers ahead!' %}"
value="{% firstof draft.content_warning parent_status.content_warning '' %}"> value="{% firstof draft.content_warning parent_status.content_warning '' %}"
data-cache-draft="id_content_warning_{{ book.id }}_{{ type }}"
>
</div> </div>

View file

@ -1,7 +1,15 @@
{% load i18n %} {% load i18n %}
<div class="control"> <div class="control">
<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"> <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"
data-cache-draft="id_sensitive_{{ book.id }}_{{ type }}{{ reply_parent.id }}"
>
{% trans "Include spoiler alert" as button_text %} {% trans "Include spoiler alert" as button_text %}
{% firstof draft.content_warning status.content_warning as pressed %} {% firstof draft.content_warning status.content_warning as pressed %}
{% include 'snippets/toggle/toggle_button.html' with text=button_text icon="warning is-size-4" controls_text="spoilers" controls_uid=uuid focus="id_content_warning" checkbox="id_show_spoilers" class="toggle-button" pressed=pressed %} {% include 'snippets/toggle/toggle_button.html' with text=button_text icon="warning is-size-4" controls_text="spoilers" controls_uid=uuid focus="id_content_warning" checkbox="id_show_spoilers" class="toggle-button" pressed=pressed %}

View file

@ -14,7 +14,7 @@ reply_parent: the Status object this post will be in reply to, if applicable
{% block form_open %} {% block form_open %}
{# default form tag syntax, can be overriddden #} {# default form tag syntax, can be overriddden #}
<form class="is-flex-grow-1" name="{{ type }}" action="/post/{{ type }}" method="post" id="tab_{{ type }}_{{ book.id }}{{ reply_parent.id }}"> <form class="is-flex-grow-1 submit-status" name="{{ type }}" action="/post/{{ type }}" method="post" id="tab_{{ type }}_{{ book.id }}{{ reply_parent.id }}">
{% endblock %} {% endblock %}
{% csrf_token %} {% csrf_token %}

View file

@ -16,7 +16,10 @@
</div> </div>
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
<button class="button is-link" type="submit">{% trans "Post" %}</button> <button class="button is-link" type="submit">
<span class="icon icon-spinner" aria-hidden="true"></span>
<span>{% trans "Post" %}</span>
</button>
</div> </div>
</div> </div>

View file

@ -11,8 +11,6 @@ draft: the content of an existing Status object to be edited (used in delete and
uuid: a unique identifier used to make html "id" attributes unique and clarify javascript controls uuid: a unique identifier used to make html "id" attributes unique and clarify javascript controls
{% endcomment %} {% endcomment %}
{% with type="quotation" %}
{% block pre_content_additions %} {% block pre_content_additions %}
<div class="field"> <div class="field">
<label class="label" for="id_quote_{{ book.id }}_{{ type }}"> <label class="label" for="id_quote_{{ book.id }}_{{ type }}">
@ -26,9 +24,48 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
id="id_quote_{{ book.id }}_{{ type }}" id="id_quote_{{ book.id }}_{{ type }}"
placeholder="{% blocktrans with book_title=book.title %}An excerpt from '{{ book_title }}'{% endblocktrans %}" placeholder="{% blocktrans with book_title=book.title %}An excerpt from '{{ book_title }}'{% endblocktrans %}"
required required
data-cache-draft="id_quote_{{ book.id }}_{{ type }}"
>{{ draft.quote|default:'' }}</textarea> >{{ draft.quote|default:'' }}</textarea>
</div> </div>
</div> </div>
<div class="field">
<label class="label" for="position_{{ uuid }}">{% trans "Position:" %}</label>
<div class="field has-addons mb-0">
<div class="control">
<div class="select">
<select
name="position_mode"
aria-label="Position mode"
data-cache-draft="id_position_mode_{{ book.id }}_{{ type }}"
>
<option
value="PG"
{% if draft.position_mode == 'PG' or not draft %}selected{% endif %}
>
{% trans "On page:" %}
</option>
<option
value="PCT"
{% if draft.position_mode == 'PCT' %}selected{% endif %}
>
{% trans "At percent:" %}
</option>
</select>
</div>
</div>
<div class="control">
<input
aria-label="{% if draft.position_mode == 'PG' %}Page{% else %}Percent{% endif %}"
class="input"
type="number"
min="0"
name="position"
size="3"
value="{% firstof draft.position '' %}"
id="position_{{ uuid }}"
data-cache-draft="id_position_{{ book.id }}_{{ type }}"
>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% endwith %}

View file

@ -11,13 +11,21 @@ draft: the content of an existing Status object to be edited (used in delete and
uuid: a unique identifier used to make html "id" attributes unique and clarify javascript controls uuid: a unique identifier used to make html "id" attributes unique and clarify javascript controls
{% endcomment %} {% endcomment %}
{% with type="review" %}
{% block pre_content_additions %} {% block pre_content_additions %}
<div class="field"> <div class="field">
<label class="label" for="id_name_{{ book.id }}">{% trans "Title:" %}</label> <label class="label" for="id_name_{{ book.id }}">{% trans "Title:" %}</label>
<div class="control"> <div class="control">
<input type="text" name="name" maxlength="255" class="input" required="" id="id_name_{{ book.id }}" placeholder="{% blocktrans with book_title=book.title %}Your review of '{{ book_title }}'{% endblocktrans %}" value="{% firstof draft.name ''%}"> <input
type="text"
name="name"
maxlength="255"
class="input"
required=""
id="id_name_{{ book.id }}"
placeholder="{% blocktrans with book_title=book.title %}Your review of '{{ book_title }}'{% endblocktrans %}"
value="{% firstof draft.name ''%}"
data-cache-draft="id_name_{{ book.id }}_{{ type }}"
>
</div> </div>
</div> </div>
@ -31,5 +39,3 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
{% block content_label %} {% block content_label %}
{% trans "Review:" %} {% trans "Review:" %}
{% endblock %} {% endblock %}
{% endwith %}

View file

@ -1,5 +1,6 @@
{% spaceless %} {% spaceless %}
{% load i18n %} {% load i18n %}
{% load stars %}
<div class=" <div class="
field is-grouped field is-grouped
@ -20,6 +21,24 @@
</label> </label>
{% for i in '12345'|make_list %} {% for i in '12345'|make_list %}
<label
class="is-sr-only"
for="{{ type|slugify }}_book{{ book.id }}_star_{{ forloop.counter0 }}_half"
>
{% blocktranslate trimmed count rating=forloop.counter0 with half_rating=forloop.counter0|half_star %}
{{ half_rating }} star
{% plural %}
{{ half_rating }} stars
{% endblocktranslate %}
</label>
<input
id="{{ type|slugify }}_book{{ book.id }}_star_{{ forloop.counter0 }}_half"
class="is-sr-only half"
type="radio"
name="rating"
value="{{ forloop.counter0 }}.5"
{% if default_rating == forloop.counter %}checked{% endif %}
/>
<input <input
id="{{ type|slugify }}_book{{ book.id }}_star_{{ forloop.counter }}" id="{{ type|slugify }}_book{{ book.id }}_star_{{ forloop.counter }}"
class="is-sr-only" class="is-sr-only"
@ -39,6 +58,7 @@
{% endif %} {% endif %}
" "
for="{{ type|slugify }}_book{{ book.id }}_star_{{ forloop.counter }}" for="{{ type|slugify }}_book{{ book.id }}_star_{{ forloop.counter }}"
data-for-half="{{ type|slugify }}_book{{ book.id }}_star_{{ forloop.counter0 }}_half"
> >
<span class="is-sr-only"> <span class="is-sr-only">
{% blocktranslate trimmed count rating=forloop.counter %} {% blocktranslate trimmed count rating=forloop.counter %}

View file

@ -9,7 +9,7 @@ Finish "<em>{{ book_title }}</em>"
{% endblock %} {% endblock %}
{% block modal-form-open %} {% block modal-form-open %}
<form name="finish-reading" action="{% url 'reading-status' 'finish' book.id %}" method="post"> <form name="finish-reading" action="{% url 'reading-status' 'finish' book.id %}" method="post" class="submit-status">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="reading_status" value="read"> <input type="hidden" name="reading_status" value="read">
{% endblock %} {% endblock %}

View file

@ -6,7 +6,7 @@
{% endblock %} {% endblock %}
{% block modal-form-open %} {% block modal-form-open %}
<form action="{% url 'edit-readthrough' %}" method="POST"> <form action="{% url 'edit-readthrough' %}" method="POST" class="submit-status">
{% endblock %} {% endblock %}
{% block modal-body %} {% block modal-body %}

View file

@ -9,7 +9,7 @@ Start "<em>{{ book_title }}</em>"
{% endblock %} {% endblock %}
{% block modal-form-open %} {% block modal-form-open %}
<form name="start-reading" action="{% url 'reading-status' 'start' book.id %}" method="post"> <form name="start-reading" action="{% url 'reading-status' 'start' book.id %}" method="post" class="submit-status">
<input type="hidden" name="reading_status" value="reading"> <input type="hidden" name="reading_status" value="reading">
{% csrf_token %} {% csrf_token %}
{% endblock %} {% endblock %}

View file

@ -9,7 +9,7 @@ Want to Read "<em>{{ book_title }}</em>"
{% endblock %} {% endblock %}
{% block modal-form-open %} {% block modal-form-open %}
<form name="shelve" action="{% url 'reading-status' 'want' book.id %}" method="post"> <form name="shelve" action="{% url 'reading-status' 'want' book.id %}" method="post" class="submit-status">
<input type="hidden" name="reading_status" value="to-read"> <input type="hidden" name="reading_status" value="to-read">
{% csrf_token %} {% csrf_token %}
{% endblock %} {% endblock %}

View file

@ -6,7 +6,7 @@
{% with book.id|uuid as uuid %} {% with book.id|uuid as uuid %}
{% active_shelf book as active_shelf %} {% active_shelf book as active_shelf %}
{% latest_read_through book request.user as readthrough %} {% latest_read_through book request.user as readthrough %}
<div class="field has-addons mb-0"> <div class="field has-addons mb-0" data-shelve-button-book="{{ book.id }}">
{% if switch_mode and active_shelf.book != book %} {% if switch_mode and active_shelf.book != book %}
<div class="control"> <div class="control">
{% include 'snippets/switch_edition_button.html' with edition=book size='is-small' %} {% include 'snippets/switch_edition_button.html' with edition=book size='is-small' %}

View file

@ -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=user_shelves dropdown=True class="shelf-option is-fullwidth is-small is-radiusless is-white" %} {% include 'snippets/shelve_button/shelve_button_dropdown_options.html' with active_shelf=active_shelf shelves=user_shelves dropdown=True class="shelf-option is-fullwidth is-small is-radiusless is-white" %}
{% endblock %} {% endblock %}

View file

@ -0,0 +1,69 @@
{% load bookwyrm_tags %}
{% load utilities %}
{% load i18n %}
{% with next_shelf_identifier=active_shelf.shelf.identifier|next_shelf %}
{% for shelf in shelves %}
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
<li role="menuitem" class="dropdown-item p-0">
<div
class="{% if next_shelf_identifier == shelf.identifier %}is-hidden{% endif %}"
data-shelf-dropdown-identifier="{{ shelf.identifier }}"
data-shelf-next="{{ shelf.identifier|next_shelf }}"
>
{% if shelf.identifier == 'reading' %}
{% trans "Start reading" as button_text %}
{% url 'reading-status' 'start' book.id as fallback_url %}
{% 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 fallback_url=fallback_url %}
{% elif shelf.identifier == 'read' %}
{% trans "Read" as button_text %}
{% url 'reading-status' 'finish' book.id as fallback_url %}
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="finish_reading" controls_uid=button_uuid focus="modal_title_finish_reading" disabled=is_current fallback_url=fallback_url %}
{% elif shelf.identifier == 'to-read' %}
{% trans "Want to read" as button_text %}
{% url 'reading-status' 'want' book.id as fallback_url %}
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="want_to_read" controls_uid=button_uuid focus="modal_title_want_to_read" disabled=is_current fallback_url=fallback_url %}
{% elif shelf.editable %}
<form name="shelve" action="/shelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<button class="button {{ class }}" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>
<span>{{ shelf.name }}</span>
</button>
</form>
{% endif %}
</div>
</li>
{% endfor %}
{% if readthrough and active_shelf.shelf.identifier != 'read' %}
<li role="menuitem" class="dropdown-item p-0" data-extra-options>
{% 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" %}
</li>
{% endif %}
{% if active_shelf.shelf %}
<li role="menuitem" class="dropdown-item p-0" data-extra-options>
<form name="shelve" action="/unshelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}">
<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>
</li>
{% endif %}
{% endwith %}

View file

@ -2,33 +2,43 @@
{% load utilities %} {% load utilities %}
{% load i18n %} {% load i18n %}
{% for shelf in shelves %} {% with next_shelf_identifier=active_shelf.shelf.identifier|next_shelf %}
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
{% if dropdown %}<li role="menuitem" class="dropdown-item p-0">{% 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 %}
{% trans "Start reading" as button_text %} <div
{% url 'reading-status' 'start' book.id as fallback_url %} class="{% if next_shelf_identifier != 'complete' %}is-hidden{% endif %}"
{% 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 fallback_url=fallback_url %} data-shelf-identifier="complete"
>
{% endif %}{% elif shelf.identifier == 'read' and active_shelf.shelf.identifier == 'read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
<button type="button" class="button {{ class }}" disabled> <button type="button" class="button {{ class }}" disabled>
<span>{% trans "Read" %}</span> <span>{% trans "Read" %}</span>
</button> </button>
{% endif %}{% elif shelf.identifier == 'read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %} </div>
{% trans "Finish reading" as button_text %} {% for shelf in shelves %}
{% url 'reading-status' 'finish' book.id as fallback_url %} <div
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="finish_reading" controls_uid=button_uuid focus="modal_title_finish_reading" disabled=is_current fallback_url=fallback_url %} class="{% if next_shelf_identifier != shelf.identifier %}is-hidden{% endif %}"
data-shelf-identifier="{{ shelf.identifier }}"
data-shelf-next="{{ shelf.identifier|next_shelf }}"
>
{% if shelf.identifier == 'reading' %}
{% endif %}{% elif shelf.identifier == 'to-read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %} {% trans "Start reading" as button_text %}
{% url 'reading-status' 'start' book.id as fallback_url %}
{% 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" fallback_url=fallback_url %}
{% trans "Want to read" as button_text %} {% elif shelf.identifier == 'read' %}
{% url 'reading-status' 'want' book.id as fallback_url %}
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="want_to_read" controls_uid=button_uuid focus="modal_title_want_to_read" disabled=is_current fallback_url=fallback_url %} {% trans "Finish reading" as button_text %}
{% url 'reading-status' 'finish' book.id as fallback_url %}
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="finish_reading" controls_uid=button_uuid focus="modal_title_finish_reading" fallback_url=fallback_url %}
{% elif shelf.identifier == 'to-read' %}
{% trans "Want to read" as button_text %}
{% url 'reading-status' 'want' book.id as fallback_url %}
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="want_to_read" controls_uid=button_uuid focus="modal_title_want_to_read" fallback_url=fallback_url %}
{% elif shelf.editable %}
{% endif %}{% elif shelf.editable %}
<form name="shelve" action="/shelve/" method="post"> <form name="shelve" action="/shelve/" 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 }}">
@ -36,30 +46,9 @@
<span>{{ shelf.name }}</span> <span>{{ shelf.name }}</span>
</button> </button>
</form> </form>
{% endif %} {% endif %}
</div> </div>
{% if dropdown %}</li>{% endif %}
{% endfor %} {% endfor %}
{% if dropdown %}
{% if readthrough and active_shelf.shelf.identifier != 'read' %} {% endwith %}
<li role="menuitem" class="dropdown-item p-0">
{% 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" %}
</li>
{% endif %}
{% if active_shelf.shelf %}
<li role="menuitem" class="dropdown-item p-0">
<form name="shelve" action="/unshelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}">
<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>
</li>
{% endif %}
{% endif %}

View file

@ -2,6 +2,7 @@
{% load markdown %} {% load markdown %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load humanize %}
{% with status_type=status.status_type %} {% with status_type=status.status_type %}
<div <div
@ -95,7 +96,16 @@
<div class="quote block"> <div class="quote block">
<blockquote dir="auto" class="content mb-2">{{ status.quote|safe }}</blockquote> <blockquote dir="auto" class="content mb-2">{{ status.quote|safe }}</blockquote>
<p> &mdash; {% include 'snippets/book_titleby.html' with book=status.book %}</p> <p>
&mdash; {% include 'snippets/book_titleby.html' with book=status.book %}
{% if status.position %}
{% if status.position_mode == 'PG' %}
{% blocktrans with page=status.position|intcomma %}(Page {{ page }}){% endblocktrans %}
{% else %}
{% blocktrans with percent=status.position %}({{ percent }}%){% endblocktrans %}
{% endif %}
{% endif %}
</p>
</div> </div>
{% endif %} {% endif %}

Some files were not shown because too many files have changed in this diff Show more