mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-26 11:31:08 +00:00
Merge branch 'bookwyrm-social:main' into main
This commit is contained in:
commit
58fb9ba0d4
16 changed files with 303 additions and 33 deletions
46
bookwyrm/apps.py
Normal file
46
bookwyrm/apps.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
"""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"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
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)
|
|
@ -4,6 +4,7 @@ import os
|
||||||
import textwrap
|
import textwrap
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
import logging
|
||||||
|
|
||||||
import colorsys
|
import colorsys
|
||||||
from colorthief import ColorThief
|
from colorthief import ColorThief
|
||||||
|
@ -17,34 +18,49 @@ from django.db.models import Avg
|
||||||
from bookwyrm import models, settings
|
from bookwyrm import models, settings
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
IMG_WIDTH = settings.PREVIEW_IMG_WIDTH
|
IMG_WIDTH = settings.PREVIEW_IMG_WIDTH
|
||||||
IMG_HEIGHT = settings.PREVIEW_IMG_HEIGHT
|
IMG_HEIGHT = settings.PREVIEW_IMG_HEIGHT
|
||||||
BG_COLOR = settings.PREVIEW_BG_COLOR
|
BG_COLOR = settings.PREVIEW_BG_COLOR
|
||||||
TEXT_COLOR = settings.PREVIEW_TEXT_COLOR
|
TEXT_COLOR = settings.PREVIEW_TEXT_COLOR
|
||||||
DEFAULT_COVER_COLOR = settings.PREVIEW_DEFAULT_COVER_COLOR
|
DEFAULT_COVER_COLOR = settings.PREVIEW_DEFAULT_COVER_COLOR
|
||||||
|
DEFAULT_FONT = settings.PREVIEW_DEFAULT_FONT
|
||||||
TRANSPARENT_COLOR = (0, 0, 0, 0)
|
TRANSPARENT_COLOR = (0, 0, 0, 0)
|
||||||
|
|
||||||
margin = math.floor(IMG_HEIGHT / 10)
|
margin = math.floor(IMG_HEIGHT / 10)
|
||||||
gutter = math.floor(margin / 2)
|
gutter = math.floor(margin / 2)
|
||||||
inner_img_height = math.floor(IMG_HEIGHT * 0.8)
|
inner_img_height = math.floor(IMG_HEIGHT * 0.8)
|
||||||
inner_img_width = math.floor(inner_img_height * 0.7)
|
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):
|
def get_imagefont(name, size):
|
||||||
"""Loads custom font"""
|
"""Loads an ImageFont based on config"""
|
||||||
if font_name == "light":
|
try:
|
||||||
font_path = os.path.join(font_dir, "PublicSans-Light.ttf")
|
config = settings.FONTS[name]
|
||||||
if font_name == "regular":
|
path = os.path.join(settings.FONT_DIR, config["directory"], config["filename"])
|
||||||
font_path = os.path.join(font_dir, "PublicSans-Regular.ttf")
|
return ImageFont.truetype(path, size)
|
||||||
elif font_name == "bold":
|
except KeyError:
|
||||||
font_path = os.path.join(font_dir, "PublicSans-Bold.ttf")
|
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:
|
try:
|
||||||
font = ImageFont.truetype(font_path, size)
|
if weight == "light":
|
||||||
except OSError:
|
font.set_variation_by_name("Light")
|
||||||
font = ImageFont.load_default()
|
if weight == "bold":
|
||||||
|
font.set_variation_by_name("Bold")
|
||||||
|
if weight == "regular":
|
||||||
|
font.set_variation_by_name("Regular")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
return font
|
return font
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
|
||||||
"ol",
|
"ol",
|
||||||
"li",
|
"li",
|
||||||
]
|
]
|
||||||
|
self.allowed_attrs = ["href", "rel", "src", "alt"]
|
||||||
self.tag_stack = []
|
self.tag_stack = []
|
||||||
self.output = []
|
self.output = []
|
||||||
# if the html appears invalid, we just won't allow any at all
|
# 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):
|
def handle_starttag(self, tag, attrs):
|
||||||
"""check if the tag is valid"""
|
"""check if the tag is valid"""
|
||||||
if self.allow_html and tag in self.allowed_tags:
|
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)
|
self.tag_stack.append(tag)
|
||||||
else:
|
else:
|
||||||
self.output.append(("data", ""))
|
self.output.append(("data", ""))
|
||||||
|
|
|
@ -35,6 +35,9 @@ LOCALE_PATHS = [
|
||||||
]
|
]
|
||||||
LANGUAGE_COOKIE_NAME = env.str("LANGUAGE_COOKIE_NAME", "django_language")
|
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"
|
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||||
|
|
||||||
# Preview image
|
# 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_WIDTH = env.int("PREVIEW_IMG_WIDTH", 1200)
|
||||||
PREVIEW_IMG_HEIGHT = env.int("PREVIEW_IMG_HEIGHT", 630)
|
PREVIEW_IMG_HEIGHT = env.int("PREVIEW_IMG_HEIGHT", 630)
|
||||||
PREVIEW_DEFAULT_COVER_COLOR = env.str("PREVIEW_DEFAULT_COVER_COLOR", "#002549")
|
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
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||||
|
@ -150,6 +164,9 @@ LOGGING = {
|
||||||
"handlers": ["console", "mail_admins"],
|
"handlers": ["console", "mail_admins"],
|
||||||
"level": LOG_LEVEL,
|
"level": LOG_LEVEL,
|
||||||
},
|
},
|
||||||
|
"django.utils.autoreload": {
|
||||||
|
"level": "INFO",
|
||||||
|
},
|
||||||
# Add a bookwyrm-specific logger
|
# Add a bookwyrm-specific logger
|
||||||
"bookwyrm": {
|
"bookwyrm": {
|
||||||
"handlers": ["console"],
|
"handlers": ["console"],
|
||||||
|
@ -311,13 +328,8 @@ if USE_S3:
|
||||||
MEDIA_FULL_URL = MEDIA_URL
|
MEDIA_FULL_URL = MEDIA_URL
|
||||||
STATIC_FULL_URL = STATIC_URL
|
STATIC_FULL_URL = STATIC_URL
|
||||||
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
|
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:
|
else:
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
|
|
||||||
MEDIA_URL = "/images/"
|
MEDIA_URL = "/images/"
|
||||||
MEDIA_FULL_URL = f"{PROTOCOL}://{DOMAIN}{MEDIA_URL}"
|
MEDIA_FULL_URL = f"{PROTOCOL}://{DOMAIN}{MEDIA_URL}"
|
||||||
STATIC_FULL_URL = f"{PROTOCOL}://{DOMAIN}{STATIC_URL}"
|
STATIC_FULL_URL = f"{PROTOCOL}://{DOMAIN}{STATIC_URL}"
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
|
|
||||||
|
|
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
|
|
@ -28,7 +28,7 @@
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
{% if superlatives.top_rated %}
|
{% 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="column is-one-third is-flex">
|
||||||
<div class="media notification">
|
<div class="media notification">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
|
|
|
@ -56,9 +56,7 @@
|
||||||
|
|
||||||
{% block modal-footer %}
|
{% block modal-footer %}
|
||||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||||
{% if not static %}
|
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block modal-form-close %}</form>{% endblock %}
|
{% block modal-form-close %}</form>{% endblock %}
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
<span class="icon icon-spinner is-pulled-left" aria-hidden="true"></span>
|
<span class="icon icon-spinner is-pulled-left" aria-hidden="true"></span>
|
||||||
<span>{% trans "In progress" %}</span>
|
<span>{% trans "In progress" %}</span>
|
||||||
<span class="is-pulled-right">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="is-flex">
|
<div class="is-flex">
|
||||||
|
@ -230,7 +230,7 @@
|
||||||
|
|
||||||
{% if not legacy %}
|
{% if not legacy %}
|
||||||
<div>
|
<div>
|
||||||
{% include 'snippets/pagination.html' with page=items %}
|
{% include 'snippets/pagination.html' with page=items path=page_path %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endspaceless %}{% endblock %}
|
{% endspaceless %}{% endblock %}
|
||||||
|
|
|
@ -70,9 +70,7 @@
|
||||||
|
|
||||||
{% block modal-footer %}
|
{% block modal-footer %}
|
||||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||||
{% if not static %}
|
|
||||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal-form-close %}
|
{% block modal-form-close %}
|
||||||
|
|
|
@ -50,9 +50,7 @@
|
||||||
{% block modal-footer %}
|
{% block modal-footer %}
|
||||||
|
|
||||||
<button class="button is-success" type="submit">{% trans "Submit" %}</button>
|
<button class="button is-success" type="submit">{% trans "Submit" %}</button>
|
||||||
{% if not static %}
|
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
""" testing activitystreams """
|
""" testing activitystreams """
|
||||||
|
from datetime import datetime
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm import activitystreams, models
|
from bookwyrm import activitystreams, models
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,13 +54,63 @@ class Activitystreams(TestCase):
|
||||||
"""the abstract base class for stream objects"""
|
"""the abstract base class for stream objects"""
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.test_stream.stream_id(self.local_user),
|
self.test_stream.stream_id(self.local_user),
|
||||||
"{}-test".format(self.local_user.id),
|
f"{self.local_user.id}-test",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.test_stream.unread_id(self.local_user),
|
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, *_):
|
def test_abstractstream_get_audience(self, *_):
|
||||||
"""get a list of users that should see a status"""
|
"""get a list of users that should see a status"""
|
||||||
status = models.Status.objects.create(
|
status = models.Status.objects.create(
|
||||||
|
|
|
@ -52,3 +52,29 @@ class Activitystreams(TestCase):
|
||||||
# yes book, yes audience
|
# yes book, yes audience
|
||||||
result = activitystreams.BooksStream().get_statuses_for_user(self.local_user)
|
result = activitystreams.BooksStream().get_statuses_for_user(self.local_user)
|
||||||
self.assertEqual(list(result), [status])
|
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")
|
||||||
|
|
|
@ -24,13 +24,21 @@ class Sanitizer(TestCase):
|
||||||
self.assertEqual(input_text, output)
|
self.assertEqual(input_text, output)
|
||||||
|
|
||||||
def test_valid_html_attrs(self):
|
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>'
|
input_text = '<a href="fish.com">yes </a> <i>html</i>'
|
||||||
parser = InputHtmlParser()
|
parser = InputHtmlParser()
|
||||||
parser.feed(input_text)
|
parser.feed(input_text)
|
||||||
output = parser.get_output()
|
output = parser.get_output()
|
||||||
self.assertEqual(input_text, 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):
|
def test_invalid_html(self):
|
||||||
"""remove all html when the html is malformed"""
|
"""remove all html when the html is malformed"""
|
||||||
input_text = "<b>yes <i>html</i>"
|
input_text = "<b>yes <i>html</i>"
|
||||||
|
|
|
@ -5,6 +5,7 @@ from django.core.paginator import Paginator
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.urls import reverse
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
|
@ -35,6 +36,7 @@ class ImportTroubleshoot(View):
|
||||||
page.number, on_each_side=2, on_ends=1
|
page.number, on_each_side=2, on_ends=1
|
||||||
),
|
),
|
||||||
"complete": True,
|
"complete": True,
|
||||||
|
"page_path": reverse("import-troubleshoot", args=[job.id]),
|
||||||
}
|
}
|
||||||
|
|
||||||
return TemplateResponse(request, "import/troubleshoot.html", data)
|
return TemplateResponse(request, "import/troubleshoot.html", data)
|
||||||
|
|
|
@ -6,7 +6,7 @@ django-model-utils==4.0.0
|
||||||
environs==9.3.4
|
environs==9.3.4
|
||||||
flower==1.0.0
|
flower==1.0.0
|
||||||
Markdown==3.3.3
|
Markdown==3.3.3
|
||||||
Pillow>=8.2.0
|
Pillow>=9.0.0
|
||||||
psycopg2==2.8.4
|
psycopg2==2.8.4
|
||||||
pycryptodome==3.9.4
|
pycryptodome==3.9.4
|
||||||
python-dateutil==2.8.1
|
python-dateutil==2.8.1
|
||||||
|
|
Loading…
Reference in a new issue