Merge branch 'main' into suggestions-redis

This commit is contained in:
Mouse Reeve 2021-06-18 16:48:04 -07:00
commit 5b6048e4c6
222 changed files with 5284 additions and 3547 deletions

View file

@ -13,16 +13,10 @@ DEFAULT_LANGUAGE="English"
## Leave unset to allow all hosts ## Leave unset to allow all hosts
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]" # ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
OL_URL=https://openlibrary.org
## Database backend to use.
## Default is postgres, sqlite is for dev quickstart only (NOT production!!!)
BOOKWYRM_DATABASE_BACKEND=postgres
MEDIA_ROOT=images/ MEDIA_ROOT=images/
POSTGRES_PORT=5432 POSTGRES_PORT=5432
POSTGRES_PASSWORD=fedireads POSTGRES_PASSWORD=securedbypassword123
POSTGRES_USER=fedireads POSTGRES_USER=fedireads
POSTGRES_DB=fedireads POSTGRES_DB=fedireads
POSTGRES_HOST=db POSTGRES_HOST=db
@ -34,10 +28,8 @@ REDIS_ACTIVITY_PORT=6379
#REDIS_ACTIVITY_PASSWORD=redispassword345 #REDIS_ACTIVITY_PASSWORD=redispassword345
# Redis as celery broker # Redis as celery broker
#REDIS_BROKER_PORT=6379 REDIS_BROKER_PORT=6379
#REDIS_BROKER_PASSWORD=redispassword123 #REDIS_BROKER_PASSWORD=redispassword123
CELERY_BROKER=redis://redis_broker:6379/0
CELERY_RESULT_BACKEND=redis://redis_broker:6379/0
FLOWER_PORT=8888 FLOWER_PORT=8888
#FLOWER_USER=mouse #FLOWER_USER=mouse
@ -50,5 +42,14 @@ EMAIL_HOST_PASSWORD=emailpassword123
EMAIL_USE_TLS=true EMAIL_USE_TLS=true
EMAIL_USE_SSL=false EMAIL_USE_SSL=false
# Set this to true when initializing certbot for domain, false when not # Preview image generation can be computing and storage intensive
CERTBOT_INIT=false # ENABLE_PREVIEW_IMAGES=True
# Specify RGB tuple or RGB hex strings,
# or use_dominant_color_light / use_dominant_color_dark
PREVIEW_BG_COLOR=use_dominant_color_light
# Change to #FFF if you use use_dominant_color_dark
PREVIEW_TEXT_COLOR="#363636"
PREVIEW_IMG_WIDTH=1200
PREVIEW_IMG_HEIGHT=630
PREVIEW_DEFAULT_COVER_COLOR="#002549"

View file

@ -13,16 +13,10 @@ DEFAULT_LANGUAGE="English"
## Leave unset to allow all hosts ## Leave unset to allow all hosts
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]" # ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
OL_URL=https://openlibrary.org
## Database backend to use.
## Default is postgres, sqlite is for dev quickstart only (NOT production!!!)
BOOKWYRM_DATABASE_BACKEND=postgres
MEDIA_ROOT=images/ MEDIA_ROOT=images/
POSTGRES_PORT=5432 POSTGRES_PORT=5432
POSTGRES_PASSWORD=securedbpassword123 POSTGRES_PASSWORD=securedbypassword123
POSTGRES_USER=fedireads POSTGRES_USER=fedireads
POSTGRES_DB=fedireads POSTGRES_DB=fedireads
POSTGRES_HOST=db POSTGRES_HOST=db
@ -36,8 +30,6 @@ REDIS_ACTIVITY_PASSWORD=redispassword345
# Redis as celery broker # Redis as celery broker
REDIS_BROKER_PORT=6379 REDIS_BROKER_PORT=6379
REDIS_BROKER_PASSWORD=redispassword123 REDIS_BROKER_PASSWORD=redispassword123
CELERY_BROKER=redis://:${REDIS_BROKER_PASSWORD}@redis_broker:${REDIS_BROKER_PORT}/0
CELERY_RESULT_BACKEND=redis://:${REDIS_BROKER_PASSWORD}@redis_broker:${REDIS_BROKER_PORT}/0
FLOWER_PORT=8888 FLOWER_PORT=8888
FLOWER_USER=mouse FLOWER_USER=mouse
@ -50,5 +42,14 @@ EMAIL_HOST_PASSWORD=emailpassword123
EMAIL_USE_TLS=true EMAIL_USE_TLS=true
EMAIL_USE_SSL=false EMAIL_USE_SSL=false
# Set this to true when initializing certbot for domain, false when not # Preview image generation can be computing and storage intensive
CERTBOT_INIT=false # ENABLE_PREVIEW_IMAGES=True
# Specify RGB tuple or RGB hex strings,
# or use_dominant_color_light / use_dominant_color_dark
PREVIEW_BG_COLOR=use_dominant_color_light
# Change to #FFF if you use use_dominant_color_dark
PREVIEW_TEXT_COLOR="#363636"
PREVIEW_IMG_WIDTH=1200
PREVIEW_IMG_HEIGHT=630
PREVIEW_DEFAULT_COVER_COLOR="#002549"

View file

@ -1,4 +1,4 @@
name: Lint Python name: Python Formatting (run ./bw-dev black to fix)
on: [push, pull_request] on: [push, pull_request]

View file

@ -50,7 +50,6 @@ jobs:
SECRET_KEY: beepbeep SECRET_KEY: beepbeep
DEBUG: true DEBUG: true
DOMAIN: your.domain.here DOMAIN: your.domain.here
OL_URL: https://openlibrary.org
BOOKWYRM_DATABASE_BACKEND: postgres BOOKWYRM_DATABASE_BACKEND: postgres
MEDIA_ROOT: images/ MEDIA_ROOT: images/
POSTGRES_PASSWORD: hunter2 POSTGRES_PASSWORD: hunter2
@ -58,11 +57,13 @@ jobs:
POSTGRES_DB: github_actions POSTGRES_DB: github_actions
POSTGRES_HOST: 127.0.0.1 POSTGRES_HOST: 127.0.0.1
CELERY_BROKER: "" CELERY_BROKER: ""
CELERY_RESULT_BACKEND: "" REDIS_BROKER_PORT: 6379
FLOWER_PORT: 8888
EMAIL_HOST: "smtp.mailgun.org" EMAIL_HOST: "smtp.mailgun.org"
EMAIL_PORT: 587 EMAIL_PORT: 587
EMAIL_HOST_USER: "" EMAIL_HOST_USER: ""
EMAIL_HOST_PASSWORD: "" EMAIL_HOST_PASSWORD: ""
EMAIL_USE_TLS: true EMAIL_USE_TLS: true
ENABLE_PREVIEW_IMAGES: true
run: | run: |
python manage.py test python manage.py test

24
.github/workflows/pylint.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: Pylint
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pylint
- name: Analysing the code with pylint
run: |
pylint bookwyrm/ --ignore=migrations,tests --disable=E1101,E1135,E1136,R0903,R0901,R0902,W0707,W0511,W0406,R0401,R0801

3
.gitignore vendored
View file

@ -27,3 +27,6 @@
#nginx #nginx
nginx/default.conf nginx/default.conf
#macOS
**/.DS_Store

View file

@ -9,5 +9,3 @@ WORKDIR /app
COPY requirements.txt /app/ COPY requirements.txt /app/
RUN pip install -r requirements.txt --no-cache-dir RUN pip install -r requirements.txt --no-cache-dir
RUN apt-get update && apt-get install -y gettext libgettextpo-dev && apt-get clean RUN apt-get update && apt-get install -y gettext libgettextpo-dev && apt-get clean
COPY ./bookwyrm ./celerywyrm /app/

View file

@ -37,6 +37,7 @@ class Mention(Link):
@dataclass @dataclass
# pylint: disable=invalid-name
class Signature: class Signature:
"""public key block""" """public key block"""
@ -56,11 +57,11 @@ def naive_parse(activity_objects, activity_json, serializer=None):
activity_type = activity_json.get("type") activity_type = activity_json.get("type")
try: try:
serializer = activity_objects[activity_type] serializer = activity_objects[activity_type]
except KeyError as e: except KeyError as err:
# we know this exists and that we can't handle it # we know this exists and that we can't handle it
if activity_type in ["Question"]: if activity_type in ["Question"]:
return None return None
raise ActivitySerializerError(e) raise ActivitySerializerError(err)
return serializer(activity_objects=activity_objects, **activity_json) return serializer(activity_objects=activity_objects, **activity_json)

View file

@ -6,6 +6,7 @@ from .base_activity import ActivityObject
from .image import Document from .image import Document
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class BookData(ActivityObject): class BookData(ActivityObject):
"""shared fields for all book data and authors""" """shared fields for all book data and authors"""
@ -18,6 +19,7 @@ class BookData(ActivityObject):
lastEditedBy: str = None lastEditedBy: str = None
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class Book(BookData): class Book(BookData):
"""serializes an edition or work, abstract""" """serializes an edition or work, abstract"""
@ -40,6 +42,7 @@ class Book(BookData):
type: str = "Book" type: str = "Book"
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class Edition(Book): class Edition(Book):
"""Edition instance of a book object""" """Edition instance of a book object"""
@ -57,6 +60,7 @@ class Edition(Book):
type: str = "Edition" type: str = "Edition"
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class Work(Book): class Work(Book):
"""work instance of a book object""" """work instance of a book object"""
@ -66,6 +70,7 @@ class Work(Book):
type: str = "Work" type: str = "Work"
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class Author(BookData): class Author(BookData):
"""author of a book""" """author of a book"""

View file

@ -19,6 +19,7 @@ class Tombstone(ActivityObject):
return model.find_existing_by_remote_id(self.id) return model.find_existing_by_remote_id(self.id)
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class Note(ActivityObject): class Note(ActivityObject):
"""Note activity""" """Note activity"""
@ -52,6 +53,7 @@ class GeneratedNote(Note):
type: str = "GeneratedNote" type: str = "GeneratedNote"
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class Comment(Note): class Comment(Note):
"""like a note but with a book""" """like a note but with a book"""

View file

@ -5,6 +5,7 @@ from typing import List
from .base_activity import ActivityObject from .base_activity import ActivityObject
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class OrderedCollection(ActivityObject): class OrderedCollection(ActivityObject):
"""structure of an ordered collection activity""" """structure of an ordered collection activity"""
@ -17,6 +18,7 @@ class OrderedCollection(ActivityObject):
type: str = "OrderedCollection" type: str = "OrderedCollection"
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class OrderedCollectionPrivate(OrderedCollection): class OrderedCollectionPrivate(OrderedCollection):
"""an ordered collection with privacy settings""" """an ordered collection with privacy settings"""
@ -41,6 +43,7 @@ class BookList(OrderedCollectionPrivate):
type: str = "BookList" type: str = "BookList"
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class OrderedCollectionPage(ActivityObject): class OrderedCollectionPage(ActivityObject):
"""structure of an ordered collection activity""" """structure of an ordered collection activity"""

View file

@ -6,6 +6,7 @@ from .base_activity import ActivityObject
from .image import Image from .image import Image
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class PublicKey(ActivityObject): class PublicKey(ActivityObject):
"""public key block""" """public key block"""
@ -15,6 +16,7 @@ class PublicKey(ActivityObject):
type: str = "PublicKey" type: str = "PublicKey"
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class Person(ActivityObject): class Person(ActivityObject):
"""actor activitypub json""" """actor activitypub json"""

View file

@ -1,3 +1,4 @@
""" ActivityPub-specific json response wrapper """
from django.http import JsonResponse from django.http import JsonResponse
from .base_activity import ActivityEncoder from .base_activity import ActivityEncoder

View file

@ -22,6 +22,7 @@ class Verb(ActivityObject):
self.object.to_model() self.object.to_model()
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class Create(Verb): class Create(Verb):
"""Create activity""" """Create activity"""
@ -32,6 +33,7 @@ class Create(Verb):
type: str = "Create" type: str = "Create"
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class Delete(Verb): class Delete(Verb):
"""Create activity""" """Create activity"""
@ -57,6 +59,7 @@ class Delete(Verb):
# if we can't find it, we don't need to delete it because we don't have it # if we can't find it, we don't need to delete it because we don't have it
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class Update(Verb): class Update(Verb):
"""Update activity""" """Update activity"""
@ -192,6 +195,7 @@ class Like(Verb):
self.to_model() self.to_model()
# pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)
class Announce(Verb): class Announce(Verb):
"""boosting a status""" """boosting a status"""

View file

@ -55,6 +55,8 @@ class ActivityStream(RedisStore):
return ( return (
models.Status.objects.select_subclasses() models.Status.objects.select_subclasses()
.filter(id__in=statuses) .filter(id__in=statuses)
.select_related("user", "reply_parent")
.prefetch_related("mention_books", "mention_users")
.order_by("-published_date") .order_by("-published_date")
) )
@ -199,6 +201,19 @@ def add_status_on_create(sender, instance, created, *args, **kwargs):
for stream in streams.values(): for stream in streams.values():
stream.add_status(instance) stream.add_status(instance)
if sender != models.Boost:
return
# remove the original post and other, earlier boosts
boosted = instance.boost.boosted_status
old_versions = models.Boost.objects.filter(
boosted_status__id=boosted.id,
created_date__lt=instance.created_date,
)
for stream in streams.values():
stream.remove_object_from_related_stores(boosted)
for status in old_versions:
stream.remove_object_from_related_stores(status)
@receiver(signals.post_delete, sender=models.Boost) @receiver(signals.post_delete, sender=models.Boost)
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -206,7 +221,10 @@ def remove_boost_on_delete(sender, instance, *args, **kwargs):
"""boosts are deleted""" """boosts are deleted"""
# we're only interested in new statuses # we're only interested in new statuses
for stream in streams.values(): for stream in streams.values():
# remove the boost
stream.remove_object_from_related_stores(instance) stream.remove_object_from_related_stores(instance)
# re-add the original status
stream.add_status(instance.boosted_status)
@receiver(signals.post_save, sender=models.UserFollows) @receiver(signals.post_save, sender=models.UserFollows)

View file

@ -37,7 +37,7 @@ class AbstractMinimalConnector(ABC):
for field in self_fields: for field in self_fields:
setattr(self, field, getattr(info, field)) setattr(self, field, getattr(info, field))
def search(self, query, min_confidence=None): def search(self, query, min_confidence=None, timeout=5):
"""free text search""" """free text search"""
params = {} params = {}
if min_confidence: if min_confidence:
@ -46,6 +46,7 @@ class AbstractMinimalConnector(ABC):
data = self.get_search_data( data = self.get_search_data(
"%s%s" % (self.search_url, query), "%s%s" % (self.search_url, query),
params=params, params=params,
timeout=timeout,
) )
results = [] results = []
@ -126,8 +127,8 @@ class AbstractConnector(AbstractMinimalConnector):
edition_data = data edition_data = data
try: try:
work_data = self.get_work_from_edition_data(data) work_data = self.get_work_from_edition_data(data)
except (KeyError, ConnectorException) as e: except (KeyError, ConnectorException) as err:
logger.exception(e) logger.exception(err)
work_data = data work_data = data
if not work_data or not edition_data: if not work_data or not edition_data:
@ -218,7 +219,7 @@ def dict_from_mappings(data, mappings):
return result return result
def get_data(url, params=None): def get_data(url, params=None, timeout=10):
"""wrapper for request.get""" """wrapper for request.get"""
# check if the url is blocked # check if the url is blocked
if models.FederatedServer.is_blocked(url): if models.FederatedServer.is_blocked(url):
@ -234,23 +235,24 @@ def get_data(url, params=None):
"Accept": "application/json; charset=utf-8", "Accept": "application/json; charset=utf-8",
"User-Agent": settings.USER_AGENT, "User-Agent": settings.USER_AGENT,
}, },
timeout=timeout,
) )
except (RequestError, SSLError, ConnectionError) as e: except (RequestError, SSLError, ConnectionError) as err:
logger.exception(e) logger.exception(err)
raise ConnectorException() raise ConnectorException()
if not resp.ok: if not resp.ok:
raise ConnectorException() raise ConnectorException()
try: try:
data = resp.json() data = resp.json()
except ValueError as e: except ValueError as err:
logger.exception(e) logger.exception(err)
raise ConnectorException() raise ConnectorException()
return data return data
def get_image(url): def get_image(url, timeout=10):
"""wrapper for requesting an image""" """wrapper for requesting an image"""
try: try:
resp = requests.get( resp = requests.get(
@ -258,9 +260,10 @@ def get_image(url):
headers={ headers={
"User-Agent": settings.USER_AGENT, "User-Agent": settings.USER_AGENT,
}, },
timeout=timeout,
) )
except (RequestError, SSLError) as e: except (RequestError, SSLError) as err:
logger.exception(e) logger.exception(err)
return None return None
if not resp.ok: if not resp.ok:
return None return None

View file

@ -1,4 +1,5 @@
""" interface with whatever connectors the app has """ """ interface with whatever connectors the app has """
from datetime import datetime
import importlib import importlib
import logging import logging
import re import re
@ -29,23 +30,25 @@ def search(query, min_confidence=0.1, return_first=False):
isbn = re.sub(r"[\W_]", "", query) isbn = re.sub(r"[\W_]", "", query)
maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13 maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13
timeout = 15
start_time = datetime.now()
for connector in get_connectors(): for connector in get_connectors():
result_set = None result_set = None
if maybe_isbn and connector.isbn_search_url and connector.isbn_search_url == "": if maybe_isbn and connector.isbn_search_url and connector.isbn_search_url != "":
# Search on ISBN # Search on ISBN
try: try:
result_set = connector.isbn_search(isbn) result_set = connector.isbn_search(isbn)
except Exception as e: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
logger.exception(e) logger.exception(err)
# if this fails, we can still try regular search # if this fails, we can still try regular search
# if no isbn search results, we fallback to generic search # if no isbn search results, we fallback to generic search
if not result_set: if not result_set:
try: try:
result_set = connector.search(query, min_confidence=min_confidence) result_set = connector.search(query, min_confidence=min_confidence)
except Exception as e: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
# we don't want *any* error to crash the whole search page # we don't want *any* error to crash the whole search page
logger.exception(e) logger.exception(err)
continue continue
if return_first and result_set: if return_first and result_set:
@ -59,6 +62,8 @@ def search(query, min_confidence=0.1, return_first=False):
"results": result_set, "results": result_set,
} }
) )
if (datetime.now() - start_time).seconds >= timeout:
break
if return_first: if return_first:
return None return None

View file

@ -74,7 +74,7 @@ class Connector(AbstractConnector):
**{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks"]}, **{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks"]},
} }
def search(self, query, min_confidence=None): def search(self, query, min_confidence=None): # pylint: disable=arguments-differ
"""overrides default search function with confidence ranking""" """overrides default search function with confidence ranking"""
results = super().search(query) results = super().search(query)
if min_confidence: if min_confidence:

View file

