2
0
Fork 0

Merge branch 'production' into nix

This commit is contained in:
D Anzorge 2022-02-05 12:34:30 +01:00
commit 2c5e57c602
175 changed files with 13847 additions and 4677 deletions

View file

@ -8,6 +8,8 @@ USE_HTTPS=true
DOMAIN=your.domain.here
EMAIL=your@email.here
# Instance defualt language (see options at bookwyrm/settings.py "LANGUAGES"
LANGUAGE_CODE="en-us"
# Used for deciding which editions to prefer
DEFAULT_LANGUAGE="English"

View file

@ -38,7 +38,7 @@ class Create(Verb):
class Delete(Verb):
"""Create activity"""
to: List[str]
to: List[str] = field(default_factory=lambda: [])
cc: List[str] = field(default_factory=lambda: [])
type: str = "Delete"
@ -137,8 +137,8 @@ class Accept(Verb):
type: str = "Accept"
def action(self):
"""find and remove the activity object"""
obj = self.object.to_model(save=False, allow_create=False)
"""accept a request"""
obj = self.object.to_model(save=False, allow_create=True)
obj.accept()
@ -150,7 +150,7 @@ class Reject(Verb):
type: str = "Reject"
def action(self):
"""find and remove the activity object"""
"""reject a follow request"""
obj = self.object.to_model(save=False, allow_create=False)
obj.reject()

View file

@ -1,8 +1,34 @@
"""Do further startup configuration and initialization"""
import os
import urllib
import logging
from django.apps import AppConfig
from bookwyrm import settings
logger = logging.getLogger(__name__)
def download_file(url, destination):
"""Downloads a file to the given path"""
try:
# Ensure our destination directory exists
os.makedirs(os.path.dirname(destination))
with urllib.request.urlopen(url) as stream:
with open(destination, "b+w") as outfile:
outfile.write(stream.read())
except (urllib.error.HTTPError, urllib.error.URLError):
logger.error("Failed to download file %s", url)
except OSError:
logger.error("Couldn't open font file %s for writing", destination)
except: # pylint: disable=bare-except
logger.exception("Unknown error in file download")
class BookwyrmConfig(AppConfig):
"""Handles additional configuration"""
name = "bookwyrm"
verbose_name = "BookWyrm"
@ -11,3 +37,15 @@ class BookwyrmConfig(AppConfig):
from bookwyrm.telemetry import open_telemetry
open_telemetry.instrumentDjango()
if settings.ENABLE_PREVIEW_IMAGES and settings.FONTS:
# Download any fonts that we don't have yet
logger.debug("Downloading fonts..")
for name, config in settings.FONTS.items():
font_path = os.path.join(
settings.FONT_DIR, config["directory"], config["filename"]
)
if "url" in config and not os.path.exists(font_path):
logger.info("Just a sec, downloading %s", name)
download_file(config["url"], font_path)

View file

@ -1,7 +1,11 @@
""" functionality outline for a book data connector """
from abc import ABC, abstractmethod
import imghdr
import ipaddress
import logging
from urllib.parse import urlparse
from django.core.files.base import ContentFile
from django.db import transaction
import requests
from requests.exceptions import RequestException
@ -248,6 +252,8 @@ def dict_from_mappings(data, mappings):
def get_data(url, params=None, timeout=10):
"""wrapper for request.get"""
# check if the url is blocked
raise_not_valid_url(url)
if models.FederatedServer.is_blocked(url):
raise ConnectorException(f"Attempting to load data from blocked url: {url}")
@ -280,6 +286,7 @@ def get_data(url, params=None, timeout=10):
def get_image(url, timeout=10):
"""wrapper for requesting an image"""
raise_not_valid_url(url)
try:
resp = requests.get(
url,
@ -290,10 +297,32 @@ def get_image(url, timeout=10):
)
except RequestException as err:
logger.exception(err)
return None
return None, None
if not resp.ok:
return None
return resp
return None, None
image_content = ContentFile(resp.content)
extension = imghdr.what(None, image_content.read())
if not extension:
logger.exception("File requested was not an image: %s", url)
return None, None
return image_content, extension
def raise_not_valid_url(url):
"""do some basic reality checks on the url"""
parsed = urlparse(url)
if not parsed.scheme in ["http", "https"]:
raise ConnectorException("Invalid scheme: ", url)
try:
ipaddress.ip_address(parsed.netloc)
raise ConnectorException("Provided url is an IP address: ", url)
except ValueError:
# it's not an IP address, which is good
pass
class Mapping:

View file

@ -48,7 +48,9 @@ def moderation_report_email(report):
data["reportee"] = report.user.localname or report.user.username
data["report_link"] = report.remote_id
for admin in models.User.objects.filter(groups__name__in=["admin", "moderator"]):
for admin in models.User.objects.filter(
groups__name__in=["admin", "moderator"]
).distinct():
data["user"] = admin.display_name
send_email.delay(admin.email, *format_email("moderation_report", data))

View file

@ -1,6 +1,7 @@
""" using django model forms """
import datetime
from collections import defaultdict
from urllib.parse import urlparse
from django import forms
from django.forms import ModelForm, PasswordInput, widgets, ChoiceField
@ -227,6 +228,34 @@ class FileLinkForm(CustomForm):
model = models.FileLink
fields = ["url", "filetype", "availability", "book", "added_by"]
def clean(self):
"""make sure the domain isn't blocked or pending"""
cleaned_data = super().clean()
url = cleaned_data.get("url")
filetype = cleaned_data.get("filetype")
book = cleaned_data.get("book")
domain = urlparse(url).netloc
if models.LinkDomain.objects.filter(domain=domain).exists():
status = models.LinkDomain.objects.get(domain=domain).status
if status == "blocked":
# pylint: disable=line-too-long
self.add_error(
"url",
_(
"This domain is blocked. Please contact your administrator if you think this is an error."
),
)
elif models.FileLink.objects.filter(
url=url, book=book, filetype=filetype
).exists():
# pylint: disable=line-too-long
self.add_error(
"url",
_(
"This link with file type has already been added for this book. If it is not visible, the domain is still pending."
),
)
class EditionForm(CustomForm):
class Meta:
@ -444,6 +473,12 @@ class ListForm(CustomForm):
fields = ["user", "name", "description", "curation", "privacy", "group"]
class ListItemForm(CustomForm):
class Meta:
model = models.ListItem
fields = ["user", "book", "book_list", "notes"]
class GroupForm(CustomForm):
class Meta:
model = models.Group
@ -500,7 +535,7 @@ class ReadThroughForm(CustomForm):
cleaned_data = super().clean()
start_date = cleaned_data.get("start_date")
finish_date = cleaned_data.get("finish_date")
if start_date > finish_date:
if start_date and finish_date and start_date > finish_date:
self.add_error(
"finish_date", _("Reading finish date cannot be before start date.")
)

View file

@ -19,9 +19,7 @@ def init_permissions():
{
"codename": "edit_instance_settings",
"name": "change the instance info",
"groups": [
"admin",
],
"groups": ["admin"],
},
{
"codename": "set_user_group",
@ -55,7 +53,7 @@ def init_permissions():
},
]
content_type = models.ContentType.objects.get_for_model(User)
content_type = ContentType.objects.get_for_model(models.User)
for permission in permissions:
permission_obj = Permission.objects.create(
codename=permission["codename"],
@ -66,15 +64,12 @@ def init_permissions():
for group_name in permission["groups"]:
Group.objects.get(name=group_name).permissions.add(permission_obj)
# while the groups and permissions shouldn't be changed because the code
# depends on them, what permissions go with what groups should be editable
def init_connectors():
"""access book data sources"""
models.Connector.objects.create(
identifier="bookwyrm.social",
name="BookWyrm dot Social",
name="Bookwyrm.social",
connector_file="bookwyrm_connector",
base_url="https://bookwyrm.social",
books_url="https://bookwyrm.social/book",
@ -84,6 +79,7 @@ def init_connectors():
priority=2,
)
# pylint: disable=line-too-long
models.Connector.objects.create(
identifier="inventaire.io",
name="Inventaire",
@ -127,7 +123,7 @@ def init_settings():
)
def init_link_domains(*_):
def init_link_domains():
"""safe book links"""
domains = [
("standardebooks.org", "Standard EBooks"),
@ -144,10 +140,15 @@ def init_link_domains(*_):
)
# pylint: disable=no-self-use
# pylint: disable=unused-argument
class Command(BaseCommand):
"""command-line options"""
help = "Initializes the database with starter data"
def add_arguments(self, parser):
"""specify which function to run"""
parser.add_argument(
"--limit",
default=None,
@ -155,6 +156,7 @@ class Command(BaseCommand):
)
def handle(self, *args, **options):
"""execute init"""
limit = options.get("limit")
tables = [
"group",
@ -164,7 +166,7 @@ class Command(BaseCommand):
"settings",
"linkdomain",
]
if limit not in tables:
if limit and limit not in tables:
raise Exception("Invalid table limit:", limit)
if not limit or limit == "group":

View file

@ -0,0 +1,21 @@
# Generated by Django 3.2.10 on 2022-01-24 20:01
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0129_auto_20220117_1716"),
]
operations = [
migrations.AlterField(
model_name="listitem",
name="notes",
field=bookwyrm.models.fields.TextField(
blank=True, max_length=300, null=True
),
),
]

View file

