Merge branch 'main' into openlibrary-author-fields
This commit is contained in:
commit
39691bed3a
90 changed files with 1841 additions and 2557 deletions
|
@ -227,7 +227,7 @@ def set_related_field(
|
|||
model_field = getattr(model, related_field_name)
|
||||
if hasattr(model_field, "activitypub_field"):
|
||||
setattr(activity, getattr(model_field, "activitypub_field"), instance.remote_id)
|
||||
item = activity.to_model()
|
||||
item = activity.to_model(model=model)
|
||||
|
||||
# if the related field isn't serialized (attachments on Status), then
|
||||
# we have to set it post-creation
|
||||
|
@ -298,6 +298,7 @@ class Link(ActivityObject):
|
|||
mediaType: str = None
|
||||
id: str = None
|
||||
attributedTo: str = None
|
||||
availability: str = None
|
||||
type: str = "Link"
|
||||
|
||||
def serialize(self, **kwargs):
|
||||
|
|
54
bookwyrm/apps.py
Normal file
54
bookwyrm/apps.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
"""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"
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def ready(self):
|
||||
"""set up OTLP and preview image files, if desired"""
|
||||
if settings.OTEL_EXPORTER_OTLP_ENDPOINT:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
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)
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -7,6 +7,7 @@ from bookwyrm import settings
|
|||
r = redis.Redis(
|
||||
host=settings.REDIS_ACTIVITY_HOST,
|
||||
port=settings.REDIS_ACTIVITY_PORT,
|
||||
password=settings.REDIS_ACTIVITY_PASSWORD,
|
||||
db=settings.REDIS_ACTIVITY_DB_INDEX,
|
||||
)
|
||||
|
||||
|
|
37
bookwyrm/migrations/0132_alter_user_preferred_language.py
Normal file
37
bookwyrm/migrations/0132_alter_user_preferred_language.py
Normal 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,
|
||||
),
|
||||
),
|
||||
]
|
21
bookwyrm/migrations/0133_alter_listitem_notes.py
Normal file
21
bookwyrm/migrations/0133_alter_listitem_notes.py
Normal 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
|
||||
),
|
||||
),
|
||||
]
|
29
bookwyrm/migrations/0134_announcement_display_type.py
Normal file
29
bookwyrm/migrations/0134_announcement_display_type.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 3.2.11 on 2022-02-11 18:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0133_alter_listitem_notes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="announcement",
|
||||
name="display_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("white-ter", "None"),
|
||||
("primary-light", "Primary"),
|
||||
("success-light", "Success"),
|
||||
("link-light", "Link"),
|
||||
("warning-light", "Warning"),
|
||||
("danger-light", "Danger"),
|
||||
],
|
||||
default="white-ter",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -2,10 +2,21 @@
|
|||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .base_model import BookWyrmModel
|
||||
|
||||
|
||||
DisplayTypes = [
|
||||
("white-ter", _("None")),
|
||||
("primary-light", _("Primary")),
|
||||
("success-light", _("Success")),
|
||||
("link-light", _("Link")),
|
||||
("warning-light", _("Warning")),
|
||||
("danger-light", _("Danger")),
|
||||
]
|
||||
|
||||
|
||||
class Announcement(BookWyrmModel):
|
||||
"""The admin has something to say"""
|
||||
|
||||
|
@ -16,6 +27,13 @@ class Announcement(BookWyrmModel):
|
|||
start_date = models.DateTimeField(blank=True, null=True)
|
||||
end_date = models.DateTimeField(blank=True, null=True)
|
||||
active = models.BooleanField(default=True)
|
||||
display_type = models.CharField(
|
||||
max_length=20,
|
||||
blank=False,
|
||||
null=False,
|
||||
choices=DisplayTypes,
|
||||
default="white-ter",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def active_announcements(cls):
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -142,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, max_length=300)
|
||||
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")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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", ""))
|
||||
|
|
|
@ -9,7 +9,7 @@ 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")
|
||||
|
@ -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/
|
||||
|
@ -150,6 +164,9 @@ LOGGING = {
|
|||
"handlers": ["console", "mail_admins"],
|
||||
"level": LOG_LEVEL,
|
||||
},
|
||||
"django.utils.autoreload": {
|
||||
"level": "INFO",
|
||||
},
|
||||
# Add a bookwyrm-specific logger
|
||||
"bookwyrm": {
|
||||
"handlers": ["console"],
|
||||
|
@ -255,7 +272,7 @@ LANGUAGES = [
|
|||
("no-no", _("Norsk (Norwegian)")),
|
||||
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
|
||||
("pt-pt", _("Português Europeu (European Portuguese)")),
|
||||
("sv-se", _("Swedish (Svenska)")),
|
||||
("sv-se", _("Svenska (Swedish)")),
|
||||
("zh-hans", _("简体中文 (Simplified Chinese)")),
|
||||
("zh-hant", _("繁體中文 (Traditional Chinese)")),
|
||||
]
|
||||
|
@ -311,13 +328,12 @@ 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)
|
||||
OTEL_SERVICE_NAME = env("OTEL_SERVICE_NAME", None)
|
||||
|
|
|
@ -319,7 +319,7 @@ details.details-panel summary {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
details.details-panel summary .details-close {
|
||||
details summary .details-close {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
@ -327,7 +327,7 @@ details.details-panel summary .details-close {
|
|||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
details[open].details-panel summary .details-close {
|
||||
details[open] summary .details-close {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
|
@ -496,7 +496,7 @@ details[open].details-panel summary .details-close {
|
|||
max-height: 100%;
|
||||
|
||||
/* Useful when stretching under-sized images. */
|
||||
image-rendering: optimizeQuality;
|
||||
image-rendering: optimizequality;
|
||||
image-rendering: smooth;
|
||||
}
|
||||
|
||||
|
|
96
bookwyrm/static/fonts/source_han_sans/LICENSE.txt
Normal file
96
bookwyrm/static/fonts/source_han_sans/LICENSE.txt
Normal 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.
|
9
bookwyrm/static/fonts/source_han_sans/README.txt
Normal file
9
bookwyrm/static/fonts/source_han_sans/README.txt
Normal 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
|
22
bookwyrm/telemetry/open_telemetry.py
Normal file
22
bookwyrm/telemetry/open_telemetry.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from opentelemetry import trace
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
|
||||
trace.set_tracer_provider(TracerProvider())
|
||||
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
|
||||
|
||||
|
||||
def instrumentDjango():
|
||||
from opentelemetry.instrumentation.django import DjangoInstrumentor
|
||||
|
||||
DjangoInstrumentor().instrument()
|
||||
|
||||
|
||||
def instrumentCelery():
|
||||
from opentelemetry.instrumentation.celery import CeleryInstrumentor
|
||||
from celery.signals import worker_process_init
|
||||
|
||||
@worker_process_init.connect(weak=False)
|
||||
def init_celery_tracing(*args, **kwargs):
|
||||
CeleryInstrumentor().instrument()
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
<div class="columns">
|
||||
{% if superlatives.top_rated %}
|
||||
{% with book=superlatives.top_rated.default_edition rating=top_rated.rating %}
|
||||
{% 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">
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -6,24 +6,24 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
<header class="block content">
|
||||
<header class="block">
|
||||
<h1 class="title">
|
||||
{% blocktrans with title=book|book_title %}
|
||||
Links for "<em>{{ title }}</em>"
|
||||
{% endblocktrans %}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'book' book.id %}">{{ book|book_title }}</a></li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
{% trans "Edit links" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'book' book.id %}">{{ book|book_title }}</a></li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
{% trans "Edit links" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section class="block content">
|
||||
<div class="table-container">
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
<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' %}
|
||||
{% include 'snippets/book_cover.html' with cover_class='is-w-auto is-h-m-mobile is-h-m-tablet is-align-items-flex-start' size='medium' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
@ -79,14 +79,12 @@
|
|||
<div class="media-content">
|
||||
<div class="content">
|
||||
<header>
|
||||
{% url 'user-feed' user|username as user_path %}
|
||||
{% blocktrans trimmed with username=user.display_name %}
|
||||
{% 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 %}
|
||||
</header>
|
||||
<p>
|
||||
{{ item.notes|to_markdown|safe }}
|
||||
</p>
|
||||
{{ item.notes|to_markdown|safe }}
|
||||
</div>
|
||||
{% if item.user == request.user %}
|
||||
<div>
|
||||
|
@ -97,7 +95,7 @@
|
|||
<span class="details-close icon icon-pencil" aria-hidden></span>
|
||||
</span>
|
||||
</summary>
|
||||
{% include "lists/edit_item_form.html" %}
|
||||
{% include "lists/edit_item_form.html" with book=item.book %}
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -112,7 +110,7 @@
|
|||
<span class="details-close icon icon-plus" aria-hidden></span>
|
||||
</span>
|
||||
</summary>
|
||||
{% include "lists/edit_item_form.html" %}
|
||||
{% include "lists/edit_item_form.html" with book=item.book %}
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
{% extends 'settings/layout.html' %}
|
||||
{% load i18n %}{% load humanize %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block title %}{% trans "Announcement" %} - {{ announcement.preview }}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
{% trans "Announcement" %}
|
||||
<a href="{% url 'settings-announcements' %}" class="has-text-weight-normal help">{% trans "Back to list" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block edit-button %}
|
||||
{% trans "Edit Announcement" as button_text %}
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
{% include 'snippets/toggle/open_button.html' with controls_text="edit_announcement" icon_with_text="pencil" text=button_text focus="edit_announcement_header" %}
|
||||
<a class="button" href="{% url 'settings-announcements-edit' announcement.id %}">
|
||||
<span class="icon icon-pencil m-0-mobile" aria-hidden="true"></span>
|
||||
<span class="is-sr-only-mobile">{% trans "Edit" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
<form class="control" action="{% url 'settings-announcements-delete' announcement.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
|
@ -23,12 +26,20 @@
|
|||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'settings-announcements' %}">{% trans "Announcements" %}</a></li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
{{ announcement.preview|truncatechars:30 }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
|
||||
<form name="edit-announcement" method="post" action="{% url 'settings-announcements' announcement.id %}" class="block">
|
||||
{% include 'settings/announcements/announcement_form.html' with controls_text="edit_announcement" %}
|
||||
</form>
|
||||
|
||||
<div class="block content">
|
||||
<dl>
|
||||
<dt class="is-pulled-left mr-5 has-text-weight-bold">{% trans "Visible:" %}</dt>
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
{% extends 'components/inline_form.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block header %}
|
||||
{% if announcement %}
|
||||
{% trans "Edit Announcement" %}
|
||||
{% else %}
|
||||
{% trans "Create Announcement" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||
<p>
|
||||
<label class="label" for="id_preview">
|
||||
{% trans "Preview:" %}
|
||||
</label>
|
||||
{{ form.preview }}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.preview.errors id="desc_preview" %}
|
||||
</p>
|
||||
<p>
|
||||
<label class="label" for="id_content">
|
||||
{% trans "Content:" %}
|
||||
</label>
|
||||
{{ form.content }}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.content.errors id="desc_content" %}
|
||||
</p>
|
||||
<p>
|
||||
<label class="label" for="id_event_date">
|
||||
{% trans "Event date:" %}
|
||||
</label>
|
||||
<input type="date" name="event_date" value="{{ form.event_date.value|date:'Y-m-d' }}" class="input" id="id_event_date">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.event_date.errors id="desc_event_date" %}
|
||||
</p>
|
||||
<hr aria-hidden="true">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<p>
|
||||
<label class="label" for="id_start_date">
|
||||
{% trans "Start date:" %}
|
||||
</label>
|
||||
<input type="date" name="start_date" class="input" value="{{ form.start_date.value|date:'Y-m-d' }}" id="id_start_date">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.start_date.errors id="desc_start_date" %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<p>
|
||||
<label class="label" for="id_end_date">
|
||||
{% trans "End date:" %}
|
||||
</label>
|
||||
<input type="date" name="end_date" class="input" id="id_end_date" value="{{ form.end_date.value|date:'Y-m-d' }}">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.end_date.errors id="desc_end_date" %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<p>
|
||||
<label class="label" for="id_active">
|
||||
{% trans "Active:" %}
|
||||
</label>
|
||||
{{ form.active }}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.active.errors id="desc_active" %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -5,16 +5,15 @@
|
|||
{% block header %}{% trans "Announcements" %}{% endblock %}
|
||||
|
||||
{% block edit-button %}
|
||||
{% trans "Create Announcement" as button_text %}
|
||||
{% include 'snippets/toggle/open_button.html' with controls_text="create_announcement" icon_with_text="plus" text=button_text focus="create_announcement_header" %}
|
||||
<a href="{% url 'settings-announcements-edit' %}">
|
||||
{% trans "Create Announcement" as text %}
|
||||
<span class="icon icon-plus" title="{{ text }}" aria-hidden="true"></span>
|
||||
<span class="is-sr-only-mobile">{{ text }}</span>
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<form name="create-announcement" method="post" action="{% url 'settings-announcements' %}" class="block">
|
||||
{% include 'settings/announcements/announcement_form.html' with controls_text="create_announcement" %}
|
||||
</form>
|
||||
|
||||
<div class="block">
|
||||
<div class="block table-container">
|
||||
<table class="table is-striped">
|
||||
<tr>
|
||||
<th>
|
||||
|
@ -38,6 +37,9 @@
|
|||
{% trans "Status" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="active" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Actions" %}
|
||||
</th>
|
||||
</tr>
|
||||
{% for announcement in announcements %}
|
||||
<tr>
|
||||
|
@ -46,6 +48,15 @@
|
|||
<td>{{ announcement.start_date|naturaltime|default:'' }}</td>
|
||||
<td>{{ announcement.end_date|naturaltime|default:'' }}</td>
|
||||
<td>{% if announcement.active %}{% trans "active" %}{% else %}{% trans "inactive" %}{% endif %}</td>
|
||||
<td>
|
||||
<form class="control" action="{% url 'settings-announcements-delete' announcement.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="button is-danger is-light is-small">
|
||||
<span class="icon icon-x m-0-mobile" aria-hidden="true"></span>
|
||||
<span class="is-sr-only-mobile">{% trans "Delete" %}</span>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not announcements %}
|
||||
|
|
125
bookwyrm/templates/settings/announcements/edit_announcement.html
Normal file
125
bookwyrm/templates/settings/announcements/edit_announcement.html
Normal file
|
@ -0,0 +1,125 @@
|
|||
{% extends 'settings/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block header %}
|
||||
{% if announcement %}
|
||||
{% trans "Edit Announcement" %}
|
||||
{% else %}
|
||||
{% trans "Create Announcement" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'settings-announcements' %}">{% trans "Announcements" %}</a></li>
|
||||
{% if announcement %}
|
||||
<li>
|
||||
<a href="{% url 'settings-announcements' announcement.id %}">
|
||||
{{ announcement.preview|truncatechars:30 }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
Edit
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<form
|
||||
name="edit-announcement"
|
||||
method="POST"
|
||||
{% if announcement.id %}
|
||||
action="{% url 'settings-announcements-edit' announcement.id %}"
|
||||
{% else %}
|
||||
action="{% url 'settings-announcements-edit' %}"
|
||||
{% endif %}
|
||||
class="block"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||
<h2 class="title is-4">{% trans "Announcement content" %}</h2>
|
||||
<div class="box">
|
||||
<p class="field">
|
||||
<label class="label" for="id_preview">
|
||||
{% trans "Summary:" %}
|
||||
</label>
|
||||
{{ form.preview }}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.preview.errors id="desc_preview" %}
|
||||
</p>
|
||||
<p class="field">
|
||||
<label class="label" for="id_content">
|
||||
{% trans "Details:" %}
|
||||
</label>
|
||||
{{ form.content }}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.content.errors id="desc_content" %}
|
||||
</p>
|
||||
<p class="field">
|
||||
<label class="label" for="id_event_date">
|
||||
{% trans "Event date:" %}
|
||||
</label>
|
||||
<input type="date" name="event_date" value="{{ form.event_date.value|date:'Y-m-d' }}" class="input" id="id_event_date">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.event_date.errors id="desc_event_date" %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="title is-4">{% trans "Display settings" %}</h2>
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<p>
|
||||
<label class="label" for="id_start_date">
|
||||
{% trans "Start date:" %}
|
||||
</label>
|
||||
<input type="date" name="start_date" class="input" value="{{ form.start_date.value|date:'Y-m-d' }}" id="id_start_date">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.start_date.errors id="desc_start_date" %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<p>
|
||||
<label class="label" for="id_end_date">
|
||||
{% trans "End date:" %}
|
||||
</label>
|
||||
<input type="date" name="end_date" class="input" id="id_end_date" value="{{ form.end_date.value|date:'Y-m-d' }}">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.end_date.errors id="desc_end_date" %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<label class="label" for="id_active">
|
||||
{% trans "Color:" %}
|
||||
</label>
|
||||
<div class="select">
|
||||
{{ form.display_type }}
|
||||
</div>
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.active.errors id="desc_display_type" %}
|
||||
</div>
|
||||
</div>
|
||||
<p class="field">
|
||||
<label class="label" for="id_active">
|
||||
{% trans "Active:" %}
|
||||
{{ form.active }}
|
||||
</label>
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.active.errors id="desc_active" %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -9,6 +9,7 @@
|
|||
<div class="columns is-mobile">
|
||||
<div class="column">
|
||||
<h1 class="title">{% block header %}{% endblock %}</h1>
|
||||
{% block breadcrumbs %}{% endblock %}
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
{% block edit-button %}{% endblock %}
|
||||
|
@ -16,6 +17,7 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="block columns">
|
||||
<nav class="menu column is-one-quarter">
|
||||
<h2 class="menu-label">
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -1,32 +1,29 @@
|
|||
{% load humanize %}{% load i18n %}{% load utilities %}
|
||||
{% with announcement.id|uuid as uuid %}
|
||||
<aside
|
||||
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 has-background-{{ announcement.display_type }}"
|
||||
{% if not admin_mode %}data-hide="hide_announcement_{{ announcement.id }}"{% endif %}
|
||||
>
|
||||
<div class="columns mb-0 is-mobile">
|
||||
<div class="column pb-0">
|
||||
<details>
|
||||
<summary>
|
||||
{% if announcement.event_date %}
|
||||
<strong>{{ announcement.event_date|naturalday|title }}:</strong>
|
||||
{% endif %}
|
||||
{{ announcement.preview }}
|
||||
</div>
|
||||
|
||||
{{ announcement.preview|safe }}
|
||||
|
||||
{% if announcement.content %}
|
||||
<span class="details-close mt-4 mr-4 icon icon-x is-small" aria-hidden></span>
|
||||
{% endif %}
|
||||
</summary>
|
||||
{% if announcement.content %}
|
||||
<div class="column is-narrow pb-0">
|
||||
{% trans "Open" as button_text %}
|
||||
{% include 'snippets/toggle/open_button.html' with text=button_text controls_text="announcement" class="is-small" controls_uid=uuid icon_with_text="arrow-down" %}
|
||||
{% trans "Close" as button_text %}
|
||||
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="announcement" class="is-small" controls_uid=uuid icon_with_text="arrow-up" %}
|
||||
<div class="mb-2 mt-2" id="announcement_{{ uuid }}">
|
||||
<div class="box is-shadowless mb-0">
|
||||
{{ announcement.content|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if announcement.content %}
|
||||
<div class="mb-2 mt-2 {% if not pressed %}is-hidden{% endif %}" id="announcement_{{ uuid }}">
|
||||
<div class="box is-shadowless mb-0">
|
||||
{{ announcement.content|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</details>
|
||||
<div class="is-flex mt-0 help">
|
||||
<p>{% blocktrans with user_path=announcement.user.local_path username=announcement.user.display_name %}Posted by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p>
|
||||
{% if not admin_mode %}
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
""" testing models """
|
||||
from dateutil.parser import parse
|
||||
|
||||
from imagekit.models import ImageSpecField
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
|
|
|
@ -443,18 +443,17 @@ class ModelFields(TestCase):
|
|||
image_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../../static/images/default_avi.jpg"
|
||||
)
|
||||
image = Image.open(image_file)
|
||||
output = BytesIO()
|
||||
image.save(output, format=image.format)
|
||||
|
||||
instance = fields.ImageField()
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=image.tobytes(),
|
||||
status=200,
|
||||
)
|
||||
with open(image_file, "rb") as image_data:
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=image_data.read(),
|
||||
status=200,
|
||||
content_type="image/jpeg",
|
||||
stream=True,
|
||||
)
|
||||
loaded_image = instance.field_from_activity("http://www.example.com/image.jpg")
|
||||
self.assertIsInstance(loaded_image, list)
|
||||
self.assertIsInstance(loaded_image[1], ContentFile)
|
||||
|
@ -465,18 +464,18 @@ class ModelFields(TestCase):
|
|||
image_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../../static/images/default_avi.jpg"
|
||||
)
|
||||
image = Image.open(image_file)
|
||||
output = BytesIO()
|
||||
image.save(output, format=image.format)
|
||||
|
||||
instance = fields.ImageField(activitypub_field="cover", name="cover")
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=image.tobytes(),
|
||||
status=200,
|
||||
)
|
||||
with open(image_file, "rb") as image_data:
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=image_data.read(),
|
||||
content_type="image/jpeg",
|
||||
status=200,
|
||||
stream=True,
|
||||
)
|
||||
book = Edition.objects.create(title="hello")
|
||||
|
||||
MockActivity = namedtuple("MockActivity", ("cover"))
|
||||
|
@ -491,18 +490,18 @@ class ModelFields(TestCase):
|
|||
image_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../../static/images/default_avi.jpg"
|
||||
)
|
||||
image = Image.open(image_file)
|
||||
output = BytesIO()
|
||||
image.save(output, format=image.format)
|
||||
|
||||
instance = fields.ImageField(activitypub_field="cover", name="cover")
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=image.tobytes(),
|
||||
status=200,
|
||||
)
|
||||
with open(image_file, "rb") as image_data:
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=image_data.read(),
|
||||
status=200,
|
||||
content_type="image/jpeg",
|
||||
stream=True,
|
||||
)
|
||||
book = Edition.objects.create(title="hello")
|
||||
|
||||
MockActivity = namedtuple("MockActivity", ("cover"))
|
||||
|
@ -565,18 +564,18 @@ class ModelFields(TestCase):
|
|||
another_image_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../../static/images/logo.png"
|
||||
)
|
||||
another_image = Image.open(another_image_file)
|
||||
another_output = BytesIO()
|
||||
another_image.save(another_output, format=another_image.format)
|
||||
|
||||
instance = fields.ImageField(activitypub_field="cover", name="cover")
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=another_image.tobytes(),
|
||||
status=200,
|
||||
)
|
||||
with open(another_image_file, "rb") as another_image:
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://www.example.com/image.jpg",
|
||||
body=another_image.read(),
|
||||
status=200,
|
||||
content_type="image/jpeg",
|
||||
stream=True,
|
||||
)
|
||||
|
||||
MockActivity = namedtuple("MockActivity", ("cover"))
|
||||
mock_activity = MockActivity("http://www.example.com/image.jpg")
|
||||
|
|
|
@ -24,13 +24,21 @@ class Sanitizer(TestCase):
|
|||
self.assertEqual(input_text, output)
|
||||
|
||||
def test_valid_html_attrs(self):
|
||||
"""and don't remove attributes"""
|
||||
"""and don't remove useful attributes"""
|
||||
input_text = '<a href="fish.com">yes </a> <i>html</i>'
|
||||
parser = InputHtmlParser()
|
||||
parser.feed(input_text)
|
||||
output = parser.get_output()
|
||||
self.assertEqual(input_text, output)
|
||||
|
||||
def test_valid_html_invalid_attrs(self):
|
||||
"""do remove un-approved attributes"""
|
||||
input_text = '<a href="fish.com" fish="hello">yes </a> <i>html</i>'
|
||||
parser = InputHtmlParser()
|
||||
parser.feed(input_text)
|
||||
output = parser.get_output()
|
||||
self.assertEqual(output, '<a href="fish.com">yes </a> <i>html</i>')
|
||||
|
||||
def test_invalid_html(self):
|
||||
"""remove all html when the html is malformed"""
|
||||
input_text = "<b>yes <i>html</i>"
|
||||
|
|
|
@ -74,11 +74,12 @@ class AnnouncementViews(TestCase):
|
|||
|
||||
def test_create_announcement(self):
|
||||
"""create a new announcement"""
|
||||
view = views.Announcements.as_view()
|
||||
view = views.EditAnnouncement.as_view()
|
||||
form = forms.AnnouncementForm()
|
||||
form.data["preview"] = "hi hi"
|
||||
form.data["start_date"] = "2021-05-20"
|
||||
form.data["user"] = self.local_user.id
|
||||
form.data["display_type"] = "warning-light"
|
||||
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
@ -97,11 +98,12 @@ class AnnouncementViews(TestCase):
|
|||
announcement = models.Announcement.objects.create(
|
||||
preview="hi", user=self.local_user
|
||||
)
|
||||
view = views.Announcement.as_view()
|
||||
view = views.EditAnnouncement.as_view()
|
||||
form = forms.AnnouncementForm(instance=announcement)
|
||||
form.data["preview"] = "hi hi"
|
||||
form.data["start_date"] = "2021-05-20"
|
||||
form.data["user"] = self.local_user.id
|
||||
form.data["display_type"] = "warning-light"
|
||||
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
|
|
@ -80,6 +80,15 @@ class LinkViews(TestCase):
|
|||
activity = json.loads(mock.call_args[1]["args"][1])
|
||||
self.assertEqual(activity["type"], "Update")
|
||||
self.assertEqual(activity["object"]["type"], "Edition")
|
||||
self.assertIsInstance(activity["object"]["fileLinks"], list)
|
||||
self.assertEqual(
|
||||
activity["object"]["fileLinks"][0]["href"], "https://www.example.com"
|
||||
)
|
||||
self.assertEqual(activity["object"]["fileLinks"][0]["mediaType"], "HTML")
|
||||
self.assertEqual(
|
||||
activity["object"]["fileLinks"][0]["attributedTo"],
|
||||
self.local_user.remote_id,
|
||||
)
|
||||
|
||||
link = models.FileLink.objects.get()
|
||||
self.assertEqual(link.name, "www.example.com")
|
||||
|
|
|
@ -6,7 +6,6 @@ from unittest.mock import patch
|
|||
from django.test import TestCase
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.activitypub.base_activity import set_related_field
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
|
|
@ -85,6 +85,7 @@ class ListViews(TestCase):
|
|||
user=self.local_user,
|
||||
book=self.book,
|
||||
approved=True,
|
||||
notes="hello",
|
||||
order=1,
|
||||
)
|
||||
|
||||
|
@ -178,6 +179,7 @@ class ListViews(TestCase):
|
|||
book_list=self.list,
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
notes="hi hello",
|
||||
approved=True,
|
||||
order=1,
|
||||
)
|
||||
|
|
|
@ -67,4 +67,4 @@ class ListItemViews(TestCase):
|
|||
self.assertEqual(mock.call_count, 1)
|
||||
|
||||
item.refresh_from_db()
|
||||
self.assertEqual(item.notes, "beep boop")
|
||||
self.assertEqual(item.notes, "<p>beep boop</p>")
|
||||
|
|
|
@ -50,6 +50,43 @@ class AuthorViews(TestCase):
|
|||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_author_page(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Author.as_view()
|
||||
author = models.Author.objects.create(name="Jessica")
|
||||
self.book.authors.add(author)
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.views.author.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, author.id)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_author_page_edition_author(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Author.as_view()
|
||||
another_book = models.Edition.objects.create(
|
||||
title="Example Edition",
|
||||
remote_id="https://example.com/book/1",
|
||||
parent_work=self.work,
|
||||
isbn_13="9780300112511",
|
||||
)
|
||||
author = models.Author.objects.create(name="Jessica")
|
||||
self.book.authors.add(author)
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.views.author.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, author.id)
|
||||
books = result.context_data["books"]
|
||||
self.assertEqual(books.object_list.count(), 1)
|
||||
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_author_page_empty(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Author.as_view()
|
||||
author = models.Author.objects.create(name="Jessica")
|
||||
|
|
|
@ -93,6 +93,16 @@ urlpatterns = [
|
|||
views.Announcement.as_view(),
|
||||
name="settings-announcements",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/announcements/create/?$",
|
||||
views.EditAnnouncement.as_view(),
|
||||
name="settings-announcements-edit",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/announcements/(?P<announcement_id>\d+)/edit/?$",
|
||||
views.EditAnnouncement.as_view(),
|
||||
name="settings-announcements-edit",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/announcements/(?P<announcement_id>\d+)/delete/?$",
|
||||
views.delete_announcement,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
""" make sure all our nice views are available """
|
||||
# site admin
|
||||
from .admin.announcements import Announcements, Announcement, delete_announcement
|
||||
from .admin.announcements import Announcements, Announcement
|
||||
from .admin.announcements import EditAnnouncement, delete_announcement
|
||||
from .admin.dashboard import Dashboard
|
||||
from .admin.federation import Federation, FederatedServer
|
||||
from .admin.federation import AddFederatedServer, ImportServerBlocklist
|
||||
|
|
|
@ -45,23 +45,6 @@ class Announcements(View):
|
|||
request, "settings/announcements/announcements.html", data
|
||||
)
|
||||
|
||||
def post(self, request):
|
||||
"""edit the site settings"""
|
||||
form = forms.AnnouncementForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
# reset the create form
|
||||
form = forms.AnnouncementForm()
|
||||
data = {
|
||||
"announcements": Paginator(
|
||||
models.Announcement.objects.order_by("-created_date"), PAGE_LENGTH
|
||||
).get_page(request.GET.get("page")),
|
||||
"form": form,
|
||||
}
|
||||
return TemplateResponse(
|
||||
request, "settings/announcements/announcements.html", data
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
|
@ -76,27 +59,52 @@ class Announcement(View):
|
|||
announcement = get_object_or_404(models.Announcement, id=announcement_id)
|
||||
data = {
|
||||
"announcement": announcement,
|
||||
"form": forms.AnnouncementForm(instance=announcement),
|
||||
}
|
||||
return TemplateResponse(
|
||||
request, "settings/announcements/announcement.html", data
|
||||
)
|
||||
|
||||
def post(self, request, announcement_id):
|
||||
"""edit announcement"""
|
||||
announcement = get_object_or_404(models.Announcement, id=announcement_id)
|
||||
form = forms.AnnouncementForm(request.POST, instance=announcement)
|
||||
if form.is_valid():
|
||||
announcement = form.save()
|
||||
form = forms.AnnouncementForm(instance=announcement)
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class EditAnnouncement(View):
|
||||
"""Create of edit an announcement"""
|
||||
|
||||
def get(self, request, announcement_id=None):
|
||||
"""announcement forms"""
|
||||
announcement = None
|
||||
if announcement_id:
|
||||
announcement = get_object_or_404(models.Announcement, id=announcement_id)
|
||||
|
||||
data = {
|
||||
"announcement": announcement,
|
||||
"form": form,
|
||||
"form": forms.AnnouncementForm(instance=announcement),
|
||||
}
|
||||
return TemplateResponse(
|
||||
request, "settings/announcements/announcement.html", data
|
||||
request, "settings/announcements/edit_announcement.html", data
|
||||
)
|
||||
|
||||
def post(self, request, announcement_id=None):
|
||||
"""edit announcement"""
|
||||
announcement = None
|
||||
if announcement_id:
|
||||
announcement = get_object_or_404(models.Announcement, id=announcement_id)
|
||||
|
||||
form = forms.AnnouncementForm(request.POST, instance=announcement)
|
||||
if not form.is_valid():
|
||||
data = {
|
||||
"announcement": announcement,
|
||||
"form": form,
|
||||
}
|
||||
return TemplateResponse(
|
||||
request, "settings/announcements/edit_announcement.html", data
|
||||
)
|
||||
announcement = form.save()
|
||||
return redirect("settings-announcements", announcement.id)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
""" the good people stuff! the authors! """
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
@ -25,9 +26,11 @@ class Author(View):
|
|||
if is_api_request(request):
|
||||
return ActivitypubResponse(author.to_activity())
|
||||
|
||||
books = models.Work.objects.filter(
|
||||
authors=author, editions__authors=author
|
||||
).distinct()
|
||||
books = (
|
||||
models.Work.objects.filter(Q(authors=author) | Q(editions__authors=author))
|
||||
.order_by("-published_date")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
paginated = Paginator(books, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Avg, Q
|
||||
from django.http import Http404
|
||||
|
@ -144,13 +143,12 @@ def upload_cover(request, book_id):
|
|||
def set_cover_from_url(url):
|
||||
"""load it from a url"""
|
||||
try:
|
||||
image_file = get_image(url)
|
||||
image_content, extension = get_image(url)
|
||||
except: # pylint: disable=bare-except
|
||||
return None
|
||||
if not image_file:
|
||||
if not image_content:
|
||||
return None
|
||||
image_name = str(uuid4()) + "." + url.split(".")[-1]
|
||||
image_content = ContentFile(image_file.content)
|
||||
image_name = str(uuid4()) + "." + extension
|
||||
return [image_name, image_content]
|
||||
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.core.paginator import Paginator
|
|||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.urls import reverse
|
||||
from django.views import View
|
||||
|
||||
from bookwyrm import models
|
||||
|
@ -35,6 +36,7 @@ class ImportTroubleshoot(View):
|
|||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"complete": True,
|
||||
"page_path": reverse("import-troubleshoot", args=[job.id]),
|
||||
}
|
||||
|
||||
return TemplateResponse(request, "import/troubleshoot.html", data)
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.utils.decorators import method_decorator
|
|||
from django.views import View
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.views.status import to_markdown
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
@ -18,5 +19,9 @@ class ListItem(View):
|
|||
list_item.raise_not_editable(request.user)
|
||||
form = forms.ListItemForm(request.POST, instance=list_item)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
item = form.save(commit=False)
|
||||
item.notes = to_markdown(item.notes)
|
||||
item.save()
|
||||
else:
|
||||
raise Exception(form.errors)
|
||||
return redirect("list", list_item.book_list.id)
|
||||
|
|
|
@ -31,12 +31,17 @@ class User(View):
|
|||
shelf_preview = []
|
||||
|
||||
# only show shelves that should be visible
|
||||
shelves = user.shelf_set
|
||||
is_self = request.user.id == user.id
|
||||
if not is_self:
|
||||
shelves = models.Shelf.privacy_filter(
|
||||
request.user, privacy_levels=["public", "followers"]
|
||||
).filter(user=user, books__isnull=False)
|
||||
shelves = (
|
||||
models.Shelf.privacy_filter(
|
||||
request.user, privacy_levels=["public", "followers"]
|
||||
)
|
||||
.filter(user=user, books__isnull=False)
|
||||
.distinct()
|
||||
)
|
||||
else:
|
||||
shelves = user.shelf_set.filter(books__isnull=False).distinct()
|
||||
|
||||
for user_shelf in shelves.all()[:3]:
|
||||
shelf_preview.append(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue