Merge remote-tracking branch 'upstream/main' into images-django-imagekit

This commit is contained in:
Joachim 2021-06-19 19:32:56 +02:00
commit e251b687dc
171 changed files with 3674 additions and 2175 deletions

View file

@ -13,16 +13,10 @@ DEFAULT_LANGUAGE="English"
## Leave unset to allow all hosts ## Leave unset to allow all hosts
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]" # ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
OL_URL=https://openlibrary.org
## Database backend to use.
## Default is postgres, sqlite is for dev quickstart only (NOT production!!!)
BOOKWYRM_DATABASE_BACKEND=postgres
MEDIA_ROOT=images/ MEDIA_ROOT=images/
POSTGRES_PORT=5432 POSTGRES_PORT=5432
POSTGRES_PASSWORD=fedireads POSTGRES_PASSWORD=securedbypassword123
POSTGRES_USER=fedireads POSTGRES_USER=fedireads
POSTGRES_DB=fedireads POSTGRES_DB=fedireads
POSTGRES_HOST=db POSTGRES_HOST=db
@ -51,5 +45,14 @@ EMAIL_USE_SSL=false
# Thumbnails Generation # Thumbnails Generation
ENABLE_THUMBNAIL_GENERATION=false ENABLE_THUMBNAIL_GENERATION=false
# Set this to true when initializing certbot for domain, false when not # Preview image generation can be computing and storage intensive
CERTBOT_INIT=false # ENABLE_PREVIEW_IMAGES=True
# Specify RGB tuple or RGB hex strings,
# or use_dominant_color_light / use_dominant_color_dark
PREVIEW_BG_COLOR=use_dominant_color_light
# Change to #FFF if you use use_dominant_color_dark
PREVIEW_TEXT_COLOR="#363636"
PREVIEW_IMG_WIDTH=1200
PREVIEW_IMG_HEIGHT=630
PREVIEW_DEFAULT_COVER_COLOR="#002549"

View file

@ -13,16 +13,10 @@ DEFAULT_LANGUAGE="English"
## Leave unset to allow all hosts ## Leave unset to allow all hosts
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]" # ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
OL_URL=https://openlibrary.org
## Database backend to use.
## Default is postgres, sqlite is for dev quickstart only (NOT production!!!)
BOOKWYRM_DATABASE_BACKEND=postgres
MEDIA_ROOT=images/ MEDIA_ROOT=images/
POSTGRES_PORT=5432 POSTGRES_PORT=5432
POSTGRES_PASSWORD=securedbpassword123 POSTGRES_PASSWORD=securedbypassword123
POSTGRES_USER=fedireads POSTGRES_USER=fedireads
POSTGRES_DB=fedireads POSTGRES_DB=fedireads
POSTGRES_HOST=db POSTGRES_HOST=db
@ -51,5 +45,14 @@ EMAIL_USE_SSL=false
# Thumbnails Generation # Thumbnails Generation
ENABLE_THUMBNAIL_GENERATION=false ENABLE_THUMBNAIL_GENERATION=false
# Set this to true when initializing certbot for domain, false when not # Preview image generation can be computing and storage intensive
CERTBOT_INIT=false # ENABLE_PREVIEW_IMAGES=True
# Specify RGB tuple or RGB hex strings,
# or use_dominant_color_light / use_dominant_color_dark
PREVIEW_BG_COLOR=use_dominant_color_light
# Change to #FFF if you use use_dominant_color_dark
PREVIEW_TEXT_COLOR="#363636"
PREVIEW_IMG_WIDTH=1200
PREVIEW_IMG_HEIGHT=630
PREVIEW_DEFAULT_COVER_COLOR="#002549"

View file

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

View file