@ -0,0 +1,37 @@
# Generated by Django 3.2.10 on 2022-01-24 17:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0129_auto_20220117_1716"),
]
operations = [
migrations.AlterField(
model_name="user",
name="preferred_language",
field=models.CharField(
blank=True,
choices=[
("en-us", "English"),
("de-de", "Deutsch (German)"),
("es-es", "Español (Spanish)"),
("gl-es", "Galego (Galician)"),
("it-it", "Italiano (Italian)"),
("fr-fr", "Français (French)"),
("lt-lt", "Lietuvių (Lithuanian)"),
("no-no", "Norsk (Norwegian)"),
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
("pt-pt", "Português Europeu (European Portuguese)"),
("sv-se", "Swedish (Svenska)"),
("zh-hans", "简体中文 (Simplified Chinese)"),
("zh-hant", "繁體中文 (Traditional Chinese)"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.10 on 2022-01-25 16:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0130_alter_listitem_notes"),
("bookwyrm", "0130_alter_user_preferred_language"),
]
operations = []

View file

@ -0,0 +1,37 @@
# Generated by Django 3.2.10 on 2022-02-02 20:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0131_merge_20220125_1644"),
]
operations = [
migrations.AlterField(
model_name="user",
name="preferred_language",
field=models.CharField(
blank=True,
choices=[
("en-us", "English"),
("de-de", "Deutsch (German)"),
("es-es", "Español (Spanish)"),
("gl-es", "Galego (Galician)"),
("it-it", "Italiano (Italian)"),
("fr-fr", "Français (French)"),
("lt-lt", "Lietuvių (Lithuanian)"),
("no-no", "Norsk (Norwegian)"),
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
("pt-pt", "Português Europeu (European Portuguese)"),
("sv-se", "Svenska (Swedish)"),
("zh-hans", "简体中文 (Simplified Chinese)"),
("zh-hant", "繁體中文 (Traditional Chinese)"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,21 @@
# Generated by Django 3.2.11 on 2022-02-04 20:06
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0132_alter_user_preferred_language"),
]
operations = [
migrations.AlterField(
model_name="listitem",
name="notes",
field=bookwyrm.models.fields.HtmlField(
blank=True, max_length=300, null=True
),
),
]

View file

@ -342,6 +342,11 @@ class Edition(Book):
# set rank
self.edition_rank = self.get_rank()
# clear author cache
if self.id:
for author_id in self.authors.values_list("id", flat=True):
cache.delete(f"author-books-{author_id}")
return super().save(*args, **kwargs)
@classmethod

View file

@ -1,6 +1,5 @@
""" activitypub-aware django model fields """
from dataclasses import MISSING
import imghdr
import re
from uuid import uuid4
from urllib.parse import urljoin
@ -9,7 +8,6 @@ import dateutil.parser
from dateutil.parser import ParserError
from django.contrib.postgres.fields import ArrayField as DjangoArrayField
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.db import models
from django.forms import ClearableFileInput, ImageField as DjangoImageField
from django.utils import timezone
@ -443,12 +441,10 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
except ValidationError:
return None
response = get_image(url)
if not response:
image_content, extension = get_image(url)
if not image_content:
return None
image_content = ContentFile(response.content)
extension = imghdr.what(None, image_content.read()) or ""
image_name = f"{uuid4()}.{extension}"
return [image_name, image_content]

View file

@ -2,6 +2,7 @@
import uuid
from django.apps import apps
from django.core.exceptions import PermissionDenied
from django.db import models
from django.db.models import Q
from django.utils import timezone
@ -74,6 +75,22 @@ class List(OrderedCollectionMixin, BookWyrmModel):
return
super().raise_not_editable(viewer)
def raise_not_submittable(self, viewer):
"""can the user submit a book to the list?"""
# if you can't view the list you can't submit to it
self.raise_visible_to_user(viewer)
# all good if you're the owner or the list is open
if self.user == viewer or self.curation in ["open", "curated"]:
return
if self.curation == "group":
is_group_member = GroupMember.objects.filter(
group=self.group, user=viewer
).exists()
if is_group_member:
return
raise PermissionDenied()
@classmethod
def followers_filter(cls, queryset, viewer):
"""Override filter for "followers" privacy level to allow non-following
@ -125,7 +142,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="actor"
)
notes = fields.TextField(blank=True, null=True)
notes = fields.HtmlField(blank=True, null=True, max_length=300)
approved = models.BooleanField(default=True)
order = fields.IntegerField()
endorsement = models.ManyToManyField("User", related_name="endorsers")

View file

@ -1,5 +1,6 @@
""" flagged for moderation """
from django.db import models
from bookwyrm.settings import DOMAIN
from .base_model import BookWyrmModel
@ -15,6 +16,9 @@ class Report(BookWyrmModel):
links = models.ManyToManyField("Link", blank=True)
resolved = models.BooleanField(default=False)
def get_remote_id(self):
return f"https://{DOMAIN}/settings/reports/{self.id}"
class Meta:
"""set order by default"""

View file

@ -1,5 +1,6 @@
""" puttin' books on shelves """
import re
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.db import models
from django.utils import timezone
@ -94,8 +95,15 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel):
def save(self, *args, **kwargs):
if not self.user:
self.user = self.shelf.user
if self.id and self.user.local:
cache.delete(f"book-on-shelf-{self.book.id}-{self.shelf.id}")
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
if self.id and self.user.local:
cache.delete(f"book-on-shelf-{self.book.id}-{self.shelf.id}")
super().delete(*args, **kwargs)
class Meta:
"""an opinionated constraint!
you can't put a book on shelf twice"""

View file

@ -90,6 +90,14 @@ class SiteSettings(models.Model):
return get_absolute_url(uploaded)
return urljoin(STATIC_FULL_URL, default_path)
def save(self, *args, **kwargs):
"""if require_confirm_email is disabled, make sure no users are pending"""
if not self.require_confirm_email:
User.objects.filter(is_active=False, deactivation_reason="pending").update(
is_active=True, deactivation_reason=None
)
super().save(*args, **kwargs)
class SiteInvite(models.Model):
"""gives someone access to create an account on the instance"""

View file

@ -3,6 +3,7 @@ from dataclasses import MISSING
import re
from django.apps import apps
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
@ -373,6 +374,12 @@ class Review(BookStatus):
activity_serializer = activitypub.Review
pure_type = "Article"
def save(self, *args, **kwargs):
"""clear rating caches"""
if self.book.parent_work:
cache.delete(f"book-rating-{self.book.parent_work.id}-*")
super().save(*args, **kwargs)
class ReviewRating(Review):
"""a subtype of review that only contains a rating"""

View file

@ -4,6 +4,7 @@ import os
import textwrap
from io import BytesIO
from uuid import uuid4
import logging
import colorsys
from colorthief import ColorThief
@ -17,34 +18,49 @@ from django.db.models import Avg
from bookwyrm import models, settings
from bookwyrm.tasks import app
logger = logging.getLogger(__name__)
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
DEFAULT_FONT = settings.PREVIEW_DEFAULT_FONT
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")
def get_imagefont(name, size):
"""Loads an ImageFont based on config"""
try:
config = settings.FONTS[name]
path = os.path.join(settings.FONT_DIR, config["directory"], config["filename"])
return ImageFont.truetype(path, size)
except KeyError:
logger.error("Font %s not found in config", name)
except OSError:
logger.error("Could not load font %s from file", name)
return ImageFont.load_default()
def get_font(weight, size=28):
"""Gets a custom font with the given weight and size"""
font = get_imagefont(DEFAULT_FONT, size)
try:
font = ImageFont.truetype(font_path, size)
except OSError:
font = ImageFont.load_default()
if weight == "light":
font.set_variation_by_name("Light")
if weight == "bold":
font.set_variation_by_name("Bold")
if weight == "regular":
font.set_variation_by_name("Regular")
except AttributeError:
pass
return font

View file

@ -22,6 +22,7 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
"ol",
"li",
]
self.allowed_attrs = ["href", "rel", "src", "alt"]
self.tag_stack = []
self.output = []
# if the html appears invalid, we just won't allow any at all
@ -30,7 +31,14 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
def handle_starttag(self, tag, attrs):
"""check if the tag is valid"""
if self.allow_html and tag in self.allowed_tags:
self.output.append(("tag", self.get_starttag_text()))
allowed_attrs = " ".join(
f'{a}="{v}"' for a, v in attrs if a in self.allowed_attrs
)
reconstructed = f"<{tag}"
if allowed_attrs:
reconstructed += " " + allowed_attrs
reconstructed += ">"
self.output.append(("tag", reconstructed))
self.tag_stack.append(tag)
else:
self.output.append(("data", ""))

View file

@ -9,12 +9,12 @@ from django.utils.translation import gettext_lazy as _
env = Env()
env.read_env()
DOMAIN = env("DOMAIN")
VERSION = "0.2.0"
VERSION = "0.3.0"
PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "76c5ff1f"
JS_CACHE = "7b5303af"
# email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
@ -35,6 +35,9 @@ LOCALE_PATHS = [
]
LANGUAGE_COOKIE_NAME = env.str("LANGUAGE_COOKIE_NAME", "django_language")
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Preview image
@ -44,6 +47,17 @@ 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")
PREVIEW_DEFAULT_FONT = env.str("PREVIEW_DEFAULT_FONT", "Source Han Sans")
FONTS = {
# pylint: disable=line-too-long
"Source Han Sans": {
"directory": "source_han_sans",
"filename": "SourceHanSans-VF.ttf.ttc",
"url": "https://github.com/adobe-fonts/source-han-sans/raw/release/Variable/OTC/SourceHanSans-VF.ttf.ttc",
}
}
FONT_DIR = os.path.join(STATIC_ROOT, "fonts")
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
@ -106,6 +120,61 @@ TEMPLATES = [
},
]
LOG_LEVEL = env("LOG_LEVEL", "INFO").upper()
# Override aspects of the default handler to our taste
# See https://docs.djangoproject.com/en/3.2/topics/logging/#default-logging-configuration
# for a reference to the defaults we're overriding
#
# It seems that in order to override anything you have to include its
# entire dependency tree (handlers and filters) which makes this a
# bit verbose
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {
# These are copied from the default configuration, required for
# implementing mail_admins below
"require_debug_false": {
"()": "django.utils.log.RequireDebugFalse",
},
"require_debug_true": {
"()": "django.utils.log.RequireDebugTrue",
},
},
"handlers": {
# Overrides the default handler to make it log to console
# regardless of the DEBUG setting (default is to not log to
# console if DEBUG=False)
"console": {
"level": LOG_LEVEL,
"class": "logging.StreamHandler",
},
# This is copied as-is from the default logger, and is
# required for the django section below
"mail_admins": {
"level": "ERROR",
"filters": ["require_debug_false"],
"class": "django.utils.log.AdminEmailHandler",
},
},
"loggers": {
# Install our new console handler for Django's logger, and
# override the log level while we're at it
"django": {
"handlers": ["console", "mail_admins"],
"level": LOG_LEVEL,
},
"django.utils.autoreload": {
"level": "INFO",
},
# Add a bookwyrm-specific logger
"bookwyrm": {
"handlers": ["console"],
"level": LOG_LEVEL,
},
},
}
WSGI_APPLICATION = "bookwyrm.wsgi.application"
@ -196,7 +265,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = "en-us"
LANGUAGE_CODE = env("LANGUAGE_CODE", "en-us")
LANGUAGES = [
("en-us", _("English")),
("de-de", _("Deutsch (German)")),
@ -208,6 +277,7 @@ LANGUAGES = [
("no-no", _("Norsk (Norwegian)")),
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
("pt-pt", _("Português Europeu (European Portuguese)")),
("sv-se", _("Svenska (Swedish)")),
("zh-hans", _("简体中文 (Simplified Chinese)")),
("zh-hant", _("繁體中文 (Traditional Chinese)")),
]
@ -263,16 +333,11 @@ if USE_S3:
MEDIA_FULL_URL = MEDIA_URL
STATIC_FULL_URL = STATIC_URL
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
# I don't know if it's used, but the site crashes without it
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
else:
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
MEDIA_URL = "/images/"
MEDIA_FULL_URL = f"{PROTOCOL}://{DOMAIN}{MEDIA_URL}"
STATIC_FULL_URL = f"{PROTOCOL}://{DOMAIN}{STATIC_URL}"
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
OTEL_EXPORTER_OTLP_ENDPOINT = env("OTEL_EXPORTER_OTLP_ENDPOINT", None)
OTEL_EXPORTER_OTLP_HEADERS = env("OTEL_EXPORTER_OTLP_HEADERS", None)

View file

@ -0,0 +1,96 @@
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font
Name 'Source'. Source is a trademark of Adobe in the United States
and/or other countries.
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.

View file

@ -0,0 +1,9 @@
The font file itself is not included in the Git repository to avoid putting
large files in the repo history. The Docker image should download the correct
font into this folder automatically.
In case something goes wrong, the font used is the Variable OTC TTF, available
as of this writing from the Adobe Fonts GitHub repository:
https://github.com/adobe-fonts/source-han-sans/tree/release#user-content-variable-otcs
BookWyrm expects the file to be in this folder, named SourceHanSans-VF.ttf.ttc

View file

@ -122,39 +122,13 @@ let BookWyrm = new (class {
*/
updateCountElement(counter, data) {
let count = data.count;
const count_by_type = data.count_by_type;
if (count === undefined) {
return;
}
const currentCount = counter.innerText;
const hasMentions = data.has_mentions;
const allowedStatusTypesEl = document.getElementById("unread-notifications-wrapper");
// If we're on the right counter element
if (counter.closest("[data-poll-wrapper]").contains(allowedStatusTypesEl)) {
const allowedStatusTypes = JSON.parse(allowedStatusTypesEl.textContent);
// For keys in common between allowedStatusTypes and count_by_type
// This concerns 'review', 'quotation', 'comment'
count = allowedStatusTypes.reduce(function (prev, currentKey) {
const currentValue = count_by_type[currentKey] | 0;
return prev + currentValue;
}, 0);
// Add all the "other" in count_by_type if 'everything' is allowed
if (allowedStatusTypes.includes("everything")) {
// Clone count_by_type with 0 for reviews/quotations/comments
const count_by_everything_else = Object.assign({}, count_by_type, {
review: 0,
quotation: 0,
comment: 0,
});
count = Object.keys(count_by_everything_else).reduce(function (prev, currentKey) {
const currentValue = count_by_everything_else[currentKey] | 0;
return prev + currentValue;
}, count);
}
}
if (count != currentCount) {
this.addRemoveClass(counter.closest("[data-poll-wrapper]"), "is-hidden", count < 1);
@ -517,7 +491,7 @@ let BookWyrm = new (class {
duplicateInput(event) {
const trigger = event.currentTarget;
const input_id = trigger.dataset["duplicate"];
const input_id = trigger.dataset.duplicate;
const orig = document.getElementById(input_id);
const parent = orig.parentNode;
const new_count = parent.querySelectorAll("input").length + 1;

View file

@ -2,7 +2,7 @@
{% load humanize %}
{% load i18n %}
{% load utilities %}
{% load bookwyrm_tags %}
{% load landing_page_tags %}
{% load cache %}
{% block title %}
@ -12,6 +12,7 @@
{% block about_content %}
{# seven day cache #}
{% cache 604800 about_page %}
{% get_book_superlatives as superlatives %}
<section class="content pb-4">
<h2>
@ -26,8 +27,8 @@
</p>
<div class="columns">
{% if top_rated %}
{% with book=superlatives.top_rated.default_edition rating=top_rated.rating %}
{% if superlatives.top_rated %}
{% with book=superlatives.top_rated.default_edition rating=superlatives.top_rated.rating %}
<div class="column is-one-third is-flex">
<div class="media notification">
<div class="media-left">
@ -45,7 +46,7 @@
{% endwith %}
{% endif %}
{% if wanted %}
{% if superlatives.wanted %}
{% with book=superlatives.wanted.default_edition %}
<div class="column is-one-third is-flex">
<div class="media notification">
@ -64,7 +65,7 @@
{% endwith %}
{% endif %}
{% if controversial %}
{% if superlatives.controversial %}
{% with book=superlatives.controversial.default_edition %}
<div class="column is-one-third is-flex">
<div class="media notification">
@ -95,7 +96,7 @@
<h2 class="title is-3">{% trans "Meet your admins" %}</h2>
<p>
{% url "conduct" as coc_path %}
{% blocktrans with site_name=site.name %}
{% blocktrans trimmed with site_name=site.name %}
{{ site_name }}'s moderators and administrators keep the site up and running, enforce the <a href="coc_path">code of conduct</a>, and respond when users report spam and bad behavior.
{% endblocktrans %}
</p>

View file

@ -66,7 +66,7 @@
<div class="box">
{% if author.wikipedia_link %}
<div>
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener" target="_blank">
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener noreferrer" target="_blank">
{% trans "Wikipedia" %}
</a>
</div>
@ -74,7 +74,7 @@
{% if author.isni %}
<div class="mt-1">
<a itemprop="sameAs" href="{{ author.isni_link }}" rel="noopener" target="_blank">
<a itemprop="sameAs" href="{{ author.isni_link }}" rel="noopener noreferrer" target="_blank">
{% trans "View ISNI record" %}
</a>
</div>
@ -83,7 +83,7 @@
{% trans "Load data" as button_text %}
{% if author.openlibrary_key %}
<div class="mt-1 is-flex">
<a class="mr-3" itemprop="sameAs" href="{{ author.openlibrary_link }}" target="_blank" rel="noopener">
<a class="mr-3" itemprop="sameAs" href="{{ author.openlibrary_link }}" target="_blank" rel="noopener noreferrer">
{% trans "View on OpenLibrary" %}
</a>
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
@ -98,7 +98,7 @@
{% if author.inventaire_id %}
<div class="mt-1 is-flex">
<a class="mr-3" itemprop="sameAs" href="{{ author.inventaire_link }}" target="_blank" rel="noopener">
<a class="mr-3" itemprop="sameAs" href="{{ author.inventaire_link }}" target="_blank" rel="noopener noreferrer">
{% trans "View on Inventaire" %}
</a>
@ -114,7 +114,7 @@
{% if author.librarything_key %}
<div class="mt-1">
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener">
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener noreferrer">
{% trans "View on LibraryThing" %}
</a>
</div>
@ -122,7 +122,7 @@
{% if author.goodreads_key %}
<div>
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener">
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener noreferrer">
{% trans "View on Goodreads" %}
</a>
</div>
@ -141,12 +141,14 @@
<h2 class="title is-4">{% blocktrans with name=author.name %}Books by {{ name }}{% endblocktrans %}</h2>
<div class="columns is-multiline is-mobile">
{% for book in books %}
{% with book=book.default_edition %}
<div class="column is-one-fifth-tablet is-half-mobile is-flex is-flex-direction-column">
<div class="is-flex-grow-1">
{% include 'landing/small-book.html' with book=book %}
</div>
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
</div>
{% endwith %}
{% endfor %}
</div>

View file

@ -1,6 +1,6 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load book_display_tags %}
{% load humanize %}
{% load utilities %}
{% load static %}
@ -122,7 +122,7 @@
{% trans "Load data" as button_text %}
{% if book.openlibrary_key %}
<p>
<a href="{{ book.openlibrary_link }}" target="_blank" rel="noopener">
<a href="{{ book.openlibrary_link }}" target="_blank" rel="noopener noreferrer">
{% trans "View on OpenLibrary" %}
</a>
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
@ -136,7 +136,7 @@
{% endif %}
{% if book.inventaire_id %}
<p>
<a href="{{ book.inventaire_link }}" target="_blank" rel="noopener">
<a href="{{ book.inventaire_link }}" target="_blank" rel="noopener noreferrer">
{% trans "View on Inventaire" %}
</a>
@ -356,10 +356,11 @@
<form name="list-add" method="post" action="{% url 'list-add-book' %}">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="user" value="{{ request.user.id }}">
<label class="label" for="id_list">{% trans "Add to list" %}</label>
<div class="field has-addons">
<div class="select control is-clipped">
<select name="list" id="id_list">
<select name="book_list" id="id_list">
{% for list in user.list_set.all %}
<option value="{{ list.id }}">{{ list.name }}</option>
{% endfor %}

View file

@ -2,15 +2,17 @@
{% load i18n %}
{% block filter %}
<label class="label is-block" for="id_format">{% trans "Format:" %}</label>
<div class="select">
<select id="id_format" name="format">
<option value="">{% trans "Any" %}</option>
{% for format in formats %}{% if format %}
<option value="{{ format }}" {% if request.GET.format == format %}selected{% endif %}>
{{ format|title }}
</option>
{% endif %}{% endfor %}
</select>
<div class="control">
<label class="label is-block" for="id_format">{% trans "Format:" %}</label>
<div class="select">
<select id="id_format" name="format">
<option value="">{% trans "Any" %}</option>
{% for format in formats %}{% if format %}
<option value="{{ format }}" {% if request.GET.format == format %}selected{% endif %}>
{{ format|title }}
</option>
{% endif %}{% endfor %}
</select>
</div>
</div>
{% endblock %}

View file

@ -2,15 +2,17 @@
{% load i18n %}
{% block filter %}
<label class="label is-block" for="id_language">{% trans "Language:" %}</label>
<div class="select">
<select id="id_language" name="language">
<option value="">{% trans "Any" %}</option>
{% for language in languages %}
<option value="{{ language }}" {% if request.GET.language == language %}selected{% endif %}>
{{ language }}
</option>
{% endfor %}
</select>
<div class="control">
<label class="label is-block" for="id_language">{% trans "Language:" %}</label>
<div class="select">
<select id="id_language" name="language">
<option value="">{% trans "Any" %}</option>
{% for language in languages %}
<option value="{{ language }}" {% if request.GET.language == language %}selected{% endif %}>
{{ language }}
</option>
{% endfor %}
</select>
</div>
</div>
{% endblock %}

View file

@ -2,7 +2,9 @@
{% load i18n %}
{% block filter %}
<label class="label" for="id_search">{% trans "Search editions" %}</label>
<input type="text" class="input" name="q" value="{{ request.GET.q|default:'' }}" id="id_search">
<div class="control">
<label class="label" for="id_search">{% trans "Search editions" %}</label>
<input type="text" class="input" name="q" value="{{ request.GET.q|default:'' }}" id="id_search">
</div>
{% endblock %}

View file

@ -56,9 +56,7 @@
{% block modal-footer %}
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
{% if not static %}
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% endif %}
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% endblock %}
{% block modal-form-close %}</form>{% endblock %}

View file

@ -39,7 +39,7 @@
{% for link in links %}
<tr>
<td class="overflow-wrap-anywhere">
<a href="{{ link.url }}" target="_blank" rel="noopener">{{ link.url }}</a>
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer">{{ link.url }}</a>
</td>
<td>
<a href="{% url 'user-feed' link.added_by.id %}">{{ link.added_by.display_name }}</a>

View file

@ -1,5 +1,5 @@
{% load i18n %}
{% load bookwyrm_tags %}
{% load book_display_tags %}
{% load utilities %}
{% get_book_file_links book as links %}
@ -28,7 +28,7 @@
{% for link in links.all %}
{% join "verify" link.id as verify_modal %}
<li>
<a href="{{ link.url }}" rel="noopener" target="_blank" title="{{ link.url }}" data-modal-open="{{ verify_modal }}">{{ link.name }}</a>
<a href="{{ link.url }}" rel="noopener noreferrer" target="_blank" title="{{ link.url }}" data-modal-open="{{ verify_modal }}">{{ link.name }}</a>
({{ link.filetype }})
{% if link.availability != "free" %}

View file

@ -17,7 +17,7 @@ Is that where you'd like to go?
{% block modal-footer %}
<a href="{{ link.url }}" target="_blank" rel="noopener" class="button is-primary">{% trans "Continue" %}</a>
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" class="button is-primary">{% trans "Continue" %}</a>
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% if request.user.is_authenticated %}

View file

@ -1,4 +1,4 @@
{% load bookwyrm_tags %}
{% load rating_tags %}
{% load i18n %}
{% load utilities %}
{% load status_display %}

View file

@ -1,4 +1,4 @@
{% load bookwyrm_tags %}
{% load landing_page_tags %}
{% load utilities %}
{% load i18n %}
{% load status_display %}

View file

@ -24,9 +24,12 @@
{# announcements and system messages #}
{% if not activities.number > 1 %}
<a href="{{ request.path }}" class="transition-y is-hidden notification is-primary is-block" data-poll-wrapper>
{% blocktrans with tab_key=tab.key %}load <span data-poll="stream/{{ tab_key }}">0</span> unread status(es){% endblocktrans %}
{{ allowed_status_types|json_script:"unread-notifications-wrapper" }}
<a
href="{{ request.path }}"
class="transition-y is-hidden notification is-primary is-block"
data-poll-wrapper
>
<span data-poll="stream/{{ tab.key }}"></span>
</a>
{% if request.user.show_goal and not goal and tab.key == 'home' %}

View file

@ -1,6 +1,6 @@
{% extends 'feed/layout.html' %}
{% load feed_page_tags %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block opengraph_images %}

View file

@ -1,5 +1,5 @@
{% load i18n %}
{% load bookwyrm_tags %}
{% load feed_page_tags %}
{% suggested_books as suggested_books %}
<section class="block">

View file

@ -9,7 +9,7 @@
<div class="modal is-active" role="dialog" aria-modal="true" aria-labelledby="get_started_header">
<div class="modal-background"></div>
<div class="modal-card is-fullwidth">
<header class="modal-card-head">
<header class="modal-card-head navbar">
<img
class="image logo mr-2"
src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}"

View file

@ -1,7 +1,6 @@
{% extends 'groups/layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load bookwyrm_group_tags %}
{% load group_tags %}
{% load markdown %}
{% block panel %}

View file

@ -1,6 +1,6 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_group_tags %}
{% load group_tags %}
{% block title %}{{ group.name }}{% endblock %}

View file

@ -1,8 +1,7 @@
{% load i18n %}
{% load utilities %}
{% load humanize %}
{% load bookwyrm_tags %}
{% load bookwyrm_group_tags %}
{% load group_tags %}
<h2 class="title is-5">Group Members</h2>
{% if group.user == request.user %}

View file

@ -47,7 +47,7 @@
<span class="icon icon-spinner is-pulled-left" aria-hidden="true"></span>
<span>{% trans "In progress" %}</span>
<span class="is-pulled-right">
<a href="#" class="button is-small">{% trans "Refresh" %}</a>
<a href="{% url 'import-status' job.id %}" class="button is-small">{% trans "Refresh" %}</a>
</span>
</div>
<div class="is-flex">
@ -230,7 +230,7 @@
{% if not legacy %}
<div>
{% include 'snippets/pagination.html' with page=items %}
{% include 'snippets/pagination.html' with page=items path=page_path %}
</div>
{% endif %}
{% endspaceless %}{% endblock %}

View file

@ -3,6 +3,6 @@
{% block tooltip_content %}
{% trans 'You can download your Goodreads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener">Import/Export page</a> of your Goodreads account.' %}
{% trans 'You can download your Goodreads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener noreferrer">Import/Export page</a> of your Goodreads account.' %}
{% endblock %}

View file

@ -1,7 +1,7 @@
{% extends 'landing/layout.html' %}
{% load i18n %}
{% load cache %}
{% load bookwyrm_tags %}
{% load landing_page_tags %}
{% block panel %}

View file

@ -1,4 +1,5 @@
{% load bookwyrm_tags %}
{% load book_display_tags %}
{% load rating_tags %}
{% load markdown %}
{% load i18n %}

View file

@ -1,4 +1,4 @@
{% load bookwyrm_tags %}
{% load rating_tags %}
{% load i18n %}
{% if book %}

View file

@ -0,0 +1,45 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% load utilities %}
{% load group_tags %}
{% block modal-title %}
{% if list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %}
{% blocktrans trimmed with title=book|book_title %}
Add "<em>{{ title }}</em>" to this list
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with title=book|book_title %}
Suggest "<em>{{ title }}</em>" for this list
{% endblocktrans %}
{% endif %}
{% endblock %}
{% block modal-form-open %}
<form
name="add-book-{{ book.id }}"
method="POST"
action="{% url 'list-add-book' %}{% if query %}?q={{ query }}{% endif %}"
>
{% endblock %}
{% block modal-body %}
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="book_list" value="{{ list.id }}">
<input type="hidden" name="user" value="{{ request.user.id }}">
{% include "lists/item_notes_field.html" with form_id=id show_label=True %}
{% endblock %}
{% block modal-footer %}
<button type="submit" class="button is-link">
{% if list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %}
{% trans "Add" %}
{% else %}
{% trans "Suggest" %}
{% endif %}
</button>
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% endblock %}
{% block modal-form-close %}</form>{% endblock %}

View file

@ -1,5 +1,6 @@
{% extends 'lists/layout.html' %}
{% load i18n %}
{% load utilities %}
{% block breadcrumbs %}
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
@ -38,6 +39,17 @@
<div class="column ml-3">
{% include 'snippets/book_titleby.html' %}
{% if item.notes %}
<div>
{% url 'user-feed' item.user|username as user_path %}
{% blocktrans trimmed with username=item.user.display_name %}
<a href="{{ user_path }}">{{ username }}</a> says:
{% endblocktrans %}
<p class="notification">
{{ item.notes }}
</p>
</div>
{% endif %}
<p>
{% trans "Suggested by" %}

View file

@ -0,0 +1,20 @@
{% load i18n %}
<form
name="edit-notes-{{ item.id }}"
method="POST"
action="{% url 'list-item' list.id item.id %}"
>
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="book_list" value="{{ list.id }}">
<input type="hidden" name="user" value="{{ request.user.id }}">
{% include "lists/item_notes_field.html" with form_id=item.id %}
<div class="field">
<div class="control">
<button type="submit" class="button is-success">
{% trans "Save" %}
</button>
</div>
</div>
</form>

View file

@ -1,7 +1,8 @@
{% extends 'embed-layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load bookwyrm_group_tags %}
{% load book_display_tags %}
{% load rating_tags %}
{% load group_tags %}
{% load markdown %}
{% block title %}{% blocktrans with list_name=list.name owner=list.user.display_name %}{{ list_name }}, a list by {{owner}}{% endblocktrans %}{% endblock title %}

View file

@ -0,0 +1,21 @@
{% load i18n %}
<div class="field">
<label
for="id_notes_{{ form_id }}"
class="{% if show_label %}label{% else %}is-sr-only{% endif %}"
>
{% trans "Notes:" %}
</label>
<div class="control">
<textarea
class="textarea"
id="id_notes_{{ form_id }}"
maxlength="300"
name="notes"
aria-describedby="notes_description_{{ form_id }}"
>{{ item.notes|default:'' }}</textarea>
</div>
<p class="help" id="notes_description_{{ form_id }}">
{% trans "An optional note that will be displayed with the book." %}
</p>
</div>

View file

@ -1,8 +1,10 @@
{% extends 'lists/layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load bookwyrm_group_tags %}
{% load rating_tags %}
{% load group_tags %}
{% load book_display_tags %}
{% load markdown %}
{% load utilities %}
{% block breadcrumbs %}
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
@ -20,7 +22,7 @@
{% block panel %}
{% if request.user == list.user and pending_count %}
<div class="block content">
<p>
<p class="notification">
<a href="{% url 'list-curate' list.id %}">{{ pending_count }} book{{ pending_count|pluralize }} awaiting your approval</a>
</p>
</div>
@ -45,21 +47,16 @@
{% for item in items %}
<li class="block mb-5">
<div class="card">
{% with book=item.book %}
<div
class="
card-content p-0 mb-0
columns is-gapless
is-mobile
"
>
<div class="column is-3-mobile is-2-tablet is-cover align to-t">
<div class="card-content">
{% with book=item.book %}
<div class="columns is-mobile">
<div class="column is-narrow is-cover">
<a href="{{ item.book.local_path }}" aria-hidden="true">
{% include 'snippets/book_cover.html' with cover_class='is-w-auto is-h-m-tablet is-align-items-flex-start' size='medium' %}
</a>
</div>
<div class="column mx-3 my-2">
<div class="column">
<p>
{% include 'snippets/book_titleby.html' %}
</p>
@ -72,8 +69,52 @@
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
</div>
</div>
{% endwith %}
{% endwith %}
{% if item.notes %}
<div class="media notification">
<figure class="media-left" aria-hidden="true">
{% include "snippets/avatar.html" with user=item.user %}
</figure>
<div class="media-content">
<div class="content">
<header>
{% url 'user-feed' item.user|username as user_path %}
{% blocktrans trimmed with username=user.display_name %}
<a href="{{ user_path }}">{{ username }}</a> says:
{% endblocktrans %}
</header>
{{ item.notes|to_markdown|safe }}
</div>
{% if item.user == request.user %}
<div>
<details class="details-panel box">
<summary>
<span role="heading" aria-level="3">
{% trans "Edit notes" %}
<span class="details-close icon icon-pencil" aria-hidden></span>
</span>
</summary>
{% include "lists/edit_item_form.html" with book=item.book %}
</details>
</div>
{% endif %}
</div>
</div>
{% elif item.user == request.user %}
<div>
<details class="details-panel box">
<summary>
<span role="heading" aria-level="3">
{% trans "Add notes" %}
<span class="details-close icon icon-plus" aria-hidden></span>
</span>
</summary>
{% include "lists/edit_item_form.html" with book=item.book %}
</details>
</div>
{% endif %}
</div>
<div class="card-footer is-stacked-mobile has-background-white-bis is-align-items-stretch">
<div class="card-footer-item">
<p>
@ -194,23 +235,19 @@
<div class="column ml-3">
<p>{% include 'snippets/book_titleby.html' with book=book %}</p>
<form
class="mt-1"
name="add-book-{{ book.id }}"
method="post"
action="{% url 'list-add-book' %}{% if query %}?q={{ query }}{% endif %}"
{% join "add_item" list.id book.id as modal_id %}
<button
type="button"
class="button is-small is-link"
data-modal-open="{{ modal_id }}"
>
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="list" value="{{ list.id }}">
<button type="submit" class="button is-small is-link">
{% if list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %}
{% trans "Add" %}
{% else %}
{% trans "Suggest" %}
{% endif %}
</button>
</form>
{% if list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %}
{% trans "Add" %}
{% else %}
{% trans "Suggest" %}
{% endif %}
</button>
{% include "lists/add_item_modal.html" with id=modal_id %}
</div>
</div>
{% endfor %}
@ -224,7 +261,7 @@
<textarea
readonly
class="textarea is-small"
aria-labelledby="embed-label"
aria-describedby="embed-label"
data-copytext
data-copytext-label="{% trans 'Copy embed code' %}"
data-copytext-success="{% trans 'Copied!' %}"

View file

@ -1,4 +1,4 @@
{% load bookwyrm_tags %}
{% load notification_page_tags %}
{% related_status notification as related_status %}
<div class="notification {% if notification.id in unread %}has-background-primary{% endif %}">
<div class="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-grey{% endif %}">

View file

@ -70,9 +70,7 @@
{% block modal-footer %}
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
{% if not static %}
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% endif %}
{% endblock %}
{% block modal-form-close %}

View file

@ -6,5 +6,5 @@
{% endblock %}
{% block content %}
{% include "snippets/report_modal.html" with user=user active=True static=True %}
{% include "snippets/report_modal.html" with user=user active=True static=True id="report-modal" %}
{% endblock %}

View file

@ -1,5 +1,78 @@
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
##### AhrefsBot #####
# see http://ahrefs.com/robot/
User-agent: AhrefsBot
Crawl-Delay: 10
#Disallow: /
##### SemrushBot #####
# see http://www.semrush.com/bot.html
User-agent: SemrushBot
Crawl-Delay: 10
#Disallow: /
# To block SemrushBot from crawling your site for different SEO and technical issues:
User-agent: SiteAuditBot
Disallow: /
#To block SemrushBot from crawling your site for Backlink Audit tool:
User-agent: SemrushBot-BA
Disallow: /
#To block SemrushBot from crawling your site for On Page SEO Checker tool and similar tools:
User-agent: SemrushBot-SI
Disallow: /
#To block SemrushBot from checking URLs on your site for SWA tool:
User-agent: SemrushBot-SWA
Disallow: /
#To block SemrushBot from crawling your site for Content Analyzer and Post Tracking tools:
User-agent: SemrushBot-CT
Disallow: /
#To block SemrushBot from crawling your site for Brand Monitoring:
User-agent: SemrushBot-BM
Disallow: /
#To block SplitSignalBot from crawling your site for SplitSignal tool:
User-agent: SplitSignalBot
Disallow: /
#To block SemrushBot-COUB from crawling your site for Content Outline Builder tool:
User-agent: SemrushBot-COUB
Disallow: /
##### DotBot #####
# see https://opensiteexplorer.org/dotbot
User-agent: dotbot
Crawl-delay: 10
#Disallow: /
##### BLEXBot #####
# see http://webmeup-crawler.com/
User-agent: BLEXBot
Crawl-delay: 10
#Disallow: /
##### MJ12bot #####
# see http://mj12bot.com/
User-Agent: MJ12bot
Crawl-Delay: 20
#Disallow: /
##### PetalBot #####
# see https://webmaster.petalsearch.com/site/petalbot
User-agent: PetalBot
Disallow: /
User-agent: *
Disallow: /static/js/
Disallow: /static/css/

View file

@ -63,7 +63,7 @@
<strong>
<a
href="{{ result.view_link|default:result.key }}"
rel="noopener"
rel="noopener noreferrer"
target="_blank"
>{{ result.title }}</a>
</strong>

View file

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

View file

@ -36,7 +36,7 @@
<header class="column">
<h2 class="title is-5">
{{ domain.name }}
(<a href="http://{{ domain.domain }}" target="_blank" rel="noopener">{{ domain.domain }}</a>)
(<a href="http://{{ domain.domain }}" target="_blank" rel="noopener noreferrer">{{ domain.domain }}</a>)
</h2>
</header>
<div class="column is-narrow">

View file

@ -12,7 +12,7 @@
{% for link in links %}
<tr>
<td class="overflow-wrap-anywhere">
<a href="{{ link.url }}" target="_blank" rel="noopener">{{ link.url }}</a>
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer">{{ link.url }}</a>
</td>
<td>
<a href="{% url 'settings-user' link.added_by.id %}">@{{ link.added_by|username }}</a>

View file

@ -17,6 +17,19 @@
{% include 'settings/reports/report_preview.html' with report=report %}
</div>
<div class="block">
<details class="details-panel box">
<summary>
<span class="title is-4">{% trans "Message reporter" %}</span>
<span class="details-close icon icon-x" aria-hidden></span>
</summary>
<div class="box">
{% trans "Update on your report:" as dm_template %}
{% include 'snippets/create_status/status.html' with type="direct" uuid=1 mention=report.reporter prepared_content=dm_template no_script=True %}
</div>
</details>
</div>
{% if report.statuses.exists %}
<div class="block">
<h3 class="title is-4">{% trans "Reported statuses" %}</h3>
@ -68,9 +81,13 @@
{% endfor %}
<form class="block" name="report-comment" method="post" action="{% url 'settings-report' report.id %}">
{% csrf_token %}
<label for="report_comment" class="label">Comment on report</label>
<textarea name="note" id="report_comment" class="textarea"></textarea>
<button class="button">{% trans "Comment" %}</button>
<div class="field">
<label for="report_comment" class="label">Comment on report</label>
<textarea name="note" id="report_comment" class="textarea"></textarea>
</div>
<div class="field">
<button class="button">{% trans "Comment" %}</button>
</div>
</form>
</div>
{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends 'layout.html' %}
{% load bookwyrm_tags %}
{% load shelf_tags %}
{% load utilities %}
{% load humanize %}
{% load i18n %}
@ -45,7 +45,7 @@
href="{{ shelf_tab.local_path }}"
{% if shelf_tab.identifier == shelf.identifier %} aria-current="page"{% endif %}
>
{% include 'user/books_header.html' with shelf=shelf_tab %}
{% include "snippets/translated_shelf_name.html" with shelf=shelf_tab %}
</a>
</li>
{% endfor %}
@ -92,7 +92,7 @@
</span>
{% with count=books.paginator.count %}
{% if count %}
<p class="help">
<span class="help">
{% blocktrans trimmed count counter=count with formatted_count=count|intcomma %}
{{ formatted_count }} book
{% plural %}
@ -104,7 +104,7 @@
(showing {{ start }}-{{ end }})
{% endblocktrans %}
{% endif %}
</p>
</span>
{% endif %}
{% endwith %}
</h2>

View file

@ -1,5 +1,6 @@
{% load i18n %}
{% load bookwyrm_group_tags %}
{% load group_tags %}
{% if request.user == user or not request.user == group.user or not request.user.is_authenticated %}
{% elif user in request.user.blocks.all %}
{% include 'snippets/block_button.html' with blocks=True %}

View file

@ -1,4 +1,8 @@
{% load static %}
<img class="avatar image {% if large %}is-96x96{% elif medium %}is-48x48{% else %}is-32x32{% endif %}" src="{% if user.avatar %}{% get_media_prefix %}{{ user.avatar }}{% else %}{% static "images/default_avi.jpg" %}{% endif %}" {% if ariaHide %}aria-hidden="true"{% endif %} alt="{{ user.alt_text }}">
<img
class="avatar image {% if large %}is-96x96{% elif medium %}is-48x48{% else %}is-32x32{% endif %}"
src="{% if user.avatar %}{% get_media_prefix %}{{ user.avatar }}{% else %}{% static "images/default_avi.jpg" %}{% endif %}"
{% if ariaHide %}aria-hidden="true"{% endif %}
alt="{{ user.alt_text }}"
>

View file

@ -1,5 +1,5 @@
{% extends "snippets/create_status/layout.html" %}
{% load bookwyrm_tags %}
{% load shelf_tags %}
{% load i18n %}
{% load utilities %}
{% load status_display %}

View file

@ -8,13 +8,14 @@ reply_parent: if applicable, the Status object that this post is in reply to
mention: a user who is @ mentioned by default in the post
draft: an existing Status object that is providing default values for input fields
{% endcomment %}
<textarea
name="content"
class="textarea save-draft"
{% if not draft %}data-cache-draft="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}"{% endif %}
id="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}{{ uuid }}"
placeholder="{{ placeholder }}"
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}"
{% if not optional and type != "quotation" and type != "review" %}required{% endif %}
>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mention %}@{{ mention|username }} {% endif %}{% firstof draft.raw_content draft.content '' %}</textarea>
<div class="control">
<textarea
name="content"
class="textarea save-draft"
{% if not draft %}data-cache-draft="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}"{% endif %}
id="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}{{ uuid }}"
placeholder="{{ placeholder }}"
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}"
{% if not optional and type != "quotation" and type != "review" %}required{% endif %}
>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mention %}@{{ mention|username }} {% endif %}{{ prepared_content }}{% firstof draft.raw_content draft.content '' %}</textarea>
</div>

View file

@ -1,4 +1,3 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% load utilities %}
{% load status_display %}

View file

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

View file

@ -1,5 +1,4 @@
{% extends "snippets/create_status/layout.html" %}
{% load bookwyrm_tags %}
{% load utilities %}
{% load status_display %}
{% load i18n %}

View file

@ -1,5 +1,4 @@
{% extends "snippets/create_status/layout.html" %}
{% load bookwyrm_tags %}
{% load utilities %}
{% load status_display %}
{% load i18n %}

View file

@ -38,9 +38,11 @@
{% block filter_fields %}
{% endblock %}
</div>
<button type="submit" class="button is-primary is-small">
<div class="control">
<button type="submit" class="button is-primary">
{% trans "Apply filters" %}
</button>
</div>
</form>
</div>
</details>

View file

@ -2,15 +2,15 @@
{% if rating %}
{% blocktrans trimmed with book_title=book.title|safe book_path=book.local_path display_rating=rating|floatformat:"-1" review_title=name|safe count counter=rating %}
Review of "<a href='{{ book_path }}'>{{ book_title }}</a>" ({{ display_rating }} star): {{ review_title }}
Review of "{{ book_title }}" ({{ display_rating }} star): {{ review_title }}
{% plural %}
Review of "<a href='{{ book_path }}'>{{ book_title }}</a>" ({{ display_rating }} stars): {{ review_title }}
Review of "{{ book_title }}" ({{ display_rating }} stars): {{ review_title }}
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with book_title=book.title|safe book_path=book.local_path review_title=name|safe %}
Review of "<a href='{{ book_path }}'>{{ book_title }}</a>": {{ review_title }}
Review of "{{ book_title }}": {{ review_title }}
{% endblocktrans %}
{% endif %}