@ -3,7 +3,7 @@ from functools import reduce
import operator import operator
from django.contrib.postgres.search import SearchRank, SearchVector from django.contrib.postgres.search import SearchRank, SearchVector
from django.db.models import Count, OuterRef, Subquery, F, Q from django.db.models import OuterRef, Subquery, F, Q
from bookwyrm import models from bookwyrm import models
from .abstract_connector import AbstractConnector, SearchResult from .abstract_connector import AbstractConnector, SearchResult
@ -114,6 +114,7 @@ class Connector(AbstractConnector):
def search_identifiers(query, *filters): def search_identifiers(query, *filters):
"""tries remote_id, isbn; defined as dedupe fields on the model""" """tries remote_id, isbn; defined as dedupe fields on the model"""
# pylint: disable=W0212
or_filters = [ or_filters = [
{f.name: query} {f.name: query}
for f in models.Edition._meta.get_fields() for f in models.Edition._meta.get_fields()
@ -122,6 +123,8 @@ def search_identifiers(query, *filters):
results = models.Edition.objects.filter( results = models.Edition.objects.filter(
*filters, reduce(operator.or_, (Q(**f) for f in or_filters)) *filters, reduce(operator.or_, (Q(**f) for f in or_filters))
).distinct() ).distinct()
if results.count() <= 1:
return results
# when there are multiple editions of the same work, pick the default. # when there are multiple editions of the same work, pick the default.
# it would be odd for this to happen. # it would be odd for this to happen.
@ -146,19 +149,15 @@ def search_title_author(query, min_confidence, *filters):
) )
results = ( results = (
models.Edition.objects.annotate(search=vector) models.Edition.objects.annotate(rank=SearchRank(vector, query))
.annotate(rank=SearchRank(vector, query))
.filter(*filters, rank__gt=min_confidence) .filter(*filters, rank__gt=min_confidence)
.order_by("-rank") .order_by("-rank")
) )
# when there are multiple editions of the same work, pick the closest # when there are multiple editions of the same work, pick the closest
editions_of_work = ( editions_of_work = results.values("parent_work__id").values_list("parent_work__id")
results.values("parent_work")
.annotate(Count("parent_work"))
.values_list("parent_work")
)
# filter out multiple editions of the same work
for work_id in set(editions_of_work): for work_id in set(editions_of_work):
editions = results.filter(parent_work=work_id) editions = results.filter(parent_work=work_id)
default = editions.order_by("-edition_rank").first() default = editions.order_by("-edition_rank").first()

View file

@ -1,10 +1,20 @@
""" customize the info available in context for rendering templates """ """ customize the info available in context for rendering templates """
from bookwyrm import models from bookwyrm import models, settings
def site_settings(request): # pylint: disable=unused-argument def site_settings(request): # pylint: disable=unused-argument
"""include the custom info about the site""" """include the custom info about the site"""
request_protocol = "https://"
if not request.is_secure():
request_protocol = "http://"
return { return {
"site": models.SiteSettings.objects.get(), "site": models.SiteSettings.objects.get(),
"active_announcements": models.Announcement.active_announcements(), "active_announcements": models.Announcement.active_announcements(),
"static_url": settings.STATIC_URL,
"media_url": settings.MEDIA_URL,
"static_path": settings.STATIC_PATH,
"media_path": settings.MEDIA_PATH,
"preview_images_enabled": settings.ENABLE_PREVIEW_IMAGES,
"request_protocol": request_protocol,
} }

View file

@ -22,6 +22,7 @@ class CustomForm(ModelForm):
css_classes["number"] = "input" css_classes["number"] = "input"
css_classes["checkbox"] = "checkbox" css_classes["checkbox"] = "checkbox"
css_classes["textarea"] = "textarea" css_classes["textarea"] = "textarea"
# pylint: disable=super-with-arguments
super(CustomForm, self).__init__(*args, **kwargs) super(CustomForm, self).__init__(*args, **kwargs)
for visible in self.visible_fields(): for visible in self.visible_fields():
if hasattr(visible.field.widget, "input_type"): if hasattr(visible.field.widget, "input_type"):
@ -150,6 +151,12 @@ class LimitedEditUserForm(CustomForm):
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
class DeleteUserForm(CustomForm):
class Meta:
model = models.User
fields = ["password"]
class UserGroupForm(CustomForm): class UserGroupForm(CustomForm):
class Meta: class Meta:
model = models.User model = models.User
@ -175,8 +182,6 @@ class EditionForm(CustomForm):
"authors", "authors",
"parent_work", "parent_work",
"shelves", "shelves",
"subjects", # TODO
"subject_places", # TODO
"connector", "connector",
] ]

View file

@ -67,8 +67,8 @@ def import_data(source, job_id):
for item in job.items.all(): for item in job.items.all():
try: try:
item.resolve() item.resolve()
except Exception as e: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
logger.exception(e) logger.exception(err)
item.fail_reason = "Error loading book" item.fail_reason = "Error loading book"
item.save() item.save()
continue continue

View file

@ -0,0 +1,65 @@
""" Generate preview images """
from django.core.management.base import BaseCommand
from bookwyrm import models, preview_images
# pylint: disable=line-too-long
class Command(BaseCommand):
"""Creates previews for existing objects"""
help = "Generate preview images"
def add_arguments(self, parser):
parser.add_argument(
"--all",
"-a",
action="store_true",
help="Generates images for ALL types: site, users and books. Can use a lot of computing power.",
)
# pylint: disable=no-self-use,unused-argument
def handle(self, *args, **options):
"""generate preview images"""
self.stdout.write(
" | Hello! I will be generating preview images for your instance."
)
if options["all"]:
self.stdout.write(
"🧑‍🎨 ⎨ This might take quite long if your instance has a lot of books and users."
)
self.stdout.write(" | ✧ Thank you for your patience ✧")
else:
self.stdout.write("🧑‍🎨 ⎨ I will only generate the instance preview image.")
self.stdout.write(" | ✧ Be right back! ✧")
# Site
self.stdout.write(" → Site preview image: ", ending="")
preview_images.generate_site_preview_image_task.delay()
self.stdout.write(" OK 🖼")
if options["all"]:
# Users
users = models.User.objects.filter(
local=True,
is_active=True,
)
self.stdout.write(
" → User preview images ({}): ".format(len(users)), ending=""
)
for user in users:
preview_images.generate_user_preview_image_task.delay(user.id)
self.stdout.write(".", ending="")
self.stdout.write(" OK 🖼")
# Books
books = models.Book.objects.select_subclasses().filter()
self.stdout.write(
" → Book preview images ({}): ".format(len(books)), ending=""
)
for book in books:
preview_images.generate_edition_preview_image_task.delay(book.id)
self.stdout.write(".", ending="")
self.stdout.write(" OK 🖼")
self.stdout.write("🧑‍🎨 ⎨ Im all done! ✧ Enjoy ✧")

View file

@ -0,0 +1,32 @@
# Generated by Django 3.2 on 2021-05-26 12:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0075_announcement"),
]
operations = [
migrations.AddField(
model_name="book",
name="preview_image",
field=models.ImageField(
blank=True, null=True, upload_to="previews/covers/"
),
),
migrations.AddField(
model_name="sitesettings",
name="preview_image",
field=models.ImageField(blank=True, null=True, upload_to="previews/logos/"),
),
migrations.AddField(
model_name="user",
name="preview_image",
field=models.ImageField(
blank=True, null=True, upload_to="previews/avatars/"
),
),
]

View file

@ -2,10 +2,13 @@
import re import re
from django.db import models from django.db import models
from django.dispatch import receiver
from model_utils import FieldTracker
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN, DEFAULT_LANGUAGE from bookwyrm.preview_images import generate_edition_preview_image_task
from bookwyrm.settings import DOMAIN, DEFAULT_LANGUAGE, ENABLE_PREVIEW_IMAGES
from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
@ -82,10 +85,14 @@ class Book(BookDataModel):
cover = fields.ImageField( cover = fields.ImageField(
upload_to="covers/", blank=True, null=True, alt_field="alt_text" upload_to="covers/", blank=True, null=True, alt_field="alt_text"
) )
preview_image = models.ImageField(
upload_to="previews/covers/", blank=True, null=True
)
first_published_date = fields.DateTimeField(blank=True, null=True) first_published_date = fields.DateTimeField(blank=True, null=True)
published_date = fields.DateTimeField(blank=True, null=True) published_date = fields.DateTimeField(blank=True, null=True)
objects = InheritanceManager() objects = InheritanceManager()
field_tracker = FieldTracker(fields=["authors", "title", "subtitle", "cover"])
@property @property
def author_text(self): def author_text(self):
@ -293,3 +300,17 @@ def isbn_13_to_10(isbn_13):
if checkdigit == 10: if checkdigit == 10:
checkdigit = "X" checkdigit = "X"
return converted + str(checkdigit) return converted + str(checkdigit)
# pylint: disable=unused-argument
@receiver(models.signals.post_save, sender=Edition)
def preview_image(instance, *args, **kwargs):
"""create preview image on book create"""
if not ENABLE_PREVIEW_IMAGES:
return
changed_fields = {}
if instance.field_tracker:
changed_fields = instance.field_tracker.changed()
if len(changed_fields) > 0:
generate_edition_preview_image_task.delay(instance.id)

View file

@ -1,5 +1,6 @@
""" activitypub-aware django model fields """ """ activitypub-aware django model fields """
from dataclasses import MISSING from dataclasses import MISSING
import imghdr
import re import re
from uuid import uuid4 from uuid import uuid4
@ -9,6 +10,7 @@ from django.contrib.postgres.fields import ArrayField as DjangoArrayField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import models from django.db import models
from django.forms import ClearableFileInput, ImageField as DjangoImageField
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from bookwyrm import activitypub from bookwyrm import activitypub
@ -200,6 +202,7 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
*args, max_length=255, choices=PrivacyLevels.choices, default="public" *args, max_length=255, choices=PrivacyLevels.choices, default="public"
) )
# pylint: disable=invalid-name
def set_field_from_activity(self, instance, data): def set_field_from_activity(self, instance, data):
to = data.to to = data.to
cc = data.cc cc = data.cc
@ -218,6 +221,7 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
if hasattr(instance, "mention_users"): if hasattr(instance, "mention_users"):
mentions = [u.remote_id for u in instance.mention_users.all()] mentions = [u.remote_id for u in instance.mention_users.all()]
# this is a link to the followers list # this is a link to the followers list
# pylint: disable=protected-access
followers = instance.user.__class__._meta.get_field( followers = instance.user.__class__._meta.get_field(
"followers" "followers"
).field_to_activity(instance.user.followers) ).field_to_activity(instance.user.followers)
@ -332,6 +336,18 @@ class TagField(ManyToManyField):
return items return items
class ClearableFileInputWithWarning(ClearableFileInput):
"""max file size warning"""
template_name = "widgets/clearable_file_input_with_warning.html"
class CustomImageField(DjangoImageField):
"""overwrites image field for form"""
widget = ClearableFileInputWithWarning
def image_serializer(value, alt): def image_serializer(value, alt):
"""helper for serializing images""" """helper for serializing images"""
if value and hasattr(value, "url"): if value and hasattr(value, "url"):
@ -391,10 +407,19 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
if not response: if not response:
return None return None
image_name = str(uuid4()) + "." + url.split(".")[-1]
image_content = ContentFile(response.content) image_content = ContentFile(response.content)
image_name = str(uuid4()) + "." + imghdr.what(None, image_content.read())
return [image_name, image_content] return [image_name, image_content]
def formfield(self, **kwargs):
"""special case for forms"""
return super().formfield(
**{
"form_class": CustomImageField,
**kwargs,
}
)
class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
"""activitypub-aware datetime field""" """activitypub-aware datetime field"""

View file

@ -75,7 +75,12 @@ class ImportItem(models.Model):
def resolve(self): def resolve(self):
"""try various ways to lookup a book""" """try various ways to lookup a book"""
self.book = self.get_book_from_isbn() or self.get_book_from_title_author() if self.isbn:
self.book = self.get_book_from_isbn()
else:
# don't fall back on title/author search is isbn is present.
# you're too likely to mismatch
self.get_book_from_title_author()
def get_book_from_isbn(self): def get_book_from_isbn(self):
"""search by isbn""" """search by isbn"""

View file

@ -93,7 +93,8 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
) )
class Meta: class Meta:
# A book may only be placed into a list once, and each order in the list may be used only """A book may only be placed into a list once,
# once and each order in the list may be used only once"""
unique_together = (("book", "book_list"), ("order", "book_list")) unique_together = (("book", "book_list"), ("order", "book_list"))
ordering = ("-created_date",) ordering = ("-created_date",)

View file

@ -99,7 +99,7 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
status = "follow_request" status = "follow_request"
activity_serializer = activitypub.Follow activity_serializer = activitypub.Follow
def save(self, *args, broadcast=True, **kwargs): def save(self, *args, broadcast=True, **kwargs): # pylint: disable=arguments-differ
"""make sure the follow or block relationship doesn't already exist""" """make sure the follow or block relationship doesn't already exist"""
# if there's a request for a follow that already exists, accept it # if there's a request for a follow that already exists, accept it
# without changing the local database state # without changing the local database state

View file

@ -4,9 +4,12 @@ import datetime
from Crypto import Random from Crypto import Random
from django.db import models, IntegrityError from django.db import models, IntegrityError
from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from model_utils import FieldTracker
from bookwyrm.settings import DOMAIN from bookwyrm.preview_images import generate_site_preview_image_task
from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
from .user import User from .user import User
@ -35,6 +38,9 @@ class SiteSettings(models.Model):
logo = models.ImageField(upload_to="logos/", null=True, blank=True) logo = models.ImageField(upload_to="logos/", null=True, blank=True)
logo_small = models.ImageField(upload_to="logos/", null=True, blank=True) logo_small = models.ImageField(upload_to="logos/", null=True, blank=True)
favicon = models.ImageField(upload_to="logos/", null=True, blank=True) favicon = models.ImageField(upload_to="logos/", null=True, blank=True)
preview_image = models.ImageField(
upload_to="previews/logos/", null=True, blank=True
)
# footer # footer
support_link = models.CharField(max_length=255, null=True, blank=True) support_link = models.CharField(max_length=255, null=True, blank=True)
@ -42,6 +48,8 @@ class SiteSettings(models.Model):
admin_email = models.EmailField(max_length=255, null=True, blank=True) admin_email = models.EmailField(max_length=255, null=True, blank=True)
footer_item = models.TextField(null=True, blank=True) footer_item = models.TextField(null=True, blank=True)
field_tracker = FieldTracker(fields=["name", "instance_tagline", "logo"])
@classmethod @classmethod
def get(cls): def get(cls):
"""gets the site settings db entry or defaults""" """gets the site settings db entry or defaults"""
@ -119,3 +127,15 @@ class PasswordReset(models.Model):
def link(self): def link(self):
"""formats the invite link""" """formats the invite link"""
return "https://{}/password-reset/{}".format(DOMAIN, self.code) return "https://{}/password-reset/{}".format(DOMAIN, self.code)
# pylint: disable=unused-argument
@receiver(models.signals.post_save, sender=SiteSettings)
def preview_image(instance, *args, **kwargs):
"""Update image preview for the default site image"""
if not ENABLE_PREVIEW_IMAGES:
return
changed_fields = instance.field_tracker.changed()
if len(changed_fields) > 0:
generate_site_preview_image_task.delay()

View file

@ -5,11 +5,15 @@ import re
from django.apps import apps from django.apps import apps
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.dispatch import receiver
from django.template.loader import get_template from django.template.loader import get_template
from django.utils import timezone from django.utils import timezone
from model_utils import FieldTracker
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.preview_images import generate_edition_preview_image_task
from bookwyrm.settings import ENABLE_PREVIEW_IMAGES
from .activitypub_mixin import ActivitypubMixin, ActivityMixin from .activitypub_mixin import ActivitypubMixin, ActivityMixin
from .activitypub_mixin import OrderedCollectionPageMixin from .activitypub_mixin import OrderedCollectionPageMixin
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
@ -304,6 +308,8 @@ class Review(Status):
max_digits=3, max_digits=3,
) )
field_tracker = FieldTracker(fields=["rating"])
@property @property
def pure_name(self): def pure_name(self):
"""clarify review names for mastodon serialization""" """clarify review names for mastodon serialization"""
@ -398,3 +404,17 @@ class Boost(ActivityMixin, Status):
# This constraint can't work as it would cross tables. # This constraint can't work as it would cross tables.
# class Meta: # class Meta:
# unique_together = ('user', 'boosted_status') # unique_together = ('user', 'boosted_status')
# pylint: disable=unused-argument
@receiver(models.signals.post_save)
def preview_image(instance, sender, *args, **kwargs):
"""Updates book previews if the rating has changed"""
if not ENABLE_PREVIEW_IMAGES or sender not in (Review, ReviewRating):
return
changed_fields = instance.field_tracker.changed()
if len(changed_fields) > 0:
edition = instance.book
generate_edition_preview_image_task.delay(edition.id)

View file

