Merge branch 'main' into activitystreams-celery

This commit is contained in:
Mouse Reeve 2021-08-30 13:48:34 -07:00
commit 54f1b0aee2
134 changed files with 2874 additions and 1568 deletions

View file

@ -32,7 +32,7 @@ indent_size = 2
max_line_length = off
# Computer generated files
[{package.json,*.lock,*.mo}]
[{icons.css,package.json,*.lock,*.mo}]
indent_size = unset
indent_style = unset
max_line_length = unset

View file

@ -43,6 +43,9 @@ EMAIL_HOST_PASSWORD=emailpassword123
EMAIL_USE_TLS=true
EMAIL_USE_SSL=false
# Thumbnails Generation
ENABLE_THUMBNAIL_GENERATION=false
# S3 configuration
USE_S3=false
AWS_ACCESS_KEY_ID=
@ -58,6 +61,7 @@ AWS_SECRET_ACCESS_KEY=
# AWS_S3_REGION_NAME=None # "fr-par"
# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud"
# Preview image generation can be computing and storage intensive
# ENABLE_PREVIEW_IMAGES=True

View file

@ -43,6 +43,9 @@ EMAIL_HOST_PASSWORD=emailpassword123
EMAIL_USE_TLS=true
EMAIL_USE_SSL=false
# Thumbnails Generation
ENABLE_THUMBNAIL_GENERATION=false
# S3 configuration
USE_S3=false
AWS_ACCESS_KEY_ID=
@ -58,6 +61,7 @@ AWS_SECRET_ACCESS_KEY=
# AWS_S3_REGION_NAME=None # "fr-par"
# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud"
# Preview image generation can be computing and storage intensive
# ENABLE_PREVIEW_IMAGES=True

28
.github/workflows/curlylint.yaml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Templates validator
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install curlylint
run: pip install curlylint
- name: Run linter
run: >
curlylint --rule 'aria_role: true' \
--rule 'django_forms_rendering: true' \
--rule 'html_has_lang: true' \
--rule 'image_alt: true' \
--rule 'meta_viewport: true' \
--rule 'no_autofocus: true' \
--rule 'tabindex_no_positive: true' \
--exclude '_modal.html|create_status/layout.html' \
bookwyrm/templates

View file

@ -106,8 +106,10 @@ class ActivityObject:
value = field.default
setattr(self, field.name, value)
# pylint: disable=too-many-locals,too-many-branches
def to_model(self, model=None, instance=None, allow_create=True, save=True):
# pylint: disable=too-many-locals,too-many-branches,too-many-arguments
def to_model(
self, model=None, instance=None, allow_create=True, save=True, overwrite=True
):
"""convert from an activity to a model instance"""
model = model or get_model_from_type(self.type)
@ -129,9 +131,12 @@ class ActivityObject:
# keep track of what we've changed
update_fields = []
# sets field on the model using the activity value
for field in instance.simple_fields:
try:
changed = field.set_field_from_activity(instance, self)
changed = field.set_field_from_activity(
instance, self, overwrite=overwrite
)
if changed:
update_fields.append(field.name)
except AttributeError as e:
@ -140,7 +145,9 @@ class ActivityObject:
# image fields have to be set after other fields because they can save
# too early and jank up users
for field in instance.image_fields:
changed = field.set_field_from_activity(instance, self, save=save)
changed = field.set_field_from_activity(
instance, self, save=save, overwrite=overwrite
)
if changed:
update_fields.append(field.name)
@ -268,6 +275,8 @@ def resolve_remote_id(
):
"""take a remote_id and return an instance, creating if necessary"""
if model: # a bonus check we can do if we already know the model
if isinstance(model, str):
model = apps.get_model(f"bookwyrm.{model}", require_ready=True)
result = model.find_existing_by_remote_id(remote_id)
if result and not refresh:
return result if not get_activity else result.to_activity_dataclass()

View file

@ -30,8 +30,8 @@ class Note(ActivityObject):
to: List[str] = field(default_factory=lambda: [])
cc: List[str] = field(default_factory=lambda: [])
replies: Dict = field(default_factory=lambda: {})
inReplyTo: str = ""
summary: str = ""
inReplyTo: str = None
summary: str = None
tag: List[Link] = field(default_factory=lambda: [])
attachment: List[Document] = field(default_factory=lambda: [])
sensitive: bool = False
@ -59,6 +59,9 @@ class Comment(Note):
"""like a note but with a book"""
inReplyToBook: str
readingStatus: str = None
progress: int = None
progressMode: str = None
type: str = "Comment"

View file

@ -24,14 +24,15 @@ class ActivityStream(RedisStore):
"""statuses are sorted by date published"""
return obj.published_date.timestamp()
def add_status(self, status):
def add_status(self, status, increment_unread=False):
"""add a status to users' feeds"""
# the pipeline contains all the add-to-stream activities
pipeline = self.add_object_to_related_stores(status, execute=False)
for user in self.get_audience(status):
# add to the unread status count
pipeline.incr(self.unread_id(user))
if increment_unread:
for user in self.get_audience(status):
# add to the unread status count
pipeline.incr(self.unread_id(user))
# and go!
pipeline.execute()
@ -262,12 +263,14 @@ def add_status_on_create(sender, instance, created, *args, **kwargs):
return
# when creating new things, gotta wait on the transaction
transaction.on_commit(lambda: add_status_on_create_command(sender, instance))
transaction.on_commit(
lambda: add_status_on_create_command(sender, instance, created)
)
def add_status_on_create_command(sender, instance):
def add_status_on_create_command(sender, instance, created):
"""runs this code only after the database commit completes"""
add_status_task.delay(instance.id)
add_status_task.delay(instance.id, increment_unread_unread=created)
if sender != models.Boost:
return
@ -440,11 +443,11 @@ def remove_status_task(status_ids):
@app.task
def add_status_task(status_id):
def add_status_task(status_id, increment_unread=False):
"""remove a status from any stream it might be in"""
status = models.Status.objects.get(id=status_id)
for stream in streams.values():
stream.add_status(status)
stream.add_status(status, increment_unread=increment_unread)
@app.task

View file

@ -139,7 +139,7 @@ class AbstractConnector(AbstractMinimalConnector):
**dict_from_mappings(work_data, self.book_mappings)
)
# this will dedupe automatically
work = work_activity.to_model(model=models.Work)
work = work_activity.to_model(model=models.Work, overwrite=False)
for author in self.get_authors_from_data(work_data):
work.authors.add(author)
@ -156,7 +156,7 @@ class AbstractConnector(AbstractMinimalConnector):
mapped_data = dict_from_mappings(edition_data, self.book_mappings)
mapped_data["work"] = work.remote_id
edition_activity = activitypub.Edition(**mapped_data)
edition = edition_activity.to_model(model=models.Edition)
edition = edition_activity.to_model(model=models.Edition, overwrite=False)
edition.connector = self.connector
edition.save()
@ -182,7 +182,7 @@ class AbstractConnector(AbstractMinimalConnector):
return None
# this will dedupe
return activity.to_model(model=models.Author)
return activity.to_model(model=models.Author, overwrite=False)
@abstractmethod
def is_work_data(self, data):

View file

@ -145,8 +145,8 @@ class Connector(AbstractConnector):
def get_edition_from_work_data(self, data):
data = self.load_edition_data(data.get("uri"))
try:
uri = data["uris"][0]
except KeyError:
uri = data.get("uris", [])[0]
except IndexError:
raise ConnectorException("Invalid book data")
return self.get_book_data(self.get_remote_id(uri))

View file

@ -11,6 +11,7 @@ def site_settings(request): # pylint: disable=unused-argument
return {
"site": models.SiteSettings.objects.get(),
"active_announcements": models.Announcement.active_announcements(),
"thumbnail_generation_enabled": settings.ENABLE_THUMBNAIL_GENERATION,
"media_full_url": settings.MEDIA_FULL_URL,
"preview_images_enabled": settings.ENABLE_PREVIEW_IMAGES,
"request_protocol": request_protocol,

View file

@ -86,6 +86,7 @@ class CommentForm(CustomForm):
"privacy",
"progress",
"progress_mode",
"reading_status",
]

113
bookwyrm/imagegenerators.py Normal file
View file

@ -0,0 +1,113 @@
"""Generators for all the different thumbnail sizes"""
from imagekit import ImageSpec, register
from imagekit.processors import ResizeToFit
class BookXSmallWebp(ImageSpec):
"""Handles XSmall size in Webp format"""
processors = [ResizeToFit(80, 80)]
format = "WEBP"
options = {"quality": 95}
class BookXSmallJpg(ImageSpec):
"""Handles XSmall size in Jpeg format"""
processors = [ResizeToFit(80, 80)]
format = "JPEG"
options = {"quality": 95}
class BookSmallWebp(ImageSpec):
"""Handles Small size in Webp format"""
processors = [ResizeToFit(100, 100)]
format = "WEBP"
options = {"quality": 95}
class BookSmallJpg(ImageSpec):
"""Handles Small size in Jpeg format"""
processors = [ResizeToFit(100, 100)]
format = "JPEG"
options = {"quality": 95}
class BookMediumWebp(ImageSpec):
"""Handles Medium size in Webp format"""
processors = [ResizeToFit(150, 150)]
format = "WEBP"
options = {"quality": 95}
class BookMediumJpg(ImageSpec):
"""Handles Medium size in Jpeg format"""
processors = [ResizeToFit(150, 150)]
format = "JPEG"
options = {"quality": 95}
class BookLargeWebp(ImageSpec):
"""Handles Large size in Webp format"""
processors = [ResizeToFit(200, 200)]
format = "WEBP"
options = {"quality": 95}
class BookLargeJpg(ImageSpec):
"""Handles Large size in Jpeg format"""
processors = [ResizeToFit(200, 200)]
format = "JPEG"
options = {"quality": 95}
class BookXLargeWebp(ImageSpec):
"""Handles XLarge size in Webp format"""
processors = [ResizeToFit(250, 250)]
format = "WEBP"
options = {"quality": 95}
class BookXLargeJpg(ImageSpec):
"""Handles XLarge size in Jpeg format"""
processors = [ResizeToFit(250, 250)]
format = "JPEG"
options = {"quality": 95}
class BookXxLargeWebp(ImageSpec):
"""Handles XxLarge size in Webp format"""
processors = [ResizeToFit(500, 500)]
format = "WEBP"
options = {"quality": 95}
class BookXxLargeJpg(ImageSpec):
"""Handles XxLarge size in Jpeg format"""
processors = [ResizeToFit(500, 500)]
format = "JPEG"
options = {"quality": 95}
register.generator("bw:book:xsmall:webp", BookXSmallWebp)
register.generator("bw:book:xsmall:jpg", BookXSmallJpg)
register.generator("bw:book:small:webp", BookSmallWebp)
register.generator("bw:book:small:jpg", BookSmallJpg)
register.generator("bw:book:medium:webp", BookMediumWebp)
register.generator("bw:book:medium:jpg", BookMediumJpg)
register.generator("bw:book:large:webp", BookLargeWebp)
register.generator("bw:book:large:jpg", BookLargeJpg)
register.generator("bw:book:xlarge:webp", BookXLargeWebp)
register.generator("bw:book:xlarge:jpg", BookXLargeJpg)
register.generator("bw:book:xxlarge:webp", BookXxLargeWebp)
register.generator("bw:book:xxlarge:jpg", BookXxLargeJpg)

View file

