Merge branch 'main' into readthrough-dates

This commit is contained in:
Mouse Reeve 2021-08-27 10:25:41 -07:00
commit 01d82cfa3a
123 changed files with 3139 additions and 1768 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)

View file

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

@ -23,11 +23,12 @@ 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)
if increment_unread:
for user in self.get_audience(status):
# add to the unread status count
pipeline.incr(self.unread_id(user))
@ -262,7 +263,7 @@ def add_status_on_create(sender, instance, created, *args, **kwargs):
return
for stream in streams.values():
stream.add_status(instance)
stream.add_status(instance, increment_unread=created)
if sender != models.Boost:
return
@ -273,9 +274,10 @@ def add_status_on_create(sender, instance, created, *args, **kwargs):
created_date__lt=instance.created_date,
)
for stream in streams.values():
stream.remove_object_from_related_stores(boosted)
audience = stream.get_stores_for_object(instance)
stream.remove_object_from_related_stores(boosted, stores=audience)
for status in old_versions:
stream.remove_object_from_related_stores(status)
stream.remove_object_from_related_stores(status, stores=audience)
@receiver(signals.post_delete, sender=models.Boost)

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

@ -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,7 +217,10 @@ 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
@ -273,8 +283,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 +390,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

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

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

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,13 +1,10 @@
/** @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');
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;
@ -37,9 +34,6 @@
.icon-graphic-banknote:before {
content: "\e920";
}
.icon-stars:before {
content: "\e91a";
}
.icon-warning:before {
content: "\e91b";
}
@ -47,7 +41,7 @@
content: "\e900";
}
.icon-bookmark:before {
content: "\e91c";
content: "\e91a";
}
.icon-rss:before {
content: "\e91d";

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>
@ -109,7 +109,7 @@
<div class="columns is-multiline is-mobile">
{% for book in books %}
<div class="column is-one-fifth">
{% include 'discover/small-book.html' with book=book %}
{% include 'landing/small-book.html' with book=book %}
</div>
{% endfor %}
</div>

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

View file

@ -42,11 +42,18 @@
</div>
{% endif %}
<form
class="block"
{% 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">
name="edit-book"
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
{% else %}
<form class="block" name="create-book" action="/create-book{% if confirm_mode %}/confirm{% endif %}" method="post" enctype="multipart/form-data">
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,9 +40,8 @@
{% if suggested_users %}
{# suggested users for when things are very lonely #}
{% include 'feed/suggested_users.html' with suggested_users=suggested_users %}
</div>
{% endif %}
</div>
{% endif %}
{% for activity in activities %}

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>
</dl>
{% elif task.failed %}
<div class="notification is-danger">{% trans "TASK FAILED" %}</div>
{% endif %}
</dl>
</div>
<div class="block">
@ -84,6 +84,7 @@
<button class="button is-block mt-3" type="submit">{% trans "Retry items" %}</button>
</fieldset>
</form>
<hr>
@ -103,7 +104,6 @@
{% endfor %}
</ul>
{% endif %}
</form>
</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

@ -49,7 +49,7 @@
</div>
</form>
<div role="button" tabindex="0" class="navbar-burger pulldown-menu" data-controls="main-nav" aria-expanded="false">
<div role="button" tabindex="0" class="navbar-burger pulldown-menu" data-controls="main_nav" aria-expanded="false">
<div class="navbar-item mt-3">
<div class="icon icon-dots-three-vertical" title="{% trans 'Main navigation menu' %}">
<span class="is-sr-only">{% trans "Main navigation menu" %}</span>

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>
<div class="column is-narrow is-flex">
{% 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>
{% endif %}
{% include "lists/bookmark_button.html" with list=list %}
</div>
</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

@ -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 %}
<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"
{% load imagekit %}
{% load utilities %}
{% if book.cover %}
src="{% if img_path is None %}{% get_media_prefix %}{% else %}{{ img_path }}{% endif %}{{ book.cover }}"
<picture class="cover-container {{ cover_class }}">
{% if external_path %}
<img
class="book-cover"
src="{{ book.cover }}"
itemprop="thumbnailUrl"
{% if book.alt_text %}
alt="{{ book.alt_text }}"
{% endif %}
alt="{{ book.alt_text|default:'' }}"
>
{% else %}
src="{% static "images/no_cover.jpg" %}"
alt="{% trans "No cover" %}"
{% 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 %}
<figcaption class="cover_caption">
<figure class="cover-container no-cover {{ cover_class }}">
<img
class="book-cover"
src="{% static "images/no_cover.jpg" %}"
alt="{% trans "No cover" %}"
>
<figcaption class="cover-caption">
<p>{{ book.alt_text }}</p>
</figcaption>
{% endif %}
</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

@ -3,7 +3,7 @@
{% block card-content %}
{% with status_type=status.status_type %}
{% if status_type == 'GeneratedNote' or status_type == 'Rating' or not status.content %}
{% if status_type == 'GeneratedNote' or status_type == 'Rating' %}
{% include 'snippets/status/generated_status.html' with status=status %}
{% else %}
{% include 'snippets/status/content_status.html' with status=status %}

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

@ -13,7 +13,7 @@
{% with parent_status=status|parent %}
{% if parent_status %}
{% blocktrans trimmed with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}
replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">status</a>
replied to <a href="{{ user_path }}">{{ username}}</a>'s <a href="{{ status_path }}">status</a>
{% endblocktrans %}
{% endif %}
{% endwith %}

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

@ -3,4 +3,5 @@
{% block filter_fields %}
{% include 'user_admin/server_filter.html' %}
{% include 'user_admin/username_filter.html' %}
{% include 'directory/community_filter.html' %}
{% endblock %}

View file

@ -6,19 +6,26 @@
{{ 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 %}
</label>
</p>
<p class="mb-1">
{{ widget.input_text }}:
{% else %}
<p class="mb-1">
{% endif %}
{% endif %}
<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>
<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

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

View file

@ -77,6 +77,33 @@ class InboxCreate(TestCase):
views.inbox.activity_task(activity)
self.assertEqual(models.Status.objects.count(), 1)
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_create_comment_with_reading_status(self, *_):
"""the "it justs works" mode"""
datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_comment.json")
status_data = json.loads(datafile.read_bytes())
status_data["readingStatus"] = "to-read"
models.Edition.objects.create(
title="Test Book", remote_id="https://example.com/book/1"
)
activity = self.create_json
activity["object"] = status_data
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
status = models.Comment.objects.get()
self.assertEqual(status.remote_id, "https://example.com/user/mouse/comment/6")
self.assertEqual(status.content, "commentary")
self.assertEqual(status.reading_status, "to-read")
self.assertEqual(status.user, self.local_user)
# while we're here, lets ensure we avoid dupes
views.inbox.activity_task(activity)
self.assertEqual(models.Status.objects.count(), 1)
def test_create_status_remote_note_with_mention(self, _):
"""should only create it under the right circumstances"""
self.assertFalse(

View file

@ -280,49 +280,6 @@ class BookViews(TestCase):
self.assertEqual(book.authors.first().name, "Sappho")
self.assertEqual(book.authors.first(), book.parent_work.authors.first())
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
def test_switch_edition(self, _):
"""updates user's relationships to a book"""
work = models.Work.objects.create(title="test work")
edition1 = models.Edition.objects.create(title="first ed", parent_work=work)
edition2 = models.Edition.objects.create(title="second ed", parent_work=work)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
shelf = models.Shelf.objects.create(name="Test Shelf", user=self.local_user)
models.ShelfBook.objects.create(
book=edition1,
user=self.local_user,
shelf=shelf,
)
models.ReadThrough.objects.create(user=self.local_user, book=edition1)
self.assertEqual(models.ShelfBook.objects.get().book, edition1)
self.assertEqual(models.ReadThrough.objects.get().book, edition1)
request = self.factory.post("", {"edition": edition2.id})
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.switch_edition(request)
self.assertEqual(models.ShelfBook.objects.get().book, edition2)
self.assertEqual(models.ReadThrough.objects.get().book, edition2)
def test_editions_page(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.Editions.as_view()
request = self.factory.get("")
with patch("bookwyrm.views.books.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.work.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
request = self.factory.get("")
with patch("bookwyrm.views.books.is_api_request") as is_api:
is_api.return_value = True
result = view(request, self.work.id)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_upload_cover_file(self):
"""add a cover via file upload"""
self.assertFalse(self.book.cover)

View file

@ -0,0 +1,126 @@
""" test for app action functionality """
from unittest.mock import patch
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.activitypub import ActivitypubResponse
class BookViews(TestCase):
"""books books books"""
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
remote_id="https://example.com/users/mouse",
)
self.work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
parent_work=self.work,
physical_format="paperback",
)
models.SiteSettings.objects.create()
def test_editions_page(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.Editions.as_view()
request = self.factory.get("")
with patch("bookwyrm.views.editions.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.work.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
self.assertTrue("paperback" in result.context_data["formats"])
def test_editions_page_filtered(self):
"""editions view with filters"""
models.Edition.objects.create(
title="Fish",
physical_format="okay",
parent_work=self.work,
)
view = views.Editions.as_view()
request = self.factory.get("")
with patch("bookwyrm.views.editions.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.work.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 2)
self.assertEqual(len(result.context_data["formats"]), 2)
self.assertTrue("paperback" in result.context_data["formats"])
self.assertTrue("okay" in result.context_data["formats"])
request = self.factory.get("", {"q": "fish"})
with patch("bookwyrm.views.editions.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.work.id)
result.render()
self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 1)
request = self.factory.get("", {"q": "okay"})
with patch("bookwyrm.views.editions.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.work.id)
result.render()
self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 1)
request = self.factory.get("", {"format": "okay"})
with patch("bookwyrm.views.editions.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.work.id)
result.render()
self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 1)
def test_editions_page_api(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.Editions.as_view()
request = self.factory.get("")
with patch("bookwyrm.views.editions.is_api_request") as is_api:
is_api.return_value = True
result = view(request, self.work.id)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
def test_switch_edition(self, _):
"""updates user's relationships to a book"""
work = models.Work.objects.create(title="test work")
edition1 = models.Edition.objects.create(title="first ed", parent_work=work)
edition2 = models.Edition.objects.create(title="second ed", parent_work=work)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
shelf = models.Shelf.objects.create(name="Test Shelf", user=self.local_user)
models.ShelfBook.objects.create(
book=edition1,
user=self.local_user,
shelf=shelf,
)
models.ReadThrough.objects.create(user=self.local_user, book=edition1)
self.assertEqual(models.ShelfBook.objects.get().book, edition1)
self.assertEqual(models.ReadThrough.objects.get().book, edition1)
request = self.factory.post("", {"edition": edition2.id})
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.switch_edition(request)
self.assertEqual(models.ShelfBook.objects.get().book, edition2)
self.assertEqual(models.ReadThrough.objects.get().book, edition2)

View file

@ -91,6 +91,52 @@ class ListViews(TestCase):
result.render()
self.assertEqual(result.status_code, 200)
def test_saved_lists_page(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.SavedLists.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
booklist = models.List.objects.create(
name="Public list", user=self.local_user
)
models.List.objects.create(
name="Private list", privacy="direct", user=self.local_user
)
self.local_user.saved_lists.add(booklist)
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
self.assertEqual(result.context_data["lists"].object_list, [booklist])
def test_saved_lists_page_empty(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.SavedLists.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.List.objects.create(name="Public list", user=self.local_user)
models.List.objects.create(
name="Private list", privacy="direct", user=self.local_user
)
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["lists"].object_list), 0)
def test_saved_lists_page_logged_out(self):
"""logged out saved lists"""
view = views.SavedLists.as_view()
request = self.factory.get("")
request.user = self.anonymous_user
result = view(request)
self.assertEqual(result.status_code, 302)
def test_lists_create(self):
"""create list view"""
view = views.Lists.as_view()
@ -328,15 +374,8 @@ class ListViews(TestCase):
def test_user_lists_page_logged_out(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.UserLists.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.List.objects.create(name="Public list", user=self.local_user)
models.List.objects.create(
name="Private list", privacy="direct", user=self.local_user
)
request = self.factory.get("")
request.user = self.anonymous_user
result = view(request, self.local_user.username)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
self.assertEqual(result.status_code, 302)

View file

@ -1,5 +1,4 @@
""" testing import """
from unittest.mock import patch
from django.test import RequestFactory, TestCase
@ -7,59 +6,77 @@ from bookwyrm import models
from bookwyrm.views import rss_feed
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
@patch("bookwyrm.activitystreams.ActivityStream.get_activity_stream")
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
class RssFeedView(TestCase):
"""rss feed behaves as expected"""
def setUp(self):
"""test data"""
self.site = models.SiteSettings.objects.create()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.user = models.User.objects.create_user(
self.local_user = models.User.objects.create_user(
"rss_user", "rss@test.rss", "password", local=True
)
work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
parent_work=work,
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
self.review = models.Review.objects.create(
name="Review name",
content="test content",
rating=3,
user=self.user,
book=self.book,
)
self.quote = models.Quotation.objects.create(
quote="a sickening sense",
content="test content",
user=self.user,
book=self.book,
)
self.generatednote = models.GeneratedNote.objects.create(
content="test content", user=self.user
)
self.factory = RequestFactory()
@patch("bookwyrm.activitystreams.ActivityStream.get_activity_stream")
def test_rss_feed(self, _):
models.SiteSettings.objects.create()
def test_rss_empty(self, *_):
"""load an rss feed"""
view = rss_feed.RssFeed()
request = self.factory.get("/user/rss_user/rss")
request.user = self.user
with patch("bookwyrm.models.SiteSettings.objects.get") as site:
site.return_value = self.site
result = view(request, username=self.user.username)
request.user = self.local_user
result = view(request, username=self.local_user.username)
self.assertEqual(result.status_code, 200)
self.assertIn(b"Status updates from rss_user", result.content)
def test_rss_comment(self, *_):
"""load an rss feed"""
models.Comment.objects.create(
content="comment test content",
user=self.local_user,
book=self.book,
)
view = rss_feed.RssFeed()
request = self.factory.get("/user/rss_user/rss")
request.user = self.local_user
result = view(request, username=self.local_user.username)
self.assertEqual(result.status_code, 200)
self.assertIn(b"Example Edition", result.content)
def test_rss_review(self, *_):
"""load an rss feed"""
models.Review.objects.create(
name="Review name",
content="test content",
rating=3,
user=self.local_user,
book=self.book,
)
view = rss_feed.RssFeed()
request = self.factory.get("/user/rss_user/rss")
request.user = self.local_user
result = view(request, username=self.local_user.username)
self.assertEqual(result.status_code, 200)
def test_rss_quotation(self, *_):
"""load an rss feed"""
models.Quotation.objects.create(
quote="a sickening sense",
content="test content",
user=self.local_user,
book=self.book,
)
view = rss_feed.RssFeed()
request = self.factory.get("/user/rss_user/rss")
request.user = self.local_user
result = view(request, username=self.local_user.username)
self.assertEqual(result.status_code, 200)
self.assertIn(b"Status updates from rss_user", result.content)
self.assertIn(b"a sickening sense", result.content)
self.assertIn(b"Example Edition", result.content)

View file

@ -218,6 +218,7 @@ urlpatterns = [
# lists
re_path(r"%s/lists/?$" % USER_PATH, views.UserLists.as_view(), name="user-lists"),
re_path(r"^list/?$", views.Lists.as_view(), name="lists"),
re_path(r"^list/saved/?$", views.SavedLists.as_view(), name="saved-lists"),
re_path(r"^list/(?P<list_id>\d+)(.json)?/?$", views.List.as_view(), name="list"),
re_path(r"^list/add-book/?$", views.list.add_book, name="list-add-book"),
re_path(
@ -233,6 +234,8 @@ urlpatterns = [
re_path(
r"^list/(?P<list_id>\d+)/curate/?$", views.Curate.as_view(), name="list-curate"
),
re_path(r"^save-list/(?P<list_id>\d+)/?$", views.save_list, name="list-save"),
re_path(r"^unsave-list/(?P<list_id>\d+)/?$", views.unsave_list, name="list-unsave"),
# User books
re_path(r"%s/books/?$" % USER_PATH, views.Shelf.as_view(), name="user-shelves"),
re_path(
@ -294,8 +297,10 @@ urlpatterns = [
name="redraft",
),
# interact
re_path(r"^favorite/(?P<status_id>\d+)/?$", views.Favorite.as_view()),
re_path(r"^unfavorite/(?P<status_id>\d+)/?$", views.Unfavorite.as_view()),
re_path(r"^favorite/(?P<status_id>\d+)/?$", views.Favorite.as_view(), name="fav"),
re_path(
r"^unfavorite/(?P<status_id>\d+)/?$", views.Unfavorite.as_view(), name="unfav"
),
re_path(r"^boost/(?P<status_id>\d+)/?$", views.Boost.as_view()),
re_path(r"^unboost/(?P<status_id>\d+)/?$", views.Unboost.as_view()),
# books

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