View file

@ -1,5 +1,6 @@
{% load i18n %}
{% load bookwyrm_group_tags %}
{% load group_tags %}
{% if group|is_invited:request.user %}
<div class="field is-grouped">
<form action="/accept-group-invitation/" method="POST">

View file

@ -1,5 +1,6 @@
{% load i18n %}
{% load bookwyrm_tags %}
{% load rating_tags %}
{% if request.user.is_authenticated %}
<span class="is-sr-only">{% trans "Leave a rating" %}</span>
<div class="block">

View file

@ -1,5 +1,6 @@
{% load i18n %}
{% load bookwyrm_group_tags %}
{% load group_tags %}
{% if request.user == user or not request.user == group.user or not request.user.is_authenticated %}
{% else %}
{% if user in request.user.blocks.all %}

View file

@ -22,7 +22,7 @@
{% csrf_token %}
<input type="hidden" name="reporter" value="{{ request.user.id }}">
<input type="hidden" name="user" value="{{ user.id }}">
{% if status %}
{% if status_id %}
<input type="hidden" name="statuses" value="{{ status_id }}">
{% endif %}
{% if link %}
@ -50,9 +50,7 @@
{% block modal-footer %}
<button class="button is-success" type="submit">{% trans "Submit" %}</button>
{% if not static %}
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% endif %}
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% endblock %}