@ -6,15 +6,18 @@ from django.apps import apps
from django.contrib.auth.models import AbstractUser, Group from django.contrib.auth.models import AbstractUser, Group
from django.contrib.postgres.fields import CICharField from django.contrib.postgres.fields import CICharField
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.dispatch import receiver
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from model_utils import FieldTracker
import pytz import pytz
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.connectors import get_data, ConnectorException from bookwyrm.connectors import get_data, ConnectorException
from bookwyrm.models.shelf import Shelf from bookwyrm.models.shelf import Shelf
from bookwyrm.models.status import Status, Review from bookwyrm.models.status import Status, Review
from bookwyrm.settings import DOMAIN from bookwyrm.preview_images import generate_user_preview_image_task
from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES
from bookwyrm.signatures import create_key_pair from bookwyrm.signatures import create_key_pair
from bookwyrm.tasks import app from bookwyrm.tasks import app
from bookwyrm.utils import regex from bookwyrm.utils import regex
@ -70,6 +73,9 @@ class User(OrderedCollectionPageMixin, AbstractUser):
activitypub_field="icon", activitypub_field="icon",
alt_field="alt_text", alt_field="alt_text",
) )
preview_image = models.ImageField(
upload_to="previews/avatars/", blank=True, null=True
)
followers = fields.ManyToManyField( followers = fields.ManyToManyField(
"self", "self",
link_only=True, link_only=True,
@ -117,6 +123,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
name_field = "username" name_field = "username"
property_fields = [("following_link", "following")] property_fields = [("following_link", "following")]
field_tracker = FieldTracker(fields=["name", "avatar"])
@property @property
def following_link(self): def following_link(self):
@ -232,7 +239,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""populate fields for new local users""" """populate fields for new local users"""
created = not bool(self.id) created = not bool(self.id)
if not self.local and not re.match(regex.full_username, self.username): if not self.local and not re.match(regex.FULL_USERNAME, self.username):
# generate a username that uses the domain (webfinger format) # generate a username that uses the domain (webfinger format)
actor_parts = urlparse(self.remote_id) actor_parts = urlparse(self.remote_id)
self.username = "%s@%s" % (self.username, actor_parts.netloc) self.username = "%s@%s" % (self.username, actor_parts.netloc)
@ -356,7 +363,7 @@ class AnnualGoal(BookWyrmModel):
def get_remote_id(self): def get_remote_id(self):
"""put the year in the path""" """put the year in the path"""
return "%s/goal/%d" % (self.user.remote_id, self.year) return "{:s}/goal/{:d}".format(self.user.remote_id, self.year)
@property @property
def books(self): def books(self):
@ -381,17 +388,16 @@ class AnnualGoal(BookWyrmModel):
return {r.book.id: r.rating for r in reviews} return {r.book.id: r.rating for r in reviews}
@property @property
def progress_percent(self): def progress(self):
"""how close to your goal, in percent form"""
return int(float(self.book_count / self.goal) * 100)
@property
def book_count(self):
"""how many books you've read this year""" """how many books you've read this year"""
return self.user.readthrough_set.filter( count = self.user.readthrough_set.filter(
finish_date__year__gte=self.year, finish_date__year__gte=self.year,
finish_date__year__lt=self.year + 1, finish_date__year__lt=self.year + 1,
).count() ).count()
return {
"count": count,
"percent": int(float(count / self.goal) * 100),
}
@app.task @app.task
@ -444,3 +450,15 @@ def get_remote_reviews(outbox):
if not activity["type"] == "Review": if not activity["type"] == "Review":
continue continue
activitypub.Review(**activity).to_model() activitypub.Review(**activity).to_model()
# pylint: disable=unused-argument
@receiver(models.signals.post_save, sender=User)
def preview_image(instance, *args, **kwargs):
"""create preview images when user is updated"""
if not ENABLE_PREVIEW_IMAGES:
return
changed_fields = instance.field_tracker.changed()
if len(changed_fields) > 0:
generate_user_preview_image_task.delay(instance.id)

424
bookwyrm/preview_images.py Normal file
View file

@ -0,0 +1,424 @@
""" Generate social media preview images for twitter/mastodon/etc """
import math
import os
import textwrap
from io import BytesIO
from uuid import uuid4
import colorsys
from colorthief import ColorThief
from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageColor
from django.core.files.base import ContentFile
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.db.models import Avg
from bookwyrm import models, settings
from bookwyrm.tasks import app
IMG_WIDTH = settings.PREVIEW_IMG_WIDTH
IMG_HEIGHT = settings.PREVIEW_IMG_HEIGHT
BG_COLOR = settings.PREVIEW_BG_COLOR
TEXT_COLOR = settings.PREVIEW_TEXT_COLOR
DEFAULT_COVER_COLOR = settings.PREVIEW_DEFAULT_COVER_COLOR
TRANSPARENT_COLOR = (0, 0, 0, 0)
margin = math.floor(IMG_HEIGHT / 10)
gutter = math.floor(margin / 2)
inner_img_height = math.floor(IMG_HEIGHT * 0.8)
inner_img_width = math.floor(inner_img_height * 0.7)
font_dir = os.path.join(settings.STATIC_ROOT, "fonts/public_sans")
def get_font(font_name, size=28):
"""Loads custom font"""
if font_name == "light":
font_path = os.path.join(font_dir, "PublicSans-Light.ttf")
if font_name == "regular":
font_path = os.path.join(font_dir, "PublicSans-Regular.ttf")
elif font_name == "bold":
font_path = os.path.join(font_dir, "PublicSans-Bold.ttf")
try:
font = ImageFont.truetype(font_path, size)
except OSError:
font = ImageFont.load_default()
return font
def generate_texts_layer(texts, content_width):
"""Adds text for images"""
font_text_zero = get_font("bold", size=20)
font_text_one = get_font("bold", size=48)
font_text_two = get_font("bold", size=40)
font_text_three = get_font("regular", size=40)
text_layer = Image.new("RGBA", (content_width, IMG_HEIGHT), color=TRANSPARENT_COLOR)
text_layer_draw = ImageDraw.Draw(text_layer)
text_y = 0
if "text_zero" in texts and texts["text_zero"]:
# Text one (Book title)
text_zero = textwrap.fill(texts["text_zero"], width=72)
text_layer_draw.multiline_text(
(0, text_y), text_zero, font=font_text_zero, fill=TEXT_COLOR
)
try:
text_y = text_y + font_text_zero.getsize_multiline(text_zero)[1] + 16
except (AttributeError, IndexError):
text_y = text_y + 26
if "text_one" in texts and texts["text_one"]:
# Text one (Book title)
text_one = textwrap.fill(texts["text_one"], width=28)
text_layer_draw.multiline_text(
(0, text_y), text_one, font=font_text_one, fill=TEXT_COLOR
)
try:
text_y = text_y + font_text_one.getsize_multiline(text_one)[1] + 16
except (AttributeError, IndexError):
text_y = text_y + 26
if "text_two" in texts and texts["text_two"]:
# Text one (Book subtitle)
text_two = textwrap.fill(texts["text_two"], width=36)
text_layer_draw.multiline_text(
(0, text_y), text_two, font=font_text_two, fill=TEXT_COLOR
)
try:
text_y = text_y + font_text_one.getsize_multiline(text_two)[1] + 16
except (AttributeError, IndexError):
text_y = text_y + 26
if "text_three" in texts and texts["text_three"]:
# Text three (Book authors)
text_three = textwrap.fill(texts["text_three"], width=36)
text_layer_draw.multiline_text(
(0, text_y), text_three, font=font_text_three, fill=TEXT_COLOR
)
text_layer_box = text_layer.getbbox()
return text_layer.crop(text_layer_box)
def generate_instance_layer(content_width):
"""Places components for instance preview"""
font_instance = get_font("light", size=28)
site = models.SiteSettings.objects.get()
if site.logo_small:
logo_img = Image.open(site.logo_small)
else:
try:
static_path = os.path.join(settings.STATIC_ROOT, "images/logo-small.png")
logo_img = Image.open(static_path)
except FileNotFoundError:
logo_img = None
instance_layer = Image.new("RGBA", (content_width, 62), color=TRANSPARENT_COLOR)
instance_text_x = 0
if logo_img:
logo_img.thumbnail((50, 50), Image.ANTIALIAS)
instance_layer.paste(logo_img, (0, 0))
instance_text_x = instance_text_x + 60
instance_layer_draw = ImageDraw.Draw(instance_layer)
instance_layer_draw.text(
(instance_text_x, 10), site.name, font=font_instance, fill=TEXT_COLOR
)
line_width = 50 + 10 + font_instance.getsize(site.name)[0]
line_layer = Image.new(
"RGBA", (line_width, 2), color=(*(ImageColor.getrgb(TEXT_COLOR)), 50)
)
instance_layer.alpha_composite(line_layer, (0, 60))
return instance_layer
def generate_rating_layer(rating, content_width):
"""Places components for rating preview"""
try:
icon_star_full = Image.open(
os.path.join(settings.STATIC_ROOT, "images/icons/star-full.png")
)
icon_star_empty = Image.open(
os.path.join(settings.STATIC_ROOT, "images/icons/star-empty.png")
)
icon_star_half = Image.open(
os.path.join(settings.STATIC_ROOT, "images/icons/star-half.png")
)
except FileNotFoundError:
return None
icon_size = 64
icon_margin = 10
rating_layer_base = Image.new(
"RGBA", (content_width, icon_size), color=TRANSPARENT_COLOR
)
rating_layer_color = Image.new("RGBA", (content_width, icon_size), color=TEXT_COLOR)
rating_layer_mask = Image.new(
"RGBA", (content_width, icon_size), color=TRANSPARENT_COLOR
)
position_x = 0
for _ in range(math.floor(rating)):
rating_layer_mask.alpha_composite(icon_star_full, (position_x, 0))
position_x = position_x + icon_size + icon_margin
if math.floor(rating) != math.ceil(rating):
rating_layer_mask.alpha_composite(icon_star_half, (position_x, 0))
position_x = position_x + icon_size + icon_margin
for _ in range(5 - math.ceil(rating)):
rating_layer_mask.alpha_composite(icon_star_empty, (position_x, 0))
position_x = position_x + icon_size + icon_margin
rating_layer_mask = rating_layer_mask.getchannel("A")
rating_layer_mask = ImageOps.invert(rating_layer_mask)
rating_layer_composite = Image.composite(
rating_layer_base, rating_layer_color, rating_layer_mask
)
return rating_layer_composite
def generate_default_inner_img():
"""Adds cover image"""
font_cover = get_font("light", size=28)
default_cover = Image.new(
"RGB", (inner_img_width, inner_img_height), color=DEFAULT_COVER_COLOR
)
default_cover_draw = ImageDraw.Draw(default_cover)
text = "no image :("
text_dimensions = font_cover.getsize(text)
text_coords = (
math.floor((inner_img_width - text_dimensions[0]) / 2),
math.floor((inner_img_height - text_dimensions[1]) / 2),
)
default_cover_draw.text(text_coords, text, font=font_cover, fill="white")
return default_cover
# pylint: disable=too-many-locals
def generate_preview_image(
texts=None, picture=None, rating=None, show_instance_layer=True
):
"""Puts everything together"""
texts = texts or {}
# Cover
try:
inner_img_layer = Image.open(picture)
inner_img_layer.thumbnail((inner_img_width, inner_img_height), Image.ANTIALIAS)
color_thief = ColorThief(picture)
dominant_color = color_thief.get_color(quality=1)
except: # pylint: disable=bare-except
inner_img_layer = generate_default_inner_img()
dominant_color = ImageColor.getrgb(DEFAULT_COVER_COLOR)
# Color
if BG_COLOR in ["use_dominant_color_light", "use_dominant_color_dark"]:
image_bg_color = "rgb(%s, %s, %s)" % dominant_color
# Adjust color
image_bg_color_rgb = [x / 255.0 for x in ImageColor.getrgb(image_bg_color)]
image_bg_color_hls = colorsys.rgb_to_hls(*image_bg_color_rgb)
if BG_COLOR == "use_dominant_color_light":
lightness = max(0.9, image_bg_color_hls[1])
else:
lightness = min(0.15, image_bg_color_hls[1])
image_bg_color_hls = (
image_bg_color_hls[0],
lightness,
image_bg_color_hls[2],
)
image_bg_color = tuple(
math.ceil(x * 255) for x in colorsys.hls_to_rgb(*image_bg_color_hls)
)
else:
image_bg_color = BG_COLOR
# Background (using the color)
img = Image.new("RGBA", (IMG_WIDTH, IMG_HEIGHT), color=image_bg_color)
# Contents
inner_img_x = margin + inner_img_width - inner_img_layer.width
inner_img_y = math.floor((IMG_HEIGHT - inner_img_layer.height) / 2)
content_x = margin + inner_img_width + gutter
content_width = IMG_WIDTH - content_x - margin
contents_layer = Image.new(
"RGBA", (content_width, IMG_HEIGHT), color=TRANSPARENT_COLOR
)
contents_composite_y = 0
if show_instance_layer:
instance_layer = generate_instance_layer(content_width)
contents_layer.alpha_composite(instance_layer, (0, contents_composite_y))
contents_composite_y = contents_composite_y + instance_layer.height + gutter
texts_layer = generate_texts_layer(texts, content_width)
contents_layer.alpha_composite(texts_layer, (0, contents_composite_y))
contents_composite_y = contents_composite_y + texts_layer.height + gutter
if rating:
# Add some more margin
contents_composite_y = contents_composite_y + gutter
rating_layer = generate_rating_layer(rating, content_width)
if rating_layer:
contents_layer.alpha_composite(rating_layer, (0, contents_composite_y))
contents_composite_y = contents_composite_y + rating_layer.height + gutter
contents_layer_box = contents_layer.getbbox()
contents_layer_height = contents_layer_box[3] - contents_layer_box[1]
contents_y = math.floor((IMG_HEIGHT - contents_layer_height) / 2)
if show_instance_layer:
# Remove Instance Layer from centering calculations
contents_y = contents_y - math.floor((instance_layer.height + gutter) / 2)
contents_y = max(contents_y, margin)
# Composite layers
img.paste(
inner_img_layer, (inner_img_x, inner_img_y), inner_img_layer.convert("RGBA")
)
img.alpha_composite(contents_layer, (content_x, contents_y))
return img.convert("RGB")
def save_and_cleanup(image, instance=None):
"""Save and close the file"""
if not isinstance(instance, (models.Book, models.User, models.SiteSettings)):
return False
file_name = "%s-%s.jpg" % (str(instance.id), str(uuid4()))
image_buffer = BytesIO()
try:
try:
old_path = instance.preview_image.path
except ValueError:
old_path = ""
# Save
image.save(image_buffer, format="jpeg", quality=75)
instance.preview_image = InMemoryUploadedFile(
ContentFile(image_buffer.getvalue()),
"preview_image",
file_name,
"image/jpg",
image_buffer.tell(),
None,
)
save_without_broadcast = isinstance(instance, (models.Book, models.User))
if save_without_broadcast:
instance.save(broadcast=False)
else:
instance.save()
# Clean up old file after saving
if os.path.exists(old_path):
os.remove(old_path)
finally:
image_buffer.close()
return True
# pylint: disable=invalid-name
@app.task
def generate_site_preview_image_task():
"""generate preview_image for the website"""
if not settings.ENABLE_PREVIEW_IMAGES:
return
site = models.SiteSettings.objects.get()
if site.logo:
logo = site.logo
else:
logo = os.path.join(settings.STATIC_ROOT, "images/logo.png")
texts = {
"text_zero": settings.DOMAIN,
"text_one": site.name,
"text_three": site.instance_tagline,
}
image = generate_preview_image(texts=texts, picture=logo, show_instance_layer=False)
save_and_cleanup(image, instance=site)
# pylint: disable=invalid-name
@app.task
def generate_edition_preview_image_task(book_id):
"""generate preview_image for a book"""
if not settings.ENABLE_PREVIEW_IMAGES:
return
book = models.Book.objects.select_subclasses().get(id=book_id)
rating = models.Review.objects.filter(
privacy="public",
deleted=False,
book__in=[book_id],
).aggregate(Avg("rating"))["rating__avg"]
texts = {
"text_one": book.title,
"text_two": book.subtitle,
"text_three": book.author_text,
}
image = generate_preview_image(texts=texts, picture=book.cover, rating=rating)
save_and_cleanup(image, instance=book)
@app.task
def generate_user_preview_image_task(user_id):
"""generate preview_image for a book"""
if not settings.ENABLE_PREVIEW_IMAGES:
return
user = models.User.objects.get(id=user_id)
texts = {
"text_one": user.display_name,
"text_three": "@{}@{}".format(user.localname, settings.DOMAIN),
}
if user.avatar:
avatar = user.avatar
else:
avatar = os.path.join(settings.STATIC_ROOT, "images/default_avi.jpg")
image = generate_preview_image(texts=texts, picture=avatar)
save_and_cleanup(image, instance=user)

View file

@ -14,13 +14,18 @@ PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English") DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
# celery # celery
CELERY_BROKER = env("CELERY_BROKER") CELERY_BROKER = "redis://:{}@redis_broker:{}/0".format(
CELERY_RESULT_BACKEND = env("CELERY_RESULT_BACKEND") requests.utils.quote(env("REDIS_BROKER_PASSWORD", "")), env("REDIS_BROKER_PORT")
)
CELERY_RESULT_BACKEND = "redis://:{}@redis_broker:{}/0".format(
requests.utils.quote(env("REDIS_BROKER_PASSWORD", "")), env("REDIS_BROKER_PORT")
)
CELERY_ACCEPT_CONTENT = ["application/json"] CELERY_ACCEPT_CONTENT = ["application/json"]
CELERY_TASK_SERIALIZER = "json" CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json" CELERY_RESULT_SERIALIZER = "json"
# email # email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
EMAIL_HOST = env("EMAIL_HOST") EMAIL_HOST = env("EMAIL_HOST")
EMAIL_PORT = env("EMAIL_PORT", 587) EMAIL_PORT = env("EMAIL_PORT", 587)
EMAIL_HOST_USER = env("EMAIL_HOST_USER") EMAIL_HOST_USER = env("EMAIL_HOST_USER")
@ -37,6 +42,14 @@ LOCALE_PATHS = [
DEFAULT_AUTO_FIELD = "django.db.models.AutoField" DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Preview image
ENABLE_PREVIEW_IMAGES = env.bool("ENABLE_PREVIEW_IMAGES", False)
PREVIEW_BG_COLOR = env.str("PREVIEW_BG_COLOR", "use_dominant_color_light")
PREVIEW_TEXT_COLOR = env.str("PREVIEW_TEXT_COLOR", "#363636")
PREVIEW_IMG_WIDTH = env.int("PREVIEW_IMG_WIDTH", 1200)
PREVIEW_IMG_HEIGHT = env.int("PREVIEW_IMG_HEIGHT", 630)
PREVIEW_DEFAULT_COVER_COLOR = env.str("PREVIEW_DEFAULT_COVER_COLOR", "#002549")
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
@ -47,7 +60,6 @@ SECRET_KEY = env("SECRET_KEY")
DEBUG = env.bool("DEBUG", True) DEBUG = env.bool("DEBUG", True)
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"]) ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"])
OL_URL = env("OL_URL")
# Application definition # Application definition
@ -109,10 +121,8 @@ STREAMS = ["home", "local", "federated"]
# Database # Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases # https://docs.djangoproject.com/en/3.2/ref/settings/#databases
BOOKWYRM_DATABASE_BACKEND = env("BOOKWYRM_DATABASE_BACKEND", "postgres") DATABASES = {
"default": {
BOOKWYRM_DBS = {
"postgres": {
"ENGINE": "django.db.backends.postgresql_psycopg2", "ENGINE": "django.db.backends.postgresql_psycopg2",
"NAME": env("POSTGRES_DB", "fedireads"), "NAME": env("POSTGRES_DB", "fedireads"),
"USER": env("POSTGRES_USER", "fedireads"), "USER": env("POSTGRES_USER", "fedireads"),
@ -122,8 +132,6 @@ BOOKWYRM_DBS = {
}, },
} }
DATABASES = {"default": BOOKWYRM_DBS[BOOKWYRM_DATABASE_BACKEND]}
LOGIN_URL = "/login/" LOGIN_URL = "/login/"
AUTH_USER_MODEL = "bookwyrm.User" AUTH_USER_MODEL = "bookwyrm.User"
@ -131,6 +139,7 @@ AUTH_USER_MODEL = "bookwyrm.User"
# Password validation # Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
# pylint: disable=line-too-long
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
@ -174,8 +183,10 @@ USE_TZ = True
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_URL = "/static/" STATIC_URL = "/static/"
STATIC_PATH = "%s/%s" % (DOMAIN, env("STATIC_ROOT", "static"))
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static")) STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
MEDIA_URL = "/images/" MEDIA_URL = "/images/"
MEDIA_PATH = "%s/%s" % (DOMAIN, env("MEDIA_ROOT", "images"))
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images")) MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % ( USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % (

View file

@ -73,6 +73,7 @@ class Signature:
self.headers = headers self.headers = headers
self.signature = signature self.signature = signature
# pylint: disable=invalid-name
@classmethod @classmethod
def parse(cls, request): def parse(cls, request):
"""extract and parse a signature from an http request""" """extract and parse a signature from an http request"""

View file

@ -43,6 +43,19 @@ body {
white-space: nowrap !important; white-space: nowrap !important;
width: 0.01em !important; width: 0.01em !important;
} }
.m-0-mobile {
margin: 0 !important;
}
.card-footer.is-stacked-mobile {
flex-direction: column;
}
.card-footer.is-stacked-mobile .card-footer-item:not(:last-child) {
border-bottom: 1px solid #ededed;
border-right: 0;
}
} }
.button.is-transparent { .button.is-transparent {
@ -331,6 +344,49 @@ body {
} }
} }
/* Book list
******************************************************************************/
ol.ordered-list {
list-style: none;
counter-reset: list-counter;
}
ol.ordered-list li {
counter-increment: list-counter;
}
ol.ordered-list li::before {
content: counter(list-counter);
position: absolute;
left: -20px;
width: 20px;
height: 24px;
background-color: #fff;
border: 1px solid #dbdbdb;
border-right: 0;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
display: flex;
justify-content: center;
align-items: center;
color: #888;
font-size: 0.8em;
font-weight: bold;
}
@media only screen and (max-width: 768px) {
ol.ordered-list li::before {
left: 0;
z-index: 1;
border: 0;
border-right: 1px solid #dbdbdb;
border-bottom: 1px solid #dbdbdb;
border-radius: 0;
border-bottom-right-radius: 2px;
}
}
/* Dimensions /* Dimensions
* @todo These could be in rem. * @todo These could be in rem.
******************************************************************************/ ******************************************************************************/

View file

@ -0,0 +1,94 @@
Copyright (c) 2015, Pablo Impallari, Rodrigo Fuenzalida
(Modified by Dan O. Williams and USWDS) (https://github.com/uswds/public-sans)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -3,6 +3,7 @@
let BookWyrm = new class { let BookWyrm = new class {
constructor() { constructor() {
this.MAX_FILE_SIZE_BYTES = 10 * 1000000;
this.initOnDOMLoaded(); this.initOnDOMLoaded();
this.initReccuringTasks(); this.initReccuringTasks();
this.initEventListeners(); this.initEventListeners();
@ -32,15 +33,26 @@ let BookWyrm = new class {
'click', 'click',
this.back) this.back)
); );
document.querySelectorAll('input[type="file"]')
.forEach(node => node.addEventListener(
'change',
this.disableIfTooLarge.bind(this)
));
} }
/** /**
* Execute code once the DOM is loaded. * Execute code once the DOM is loaded.
*/ */
initOnDOMLoaded() { initOnDOMLoaded() {
const bookwyrm = this;
window.addEventListener('DOMContentLoaded', function() { window.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.tab-group') document.querySelectorAll('.tab-group')
.forEach(tabs => new TabGroup(tabs)); .forEach(tabs => new TabGroup(tabs));
document.querySelectorAll('input[type="file"]').forEach(
bookwyrm.disableIfTooLarge.bind(bookwyrm)
);
}); });
} }
@ -126,6 +138,7 @@ let BookWyrm = new class {
* @return {undefined} * @return {undefined}
*/ */
toggleAction(event) { toggleAction(event) {
event.preventDefault();
let trigger = event.currentTarget; let trigger = event.currentTarget;
let pressed = trigger.getAttribute('aria-pressed') === 'false'; let pressed = trigger.getAttribute('aria-pressed') === 'false';
let targetId = trigger.dataset.controls; let targetId = trigger.dataset.controls;
@ -170,6 +183,8 @@ let BookWyrm = new class {
if (focus) { if (focus) {
this.toggleFocus(focus); this.toggleFocus(focus);
} }
return false;
} }
/** /**
@ -284,4 +299,27 @@ let BookWyrm = new class {
node.classList.remove(classname); node.classList.remove(classname);
} }
} }
}
disableIfTooLarge(eventOrElement) {
const { addRemoveClass, MAX_FILE_SIZE_BYTES } = this;
const element = eventOrElement.currentTarget || eventOrElement;
const submits = element.form.querySelectorAll('[type="submit"]');
const warns = element.parentElement.querySelectorAll('.file-too-big');
const isTooBig = element.files &&
element.files[0] &&
element.files[0].size > MAX_FILE_SIZE_BYTES;
if (isTooBig) {
submits.forEach(submitter => submitter.disabled = true);
warns.forEach(
sib => addRemoveClass(sib, 'is-hidden', false)
);
} else {
submits.forEach(submitter => submitter.disabled = false);
warns.forEach(
sib => addRemoveClass(sib, 'is-hidden', true)
);
}
}
}();

View file

@ -17,7 +17,7 @@ let LocalStorageTools = new class {
* @return {undefined} * @return {undefined}
*/ */
updateDisplay(event) { updateDisplay(event) {
// used in set reading goal // Used in set reading goal
let key = event.target.dataset.id; let key = event.target.dataset.id;
let value = event.target.dataset.value; let value = event.target.dataset.value;
@ -34,10 +34,10 @@ let LocalStorageTools = new class {
* @return {undefined} * @return {undefined}
*/ */
setDisplay(node) { setDisplay(node) {
// used in set reading goal // Used in set reading goal
let key = node.dataset.hide; let key = node.dataset.hide;
let value = window.localStorage.getItem(key); let value = window.localStorage.getItem(key);
BookWyrm.addRemoveClass(node, 'is-hidden', value); BookWyrm.addRemoveClass(node, 'is-hidden', value);
} }
} }();

View file

@ -15,49 +15,83 @@
<div class="column is-narrow"> <div class="column is-narrow">
<a href="{{ author.local_path }}/edit"> <a href="{{ author.local_path }}/edit">
<span class="icon icon-pencil" title="{% trans 'Edit Author' %}" aria-hidden="True"></span> <span class="icon icon-pencil" title="{% trans 'Edit Author' %}" aria-hidden="True"></span>
<span>{% trans "Edit Author" %}</span> <span class="is-hidden-mobile">{% trans "Edit Author" %}</span>
</a> </a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="block content columns"> <div class="block columns" itemscope itemtype="https://schema.org/Person">
{% if author.aliases or author.born or author.died or author.wikipedia_link %} <meta itemprop="name" content="{{ author.name }}">
<div class="column is-narrow">
<div class="box"> {% if author.aliases or author.born or author.died or author.wikipedia_link or author.openlibrary_key or author.inventaire_id %}
<div class="column is-two-fifths">
<div class="box py-2">
<dl> <dl>
{% if author.aliases %} {% if author.aliases %}
<div class="is-flex"> <div class="is-flex is-flex-wrap-wrap my-1">
<dt class="mr-1">{% trans "Aliases:" %}</dt> <dt class="has-text-weight-bold mr-1">{% trans "Aliases:" %}</dt>
<dd itemprop="aliases">{{ author.aliases|join:', ' }}</dd> {% for alias in author.aliases %}
<dd itemprop="alternateName" content="{{alias}}">
{{alias}}{% if not forloop.last %},&nbsp;{% endif %}
</dd>
{% endfor %}
</div> </div>
{% endif %} {% endif %}
{% if author.born %} {% if author.born %}
<div class="is-flex"> <div class="is-flex my-1">
<dt class="mr-1">{% trans "Born:" %}</dt> <dt class="has-text-weight-bold mr-1">{% trans "Born:" %}</dt>
<dd itemprop="aliases">{{ author.born|naturalday }}</dd> <dd itemprop="birthDate">{{ author.born|naturalday }}</dd>
</div> </div>
{% endif %} {% endif %}
{% if author.aliases %}
<div class="is-flex"> {% if author.died %}
<dt class="mr-1">{% trans "Died:" %}</dt> <div class="is-flex my-1">
<dd itemprop="aliases">{{ author.died|naturalday }}</dd> <dt class="has-text-weight-bold mr-1">{% trans "Died:" %}</dt>
<dd itemprop="deathDate">{{ author.died|naturalday }}</dd>
</div> </div>
{% endif %} {% endif %}
</dl> </dl>
{% if author.wikipedia_link %} {% if author.wikipedia_link %}
<p><a href="{{ author.wikipedia_link }}" rel=”noopener” target="_blank">{% trans "Wikipedia" %}</a></p> <p class="my-1">
{% endif %} <a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel=”noopener” target="_blank">
{% if author.openlibrary_key %} {% trans "Wikipedia" %}
<p class="mb-0"> </a>
<a href="https://openlibrary.org/authors/{{ author.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a>
</p> </p>
{% endif %} {% endif %}
{% if author.openlibrary_key %}
<p class="my-1">
<a itemprop="sameAs" href="https://openlibrary.org/authors/{{ author.openlibrary_key }}" target="_blank" rel="noopener">
{% trans "View on OpenLibrary" %}
</a>
</p>
{% endif %}
{% if author.inventaire_id %} {% if author.inventaire_id %}
<p class="mb-0"> <p class="my-1">
<a href="https://inventaire.io/entity/{{ author.inventaire_id }}" target="_blank" rel="noopener">{% trans "View on Inventaire" %}</a> <a itemprop="sameAs" href="https://inventaire.io/entity/{{ author.inventaire_id }}" target="_blank" rel="noopener">
{% trans "View on Inventaire" %}
</a>
</p>
{% endif %}
{% if author.librarything_key %}
<p class="my-1">
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener">
{% trans "View on LibraryThing" %}
</a>
</p>
{% endif %}
{% if author.goodreads_key %}
<p class="my-1">
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener">
{% trans "View on Goodreads" %}
</a>
</p> </p>
{% endif %} {% endif %}
</div> </div>

View file

@ -29,67 +29,85 @@
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<h2 class="title is-4">{% trans "Metadata" %}</h2> <h2 class="title is-4">{% trans "Metadata" %}</h2>
<p class="mb-2"><label class="label" for="id_name">{% trans "Name:" %}</label> {{ form.name }}</p> <div class="field">
{% for error in form.name.errors %} <label class="label" for="id_name">{% trans "Name:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.name }}
{% endfor %} {% for error in form.name.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<p class="mb-2"> <div class="field">
<label class="label" for="id_aliases">{% trans "Aliases:" %}</label> <label class="label" for="id_aliases">{% trans "Aliases:" %}</label>
{{ form.aliases }} {{ form.aliases }}
<span class="help">{% trans "Separate multiple values with commas." %}</span> <span class="help">{% trans "Separate multiple values with commas." %}</span>
</p> {% for error in form.aliases.errors %}
{% for error in form.aliases.errors %} <p class="help is-danger">{{ error | escape }}</p>
<p class="help is-danger">{{ error | escape }}</p> {% endfor %}
{% endfor %} </div>
<p class="mb-2"><label class="label" for="id_bio">{% trans "Bio:" %}</label> {{ form.bio }}</p> <div class="field">
{% for error in form.bio.errors %} <label class="label" for="id_bio">{% trans "Bio:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.bio }}
{% endfor %} {% for error in form.bio.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<p class="mb-2"><label class="label" for="id_wikipedia_link">{% trans "Wikipedia link:" %}</label> {{ form.wikipedia_link }}</p> <p class="field"><label class="label" for="id_wikipedia_link">{% trans "Wikipedia link:" %}</label> {{ form.wikipedia_link }}</p>
{% for error in form.wikipedia_link.errors %} {% for error in form.wikipedia_link.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
<p class="mb-2"> <div class="field">
<label class="label" for="id_born">{% trans "Birth date:" %}</label> <label class="label" for="id_born">{% trans "Birth date:" %}</label>
<input type="date" name="born" value="{{ form.born.value|date:'Y-m-d' }}" class="input" id="id_born"> <input type="date" name="born" value="{{ form.born.value|date:'Y-m-d' }}" class="input" id="id_born">
</p> {% for error in form.born.errors %}
{% for error in form.born.errors %} <p class="help is-danger">{{ error | escape }}</p>
<p class="help is-danger">{{ error | escape }}</p> {% endfor %}
{% endfor %} </div>
<p class="mb-2"> <div class="field">
<label class="label" for="id_died">{% trans "Death date:" %}</label> <label class="label" for="id_died">{% trans "Death date:" %}</label>
<input type="date" name="died" value="{{ form.died.value|date:'Y-m-d' }}" class="input" id="id_died"> <input type="date" name="died" value="{{ form.died.value|date:'Y-m-d' }}" class="input" id="id_died">
</p> {% for error in form.died.errors %}
{% for error in form.died.errors %} <p class="help is-danger">{{ error | escape }}</p>
<p class="help is-danger">{{ error | escape }}</p> {% endfor %}
{% endfor %} </div>
</div> </div>
<div class="column"> <div class="column">
<h2 class="title is-4">{% trans "Author Identifiers" %}</h2> <h2 class="title is-4">{% trans "Author Identifiers" %}</h2>
<p class="mb-2"><label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label> {{ form.openlibrary_key }}</p> <div class="field">
{% for error in form.openlibrary_key.errors %} <label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.openlibrary_key }}
{% endfor %} {% for error in form.openlibrary_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<p class="mb-2"><label class="label" for="id_inventaire_id">{% trans "Inventaire ID:" %}</label> {{ form.inventaire_id }}</p> <div class="field">
{% for error in form.inventaire_id.errors %} <label class="label" for="id_inventaire_id">{% trans "Inventaire ID:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.inventaire_id }}
{% endfor %} {% for error in form.inventaire_id.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<p class="mb-2"><label class="label" for="id_librarything_key">{% trans "Librarything key:" %}</label> {{ form.librarything_key }}</p> <div class="field">
{% for error in form.librarything_key.errors %} <label class="label" for="id_librarything_key">{% trans "Librarything key:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.librarything_key }}
{% endfor %} {% for error in form.librarything_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<p class="mb-2"><label class="label" for="id_goodreads_key">{% trans "Goodreads key:" %}</label> {{ form.goodreads_key }}</p> <div class="field">
{% for error in form.goodreads_key.errors %} <label class="label" for="id_goodreads_key">{% trans "Goodreads key:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.goodreads_key }}
{% endfor %} {% for error in form.goodreads_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div> </div>
</div> </div>

View file

@ -1,35 +1,47 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %}{% load bookwyrm_tags %}{% load humanize %}{% load utilities %} {% load i18n %}{% load bookwyrm_tags %}{% load humanize %}{% load utilities %}{% load layout %}
{% block title %}{{ book.title }}{% endblock %} {% block title %}{{ book|book_title }}{% endblock %}
{% block opengraph_images %}
{% include 'snippets/opengraph_images.html' with image=book.preview_image %}
{% endblock %}
{% block content %} {% block content %}
{% with user_authenticated=request.user.is_authenticated can_edit_book=perms.bookwyrm.edit_book %} {% with user_authenticated=request.user.is_authenticated can_edit_book=perms.bookwyrm.edit_book %}
<div class="block" itemscope itemtype="https://schema.org/Book"> <div class="block" itemscope itemtype="https://schema.org/Book">
<div class="columns is-mobile"> <div class="columns is-mobile">
<div class="column"> <div class="column">
<h1 class="title"> <h1 class="title" itemprop="name">
<span itemprop="name"> {{ book.title }}
{{ book.title }}{% if book.subtitle %}:
<small>{{ book.subtitle }}</small>
{% endif %}
</span>
{% if book.series %}
<meta itemprop="isPartOf" content="{{ book.series }}">
<meta itemprop="volumeNumber" content="{{ book.series_number }}">
<small class="has-text-grey-dark">
({{ book.series }}
{% if book.series_number %} #{{ book.series_number }}{% endif %})
</small>
<br>
{% endif %}
</h1> </h1>
{% if book.subtitle or book.series %}
<p class="subtitle title is-5">
{% if book.subtitle %}
<meta
itemprop="alternativeHeadline"
content="{{ book.subtitle | escape }}"
>
<span class="has-text-weight-bold">
{{ book.subtitle }}
</span>
{% endif %}
{% if book.series %}
<meta itemprop="isPartOf" content="{{ book.series | escape }}">
<meta itemprop="volumeNumber" content="{{ book.series_number }}">
({{ book.series }}{% if book.series_number %} #{{ book.series_number }}{% endif %})
{% endif %}
</p>
{% endif %}
{% if book.authors %} {% if book.authors %}
<h2 class="subtitle"> <div class="subtitle">
{% trans "by" %} {% include 'snippets/authors.html' with book=book %} {% trans "by" %} {% include 'snippets/authors.html' with book=book %}
</h2> </div>
{% endif %} {% endif %}
</div> </div>
@ -37,7 +49,7 @@
<div class="column is-narrow"> <div class="column is-narrow">
<a href="{{ book.id }}/edit"> <a href="{{ book.id }}/edit">
<span class="icon icon-pencil" title="{% trans "Edit Book" %}" aria-hidden=True></span> <span class="icon icon-pencil" title="{% trans "Edit Book" %}" aria-hidden=True></span>
<span>{% trans "Edit Book" %}</span> <span class="is-hidden-mobile">{% trans "Edit Book" %}</span>
</a> </a>
</div> </div>
{% endif %} {% endif %}
@ -85,7 +97,7 @@
<div class="column is-three-fifths"> <div class="column is-three-fifths">
<div class="block"> <div class="block">
<h3 <div
class="field is-grouped" class="field is-grouped"
itemprop="aggregateRating" itemprop="aggregateRating"
itemscope itemscope
@ -103,7 +115,7 @@
{% plural %} {% plural %}
({{ review_count }} reviews) ({{ review_count }} reviews)
{% endblocktrans %} {% endblocktrans %}
</h3> </div>
{% with full=book|book_description itemprop='abstract' %} {% with full=book|book_description itemprop='abstract' %}
{% include 'snippets/trimmed_text.html' %} {% include 'snippets/trimmed_text.html' %}
@ -137,7 +149,7 @@
{# user's relationship to the book #} {# user's relationship to the book #}
<div class="block"> <div class="block">
{% for shelf in user_shelves %} {% for shelf in user_shelfbooks %}
<p> <p>
{% blocktrans with path=shelf.shelf.local_path shelf_name=shelf.shelf.name %}This edition is on your <a href="{{ path }}">{{ shelf_name }}</a> shelf.{% endblocktrans %} {% blocktrans with path=shelf.shelf.local_path shelf_name=shelf.shelf.name %}This edition is on your <a href="{{ path }}">{{ shelf_name }}</a> shelf.{% endblocktrans %}
{% include 'snippets/shelf_selector.html' with current=shelf.shelf %} {% include 'snippets/shelf_selector.html' with current=shelf.shelf %}
@ -181,7 +193,7 @@
<p>{% trans "You don't have any reading activity for this book." %}</p> <p>{% trans "You don't have any reading activity for this book." %}</p>
{% endif %} {% endif %}
{% for readthrough in readthroughs %} {% for readthrough in readthroughs %}
{% include 'snippets/readthrough.html' with readthrough=readthrough %} {% include 'book/readthrough.html' with readthrough=readthrough %}
{% endfor %} {% endfor %}
</section> </section>
<hr aria-hidden="true"> <hr aria-hidden="true">

View file

@ -14,11 +14,25 @@
{% endif %} {% endif %}
</h1> </h1>
{% if book %} {% if book %}
<div> <dl>
<p>{% trans "Added:" %} {{ book.created_date | naturaltime }}</p> <div class="is-flex">
<p>{% trans "Updated:" %} {{ book.updated_date | naturaltime }}</p> <dt class="has-text-weight-semibold">{% trans "Added:" %}</dt>
<p>{% trans "Last edited by:" %} <a href="{{ book.last_edited_by.remote_id }}">{{ book.last_edited_by.display_name }}</a></p> <dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
</div> </div>
<div class="is-flex">
<dt class="has-text-weight-semibold">{% trans "Updated:" %}</dt>
<dd class="ml-2">{{ book.updated_date | naturaltime }}</dd>
</div>
{% if book.last_edited_by %}
<div class="is-flex">
<dt class="has-text-weight-semibold">{% trans "Last edited by:" %}</dt>
<dd class="ml-2"><a href="{{ book.last_edited_by.remote_id }}">{{ book.last_edited_by.display_name }}</a></dd>
</div>
{% endif %}
</dl>
{% endif %} {% endif %}
</header> </header>
@ -38,21 +52,28 @@
{% if confirm_mode %} {% if confirm_mode %}
<div class="box"> <div class="box">
<h2 class="title is-4">{% trans "Confirm Book Info" %}</h2> <h2 class="title is-4">{% trans "Confirm Book Info" %}</h2>
<div class="columns"> <div class="columns mb-4">
{% if author_matches %} {% if author_matches %}
<input type="hidden" name="author-match-count" value="{{ author_matches|length }}"> <input type="hidden" name="author-match-count" value="{{ author_matches|length }}">
<div class="column is-half"> <div class="column is-half">
{% for author in author_matches %} {% for author in author_matches %}
<fieldset class="mb-4"> <fieldset>
<legend class="title is-5 mb-1">{% blocktrans with name=author.name %}Is "{{ name }}" an existing author?{% endblocktrans %}</legend> <legend class="title is-5 mb-1">
{% blocktrans with name=author.name %}Is "{{ name }}" an existing author?{% endblocktrans %}
</legend>
{% with forloop.counter0 as counter %} {% with forloop.counter0 as counter %}
{% for match in author.matches %} {% for match in author.matches %}
<label><input type="radio" name="author_match-{{ counter }}" value="{{ match.id }}" required> {{ match.name }}</label> <label class="label mb-2">
<input type="radio" name="author_match-{{ counter }}" value="{{ match.id }}" required>
{{ match.name }}
</label>
<p class="help"> <p class="help">
<a href="{{ match.local_path }}" target="_blank">{% blocktrans with book_title=match.book_set.first.title %}Author of <em>{{ book_title }}</em>{% endblocktrans %}</a> <a href="{{ match.local_path }}" target="_blank">{% blocktrans with book_title=match.book_set.first.title %}Author of <em>{{ book_title }}</em>{% endblocktrans %}</a>
</p> </p>
{% endfor %} {% endfor %}
<label><input type="radio" name="author_match-{{ counter }}" value="{{ author.name }}" required> {% trans "This is a new author" %}</label> <label class="label">
<input type="radio" name="author_match-{{ counter }}" value="{{ author.name }}" required> {% trans "This is a new author" %}
</label>
{% endwith %} {% endwith %}
</fieldset> </fieldset>
{% endfor %} {% endfor %}
@ -64,11 +85,17 @@
{% if not book %} {% if not book %}
<div class="column is-half"> <div class="column is-half">
<fieldset> <fieldset>
<legend class="title is-5 mb-1">{% trans "Is this an edition of an existing work?" %}</legend> <legend class="title is-5 mb-1">
{% trans "Is this an edition of an existing work?" %}
</legend>
{% for match in book_matches %} {% for match in book_matches %}
<label class="label"><input type="radio" name="parent_work" value="{{ match.parent_work.id }}"> {{ match.parent_work.title }}</label> <label class="label">
<input type="radio" name="parent_work" value="{{ match.parent_work.id }}"> {{ match.parent_work.title }}
</label>
{% endfor %} {% endfor %}
<label><input type="radio" name="parent_work" value="0" required> {% trans "This is a new work" %}</label> <label>
<input type="radio" name="parent_work" value="0" required> {% trans "This is a new work" %}
</label>
</fieldset> </fieldset>
</div> </div>
{% endif %} {% endif %}
@ -89,76 +116,79 @@
<section class="block"> <section class="block">
<h2 class="title is-4">{% trans "Metadata" %}</h2> <h2 class="title is-4">{% trans "Metadata" %}</h2>
<p class="mb-2"> <div class="field">
<label class="label" for="id_title">{% trans "Title:" %}</label> <label class="label" for="id_title">{% trans "Title:" %}</label>
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title"> <input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title">
</p> {% for error in form.title.errors %}
{% for error in form.title.errors %} <p class="help is-danger">{{ error | escape }}</p>
<p class="help is-danger">{{ error | escape }}</p> {% endfor %}
{% endfor %} </div>
<p class="mb-2"> <div class="field">
<label class="label" for="id_subtitle">{% trans "Subtitle:" %}</label> <label class="label" for="id_subtitle">{% trans "Subtitle:" %}</label>
<input type="text" name="subtitle" value="{{ form.subtitle.value|default:'' }}" maxlength="255" class="input" id="id_subtitle"> <input type="text" name="subtitle" value="{{ form.subtitle.value|default:'' }}" maxlength="255" class="input" id="id_subtitle">
</p> {% for error in form.subtitle.errors %}
{% for error in form.subtitle.errors %} <p class="help is-danger">{{ error | escape }}</p>
<p class="help is-danger">{{ error | escape }}</p> {% endfor %}
{% endfor %} </div>
<p class="mb-2"><label class="label" for="id_description">{% trans "Description:" %}</label> {{ form.description }} </p> <div class="field">
{% for error in form.description.errors %} <label class="label" for="id_description">{% trans "Description:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.description }}
{% endfor %} {% for error in form.description.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<p class="mb-2"> <div class="field">
<label class="label" for="id_series">{% trans "Series:" %}</label> <label class="label" for="id_series">{% trans "Series:" %}</label>
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}"> <input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}">
</p> {% for error in form.series.errors %}
{% for error in form.series.errors %} <p class="help is-danger">{{ error | escape }}</p>
<p class="help is-danger">{{ error | escape }}</p> {% endfor %}
{% endfor %} </div>
<p class="mb-2"> <div class="field">
<label class="label" for="id_series_number">{% trans "Series number:" %}</label> <label class="label" for="id_series_number">{% trans "Series number:" %}</label>
{{ form.series_number }} {{ form.series_number }}
</p> {% for error in form.series_number.errors %}
{% for error in form.series_number.errors %} <p class="help is-danger">{{ error | escape }}</p>
<p class="help is-danger">{{ error | escape }}</p> {% endfor %}
{% endfor %} </div>
<p class="mb-2"> <div class="field">
<label class="label" for="id_languages">{% trans "Languages:" %}</label> <label class="label" for="id_languages">{% trans "Languages:" %}</label>
{{ form.languages }} {{ form.languages }}
<span class="help">{% trans "Separate multiple values with commas." %}</span> <span class="help">{% trans "Separate multiple values with commas." %}</span>
</p> {% for error in form.languages.errors %}
{% for error in form.languages.errors %} <p class="help is-danger">{{ error | escape }}</p>
<p class="help is-danger">{{ error | escape }}</p> {% endfor %}
{% endfor %} </div>
<p class="mb-2"> <div class="field">
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label> <label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
{{ form.publishers }} {{ form.publishers }}
<span class="help">{% trans "Separate multiple values with commas." %}</span> <span class="help">{% trans "Separate multiple values with commas." %}</span>
</p> {% for error in form.publishers.errors %}
{% for error in form.publishers.errors %} <p class="help is-danger">{{ error | escape }}</p>
<p class="help is-danger">{{ error | escape }}</p> {% endfor %}
{% endfor %} </div>
<p class="mb-2"> <div class="field">
<label class="label" for="id_first_published_date">{% trans "First published date:" %}</label> <label class="label" for="id_first_published_date">{% trans "First published date:" %}</label>
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %}> <input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %}>
</p> {% for error in form.first_published_date.errors %}
{% for error in form.first_published_date.errors %} <p class="help is-danger">{{ error | escape }}</p>
<p class="help is-danger">{{ error | escape }}</p> {% endfor %}
{% endfor %} </div>
<p class="mb-2"> <div class="field">
<label class="label" for="id_published_date">{% trans "Published date:" %}</label> <label class="label" for="id_published_date">{% trans "Published date:" %}</label>
<input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %}> <input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %}>
</p> {% for error in form.published_date.errors %}
{% for error in form.published_date.errors %} <p class="help is-danger">{{ error | escape }}</p>
<p class="help is-danger">{{ error | escape }}</p> {% endfor %}
{% endfor %} </div>
</section> </section>
<section class="block"> <section class="block">
@ -166,16 +196,23 @@
{% if book.authors.exists %} {% if book.authors.exists %}
<fieldset> <fieldset>
{% for author in book.authors.all %} {% for author in book.authors.all %}
<label class="label mb-2"> <div class="is-flex is-justify-content-space-between">
<input type="checkbox" name="remove_authors" value="{{ author.id }}" {% if author.id|stringformat:"i" in remove_authors %}checked{% endif %}> <label class="label mb-2">
{% blocktrans with name=author.name path=author.local_path %}Remove <a href="{{ path }}">{{ name }}</a>{% endblocktrans %} <input type="checkbox" name="remove_authors" value="{{ author.id }}" {% if author.id|stringformat:"i" in remove_authors %}checked{% endif %}>
</label> {% blocktrans with name=author.name %}Remove {{ name }}{% endblocktrans %}
</label>
<p class="help">
<a href="{{ author.local_path }}">{% blocktrans with name=author.name %}Author page for {{ name }}{% endblocktrans %}</a>
</p>
</div>
{% endfor %} {% endfor %}
</fieldset> </fieldset>
{% endif %} {% endif %}
<label class="label" for="id_add_author">{% trans "Add Authors:" %}</label> <div class="field">
<input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'John Doe, Jane Smith' %}" value="{{ add_author }}" {% if confirm_mode %}readonly{% endif %}> <label class="label" for="id_add_author">{% trans "Add Authors:" %}</label>
<span class="help">{% trans "Separate multiple values with commas." %}</span> <input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'John Doe, Jane Smith' %}" value="{{ add_author }}" {% if confirm_mode %}readonly{% endif %}>
<span class="help">{% trans "Separate multiple values with commas." %}</span>
</div>
</section> </section>
</div> </div>
@ -188,17 +225,17 @@
<div class="column"> <div class="column">
<div class="block"> <div class="block">
<p> <div class="field">
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label> <label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
{{ form.cover }} {{ form.cover }}
</p> </div>
{% if book %} {% if book %}
<p> <div class="field">
<label class="label" for="id_cover_url"> <label class="label" for="id_cover_url">
{% trans "Load cover from url:" %} {% trans "Load cover from url:" %}
</label> </label>
<input class="input" name="cover-url" id="id_cover_url"> <input class="input" name="cover-url" id="id_cover_url">
</p> </div>
{% endif %} {% endif %}
{% for error in form.cover.errors %} {% for error in form.cover.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
@ -209,51 +246,72 @@
<div class="block"> <div class="block">
<h2 class="title is-4">{% trans "Physical Properties" %}</h2> <h2 class="title is-4">{% trans "Physical Properties" %}</h2>
<p class="mb-2"><label class="label" for="id_physical_format">{% trans "Format:" %}</label> {{ form.physical_format }} </p> <div class="field">
{% for error in form.physical_format.errors %} <label class="label" for="id_physical_format">{% trans "Format:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.physical_format }}
{% endfor %} {% for error in form.physical_format.errors %}
{% for error in form.physical_format.errors %} <p class="help is-danger">{{ error | escape }}</p>
<p class="help is-danger">{{ error | escape }}</p> {% endfor %}
{% endfor %} </div>
<p class="mb-2"><label class="label" for="id_pages">{% trans "Pages:" %}</label> {{ form.pages }} </p> <div class="field">
{% for error in form.pages.errors %} <label class="label" for="id_pages">{% trans "Pages:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.pages }}
{% endfor %} {% for error in form.pages.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div> </div>
<div class="block"> <div class="block">
<h2 class="title is-4">{% trans "Book Identifiers" %}</h2> <h2 class="title is-4">{% trans "Book Identifiers" %}</h2>
<p class="mb-2"><label class="label" for="id_isbn_13">{% trans "ISBN 13:" %}</label> {{ form.isbn_13 }} </p> <div class="field">
{% for error in form.isbn_13.errors %} <label class="label" for="id_isbn_13">{% trans "ISBN 13:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.isbn_13 }}
{% endfor %} {% for error in form.isbn_13.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<p class="mb-2"><label class="label" for="id_isbn_10">{% trans "ISBN 10:" %}</label> {{ form.isbn_10 }} </p> <div class="field">
{% for error in form.isbn_10.errors %} <label class="label" for="id_isbn_10">{% trans "ISBN 10:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.isbn_10 }}
{% endfor %} {% for error in form.isbn_10.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<p class="mb-2"><label class="label" for="id_openlibrary_key">{% trans "Openlibrary ID:" %}</label> {{ form.openlibrary_key }} </p> <div class="field">
{% for error in form.openlibrary_key.errors %} <label class="label" for="id_openlibrary_key">{% trans "Openlibrary ID:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.openlibrary_key }}
{% endfor %} {% for error in form.openlibrary_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<p class="mb-2"><label class="label" for="id_inventaire_id">{% trans "Inventaire ID:" %}</label> {{ form.inventaire_id }} </p> <div class="field">
{% for error in form.inventaire_id.errors %} <label class="label" for="id_inventaire_id">{% trans "Inventaire ID:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.inventaire_id }}
{% endfor %} {% for error in form.inventaire_id.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<p class="mb-2"><label class="label" for="id_oclc_number">{% trans "OCLC Number:" %}</label> {{ form.oclc_number }} </p> <div class="field">
{% for error in form.oclc_number.errors %} <label class="label" for="id_oclc_number">{% trans "OCLC Number:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.oclc_number }}
{% endfor %} {% for error in form.oclc_number.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<p class="mb-2"><label class="label" for="id_asin">{% trans "ASIN:" %}</label> {{ form.asin }} </p> <div class="field">
{% for error in form.ASIN.errors %} <label class="label" for="id_asin">{% trans "ASIN:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> {{ form.asin }}
{% endfor %} {% for error in form.ASIN.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -261,7 +319,7 @@
{% if not confirm_mode %} {% if not confirm_mode %}
<div class="block"> <div class="block">
<button class="button is-primary" type="submit">{% trans "Save" %}</button> <button class="button is-primary" type="submit">{% trans "Save" %}</button>
<a class="button" href="{{ book.local_path}}">{% trans "Cancel" %}</a> <a class="button" href="{{ book.local_path }}">{% trans "Cancel" %}</a>
</div> </div>
{% endif %} {% endif %}
</form> </form>

View file

@ -40,7 +40,7 @@
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
{% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True %} {% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True right=True %}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}

View file

@ -1,8 +1,8 @@
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
{% load tz %} {% load tz %}
<div class="content box is-shadowless has-background-white-bis"> <div class="content">
<div id="hide-edit-readthrough-{{ readthrough.id }}"> <div id="hide-edit-readthrough-{{ readthrough.id }}" class="box is-shadowless has-background-white-bis">
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
{% trans "Progress Updates:" %} {% trans "Progress Updates:" %}

View file

@ -1,7 +1,7 @@
{% load i18n %} {% load i18n %}
<div <div
role="dialog" role="dialog"
class="modal is-hidden" class="modal {% if active %}is-active{% else %}is-hidden{% endif %}"
id="{{ controls_text }}-{{ controls_uid }}" id="{{ controls_text }}-{{ controls_uid }}"
aria-labelledby="modal-card-title-{{ controls_text }}-{{ controls_uid }}" aria-labelledby="modal-card-title-{{ controls_text }}-{{ controls_uid }}"
aria-modal="true" aria-modal="true"
@ -11,7 +11,7 @@
{% trans "Close" as label %} {% trans "Close" as label %}
<div class="modal-card"> <div class="modal-card">
<header class="modal-card-head" tabindex="0" id="modal-title-{{ controls_text }}-{{ controls_uid }}"> <header class="modal-card-head" tabindex="0" id="modal-title-{{ controls_text }}-{{ controls_uid }}">
<h2 class="modal-card-title" id="modal-card-title-{{ controls_text }}-{{ controls_uid }}"> <h2 class="modal-card-title is-flex-shrink-1" id="modal-card-title-{{ controls_text }}-{{ controls_uid }}">
{% block modal-title %}{% endblock %} {% block modal-title %}{% endblock %}
</h2> </h2>
{% include 'snippets/toggle/toggle_button.html' with label=label class="delete" nonbutton=True %} {% include 'snippets/toggle/toggle_button.html' with label=label class="delete" nonbutton=True %}

View file

@ -20,8 +20,8 @@
{% csrf_token %} {% csrf_token %}
<button class="button is-primary" type="submit">Join Directory</button> <button class="button is-primary" type="submit">Join Directory</button>
<p class="help"> <p class="help">
{% url 'settings-profile' as path %} {% url 'prefs-profile' as path %}
{% blocktrans %}You can opt-out at any time in your <a href="{{ path }}">profile settings.</a>{% endblocktrans %} {% blocktrans with path=path %}You can opt-out at any time in your <a href="{{ path }}">profile settings.</a>{% endblocktrans %}
</p> </p>
</form> </form>
</div> </div>

View file

@ -5,7 +5,7 @@
<div class="select is-small mt-1 mb-3"> <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 %}"> <select name="{{ book.id }}" aria-label="{% blocktrans with book_title=book.title %}Have you read {{ book_title }}?{% endblocktrans %}">
<option disabled selected value>Add to your books</option> <option disabled selected value>Add to your books</option>
{% for shelf in request.user.shelf_set.all %} {% for shelf in user_shelves %}
<option value="{{ shelf.id }}">{{ shelf.name }}</option> <option value="{{ shelf.id }}">{{ shelf.name }}</option>
{% endfor %} {% endfor %}
</select> </select>

View file

@ -41,8 +41,8 @@
</label> </label>
</div> </div>
<div class="field"> <div class="field">
<label class="label"> <label>
<p>{% trans "Privacy setting for imported reviews:" %}</p> <span class="label">{% trans "Privacy setting for imported reviews:" %}</span>
{% include 'snippets/privacy_select.html' with no_label=True %} {% include 'snippets/privacy_select.html' with no_label=True %}
</label> </label>
</div> </div>

View file

@ -7,14 +7,19 @@
{% block content %}{% spaceless %} {% block content %}{% spaceless %}
<div class="block"> <div class="block">
<h1 class="title">{% trans "Import Status" %}</h1> <h1 class="title">{% trans "Import Status" %}</h1>
<a href="{% url 'import' %}" class="has-text-weight-normal help subtitle is-link">{% trans "Back to imports" %}</a>
<p> <dl>
{% trans "Import started:" %} {{ job.created_date | naturaltime }} <div class="is-flex">
</p> <dt class="has-text-weight-medium">{% trans "Import started:" %}</dt>
{% if job.complete %} <dd class="ml-2">{{ job.created_date | naturaltime }}</dd>
<p> </div>
{% trans "Import completed:" %} {{ task.date_done | naturaltime }} {% if job.complete %}
</p> <div class="is-flex">
<dt class="has-text-weight-medium">{% trans "Import completed:" %}</dt>
<dd class="ml-2">{{ task.date_done | naturaltime }}</dd>
</div>
</dl>
{% elif task.failed %} {% elif task.failed %}
<div class="notification is-danger">{% trans "TASK FAILED" %}</div> <div class="notification is-danger">{% trans "TASK FAILED" %}</div>
{% endif %} {% endif %}
@ -22,8 +27,9 @@
<div class="block"> <div class="block">
{% if not job.complete %} {% if not job.complete %}
{% trans "Import still in progress." %}
<p> <p>
{% trans "Import still in progress." %}
<br/>
{% trans "(Hit reload to update!)" %} {% trans "(Hit reload to update!)" %}
</p> </p>
{% endif %} {% endif %}
@ -49,16 +55,13 @@
<fieldset id="failed-imports"> <fieldset id="failed-imports">
<ul> <ul>
{% for item in failed_items %} {% for item in failed_items %}
<li class="pb-1"> <li class="mb-2 is-flex is-align-items-start">
<input class="checkbox" type="checkbox" name="import_item" value="{{ item.id }}" id="import-item-{{ item.id }}"> <input class="checkbox mt-1" type="checkbox" name="import_item" value="{{ item.id }}" id="import-item-{{ item.id }}">
<label for="import-item-{{ item.id }}"> <label class="ml-1" for="import-item-{{ item.id }}">
Line {{ item.index }}: {% blocktrans with index=item.index title=item.data.Title author=item.data.Author %}Line {{ index }}: <strong>{{ title }}</strong> by {{ author }}{% endblocktrans %}
<strong>{{ item.data.Title }}</strong> by <br/>
{{ item.data.Author }}
</label>
<p>
{{ item.fail_reason }}. {{ item.fail_reason }}.
</p> </label>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -104,7 +107,11 @@
{% endif %} {% endif %}
<div class="block"> <div class="block">
{% if job.complete %}
<h2 class="title is-4">{% trans "Successfully imported" %}</h2> <h2 class="title is-4">{% trans "Successfully imported" %}</h2>
{% else %}
<h2 class="title is-4">{% trans "Import Progress" %}</h2>
{% endif %}
<table class="table"> <table class="table">
<tr> <tr>
<th> <th>

View file

@ -8,16 +8,21 @@
<link rel="stylesheet" href="/static/css/vendor/icons.css"> <link rel="stylesheet" href="/static/css/vendor/icons.css">
<link rel="stylesheet" href="/static/css/bookwyrm.css"> <link rel="stylesheet" href="/static/css/bookwyrm.css">
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}/images/{{ site.favicon }}{% else %}/static/images/favicon.ico{% endif %}"> <link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{{ media_url }}{{ site.favicon }}{% else %}/static/images/favicon.ico{% endif %}">
{% if preview_images_enabled is True %}
<meta name="twitter:card" content="summary_large_image">
{% else %}
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
{% endif %}
<meta name="twitter:title" content="{% if title %}{{ title }} | {% endif %}{{ site.name }}"> <meta name="twitter:title" content="{% if title %}{{ title }} | {% endif %}{{ site.name }}">
<meta name="og:title" content="{% if title %}{{ title }} | {% endif %}{{ site.name }}"> <meta name="og:title" content="{% if title %}{{ title }} | {% endif %}{{ site.name }}">
<meta name="twitter:description" content="{{ site.instance_tagline }}"> <meta name="twitter:description" content="{{ site.instance_tagline }}">
<meta name="og:description" content="{{ site.instance_tagline }}"> <meta name="og:description" content="{{ site.instance_tagline }}">
<meta name="twitter:image" content="{% if site.logo %}/images/{{ site.logo }}{% else %}/static/images/logo.png{% endif %}"> {% block opengraph_images %}
<meta name="og:image" content="{% if site.logo %}/images/{{ site.logo }}{% else %}/static/images/logo.png{% endif %}"> {% include 'snippets/opengraph_images.html' %}
{% endblock %}
<meta name="twitter:image:alt" content="BookWyrm Logo"> <meta name="twitter:image:alt" content="BookWyrm Logo">
</head> </head>
<body> <body>
@ -25,7 +30,7 @@
<div class="container"> <div class="container">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item" href="/"> <a class="navbar-item" href="/">
<img class="image logo" src="{% if site.logo_small %}/images/{{ site.logo_small }}{% else %}/static/images/logo-small.png{% endif %}" alt="Home page"> <img class="image logo" src="{% if site.logo_small %}{{ media_url }}{{ site.logo_small }}{% else %}/static/images/logo-small.png{% endif %}" alt="Home page">
</a> </a>
<form class="navbar-item column" action="/search/"> <form class="navbar-item column" action="/search/">
<div class="field has-addons"> <div class="field has-addons">
@ -54,15 +59,15 @@
<div class="navbar-menu" id="main-nav"> <div class="navbar-menu" id="main-nav">
<div class="navbar-start"> <div class="navbar-start">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item">
{% trans "Your books" %}
</a>
<a href="/#feed" class="navbar-item"> <a href="/#feed" class="navbar-item">
{% trans "Feed" %} {% trans "Feed" %}
</a> </a>
<a href="{% url 'lists' %}" class="navbar-item"> <a href="{% url 'lists' %}" class="navbar-item">
{% trans "Lists" %} {% trans "Lists" %}
</a> </a>
<a href="{% url 'directory' %}" class="navbar-item">
{% trans "Directory" %}
</a>
{% endif %} {% endif %}
</div> </div>
@ -82,21 +87,16 @@
<span class="ml-2">{{ request.user.display_name }}</span> <span class="ml-2">{{ request.user.display_name }}</span>
</a> </a>
<ul class="navbar-dropdown" id="navbar-dropdown"> <ul class="navbar-dropdown" id="navbar-dropdown">
<li>
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item">
{% trans 'Your Books' %}
</a>
</li>
<li> <li>
<a href="{% url 'direct-messages' %}" class="navbar-item"> <a href="{% url 'direct-messages' %}" class="navbar-item">
{% trans "Direct Messages" %} {% trans "Direct Messages" %}
</a> </a>
</li> </li>
<li>
<a href="{% url 'directory' %}" class="navbar-item">
{% trans 'Directory' %}
</a>
</li>
<li>
<a href="{% url 'import' %}" class="navbar-item">
{% trans 'Import Books' %}
</a>
</li>
<li> <li>
<a href="{% url 'prefs-profile' %}" class="navbar-item"> <a href="{% url 'prefs-profile' %}" class="navbar-item">
{% trans 'Settings' %} {% trans 'Settings' %}
@ -134,12 +134,14 @@
<span class="is-sr-only">{% trans "Notifications" %}</span> <span class="is-sr-only">{% trans "Notifications" %}</span>
</span> </span>
</span> </span>
{% with request.user.unread_notification_count as notification_count %}
<span <span
class="{% if not request.user.unread_notification_count %}is-hidden {% elif request.user.has_unread_mentions %}is-danger {% endif %}tag is-medium transition-x" class="{% if not notification_count %}is-hidden {% elif request.user.has_unread_mentions %}is-danger {% endif %}tag is-medium transition-x"
data-poll-wrapper data-poll-wrapper
> >
<span data-poll="notifications">{{ request.user.unread_notification_count }}</span> <span data-poll="notifications">{{ notification_count }}</span>
</span> </span>
{% endwith %}
</a> </a>
</div> </div>
{% else %} {% else %}
@ -193,8 +195,11 @@
<div class="section is-flex-grow-1"> <div class="section is-flex-grow-1">
<div class="container"> <div class="container">
{# almost every view needs to know the user shelves #}
{% with request.user.shelf_set.all as user_shelves %}
{% block content %} {% block content %}
{% endblock %} {% endblock %}
{% endwith %}
</div> </div>
</div> </div>
@ -203,7 +208,7 @@
<div class="columns"> <div class="columns">
<div class="column is-one-fifth"> <div class="column is-one-fifth">
<p> <p>
<a href="{% url 'about' %}">{% trans "About this server" %}</a> <a href="{% url 'about' %}">{% trans "About this instance" %}</a>
</p> </p>
{% if site.admin_email %} {% if site.admin_email %}
<p> <p>

View file

@ -27,7 +27,7 @@
{% if not items.object_list.exists %} {% if not items.object_list.exists %}
<p>{% trans "This list is currently empty" %}</p> <p>{% trans "This list is currently empty" %}</p>
{% else %} {% else %}
<ol start="{{ items.start_index }}"> <ol start="{{ items.start_index }}" class="ordered-list">
{% for item in items %} {% for item in items %}
<li class="block mb-5"> <li class="block mb-5">
<div class="card"> <div class="card">
@ -35,16 +35,17 @@
<div <div
class=" class="
card-content p-0 mb-0 card-content p-0 mb-0
columns is-mobile is-gapless columns is-gapless
is-mobile
" "
> >
<div class="column is-2-mobile is-cover align to-t"> <div class="column is-3-mobile is-2-tablet is-cover align to-t">
<a href="{{ item.book.local_path }}" aria-hidden="true"> <a href="{{ item.book.local_path }}" aria-hidden="true">
{% include 'snippets/book_cover.html' with cover_class='is-w-auto is-h-m-tablet' %} {% include 'snippets/book_cover.html' with cover_class='is-w-auto is-h-m-tablet is-align-items-flex-start' %}
</a> </a>
</div> </div>
<div class="column ml-3"> <div class="column mx-3 my-2">
<p> <p>
{% include 'snippets/book_titleby.html' %} {% include 'snippets/book_titleby.html' %}
</p> </p>
@ -59,16 +60,19 @@
</div> </div>
{% endwith %} {% endwith %}
<div class="card-footer has-background-white-bis is-align-items-baseline"> <div class="card-footer is-stacked-mobile has-background-white-bis is-align-items-stretch">
<div class="card-footer-item"> <div class="card-footer-item">
<div> <div>
<p>{% blocktrans with username=item.user.display_name user_path=user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p> <p>{% blocktrans with username=item.user.display_name user_path=item.user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p>
</div> </div>
</div> </div>
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %} {% if list.user == request.user or list.curation == 'open' and item.user == request.user %}
<div class="card-footer-item"> <div class="card-footer-item">
<form name="set-position" method="post" action="{% url 'list-set-book-position' item.id %}"> <form name="set-position" method="post" action="{% url 'list-set-book-position' item.id %}">
<div class="field has-addons mb-0"> <div class="field has-addons mb-0">
<div class="control">
<label for="input-list-position" class="button is-transparent is-small">{% trans "List position" %}</label>
</div>
{% csrf_token %} {% csrf_token %}
<div class="control"> <div class="control">
<input id="input-list-position" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}"> <input id="input-list-position" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}">
@ -77,7 +81,6 @@
<button type="submit" class="button is-info is-small is-tablet">{% trans "Set" %}</button> <button type="submit" class="button is-info is-small is-tablet">{% trans "Set" %}</button>
</div> </div>
</div> </div>
<label for="input-list-position" class="help">{% trans "List position" %}</label>
</form> </form>
</div> </div>
<form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item"> <form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item">
@ -96,24 +99,36 @@
</section> </section>
<section class="column is-one-quarter"> <section class="column is-one-quarter">
<h2>{% trans "Sort List" %}</h2> <h2 class="title is-5">
{% trans "Sort List" %}
</h2>
<form name="sort" action="{% url 'list' list.id %}" method="GET" class="block"> <form name="sort" action="{% url 'list' list.id %}" method="GET" class="block">
<label class="label" for="id_sort_by">{% trans "Sort By" %}</label> <div class="field">
<div class="select is-fullwidth"> <label class="label" for="id_sort_by">{% trans "Sort By" %}</label>
{{ sort_form.sort_by }} <div class="select is-fullwidth">
{{ sort_form.sort_by }}
</div>
</div> </div>
<label class="label" for="id_direction">{% trans "Direction" %}</label> <div class="field">
<div class="select is-fullwidth"> <label class="label" for="id_direction">{% trans "Direction" %}</label>
{{ sort_form.direction }} <div class="select is-fullwidth">
{{ sort_form.direction }}
</div>
</div> </div>
<div> <div class="field">
<button class="button is-primary is-fullwidth" type="submit"> <button class="button is-primary is-fullwidth" type="submit">
{% trans "Sort List" %} {% trans "Sort List" %}
</button> </button>
</div> </div>
</form> </form>
{% if request.user.is_authenticated and not list.curation == 'closed' or request.user == list.user %} {% if request.user.is_authenticated and not list.curation == 'closed' or request.user == list.user %}
<h2>{% if list.curation == 'open' or request.user == list.user %}{% trans "Add Books" %}{% else %}{% trans "Suggest Books" %}{% endif %}</h2> <h2 class="title is-5 mt-6">
{% if list.curation == 'open' or request.user == list.user %}
{% trans "Add Books" %}
{% else %}
{% trans "Suggest Books" %}
{% endif %}
</h2>
<form name="search" action="{% url 'list' list.id %}" method="GET" class="block"> <form name="search" action="{% url 'list' list.id %}" method="GET" class="block">
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">

View file

@ -2,7 +2,7 @@
<div class="columns is-multiline"> <div class="columns is-multiline">
{% for list in lists %} {% for list in lists %}
<div class="column is-one-quarter"> <div class="column is-one-quarter">
<div class="card"> <div class="card is-stretchable">
<header class="card-header"> <header class="card-header">
<h4 class="card-header-title"> <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> <a href="{{ list.local_path }}">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>

View file

@ -10,7 +10,6 @@
<p class="subtitle help"> <p class="subtitle help">
{% include 'lists/created_text.html' with list=list %} {% include 'lists/created_text.html' with list=list %}
</p> </p>
{% include 'snippets/trimmed_text.html' with full=list.description %}
</div> </div>
{% if request.user == list.user %} {% if request.user == list.user %}
<div class="column is-narrow"> <div class="column is-narrow">
@ -20,6 +19,10 @@
{% endif %} {% endif %}
</header> </header>
<div class="block content">
{% include 'snippets/trimmed_text.html' with full=list.description %}
</div>
<div class="block"> <div class="block">
{% include 'lists/edit_form.html' with controls_text="edit-list" %} {% include 'lists/edit_form.html' with controls_text="edit-list" %}
</div> </div>

View file

@ -3,7 +3,7 @@
{% block title %} {% block title %}
{% if server %} {% if server %}
{% blocktrans with server_name=server.server_name %}Reports: {{ server_name }}{% endblocktrans %} {% blocktrans with instance_name=server.server_name %}Reports: {{ instance_name }}{% endblocktrans %}
{% else %} {% else %}
{% trans "Reports" %} {% trans "Reports" %}
{% endif %} {% endif %}
@ -11,7 +11,7 @@
{% block header %} {% block header %}
{% if server %} {% if server %}
{% blocktrans with server_name=server.server_name %}Reports: <small>{{ server_name }}</small>{% endblocktrans %} {% blocktrans with instance_name=server.server_name %}Reports: <small>{{ instance_name }}</small>{% endblocktrans %}
<a href="{% url 'settings-reports' %}" class="help has-text-weight-normal">Clear filters</a> <a href="{% url 'settings-reports' %}" class="help has-text-weight-normal">Clear filters</a>
{% else %} {% else %}
{% trans "Reports" %} {% trans "Reports" %}

View file

@ -56,7 +56,7 @@
<span class="icon icon-warning"></span> <span class="icon icon-warning"></span>
{% endif %} {% endif %}
</div> </div>
<div class="column"> <div class="column is-clipped">
<div class="block"> <div class="block">
<p> <p>
{# DESCRIPTION #} {# DESCRIPTION #}
@ -137,7 +137,7 @@
{# PREVIEW #} {# PREVIEW #}
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white{% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %} has-text-black{% else %}-bis has-text-grey-dark{% endif %}{% endif %}"> <div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white{% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %} has-text-black{% else %}-bis has-text-grey-dark{% endif %}{% endif %}">
<div class="columns"> <div class="columns">
<div class="column"> <div class="column is-clipped">
{% include 'snippets/status_preview.html' with status=related_status %} {% include 'snippets/status_preview.html' with status=related_status %}
</div> </div>
<div class="column is-narrow {% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %}has-text-black{% else %}has-text-grey-dark{% endif %}"> <div class="column is-narrow {% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %}has-text-black{% else %}has-text-grey-dark{% endif %}">

View file

@ -1,4 +1,4 @@
{% extends 'preferences/preferences_layout.html' %} {% extends 'preferences/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Blocked Users" %}{{ author.name }}{% endblock %} {% block title %}{% trans "Blocked Users" %}{{ author.name }}{% endblock %}
@ -18,7 +18,7 @@
<a href="{{ user.local_path }}">{% include 'snippets/avatar.html' with user=user %} {{ user.display_name }}</a> <a href="{{ user.local_path }}">{% include 'snippets/avatar.html' with user=user %} {{ user.display_name }}</a>
</p> </p>
<p class="mr-2"> <p class="mr-2">
{% include 'snippets/block_button.html' with user=user %} {% include 'snippets/block_button.html' with user=user blocks=True %}
</p> </p>
</li> </li>
{% endfor %} {% endfor %}

View file

@ -1,4 +1,4 @@
{% extends 'preferences/preferences_layout.html' %} {% extends 'preferences/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Change Password" %}{% endblock %} {% block title %}{% trans "Change Password" %}{% endblock %}

View file

@ -0,0 +1,30 @@
{% extends 'preferences/layout.html' %}
{% load i18n %}
{% block title %}{% trans "Delete Account" %}{% endblock %}
{% block header %}
{% trans "Delete Account" %}
{% endblock %}
{% block panel %}
<div class="block">
<h2 class="title is-4">{% trans "Permanently delete account" %}</h2>
<p class="notification is-danger is-light">
{% trans "Deleting your account cannot be undone. The username will not be available to register in the future." %}
</p>
<form name="delete-user" action="{% url 'prefs-delete' %}" method="post">
{% csrf_token %}
<div class="field">
<label class="label" for="id_password">{% trans "Confirm password:" %}</label>
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required>
{% for error in form.password.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<button type="submit" class="button is-danger">{% trans "Delete Account" %}</button>
</form>
</div>
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'preferences/preferences_layout.html' %} {% extends 'preferences/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Edit Profile" %}{% endblock %} {% block title %}{% trans "Edit Profile" %}{% endblock %}

View file

@ -18,6 +18,10 @@
{% url 'prefs-password' as url %} {% url 'prefs-password' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Change Password" %}</a> <a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Change Password" %}</a>
</li> </li>
<li>
{% url 'prefs-delete' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Delete Account" %}</a>
</li>
</ul> </ul>
<h2 class="menu-label">{% trans "Relationships" %}</h2> <h2 class="menu-label">{% trans "Relationships" %}</h2>
<ul class="menu-list"> <ul class="menu-list">

View file

@ -0,0 +1,14 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block title %}
{% blocktrans trimmed with book_title=book.title %}
Finish "{{ book_title }}"
{% endblocktrans %}
{% endblock %}
{% block content %}
{% include "snippets/shelve_button/finish_reading_modal.html" with book=book active=True %}
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block title %}
{% blocktrans trimmed with book_title=book.title %}
Start "{{ book_title }}"
{% endblocktrans %}
{% endblock %}
{% block content %}
{% include "snippets/shelve_button/start_reading_modal.html" with book=book active=True %}
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block title %}
{% blocktrans trimmed with book_title=book.title %}
Want to Read "{{ book_title }}"
{% endblocktrans %}
{% endblock %}
{% block content %}
{% include "snippets/shelve_button/want_to_read_modal.html" with book=book active=True no_body=True %}
{% endblock %}

View file

@ -36,7 +36,7 @@
</li> </li>
<li> <li>
{% url 'settings-federation' as url %} {% url 'settings-federation' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Federated Servers" %}</a> <a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Federated Instances" %}</a>
</li> </li>
</ul> </ul>
{% endif %} {% endif %}

View file

@ -16,8 +16,8 @@
<form class="control" action="{% url 'settings-announcements-delete' announcement.id %}" method="post"> <form class="control" action="{% url 'settings-announcements-delete' announcement.id %}" method="post">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="button is-danger"> <button type="submit" class="button is-danger">
<span class="icon icon-x" aria-hidden="true"></span> <span class="icon icon-x m-0-mobile" aria-hidden="true"></span>
<span>{% trans "Delete" %}</span> <span class="is-sr-only-mobile">{% trans "Delete" %}</span>
</button> </button>
</form> </form>
</div> </div>

View file

@ -2,7 +2,11 @@
{% load i18n %} {% load i18n %}
{% block header %} {% block header %}
{% if announcement %}
{% trans "Edit Announcement" %}
{% else %}
{% trans "Create Announcement" %} {% trans "Create Announcement" %}
{% endif %}
{% endblock %} {% endblock %}
{% block form %} {% block form %}
@ -24,7 +28,7 @@
</p> </p>
<p> <p>
<label class="label" for="id_event_date">Event date:</label> <label class="label" for="id_event_date">Event date:</label>
<input type="date" name="event_date" value="{{ form.event_date.value }}" class="input" id="id_event_date"> <input type="date" name="event_date" value="{{ form.event_date.value|date:'Y-m-d' }}" class="input" id="id_event_date">
{% for error in form.event_date.errors %} {% for error in form.event_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
@ -34,7 +38,7 @@
<div class="column"> <div class="column">
<p> <p>
<label class="label" for="id_start_date">Start date:</label> <label class="label" for="id_start_date">Start date:</label>
<input type="date" name="start_date" class="input" id="id_start_date"> <input type="date" name="start_date" class="input" value="{{ form.start_date.value|date:'Y-m-d' }}" id="id_start_date">
{% for error in form.start_date.errors %} {% for error in form.start_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
@ -43,7 +47,7 @@
<div class="column"> <div class="column">
<p> <p>
<label class="label" for="id_end_date">End date:</label> <label class="label" for="id_end_date">End date:</label>
<input type="date" name="end_date" class="input" id="id_end_date"> <input type="date" name="end_date" class="input" id="id_end_date" value="{{ form.end_date.value|date:'Y-m-d' }}">
{% for error in form.end_date.errors %} {% for error in form.end_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}

View file

@ -1,10 +1,10 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/admin_layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Add server" %}{% endblock %} {% block title %}{% trans "Add instance" %}{% endblock %}
{% block header %} {% block header %}
{% trans "Add server" %} {% trans "Add instance" %}
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to server list" %}</a> <a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to instance list" %}</a>
{% endblock %} {% endblock %}
{% block panel %} {% block panel %}
@ -17,7 +17,7 @@
</li> </li>
{% url 'settings-add-federated-server' as url %} {% url 'settings-add-federated-server' as url %}
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}> <li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Add server" %}</a> <a href="{{ url }}">{% trans "Add instance" %}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -26,14 +26,14 @@
{% csrf_token %} {% csrf_token %}
<div class="columns"> <div class="columns">
<div class="column is-half"> <div class="column is-half">
<div> <div class="field">
<label class="label" for="id_server_name">{% trans "Instance:" %}</label> <label class="label" for="id_server_name">{% trans "Instance:" %}</label>
<input type="text" name="server_name" maxlength="255" class="input" required id="id_server_name" value="{{ form.server_name.value|default:'' }}" placeholder="domain.com"> <input type="text" name="server_name" maxlength="255" class="input" required id="id_server_name" value="{{ form.server_name.value|default:'' }}" placeholder="domain.com">
{% for error in form.server_name.errors %} {% for error in form.server_name.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</div> </div>
<div> <div class="field">
<label class="label" for="id_status">{% trans "Status:" %}</label> <label class="label" for="id_status">{% trans "Status:" %}</label>
<div class="select"> <div class="select">
<select name="status" class="" id="id_status"> <select name="status" class="" id="id_status">
@ -44,14 +44,14 @@
</div> </div>
</div> </div>
<div class="column is-half"> <div class="column is-half">
<div> <div class="field">
<label class="label" for="id_application_type">{% trans "Software:" %}</label> <label class="label" for="id_application_type">{% trans "Software:" %}</label>
<input type="text" name="application_type" maxlength="255" class="input" id="id_application_type" value="{{ form.application_type.value|default:'' }}"> <input type="text" name="application_type" maxlength="255" class="input" id="id_application_type" value="{{ form.application_type.value|default:'' }}">
{% for error in form.application_type.errors %} {% for error in form.application_type.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</div> </div>
<div> <div class="field">
<label class="label" for="id_application_version">{% trans "Version:" %}</label> <label class="label" for="id_application_version">{% trans "Version:" %}</label>
<input type="text" name="application_version" maxlength="255" class="input" id="id_application_version" value="{{ form.application_version.value|default:'' }}"> <input type="text" name="application_version" maxlength="255" class="input" id="id_application_version" value="{{ form.application_version.value|default:'' }}">
{% for error in form.application_version.errors %} {% for error in form.application_version.errors %}
@ -60,10 +60,10 @@
</div> </div>
</div> </div>
</div> </div>
<p> <div class="field">
<label class="label" for="id_notes">{% trans "Notes:" %}</label> <label class="label" for="id_notes">{% trans "Notes:" %}</label>
<textarea name="notes" cols="None" rows="None" class="textarea" id="id_notes">{{ form.notes.value|default:'' }}</textarea> <textarea name="notes" cols="None" rows="None" class="textarea" id="id_notes">{{ form.notes.value|default:'' }}</textarea>
</p> </div>
<button type="submit" class="button is-primary">{% trans "Save" %}</button> <button type="submit" class="button is-primary">{% trans "Save" %}</button>
</form> </form>

View file

@ -1,13 +1,13 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/admin_layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Federated Servers" %}{% endblock %} {% block title %}{% trans "Federated Instances" %}{% endblock %}
{% block header %}{% trans "Federated Servers" %}{% endblock %} {% block header %}{% trans "Federated Instances" %}{% endblock %}
{% block edit-button %} {% block edit-button %}
<a href="{% url 'settings-import-blocklist' %}"> <a href="{% url 'settings-import-blocklist' %}">
<span class="icon icon-plus" title="{% trans 'Add server' %}" aria-hidden="True"></span> <span class="icon icon-plus" title="{% trans 'Add instance' %}" aria-hidden="True"></span>
<span>{% trans "Add server" %}</span> <span class="is-hidden-mobile">{% trans "Add instance" %}</span>
</a> </a>
{% endblock %} {% endblock %}
@ -16,7 +16,7 @@
<tr> <tr>
{% url 'settings-federation' as url %} {% url 'settings-federation' as url %}
<th> <th>
{% trans "Server name" as text %} {% trans "Instance name" as text %}
{% include 'snippets/table-sort-header.html' with field="server_name" sort=sort text=text %} {% include 'snippets/table-sort-header.html' with field="server_name" sort=sort text=text %}
</th> </th>
<th> <th>

View file

@ -1,10 +1,10 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/admin_layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Add server" %}{% endblock %} {% block title %}{% trans "Add instance" %}{% endblock %}
{% block header %} {% block header %}
{% trans "Import Blocklist" %} {% trans "Import Blocklist" %}
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to server list" %}</a> <a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to instance list" %}</a>
{% endblock %} {% endblock %}
{% block panel %} {% block panel %}
@ -17,7 +17,7 @@
</li> </li>
{% url 'settings-add-federated-server' as url %} {% url 'settings-add-federated-server' as url %}
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}> <li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Add server" %}</a> <a href="{{ url }}">{% trans "Add instance" %}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -51,7 +51,7 @@
<pre> <pre>
[ [
{ {
"instance": "example.server.com", "instance": "example.instance.com",
"url": "https://link.to.more/info" "url": "https://link.to.more/info"
}, },
... ...

View file

@ -11,23 +11,23 @@
{% csrf_token %} {% csrf_token %}
<section class="block" id="instance-info"> <section class="block" id="instance-info">
<h2 class="title is-4">{% trans "Instance Info" %}</h2> <h2 class="title is-4">{% trans "Instance Info" %}</h2>
<div class="control"> <div class="field">
<label class="label" for="id_name">{% trans "Instance Name:" %}</label> <label class="label" for="id_name">{% trans "Instance Name:" %}</label>
{{ site_form.name }} {{ site_form.name }}
</div> </div>
<div class="control"> <div class="field">
<label class="label" for="id_instance_tagline">{% trans "Tagline:" %}</label> <label class="label" for="id_instance_tagline">{% trans "Tagline:" %}</label>
{{ site_form.instance_tagline }} {{ site_form.instance_tagline }}
</div> </div>
<div class="control"> <div class="field">
<label class="label" for="id_instance_description">{% trans "Instance description:" %}</label> <label class="label" for="id_instance_description">{% trans "Instance description:" %}</label>
{{ site_form.instance_description }} {{ site_form.instance_description }}
</div> </div>
<div class="control"> <div class="field">
<label class="label" for="id_code_of_conduct">{% trans "Code of conduct:" %}</label> <label class="label" for="id_code_of_conduct">{% trans "Code of conduct:" %}</label>
{{ site_form.code_of_conduct }} {{ site_form.code_of_conduct }}
</div> </div>
<div class="control"> <div class="field">
<label class="label" for="id_privacy_policy">{% trans "Privacy Policy:" %}</label> <label class="label" for="id_privacy_policy">{% trans "Privacy Policy:" %}</label>
{{ site_form.privacy_policy }} {{ site_form.privacy_policy }}
</div> </div>
@ -57,19 +57,19 @@
<section class="block" id="footer"> <section class="block" id="footer">
<h2 class="title is-4">{% trans "Footer Content" %}</h2> <h2 class="title is-4">{% trans "Footer Content" %}</h2>
<div class="control"> <div class="field">
<label class="label" for="id_support_link">{% trans "Support link:" %}</label> <label class="label" for="id_support_link">{% trans "Support link:" %}</label>
<input type="text" name="support_link" maxlength="255" class="input" id="id_support_link" placeholder="https://www.patreon.com/bookwyrm"{% if site.support_link %} value="{{ site.support_link }}"{% endif %}> <input type="text" name="support_link" maxlength="255" class="input" id="id_support_link" placeholder="https://www.patreon.com/bookwyrm"{% if site.support_link %} value="{{ site.support_link }}"{% endif %}>
</div> </div>
<div class="control"> <div class="field">
<label class="label" for="id_support_title">{% trans "Support title:" %}</label> <label class="label" for="id_support_title">{% trans "Support title:" %}</label>
<input type="text" name="support_title" maxlength="100" class="input" id="id_support_title" placeholder="Patreon"{% if site.support_title %} value="{{ site.support_title }}"{% endif %}> <input type="text" name="support_title" maxlength="100" class="input" id="id_support_title" placeholder="Patreon"{% if site.support_title %} value="{{ site.support_title }}"{% endif %}>
</div> </div>
<div class="control"> <div class="field">
<label class="label" for="id_admin_email">{% trans "Admin email:" %}</label> <label class="label" for="id_admin_email">{% trans "Admin email:" %}</label>
{{ site_form.admin_email }} {{ site_form.admin_email }}
</div> </div>
<div class="control"> <div class="field">
<label class="label" for="id_footer_item">{% trans "Additional info:" %}</label> <label class="label" for="id_footer_item">{% trans "Additional info:" %}</label>
{{ site_form.footer_item }} {{ site_form.footer_item }}
</div> </div>
@ -79,15 +79,19 @@
<section class="block" id="registration"> <section class="block" id="registration">
<h2 class="title is-4">{% trans "Registration" %}</h2> <h2 class="title is-4">{% trans "Registration" %}</h2>
<div class="control"> <div class="field">
<label class="label" for="id_allow_registration">{% trans "Allow registration:" %} <label class="label" for="id_allow_registration">
{{ site_form.allow_registration }} {{ site_form.allow_registration }}
{% trans "Allow registration" %}
</label>
</div> </div>
<div class="control"> <div class="field">
<label class="label" for="id_allow_invite_requests">{% trans "Allow invite requests:" %} <label class="label" for="id_allow_invite_requests">
{{ site_form.allow_invite_requests }} {{ site_form.allow_invite_requests }}
{% trans "Allow invite requests" %}
</label>
</div> </div>
<div class="control"> <div class="field">
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label> <label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
{{ site_form.registration_closed_text }} {{ site_form.registration_closed_text }}
</div> </div>

View file

@ -4,7 +4,7 @@
class="notification mb-1 p-3{% if not admin_mode %} is-hidden{% endif %} transition-y" class="notification mb-1 p-3{% if not admin_mode %} is-hidden{% endif %} transition-y"
{% if not admin_mode %}data-hide="hide-announcement-{{ announcement.id }}"{% endif %} {% if not admin_mode %}data-hide="hide-announcement-{{ announcement.id }}"{% endif %}
> >
<div class="columns mb-0"> <div class="columns mb-0 is-mobile">
<div class="column pb-0"> <div class="column pb-0">
{% if announcement.event_date %} {% if announcement.event_date %}
<strong>{{ announcement.event_date|naturalday|title }}:</strong> <strong>{{ announcement.event_date|naturalday|title }}:</strong>

View file

@ -1,5 +1,5 @@
{% load i18n %} {% load i18n %}
{% if not user in request.user.blocks.all %} {% if not blocks %}
<form name="blocks" method="post" action="/block/{{ user.id }}"> <form name="blocks" method="post" action="/block/{{ user.id }}">
{% csrf_token %} {% csrf_token %}
<button class="button is-danger is-light is-small {{ class }}" type="submit">{% trans "Block" %}</button> <button class="button is-danger is-light is-small {{ class }}" type="submit">{% trans "Block" %}</button>

View file

@ -3,14 +3,31 @@
{% load i18n %} {% load i18n %}
{% with status.id|uuid as uuid %} {% with status.id|uuid as uuid %}
<form name="boost" action="/boost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} {% if request.user|boosted:status %}is-hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}"> {% with request.user|boosted:status as boosted %}
<form
name="boost"
action="/boost/{{ status.id }}"
method="post"
class="interaction boost-{{ status.id }}-{{ uuid }} {% if boosted %}is-hidden{% endif %}"
data-id="boost-{{ status.id }}-{{ uuid }}"
>
{% csrf_token %} {% csrf_token %}
<button class="button is-small is-light is-transparent" type="submit" {% if not status.boostable %}disabled{% endif %}> <button
class="button is-small is-light is-transparent"
type="submit"
{% if not status.boostable %}disabled{% endif %}
>
<span class="icon icon-boost m-0-mobile" title="{% trans 'Boost' %}"></span> <span class="icon icon-boost m-0-mobile" title="{% trans 'Boost' %}"></span>
<span class="is-sr-only-mobile">{% trans "Boost" %}</span> <span class="is-sr-only-mobile">{% trans "Boost" %}</span>
</button> </button>
</form> </form>
<form name="unboost" action="/unboost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} active {% if not request.user|boosted:status %}is-hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}"> <form
name="unboost"
action="/unboost/{{ status.id }}"
method="post"
class="interaction boost-{{ status.id }}-{{ uuid }} active {% if not boosted %}is-hidden{% endif %}"
data-id="boost-{{ status.id }}-{{ uuid }}"
>
{% csrf_token %} {% csrf_token %}
<button class="button is-small is-light is-transparent" type="submit"> <button class="button is-small is-light is-transparent" type="submit">
<span class="icon icon-boost has-text-primary m-0-mobile" title="{% trans 'Un-boost' %}"></span> <span class="icon icon-boost has-text-primary m-0-mobile" title="{% trans 'Un-boost' %}"></span>
@ -18,3 +35,4 @@
</button> </button>
</form> </form>
{% endwith %} {% endwith %}
{% endwith %}

View file

@ -3,7 +3,8 @@
{% load i18n %} {% load i18n %}
{% with status.id|uuid as uuid %} {% with status.id|uuid as uuid %}
<form name="favorite" action="/favorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} {% if request.user|liked:status %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}"> {% 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 }}">
{% csrf_token %} {% csrf_token %}
<button class="button is-small is-light is-transparent" type="submit"> <button class="button is-small is-light is-transparent" type="submit">
<span class="icon icon-heart m-0-mobile" title="{% trans 'Like' %}"> <span class="icon icon-heart m-0-mobile" title="{% trans 'Like' %}">
@ -11,7 +12,7 @@
<span class="is-sr-only-mobile">{% trans "Like" %}</span> <span class="is-sr-only-mobile">{% trans "Like" %}</span>
</button> </button>
</form> </form>
<form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} active {% if not request.user|liked:status %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}"> <form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} active {% if not liked %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
{% csrf_token %} {% csrf_token %}
<button class="button is-light is-transparent is-small" type="submit"> <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> <span class="icon icon-heart has-text-primary m-0-mobile" title="{% trans 'Un-like' %}"></span>
@ -19,3 +20,4 @@
</button> </button>
</form> </form>
{% endwith %} {% endwith %}
{% endwith %}

View file

@ -1,7 +1,7 @@
{% load i18n %} {% load i18n %}
{% if request.user == user or not request.user.is_authenticated %} {% if request.user == user or not request.user.is_authenticated %}
{% elif user in request.user.blocks.all %} {% elif user in request.user.blocks.all %}
{% include 'snippets/block_button.html' %} {% include 'snippets/block_button.html' with blocks=True %}
{% else %} {% else %}
<div class="field{% if not minimal %} has-addons{% else %} mb-0{% endif %}"> <div class="field{% if not minimal %} has-addons{% else %} mb-0{% endif %}">

View file

@ -11,7 +11,7 @@
class="is-sr-only" class="is-sr-only"
type="radio" type="radio"
name="rating" name="rating"
value="0" value=""
{% if default_rating == 0 or not default_rating %}checked{% endif %} {% if default_rating == 0 or not default_rating %}checked{% endif %}
> >

View file

@ -1,16 +1,19 @@
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
{% with goal.progress as progress %}
<p> <p>
{% if goal.progress_percent >= 100 %} {% if progress.percent >= 100 %}
{% trans "Success!" %} {% trans "Success!" %}
{% elif goal.progress_percent %} {% elif progress.percent %}
{% blocktrans with percent=goal.progress_percent %}{{ percent }}% complete!{% endblocktrans %} {% blocktrans with percent=progress.percent %}{{ percent }}% complete!{% endblocktrans %}
{% endif %} {% endif %}
{% if goal.user == request.user %} {% if goal.user == request.user %}
{% blocktrans with read_count=goal.book_count|intcomma goal_count=goal.goal|intcomma path=goal.local_path %}You've read <a href="{{ path }}">{{ read_count }} of {{ goal_count}} books</a>.{% endblocktrans %} {% blocktrans with read_count=progress.count|intcomma goal_count=goal.goal|intcomma path=goal.local_path %}You've read <a href="{{ path }}">{{ read_count }} of {{ goal_count}} books</a>.{% endblocktrans %}
{% else %} {% else %}
{% blocktrans with username=goal.user.display_name read_count=goal.book_count|intcomma goal_count=goal.goal|intcomma path=goal.local_path %}{{ username }} has read <a href="{{ path }}">{{ read_count }} of {{ goal_count}} books</a>.{% endblocktrans %} {% blocktrans with username=goal.user.display_name read_count=progress.count|intcomma goal_count=goal.goal|intcomma path=goal.local_path %}{{ username }} has read <a href="{{ path }}">{{ read_count }} of {{ goal_count}} books</a>.{% endblocktrans %}
{% endif %} {% endif %}
</p> </p>
<progress class="progress is-large" value="{{ goal.book_count }}" max="{{ goal.goal }}" aria-hidden="true">{{ goal.progress_percent }}%</progress> <progress class="progress is-large" value="{{ progress.count }}" max="{{ goal.goal }}" aria-hidden="true">{{ progress.percent }}%</progress>
{% endwith %}

View file

@ -0,0 +1,12 @@
{% if preview_images_enabled is True %}
{% if image %}
<meta name="twitter:image" content="{{ request.scheme }}://{{ media_path }}{{ image }}">
<meta name="og:image" content="{{ request.scheme }}://{{ media_path }}{{ image }}">
{% else %}
<meta name="twitter:image" content="{{ request.scheme }}://{{ media_path }}{{ site.preview_image }}">
<meta name="og:image" content="{{ request.scheme }}://{{ media_path }}{{ site.preview_image }}">
{% endif %}
{% else %}
<meta name="twitter:image" content="{{ request.scheme }}://{% if site.logo %}{{ media_path }}{{ site.logo }}{% else %}{{ static_path }}/images/logo.png{% endif %}">
<meta name="og:image" content="{{ request.scheme }}://{% if site.logo %}{{ media_path }}{{ site.logo }}{% else %}{{ static_path }}/images/logo.png{% endif %}">
{% endif %}

View file

@ -3,10 +3,10 @@
<input type="hidden" name="id" value="{{ readthrough.id }}"> <input type="hidden" name="id" value="{{ readthrough.id }}">
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<div class="field"> <div class="field">
<label class="label" tabindex="0" id="add-readthrough-focus"> <label class="label" tabindex="0" id="add-readthrough-focus" for="id_start_date-{{ readthrough.id }}">
{% trans "Started reading" %} {% trans "Started reading" %}
<input type="date" name="start_date" class="input" id="id_start_date-{{ readthrough.id }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
</label> </label>
<input type="date" name="start_date" class="input" id="id_start_date-{{ readthrough.id }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
</div> </div>
{# Only show progress for editing existing readthroughs #} {# Only show progress for editing existing readthroughs #}
{% if readthrough.id and not readthrough.finish_date %} {% if readthrough.id and not readthrough.finish_date %}
@ -26,8 +26,8 @@
</div> </div>
{% endif %} {% endif %}
<div class="field"> <div class="field">
<label class="label"> <label class="label" for="id_finish_date-{{ readthrough.id }}">
{% trans "Finished reading" %} {% trans "Finished reading" %}
<input type="date" name="finish_date" class="input" id="id_finish_date-{{ readthrough.id }}" value="{{ readthrough.finish_date | date:"Y-m-d" }}">
</label> </label>
<input type="date" name="finish_date" class="input" id="id_finish_date-{{ readthrough.id }}" value="{{ readthrough.finish_date | date:"Y-m-d" }}">
</div> </div>

View file

@ -6,7 +6,7 @@
{% endblock %} {% endblock %}
{% block dropdown-list %} {% block dropdown-list %}
{% for shelf in request.user.shelf_set.all %} {% for shelf in user_shelves %}
<li role="menuitem" class="dropdown-item p-0"> <li role="menuitem" class="dropdown-item p-0">
<form name="shelve" action="/shelve/" method="post"> <form name="shelve" action="/shelve/" method="post">
{% csrf_token %} {% csrf_token %}

View file

@ -7,7 +7,7 @@
{% block modal-form-open %} {% block modal-form-open %}
<form name="finish-reading" action="/finish-reading/{{ book.id }}" method="post"> <form name="finish-reading" action="{% url 'reading-status' 'finish' book.id %}" method="post">
{% endblock %} {% endblock %}
{% block modal-body %} {% block modal-body %}
@ -15,16 +15,16 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}"> <input type="hidden" name="id" value="{{ readthrough.id }}">
<div class="field"> <div class="field">
<label class="label"> <label class="label" for="finish_id_start_date-{{ uuid }}">
{% trans "Started reading" %} {% trans "Started reading" %}
<input type="date" name="start_date" class="input" id="finish_id_start_date-{{ uuid }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
</label> </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="field"> <div class="field">
<label class="label"> <label class="label" for="id_finish_date-{{ uuid }}">
{% trans "Finished reading" %} {% trans "Finished reading" %}
<input type="date" name="finish_date" class="input" id="id_finish_date-{{ uuid }}" value="{% now "Y-m-d" %}">
</label> </label>
<input type="date" name="finish_date" class="input" id="id_finish_date-{{ uuid }}" value="{% now "Y-m-d" %}">
</div> </div>
</section> </section>
{% endblock %} {% endblock %}
@ -38,7 +38,7 @@
</label> </label>
{% include 'snippets/privacy_select.html' %} {% include 'snippets/privacy_select.html' %}
</div> </div>
<div class="column"> <div class="column has-text-right">
<button type="submit" class="button is-success">{% trans "Save" %}</button> <button type="submit" class="button is-success">{% trans "Save" %}</button>
{% trans "Cancel" as button_text %} {% trans "Cancel" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="finish-reading" controls_uid=uuid %} {% include 'snippets/toggle/close_button.html' with text=button_text controls_text="finish-reading" controls_uid=uuid %}

View file

@ -13,7 +13,7 @@
</div> </div>
{% else %} {% else %}
<div class="control"> <div class="control">
{% include 'snippets/shelve_button/shelve_button_options.html' with class="shelf-option is-small" shelves=request.user.shelf_set.all active_shelf=active_shelf button_uuid=uuid %} {% include 'snippets/shelve_button/shelve_button_options.html' with class="shelf-option is-small" shelves=user_shelves active_shelf=active_shelf button_uuid=uuid %}
</div> </div>
{% include 'snippets/shelve_button/shelve_button_dropdown.html' with class="is-small" button_uuid=uuid%} {% include 'snippets/shelve_button/shelve_button_dropdown.html' with class="is-small" button_uuid=uuid%}
{% endif %} {% endif %}
@ -25,7 +25,7 @@
{% include 'snippets/shelve_button/finish_reading_modal.html' with book=active_shelf.book controls_text="finish-reading" controls_uid=uuid readthrough=readthrough %} {% include 'snippets/shelve_button/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=shelf_book.book controls_text="progress-update" 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 %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}

View file

@ -7,5 +7,5 @@
{% endblock %} {% endblock %}
{% block dropdown-list %} {% block dropdown-list %}
{% include 'snippets/shelve_button/shelve_button_options.html' with active_shelf=active_shelf shelves=request.user.shelf_set.all dropdown=True class="shelf-option is-fullwidth is-small is-radiusless is-white" %} {% include 'snippets/shelve_button/shelve_button_options.html' with active_shelf=active_shelf shelves=user_shelves dropdown=True class="shelf-option is-fullwidth is-small is-radiusless is-white" %}
{% endblock %} {% endblock %}

View file

@ -7,16 +7,25 @@
{% if dropdown %}<li role="menuitem" class="dropdown-item p-0">{% endif %} {% if dropdown %}<li role="menuitem" class="dropdown-item p-0">{% endif %}
<div class="{% if not dropdown and active_shelf.shelf.identifier|next_shelf != shelf.identifier %}is-hidden{% endif %}"> <div class="{% if not dropdown and active_shelf.shelf.identifier|next_shelf != shelf.identifier %}is-hidden{% endif %}">
{% if shelf.identifier == 'reading' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %} {% if shelf.identifier == 'reading' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
{% trans "Start reading" as button_text %} {% trans "Start reading" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="start-reading" controls_uid=button_uuid focus="modal-title-start-reading" disabled=is_current %} {% url 'reading-status' 'start' book.id as fallback_url %}
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="start-reading" controls_uid=button_uuid focus="modal-title-start-reading" disabled=is_current fallback_url=fallback_url %}
{% endif %}{% elif shelf.identifier == 'read' and active_shelf.shelf.identifier == 'read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %} {% 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>
{% endif %}{% elif shelf.identifier == 'read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %} {% endif %}{% elif shelf.identifier == 'read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
{% trans "Finish reading" as button_text %} {% trans "Finish reading" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="finish-reading" controls_uid=button_uuid focus="modal-title-finish-reading" disabled=is_current %} {% url 'reading-status' 'finish' book.id as fallback_url %}
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="finish-reading" controls_uid=button_uuid focus="modal-title-finish-reading" disabled=is_current fallback_url=fallback_url %}
{% endif %}{% elif shelf.identifier == 'to-read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %} {% endif %}{% elif shelf.identifier == 'to-read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
{% trans "Want to read" as button_text %} {% trans "Want to read" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="want-to-read" controls_uid=button_uuid focus="modal-title-want-to-read" disabled=is_current %} {% url 'reading-status' 'want' book.id as fallback_url %}
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="want-to-read" controls_uid=button_uuid focus="modal-title-want-to-read" disabled=is_current fallback_url=fallback_url %}
{% endif %}{% elif shelf.editable %} {% endif %}{% elif shelf.editable %}
<form name="shelve" action="/shelve/" method="post"> <form name="shelve" action="/shelve/" method="post">
{% csrf_token %} {% csrf_token %}
@ -44,7 +53,9 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}"> <input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}"> <input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}">
<button class="button is-fullwidth is-small{% if dropdown %} is-radiusless{% endif %} is-danger is-light" type="submit">{% blocktrans with name=active_shelf.shelf.name %}Remove from {{ name }}{% endblocktrans %}</button> <button class="button is-fullwidth is-small{% if dropdown %} is-radiusless{% endif %} is-danger is-light" type="submit">
{% blocktrans with name=active_shelf.shelf.name %}Remove from {{ name }}{% endblocktrans %}
</button>
</form> </form>
</li> </li>
{% endif %} {% endif %}

View file

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

View file

@ -6,7 +6,7 @@
{% endblock %} {% endblock %}
{% block modal-form-open %} {% block modal-form-open %}
<form name="shelve" action="/shelve/" method="post"> <form name="shelve" action="{% url 'reading-status' 'want' book.id %}" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}"> <input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<input type="hidden" name="shelf" value="to-read"> <input type="hidden" name="shelf" value="to-read">

View file

@ -15,7 +15,7 @@
<div class="card-footer-item"> <div class="card-footer-item">
{# moderation options #} {# moderation options #}
<form name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post"> <form name="delete-{{ status.id }}" action="/delete-status/{{ status.id }}" method="post">
{% csrf_token %} {% csrf_token %}
<button class="button is-danger is-light" type="submit"> <button class="button is-danger is-light" type="submit">
{% trans "Delete status" %} {% trans "Delete status" %}

View file

@ -11,7 +11,7 @@
{% if status.user == request.user %} {% if status.user == request.user %}
{# things you can do to your own statuses #} {# things you can do to your own statuses #}
<li role="menuitem" class="dropdown-item p-0"> <li role="menuitem" class="dropdown-item p-0">
<form name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post"> <form name="delete-{{ status.id }}" action="/delete-status/{{ status.id }}" method="post">
{% csrf_token %} {% csrf_token %}
<button class="button is-radiusless is-danger is-light is-fullwidth is-small" type="submit"> <button class="button is-radiusless is-danger is-light is-fullwidth is-small" type="submit">
{% trans "Delete status" %} {% trans "Delete status" %}
@ -20,7 +20,7 @@
</li> </li>
{% if status.status_type != 'GeneratedNote' and status.status_type != 'Rating' %} {% if status.status_type != 'GeneratedNote' and status.status_type != 'Rating' %}
<li role="menuitem" class="dropdown-item p-0"> <li role="menuitem" class="dropdown-item p-0">
<form class="" name="delete-{{status.id}}" action="{% url 'redraft' status.id %}" method="post"> <form class="" name="delete-{{ status.id }}" action="{% url 'redraft' status.id %}" method="post">
{% csrf_token %} {% csrf_token %}
<button class="button is-radiusless is-danger is-light is-fullwidth is-small" type="submit"> <button class="button is-radiusless is-danger is-light is-fullwidth is-small" type="submit">
{% trans "Delete & re-draft" %} {% trans "Delete & re-draft" %}
@ -39,7 +39,7 @@
{% include 'snippets/report_button.html' with user=status.user status=status %} {% include 'snippets/report_button.html' with user=status.user status=status %}
</li> </li>
<li role="menuitem" class="dropdown-item p-0"> <li role="menuitem" class="dropdown-item p-0">
{% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %} {% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" blocks=False %}
</li> </li>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -1,5 +1,12 @@
{% if fallback_url %}
<form name="fallback-form-{{ controls_uuid}}" method="GET" action="{{ fallback_url }}">
{% endif %}
<button <button
type="button" {% if not fallback_url %}
type="button"
{% else %}
type="submit"
{% endif %}
class="{% if not nonbutton %}button {% endif %}{{ class }}{% if button_type %} {{ button_type }}{% endif %}" class="{% if not nonbutton %}button {% endif %}{{ class }}{% if button_type %} {{ button_type }}{% endif %}"
data-controls="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}" data-controls="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}"
{% if focus %}data-focus-target="{{ focus }}{% if controls_uid %}-{{ controls_uid }}{% endif %}"{% endif %} {% if focus %}data-focus-target="{{ focus }}{% if controls_uid %}-{{ controls_uid }}{% endif %}"{% endif %}
@ -20,3 +27,6 @@
<span>{{ text }}</span> <span>{{ text }}</span>
{% endif %} {% endif %}
</button> </button>
{% if fallback_url %}
</form>
{% endif %}

View file

@ -16,6 +16,6 @@
{% include 'snippets/report_button.html' with user=user class="is-fullwidth" %} {% include 'snippets/report_button.html' with user=user class="is-fullwidth" %}
</li> </li>
<li role="menuitem"> <li role="menuitem">
{% include 'snippets/block_button.html' with user=user class="is-fullwidth" %} {% include 'snippets/block_button.html' with user=user class="is-fullwidth" blocks=False %}
</li> </li>
{% endblock %} {% endblock %}

View file

@ -3,9 +3,14 @@
{% load humanize %} {% load humanize %}
{% load utilities %} {% load utilities %}
{% load markdown %} {% load markdown %}
{% load layout %}
{% block title %}{{ user.display_name }}{% endblock %} {% block title %}{{ user.display_name }}{% endblock %}
{% block opengraph_images %}
{% include 'snippets/opengraph_images.html' with image=user.preview_image %}
{% endblock %}
{% block content %} {% block content %}
<header class="block"> <header class="block">
{% block header %} {% block header %}

View file

@ -37,6 +37,7 @@
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Create shelf" as button_text %} {% trans "Create shelf" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="plus" controls_text="create-shelf-form" focus="create-shelf-form-header" %} {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="plus" controls_text="create-shelf-form" focus="create-shelf-form-header" %}
<a class="button" href="{% url 'import' %}">{% trans "Import Books" %}</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View file

@ -13,7 +13,7 @@
<div class="column is-narrow"> <div class="column is-narrow">
<a href="{% url 'prefs-profile' %}"> <a href="{% url 'prefs-profile' %}">
<span class="icon icon-pencil" title="Edit profile" aria-hidden="true"></span> <span class="icon icon-pencil" title="Edit profile" aria-hidden="true"></span>
<span>{% trans "Edit profile" %}</span> <span class="is-hidden-mobile">{% trans "Edit profile" %}</span>
</a> </a>
</div> </div>
{% endif %} {% endif %}
@ -26,7 +26,7 @@
<h2 class="title"> <h2 class="title">
{% include 'user/shelf/books_header.html' %} {% include 'user/shelf/books_header.html' %}
</h2> </h2>
<div class="columns"> <div class="columns is-mobile scroll-x">
{% for shelf in shelves %} {% for shelf in shelves %}
<div class="column is-narrow"> <div class="column is-narrow">
<h3>{{ shelf.name }} <h3>{{ shelf.name }}
@ -60,7 +60,7 @@
<div class="column is-narrow"> <div class="column is-narrow">
<a target="_blank" href="{{ user.local_path }}/rss"> <a target="_blank" href="{{ user.local_path }}/rss">
<span class="icon icon-rss" aria-hidden="true"></span> <span class="icon icon-rss" aria-hidden="true"></span>
<span>{% trans "RSS feed" %}</span> <span class="is-hidden-mobile">{% trans "RSS feed" %}</span>
</a> </a>
</div> </div>
</div> </div>

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