Merge branch 'production' into nix
Patch celerywyrm settings to accept Redis that isn't over docker
This commit is contained in:
commit
899805cc87
224 changed files with 7950 additions and 3113 deletions
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
34
bookwyrm/migrations/0088_auto_20210905_2233.py
Normal file
34
bookwyrm/migrations/0088_auto_20210905_2233.py
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
18
bookwyrm/migrations/0089_user_show_suggested_users.py
Normal file
18
bookwyrm/migrations/0089_user_show_suggested_users.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
45
bookwyrm/migrations/0090_auto_20210908_2346.py
Normal file
45
bookwyrm/migrations/0090_auto_20210908_2346.py
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
32
bookwyrm/migrations/0090_emailblocklist.py
Normal file
32
bookwyrm/migrations/0090_emailblocklist.py
Normal 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",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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 = []
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
39
bookwyrm/migrations/0094_auto_20210911_1550.py
Normal file
39
bookwyrm/migrations/0094_auto_20210911_1550.py
Normal 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),
|
||||||
|
]
|
25
bookwyrm/migrations/0094_importitem_book_guess.py
Normal file
25
bookwyrm/migrations/0094_importitem_book_guess.py
Normal 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
45
bookwyrm/migrations/0095_auto_20210911_2053.py
Normal file
45
bookwyrm/migrations/0095_auto_20210911_2053.py
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
13
bookwyrm/migrations/0095_merge_20210911_2143.py
Normal file
13
bookwyrm/migrations/0095_merge_20210911_2143.py
Normal 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 = []
|
13
bookwyrm/migrations/0096_merge_20210912_0044.py
Normal file
13
bookwyrm/migrations/0096_merge_20210912_0044.py
Normal 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 = []
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
Binary file not shown.
Binary file not shown.
196
bookwyrm/static/css/vendor/icons.css
vendored
196
bookwyrm/static/css/vendor/icons.css
vendored
|
@ -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";
|
||||||
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
236
bookwyrm/static/js/status_cache.js
Normal file
236
bookwyrm/static/js/status_cache.js
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
11
bookwyrm/templates/components/tooltip.html
Normal file
11
bookwyrm/templates/components/tooltip.html
Normal 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>
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}>
|
|
@ -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 %}
|
8
bookwyrm/templates/import/tooltip.html
Normal file
8
bookwyrm/templates/import/tooltip.html
Normal 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 %}
|
|
@ -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>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'lists/list_layout.html' %}
|
{% extends 'lists/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
|
|
21
bookwyrm/templates/lists/delete_list_modal.html
Normal file
21
bookwyrm/templates/lists/delete_list_modal.html
Normal 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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
120
bookwyrm/templates/settings/dashboard.html
Normal file
120
bookwyrm/templates/settings/dashboard.html
Normal 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 %}
|
26
bookwyrm/templates/settings/dashboard_status_chart.html
Normal file
26
bookwyrm/templates/settings/dashboard_status_chart.html
Normal 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>
|
||||||
|
|
29
bookwyrm/templates/settings/dashboard_user_chart.html
Normal file
29
bookwyrm/templates/settings/dashboard_user_chart.html
Normal 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>
|
30
bookwyrm/templates/settings/domain_form.html
Normal file
30
bookwyrm/templates/settings/domain_form.html
Normal 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 %}
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
61
bookwyrm/templates/settings/email_blocklist.html
Normal file
61
bookwyrm/templates/settings/email_blocklist.html
Normal 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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
|
||||||
|
|
|
@ -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 %}
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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' %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
|
||||||
|
|
|
@ -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> — {% include 'snippets/book_titleby.html' with book=status.book %}</p>
|
<p>
|
||||||
|
— {% 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
Loading…
Add table
Reference in a new issue