View file

@ -1,7 +1,7 @@
{% extends 'components/dropdown.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load shelf_tags %}
{% load utilities %}
{% load i18n %}
{% block dropdown-trigger %}
<span>{% trans "Move book" %}</span>
@ -17,7 +17,7 @@
{% if shelf.editable %}
<li role="menuitem" class="dropdown-item p-0">
<form name="shelve" action="/shelve/" method="post">
<form name="editable-shelve-{{ uuid }}" action="/shelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="change-shelf-from" value="{{ current.identifier }}">
@ -67,7 +67,7 @@
{% if user_shelf in book.shelves.all %}
<li class="navbar-divider m-0" role="separator" ></li>
<li role="menuitem" class="dropdown-item p-0">
<form name="shelve" action="/unshelve/" method="post">
<form name="shelve-{{ user_shelf.identifier }}-{{ book.id }}-{{ uuid }}" action="/unshelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="shelf" value="{{ user_shelf.id }}">
@ -79,7 +79,7 @@
{% else %}
<li class="navbar-divider" role="separator" ></li>
<li role="menuitem" class="dropdown-item p-0">
<form name="shelve" action="/unshelve/" method="post">
<form name="un-shelve-{{ shelf.identifier }}-{{ book.id }}-{{ uuid }}" action="/unshelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="shelf" value="{{ shelf.id }}">

View file

@ -1,13 +1,14 @@
{% load bookwyrm_tags %}
{% load utilities %}
{% load shelf_tags %}
{% if request.user.is_authenticated %}
{% with book.id|uuid as uuid %}
{% active_shelf book as active_shelf %}
{% latest_read_through book request.user as readthrough %}
{% with active_shelf_book=active_shelf.book %}
<div class="field has-addons mb-0 has-text-weight-normal" data-shelve-button-book="{{ book.id }}">
{% if switch_mode and active_shelf.book != book %}
{% if switch_mode and active_shelf_book != book %}
<div class="control">
{% include 'snippets/switch_edition_button.html' with edition=book size='is-small' %}
</div>
@ -20,16 +21,17 @@
</div>
{% join "want_to_read" uuid as modal_id %}
{% include 'snippets/reading_modals/want_to_read_modal.html' with book=active_shelf.book id=modal_id class="" %}
{% include 'snippets/reading_modals/want_to_read_modal.html' with book=active_shelf_book id=modal_id class="" %}
{% join "start_reading" uuid as modal_id %}
{% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf.book id=modal_id class="" %}
{% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf_book id=modal_id class="" %}
{% join "finish_reading" uuid as modal_id %}
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book id=modal_id readthrough=readthrough class="" %}
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
{% join "progress_update" uuid as modal_id %}
{% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf.book id=modal_id readthrough=readthrough class="" %}
{% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
{% endwith %}
{% endwith %}
{% endif %}

View file

@ -1,5 +1,5 @@
{% load bookwyrm_tags %}
{% load utilities %}
{% load shelf_tags %}
{% load i18n %}
{% with next_shelf_identifier=active_shelf.shelf.identifier|next_shelf %}
@ -38,7 +38,7 @@
<form name="shelve-{{ uuid }}-{{ shelf.identifier }}" action="/shelve/" method="post" autocomplete="off">
{% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<button class="button {{ class }}" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>
<button class="button {{ class }}" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if book|is_book_on_shelf:shelf %} disabled {% endif %}>
<span>{{ shelf.name }}</span>
</button>
</form>
@ -58,7 +58,7 @@
{% if active_shelf.shelf %}
<li role="menuitem" class="dropdown-item p-0" data-extra-options>
<form name="shelve" action="/unshelve/" method="post">
<form name="unshelve-{{ uuid }}-{{ shelf.identifier }}" action="/unshelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}">

View file

@ -1,5 +1,5 @@
{% load bookwyrm_tags %}
{% load utilities %}
{% load shelf_tags %}
{% load i18n %}
{% with next_shelf_identifier=active_shelf.shelf.identifier|next_shelf %}
@ -45,7 +45,13 @@
<form name="shelve-{{ uuid }}-{{ shelf.identifier }}" action="/shelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<button class="button {{ class }}" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>
<button
class="button {{ class }}"
name="shelf"
type="submit"
value="{{ shelf.identifier }}"
{% if book|is_book_on_shelf:shelf %} disabled {% endif %}
>
<span>{{ shelf.name }}</span>
</button>
</form>

View file

@ -1,4 +1,5 @@
{% load bookwyrm_tags %}
{% load book_display_tags %}
{% load rating_tags %}
{% load markdown %}
{% load i18n %}
{% load static %}

View file

@ -1,12 +1,10 @@
{% spaceless %}
{% load bookwyrm_tags %}
{% load book_display_tags %}
{% load markdown %}
{% load i18n %}
{% load cache %}
{% if not hide_book %}
{% cache 259200 generated_status_book status.id %}
{% with book=status.book|default:status.mention_books.first %}
<div class="columns is-mobile is-gapless">
<a class="column is-cover is-narrow" href="{{ book.local_path }}">
@ -26,7 +24,6 @@
</div>
</div>
{% endwith %}
{% endcache %}
{% endif %}
{% endspaceless %}

View file

@ -10,7 +10,9 @@
{% block dropdown-list %}
<li role="menuitem">
<a href="{% url 'direct-messages-user' user|username %}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
<div class="control">
<a href="{% url 'direct-messages-user' user|username %}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
</div>
</li>
<li role="menuitem">
{% include 'snippets/report_button.html' with user=user class="is-fullwidth" %}

View file

@ -4,7 +4,7 @@
{% load utilities %}
{% load markdown %}
{% load layout %}
{% load bookwyrm_group_tags %}
{% load group_tags %}
{% block title %}{{ user.display_name }}{% endblock %}

View file

@ -1,7 +1,7 @@
{% load i18n %}
{% load humanize %}
{% load utilities %}
{% load bookwyrm_tags %}
{% load user_page_tags %}
<div class="media block">
<div class="media-left">

View file

@ -0,0 +1,17 @@
""" template filters """
from django import template
register = template.Library()
@register.filter(name="book_description")
def get_book_description(book):
"""use the work's text if the book doesn't have it"""
return book.description or book.parent_work.description
@register.simple_tag(takes_context=False)
def get_book_file_links(book):
"""links for a book"""
return book.file_links.filter(domain__status="approved")

View file

@ -1,206 +0,0 @@
""" template filters """
from django import template
from django.db.models import Avg, StdDev, Count, F, Q
from bookwyrm import models
from bookwyrm.utils import cache
from bookwyrm.views.feed import get_suggested_books
register = template.Library()
@register.filter(name="rating")
def get_rating(book, user):
"""get the overall rating of a book"""
queryset = models.Review.privacy_filter(user).filter(
book__parent_work__editions=book
)
return queryset.aggregate(Avg("rating"))["rating__avg"]
@register.filter(name="user_rating")
def get_user_rating(book, user):
"""get a user's rating of a book"""
rating = (
models.Review.objects.filter(
user=user,
book=book,
rating__isnull=False,
deleted=False,
)
.order_by("-published_date")
.first()
)
if rating:
return rating.rating
return 0
@register.filter(name="book_description")
def get_book_description(book):
"""use the work's text if the book doesn't have it"""
return book.description or book.parent_work.description
@register.filter(name="next_shelf")
def get_next_shelf(current_shelf):
"""shelf you'd use to update reading progress"""
if current_shelf == "to-read":
return "reading"
if current_shelf == "reading":
return "read"
if current_shelf == "read":
return "complete"
return "to-read"
@register.filter(name="load_subclass")
def load_subclass(status):
"""sometimes you didn't select_subclass"""
if hasattr(status, "quotation"):
return status.quotation
if hasattr(status, "review"):
return status.review
if hasattr(status, "comment"):
return status.comment
if hasattr(status, "generatednote"):
return status.generatednote
return status
@register.simple_tag(takes_context=False)
def get_book_superlatives():
"""get book stats for the about page"""
total_ratings = models.Review.objects.filter(local=True, deleted=False).count()
data = {}
data["top_rated"] = (
models.Work.objects.annotate(
rating=Avg(
"editions__review__rating",
filter=Q(editions__review__local=True, editions__review__deleted=False),
),
rating_count=Count(
"editions__review",
filter=Q(editions__review__local=True, editions__review__deleted=False),
),
)
.annotate(weighted=F("rating") * F("rating_count") / total_ratings)
.filter(rating__gt=4, weighted__gt=0)
.order_by("-weighted")
.first()
)
data["controversial"] = (
models.Work.objects.annotate(
deviation=StdDev(
"editions__review__rating",
filter=Q(editions__review__local=True, editions__review__deleted=False),
),
rating_count=Count(
"editions__review",
filter=Q(editions__review__local=True, editions__review__deleted=False),
),
)
.annotate(weighted=F("deviation") * F("rating_count") / total_ratings)
.filter(weighted__gt=0)
.order_by("-weighted")
.first()
)
data["wanted"] = (
models.Work.objects.annotate(
shelf_count=Count(
"editions__shelves", filter=Q(editions__shelves__identifier="to-read")
)
)
.order_by("-shelf_count")
.first()
)
return data
@register.simple_tag(takes_context=False)
def related_status(notification):
"""for notifications"""
if not notification.related_status:
return None
return load_subclass(notification.related_status)
@register.simple_tag(takes_context=True)
def active_shelf(context, book):
"""check what shelf a user has a book on, if any"""
user = context["request"].user
return (
cache.get_or_set(
f"active_shelf-{user.id}-{book.id}",
lambda u, b: (
models.ShelfBook.objects.filter(
shelf__user=u,
book__parent_work__editions=b,
).first()
),
user,
book,
timeout=15552000,
)
or {"book": book}
)
@register.simple_tag(takes_context=False)
def latest_read_through(book, user):
"""the most recent read activity"""
return cache.get_or_set(
f"latest_read_through-{user.id}-{book.id}",
lambda u, b: (
models.ReadThrough.objects.filter(user=u, book=b, is_active=True)
.order_by("-start_date")
.first()
),
user,
book,
timeout=15552000,
)
@register.simple_tag(takes_context=False)
def get_landing_books():
"""list of books for the landing page"""
return list(
set(
models.Edition.objects.filter(
review__published_date__isnull=False,
review__deleted=False,
review__user__local=True,
review__privacy__in=["public", "unlisted"],
)
.exclude(cover__exact="")
.distinct()
.order_by("-review__published_date")[:6]
)
)
@register.simple_tag(takes_context=True)
def mutuals_count(context, user):
"""how many users that you follow, follow them"""
viewer = context["request"].user
if not viewer.is_authenticated:
return None
return user.followers.filter(followers=viewer).count()
@register.simple_tag(takes_context=True)
def suggested_books(context):
"""get books for suggested books panel"""
# this happens here instead of in the view so that the template snippet can
# be cached in the template
return get_suggested_books(context["request"].user)
@register.simple_tag(takes_context=False)
def get_book_file_links(book):
"""links for a book"""
return book.file_links.filter(domain__status="approved")

View file

@ -0,0 +1,28 @@
""" tags used on the feed pages """
from django import template
from bookwyrm.views.feed import get_suggested_books
register = template.Library()
@register.filter(name="load_subclass")
def load_subclass(status):
"""sometimes you didn't select_subclass"""
if hasattr(status, "quotation"):
return status.quotation
if hasattr(status, "review"):
return status.review
if hasattr(status, "comment"):
return status.comment
if hasattr(status, "generatednote"):
return status.generatednote
return status
@register.simple_tag(takes_context=True)
def suggested_books(context):
"""get books for suggested books panel"""
# this happens here instead of in the view so that the template snippet can
# be cached in the template
return get_suggested_books(context["request"].user)

View file

@ -0,0 +1,76 @@
""" template filters """
from django import template
from django.db.models import Avg, StdDev, Count, F, Q
from bookwyrm import models
register = template.Library()
@register.simple_tag(takes_context=False)
def get_book_superlatives():
"""get book stats for the about page"""
total_ratings = models.Review.objects.filter(local=True, deleted=False).count()
data = {}
data["top_rated"] = (
models.Work.objects.annotate(
rating=Avg(
"editions__review__rating",
filter=Q(editions__review__local=True, editions__review__deleted=False),
),
rating_count=Count(
"editions__review",
filter=Q(editions__review__local=True, editions__review__deleted=False),
),
)
.annotate(weighted=F("rating") * F("rating_count") / total_ratings)
.filter(rating__gt=4, weighted__gt=0)
.order_by("-weighted")
.first()
)
data["controversial"] = (
models.Work.objects.annotate(
deviation=StdDev(
"editions__review__rating",
filter=Q(editions__review__local=True, editions__review__deleted=False),
),
rating_count=Count(
"editions__review",
filter=Q(editions__review__local=True, editions__review__deleted=False),
),
)
.annotate(weighted=F("deviation") * F("rating_count") / total_ratings)
.filter(weighted__gt=0)
.order_by("-weighted")
.first()
)
data["wanted"] = (
models.Work.objects.annotate(
shelf_count=Count(
"editions__shelves", filter=Q(editions__shelves__identifier="to-read")
)
)
.order_by("-shelf_count")
.first()
)
return data
@register.simple_tag(takes_context=False)
def get_landing_books():
"""list of books for the landing page"""
return list(
set(
models.Edition.objects.filter(
review__published_date__isnull=False,
review__deleted=False,
review__user__local=True,
review__privacy__in=["public", "unlisted"],
)
.exclude(cover__exact="")
.distinct()
.order_by("-review__published_date")[:6]
)
)

View file

@ -0,0 +1,14 @@
""" tags used on the feed pages """
from django import template
from bookwyrm.templatetags.feed_page_tags import load_subclass
register = template.Library()
@register.simple_tag(takes_context=False)
def related_status(notification):
"""for notifications"""
if not notification.related_status:
return None
return load_subclass(notification.related_status)

View file

@ -0,0 +1,42 @@
""" template filters """
from django import template
from django.db.models import Avg
from bookwyrm import models
from bookwyrm.utils import cache
register = template.Library()
@register.filter(name="rating")
def get_rating(book, user):
"""get the overall rating of a book"""
return cache.get_or_set(
f"book-rating-{book.parent_work.id}-{user.id}",
lambda u, b: models.Review.privacy_filter(u)
.filter(book__parent_work__editions=b, rating__gt=0)
.aggregate(Avg("rating"))["rating__avg"]
or 0,
user,
book,
timeout=15552000,
)
@register.filter(name="user_rating")
def get_user_rating(book, user):
"""get a user's rating of a book"""
rating = (
models.Review.objects.filter(
user=user,
book=book,
rating__isnull=False,
deleted=False,
)
.order_by("-published_date")
.first()
)
if rating:
return rating.rating
return 0

View file

@ -0,0 +1,68 @@
""" Filters and tags related to shelving books """
from django import template
from bookwyrm import models
from bookwyrm.utils import cache
register = template.Library()
@register.filter(name="is_book_on_shelf")
def get_is_book_on_shelf(book, shelf):
"""is a book on a shelf"""
return cache.get_or_set(
f"book-on-shelf-{book.id}-{shelf.id}",
lambda b, s: s.books.filter(id=b.id).exists(),
book,
shelf,
timeout=15552000,
)
@register.filter(name="next_shelf")
def get_next_shelf(current_shelf):
"""shelf you'd use to update reading progress"""
if current_shelf == "to-read":
return "reading"
if current_shelf == "reading":
return "read"
if current_shelf == "read":
return "complete"
return "to-read"
@register.simple_tag(takes_context=True)
def active_shelf(context, book):
"""check what shelf a user has a book on, if any"""
user = context["request"].user
return cache.get_or_set(
f"active_shelf-{user.id}-{book.id}",
lambda u, b: (
models.ShelfBook.objects.filter(
shelf__user=u,
book__parent_work__editions=b,
).first()
or False
),
user,
book,
timeout=15552000,
) or {"book": book}
@register.simple_tag(takes_context=False)
def latest_read_through(book, user):
"""the most recent read activity"""
return cache.get_or_set(
f"latest_read_through-{user.id}-{book.id}",
lambda u, b: (
models.ReadThrough.objects.filter(user=u, book=b, is_active=True)
.order_by("-start_date")
.first()
or False
),
user,
book,
timeout=15552000,
)

View file

@ -0,0 +1,14 @@
""" template filters """
from django import template
register = template.Library()
@register.simple_tag(takes_context=True)
def mutuals_count(context, user):
"""how many users that you follow, follow them"""
viewer = context["request"].user
if not viewer.is_authenticated:
return None
return user.followers.filter(followers=viewer).count()

View file

@ -1,6 +1,9 @@
""" testing activitystreams """
from datetime import datetime
from unittest.mock import patch
from django.test import TestCase
from django.utils import timezone
from bookwyrm import activitystreams, models
@ -51,13 +54,63 @@ class Activitystreams(TestCase):
"""the abstract base class for stream objects"""
self.assertEqual(
self.test_stream.stream_id(self.local_user),
"{}-test".format(self.local_user.id),
f"{self.local_user.id}-test",
)
self.assertEqual(
self.test_stream.unread_id(self.local_user),
"{}-test-unread".format(self.local_user.id),
f"{self.local_user.id}-test-unread",
)
def test_unread_by_status_type_id(self, *_):
"""stream for status type"""
self.assertEqual(
self.test_stream.unread_by_status_type_id(self.local_user),
f"{self.local_user.id}-test-unread-by-type",
)
def test_get_rank(self, *_):
"""sort order"""
date = datetime(2022, 1, 28, 0, 0, tzinfo=timezone.utc)
status = models.Status.objects.create(
user=self.remote_user,
content="hi",
privacy="direct",
published_date=date,
)
self.assertEqual(
str(self.test_stream.get_rank(status)),
"1643328000.0",
)
def test_get_activity_stream(self, *_):
"""load statuses"""
status = models.Status.objects.create(
user=self.remote_user,
content="hi",
privacy="direct",
)
status2 = models.Comment.objects.create(
user=self.remote_user,
content="hi",
privacy="direct",
book=self.book,
)
models.Comment.objects.create(
user=self.remote_user,
content="hi",
privacy="direct",
book=self.book,
)
with patch("bookwyrm.activitystreams.r.set"), patch(
"bookwyrm.activitystreams.r.delete"
), patch("bookwyrm.activitystreams.ActivityStream.get_store") as redis_mock:
redis_mock.return_value = [status.id, status2.id]
result = self.test_stream.get_activity_stream(self.local_user)
self.assertEqual(result.count(), 2)
self.assertEqual(result.first(), status2)
self.assertEqual(result.last(), status)
self.assertIsInstance(result.first(), models.Comment)
def test_abstractstream_get_audience(self, *_):
"""get a list of users that should see a status"""
status = models.Status.objects.create(

View file

@ -52,3 +52,29 @@ class Activitystreams(TestCase):
# yes book, yes audience
result = activitystreams.BooksStream().get_statuses_for_user(self.local_user)
self.assertEqual(list(result), [status])
def test_book_statuses(self, *_):
"""statuses about a book"""
alt_book = models.Edition.objects.create(
title="hi", parent_work=self.book.parent_work
)
status = models.Status.objects.create(
user=self.local_user, content="hi", privacy="public"
)
status = models.Comment.objects.create(
user=self.remote_user, content="hi", privacy="public", book=alt_book
)
models.ShelfBook.objects.create(
user=self.local_user,
shelf=self.local_user.shelf_set.first(),
book=self.book,
)
with patch(
"bookwyrm.activitystreams.BooksStream.bulk_add_objects_to_store"
) as redis_mock:
activitystreams.BooksStream().add_book_statuses(self.local_user, self.book)
args = redis_mock.call_args[0]
queryset = args[0]
self.assertEqual(queryset.count(), 1)
self.assertTrue(status in queryset)
self.assertEqual(args[1], f"{self.local_user.id}-books")

View file

@ -4,8 +4,8 @@ from django.test import TestCase
import responses
from bookwyrm import models
from bookwyrm.connectors import abstract_connector
from bookwyrm.connectors.abstract_connector import Mapping
from bookwyrm.connectors import abstract_connector, ConnectorException
from bookwyrm.connectors.abstract_connector import Mapping, get_data
from bookwyrm.settings import DOMAIN
@ -163,3 +163,11 @@ class AbstractConnector(TestCase):
author.refresh_from_db()
self.assertEqual(author.name, "Test")
self.assertEqual(author.isni, "hi")
def test_get_data_invalid_url(self):
"""load json data from an arbitrary url"""
with self.assertRaises(ConnectorException):
get_data("file://hello.com/image/jpg")
with self.assertRaises(ConnectorException):
get_data("http://127.0.0.1/image/jpg")

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