@ -50,7 +50,6 @@ jobs:
SECRET_KEY: beepbeep SECRET_KEY: beepbeep
DEBUG: true DEBUG: true
DOMAIN: your.domain.here DOMAIN: your.domain.here
OL_URL: https://openlibrary.org
BOOKWYRM_DATABASE_BACKEND: postgres BOOKWYRM_DATABASE_BACKEND: postgres
MEDIA_ROOT: images/ MEDIA_ROOT: images/
POSTGRES_PASSWORD: hunter2 POSTGRES_PASSWORD: hunter2
@ -65,5 +64,6 @@ jobs:
EMAIL_HOST_USER: "" EMAIL_HOST_USER: ""
EMAIL_HOST_PASSWORD: "" EMAIL_HOST_PASSWORD: ""
EMAIL_USE_TLS: true EMAIL_USE_TLS: true
ENABLE_PREVIEW_IMAGES: true
run: | run: |
python manage.py test python manage.py test

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,8 +4,18 @@ from bookwyrm import models, settings
def site_settings(request): # pylint: disable=unused-argument def site_settings(request): # pylint: disable=unused-argument
"""include the custom info about the site""" """include the custom info about the site"""
request_protocol = "https://"
if not request.is_secure():
request_protocol = "http://"
return { return {
"site": models.SiteSettings.objects.get(), "site": models.SiteSettings.objects.get(),
"active_announcements": models.Announcement.active_announcements(), "active_announcements": models.Announcement.active_announcements(),
"enable_thumbnail_generation": settings.ENABLE_THUMBNAIL_GENERATION, "enable_thumbnail_generation": settings.ENABLE_THUMBNAIL_GENERATION,
"static_url": settings.STATIC_URL,
"media_url": settings.MEDIA_URL,
"static_path": settings.STATIC_PATH,
"media_path": settings.MEDIA_PATH,
"preview_images_enabled": settings.ENABLE_PREVIEW_IMAGES,
"request_protocol": request_protocol,
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

424
bookwyrm/preview_images.py Normal file
View file

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

View file

@ -42,6 +42,14 @@ LOCALE_PATHS = [
DEFAULT_AUTO_FIELD = "django.db.models.AutoField" DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Preview image
ENABLE_PREVIEW_IMAGES = env.bool("ENABLE_PREVIEW_IMAGES", False)
PREVIEW_BG_COLOR = env.str("PREVIEW_BG_COLOR", "use_dominant_color_light")
PREVIEW_TEXT_COLOR = env.str("PREVIEW_TEXT_COLOR", "#363636")
PREVIEW_IMG_WIDTH = env.int("PREVIEW_IMG_WIDTH", 1200)
PREVIEW_IMG_HEIGHT = env.int("PREVIEW_IMG_HEIGHT", 630)
PREVIEW_DEFAULT_COVER_COLOR = env.str("PREVIEW_DEFAULT_COVER_COLOR", "#002549")
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
@ -52,7 +60,6 @@ SECRET_KEY = env("SECRET_KEY")
DEBUG = env.bool("DEBUG", True) DEBUG = env.bool("DEBUG", True)
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"]) ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"])
OL_URL = env("OL_URL")
# Application definition # Application definition
@ -115,10 +122,8 @@ STREAMS = ["home", "local", "federated"]
# Database # Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases # https://docs.djangoproject.com/en/3.2/ref/settings/#databases
BOOKWYRM_DATABASE_BACKEND = env("BOOKWYRM_DATABASE_BACKEND", "postgres") DATABASES = {
"default": {
BOOKWYRM_DBS = {
"postgres": {
"ENGINE": "django.db.backends.postgresql_psycopg2", "ENGINE": "django.db.backends.postgresql_psycopg2",
"NAME": env("POSTGRES_DB", "fedireads"), "NAME": env("POSTGRES_DB", "fedireads"),
"USER": env("POSTGRES_USER", "fedireads"), "USER": env("POSTGRES_USER", "fedireads"),
@ -128,8 +133,6 @@ BOOKWYRM_DBS = {
}, },
} }
DATABASES = {"default": BOOKWYRM_DBS[BOOKWYRM_DATABASE_BACKEND]}
LOGIN_URL = "/login/" LOGIN_URL = "/login/"
AUTH_USER_MODEL = "bookwyrm.User" AUTH_USER_MODEL = "bookwyrm.User"
@ -137,6 +140,7 @@ AUTH_USER_MODEL = "bookwyrm.User"
# Password validation # Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
# pylint: disable=line-too-long
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
@ -180,8 +184,10 @@ USE_TZ = True
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_URL = "/static/" STATIC_URL = "/static/"
STATIC_PATH = "%s/%s" % (DOMAIN, env("STATIC_ROOT", "static"))
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static")) STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
MEDIA_URL = "/images/" MEDIA_URL = "/images/"
MEDIA_PATH = "%s/%s" % (DOMAIN, env("MEDIA_ROOT", "images"))
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images")) MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % ( USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % (

View file

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

View file

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

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

View file

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

View file

@ -15,7 +15,7 @@
<div class="column is-narrow"> <div class="column is-narrow">
<a href="{{ author.local_path }}/edit"> <a href="{{ author.local_path }}/edit">
<span class="icon icon-pencil" title="{% trans 'Edit Author' %}" aria-hidden="True"></span> <span class="icon icon-pencil" title="{% trans 'Edit Author' %}" aria-hidden="True"></span>
<span>{% trans "Edit Author" %}</span> <span class="is-hidden-mobile">{% trans "Edit Author" %}</span>
</a> </a>
</div> </div>
{% endif %} {% endif %}

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
{% load i18n %} {% load i18n %}
<div <div
role="dialog" role="dialog"
class="modal is-hidden" class="modal {% if active %}is-active{% else %}is-hidden{% endif %}"
id="{{ controls_text }}-{{ controls_uid }}" id="{{ controls_text }}-{{ controls_uid }}"
aria-labelledby="modal-card-title-{{ controls_text }}-{{ controls_uid }}" aria-labelledby="modal-card-title-{{ controls_text }}-{{ controls_uid }}"
aria-modal="true" aria-modal="true"

View file

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

View file

@ -7,6 +7,7 @@
{% block content %}{% spaceless %} {% block content %}{% spaceless %}
<div class="block"> <div class="block">
<h1 class="title">{% trans "Import Status" %}</h1> <h1 class="title">{% trans "Import Status" %}</h1>
<a href="{% url 'import' %}" class="has-text-weight-normal help subtitle is-link">{% trans "Back to imports" %}</a>
<dl> <dl>
<div class="is-flex"> <div class="is-flex">
@ -106,7 +107,11 @@
{% endif %} {% endif %}
<div class="block"> <div class="block">
{% if job.complete %}
<h2 class="title is-4">{% trans "Successfully imported" %}</h2> <h2 class="title is-4">{% trans "Successfully imported" %}</h2>
{% else %}
<h2 class="title is-4">{% trans "Import Progress" %}</h2>
{% endif %}
<table class="table"> <table class="table">
<tr> <tr>
<th> <th>

View file

@ -8,16 +8,21 @@
<link rel="stylesheet" href="/static/css/vendor/icons.css"> <link rel="stylesheet" href="/static/css/vendor/icons.css">
<link rel="stylesheet" href="/static/css/bookwyrm.css"> <link rel="stylesheet" href="/static/css/bookwyrm.css">
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}/images/{{ site.favicon }}{% else %}/static/images/favicon.ico{% endif %}"> <link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{{ media_url }}{{ site.favicon }}{% else %}/static/images/favicon.ico{% endif %}">
{% if preview_images_enabled is True %}
<meta name="twitter:card" content="summary_large_image">
{% else %}
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
{% endif %}
<meta name="twitter:title" content="{% if title %}{{ title }} | {% endif %}{{ site.name }}"> <meta name="twitter:title" content="{% if title %}{{ title }} | {% endif %}{{ site.name }}">
<meta name="og:title" content="{% if title %}{{ title }} | {% endif %}{{ site.name }}"> <meta name="og:title" content="{% if title %}{{ title }} | {% endif %}{{ site.name }}">
<meta name="twitter:description" content="{{ site.instance_tagline }}"> <meta name="twitter:description" content="{{ site.instance_tagline }}">
<meta name="og:description" content="{{ site.instance_tagline }}"> <meta name="og:description" content="{{ site.instance_tagline }}">
<meta name="twitter:image" content="{% if site.logo %}/images/{{ site.logo }}{% else %}/static/images/logo.png{% endif %}"> {% block opengraph_images %}
<meta name="og:image" content="{% if site.logo %}/images/{{ site.logo }}{% else %}/static/images/logo.png{% endif %}"> {% include 'snippets/opengraph_images.html' %}
{% endblock %}
<meta name="twitter:image:alt" content="BookWyrm Logo"> <meta name="twitter:image:alt" content="BookWyrm Logo">
</head> </head>
<body> <body>
@ -25,7 +30,7 @@
<div class="container"> <div class="container">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item" href="/"> <a class="navbar-item" href="/">
<img class="image logo" src="{% if site.logo_small %}/images/{{ site.logo_small }}{% else %}/static/images/logo-small.png{% endif %}" alt="Home page"> <img class="image logo" src="{% if site.logo_small %}{{ media_url }}{{ site.logo_small }}{% else %}/static/images/logo-small.png{% endif %}" alt="Home page">
</a> </a>
<form class="navbar-item column" action="/search/"> <form class="navbar-item column" action="/search/">
<div class="field has-addons"> <div class="field has-addons">

View file

@ -63,7 +63,7 @@
<div class="card-footer is-stacked-mobile has-background-white-bis is-align-items-stretch"> <div class="card-footer is-stacked-mobile has-background-white-bis is-align-items-stretch">
<div class="card-footer-item"> <div class="card-footer-item">
<div> <div>
<p>{% blocktrans with username=item.user.display_name user_path=user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p> <p>{% blocktrans with username=item.user.display_name user_path=item.user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p>
</div> </div>
</div> </div>
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %} {% if list.user == request.user or list.curation == 'open' and item.user == request.user %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@
{% block edit-button %} {% block edit-button %}
<a href="{% url 'settings-import-blocklist' %}"> <a href="{% url 'settings-import-blocklist' %}">
<span class="icon icon-plus" title="{% trans 'Add instance' %}" aria-hidden="True"></span> <span class="icon icon-plus" title="{% trans 'Add instance' %}" aria-hidden="True"></span>
<span>{% trans "Add instance" %}</span> <span class="is-hidden-mobile">{% trans "Add instance" %}</span>
</a> </a>
{% endblock %} {% endblock %}

View file

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

View file

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

View file

@ -7,7 +7,7 @@
{% block modal-form-open %} {% block modal-form-open %}
<form name="finish-reading" action="/finish-reading/{{ book.id }}" method="post"> <form name="finish-reading" action="{% url 'reading-status' 'finish' book.id %}" method="post">
{% endblock %} {% endblock %}
{% block modal-body %} {% block modal-body %}

View file

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

View file

@ -2,11 +2,13 @@
{% load i18n %} {% load i18n %}
{% block modal-title %} {% block modal-title %}
{% blocktrans with book_title=book.title %}Start "<em>{{ book_title }}</em>"{% endblocktrans %} {% blocktrans trimmed with book_title=book.title %}
Start "<em>{{ book_title }}</em>"
{% endblocktrans %}
{% endblock %} {% endblock %}
{% block modal-form-open %} {% block modal-form-open %}
<form name="start-reading" action="/start-reading/{{ book.id }}" method="post"> <form name="start-reading" action="{% url 'reading-status' 'start' book.id %}" method="post">
{% endblock %} {% endblock %}
{% block modal-body %} {% block modal-body %}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,12 @@
{% load i18n %} {% load i18n %}
<div class="block content"> <div class="block content">
{% if not user.is_active and user.deactivation_reason == "self_deletion" %}
<div class="notification is-danger">
{% trans "Permanently deleted" %}
</div>
{% else %}
<h3>{% trans "Actions" %}</h3> <h3>{% trans "Actions" %}</h3>
<div class="is-flex"> <div class="is-flex">
<p class="mr-1"> <p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a> <a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a>
@ -14,6 +20,7 @@
{% endif %} {% endif %}
</form> </form>
</div> </div>
{% if user.local %} {% if user.local %}
<div> <div>
<form name="permission" method="post" action="{% url 'settings-user' user.id %}"> <form name="permission" method="post" action="{% url 'settings-user' user.id %}">
@ -39,4 +46,6 @@
</form> </form>
</div> </div>
{% endif %} {% endif %}
{% endif %}
</div> </div>

View file

@ -1,7 +1,6 @@
""" template filters used for creating the layout""" """ template filters used for creating the layout"""
from django import template, utils from django import template, utils
register = template.Library() register = template.Library()

View file

@ -2,6 +2,7 @@
import os import os
from uuid import uuid4 from uuid import uuid4
from django import template from django import template
from django.utils.translation import gettext_lazy as _
register = template.Library() register = template.Library()
@ -20,13 +21,16 @@ def get_user_identifier(user):
@register.filter(name="book_title") @register.filter(name="book_title")
def get_title(book): def get_title(book, too_short=5):
"""display the subtitle if the title is short""" """display the subtitle if the title is short"""
if not book: if not book:
return "" return ""
title = book.title title = book.title
if len(title) < 6 and book.subtitle: if len(title) <= too_short and book.subtitle:
title = "{:s}: {:s}".format(title, book.subtitle) title = _("%(title)s: %(subtitle)s") % {
"title": title,
"subtitle": book.subtitle,
}
return title return title

View file

@ -1,11 +1,13 @@
import datetime import datetime
from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
from bookwyrm import models from bookwyrm import models
class Author(TestCase): class Author(TestCase):
def setUp(self): def setUp(self):
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.book = models.Edition.objects.create( self.book = models.Edition.objects.create(
title="Example Edition", title="Example Edition",
remote_id="https://example.com/book/1", remote_id="https://example.com/book/1",

View file

@ -25,6 +25,7 @@ class BaseActivity(TestCase):
def setUp(self): def setUp(self):
"""we're probably going to re-use this so why copy/paste""" """we're probably going to re-use this so why copy/paste"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
) )
@ -97,6 +98,7 @@ class BaseActivity(TestCase):
status=200, status=200,
) )
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
result = resolve_remote_id( result = resolve_remote_id(
"https://example.com/user/mouse", model=models.User "https://example.com/user/mouse", model=models.User
@ -139,6 +141,7 @@ class BaseActivity(TestCase):
self.user.avatar.file # pylint: disable=pointless-statement self.user.avatar.file # pylint: disable=pointless-statement
# this would trigger a broadcast because it's a local user # this would trigger a broadcast because it's a local user
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
activity.to_model(model=models.User, instance=self.user) activity.to_model(model=models.User, instance=self.user)
self.assertIsNotNone(self.user.avatar.file) self.assertIsNotNone(self.user.avatar.file)
@ -152,6 +155,7 @@ class BaseActivity(TestCase):
content="test status", content="test status",
user=self.user, user=self.user,
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
book = models.Edition.objects.create( book = models.Edition.objects.create(
title="Test Edition", remote_id="http://book.com/book" title="Test Edition", remote_id="http://book.com/book"
) )

View file

@ -20,6 +20,7 @@ class Person(TestCase):
def test_user_to_model(self): def test_user_to_model(self):
activity = activitypub.Person(**self.user_data) activity = activitypub.Person(**self.user_data)
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
user = activity.to_model(model=models.User) user = activity.to_model(model=models.User)
self.assertEqual(user.username, "mouse@example.com") self.assertEqual(user.username, "mouse@example.com")

View file

@ -12,6 +12,7 @@ class Quotation(TestCase):
def setUp(self): def setUp(self):
"""model objects we'll need""" """model objects we'll need"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
"mouse", "mouse",
@ -22,6 +23,7 @@ class Quotation(TestCase):
outbox="https://example.com/user/mouse/outbox", outbox="https://example.com/user/mouse/outbox",
remote_id="https://example.com/user/mouse", remote_id="https://example.com/user/mouse",
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.book = models.Edition.objects.create( self.book = models.Edition.objects.create(
title="Example Edition", title="Example Edition",
remote_id="https://example.com/book/1", remote_id="https://example.com/book/1",

View file

@ -74,6 +74,7 @@ class AbstractConnector(TestCase):
Mapping("openlibraryKey"), Mapping("openlibraryKey"),
] ]
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.book = models.Edition.objects.create( self.book = models.Edition.objects.create(
title="Test Book", title="Test Book",
remote_id="https://example.com/book/1234", remote_id="https://example.com/book/1234",
@ -110,8 +111,11 @@ class AbstractConnector(TestCase):
responses.add( responses.add(
responses.GET, "https://example.com/book/abcd", json=self.edition_data responses.GET, "https://example.com/book/abcd", json=self.edition_data
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
with patch("bookwyrm.connectors.abstract_connector.load_more_data.delay"): with patch("bookwyrm.connectors.abstract_connector.load_more_data.delay"):
result = self.connector.get_or_create_book("https://example.com/book/abcd") result = self.connector.get_or_create_book(
"https://example.com/book/abcd"
)
self.assertEqual(result, self.book) self.assertEqual(result, self.book)
self.assertEqual(models.Edition.objects.count(), 1) self.assertEqual(models.Edition.objects.count(), 1)
self.assertEqual(models.Edition.objects.count(), 1) self.assertEqual(models.Edition.objects.count(), 1)

View file

@ -1,4 +1,5 @@
""" testing book data connectors """ """ testing book data connectors """
from unittest.mock import patch
import json import json
import pathlib import pathlib
from django.test import TestCase from django.test import TestCase
@ -25,6 +26,7 @@ class BookWyrmConnector(TestCase):
def test_get_or_create_book_existing(self): def test_get_or_create_book_existing(self):
"""load book activity""" """load book activity"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
work = models.Work.objects.create(title="Test Work") work = models.Work.objects.create(title="Test Work")
book = models.Edition.objects.create(title="Test Edition", parent_work=work) book = models.Edition.objects.create(title="Test Edition", parent_work=work)
result = self.connector.get_or_create_book(book.remote_id) result = self.connector.get_or_create_book(book.remote_id)

View file

@ -1,4 +1,5 @@
""" interface between the app and various connectors """ """ interface between the app and various connectors """
from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
import responses import responses
@ -13,6 +14,7 @@ class ConnectorManager(TestCase):
def setUp(self): def setUp(self):
"""we'll need some books and a connector info entry""" """we'll need some books and a connector info entry"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.work = models.Work.objects.create(title="Example Work") self.work = models.Work.objects.create(title="Example Work")
self.edition = models.Edition.objects.create( self.edition = models.Edition.objects.create(

View file

@ -178,14 +178,20 @@ class Openlibrary(TestCase):
@responses.activate @responses.activate
def test_expand_book_data(self): def test_expand_book_data(self):
"""given a book, get more editions""" """given a book, get more editions"""
work = models.Work.objects.create(title="Test Work", openlibrary_key="OL1234W") with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
edition = models.Edition.objects.create(title="Test Edition", parent_work=work) work = models.Work.objects.create(
title="Test Work", openlibrary_key="OL1234W"
)
edition = models.Edition.objects.create(
title="Test Edition", parent_work=work
)
responses.add( responses.add(
responses.GET, responses.GET,
"https://openlibrary.org/works/OL1234W/editions", "https://openlibrary.org/works/OL1234W/editions",
json={"entries": []}, json={"entries": []},
) )
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
with patch( with patch(
"bookwyrm.connectors.abstract_connector.AbstractConnector." "bookwyrm.connectors.abstract_connector.AbstractConnector."
"create_edition_from_data" "create_edition_from_data"
@ -224,11 +230,14 @@ class Openlibrary(TestCase):
json={"hi": "there"}, json={"hi": "there"},
status=200, status=200,
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
with patch( with patch(
"bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data" "bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data"
) as mock: ) as mock:
mock.return_value = [] mock.return_value = []
result = self.connector.create_edition_from_data(work, self.edition_data) result = self.connector.create_edition_from_data(
work, self.edition_data
)
self.assertEqual(result.parent_work, work) self.assertEqual(result.parent_work, work)
self.assertEqual(result.title, "Sabriel") self.assertEqual(result.title, "Sabriel")
self.assertEqual(result.isbn_10, "0060273224") self.assertEqual(result.isbn_10, "0060273224")

View file

@ -1,4 +1,5 @@
""" testing book data connectors """ """ testing book data connectors """
from unittest.mock import patch
import datetime import datetime
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
@ -29,6 +30,7 @@ class SelfConnector(TestCase):
def test_format_search_result(self): def test_format_search_result(self):
"""create a SearchResult""" """create a SearchResult"""
author = models.Author.objects.create(name="Anonymous") author = models.Author.objects.create(name="Anonymous")
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
edition = models.Edition.objects.create( edition = models.Edition.objects.create(
title="Edition of Example Work", title="Edition of Example Work",
published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc), published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc),
@ -44,6 +46,7 @@ class SelfConnector(TestCase):
def test_search_rank(self): def test_search_rank(self):
"""prioritize certain results""" """prioritize certain results"""
author = models.Author.objects.create(name="Anonymous") author = models.Author.objects.create(name="Anonymous")
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
edition = models.Edition.objects.create( edition = models.Edition.objects.create(
title="Edition of Example Work", title="Edition of Example Work",
published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc), published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc),
@ -79,6 +82,7 @@ class SelfConnector(TestCase):
def test_search_multiple_editions(self): def test_search_multiple_editions(self):
"""it should get rid of duplicate editions for the same work""" """it should get rid of duplicate editions for the same work"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
work = models.Work.objects.create(title="Work Title") work = models.Work.objects.create(title="Work Title")
edition_1 = models.Edition.objects.create( edition_1 = models.Edition.objects.create(
title="Edition 1 Title", parent_work=work title="Edition 1 Title", parent_work=work

View file

@ -21,6 +21,7 @@ class GoodreadsImport(TestCase):
self.importer = GoodreadsImporter() self.importer = GoodreadsImporter()
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
self.csv = open(datafile, "r", encoding=self.importer.encoding) self.csv = open(datafile, "r", encoding=self.importer.encoding)
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True "mouse", "mouse@mouse.mouse", "password", local=True
) )
@ -36,6 +37,7 @@ class GoodreadsImport(TestCase):
search_url="https://%s/search?q=" % DOMAIN, search_url="https://%s/search?q=" % DOMAIN,
priority=1, priority=1,
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
work = models.Work.objects.create(title="Test Work") work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create( self.book = models.Edition.objects.create(
title="Example Edition", title="Example Edition",
@ -92,6 +94,7 @@ class GoodreadsImport(TestCase):
def test_import_data(self): def test_import_data(self):
"""resolve entry""" """resolve entry"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
book = models.Edition.objects.create(title="Test Book") book = models.Edition.objects.create(title="Test Book")
with patch( with patch(
@ -119,6 +122,7 @@ class GoodreadsImport(TestCase):
) )
break break
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book( handle_imported_book(
self.importer.service, self.user, import_item, False, "public" self.importer.service, self.user, import_item, False, "public"
@ -183,6 +187,7 @@ class GoodreadsImport(TestCase):
) )
break break
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book( handle_imported_book(
self.importer.service, self.user, import_item, False, "public" self.importer.service, self.user, import_item, False, "public"
@ -216,6 +221,7 @@ class GoodreadsImport(TestCase):
job_id=import_job.id, index=0, data=entry, book=self.book job_id=import_job.id, index=0, data=entry, book=self.book
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book( handle_imported_book(
self.importer.service, self.user, import_item, True, "unlisted" self.importer.service, self.user, import_item, True, "unlisted"
@ -242,6 +248,7 @@ class GoodreadsImport(TestCase):
job_id=import_job.id, index=0, data=entry, book=self.book job_id=import_job.id, index=0, data=entry, book=self.book
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book( handle_imported_book(
self.importer.service, self.user, import_item, True, "unlisted" self.importer.service, self.user, import_item, True, "unlisted"
@ -265,6 +272,7 @@ class GoodreadsImport(TestCase):
job_id=import_job.id, index=0, data=entry, book=self.book job_id=import_job.id, index=0, data=entry, book=self.book
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book( handle_imported_book(
self.importer.service, self.user, import_item, False, "unlisted" self.importer.service, self.user, import_item, False, "unlisted"

View file

@ -22,6 +22,7 @@ class LibrarythingImport(TestCase):
# Librarything generates latin encoded exports... # Librarything generates latin encoded exports...
self.csv = open(datafile, "r", encoding=self.importer.encoding) self.csv = open(datafile, "r", encoding=self.importer.encoding)
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
"mmai", "mmai@mmai.mmai", "password", local=True "mmai", "mmai@mmai.mmai", "password", local=True
) )
@ -37,6 +38,7 @@ class LibrarythingImport(TestCase):
search_url="https://%s/search?q=" % DOMAIN, search_url="https://%s/search?q=" % DOMAIN,
priority=1, priority=1,
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
work = models.Work.objects.create(title="Test Work") work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create( self.book = models.Edition.objects.create(
title="Example Edition", title="Example Edition",
@ -82,6 +84,7 @@ class LibrarythingImport(TestCase):
def test_import_data(self): def test_import_data(self):
"""resolve entry""" """resolve entry"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
book = models.Edition.objects.create(title="Test Book") book = models.Edition.objects.create(title="Test Book")
with patch( with patch(
@ -111,6 +114,7 @@ class LibrarythingImport(TestCase):
) )
break break
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book( handle_imported_book(
self.importer.service, self.user, import_item, False, "public" self.importer.service, self.user, import_item, False, "public"
@ -147,6 +151,7 @@ class LibrarythingImport(TestCase):
) )
break break
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book( handle_imported_book(
self.importer.service, self.user, import_item, False, "public" self.importer.service, self.user, import_item, False, "public"
@ -179,6 +184,7 @@ class LibrarythingImport(TestCase):
) )
break break
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book( handle_imported_book(
self.importer.service, self.user, import_item, False, "public" self.importer.service, self.user, import_item, False, "public"
@ -212,6 +218,7 @@ class LibrarythingImport(TestCase):
job_id=import_job.id, index=0, data=entry, book=self.book job_id=import_job.id, index=0, data=entry, book=self.book
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book( handle_imported_book(
self.importer.service, self.user, import_item, True, "unlisted" self.importer.service, self.user, import_item, True, "unlisted"
@ -235,6 +242,7 @@ class LibrarythingImport(TestCase):
job_id=import_job.id, index=0, data=entry, book=self.book job_id=import_job.id, index=0, data=entry, book=self.book
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book( handle_imported_book(
self.importer.service, self.user, import_item, False, "unlisted" self.importer.service, self.user, import_item, False, "unlisted"

View file

@ -12,11 +12,16 @@ class Activitystreams(TestCase):
def setUp(self): def setUp(self):
"""we need some stuff""" """we need some stuff"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse"
) )
self.another_user = models.User.objects.create_user( self.another_user = models.User.objects.create_user(
"nutria", "nutria@nutria.nutria", "password", local=True, localname="nutria" "nutria",
"nutria@nutria.nutria",
"password",
local=True,
localname="nutria",
) )
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
@ -28,6 +33,7 @@ class Activitystreams(TestCase):
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.book = models.Edition.objects.create(title="test book") self.book = models.Edition.objects.create(title="test book")
def test_populate_streams(self, _): def test_populate_streams(self, _):

View file

@ -19,6 +19,7 @@ class ActivitypubMixins(TestCase):
def setUp(self): def setUp(self):
"""shared data""" """shared data"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
) )
@ -70,6 +71,7 @@ class ActivitypubMixins(TestCase):
"""attempt to match a remote id to an object in the db""" """attempt to match a remote id to an object in the db"""
# uses a different remote id scheme # uses a different remote id scheme
# this isn't really part of this test directly but it's helpful to state # this isn't really part of this test directly but it's helpful to state
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
book = models.Edition.objects.create( book = models.Edition.objects.create(
title="Test Edition", remote_id="http://book.com/book" title="Test Edition", remote_id="http://book.com/book"
) )
@ -101,6 +103,7 @@ class ActivitypubMixins(TestCase):
def test_find_existing(self, _): def test_find_existing(self, _):
"""match a blob of data to a model""" """match a blob of data to a model"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
book = models.Edition.objects.create( book = models.Edition.objects.create(
title="Test edition", title="Test edition",
openlibrary_key="OL1234", openlibrary_key="OL1234",
@ -140,6 +143,7 @@ class ActivitypubMixins(TestCase):
MockSelf = namedtuple("Self", ("privacy", "user")) MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user) mock_self = MockSelf("public", self.local_user)
self.local_user.followers.add(self.remote_user) self.local_user.followers.add(self.remote_user)
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
another_remote_user = models.User.objects.create_user( another_remote_user = models.User.objects.create_user(
"nutria", "nutria",
@ -163,6 +167,7 @@ class ActivitypubMixins(TestCase):
MockSelf = namedtuple("Self", ("privacy", "user")) MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user) mock_self = MockSelf("public", self.local_user)
self.local_user.followers.add(self.remote_user) self.local_user.followers.add(self.remote_user)
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
another_remote_user = models.User.objects.create_user( another_remote_user = models.User.objects.create_user(
"nutria", "nutria",
@ -184,6 +189,7 @@ class ActivitypubMixins(TestCase):
"""should combine users with the same shared_inbox""" """should combine users with the same shared_inbox"""
self.remote_user.shared_inbox = "http://example.com/inbox" self.remote_user.shared_inbox = "http://example.com/inbox"
self.remote_user.save(broadcast=False) self.remote_user.save(broadcast=False)
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
another_remote_user = models.User.objects.create_user( another_remote_user = models.User.objects.create_user(
"nutria", "nutria",
@ -206,6 +212,7 @@ class ActivitypubMixins(TestCase):
def test_get_recipients_software(self, _): def test_get_recipients_software(self, _):
"""should differentiate between bookwyrm and other remote users""" """should differentiate between bookwyrm and other remote users"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
another_remote_user = models.User.objects.create_user( another_remote_user = models.User.objects.create_user(
"nutria", "nutria",

View file

@ -12,6 +12,7 @@ class BaseModel(TestCase):
def setUp(self): def setUp(self):
"""shared data""" """shared data"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
) )

View file

@ -2,6 +2,7 @@
from dateutil.parser import parse from dateutil.parser import parse
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
from unittest.mock import patch
from bookwyrm import models, settings from bookwyrm import models, settings
from bookwyrm.models.book import isbn_10_to_13, isbn_13_to_10 from bookwyrm.models.book import isbn_10_to_13, isbn_13_to_10
@ -12,6 +13,7 @@ class Book(TestCase):
def setUp(self): def setUp(self):
"""we'll need some books""" """we'll need some books"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.work = models.Work.objects.create( self.work = models.Work.objects.create(
title="Example Work", remote_id="https://example.com/book/1" title="Example Work", remote_id="https://example.com/book/1"
) )
@ -56,6 +58,7 @@ class Book(TestCase):
def test_get_edition_info(self): def test_get_edition_info(self):
"""text slug about an edition""" """text slug about an edition"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
book = models.Edition.objects.create(title="Test Edition") book = models.Edition.objects.create(title="Test Edition")
self.assertEqual(book.edition_info, "") self.assertEqual(book.edition_info, "")

View file

@ -11,6 +11,7 @@ class FederatedServer(TestCase):
def setUp(self): def setUp(self):
"""we'll need a user""" """we'll need a user"""
self.server = models.FederatedServer.objects.create(server_name="test.server") self.server = models.FederatedServer.objects.create(server_name="test.server")
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",

View file

@ -188,6 +188,7 @@ class ActivitypubFields(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_privacy_field_set_activity_from_field(self, *_): def test_privacy_field_set_activity_from_field(self, *_):
"""translate between to/cc fields and privacy""" """translate between to/cc fields and privacy"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
user = User.objects.create_user( user = User.objects.create_user(
"rat", "rat@rat.rat", "ratword", local=True, localname="rat" "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
) )
@ -248,13 +249,17 @@ class ActivitypubFields(TestCase):
del userdata["icon"] del userdata["icon"]
# it shouldn't match with this unrelated user: # it shouldn't match with this unrelated user:
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
unrelated_user = User.objects.create_user( unrelated_user = User.objects.create_user(
"rat", "rat@rat.rat", "ratword", local=True, localname="rat" "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
) )
# test receiving an unknown remote id and loading data # test receiving an unknown remote id and loading data
responses.add( responses.add(
responses.GET, "https://example.com/user/mouse", json=userdata, status=200 responses.GET,
"https://example.com/user/mouse",
json=userdata,
status=200,
) )
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
value = instance.field_from_activity("https://example.com/user/mouse") value = instance.field_from_activity("https://example.com/user/mouse")
@ -272,6 +277,7 @@ class ActivitypubFields(TestCase):
del userdata["icon"] del userdata["icon"]
# it shouldn't match with this unrelated user: # it shouldn't match with this unrelated user:
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
unrelated_user = User.objects.create_user( unrelated_user = User.objects.create_user(
"rat", "rat@rat.rat", "ratword", local=True, localname="rat" "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
) )
@ -288,11 +294,13 @@ class ActivitypubFields(TestCase):
instance = fields.ForeignKey(User, on_delete=models.CASCADE) instance = fields.ForeignKey(User, on_delete=models.CASCADE)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
userdata = json.loads(datafile.read_bytes()) userdata = json.loads(datafile.read_bytes())
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
user = User.objects.create_user( user = User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
) )
user.remote_id = "https://example.com/user/mouse" user.remote_id = "https://example.com/user/mouse"
user.save(broadcast=False) user.save(broadcast=False)
User.objects.create_user( User.objects.create_user(
"rat", "rat@rat.rat", "ratword", local=True, localname="rat" "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
) )
@ -304,6 +312,7 @@ class ActivitypubFields(TestCase):
def test_foreign_key_from_activity_str_existing(self): def test_foreign_key_from_activity_str_existing(self):
"""test receiving a remote id of an existing object in the db""" """test receiving a remote id of an existing object in the db"""
instance = fields.ForeignKey(User, on_delete=models.CASCADE) instance = fields.ForeignKey(User, on_delete=models.CASCADE)
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
user = User.objects.create_user( user = User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
) )
@ -351,6 +360,7 @@ class ActivitypubFields(TestCase):
responses.add( responses.add(
responses.GET, "https://example.com/user/mouse", json=userdata, status=200 responses.GET, "https://example.com/user/mouse", json=userdata, status=200
) )
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
value = instance.field_from_activity( value = instance.field_from_activity(
["https://example.com/user/mouse", "bleh"] ["https://example.com/user/mouse", "bleh"]
@ -386,6 +396,7 @@ class ActivitypubFields(TestCase):
@patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast") @patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast")
def test_image_field(self, _): def test_image_field(self, _):
"""storing images""" """storing images"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
user = User.objects.create_user( user = User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
) )

View file

@ -59,6 +59,7 @@ class ImportJob(TestCase):
unknown_read_data["Exclusive Shelf"] = "read" unknown_read_data["Exclusive Shelf"] = "read"
unknown_read_data["Date Read"] = "" unknown_read_data["Date Read"] = ""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
user = models.User.objects.create_user( user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
) )
@ -173,6 +174,9 @@ class ImportJob(TestCase):
search.return_value = result search.return_value = result
with patch( with patch(
"bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data" "bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data"
):
with patch(
"bookwyrm.preview_images.generate_edition_preview_image_task.delay"
): ):
book = self.item_1.get_book_from_isbn() book = self.item_1.get_book_from_isbn()

View file

@ -11,9 +11,11 @@ class List(TestCase):
def setUp(self): def setUp(self):
"""look, a list""" """look, a list"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
work = models.Work.objects.create(title="hello") work = models.Work.objects.create(title="hello")
self.book = models.Edition.objects.create(title="hi", parent_work=work) self.book = models.Edition.objects.create(title="hi", parent_work=work)

View file

@ -1,6 +1,7 @@
""" testing models """ """ testing models """
from django.test import TestCase from django.test import TestCase
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from unittest.mock import patch
from bookwyrm import models from bookwyrm import models
@ -10,12 +11,13 @@ class ReadThrough(TestCase):
def setUp(self): def setUp(self):
"""look, a shelf""" """look, a shelf"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.work = models.Work.objects.create(title="Example Work") self.work = models.Work.objects.create(title="Example Work")
self.edition = models.Edition.objects.create( self.edition = models.Edition.objects.create(
title="Example Edition", parent_work=self.work title="Example Edition", parent_work=self.work
) )

View file

@ -10,6 +10,7 @@ class Relationship(TestCase):
def setUp(self): def setUp(self):
"""we need some users for this""" """we need some users for this"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",

View file

@ -12,11 +12,15 @@ class Shelf(TestCase):
def setUp(self): def setUp(self):
"""look, a shelf""" """look, a shelf"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
work = models.Work.objects.create(title="Test Work") work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(title="test book", parent_work=work) self.book = models.Edition.objects.create(
title="test book", parent_work=work
)
def test_remote_id(self): def test_remote_id(self):
"""shelves use custom remote ids""" """shelves use custom remote ids"""

View file

@ -16,11 +16,13 @@ from bookwyrm import activitypub, models, settings
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
@patch("bookwyrm.models.Status.broadcast") @patch("bookwyrm.models.Status.broadcast")
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
@patch("bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores")
class Status(TestCase): class Status(TestCase):
"""lotta types of statuses""" """lotta types of statuses"""
def setUp(self): def setUp(self):
"""useful things for creating a status""" """useful things for creating a status"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
) )
@ -34,6 +36,7 @@ class Status(TestCase):
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.book = models.Edition.objects.create(title="Test Edition") self.book = models.Edition.objects.create(title="Test Edition")
image_file = pathlib.Path(__file__).parent.joinpath( image_file = pathlib.Path(__file__).parent.joinpath(
@ -58,6 +61,7 @@ class Status(TestCase):
child = models.Status.objects.create( child = models.Status.objects.create(
content="hello", reply_parent=parent, user=self.local_user content="hello", reply_parent=parent, user=self.local_user
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
models.Review.objects.create( models.Review.objects.create(
content="hey", reply_parent=parent, user=self.local_user, book=self.book content="hey", reply_parent=parent, user=self.local_user, book=self.book
) )
@ -92,6 +96,7 @@ class Status(TestCase):
child = models.Status.objects.create( child = models.Status.objects.create(
content="hello", reply_parent=parent, user=self.local_user content="hello", reply_parent=parent, user=self.local_user
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
models.Review.objects.create( models.Review.objects.create(
content="hey", reply_parent=parent, user=self.local_user, book=self.book content="hey", reply_parent=parent, user=self.local_user, book=self.book
) )
@ -251,6 +256,7 @@ class Status(TestCase):
def test_review_to_activity(self, *_): def test_review_to_activity(self, *_):
"""subclass of the base model version with a "pure" serializer""" """subclass of the base model version with a "pure" serializer"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
status = models.Review.objects.create( status = models.Review.objects.create(
name="Review name", name="Review name",
content="test content", content="test content",
@ -268,6 +274,7 @@ class Status(TestCase):
def test_review_to_pure_activity(self, *_): def test_review_to_pure_activity(self, *_):
"""subclass of the base model version with a "pure" serializer""" """subclass of the base model version with a "pure" serializer"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
status = models.Review.objects.create( status = models.Review.objects.create(
name="Review's name", name="Review's name",
content="test content", content="test content",
@ -292,6 +299,7 @@ class Status(TestCase):
def test_review_to_pure_activity_no_rating(self, *_): def test_review_to_pure_activity_no_rating(self, *_):
"""subclass of the base model version with a "pure" serializer""" """subclass of the base model version with a "pure" serializer"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
status = models.Review.objects.create( status = models.Review.objects.create(
name="Review name", name="Review name",
content="test content", content="test content",
@ -314,6 +322,7 @@ class Status(TestCase):
def test_reviewrating_to_pure_activity(self, *_): def test_reviewrating_to_pure_activity(self, *_):
"""subclass of the base model version with a "pure" serializer""" """subclass of the base model version with a "pure" serializer"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
status = models.ReviewRating.objects.create( status = models.ReviewRating.objects.create(
rating=3.0, rating=3.0,
user=self.local_user, user=self.local_user,
@ -348,6 +357,7 @@ class Status(TestCase):
status = models.Status.objects.create( status = models.Status.objects.create(
content="test content", user=self.local_user content="test content", user=self.local_user
) )
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
fav = models.Favorite.objects.create(status=status, user=self.local_user) fav = models.Favorite.objects.create(status=status, user=self.local_user)
# can't fav a status twice # can't fav a status twice
@ -384,7 +394,8 @@ class Status(TestCase):
user=self.local_user, notification_type="GLORB" user=self.local_user, notification_type="GLORB"
) )
def test_create_broadcast(self, _, broadcast_mock): # pylint: disable=unused-argument
def test_create_broadcast(self, one, two, broadcast_mock, *_):
"""should send out two verions of a status on create""" """should send out two verions of a status on create"""
models.Comment.objects.create( models.Comment.objects.create(
content="hi", user=self.local_user, book=self.book content="hi", user=self.local_user, book=self.book

View file

@ -11,6 +11,7 @@ from bookwyrm.settings import DOMAIN
# pylint: disable=missing-function-docstring # pylint: disable=missing-function-docstring
class User(TestCase): class User(TestCase):
def setUp(self): def setUp(self):
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
"mouse@%s" % DOMAIN, "mouse@%s" % DOMAIN,
"mouse@mouse.mouse", "mouse@mouse.mouse",
@ -34,6 +35,7 @@ class User(TestCase):
self.assertIsNotNone(self.user.key_pair.public_key) self.assertIsNotNone(self.user.key_pair.public_key)
def test_remote_user(self): def test_remote_user(self):
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
user = models.User.objects.create_user( user = models.User.objects.create_user(
"rat", "rat",

View file

@ -11,11 +11,16 @@ class Activitystreams(TestCase):
def setUp(self): def setUp(self):
"""use a test csv""" """use a test csv"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse"
) )
self.another_user = models.User.objects.create_user( self.another_user = models.User.objects.create_user(
"nutria", "nutria@nutria.nutria", "password", local=True, localname="nutria" "nutria",
"nutria@nutria.nutria",
"password",
local=True,
localname="nutria",
) )
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
@ -27,6 +32,7 @@ class Activitystreams(TestCase):
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.book = models.Edition.objects.create(title="test book") self.book = models.Edition.objects.create(title="test book")
class TestStream(activitystreams.ActivityStream): class TestStream(activitystreams.ActivityStream):

View file

@ -15,6 +15,7 @@ class Emailing(TestCase):
def setUp(self): def setUp(self):
"""we need basic test data and mocks""" """we need basic test data and mocks"""
self.factory = RequestFactory() self.factory = RequestFactory()
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@local.com", "mouse@local.com",
"mouse@mouse.mouse", "mouse@mouse.mouse",
@ -22,6 +23,7 @@ class Emailing(TestCase):
local=True, local=True,
localname="mouse", localname="mouse",
) )
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"):
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def test_invite_email(self, email_mock): def test_invite_email(self, email_mock):

View file

@ -0,0 +1,119 @@
""" test generating preview images """
import pathlib
from unittest.mock import patch
from PIL import Image
from django.test import TestCase
from django.test.client import RequestFactory
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models.fields.files import ImageFieldFile
from bookwyrm import models, settings
from bookwyrm.preview_images import (
generate_site_preview_image_task,
generate_edition_preview_image_task,
generate_user_preview_image_task,
generate_preview_image,
save_and_cleanup,
)
# pylint: disable=unused-argument
# pylint: disable=missing-function-docstring
class PreviewImages(TestCase):
"""every response to a get request, html or json"""
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
avatar_file = pathlib.Path(__file__).parent.joinpath(
"../static/images/no_cover.jpg"
)
self.local_user = models.User.objects.create_user(
"possum@local.com",
"possum@possum.possum",
"password",
local=True,
localname="possum",
avatar=SimpleUploadedFile(
avatar_file,
open(avatar_file, "rb").read(),
content_type="image/jpeg",
),
)
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.work = models.Work.objects.create(title="Test Work")
self.edition = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
parent_work=self.work,
)
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"):
self.site = models.SiteSettings.objects.create()
def test_generate_preview_image(self, *args, **kwargs):
image_file = pathlib.Path(__file__).parent.joinpath(
"../static/images/no_cover.jpg"
)
texts = {
"text_one": "Awesome Possum",
"text_three": "@possum@local.com",
}
result = generate_preview_image(texts=texts, picture=image_file, rating=5)
self.assertIsInstance(result, Image.Image)
self.assertEqual(
result.size, (settings.PREVIEW_IMG_WIDTH, settings.PREVIEW_IMG_HEIGHT)
)
def test_store_preview_image(self, *args, **kwargs):
image = Image.new("RGB", (200, 200), color="#F00")
result = save_and_cleanup(image, instance=self.local_user)
self.assertTrue(result)
self.local_user.refresh_from_db()
self.assertIsInstance(self.local_user.preview_image, ImageFieldFile)
self.assertIsNotNone(self.local_user.preview_image)
self.assertEqual(self.local_user.preview_image.width, 200)
self.assertEqual(self.local_user.preview_image.height, 200)
def test_site_preview(self, *args, **kwargs):
"""generate site preview"""
generate_site_preview_image_task()
self.site.refresh_from_db()
self.assertIsInstance(self.site.preview_image, ImageFieldFile)
self.assertIsNotNone(self.site.preview_image)
self.assertEqual(self.site.preview_image.width, settings.PREVIEW_IMG_WIDTH)
self.assertEqual(self.site.preview_image.height, settings.PREVIEW_IMG_HEIGHT)
def test_edition_preview(self, *args, **kwargs):
"""generate edition preview"""
generate_edition_preview_image_task(self.edition.id)
self.edition.refresh_from_db()
self.assertIsInstance(self.edition.preview_image, ImageFieldFile)
self.assertIsNotNone(self.edition.preview_image)
self.assertEqual(self.edition.preview_image.width, settings.PREVIEW_IMG_WIDTH)
self.assertEqual(self.edition.preview_image.height, settings.PREVIEW_IMG_HEIGHT)
def test_user_preview(self, *args, **kwargs):
"""generate user preview"""
generate_user_preview_image_task(self.local_user.id)
self.local_user.refresh_from_db()
self.assertIsInstance(self.local_user.preview_image, ImageFieldFile)
self.assertIsNotNone(self.local_user.preview_image)
self.assertEqual(
self.local_user.preview_image.width, settings.PREVIEW_IMG_WIDTH
)
self.assertEqual(
self.local_user.preview_image.height, settings.PREVIEW_IMG_HEIGHT
)

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