@ -0,0 +1,56 @@
# Generated by Django 3.2.4 on 2021-08-16 20:22
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0082_auto_20210806_2324"),
]
operations = [
migrations.AddField(
model_name="comment",
name="reading_status",
field=bookwyrm.models.fields.CharField(
blank=True,
choices=[
("to-read", "Toread"),
("reading", "Reading"),
("read", "Read"),
],
max_length=255,
null=True,
),
),
migrations.AddField(
model_name="quotation",
name="reading_status",
field=bookwyrm.models.fields.CharField(
blank=True,
choices=[
("to-read", "Toread"),
("reading", "Reading"),
("read", "Read"),
],
max_length=255,
null=True,
),
),
migrations.AddField(
model_name="review",
name="reading_status",
field=bookwyrm.models.fields.CharField(
blank=True,
choices=[
("to-read", "Toread"),
("reading", "Reading"),
("read", "Read"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,56 @@
# Generated by Django 3.2.4 on 2021-08-17 19:16
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0083_auto_20210816_2022"),
]
operations = [
migrations.AlterField(
model_name="comment",
name="reading_status",
field=bookwyrm.models.fields.CharField(
blank=True,
choices=[
("to-read", "To-Read"),
("reading", "Reading"),
("read", "Read"),
],
max_length=255,
null=True,
),
),
migrations.AlterField(
model_name="quotation",
name="reading_status",
field=bookwyrm.models.fields.CharField(
blank=True,
choices=[
("to-read", "To-Read"),
("reading", "Reading"),
("read", "Read"),
],
max_length=255,
null=True,
),
),
migrations.AlterField(
model_name="review",
name="reading_status",
field=bookwyrm.models.fields.CharField(
blank=True,
choices=[
("to-read", "To-Read"),
("reading", "Reading"),
("read", "Read"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 3.2.4 on 2021-08-23 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0084_auto_20210817_1916"),
]
operations = [
migrations.AddField(
model_name="user",
name="saved_lists",
field=models.ManyToManyField(
related_name="saved_lists", to="bookwyrm.List"
),
),
]

View file

@ -0,0 +1,40 @@
# Generated by Django 3.2.4 on 2021-08-27 17:27
from django.db import migrations, models
import django.db.models.expressions
def normalize_readthrough_dates(app_registry, schema_editor):
"""Find any invalid dates and reset them"""
db_alias = schema_editor.connection.alias
app_registry.get_model("bookwyrm", "ReadThrough").objects.using(db_alias).filter(
start_date__gt=models.F("finish_date")
).update(start_date=models.F("finish_date"))
def reverse_func(apps, schema_editor):
"""nothing to do here"""
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0085_user_saved_lists"),
]
operations = [
migrations.RunPython(normalize_readthrough_dates, reverse_func),
migrations.AlterModelOptions(
name="readthrough",
options={"ordering": ("-start_date",)},
),
migrations.AddConstraint(
model_name="readthrough",
constraint=models.CheckConstraint(
check=models.Q(
("finish_date__gte", django.db.models.expressions.F("start_date"))
),
name="chronology",
),
),
]

View file

@ -0,0 +1,49 @@
# Generated by Django 3.2.4 on 2021-08-28 17:24
import bookwyrm.models.fields
from django.conf import settings
from django.db import migrations, models
from django.db.models import F, Value, CharField
from django.db.models.functions import Concat
def forwards_func(apps, schema_editor):
"""generate followers url"""
db_alias = schema_editor.connection.alias
apps.get_model("bookwyrm", "User").objects.using(db_alias).annotate(
generated_url=Concat(
F("remote_id"), Value("/followers"), output_field=CharField()
)
).update(followers_url=models.F("generated_url"))
def reverse_func(apps, schema_editor):
"""noop"""
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0085_user_saved_lists"),
]
operations = [
migrations.AddField(
model_name="user",
name="followers_url",
field=bookwyrm.models.fields.CharField(
default="/followers", max_length=255
),
preserve_default=False,
),
migrations.RunPython(forwards_func, reverse_func),
migrations.AlterField(
model_name="user",
name="followers",
field=models.ManyToManyField(
related_name="following",
through="bookwyrm.UserFollows",
to=settings.AUTH_USER_MODEL,
),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.4 on 2021-08-29 18:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0086_auto_20210827_1727"),
("bookwyrm", "0086_auto_20210828_1724"),
]
operations = []

View file

@ -7,10 +7,16 @@ from django.db import models
from django.dispatch import receiver
from model_utils import FieldTracker
from model_utils.managers import InheritanceManager
from imagekit.models import ImageSpecField
from bookwyrm import activitypub
from bookwyrm.preview_images import generate_edition_preview_image_task
from bookwyrm.settings import DOMAIN, DEFAULT_LANGUAGE, ENABLE_PREVIEW_IMAGES
from bookwyrm.settings import (
DOMAIN,
DEFAULT_LANGUAGE,
ENABLE_PREVIEW_IMAGES,
ENABLE_THUMBNAIL_GENERATION,
)
from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
from .base_model import BookWyrmModel
@ -97,6 +103,40 @@ class Book(BookDataModel):
objects = InheritanceManager()
field_tracker = FieldTracker(fields=["authors", "title", "subtitle", "cover"])
if ENABLE_THUMBNAIL_GENERATION:
cover_bw_book_xsmall_webp = ImageSpecField(
source="cover", id="bw:book:xsmall:webp"
)
cover_bw_book_xsmall_jpg = ImageSpecField(
source="cover", id="bw:book:xsmall:jpg"
)
cover_bw_book_small_webp = ImageSpecField(
source="cover", id="bw:book:small:webp"
)
cover_bw_book_small_jpg = ImageSpecField(source="cover", id="bw:book:small:jpg")
cover_bw_book_medium_webp = ImageSpecField(
source="cover", id="bw:book:medium:webp"
)
cover_bw_book_medium_jpg = ImageSpecField(
source="cover", id="bw:book:medium:jpg"
)
cover_bw_book_large_webp = ImageSpecField(
source="cover", id="bw:book:large:webp"
)
cover_bw_book_large_jpg = ImageSpecField(source="cover", id="bw:book:large:jpg")
cover_bw_book_xlarge_webp = ImageSpecField(
source="cover", id="bw:book:xlarge:webp"
)
cover_bw_book_xlarge_jpg = ImageSpecField(
source="cover", id="bw:book:xlarge:jpg"
)
cover_bw_book_xxlarge_webp = ImageSpecField(
source="cover", id="bw:book:xxlarge:webp"
)
cover_bw_book_xxlarge_jpg = ImageSpecField(
source="cover", id="bw:book:xxlarge:jpg"
)
@property
def author_text(self):
"""format a list of authors"""

View file

@ -66,7 +66,7 @@ class ActivitypubFieldMixin:
self.activitypub_field = activitypub_field
super().__init__(*args, **kwargs)
def set_field_from_activity(self, instance, data):
def set_field_from_activity(self, instance, data, overwrite=True):
"""helper function for assinging a value to the field. Returns if changed"""
try:
value = getattr(data, self.get_activitypub_field())
@ -79,8 +79,15 @@ class ActivitypubFieldMixin:
if formatted is None or formatted is MISSING or formatted == {}:
return False
current_value = (
getattr(instance, self.name) if hasattr(instance, self.name) else None
)
# if we're not in overwrite mode, only continue updating the field if its unset
if current_value and not overwrite:
return False
# the field is unchanged
if hasattr(instance, self.name) and getattr(instance, self.name) == formatted:
if current_value == formatted:
return False
setattr(instance, self.name, formatted)
@ -210,12 +217,27 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
)
# pylint: disable=invalid-name
def set_field_from_activity(self, instance, data):
def set_field_from_activity(self, instance, data, overwrite=True):
if not overwrite:
return False
original = getattr(instance, self.name)
to = data.to
cc = data.cc
# we need to figure out who this is to get their followers link
for field in ["attributedTo", "owner", "actor"]:
if hasattr(data, field):
user_field = field
break
if not user_field:
raise ValidationError("No user field found for privacy", data)
user = activitypub.resolve_remote_id(getattr(data, user_field), model="User")
if to == [self.public]:
setattr(instance, self.name, "public")
elif to == [user.followers_url]:
setattr(instance, self.name, "followers")
elif cc == []:
setattr(instance, self.name, "direct")
elif self.public in cc:
@ -231,9 +253,7 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
mentions = [u.remote_id for u in instance.mention_users.all()]
# this is a link to the followers list
# pylint: disable=protected-access
followers = instance.user.__class__._meta.get_field(
"followers"
).field_to_activity(instance.user.followers)
followers = instance.user.followers_url
if instance.privacy == "public":
activity["to"] = [self.public]
activity["cc"] = [followers] + mentions
@ -273,8 +293,11 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
self.link_only = link_only
super().__init__(*args, **kwargs)
def set_field_from_activity(self, instance, data):
def set_field_from_activity(self, instance, data, overwrite=True):
"""helper function for assinging a value to the field"""
if not overwrite and getattr(instance, self.name).exists():
return False
value = getattr(data, self.get_activitypub_field())
formatted = self.field_from_activity(value)
if formatted is None or formatted is MISSING:
@ -377,13 +400,16 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
super().__init__(*args, **kwargs)
# pylint: disable=arguments-differ
def set_field_from_activity(self, instance, data, save=True):
def set_field_from_activity(self, instance, data, save=True, overwrite=True):
"""helper function for assinging a value to the field"""
value = getattr(data, self.get_activitypub_field())
formatted = self.field_from_activity(value)
if formatted is None or formatted is MISSING:
return False
if not overwrite and hasattr(instance, self.name):
return False
getattr(instance, self.name).save(*formatted, save=save)
return True

View file

@ -1,7 +1,8 @@
""" progress in a book """
from django.db import models
from django.utils import timezone
from django.core import validators
from django.db import models
from django.db.models import F, Q
from django.utils import timezone
from .base_model import BookWyrmModel
@ -41,6 +42,16 @@ class ReadThrough(BookWyrmModel):
)
return None
class Meta:
"""Don't let readthroughs end before they start"""
constraints = [
models.CheckConstraint(
check=Q(finish_date__gte=F("start_date")), name="chronology"
)
]
ordering = ("-start_date",)
class ProgressUpdate(BookWyrmModel):
"""Store progress through a book in the database."""

View file

@ -235,12 +235,31 @@ class GeneratedNote(Status):
pure_type = "Note"
class Comment(Status):
"""like a review but without a rating and transient"""
ReadingStatusChoices = models.TextChoices(
"ReadingStatusChoices", ["to-read", "reading", "read"]
)
class BookStatus(Status):
"""Shared fields for comments, quotes, reviews"""
book = fields.ForeignKey(
"Edition", on_delete=models.PROTECT, activitypub_field="inReplyToBook"
)
pure_type = "Note"
reading_status = fields.CharField(
max_length=255, choices=ReadingStatusChoices.choices, null=True, blank=True
)
class Meta:
"""not a real model, sorry"""
abstract = True
class Comment(BookStatus):
"""like a review but without a rating and transient"""
# this is it's own field instead of a foreign key to the progress update
# so that the update can be deleted without impacting the status
@ -265,16 +284,12 @@ class Comment(Status):
)
activity_serializer = activitypub.Comment
pure_type = "Note"
class Quotation(Status):
class Quotation(BookStatus):
"""like a review but without a rating and transient"""
quote = fields.HtmlField()
book = fields.ForeignKey(
"Edition", on_delete=models.PROTECT, activitypub_field="inReplyToBook"
)
@property
def pure_content(self):
@ -289,16 +304,12 @@ class Quotation(Status):
)
activity_serializer = activitypub.Quotation
pure_type = "Note"
class Review(Status):
class Review(BookStatus):
"""a book review"""
name = fields.CharField(max_length=255, null=True)
book = fields.ForeignKey(
"Edition", on_delete=models.PROTECT, activitypub_field="inReplyToBook"
)
rating = fields.DecimalField(
default=None,
null=True,

View file

@ -82,9 +82,9 @@ class User(OrderedCollectionPageMixin, AbstractUser):
preview_image = models.ImageField(
upload_to="previews/avatars/", blank=True, null=True
)
followers = fields.ManyToManyField(
followers_url = fields.CharField(max_length=255, activitypub_field="followers")
followers = models.ManyToManyField(
"self",
link_only=True,
symmetrical=False,
through="UserFollows",
through_fields=("user_object", "user_subject"),
@ -104,6 +104,9 @@ class User(OrderedCollectionPageMixin, AbstractUser):
through_fields=("user_subject", "user_object"),
related_name="blocked_by",
)
saved_lists = models.ManyToManyField(
"List", symmetrical=False, related_name="saved_lists"
)
favorites = models.ManyToManyField(
"Status",
symmetrical=False,
@ -225,7 +228,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
def to_followers_activity(self, **kwargs):
"""activitypub followers list"""
remote_id = "%s/followers" % self.remote_id
remote_id = self.followers_url
return self.to_ordered_collection(
self.followers.order_by("-updated_date").all(),
remote_id=remote_id,
@ -272,10 +275,12 @@ class User(OrderedCollectionPageMixin, AbstractUser):
return
# populate fields for local users
self.remote_id = "%s/user/%s" % (site_link(), self.localname)
self.inbox = "%s/inbox" % self.remote_id
self.shared_inbox = "%s/inbox" % site_link()
self.outbox = "%s/outbox" % self.remote_id
link = site_link()
self.remote_id = f"{link}/user/{self.localname}"
self.followers_url = f"{self.remote_id}/followers"
self.inbox = f"{self.remote_id}/inbox"
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
super().save(*args, **kwargs)

View file

@ -33,10 +33,11 @@ class RedisStore(ABC):
# and go!
return pipeline.execute()
def remove_object_from_related_stores(self, obj):
def remove_object_from_related_stores(self, obj, stores=None):
"""remove an object from all stores"""
stores = stores or self.get_stores_for_object(obj)
pipeline = r.pipeline()
for store in self.get_stores_for_object(obj):
for store in stores:
pipeline.zrem(store, -1, obj.id)
pipeline.execute()

View file

@ -75,6 +75,7 @@ INSTALLED_APPS = [
"django_rename_app",
"bookwyrm",
"celery",
"imagekit",
"storages",
]
@ -191,6 +192,9 @@ USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % (
DOMAIN,
)
# Imagekit generated thumbnails
ENABLE_THUMBNAIL_GENERATION = env.bool("ENABLE_THUMBNAIL_GENERATION", False)
IMAGEKIT_CACHEFILE_DIR = "thumbnails"
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

View file

@ -29,6 +29,10 @@ body {
min-width: 75% !important;
}
.modal-card-body {
max-height: 70vh;
}
.clip-text {
max-height: 35em;
overflow: hidden;
@ -232,16 +236,21 @@ body {
/* Cover caption
* -------------------------------------------------------------------------- */
.no-cover .cover_caption {
.no-cover .cover-caption {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: 0.25em;
padding: 0.5em;
font-size: 0.75em;
color: white;
background-color: #002549;
display: flex;
align-items: center;
justify-content: center;
white-space: initial;
text-align: center;
}
/** Avatars

View file

@ -33,13 +33,12 @@
<glyph unicode="&#xe917;" glyph-name="dots-three" d="M512.051 573.44c-62.208 0-112.691-50.432-112.691-112.64s50.483-112.64 112.691-112.64c62.208 0 112.589 50.432 112.589 112.64s-50.381 112.64-112.589 112.64zM153.651 573.44c-62.208 0-112.691-50.432-112.691-112.64s50.483-112.64 112.691-112.64c62.208 0 112.589 50.483 112.589 112.64s-50.381 112.64-112.589 112.64zM870.451 573.44c-62.208 0-112.691-50.432-112.691-112.64s50.483-112.64 112.691-112.64c62.208 0 112.589 50.432 112.589 112.64s-50.381 112.64-112.589 112.64z" />
<glyph unicode="&#xe918;" glyph-name="check" d="M424.653 102.502c-22.272 0-43.366 10.394-56.883 28.314l-182.938 241.715c-23.808 31.386-17.613 76.083 13.824 99.891 31.488 23.91 76.186 17.613 99.994-13.824l120.371-158.925 302.643 485.99c20.838 33.382 64.87 43.622 98.355 22.784 33.434-20.787 43.725-64.819 22.835-98.304l-357.581-573.952c-12.39-20.019-33.843-32.512-57.344-33.587-1.126-0.102-2.15-0.102-3.277-0.102z" />
<glyph unicode="&#xe919;" glyph-name="dots-three-vertical" d="M512.051 573.44c-62.208 0-112.691-50.432-112.691-112.64s50.483-112.64 112.691-112.64c62.208 0 112.589 50.432 112.589 112.64s-50.381 112.64-112.589 112.64zM512.051 706.56c62.208 0 112.589 50.483 112.589 112.64s-50.381 112.64-112.589 112.64c-62.208 0-112.691-50.432-112.691-112.64s50.483-112.64 112.691-112.64zM512.051 215.040c-62.208 0-112.691-50.432-112.691-112.64s50.483-112.64 112.691-112.64c62.208 0 112.589 50.432 112.589 112.64s-50.381 112.64-112.589 112.64z" />
<glyph unicode="&#xe91a;" glyph-name="stars" d="M726.857 664.381l-38.857 103.619-38.857-103.619-73.143-24.381 73.143-24.381 38.857-103.619 38.857 103.619 73.143 24.381-73.143 24.381zM662.857 280.381l-38.857 103.619-38.857-103.619-73.143-24.381 73.143-24.381 38.857-103.619 38.857 103.619 73.143 24.381-73.143 24.381zM430.703 528.432l-62.703 175.568-62.703-175.568-145.297-48.432 145.297-48.432 62.703-175.568 62.703 175.568 145.297 48.432-145.297 48.432z" />
<glyph unicode="&#xe91a;" glyph-name="bookmark" horiz-adv-x="731" d="M665.143 877.714c8.571 0 17.143-1.714 25.143-5.143 25.143-9.714 41.143-33.143 41.143-58.857v-736.571c0-25.714-16-49.143-41.143-58.857-8-3.429-16.571-4.571-25.143-4.571-17.714 0-34.286 6.286-47.429 18.286l-252 242.286-252-242.286c-13.143-12-29.714-18.857-47.429-18.857-8.571 0-17.143 1.714-25.143 5.143-25.143 9.714-41.143 33.143-41.143 58.857v736.571c0 25.714 16 49.143 41.143 58.857 8 3.429 16.571 5.143 25.143 5.143h598.857z" />
<glyph unicode="&#xe91b;" glyph-name="warning" d="M907.5 204.9l-345.1 558.4c-27.8 44.9-73 45-100.8 0v0l-345.1-558.4c-37.3-60.3-10.2-108.9 60.4-108.9h670.2c70.5 0 97.6 48.7 60.4 108.9zM512 192c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zM544 351.9c0-17.4-14.3-31.9-32-31.9-17.8 0-32 14.3-32 31.9v192.2c0 17.4 14.3 31.9 32 31.9v0c17.8 0 32-14.3 32-31.9v-192.2z" />
<glyph unicode="&#xe91c;" glyph-name="bookmark" d="M800 32h-512v704h224v-292l113.312 86.016 110.688-86.016v292h128v-640c0-35.328-28.672-64-64-64zM625.312 572l-81.312-64v260h160v-260l-78.688 64zM192 800v-32c0-17.664 14.336-32 32-32h32v-704h-32c-35.328 0-64 28.672-64 64v704c0 35.328 28.672 64 64 64h576c23.616 0 44.032-12.928 55.136-32h-631.136c-17.664 0-32-14.304-32-32z" />
<glyph unicode="&#xe91d;" glyph-name="rss" horiz-adv-x="805" d="M219.429 182.857c0-60.571-49.143-109.714-109.714-109.714s-109.714 49.143-109.714 109.714 49.143 109.714 109.714 109.714 109.714-49.143 109.714-109.714zM512 112.571c0.571-10.286-2.857-20-9.714-27.429-6.857-8-16.571-12-26.857-12h-77.143c-18.857 0-34.286 14.286-36 33.143-16.571 174.286-154.857 312.571-329.143 329.143-18.857 1.714-33.143 17.143-33.143 36v77.143c0 10.286 4 20 12 26.857 6.286 6.286 15.429 9.714 24.571 9.714h2.857c121.714-9.714 236.571-62.857 322.857-149.714 86.857-86.286 140-201.143 149.714-322.857zM804.571 111.428c0.571-9.714-2.857-19.429-10.286-26.857-6.857-7.429-16-11.429-26.286-11.429h-81.714c-19.429 0-35.429 14.857-36.571 34.286-18.857 332-283.429 596.571-615.429 616-19.429 1.143-34.286 17.143-34.286 36v81.714c0 10.286 4 19.429 11.429 26.286 6.857 6.857 16 10.286 25.143 10.286h1.714c200-10.286 388-94.286 529.714-236.571 142.286-141.714 226.286-329.714 236.571-529.714z" />
<glyph unicode="&#xe91e;" glyph-name="heart1" d="M934.176 791.52c-116.128 115.072-301.824 117.472-422.112 9.216-120.32 108.256-305.952 105.856-422.144-9.216-119.712-118.528-119.712-310.688 0-429.28 34.208-33.888 353.696-350.112 353.696-350.112 37.856-37.504 99.072-37.504 136.896 0 0 0 349.824 346.304 353.696 350.112 119.744 118.592 119.744 310.752-0.032 429.28zM888.576 407.424l-353.696-350.112c-12.576-12.512-33.088-12.512-45.6 0l-353.696 350.112c-94.4 93.44-94.4 245.472 0 338.912 91.008 90.080 237.312 93.248 333.088 7.104l43.392-39.040 43.36 39.040c95.808 86.144 242.112 83.008 333.12-7.104 94.4-93.408 94.4-245.44 0.032-338.912zM296.096 719.968c8.864 0 16-7.168 16-16s-7.168-16-16-16h-0.032c-57.408 0-103.968-46.56-103.968-103.968v-0.032c0-8.832-7.168-16-16-16s-16 7.168-16 16v0c0 75.072 60.832 135.904 135.872 135.968 0.064 0 0.064 0.032 0.128 0.032z" />
<glyph unicode="&#xe91f;" glyph-name="paperplane" d="M1009.376 954.88c-5.312 3.424-11.36 5.12-17.376 5.12-6.176 0-12.384-1.76-17.76-5.376l-960-640c-9.888-6.56-15.328-18.112-14.048-29.952 1.216-11.808 8.896-22.016 19.936-26.368l250.368-100.192 117.728-206.016c5.632-9.888 16.096-16 27.424-16.128 0.128 0 0.224 0 0.352 0 11.232 0 21.664 5.952 27.424 15.552l66.464 110.816 310.24-124.064c3.808-1.536 7.808-2.272 11.872-2.272 5.44 0 10.816 1.376 15.68 4.128 8.448 4.736 14.24 13.056 15.872 22.624l160 960c2.080 12.576-3.488 25.184-14.176 32.128zM100.352 295.136l741.6 494.432-539.2-577.184c-2.848 1.696-5.376 3.936-8.512 5.184l-193.888 77.568zM326.048 189.888c-0.064 0.128-0.16 0.192-0.224 0.32l606.176 648.8-516.768-805.184-89.184 156.064zM806.944 12.512l-273.312 109.312c-6.496 2.56-13.248 3.424-19.936 3.808l420.864 652.416-127.616-765.536z" />
<glyph unicode="&#xe920;" glyph-name="banknote" d="M1005.28 621.248l-320 320c-15.872 15.872-38.88 22.24-60.672 16.864-11.488-2.816-21.76-8.736-29.888-16.864-7.264-7.264-12.736-16.256-15.872-26.304-14.496-47.008-39.552-87.872-76.64-124.928-49.536-49.504-114.048-87.008-182.304-126.656-72.448-41.984-147.296-85.504-208.64-146.816-52.128-52.192-87.616-110.24-108.416-177.632-7.008-22.752-0.896-47.36 15.872-64.192l320-320c15.872-15.872 38.88-22.24 60.672-16.864 11.488 2.88 21.76 8.736 29.888 16.864 7.264 7.264 12.736 16.256 15.872 26.368 14.528 47.008 39.584 87.872 76.704 124.928 49.504 49.504 113.984 86.944 182.304 126.56 72.384 42.048 147.264 85.568 208.576 146.88 52.128 52.128 87.616 110.24 108.448 177.632 6.976 22.72 0.832 47.424-15.904 64.16zM384 0c-105.984 105.984-214.016 214.048-320 320 90.944 294.432 485.12 281.568 576 576 105.984-105.952 214.048-214.016 320.064-320-90.976-294.368-485.152-281.568-576.064-576zM625.984 483.2c-10.432 8.736-20.928 14.688-31.488 17.632-10.496 2.944-20.992 4.128-31.616 3.36-10.496-0.8-21.248-3.2-32-7.328-10.752-4.192-21.568-8.736-32.448-14.016-17.184 19.744-34.368 39.264-51.552 57.376 7.744 7.008 15.264 10.56 22.496 10.816 7.264 0.32 14.24-0.448 20.864-2.112 6.752-1.696 12.928-3.136 18.624-4.256 5.76-1.12 10.752 0.128 15.136 3.808 4.64 4 7.2 9.184 7.552 15.424 0.32 6.304-2.048 12.448-7.328 18.432-6.752 7.744-14.88 12.448-24.64 14.176-9.632 1.696-19.488 1.568-29.76-0.672-10.112-2.304-19.744-6.112-28.864-11.488s-16.448-10.88-21.888-16.256c-2.080 1.984-4.16 3.936-6.24 5.888-2.304 2.112-5.184 3.264-8.64 3.2-3.488 0-6.368-1.504-8.736-4.256-2.304-2.688-3.36-5.824-2.944-9.12 0.32-3.424 1.696-6.048 4.064-8.064 2.080-1.76 4.16-3.488 6.24-5.312-8.192-9.888-14.944-20.8-20.256-32.32-5.376-11.552-8.576-23.008-9.76-34.112-1.248-11.2-0.064-21.44 3.36-30.944 3.424-9.568 9.76-17.696 19.008-25.376 15.072-12.512 32.8-17.824 53.376-16.64 20.512 1.248 42.624 7.36 66.4 20.128 18.88-21.824 37.824-43.488 56.736-63.616-8-6.752-15.008-10.624-21.184-11.872-6.176-1.312-11.68-1.184-16.672 0.32-4.992 1.568-9.632 3.808-13.888 6.688-4.256 2.944-8.448 5.44-12.64 7.488-4.128 2.048-8.384 3.2-12.736 3.264s-8.992-2.048-14.112-6.432c-5.248-4.576-7.872-9.888-7.872-15.872 0-5.952 2.752-12 8.128-18.112 5.44-6.112 12.512-11.264 21.056-15.328s18.208-6.624 28.832-7.328c10.624-0.736 21.824 0.864 33.632 5.248 11.872 4.32 23.616 12.128 35.2 23.744 5.568-5.44 11.2-10.624 16.8-15.616 2.368-2.048 5.248-3.072 8.736-2.816 3.36 0.128 6.304 1.696 8.64 4.512 2.368 2.88 3.36 6.048 3.008 9.376-0.32 3.36-1.696 5.952-4 7.808-5.632 4.512-11.264 9.248-16.864 14.24 9.568 11.744 17.248 24.128 22.944 36.384 5.696 12.32 9.056 24.192 10.176 35.2 1.12 11.072-0.192 21.056-3.808 30.112-3.584 9.184-9.952 17.056-19.072 24.64zM447.072 461.504c-9.056-0.384-16.96 2.624-23.872 9.312-2.944 2.816-4.992 6.24-6.24 10.304-1.312 4.064-1.76 8.512-1.248 13.376 0.448 4.8 1.888 9.824 4.384 14.88 2.368 5.056 5.888 10.112 10.368 15.008 16.224-16.128 32.416-33.824 48.64-52.128-12.288-6.752-22.976-10.368-32.032-10.752zM598.016 397.44c-2.88-5.312-6.176-10.048-10.048-14.176-17.952 18.112-35.872 38.016-53.76 58.432 4.576 2.048 9.376 4.192 14.56 6.368s10.368 3.616 15.552 4.512c5.312 0.8 10.56 0.576 15.808-0.672 5.184-1.312 10.112-4.128 14.688-8.576 4.512-4.512 7.36-9.184 8.512-14.24 1.248-5.12 1.312-10.304 0.448-15.616-0.928-5.344-2.816-10.656-5.76-16.032zM470.944 250.24c6.304 5.088 15.584 4.832 21.376-1.056 6.272-6.24 6.272-16.448 0-22.688-0.512-0.512-1.056-0.864-1.632-1.312l0.064-0.064c-20.256-15.392-36.896-29.248-54.848-47.2-16.224-16.192-30.88-33.248-43.552-50.56l-20.448-28c-0.64-1.152-1.408-2.208-2.368-3.2-6.272-6.24-16.48-6.24-22.72 0-5.44 5.44-6.112 13.824-2.112 20.064l-0.064 0.064 21.888 29.888c13.664 18.688 29.376 36.992 46.752 54.368 18.080 18.144 37.6 34.336 57.6 49.696h0.064zM588.096 713.12c16.192 16.192 30.816 33.184 43.52 50.592l21.248 29.12c0.768 1.376 1.632 2.752 2.816 3.936 6.304 6.304 16.512 6.304 22.816 0 5.984-6.016 6.24-15.52 0.8-21.888l0.064-0.064-21.888-30.016c-13.696-18.688-29.376-36.928-46.752-54.304-18.080-18.080-37.568-34.336-57.568-49.696l-0.128 0.064c-6.368-5.856-16.256-5.728-22.368 0.448-6.304 6.304-6.304 16.576 0 22.88 1.12 1.184 2.432 2.016 3.744 2.752 18.816 14.368 36.96 29.44 53.696 46.176z" />
<glyph unicode="&#xe91e;" glyph-name="graphic-heart" d="M934.176 791.52c-116.128 115.072-301.824 117.472-422.112 9.216-120.32 108.256-305.952 105.856-422.144-9.216-119.712-118.528-119.712-310.688 0-429.28 34.208-33.888 353.696-350.112 353.696-350.112 37.856-37.504 99.072-37.504 136.896 0 0 0 349.824 346.304 353.696 350.112 119.744 118.592 119.744 310.752-0.032 429.28zM888.576 407.424l-353.696-350.112c-12.576-12.512-33.088-12.512-45.6 0l-353.696 350.112c-94.4 93.44-94.4 245.472 0 338.912 91.008 90.080 237.312 93.248 333.088 7.104l43.392-39.040 43.36 39.040c95.808 86.144 242.112 83.008 333.12-7.104 94.4-93.408 94.4-245.44 0.032-338.912zM296.096 719.968c8.864 0 16-7.168 16-16s-7.168-16-16-16h-0.032c-57.408 0-103.968-46.56-103.968-103.968v-0.032c0-8.832-7.168-16-16-16s-16 7.168-16 16v0c0 75.072 60.832 135.904 135.872 135.968 0.064 0 0.064 0.032 0.128 0.032z" />
<glyph unicode="&#xe91f;" glyph-name="graphic-paperplane" d="M1009.376 954.88c-5.312 3.424-11.36 5.12-17.376 5.12-6.176 0-12.384-1.76-17.76-5.376l-960-640c-9.888-6.56-15.328-18.112-14.048-29.952 1.216-11.808 8.896-22.016 19.936-26.368l250.368-100.192 117.728-206.016c5.632-9.888 16.096-16 27.424-16.128 0.128 0 0.224 0 0.352 0 11.232 0 21.664 5.952 27.424 15.552l66.464 110.816 310.24-124.064c3.808-1.536 7.808-2.272 11.872-2.272 5.44 0 10.816 1.376 15.68 4.128 8.448 4.736 14.24 13.056 15.872 22.624l160 960c2.080 12.576-3.488 25.184-14.176 32.128zM100.352 295.136l741.6 494.432-539.2-577.184c-2.848 1.696-5.376 3.936-8.512 5.184l-193.888 77.568zM326.048 189.888c-0.064 0.128-0.16 0.192-0.224 0.32l606.176 648.8-516.768-805.184-89.184 156.064zM806.944 12.512l-273.312 109.312c-6.496 2.56-13.248 3.424-19.936 3.808l420.864 652.416-127.616-765.536z" />
<glyph unicode="&#xe920;" glyph-name="graphic-banknote" d="M1005.28 621.248l-320 320c-15.872 15.872-38.88 22.24-60.672 16.864-11.488-2.816-21.76-8.736-29.888-16.864-7.264-7.264-12.736-16.256-15.872-26.304-14.496-47.008-39.552-87.872-76.64-124.928-49.536-49.504-114.048-87.008-182.304-126.656-72.448-41.984-147.296-85.504-208.64-146.816-52.128-52.192-87.616-110.24-108.416-177.632-7.008-22.752-0.896-47.36 15.872-64.192l320-320c15.872-15.872 38.88-22.24 60.672-16.864 11.488 2.88 21.76 8.736 29.888 16.864 7.264 7.264 12.736 16.256 15.872 26.368 14.528 47.008 39.584 87.872 76.704 124.928 49.504 49.504 113.984 86.944 182.304 126.56 72.384 42.048 147.264 85.568 208.576 146.88 52.128 52.128 87.616 110.24 108.448 177.632 6.976 22.72 0.832 47.424-15.904 64.16zM384 0c-105.984 105.984-214.016 214.048-320 320 90.944 294.432 485.12 281.568 576 576 105.984-105.952 214.048-214.016 320.064-320-90.976-294.368-485.152-281.568-576.064-576zM625.984 483.2c-10.432 8.736-20.928 14.688-31.488 17.632-10.496 2.944-20.992 4.128-31.616 3.36-10.496-0.8-21.248-3.2-32-7.328-10.752-4.192-21.568-8.736-32.448-14.016-17.184 19.744-34.368 39.264-51.552 57.376 7.744 7.008 15.264 10.56 22.496 10.816 7.264 0.32 14.24-0.448 20.864-2.112 6.752-1.696 12.928-3.136 18.624-4.256 5.76-1.12 10.752 0.128 15.136 3.808 4.64 4 7.2 9.184 7.552 15.424 0.32 6.304-2.048 12.448-7.328 18.432-6.752 7.744-14.88 12.448-24.64 14.176-9.632 1.696-19.488 1.568-29.76-0.672-10.112-2.304-19.744-6.112-28.864-11.488s-16.448-10.88-21.888-16.256c-2.080 1.984-4.16 3.936-6.24 5.888-2.304 2.112-5.184 3.264-8.64 3.2-3.488 0-6.368-1.504-8.736-4.256-2.304-2.688-3.36-5.824-2.944-9.12 0.32-3.424 1.696-6.048 4.064-8.064 2.080-1.76 4.16-3.488 6.24-5.312-8.192-9.888-14.944-20.8-20.256-32.32-5.376-11.552-8.576-23.008-9.76-34.112-1.248-11.2-0.064-21.44 3.36-30.944 3.424-9.568 9.76-17.696 19.008-25.376 15.072-12.512 32.8-17.824 53.376-16.64 20.512 1.248 42.624 7.36 66.4 20.128 18.88-21.824 37.824-43.488 56.736-63.616-8-6.752-15.008-10.624-21.184-11.872-6.176-1.312-11.68-1.184-16.672 0.32-4.992 1.568-9.632 3.808-13.888 6.688-4.256 2.944-8.448 5.44-12.64 7.488-4.128 2.048-8.384 3.2-12.736 3.264s-8.992-2.048-14.112-6.432c-5.248-4.576-7.872-9.888-7.872-15.872 0-5.952 2.752-12 8.128-18.112 5.44-6.112 12.512-11.264 21.056-15.328s18.208-6.624 28.832-7.328c10.624-0.736 21.824 0.864 33.632 5.248 11.872 4.32 23.616 12.128 35.2 23.744 5.568-5.44 11.2-10.624 16.8-15.616 2.368-2.048 5.248-3.072 8.736-2.816 3.36 0.128 6.304 1.696 8.64 4.512 2.368 2.88 3.36 6.048 3.008 9.376-0.32 3.36-1.696 5.952-4 7.808-5.632 4.512-11.264 9.248-16.864 14.24 9.568 11.744 17.248 24.128 22.944 36.384 5.696 12.32 9.056 24.192 10.176 35.2 1.12 11.072-0.192 21.056-3.808 30.112-3.584 9.184-9.952 17.056-19.072 24.64zM447.072 461.504c-9.056-0.384-16.96 2.624-23.872 9.312-2.944 2.816-4.992 6.24-6.24 10.304-1.312 4.064-1.76 8.512-1.248 13.376 0.448 4.8 1.888 9.824 4.384 14.88 2.368 5.056 5.888 10.112 10.368 15.008 16.224-16.128 32.416-33.824 48.64-52.128-12.288-6.752-22.976-10.368-32.032-10.752zM598.016 397.44c-2.88-5.312-6.176-10.048-10.048-14.176-17.952 18.112-35.872 38.016-53.76 58.432 4.576 2.048 9.376 4.192 14.56 6.368s10.368 3.616 15.552 4.512c5.312 0.8 10.56 0.576 15.808-0.672 5.184-1.312 10.112-4.128 14.688-8.576 4.512-4.512 7.36-9.184 8.512-14.24 1.248-5.12 1.312-10.304 0.448-15.616-0.928-5.344-2.816-10.656-5.76-16.032zM470.944 250.24c6.304 5.088 15.584 4.832 21.376-1.056 6.272-6.24 6.272-16.448 0-22.688-0.512-0.512-1.056-0.864-1.632-1.312l0.064-0.064c-20.256-15.392-36.896-29.248-54.848-47.2-16.224-16.192-30.88-33.248-43.552-50.56l-20.448-28c-0.64-1.152-1.408-2.208-2.368-3.2-6.272-6.24-16.48-6.24-22.72 0-5.44 5.44-6.112 13.824-2.112 20.064l-0.064 0.064 21.888 29.888c13.664 18.688 29.376 36.992 46.752 54.368 18.080 18.144 37.6 34.336 57.6 49.696h0.064zM588.096 713.12c16.192 16.192 30.816 33.184 43.52 50.592l21.248 29.12c0.768 1.376 1.632 2.752 2.816 3.936 6.304 6.304 16.512 6.304 22.816 0 5.984-6.016 6.24-15.52 0.8-21.888l0.064-0.064-21.888-30.016c-13.696-18.688-29.376-36.928-46.752-54.304-18.080-18.080-37.568-34.336-57.568-49.696l-0.128 0.064c-6.368-5.856-16.256-5.728-22.368 0.448-6.304 6.304-6.304 16.576 0 22.88 1.12 1.184 2.432 2.016 3.744 2.752 18.816 14.368 36.96 29.44 53.696 46.176z" />
<glyph unicode="&#xe986;" glyph-name="search" d="M992.262 88.604l-242.552 206.294c-25.074 22.566-51.89 32.926-73.552 31.926 57.256 67.068 91.842 154.078 91.842 249.176 0 212.078-171.922 384-384 384-212.076 0-384-171.922-384-384s171.922-384 384-384c95.098 0 182.108 34.586 249.176 91.844-1-21.662 9.36-48.478 31.926-73.552l206.294-242.552c35.322-39.246 93.022-42.554 128.22-7.356s31.892 92.898-7.354 128.22zM384 320c-141.384 0-256 114.616-256 256s114.616 256 256 256 256-114.616 256-256-114.614-256-256-256z" />
<glyph unicode="&#xe9d7;" glyph-name="star-empty" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538zM512 206.502l-223.462-117.48 42.676 248.83-180.786 176.222 249.84 36.304 111.732 226.396 111.736-226.396 249.836-36.304-180.788-176.222 42.678-248.83-223.462 117.48z" />
<glyph unicode="&#xe9d8;" glyph-name="star-half" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538zM512 206.502l-0.942-0.496 0.942 570.768 111.736-226.396 249.836-36.304-180.788-176.222 42.678-248.83-223.462 117.48z" />

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -1,156 +1,150 @@
/** @todo Replace icons with SVG symbols.
@see https://www.youtube.com/watch?v=9xXBYcWgCHA */
@font-face {
font-family: 'icomoon';
src: url('../fonts/icomoon.eot?n5x55');
src: url('../fonts/icomoon.eot?n5x55#iefix') format('embedded-opentype'),
url('../fonts/icomoon.ttf?n5x55') format('truetype'),
url('../fonts/icomoon.woff?n5x55') format('woff'),
url('../fonts/icomoon.svg?n5x55#icomoon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
font-family: 'icomoon';
src: url('../fonts/icomoon.eot?19nagi');
src: url('../fonts/icomoon.eot?19nagi#iefix') format('embedded-opentype'),
url('../fonts/icomoon.ttf?19nagi') format('truetype'),
url('../fonts/icomoon.woff?19nagi') format('woff'),
url('../fonts/icomoon.svg?19nagi#icomoon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}
[class^="icon-"], [class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icomoon' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icomoon' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-graphic-heart:before {
content: "\e91e";
content: "\e91e";
}
.icon-graphic-paperplane:before {
content: "\e91f";
content: "\e91f";
}
.icon-graphic-banknote:before {
content: "\e920";
}
.icon-stars:before {
content: "\e91a";
content: "\e920";
}
.icon-warning:before {
content: "\e91b";
content: "\e91b";
}
.icon-book:before {
content: "\e900";
content: "\e900";
}
.icon-bookmark:before {
content: "\e91c";
content: "\e91a";
}
.icon-rss:before {
content: "\e91d";
content: "\e91d";
}
.icon-envelope:before {
content: "\e901";
content: "\e901";
}
.icon-arrow-right:before {
content: "\e902";
content: "\e902";
}
.icon-bell:before {
content: "\e903";
content: "\e903";
}
.icon-x:before {
content: "\e904";
content: "\e904";
}
.icon-quote-close:before {
content: "\e905";
content: "\e905";
}
.icon-quote-open:before {
content: "\e906";
content: "\e906";
}
.icon-image:before {
content: "\e907";
content: "\e907";
}
.icon-pencil:before {
content: "\e908";
content: "\e908";
}
.icon-list:before {
content: "\e909";
content: "\e909";
}
.icon-unlock:before {
content: "\e90a";
content: "\e90a";
}
.icon-unlisted:before {
content: "\e90a";
content: "\e90a";
}
.icon-globe:before {
content: "\e90b";
content: "\e90b";
}
.icon-public:before {
content: "\e90b";
content: "\e90b";
}
.icon-lock:before {
content: "\e90c";
content: "\e90c";
}
.icon-followers:before {
content: "\e90c";
content: "\e90c";
}
.icon-chain-broken:before {
content: "\e90d";
content: "\e90d";
}
.icon-chain:before {
content: "\e90e";
content: "\e90e";
}
.icon-comments:before {
content: "\e90f";
content: "\e90f";
}
.icon-comment:before {
content: "\e910";
content: "\e910";
}
.icon-boost:before {
content: "\e911";
content: "\e911";
}
.icon-arrow-left:before {
content: "\e912";
content: "\e912";
}
.icon-arrow-up:before {
content: "\e913";
content: "\e913";
}
.icon-arrow-down:before {
content: "\e914";
content: "\e914";
}
.icon-home:before {
content: "\e915";
content: "\e915";
}
.icon-local:before {
content: "\e916";
content: "\e916";
}
.icon-dots-three:before {
content: "\e917";
content: "\e917";
}
.icon-check:before {
content: "\e918";
content: "\e918";
}
.icon-dots-three-vertical:before {
content: "\e919";
content: "\e919";
}
.icon-search:before {
content: "\e986";
content: "\e986";
}
.icon-star-empty:before {
content: "\e9d7";
content: "\e9d7";
}
.icon-star-half:before {
content: "\e9d8";
content: "\e9d8";
}
.icon-star-full:before {
content: "\e9d9";
content: "\e9d9";
}
.icon-heart:before {
content: "\e9da";
content: "\e9da";
}
.icon-plus:before {
content: "\ea0a";
content: "\ea0a";
}

View file

@ -138,8 +138,11 @@ let BookWyrm = new class {
* @return {undefined}
*/
toggleAction(event) {
event.preventDefault();
let trigger = event.currentTarget;
if (!trigger.dataset.allowDefault || event.currentTarget == event.target) {
event.preventDefault();
}
let pressed = trigger.getAttribute('aria-pressed') === 'false';
let targetId = trigger.dataset.controls;
@ -177,6 +180,13 @@ let BookWyrm = new class {
this.toggleCheckbox(checkbox, pressed);
}
// Toggle form disabled, if appropriate
let disable = trigger.dataset.disables;
if (disable) {
this.toggleDisabled(disable, !pressed);
}
// Set focus, if appropriate.
let focus = trigger.dataset.focusTarget;
@ -227,6 +237,17 @@ let BookWyrm = new class {
document.getElementById(checkbox).checked = !!pressed;
}
/**
* Enable or disable a form element or fieldset
*
* @param {string} form_element - id of the element
* @param {boolean} pressed - Is the trigger pressed?
* @return {undefined}
*/
toggleDisabled(form_element, pressed) {
document.getElementById(form_element).disabled = !!pressed;
}
/**
* Give the focus to an element.
* Only move the focus based on user interactions.

View file

@ -1,4 +1,6 @@
"""Handles backends for storages"""
import os
from tempfile import SpooledTemporaryFile
from storages.backends.s3boto3 import S3Boto3Storage
@ -15,3 +17,33 @@ class ImagesStorage(S3Boto3Storage): # pylint: disable=abstract-method
location = "images"
default_acl = "public-read"
file_overwrite = False
"""
This is our custom version of S3Boto3Storage that fixes a bug in
boto3 where the passed in file is closed upon upload.
From:
https://github.com/matthewwithanm/django-imagekit/issues/391#issuecomment-275367006
https://github.com/boto/boto3/issues/929
https://github.com/matthewwithanm/django-imagekit/issues/391
"""
def _save(self, name, content):
"""
We create a clone of the content file as when this is passed to
boto3 it wrongly closes the file upon upload where as the storage
backend expects it to still be open
"""
# Seek our content back to the start
content.seek(0, os.SEEK_SET)
# Create a temporary file that will write to disk after a specified
# size. This file will be automatically deleted when closed by
# boto3 or after exiting the `with` statement if the boto3 is fixed
with SpooledTemporaryFile() as content_autoclose:
# Write our original content into our copy that will be closed by boto3
content_autoclose.write(content.read())
# Upload the object which will auto close the
# content_autoclose instance
return super()._save(name, content_autoclose)

View file

@ -57,7 +57,7 @@
{% if author.wikipedia_link %}
<p class="my-1">
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel=”noopener” target="_blank">
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener" target="_blank">
{% trans "Wikipedia" %}
</a>
</p>

View file

@ -62,7 +62,7 @@
<div class="columns">
<div class="column is-one-fifth">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m-mobile' %}
{% include 'snippets/book_cover.html' with size='xxlarge' size_mobile='medium' book=book cover_class='is-h-m-mobile' %}
{% include 'snippets/rate_action.html' with user=request.user book=book %}
<div class="mb-3">
@ -134,7 +134,7 @@
<form name="add-description" method="POST" action="/add-description/{{ book.id }}">
{% csrf_token %}
<p class="fields is-grouped">
<label class="label"for="id_description_{{ book.id }}">{% trans "Description:" %}</label>
<label class="label" for="id_description_{{ book.id }}">{% trans "Description:" %}</label>
<textarea name="description" cols="None" rows="None" class="textarea" id="id_description_{{ book.id }}"></textarea>
</p>
<div class="field">

View file

@ -42,11 +42,18 @@
</div>
{% endif %}
{% if book %}
<form class="block" name="edit-book" action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}" method="post" enctype="multipart/form-data">
{% else %}
<form class="block" name="create-book" action="/create-book{% if confirm_mode %}/confirm{% endif %}" method="post" enctype="multipart/form-data">
{% endif %}
<form
class="block"
{% if book %}
name="edit-book"
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
{% else %}
name="create-book"
action="/create-book{% if confirm_mode %}/confirm{% endif %}"
{% endif %}
method="post"
enctype="multipart/form-data"
>
{% csrf_token %}
{% if confirm_mode %}
@ -220,7 +227,7 @@
<h2 class="title is-4">{% trans "Cover" %}</h2>
<div class="columns">
<div class="column is-3 is-cover">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-xl-mobile is-w-auto-tablet' %}
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-xl-mobile is-w-auto-tablet' size_mobile='xlarge' size='large' %}
</div>
<div class="column">

View file

@ -1,6 +1,7 @@
{% extends 'snippets/filters_panel/filters_panel.html' %}
{% block filter_fields %}
{% include 'book/search_filter.html' %}
{% include 'book/language_filter.html' %}
{% include 'book/format_filter.html' %}
{% endblock %}

View file

@ -15,7 +15,7 @@
<div class="columns is-gapless mb-6">
<div class="column is-cover">
<a href="{{ book.local_path }}">
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-m is-h-m align to-l-mobile' %}
{% include 'snippets/book_cover.html' with size='medium' book=book cover_class='is-w-m is-h-m align to-l-mobile' %}
</a>
</div>

View file

@ -6,7 +6,6 @@
<div class="columns">
<div class="column">
{% trans "Progress Updates:" %}
</dl>
<ul>
{% if readthrough.finish_date or readthrough.progress %}
<li>

View file

@ -0,0 +1,8 @@
{% extends 'snippets/filters_panel/filter_field.html' %}
{% load i18n %}
{% block filter %}
<label class="label" for="id_search">{% trans "Search editions" %}</label>
<input type="text" class="input" name="q" value="{{ request.GET.q|default:'' }}" id="id_search">
{% endblock %}

View file

@ -10,7 +10,7 @@
<a
class="align to-b to-l"
href="{{ book.local_path }}"
>{% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-w-auto-tablet' %}</a>
>{% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-w-auto-tablet' size='xxlarge' %}</a>
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
<h3 class="title is-6">

View file

@ -6,7 +6,7 @@
{% if status.book or status.mention_books.exists %}
{% load_book status as book %}
<a href="{{ book.local_path }}">
{% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-w-auto align to-b to-l' %}
{% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-w-auto align to-b to-l' size='xxlarge' %}
</a>
<div class="block mt-2">

View file

@ -1,4 +1,4 @@
<html>
<html lang="{% get_lang %}">
<body>
<div>
<strong>Subject:</strong> {% include subject_path %}

View file

@ -40,11 +40,10 @@
{% if suggested_users %}
{# suggested users for when things are very lonely #}
{% include 'feed/suggested_users.html' with suggested_users=suggested_users %}
{% endif %}
</div>
{% endif %}
{% endif %}
{% for activity in activities %}
{% if not activities.number > 1 and forloop.counter0 == 2 and suggested_users %}

View file

@ -2,5 +2,5 @@
<section class="block">
<h2 class="title is-5">{% trans "Who to follow" %}</h2>
{% include 'snippets/suggested_users.html' with suggested_users=suggested_users %}
<a class="help" href="{% url 'directory' %}">View directory <span class="icon icon-arrow-right"></a>
<a class="help" href="{% url 'directory' %}">{% trans "View directory" %} <span class="icon icon-arrow-right"></span></a>
</section>

View file

@ -1,6 +1,6 @@
{% load i18n %}
<div class="column is-cover">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-l is-h-m-mobile' %}
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-l is-h-m-mobile' size_mobile='medium' size='large' %}
<div class="select is-small mt-1 mb-3">
<select name="{{ book.id }}" aria-label="{% blocktrans with book_title=book.title %}Have you read {{ book_title }}?{% endblocktrans %}">

View file

@ -20,10 +20,10 @@
<dt class="has-text-weight-medium">{% trans "Import completed:" %}</dt>
<dd class="ml-2">{{ task.date_done | naturaltime }}</dd>
</div>
{% elif task.failed %}
<div class="notification is-danger">{% trans "TASK FAILED" %}</div>
{% endif %}
</dl>
{% elif task.failed %}
<div class="notification is-danger">{% trans "TASK FAILED" %}</div>
{% endif %}
</div>
<div class="block">
@ -84,26 +84,26 @@
<button class="button is-block mt-3" type="submit">{% trans "Retry items" %}</button>
</fieldset>
<hr>
{% else %}
<ul>
{% for item in failed_items %}
<li class="pb-1">
<p>
Line {{ item.index }}:
<strong>{{ item.data.Title }}</strong> by
{{ item.data.Author }}
</p>
<p>
{{ item.fail_reason }}.
</p>
</li>
{% endfor %}
</ul>
{% endif %}
</form>
<hr>
{% else %}
<ul>
{% for item in failed_items %}
<li class="pb-1">
<p>
Line {{ item.index }}:
<strong>{{ item.data.Title }}</strong> by
{{ item.data.Author }}
</p>
<p>
{{ item.fail_reason }}.
</p>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endif %}
@ -132,7 +132,7 @@
<td>
{% if item.book %}
<a href="{{ item.book.local_path }}">
{% include 'snippets/book_cover.html' with book=item.book cover_class='is-h-s' %}
{% include 'snippets/book_cover.html' with book=item.book cover_class='is-h-s' size='small' %}
</a>
{% endif %}
</td>
@ -153,7 +153,6 @@
{% endfor %}
</table>
</div>
</div>
{% endspaceless %}{% endblock %}
{% block scripts %}

View file

@ -1,33 +0,0 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block title %}{% trans "Search Results" %}{% endblock %}
{% block content %}
{% with book_results|first as local_results %}
<div class="block">
<h1 class="title">{% blocktrans %}Search Results for "{{ query }}"{% endblocktrans %}</h1>
</div>
<div class="block columns">
<div class="column">
<h2 class="title">{% trans "Matching Books" %}</h2>
<section class="block">
{% if not results %}
<p>{% blocktrans %}No books found for "{{ query }}"{% endblocktrans %}</p>
{% else %}
<ul>
{% for result in results %}
<li class="pd-4">
<a href="{{ result.key }}">{% include 'snippets/search_result_text.html' with result=result link=True %}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</section>
<div class="column">
</div>
</div>
{% endwith %}
{% endblock %}

View file

@ -9,7 +9,7 @@
<a
class="align to-b to-l"
href="{{ book.local_path }}"
>{% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-w-auto-tablet' %}</a>
>{% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-w-auto-tablet' size='xxlarge' %}</a>
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
</div>

View file

@ -4,7 +4,7 @@
{% if book %}
{% with book=book %}
<a href="{{ book.local_path }}">
{% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-w-auto align to-b to-l' %}
{% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-w-auto align to-b to-l' size='xxlarge' %}
</a>
{% include 'snippets/stars.html' with rating=book|rating:request.user %}

View file

@ -109,17 +109,17 @@
{% trans 'Settings' %}
</a>
</li>
{% if perms.bookwyrm.create_invites or perms.moderate_users %}
{% if perms.bookwyrm.create_invites or perms.moderate_user %}
<li class="navbar-divider" role="presentation"></li>
{% endif %}
{% if perms.bookwyrm.create_invites %}
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
<li>
<a href="{% url 'settings-invite-requests' %}" class="navbar-item">
{% trans 'Invites' %}
</a>
</li>
{% endif %}
{% if perms.bookwyrm.moderate_users %}
{% if perms.bookwyrm.moderate_user %}
<li>
<a href="{% url 'settings-users' %}" class="navbar-item">
{% trans 'Admin' %}

View file

@ -0,0 +1,39 @@
{% load i18n %}
{% load interaction %}
{% if request.user.is_authenticated %}
{% with request.user|saved:list as saved %}
<form
name="save-{{ list.id }}"
action="{% url 'list-save' list.id %}"
method="POST"
class="interaction save_{{ list.id }} {% if saved %}is-hidden{% endif %}"
data-id="save_{{ list.id }}"
>
{% csrf_token %}
{% trans "Save" as text %}
<button class="button" type="submit">
<span class="icon icon-bookmark m-0-mobile" title="{{ text }}"></span>
<span class="is-sr-only-mobile">{{ text }}</span>
</button>
</form>
<form
name="unsave-{{ list.id }}"
action="{% url 'list-unsave' list.id %}"
method="POST"
class="interaction save_{{ list.id }} active {% if not saved %}is-hidden{% endif %}"
data-id="save_{{ list.id }}"
>
{% csrf_token %}
{% trans "Un-save" as text %}
<button class="button" type="submit">
<span class="icon icon-bookmark m-0-mobile has-text-primary" title="{{ text }}"></span>
<span class="is-sr-only-mobile">{{ text }}</span>
</button>
</form>
{% endwith %}
{% endif %}

View file

@ -32,7 +32,7 @@
href="{{ book.local_path }}"
aria-hidden="true"
>
{% include 'snippets/book_cover.html' with cover_class='is-w-xs-mobile is-w-s is-h-xs-mobile is-h-s' %}
{% include 'snippets/book_cover.html' with cover_class='is-w-xs-mobile is-w-s is-h-xs-mobile is-h-s' size_mobile='xsmall' size='small' %}
</a>
<div class="column ml-3">

View file

@ -41,7 +41,7 @@
>
<div class="column is-3-mobile is-2-tablet is-cover align to-t">
<a href="{{ item.book.local_path }}" aria-hidden="true">
{% include 'snippets/book_cover.html' with cover_class='is-w-auto is-h-m-tablet is-align-items-flex-start' %}
{% include 'snippets/book_cover.html' with cover_class='is-w-auto is-h-m-tablet is-align-items-flex-start' size='medium' %}
</a>
</div>
@ -161,7 +161,7 @@
href="{{ book.local_path }}"
aria-hidden="true"
>
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto is-h-s-mobile align to-t' %}
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto is-h-s-mobile align to-t' size='small' %}
</a>
<div class="column ml-3">

View file

@ -1,4 +1,7 @@
{% load i18n %}
{% load markdown %}
{% load interaction %}
<div class="columns is-multiline">
{% for list in lists %}
<div class="column is-one-quarter">
@ -7,6 +10,14 @@
<h4 class="card-header-title">
<a href="{{ list.local_path }}">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
</h4>
{% if request.user.is_authenticated and request.user|saved:list %}
<div class="card-header-icon">
{% trans "Saved" as text %}
<span class="icon icon-bookmark has-text-grey" title="{{ text }}">
<span class="is-sr-only">{{ text }}</span>
</span>
</div>
{% endif %}
</header>
{% with list_books=list.listitem_set.all|slice:5 %}
@ -14,7 +25,7 @@
<div class="card-image columns is-mobile is-gapless is-clipped">
{% for book in list_books %}
<a class="column is-cover" href="{{ book.book.local_path }}">
{% include 'snippets/book_cover.html' with book=book.book cover_class='is-h-s' aria='show' %}
{% include 'snippets/book_cover.html' with book=book.book cover_class='is-h-s' size='small' aria='show' %}
</a>
{% endfor %}
</div>

View file

@ -11,12 +11,13 @@
{% include 'lists/created_text.html' with list=list %}
</p>
</div>
{% if request.user == list.user %}
<div class="column is-narrow">
{% trans "Edit List" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_list" focus="edit_list_header" %}
<div class="column is-narrow is-flex">
{% if request.user == list.user %}
{% trans "Edit List" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_list" focus="edit_list_header" %}
{% endif %}
{% include "lists/bookmark_button.html" with list=list %}
</div>
{% endif %}
</header>
<div class="block content">

View file

@ -27,6 +27,21 @@
{% include 'lists/create_form.html' with controls_text="create_list" %}
</div>
{% if request.user.is_authenticated %}
<nav class="tabs">
<ul>
{% url 'lists' as url %}
<li{% if request.path in url %} class="is-active"{% endif %}>
<a href="{{ url }}">{% trans "All Lists" %}</a>
</li>
{% url 'saved-lists' as url %}
<li{% if url in request.path %} class="is-active"{% endif %}>
<a href="{{ url }}">{% trans "Saved Lists" %}</a>
</li>
</ul>
</nav>
{% endif %}
{% if lists %}
<section class="block">
{% include 'lists/list_items.html' with lists=lists %}

View file

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

View file

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

View file

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

View file

@ -21,23 +21,29 @@
{% if perms.bookwyrm.create_invites %}
<h2 class="menu-label">{% trans "Manage Users" %}</h2>
<ul class="menu-list">
{% if perms.bookwyrm.moderate_user %}
<li>
{% url 'settings-users' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Users" %}</a>
</li>
{% endif %}
<li>
{% url 'settings-invite-requests' as 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>
</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 %}
<li>
{% url 'settings-federation' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Federated Instances" %}</a>
</li>
{% endif %}
</ul>
{% endif %}
{% if perms.bookwyrm.edit_instance_settings %}

View file

@ -7,7 +7,6 @@
{% block edit-button %}
{% trans "Create Announcement" as button_text %}
{% include 'snippets/toggle/open_button.html' with controls_text="create_announcement" icon_with_text="plus" text=button_text focus="create_announcement_header" %}
</a>
{% endblock %}
{% block panel %}

View file

@ -54,7 +54,7 @@
{% endif %}
{% for invite in invites %}
<tr>
<td><a href="{{ invite.link }}">{{ invite.link }}</td>
<td><a href="{{ invite.link }}">{{ invite.link }}</a></td>
<td>{{ invite.expiry|naturaltime }}</td>
<td>{{ invite.use_limit }}</td>
<td>{{ invite.times_used }}</td>

View file

@ -47,7 +47,7 @@
<div class="field">
<label class="label" for="id_file">JSON data:</label>
<aside class="help">
Expects a json file in the format provided by <a href="https://fediblock.org/" target="_blank" rel=”noopener”>FediBlock</a>, with a list of entries that have <code>instance</code> and <code>url</code> fields. For example:
Expects a json file in the format provided by <a href="https://fediblock.org/" target="_blank" rel="noopener">FediBlock</a>, with a list of entries that have <code>instance</code> and <code>url</code> fields. For example:
<pre>
[
{

View file

@ -2,41 +2,67 @@
{% load i18n %}
{% load static %}
{% load imagekit %}
{% load utilities %}
<figure
class="
cover-container
{{ cover_class }}
{% if not book.cover %}
no-cover
{% endif %}
"
{% if book.alt_text %}
title="{{ book.alt_text }}"
{% endif %}
>
<img
class="book-cover"
{% if book.cover %}
src="{% if img_path is None %}{% get_media_prefix %}{% else %}{{ img_path }}{% endif %}{{ book.cover }}"
itemprop="thumbnailUrl"
{% if book.alt_text %}
alt="{{ book.alt_text }}"
{% endif %}
{% if book.cover %}
<picture class="cover-container {{ cover_class }}">
{% if external_path %}
<img
class="book-cover"
src="{{ book.cover }}"
itemprop="thumbnailUrl"
alt="{{ book.alt_text|default:'' }}"
>
{% else %}
{% if thumbnail_generation_enabled %}
{% if size_mobile %}
<source
media="(max-width: 768px)"
type="image/webp"
srcset="{% get_book_cover_thumbnail book=book size=size_mobile ext='webp' %}"
/>
<source
media="(max-width: 768px)"
type="image/jpg"
srcset="{% get_book_cover_thumbnail book=book size=size_mobile ext='jpg' %}"
/>
{% endif %}
<source
type="image/webp"
srcset="{% get_book_cover_thumbnail book=book size=size ext='webp' %}"
/>
<source
type="image/jpg"
srcset="{% get_book_cover_thumbnail book=book size=size ext='jpg' %}"
/>
{% endif %}
<img
alt="{{ book.alt_text|default:'' }}"
class="book-cover"
itemprop="thumbnailUrl"
src="{% if img_path is None %}{% get_media_prefix %}{% else %}{{ img_path }}{% endif %}{{ book.cover }}"
>
{% endif %}
</picture>
{% endif %}
{% if not book.cover and book.alt_text %}
<figure class="cover-container no-cover {{ cover_class }}">
<img
class="book-cover"
src="{% static "images/no_cover.jpg" %}"
alt="{% trans "No cover" %}"
{% endif %}
>
{% if not book.cover and book.alt_text %}
<figcaption class="cover_caption">
>
<figcaption class="cover-caption">
<p>{{ book.alt_text }}</p>
</figcaption>
{% endif %}
</figure>
</figure>
{% endif %}
{% endspaceless %}

View file

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

View file

@ -4,15 +4,27 @@
{% with status.id|uuid as uuid %}
{% with request.user|liked:status as liked %}
<form name="favorite" action="/favorite/{{ status.id }}" method="POST" class="interaction fav_{{ status.id }}_{{ uuid }} {% if liked %}is-hidden{% endif %}" data-id="fav_{{ status.id }}_{{ uuid }}">
<form
name="favorite"
action="{% url 'fav' status.id %}"
method="POST"
class="interaction fav_{{ status.id }}_{{ uuid }} {% if liked %}is-hidden{% endif %}"
data-id="fav_{{ status.id }}_{{ uuid }}"
>
{% csrf_token %}
<button class="button is-small is-light is-transparent" type="submit">
<span class="icon icon-heart m-0-mobile" title="{% trans 'Like' %}">
</span>
<span class="icon icon-heart m-0-mobile" title="{% trans 'Like' %}"></span>
<span class="is-sr-only-mobile">{% trans "Like" %}</span>
</button>
</form>
<form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" class="interaction fav_{{ status.id }}_{{ uuid }} active {% if not liked %}is-hidden{% endif %}" data-id="fav_{{ status.id }}_{{ uuid }}">
<form
name="unfavorite"
action="{% url 'unfav' status.id %}"
method="POST"
class="interaction fav_{{ status.id }}_{{ uuid }} active {% if not liked %}is-hidden{% endif %}"
data-id="fav_{{ status.id }}_{{ uuid }}"
>
{% csrf_token %}
<button class="button is-light is-transparent is-small" type="submit">
<span class="icon icon-heart has-text-primary m-0-mobile" title="{% trans 'Un-like' %}"></span>

View file

@ -0,0 +1,36 @@
{% extends 'snippets/reading_modals/layout.html' %}
{% load i18n %}
{% load utilities %}
{% block modal-title %}
{% blocktrans trimmed with book_title=book|book_title %}
Finish "<em>{{ book_title }}</em>"
{% endblocktrans %}
{% endblock %}
{% block modal-form-open %}
<form name="finish-reading" action="{% url 'reading-status' 'finish' book.id %}" method="post">
{% csrf_token %}
<input type="hidden" name="reading_status" value="read">
{% endblock %}
{% block reading-dates %}
<div class="columns">
<div class="column is-half">
<div class="field">
<label class="label" for="finish_id_start_date_{{ uuid }}">
{% trans "Started reading" %}
</label>
<input type="date" name="start_date" class="input" id="finish_id_start_date_{{ uuid }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
</div>
</div>
<div class="column is-half">
<div class="field">
<label class="label" for="id_finish_date_{{ uuid }}">
{% trans "Finished reading" %}
</label>
<input type="date" name="finish_date" class="input" id="id_finish_date_{{ uuid }}" value="{% now "Y-m-d" %}">
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends "snippets/create_status/layout.html" %}
{% load i18n %}
{% block form_open %}{% endblock %}
{% block content_label %}
{% trans "Comment:" %}
<span class="help mt-0 has-text-weight-normal">{% trans "(Optional)" %}</span>
{% endblock %}
{% block initial_fields %}
<input type="hidden" name="user" value="{{ request.user.id }}">
<input type="hidden" name="mention_books" value="{{ book.id }}">
<input type="hidden" name="book" value="{{ book.id }}">
{% endblock %}

View file

@ -0,0 +1,28 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% load utilities %}
{% block modal-body %}
{% block reading-dates %}{% endblock %}
{% with 0|uuid as local_uuid %}
<div class="is-flex is-justify-content-space-between">
<label for="post_status_{{ local_uuid }}_{{ uuid }}" data-controls="reading_content_{{ local_uuid }}_{{ uuid }}" data-controls-checkbox="post_status_{{ local_uuid }}_{{ uuid }}" data-disables="reading_content_fieldset_{{ local_uuid }}_{{ uuid }}" aria-pressed="true" data-allow-default="true">
<input type="checkbox" name="post-status" class="checkbox" id="post_status_{{ local_uuid }}_{{ uuid }}" checked>
{% trans "Post to feed" %}
</label>
<div class="is-hidden" id="hide_reading_content_{{ local_uuid }}_{{ uuid }}">
<button class="button is-link" type="submit">{% trans "Save" %}</button>
</div>
</div>
<div id="reading_content_{{ local_uuid }}_{{ uuid }}">
<hr aria-hidden="true">
<fieldset id="reading_content_fieldset_{{ local_uuid }}_{{ uuid }}">
{% include "snippets/reading_modals/form.html" with optional=True %}
</fieldset>
</div>
{% endwith %}
{% endblock %}

View file

@ -0,0 +1,24 @@
{% extends 'snippets/reading_modals/layout.html' %}
{% load i18n %}
{% load utilities %}
{% block modal-title %}
{% blocktrans trimmed with book_title=book|book_title %}
Start "<em>{{ book_title }}</em>"
{% endblocktrans %}
{% endblock %}
{% block modal-form-open %}
<form name="start-reading" action="{% url 'reading-status' 'start' book.id %}" method="post">
<input type="hidden" name="reading_status" value="reading">
{% csrf_token %}
{% endblock %}
{% block reading-dates %}
<div class="field">
<label class="label" for="start_id_start_date_{{ uuid }}">
{% trans "Started reading" %}
</label>
<input type="date" name="start_date" class="input" id="start_id_start_date_{{ uuid }}" value="{% now "Y-m-d" %}">
</div>
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends 'snippets/reading_modals/layout.html' %}
{% load i18n %}
{% load utilities %}
{% block modal-title %}
{% blocktrans trimmed with book_title=book|book_title %}
Want to Read "<em>{{ book_title }}</em>"
{% endblocktrans %}
{% endblock %}
{% block modal-form-open %}
<form name="shelve" action="{% url 'reading-status' 'want' book.id %}" method="post">
<input type="hidden" name="reading_status" value="to-read">
{% csrf_token %}
{% endblock %}

View file

@ -1,7 +1,7 @@
{% load i18n %}
<div class="columns is-mobile is-gapless">
<div class="column is-cover">
{% include 'snippets/book_cover.html' with book=result cover_class='is-w-xs is-h-xs' img_path=false %}
{% include 'snippets/book_cover.html' with book=result cover_class='is-w-xs is-h-xs' external_path=True %}
</div>
<div class="column is-10 ml-3">
@ -10,7 +10,7 @@
<a
href="{{ result.view_link|default:result.key }}"
{% if remote_result %}
rel=”noopener”
rel="noopener"
target="_blank"
{% endif %}
>{{ result.title }}</a>

View file

@ -1,48 +0,0 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% block modal-title %}
{% blocktrans with book_title=book.title %}Finish "<em>{{ book_title }}</em>"{% endblocktrans %}
{% endblock %}
{% block modal-form-open %}
<form name="finish-reading" action="{% url 'reading-status' 'finish' book.id %}" method="post">
{% endblock %}
{% block modal-body %}
<section class="modal-card-body">
{% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}">
<div class="field">
<label class="label" for="finish_id_start_date-{{ uuid }}">
{% trans "Started reading" %}
</label>
<input type="date" name="start_date" class="input" id="finish_id_start_date-{{ uuid }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
</div>
<div class="field">
<label class="label" for="id_finish_date-{{ uuid }}">
{% trans "Finished reading" %}
</label>
<input type="date" name="finish_date" class="input" id="id_finish_date-{{ uuid }}" value="{% now "Y-m-d" %}">
</div>
</section>
{% endblock %}
{% block modal-footer %}
<div class="columns">
<div class="column field">
<label for="post_status-{{ uuid }}">
<input type="checkbox" name="post-status" class="checkbox" id="post_status-{{ uuid }}" checked>
{% trans "Post to feed" %}
</label>
{% include 'snippets/privacy_select.html' %}
</div>
<div class="column has-text-right">
<button type="submit" class="button is-success">{% trans "Save" %}</button>
{% trans "Cancel" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="finish-reading" controls_uid=uuid %}
</div>
</div>
{% endblock %}
{% block modal-form-close %}</form>{% endblock %}

View file

@ -19,13 +19,13 @@
{% endif %}
</div>
{% include 'snippets/shelve_button/want_to_read_modal.html' with book=active_shelf.book controls_text="want_to_read" controls_uid=uuid %}
{% include 'snippets/reading_modals/want_to_read_modal.html' with book=active_shelf.book controls_text="want_to_read" controls_uid=uuid %}
{% include 'snippets/shelve_button/start_reading_modal.html' with book=active_shelf.book controls_text="start_reading" controls_uid=uuid %}
{% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf.book controls_text="start_reading" controls_uid=uuid %}
{% include 'snippets/shelve_button/finish_reading_modal.html' with book=active_shelf.book controls_text="finish_reading" controls_uid=uuid readthrough=readthrough %}
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book controls_text="finish_reading" controls_uid=uuid readthrough=readthrough %}
{% include 'snippets/shelve_button/progress_update_modal.html' with book=active_shelf_book.book controls_text="progress_update" controls_uid=uuid readthrough=readthrough %}
{% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf_book.book controls_text="progress_update" controls_uid=uuid readthrough=readthrough %}
{% endwith %}
{% endif %}

View file

@ -13,7 +13,9 @@
{% 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 %}
{% 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><span>{% trans "Read" %}</span>
<button type="button" class="button {{ class }}" disabled>
<span>{% trans "Read" %}</span>
</button>
{% endif %}{% elif shelf.identifier == 'read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
{% trans "Finish reading" as button_text %}

View file

@ -1,42 +0,0 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% block modal-title %}
{% blocktrans trimmed with book_title=book.title %}
Start "<em>{{ book_title }}</em>"
{% endblocktrans %}
{% endblock %}
{% block modal-form-open %}
<form name="start-reading" action="{% url 'reading-status' 'start' book.id %}" method="post">
{% endblock %}
{% block modal-body %}
<section class="modal-card-body">
{% csrf_token %}
<div class="field">
<label class="label" for="start_id_start_date-{{ uuid }}">
{% trans "Started reading" %}
</label>
<input type="date" name="start_date" class="input" id="start_id_start_date-{{ uuid }}" value="{% now "Y-m-d" %}">
</div>
</section>
{% endblock %}
{% block modal-footer %}
<div class="columns">
<div class="column field">
<label for="post_status_start-{{ uuid }}">
<input type="checkbox" name="post-status" class="checkbox" id="post_status_start-{{ uuid }}" checked>
{% trans "Post to feed" %}
</label>
{% include 'snippets/privacy_select.html' %}
</div>
<div class="column has-text-right">
<button class="button is-success" type="submit">{% trans "Save" %}</button>
{% trans "Cancel" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="start-reading" controls_uid=uuid %}
</div>
</div>
{% endblock %}
{% block modal-form-close %}</form>{% endblock %}

View file

@ -1,33 +0,0 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% block modal-title %}
{% blocktrans with book_title=book.title %}Want to Read "<em>{{ book_title }}</em>"{% endblocktrans %}
{% endblock %}
{% block modal-form-open %}
<form name="shelve" action="{% url 'reading-status' 'want' book.id %}" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<input type="hidden" name="shelf" value="to-read">
{% endblock %}
{% block modal-footer %}
<div class="columns">
<div class="column field">
<label for="post_status_want-{{ uuid }}">
<input type="checkbox" name="post-status" class="checkbox" id="post_status_want-{{ uuid }}" checked>
{% trans "Post to feed" %}
</label>
{% include 'snippets/privacy_select.html' %}
</div>
<div class="column">
<button class="button is-success" type="submit">
<span>{% trans "Want to read" %}</span>
</button>
{% trans "Cancel" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="want-to-read" controls_uid=uuid %}
</div>
</div>
{% endblock %}
{% block modal-form-close %}</form>{% endblock %}

View file

@ -19,7 +19,7 @@
<div class="column is-cover">
<div class="columns is-mobile is-gapless">
<div class="column is-cover">
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book cover_class='is-w-s-mobile is-h-l-tablet' %}</a>
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book cover_class='is-w-s-mobile is-h-l-tablet' size_mobile='medium' size='large' %}</a>
{% include 'snippets/stars.html' with rating=book|rating:request.user %}

View file

@ -8,7 +8,7 @@
{% with book=status.book|default:status.mention_books.first %}
<div class="columns is-mobile is-gapless">
<a class="column is-cover is-narrow" href="{{ book.local_path }}">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-xs is-h-s-tablet' %}
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-xs is-h-s-tablet' size='small' size_mobile='xsmall' %}
</a>
<div class="column ml-3">

View file

@ -1,2 +1,2 @@
{% load i18n %}{% load utilities %}
{% blocktrans with book_path=book.local_path book=status.book|book_title %}commented on <a href="{{ book_path }}">{{ book }}</a>{% endblocktrans %}
{% blocktrans with book_path=status.book.local_path book=status.book|book_title %}commented on <a href="{{ book_path }}">{{ book }}</a>{% endblocktrans %}

View file

@ -1,7 +1,8 @@
{% spaceless %}
{% load i18n %}{% load utilities %}
{% load i18n %}
{% load utilities %}
{% load status_display %}
{% with book=status.mention_books.first %}
{% load_book status as book %}
{% blocktrans with book_path=book.remote_id book=book|book_title %}finished reading <a href="{{ book_path }}">{{ book }}</a>{% endblocktrans %}
{% endwith %}
{% endspaceless %}

View file

@ -1,9 +1,8 @@
{% spaceless %}
{% load i18n %}
{% load utilities %}
{% load status_display %}
{% with book=status.mention_books.first %}
{% load_book status as book %}
{% blocktrans with book_path=book.remote_id book=book|book_title %}started reading <a href="{{ book_path }}">{{ book }}</a>{% endblocktrans %}
{% endwith %}
{% endspaceless %}

View file

@ -1,8 +1,8 @@
{% spaceless %}
{% load i18n %}
{% load utilities %}
{% load status_display %}
{% with book=status.mention_books.first %}
{% load_book status as book %}
{% blocktrans with book_path=book.remote_id book=book|book_title %}<a href="{{ user_path }}">{{ username }}</a> wants to read <a href="{{ book_path }}">{{ book }}</a>{% endblocktrans %}
{% endwith %}
{% endspaceless %}

View file

@ -25,7 +25,7 @@
<div class="content">
<div
dir="auto"
{% if itemprop %}itemprop="{{ itemprop }}{% endif %}"
{% if itemprop %}itemprop="{{ itemprop }}"{% endif %}
>
{{ full }}
</div>
@ -41,7 +41,7 @@
<div class="content">
<div
dir="auto"
{% if itemprop %}itemprop="{{ itemprop }}{% endif %}"
{% if itemprop %}itemprop="{{ itemprop }}"{% endif %}
>
{{ full }}
</div>

2
bookwyrm/templates/user/layout.html Normal file → Executable file
View file

@ -31,8 +31,8 @@
{% spaceless %}
<div class="column box has-background-white-bis content preserve-whitespace">
{{ user.summary|to_markdown|safe }}
{% endspaceless %}
</div>
{% endspaceless %}
{% endif %}
</div>
{% if not is_self and request.user.is_authenticated %}

0
bookwyrm/templates/user/lists.html Normal file → Executable file
View file

View file

@ -1,4 +1,4 @@
{% extends 'user/layout.html' %}
{% extends 'layout.html' %}
{% load bookwyrm_tags %}
{% load utilities %}
{% load humanize %}
@ -8,15 +8,17 @@
{% include 'user/shelf/books_header.html' %}
{% endblock %}
{% block header %}
<header class="columns">
{% block opengraph_images %}
{% include 'snippets/opengraph_images.html' with image=user.preview_image %}
{% endblock %}
{% block content %}
<header class="block">
<h1 class="title">
{% include 'user/shelf/books_header.html' %}
</h1>
</header>
{% endblock %}
{% block tabs %}
<div class="block columns">
<div class="column">
<div class="tabs">
@ -41,9 +43,7 @@
</div>
{% endif %}
</div>
{% endblock %}
{% block panel %}
<div class="block">
{% include 'user/shelf/create_shelf_form.html' with controls_text='create_shelf_form' %}
</div>
@ -94,7 +94,7 @@
{% spaceless %}
<tr class="book-preview">
<td class="book-preview-top-row">
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book cover_class='is-w-s-tablet is-h-s' %}</a>
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book cover_class='is-w-s-tablet is-h-s' size='small' %}</a>
</td>
<td data-title="{% trans "Title" %}">
<a href="{{ book.local_path }}">{{ book.title }}</a>

4
bookwyrm/templates/user/user.html Normal file → Executable file
View file

@ -35,7 +35,7 @@
{% for book in shelf.books %}
<div class="control">
<a href="{{ book.local_path }}">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m is-h-s-mobile' %}
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m is-h-s-mobile' size_mobile='small' size='medium' %}
</a>
</div>
{% endfor %}
@ -71,7 +71,7 @@
{% endfor %}
{% if not activities %}
<div class="block">
<p>{% trans "No activities yet!" %}</a>
<p>{% trans "No activities yet!" %}</p>
</div>
{% endif %}

0
bookwyrm/templates/user/user_preview.html Normal file → Executable file
View file

View file

@ -2,23 +2,30 @@
{% load utilities %}
{% if widget.is_initial %}
<p class="mb-1">
{{ widget.initial_text }}:
<a href="{{ widget.value.url }}">{{ widget.value|truncatepath:10 }}</a>
</p>
{% if not widget.required %}
<p class="mb-1">
<label class="has-text-weight-normal">
<p class="mb-1">
{{ widget.initial_text }}:
<a href="{{ widget.value.url }}">{{ widget.value|truncatepath:10 }}</a>
</p>
{% if not widget.required %}
<p class="mb-1">
<label class="has-text-weight-normal">
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"{% if widget.attrs.disabled %} disabled{% endif %}>
{{ widget.clear_checkbox_label }}
</label>{% endif %}
</p>
<p class="mb-1">
{{ widget.input_text }}:
{% else %}
<p class="mb-1">
</label>
</p>
{% endif %}
{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
<span class="help file-cta is-hidden file-too-big">{% trans "File exceeds maximum size: 10MB" %}</span>
<p class="mb-1">
{% if widget.is_initial %}
{{ widget.input_text }}:
{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
<span class="help file-cta is-hidden file-too-big">
{% trans "File exceeds maximum size: 10MB" %}
</span>
</p>

View file

@ -16,3 +16,9 @@ def get_user_liked(user, status):
def get_user_boosted(user, status):
"""did the given user fav a status?"""
return status.boosters.filter(user=user).exists()
@register.filter(name="saved")
def get_user_saved_lists(user, book_list):
"""did the user save a list"""
return user.saved_lists.filter(id=book_list.id).exists()

View file

@ -70,7 +70,13 @@ def get_header_template(status):
"""get the path for the status template"""
if isinstance(status, models.Boost):
status = status.boosted_status
filename = "snippets/status/headers/{:s}.html".format(status.status_type.lower())
try:
header_type = status.reading_status.replace("-", "_")
if not header_type:
raise AttributeError()
except AttributeError:
header_type = status.status_type.lower()
filename = f"snippets/status/headers/{header_type}.html"
header_template = select_template([filename, "snippets/status/headers/note.html"])
return header_template.render({"status": status})

View file

@ -3,6 +3,7 @@ import os
from uuid import uuid4
from django import template
from django.utils.translation import gettext_lazy as _
from django.templatetags.static import static
register = template.Library()
@ -50,3 +51,15 @@ def truncatepath(value, arg):
except ValueError: # invalid literal for int()
return path_list[-1] # Fail silently.
return "%s/…%s" % (path_list[0], path_list[-1][-length:])
@register.simple_tag(takes_context=False)
def get_book_cover_thumbnail(book, size="medium", ext="jpg"):
"""Returns a book thumbnail at the specified size and extension, with fallback if needed"""
if size == "":
size = "medium"
try:
cover_thumbnail = getattr(book, "cover_bw_book_%s_%s" % (size, ext))
return cover_thumbnail.url
except OSError:
return static("images/no_cover.jpg")

View file

@ -25,3 +25,4 @@ class Person(TestCase):
self.assertEqual(user.username, "mouse@example.com")
self.assertEqual(user.remote_id, "https://example.com/user/mouse")
self.assertFalse(user.local)
self.assertEqual(user.followers_url, "https://example.com/user/mouse/followers")

View file

@ -146,6 +146,15 @@ class ModelFields(TestCase):
def test_privacy_field_set_field_from_activity(self, _):
"""translate between to/cc fields and privacy"""
with patch("bookwyrm.models.user.set_remote_server.delay"):
test_user = User.objects.create_user(
username="test_user@example.com",
local=False,
remote_id="https://example.com/test_user",
inbox="https://example.com/users/test_user/inbox",
followers_url="https://example.com/users/test_user/followers",
)
@dataclass(init=False)
class TestActivity(ActivityObject):
"""real simple mock"""
@ -154,6 +163,7 @@ class ModelFields(TestCase):
cc: List[str]
id: str = "http://hi.com"
type: str = "Test"
attributedTo: str = test_user.remote_id
class TestPrivacyModel(ActivitypubMixin, BookWyrmModel):
"""real simple mock model because BookWyrmModel is abstract"""
@ -185,6 +195,16 @@ class ModelFields(TestCase):
instance.set_field_from_activity(model_instance, data)
self.assertEqual(model_instance.privacy_field, "unlisted")
data.to = [test_user.followers_url]
data.cc = []
instance.set_field_from_activity(model_instance, data)
self.assertEqual(model_instance.privacy_field, "followers")
data.to = ["http://user_remote/followers"]
data.cc = ["http://mentioned_user/remote_id"]
instance.set_field_from_activity(model_instance, data)
self.assertEqual(model_instance.privacy_field, "followers")
@patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast")
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_privacy_field_set_activity_from_field(self, *_):

View file

@ -1,7 +1,9 @@
""" testing models """
import datetime
from unittest.mock import patch
from django.test import TestCase
from django.core.exceptions import ValidationError
from django.utils import timezone
from bookwyrm import models
@ -21,27 +23,79 @@ class ReadThrough(TestCase):
title="Example Edition", parent_work=self.work
)
self.readthrough = models.ReadThrough.objects.create(
user=self.user, book=self.edition
def test_valid_date(self):
"""can't finish a book before you start it"""
start = timezone.now()
finish = start + datetime.timedelta(days=1)
# just make sure there's no errors
models.ReadThrough.objects.create(
user=self.user,
book=self.edition,
start_date=start,
finish_date=finish,
)
def test_valid_date_null_start(self):
"""can't finish a book before you start it"""
start = timezone.now()
finish = start + datetime.timedelta(days=1)
# just make sure there's no errors
models.ReadThrough.objects.create(
user=self.user,
book=self.edition,
finish_date=finish,
)
def test_valid_date_null_finish(self):
"""can't finish a book before you start it"""
start = timezone.now()
# just make sure there's no errors
models.ReadThrough.objects.create(
user=self.user,
book=self.edition,
start_date=start,
)
def test_valid_date_null(self):
"""can't finish a book before you start it"""
# just make sure there's no errors
models.ReadThrough.objects.create(
user=self.user,
book=self.edition,
)
def test_valid_date_same(self):
"""can't finish a book before you start it"""
start = timezone.now()
# just make sure there's no errors
models.ReadThrough.objects.create(
user=self.user,
book=self.edition,
start_date=start,
finish_date=start,
)
def test_progress_update(self):
"""Test progress updates"""
self.readthrough.create_update() # No-op, no progress yet
self.readthrough.progress = 10
self.readthrough.create_update()
self.readthrough.progress = 20
self.readthrough.progress_mode = models.ProgressMode.PERCENT
self.readthrough.create_update()
readthrough = models.ReadThrough.objects.create(
user=self.user, book=self.edition
)
updates = self.readthrough.progressupdate_set.order_by("created_date").all()
readthrough.create_update() # No-op, no progress yet
readthrough.progress = 10
readthrough.create_update()
readthrough.progress = 20
readthrough.progress_mode = models.ProgressMode.PERCENT
readthrough.create_update()
updates = readthrough.progressupdate_set.order_by("created_date").all()
self.assertEqual(len(updates), 2)
self.assertEqual(updates[0].progress, 10)
self.assertEqual(updates[0].mode, models.ProgressMode.PAGE)
self.assertEqual(updates[1].progress, 20)
self.assertEqual(updates[1].mode, models.ProgressMode.PERCENT)
self.readthrough.progress = -10
self.assertRaises(ValidationError, self.readthrough.clean_fields)
update = self.readthrough.create_update()
readthrough.progress = -10
self.assertRaises(ValidationError, readthrough.clean_fields)
update = readthrough.create_update()
self.assertRaises(ValidationError, update.clean_fields)

View file

@ -5,11 +5,13 @@ from django.test import TestCase
import responses
from bookwyrm import models
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import USE_HTTPS, DOMAIN
# pylint: disable=missing-class-docstring
# pylint: disable=missing-function-docstring
class User(TestCase):
protocol = "https://" if USE_HTTPS else "http://"
def setUp(self):
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.user = models.User.objects.create_user(
@ -24,13 +26,14 @@ class User(TestCase):
def test_computed_fields(self):
"""username instead of id here"""
expected_id = "https://%s/user/mouse" % DOMAIN
expected_id = f"{self.protocol}{DOMAIN}/user/mouse"
self.assertEqual(self.user.remote_id, expected_id)
self.assertEqual(self.user.username, "mouse@%s" % DOMAIN)
self.assertEqual(self.user.username, f"mouse@{DOMAIN}")
self.assertEqual(self.user.localname, "mouse")
self.assertEqual(self.user.shared_inbox, "https://%s/inbox" % DOMAIN)
self.assertEqual(self.user.inbox, "%s/inbox" % expected_id)
self.assertEqual(self.user.outbox, "%s/outbox" % expected_id)
self.assertEqual(self.user.shared_inbox, f"{self.protocol}{DOMAIN}/inbox")
self.assertEqual(self.user.inbox, f"{expected_id}/inbox")
self.assertEqual(self.user.outbox, f"{expected_id}/outbox")
self.assertEqual(self.user.followers_url, f"{expected_id}/followers")
self.assertIsNotNone(self.user.key_pair.private_key)
self.assertIsNotNone(self.user.key_pair.public_key)

View file

@ -8,6 +8,7 @@ from bookwyrm import activitystreams, models
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
@patch("bookwyrm.activitystreams.BooksStream.add_book_statuses")
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
# pylint: disable=too-many-public-methods
class Activitystreams(TestCase):
"""using redis to build activity streams"""
@ -286,3 +287,76 @@ class Activitystreams(TestCase):
# yes book, yes audience
result = activitystreams.BooksStream().get_statuses_for_user(self.local_user)
self.assertEqual(list(result), [status])
@patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores")
@patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores")
def test_boost_to_another_timeline(self, *_):
"""add a boost and deduplicate the boosted status on the timeline"""
status = models.Status.objects.create(user=self.local_user, content="hi")
with patch(
"bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores"
):
boost = models.Boost.objects.create(
boosted_status=status,
user=self.another_user,
)
with patch(
"bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores"
) as mock:
activitystreams.add_status_on_create(models.Boost, boost, True)
self.assertTrue(mock.called)
call_args = mock.call_args
self.assertEqual(call_args[0][0], status)
self.assertEqual(
call_args[1]["stores"], ["{:d}-home".format(self.another_user.id)]
)
@patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores")
@patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores")
def test_boost_to_following_timeline(self, *_):
"""add a boost and deduplicate the boosted status on the timeline"""
self.local_user.following.add(self.another_user)
status = models.Status.objects.create(user=self.local_user, content="hi")
with patch(
"bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores"
):
boost = models.Boost.objects.create(
boosted_status=status,
user=self.another_user,
)
with patch(
"bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores"
) as mock:
activitystreams.add_status_on_create(models.Boost, boost, True)
self.assertTrue(mock.called)
call_args = mock.call_args
self.assertEqual(call_args[0][0], status)
self.assertTrue(
"{:d}-home".format(self.another_user.id) in call_args[1]["stores"]
)
self.assertTrue(
"{:d}-home".format(self.local_user.id) in call_args[1]["stores"]
)
@patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores")
@patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores")
def test_boost_to_same_timeline(self, *_):
"""add a boost and deduplicate the boosted status on the timeline"""
status = models.Status.objects.create(user=self.local_user, content="hi")
with patch(
"bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores"
):
boost = models.Boost.objects.create(
boosted_status=status,
user=self.local_user,
)
with patch(
"bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores"
) as mock:
activitystreams.add_status_on_create(models.Boost, boost, True)
self.assertTrue(mock.called)
call_args = mock.call_args
self.assertEqual(call_args[0][0], status)
self.assertEqual(
call_args[1]["stores"], ["{:d}-home".format(self.local_user.id)]
)

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