Merge remote-tracking branch 'upstream/main' into storage-s3

This commit is contained in:
Joachim 2021-06-19 11:53:05 +02:00
commit d862be0564
172 changed files with 3693 additions and 2147 deletions

View file

@ -54,3 +54,15 @@ USE_S3=false
# AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud" # AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud"
# AWS_S3_REGION_NAME=None # "fr-par" # AWS_S3_REGION_NAME=None # "fr-par"
# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud" # AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud"
# Preview image generation can be computing and storage intensive
# 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

@ -54,3 +54,15 @@ USE_S3=false
# AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud" # AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud"
# AWS_S3_REGION_NAME=None # "fr-par" # AWS_S3_REGION_NAME=None # "fr-par"
# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud" # AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud"
# Preview image generation can be computing and storage intensive
# 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

@ -64,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

3
.gitignore vendored
View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
""" activitypub-aware django model fields """ """ activitypub-aware django model fields """
from dataclasses import MISSING from dataclasses import MISSING
import imghdr
import re import re
from uuid import uuid4 from uuid import uuid4
@ -9,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/
@ -177,6 +185,7 @@ USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % (
DOMAIN, DOMAIN,
) )
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/ # https://docs.djangoproject.com/en/3.2/howto/static-files/
@ -203,6 +212,7 @@ if USE_S3:
# S3 Media settings # S3 Media settings
MEDIA_LOCATION = "images" MEDIA_LOCATION = "images"
MEDIA_URL = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, MEDIA_LOCATION) MEDIA_URL = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, MEDIA_LOCATION)
MEDIA_PATH = "%s/%s" % (AWS_S3_CUSTOM_DOMAIN, MEDIA_LOCATION)
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage" DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
# I don't know if it's used, but the site crashes without it # I don't know if it's used, but the site crashes without it
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images")) MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
@ -210,4 +220,5 @@ else:
STATIC_URL = "/static/" STATIC_URL = "/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 = "https://%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"))

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

@ -4,36 +4,49 @@
{% load humanize %} {% load humanize %}
{% load utilities %} {% load utilities %}
{% load static %} {% load static %}
{% 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 %}:
<small>{{ book.subtitle }}</small>
{% endif %}
</span>
{% if book.series %}
<meta itemprop="isPartOf" content="{{ book.series }}">
<meta itemprop="volumeNumber" content="{{ book.series_number }}">
<small class="has-text-grey-dark">
({{ book.series }}
{% if book.series_number %} #{{ book.series_number }}{% endif %})
</small>
<br>
{% endif %}
</h1> </h1>
{% if book.subtitle or book.series %}
<p class="subtitle title is-5">
{% if book.subtitle %}
<meta
itemprop="alternativeHeadline"
content="{{ book.subtitle | escape }}"
>
<span class="has-text-weight-bold">
{{ book.subtitle }}
</span>
{% endif %}
{% if book.series %}
<meta itemprop="isPartOf" content="{{ book.series | escape }}">
<meta itemprop="volumeNumber" content="{{ book.series_number }}">
({{ book.series }}{% if book.series_number %} #{{ book.series_number }}{% endif %})
{% endif %}
</p>
{% endif %}
{% if book.authors %} {% if book.authors %}
<h2 class="subtitle"> <div class="subtitle">
{% trans "by" %} {% include 'snippets/authors.html' with book=book %} {% trans "by" %} {% include 'snippets/authors.html' with book=book %}
</h2> </div>
{% endif %} {% endif %}
</div> </div>
@ -41,7 +54,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 %}
@ -89,7 +102,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
@ -107,7 +120,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' %}
@ -185,7 +198,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

@ -8,6 +8,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">
@ -107,7 +108,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

@ -12,14 +12,19 @@
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{% get_media_prefix %}{{ site.favicon }}{% else %}{% static "images/favicon.ico" %}{% endif %}"> <link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{% get_media_prefix %}{{ 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 %}{% get_media_prefix %}{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}"> {% block opengraph_images %}
<meta name="og:image" content="{% if site.logo %}{% get_media_prefix %}{{ 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>

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
type="button" {% if not fallback_url %}
type="button"
{% else %}
type="submit"
{% endif %}
class="{% if not nonbutton %}button {% endif %}{{ class }}{% if button_type %} {{ button_type }}{% endif %}" class="{% if not nonbutton %}button {% endif %}{{ class }}{% if button_type %} {{ button_type }}{% endif %}"
data-controls="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}" data-controls="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}"
{% if focus %}data-focus-target="{{ focus }}{% if controls_uid %}-{{ controls_uid }}{% endif %}"{% endif %} {% if focus %}data-focus-target="{{ focus }}{% if controls_uid %}-{{ controls_uid }}{% endif %}"{% endif %}
@ -20,3 +27,6 @@
<span>{{ text }}</span> <span>{{ text }}</span>
{% endif %} {% endif %}
</button> </button>
{% if fallback_url %}
</form>
{% endif %}

View file

@ -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,15 +1,17 @@
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):
self.book = models.Edition.objects.create( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
title="Example Edition", self.book = models.Edition.objects.create(
remote_id="https://example.com/book/1", title="Example Edition",
) remote_id="https://example.com/book/1",
)
self.author = models.Author.objects.create( self.author = models.Author.objects.create(
name="Author fullname", name="Author fullname",
aliases=["One", "Two"], aliases=["One", "Two"],

View file

@ -25,24 +25,25 @@ 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"""
self.user = models.User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" self.user = models.User.objects.create_user(
) "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
self.user.remote_id = "http://example.com/a/b" )
self.user.save(broadcast=False) self.user.remote_id = "http://example.com/a/b"
self.user.save(broadcast=False)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
self.userdata = json.loads(datafile.read_bytes()) self.userdata = json.loads(datafile.read_bytes())
# don't try to load the user icon # don't try to load the user icon
del self.userdata["icon"] del self.userdata["icon"]
image_file = pathlib.Path(__file__).parent.joinpath( image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/default_avi.jpg" "../../static/images/default_avi.jpg"
) )
image = Image.open(image_file) image = Image.open(image_file)
output = BytesIO() output = BytesIO()
image.save(output, format=image.format) image.save(output, format=image.format)
self.image_data = output.getvalue() self.image_data = output.getvalue()
def test_init(self, _): def test_init(self, _):
"""simple successfuly init""" """simple successfuly init"""
@ -97,10 +98,11 @@ class BaseActivity(TestCase):
status=200, status=200,
) )
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
result = resolve_remote_id( with patch("bookwyrm.models.user.set_remote_server.delay"):
"https://example.com/user/mouse", model=models.User result = resolve_remote_id(
) "https://example.com/user/mouse", model=models.User
)
self.assertIsInstance(result, models.User) self.assertIsInstance(result, models.User)
self.assertEqual(result.remote_id, "https://example.com/user/mouse") self.assertEqual(result.remote_id, "https://example.com/user/mouse")
self.assertEqual(result.name, "MOUSE?? MOUSE!!") self.assertEqual(result.name, "MOUSE?? MOUSE!!")
@ -139,8 +141,9 @@ 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.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
activity.to_model(model=models.User, instance=self.user) with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
activity.to_model(model=models.User, instance=self.user)
self.assertIsNotNone(self.user.avatar.file) self.assertIsNotNone(self.user.avatar.file)
self.assertEqual(self.user.name, "New Name") self.assertEqual(self.user.name, "New Name")
self.assertEqual(self.user.key_pair.public_key, "hi") self.assertEqual(self.user.key_pair.public_key, "hi")
@ -152,9 +155,10 @@ class BaseActivity(TestCase):
content="test status", content="test status",
user=self.user, user=self.user,
) )
book = models.Edition.objects.create( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
title="Test Edition", remote_id="http://book.com/book" book = models.Edition.objects.create(
) title="Test Edition", remote_id="http://book.com/book"
)
update_data = activitypub.Note( update_data = activitypub.Note(
id=status.remote_id, id=status.remote_id,
content=status.content, content=status.content,

View file

@ -20,8 +20,9 @@ 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.models.user.set_remote_server.delay"): with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
user = activity.to_model(model=models.User) with patch("bookwyrm.models.user.set_remote_server.delay"):
user = activity.to_model(model=models.User)
self.assertEqual(user.username, "mouse@example.com") self.assertEqual(user.username, "mouse@example.com")
self.assertEqual(user.remote_id, "https://example.com/user/mouse") self.assertEqual(user.remote_id, "https://example.com/user/mouse")
self.assertFalse(user.local) self.assertFalse(user.local)

View file

@ -12,20 +12,22 @@ class Quotation(TestCase):
def setUp(self): def setUp(self):
"""model objects we'll need""" """model objects we'll need"""
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.user = models.User.objects.create_user( with patch("bookwyrm.models.user.set_remote_server.delay"):
"mouse", self.user = models.User.objects.create_user(
"mouse@mouse.mouse", "mouse",
"mouseword", "mouse@mouse.mouse",
local=False, "mouseword",
inbox="https://example.com/user/mouse/inbox", local=False,
outbox="https://example.com/user/mouse/outbox", inbox="https://example.com/user/mouse/inbox",
remote_id="https://example.com/user/mouse", outbox="https://example.com/user/mouse/outbox",
remote_id="https://example.com/user/mouse",
)
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.book = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
) )
self.book = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_quotation.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_quotation.json")
self.status_data = json.loads(datafile.read_bytes()) self.status_data = json.loads(datafile.read_bytes())

View file

@ -74,11 +74,12 @@ class AbstractConnector(TestCase):
Mapping("openlibraryKey"), Mapping("openlibraryKey"),
] ]
self.book = models.Edition.objects.create( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
title="Test Book", self.book = models.Edition.objects.create(
remote_id="https://example.com/book/1234", title="Test Book",
openlibrary_key="OL1234M", remote_id="https://example.com/book/1234",
) openlibrary_key="OL1234M",
)
def test_abstract_connector_init(self): def test_abstract_connector_init(self):
"""barebones connector for search with defaults""" """barebones connector for search with defaults"""
@ -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.connectors.abstract_connector.load_more_data.delay"): with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
result = self.connector.get_or_create_book("https://example.com/book/abcd") with patch("bookwyrm.connectors.abstract_connector.load_more_data.delay"):
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,8 +26,9 @@ 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"""
work = models.Work.objects.create(title="Test Work") with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
book = models.Edition.objects.create(title="Test Edition", parent_work=work) work = models.Work.objects.create(title="Test 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)
self.assertEqual(book, result) self.assertEqual(book, result)

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,14 +14,15 @@ 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"""
self.work = models.Work.objects.create(title="Example Work") with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
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, isbn_10="0000000000" title="Example Edition", parent_work=self.work, isbn_10="0000000000"
) )
self.edition = models.Edition.objects.create( self.edition = models.Edition.objects.create(
title="Another Edition", parent_work=self.work, isbn_10="1111111111" title="Another Edition", parent_work=self.work, isbn_10="1111111111"
) )
self.connector = models.Connector.objects.create( self.connector = models.Connector.objects.create(
identifier="test_connector", identifier="test_connector",

View file

@ -178,20 +178,26 @@ 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( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"bookwyrm.connectors.abstract_connector.AbstractConnector." with patch(
"create_edition_from_data" "bookwyrm.connectors.abstract_connector.AbstractConnector."
): "create_edition_from_data"
self.connector.expand_book_data(edition) ):
self.connector.expand_book_data(work) self.connector.expand_book_data(edition)
self.connector.expand_book_data(work)
def test_get_description(self): def test_get_description(self):
"""should do some cleanup on the description data""" """should do some cleanup on the description data"""
@ -224,11 +230,14 @@ class Openlibrary(TestCase):
json={"hi": "there"}, json={"hi": "there"},
status=200, status=200,
) )
with patch( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
"bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data" with patch(
) as mock: "bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data"
mock.return_value = [] ) as mock:
result = self.connector.create_edition_from_data(work, self.edition_data) mock.return_value = []
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,12 +30,13 @@ 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")
edition = models.Edition.objects.create( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
title="Edition of Example Work", edition = models.Edition.objects.create(
published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc), title="Edition of Example Work",
) published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc),
edition.authors.add(author) )
result = self.connector.search("Edition of Example")[0] edition.authors.add(author)
result = self.connector.search("Edition of Example")[0]
self.assertEqual(result.title, "Edition of Example Work") self.assertEqual(result.title, "Edition of Example Work")
self.assertEqual(result.key, edition.remote_id) self.assertEqual(result.key, edition.remote_id)
self.assertEqual(result.author, "Anonymous") self.assertEqual(result.author, "Anonymous")
@ -44,34 +46,35 @@ 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")
edition = models.Edition.objects.create( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
title="Edition of Example Work", edition = models.Edition.objects.create(
published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc), title="Edition of Example Work",
parent_work=models.Work.objects.create(title=""), published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc),
) parent_work=models.Work.objects.create(title=""),
# author text is rank C )
edition.authors.add(author) # author text is rank C
edition.authors.add(author)
# series is rank D # series is rank D
models.Edition.objects.create( models.Edition.objects.create(
title="Another Edition", title="Another Edition",
series="Anonymous", series="Anonymous",
parent_work=models.Work.objects.create(title=""), parent_work=models.Work.objects.create(title=""),
) )
# subtitle is rank B # subtitle is rank B
models.Edition.objects.create( models.Edition.objects.create(
title="More Editions", title="More Editions",
subtitle="The Anonymous Edition", subtitle="The Anonymous Edition",
parent_work=models.Work.objects.create(title=""), parent_work=models.Work.objects.create(title=""),
) )
# title is rank A # title is rank A
models.Edition.objects.create(title="Anonymous") models.Edition.objects.create(title="Anonymous")
# doesn't rank in this search # doesn't rank in this search
edition = models.Edition.objects.create( edition = models.Edition.objects.create(
title="An Edition", parent_work=models.Work.objects.create(title="") title="An Edition", parent_work=models.Work.objects.create(title="")
) )
results = self.connector.search("Anonymous") results = self.connector.search("Anonymous")
self.assertEqual(len(results), 3) self.assertEqual(len(results), 3)
self.assertEqual(results[0].title, "Anonymous") self.assertEqual(results[0].title, "Anonymous")
self.assertEqual(results[1].title, "More Editions") self.assertEqual(results[1].title, "More Editions")
@ -79,28 +82,29 @@ 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"""
work = models.Work.objects.create(title="Work Title") with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
edition_1 = models.Edition.objects.create( work = models.Work.objects.create(title="Work Title")
title="Edition 1 Title", parent_work=work edition_1 = models.Edition.objects.create(
) title="Edition 1 Title", parent_work=work
edition_2 = models.Edition.objects.create( )
title="Edition 2 Title", edition_2 = models.Edition.objects.create(
parent_work=work, title="Edition 2 Title",
edition_rank=20, # that's default babey parent_work=work,
) edition_rank=20, # that's default babey
edition_3 = models.Edition.objects.create(title="Fish", parent_work=work) )
edition_3 = models.Edition.objects.create(title="Fish", parent_work=work)
# pick the best edition # pick the best edition
results = self.connector.search("Edition 1 Title") results = self.connector.search("Edition 1 Title")
self.assertEqual(len(results), 1) self.assertEqual(len(results), 1)
self.assertEqual(results[0].key, edition_1.remote_id) self.assertEqual(results[0].key, edition_1.remote_id)
# pick the default edition when no match is best # pick the default edition when no match is best
results = self.connector.search("Edition Title") results = self.connector.search("Edition Title")
self.assertEqual(len(results), 1) self.assertEqual(len(results), 1)
self.assertEqual(results[0].key, edition_2.remote_id) self.assertEqual(results[0].key, edition_2.remote_id)
# only matches one edition, so no deduplication takes place # only matches one edition, so no deduplication takes place
results = self.connector.search("Fish") results = self.connector.search("Fish")
self.assertEqual(len(results), 1) self.assertEqual(len(results), 1)
self.assertEqual(results[0].key, edition_3.remote_id) self.assertEqual(results[0].key, edition_3.remote_id)

View file

@ -21,9 +21,10 @@ 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)
self.user = models.User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse", "mouse@mouse.mouse", "password", local=True self.user = models.User.objects.create_user(
) "mouse", "mouse@mouse.mouse", "password", local=True
)
models.Connector.objects.create( models.Connector.objects.create(
identifier=DOMAIN, identifier=DOMAIN,
@ -36,12 +37,13 @@ class GoodreadsImport(TestCase):
search_url="https://%s/search?q=" % DOMAIN, search_url="https://%s/search?q=" % DOMAIN,
priority=1, priority=1,
) )
work = models.Work.objects.create(title="Test Work") with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.book = models.Edition.objects.create( work = models.Work.objects.create(title="Test Work")
title="Example Edition", self.book = models.Edition.objects.create(
remote_id="https://example.com/book/1", title="Example Edition",
parent_work=work, remote_id="https://example.com/book/1",
) parent_work=work,
)
def test_create_job(self): def test_create_job(self):
"""creates the import job entry and checks csv""" """creates the import job entry and checks csv"""
@ -92,7 +94,8 @@ 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")
book = models.Edition.objects.create(title="Test Book") with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
book = models.Edition.objects.create(title="Test Book")
with patch( with patch(
"bookwyrm.models.import_job.ImportItem.get_book_from_isbn" "bookwyrm.models.import_job.ImportItem.get_book_from_isbn"
@ -119,10 +122,11 @@ class GoodreadsImport(TestCase):
) )
break break
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
handle_imported_book( with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
self.importer.service, self.user, import_item, False, "public" handle_imported_book(
) self.importer.service, self.user, import_item, False, "public"
)
shelf.refresh_from_db() shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book) self.assertEqual(shelf.books.first(), self.book)
@ -183,13 +187,14 @@ class GoodreadsImport(TestCase):
) )
break break
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
handle_imported_book( with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
self.importer.service, self.user, import_item, False, "public" handle_imported_book(
) self.importer.service, self.user, import_item, False, "public"
handle_imported_book( )
self.importer.service, self.user, import_item, False, "public" handle_imported_book(
) self.importer.service, self.user, import_item, False, "public"
)
shelf.refresh_from_db() shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book) self.assertEqual(shelf.books.first(), self.book)
@ -216,10 +221,11 @@ 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.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
handle_imported_book( with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
self.importer.service, self.user, import_item, True, "unlisted" handle_imported_book(
) self.importer.service, self.user, import_item, True, "unlisted"
)
review = models.Review.objects.get(book=self.book, user=self.user) review = models.Review.objects.get(book=self.book, user=self.user)
self.assertEqual(review.content, "mixed feelings") self.assertEqual(review.content, "mixed feelings")
self.assertEqual(review.rating, 2) self.assertEqual(review.rating, 2)
@ -242,10 +248,11 @@ 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.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
handle_imported_book( with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
self.importer.service, self.user, import_item, True, "unlisted" handle_imported_book(
) self.importer.service, self.user, import_item, True, "unlisted"
)
review = models.ReviewRating.objects.get(book=self.book, user=self.user) review = models.ReviewRating.objects.get(book=self.book, user=self.user)
self.assertIsInstance(review, models.ReviewRating) self.assertIsInstance(review, models.ReviewRating)
self.assertEqual(review.rating, 2) self.assertEqual(review.rating, 2)
@ -265,10 +272,11 @@ 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.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
handle_imported_book( with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
self.importer.service, self.user, import_item, False, "unlisted" handle_imported_book(
) self.importer.service, self.user, import_item, False, "unlisted"
)
self.assertFalse( self.assertFalse(
models.Review.objects.filter(book=self.book, user=self.user).exists() models.Review.objects.filter(book=self.book, user=self.user).exists()
) )

View file

@ -22,9 +22,10 @@ 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)
self.user = models.User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mmai", "mmai@mmai.mmai", "password", local=True self.user = models.User.objects.create_user(
) "mmai", "mmai@mmai.mmai", "password", local=True
)
models.Connector.objects.create( models.Connector.objects.create(
identifier=DOMAIN, identifier=DOMAIN,
@ -37,12 +38,13 @@ class LibrarythingImport(TestCase):
search_url="https://%s/search?q=" % DOMAIN, search_url="https://%s/search?q=" % DOMAIN,
priority=1, priority=1,
) )
work = models.Work.objects.create(title="Test Work") with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.book = models.Edition.objects.create( work = models.Work.objects.create(title="Test Work")
title="Example Edition", self.book = models.Edition.objects.create(
remote_id="https://example.com/book/1", title="Example Edition",
parent_work=work, remote_id="https://example.com/book/1",
) parent_work=work,
)
def test_create_job(self): def test_create_job(self):
"""creates the import job entry and checks csv""" """creates the import job entry and checks csv"""
@ -82,7 +84,8 @@ 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")
book = models.Edition.objects.create(title="Test Book") with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
book = models.Edition.objects.create(title="Test Book")
with patch( with patch(
"bookwyrm.models.import_job.ImportItem.get_book_from_isbn" "bookwyrm.models.import_job.ImportItem.get_book_from_isbn"
@ -111,10 +114,11 @@ class LibrarythingImport(TestCase):
) )
break break
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
handle_imported_book( with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
self.importer.service, self.user, import_item, False, "public" handle_imported_book(
) self.importer.service, self.user, import_item, False, "public"
)
shelf.refresh_from_db() shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book) self.assertEqual(shelf.books.first(), self.book)
@ -147,10 +151,11 @@ class LibrarythingImport(TestCase):
) )
break break
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
handle_imported_book( with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
self.importer.service, self.user, import_item, False, "public" handle_imported_book(
) self.importer.service, self.user, import_item, False, "public"
)
shelf.refresh_from_db() shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book) self.assertEqual(shelf.books.first(), self.book)
@ -179,13 +184,14 @@ class LibrarythingImport(TestCase):
) )
break break
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
handle_imported_book( with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
self.importer.service, self.user, import_item, False, "public" handle_imported_book(
) self.importer.service, self.user, import_item, False, "public"
handle_imported_book( )
self.importer.service, self.user, import_item, False, "public" handle_imported_book(
) self.importer.service, self.user, import_item, False, "public"
)
shelf.refresh_from_db() shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book) self.assertEqual(shelf.books.first(), self.book)
@ -212,10 +218,11 @@ 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.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
handle_imported_book( with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
self.importer.service, self.user, import_item, True, "unlisted" handle_imported_book(
) self.importer.service, self.user, import_item, True, "unlisted"
)
review = models.Review.objects.get(book=self.book, user=self.user) review = models.Review.objects.get(book=self.book, user=self.user)
self.assertEqual(review.content, "chef d'oeuvre") self.assertEqual(review.content, "chef d'oeuvre")
self.assertEqual(review.rating, 5) self.assertEqual(review.rating, 5)
@ -235,10 +242,11 @@ 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.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
handle_imported_book( with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
self.importer.service, self.user, import_item, False, "unlisted" handle_imported_book(
) self.importer.service, self.user, import_item, False, "unlisted"
)
self.assertFalse( self.assertFalse(
models.Review.objects.filter(book=self.book, user=self.user).exists() models.Review.objects.filter(book=self.book, user=self.user).exists()
) )

View file

@ -12,23 +12,29 @@ class Activitystreams(TestCase):
def setUp(self): def setUp(self):
"""we need some stuff""" """we need some stuff"""
self.local_user = models.User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse" self.local_user = models.User.objects.create_user(
) "mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse"
self.another_user = models.User.objects.create_user(
"nutria", "nutria@nutria.nutria", "password", local=True, localname="nutria"
)
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=False,
remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox",
) )
self.book = models.Edition.objects.create(title="test book") self.another_user = models.User.objects.create_user(
"nutria",
"nutria@nutria.nutria",
"password",
local=True,
localname="nutria",
)
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=False,
remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox",
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")
def test_populate_streams(self, _): def test_populate_streams(self, _):
"""make sure the function on the redis manager gets called""" """make sure the function on the redis manager gets called"""

View file

@ -19,21 +19,22 @@ class ActivitypubMixins(TestCase):
def setUp(self): def setUp(self):
"""shared data""" """shared data"""
self.local_user = models.User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" self.local_user = models.User.objects.create_user(
) "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
self.local_user.remote_id = "http://example.com/a/b"
self.local_user.save(broadcast=False)
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=False,
remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox",
) )
self.local_user.remote_id = "http://example.com/a/b"
self.local_user.save(broadcast=False)
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=False,
remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox",
)
self.object_mock = { self.object_mock = {
"to": "to field", "to": "to field",
@ -70,9 +71,10 @@ 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
book = models.Edition.objects.create( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
title="Test Edition", remote_id="http://book.com/book" book = models.Edition.objects.create(
) title="Test Edition", remote_id="http://book.com/book"
)
self.assertEqual(book.origin_id, "http://book.com/book") self.assertEqual(book.origin_id, "http://book.com/book")
self.assertNotEqual(book.remote_id, "http://book.com/book") self.assertNotEqual(book.remote_id, "http://book.com/book")
@ -101,10 +103,11 @@ 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"""
book = models.Edition.objects.create( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
title="Test edition", book = models.Edition.objects.create(
openlibrary_key="OL1234", title="Test edition",
) openlibrary_key="OL1234",
)
result = models.Edition.find_existing({"openlibraryKey": "OL1234"}) result = models.Edition.find_existing({"openlibraryKey": "OL1234"})
self.assertEqual(result, book) self.assertEqual(result, book)
@ -140,16 +143,17 @@ 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.models.user.set_remote_server.delay"): with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
another_remote_user = models.User.objects.create_user( with patch("bookwyrm.models.user.set_remote_server.delay"):
"nutria", another_remote_user = models.User.objects.create_user(
"nutria@nutria.com", "nutria",
"nutriaword", "nutria@nutria.com",
local=False, "nutriaword",
remote_id="https://example.com/users/nutria", local=False,
inbox="https://example.com/users/nutria/inbox", remote_id="https://example.com/users/nutria",
outbox="https://example.com/users/nutria/outbox", inbox="https://example.com/users/nutria/inbox",
) outbox="https://example.com/users/nutria/outbox",
)
MockSelf = namedtuple("Self", ("privacy", "user", "recipients")) MockSelf = namedtuple("Self", ("privacy", "user", "recipients"))
mock_self = MockSelf("public", self.local_user, [another_remote_user]) mock_self = MockSelf("public", self.local_user, [another_remote_user])
@ -163,16 +167,17 @@ 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.models.user.set_remote_server.delay"): with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
another_remote_user = models.User.objects.create_user( with patch("bookwyrm.models.user.set_remote_server.delay"):
"nutria", another_remote_user = models.User.objects.create_user(
"nutria@nutria.com", "nutria",
"nutriaword", "nutria@nutria.com",
local=False, "nutriaword",
remote_id="https://example.com/users/nutria", local=False,
inbox="https://example.com/users/nutria/inbox", remote_id="https://example.com/users/nutria",
outbox="https://example.com/users/nutria/outbox", inbox="https://example.com/users/nutria/inbox",
) outbox="https://example.com/users/nutria/outbox",
)
MockSelf = namedtuple("Self", ("privacy", "user", "recipients")) MockSelf = namedtuple("Self", ("privacy", "user", "recipients"))
mock_self = MockSelf("direct", self.local_user, [another_remote_user]) mock_self = MockSelf("direct", self.local_user, [another_remote_user])
@ -184,17 +189,18 @@ 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.models.user.set_remote_server.delay"): with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
another_remote_user = models.User.objects.create_user( with patch("bookwyrm.models.user.set_remote_server.delay"):
"nutria", another_remote_user = models.User.objects.create_user(
"nutria@nutria.com", "nutria",
"nutriaword", "nutria@nutria.com",
local=False, "nutriaword",
remote_id="https://example.com/users/nutria", local=False,
inbox="https://example.com/users/nutria/inbox", remote_id="https://example.com/users/nutria",
shared_inbox="http://example.com/inbox", inbox="https://example.com/users/nutria/inbox",
outbox="https://example.com/users/nutria/outbox", shared_inbox="http://example.com/inbox",
) outbox="https://example.com/users/nutria/outbox",
)
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)
@ -206,17 +212,18 @@ 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.models.user.set_remote_server.delay"): with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
another_remote_user = models.User.objects.create_user( with patch("bookwyrm.models.user.set_remote_server.delay"):
"nutria", another_remote_user = models.User.objects.create_user(
"nutria@nutria.com", "nutria",
"nutriaword", "nutria@nutria.com",
local=False, "nutriaword",
remote_id="https://example.com/users/nutria", local=False,
inbox="https://example.com/users/nutria/inbox", remote_id="https://example.com/users/nutria",
outbox="https://example.com/users/nutria/outbox", inbox="https://example.com/users/nutria/inbox",
bookwyrm_user=False, outbox="https://example.com/users/nutria/outbox",
) bookwyrm_user=False,
)
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)

View file

@ -12,19 +12,20 @@ class BaseModel(TestCase):
def setUp(self): def setUp(self):
"""shared data""" """shared data"""
self.local_user = models.User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" self.local_user = models.User.objects.create_user(
) "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=False,
remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox",
) )
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=False,
remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox",
)
class BookWyrmTestModel(base_model.BookWyrmModel): class BookWyrmTestModel(base_model.BookWyrmModel):
"""just making it not abstract""" """just making it not abstract"""

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,17 +13,18 @@ class Book(TestCase):
def setUp(self): def setUp(self):
"""we'll need some books""" """we'll need some books"""
self.work = models.Work.objects.create( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
title="Example Work", remote_id="https://example.com/book/1" self.work = models.Work.objects.create(
) title="Example Work", remote_id="https://example.com/book/1"
self.first_edition = models.Edition.objects.create( )
title="Example Edition", self.first_edition = models.Edition.objects.create(
parent_work=self.work, title="Example Edition",
) parent_work=self.work,
self.second_edition = models.Edition.objects.create( )
title="Another Example Edition", self.second_edition = models.Edition.objects.create(
parent_work=self.work, title="Another Example Edition",
) parent_work=self.work,
)
def test_remote_id(self): def test_remote_id(self):
"""fanciness with remote/origin ids""" """fanciness with remote/origin ids"""
@ -56,7 +58,8 @@ 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"""
book = models.Edition.objects.create(title="Test Edition") with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
book = models.Edition.objects.create(title="Test Edition")
self.assertEqual(book.edition_info, "") self.assertEqual(book.edition_info, "")
book.physical_format = "worm" book.physical_format = "worm"

View file

@ -11,29 +11,30 @@ 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.models.user.set_remote_server.delay"): with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.remote_user = models.User.objects.create_user( with patch("bookwyrm.models.user.set_remote_server.delay"):
"rat", self.remote_user = models.User.objects.create_user(
"rat@rat.com", "rat",
"ratword", "rat@rat.com",
federated_server=self.server, "ratword",
local=False, federated_server=self.server,
remote_id="https://example.com/users/rat", local=False,
inbox="https://example.com/users/rat/inbox", remote_id="https://example.com/users/rat",
outbox="https://example.com/users/rat/outbox", inbox="https://example.com/users/rat/inbox",
) outbox="https://example.com/users/rat/outbox",
self.inactive_remote_user = models.User.objects.create_user( )
"nutria", self.inactive_remote_user = models.User.objects.create_user(
"nutria@nutria.com", "nutria",
"nutriaword", "nutria@nutria.com",
federated_server=self.server, "nutriaword",
local=False, federated_server=self.server,
remote_id="https://example.com/users/nutria", local=False,
inbox="https://example.com/users/nutria/inbox", remote_id="https://example.com/users/nutria",
outbox="https://example.com/users/nutria/outbox", inbox="https://example.com/users/nutria/inbox",
is_active=False, outbox="https://example.com/users/nutria/outbox",
deactivation_reason="self_deletion", is_active=False,
) deactivation_reason="self_deletion",
)
def test_block_unblock(self): def test_block_unblock(self):
"""block a server and all users on it""" """block a server and all users on it"""

View file

@ -188,9 +188,10 @@ 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"""
user = User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"rat", "rat@rat.rat", "ratword", local=True, localname="rat" user = User.objects.create_user(
) "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
)
public = "https://www.w3.org/ns/activitystreams#Public" public = "https://www.w3.org/ns/activitystreams#Public"
followers = "%s/followers" % user.remote_id followers = "%s/followers" % user.remote_id
@ -248,16 +249,20 @@ 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:
unrelated_user = User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"rat", "rat@rat.rat", "ratword", local=True, localname="rat" unrelated_user = User.objects.create_user(
) "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",
with patch("bookwyrm.models.user.set_remote_server.delay"): json=userdata,
value = instance.field_from_activity("https://example.com/user/mouse") status=200,
)
with patch("bookwyrm.models.user.set_remote_server.delay"):
value = instance.field_from_activity("https://example.com/user/mouse")
self.assertIsInstance(value, User) self.assertIsInstance(value, User)
self.assertNotEqual(value, unrelated_user) self.assertNotEqual(value, unrelated_user)
self.assertEqual(value.remote_id, "https://example.com/user/mouse") self.assertEqual(value.remote_id, "https://example.com/user/mouse")
@ -272,11 +277,12 @@ 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:
unrelated_user = User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"rat", "rat@rat.rat", "ratword", local=True, localname="rat" unrelated_user = User.objects.create_user(
) "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
with patch("bookwyrm.models.user.set_remote_server.delay"): )
value = instance.field_from_activity(activitypub.Person(**userdata)) with patch("bookwyrm.models.user.set_remote_server.delay"):
value = instance.field_from_activity(activitypub.Person(**userdata))
self.assertIsInstance(value, User) self.assertIsInstance(value, User)
self.assertNotEqual(value, unrelated_user) self.assertNotEqual(value, unrelated_user)
self.assertEqual(value.remote_id, "https://example.com/user/mouse") self.assertEqual(value.remote_id, "https://example.com/user/mouse")
@ -288,28 +294,31 @@ 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())
user = User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" user = User.objects.create_user(
) "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
user.remote_id = "https://example.com/user/mouse" )
user.save(broadcast=False) user.remote_id = "https://example.com/user/mouse"
User.objects.create_user( user.save(broadcast=False)
"rat", "rat@rat.rat", "ratword", local=True, localname="rat"
)
with patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast"): User.objects.create_user(
value = instance.field_from_activity(activitypub.Person(**userdata)) "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
)
with patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast"):
value = instance.field_from_activity(activitypub.Person(**userdata))
self.assertEqual(value, user) self.assertEqual(value, user)
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)
user = User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" user = User.objects.create_user(
) "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
User.objects.create_user( )
"rat", "rat@rat.rat", "ratword", local=True, localname="rat" User.objects.create_user(
) "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
)
value = instance.field_from_activity(user.remote_id) value = instance.field_from_activity(user.remote_id)
self.assertEqual(value, user) self.assertEqual(value, user)
@ -351,10 +360,11 @@ 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.models.user.set_remote_server.delay"): with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
value = instance.field_from_activity( with patch("bookwyrm.models.user.set_remote_server.delay"):
["https://example.com/user/mouse", "bleh"] value = instance.field_from_activity(
) ["https://example.com/user/mouse", "bleh"]
)
self.assertIsInstance(value, list) self.assertIsInstance(value, list)
self.assertEqual(len(value), 1) self.assertEqual(len(value), 1)
self.assertIsInstance(value[0], User) self.assertIsInstance(value[0], User)
@ -386,16 +396,17 @@ 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"""
user = User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" user = User.objects.create_user(
) "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
image_file = pathlib.Path(__file__).parent.joinpath( )
"../../static/images/default_avi.jpg" image_file = pathlib.Path(__file__).parent.joinpath(
) "../../static/images/default_avi.jpg"
image = Image.open(image_file) )
output = BytesIO() image = Image.open(image_file)
image.save(output, format=image.format) output = BytesIO()
user.avatar.save("test.jpg", ContentFile(output.getvalue())) image.save(output, format=image.format)
user.avatar.save("test.jpg", ContentFile(output.getvalue()))
output = fields.image_serializer(user.avatar, alt="alt text") output = fields.image_serializer(user.avatar, alt="alt text")
self.assertIsNotNone( self.assertIsNotNone(

View file

@ -59,9 +59,10 @@ 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"] = ""
user = models.User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" user = models.User.objects.create_user(
) "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
)
job = models.ImportJob.objects.create(user=user) job = models.ImportJob.objects.create(user=user)
self.item_1 = models.ImportItem.objects.create( self.item_1 = models.ImportItem.objects.create(
job=job, index=1, data=currently_reading_data job=job, index=1, data=currently_reading_data
@ -174,6 +175,9 @@ class ImportJob(TestCase):
with patch( with patch(
"bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data" "bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data"
): ):
book = self.item_1.get_book_from_isbn() with patch(
"bookwyrm.preview_images.generate_edition_preview_image_task.delay"
):
book = self.item_1.get_book_from_isbn()
self.assertEqual(book.title, "Sabriel") self.assertEqual(book.title, "Sabriel")

View file

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

@ -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,15 +11,16 @@ class ReadThrough(TestCase):
def setUp(self): def setUp(self):
"""look, a shelf""" """look, a shelf"""
self.user = models.User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" self.user = models.User.objects.create_user(
) "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
)
self.work = models.Work.objects.create(title="Example Work") with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
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
) )
self.readthrough = models.ReadThrough.objects.create( self.readthrough = models.ReadThrough.objects.create(
user=self.user, book=self.edition user=self.user, book=self.edition

View file

@ -10,21 +10,22 @@ class Relationship(TestCase):
def setUp(self): def setUp(self):
"""we need some users for this""" """we need some users for this"""
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
self.remote_user = models.User.objects.create_user( with patch("bookwyrm.models.user.set_remote_server.delay"):
"rat", self.remote_user = models.User.objects.create_user(
"rat@rat.com", "rat",
"ratword", "rat@rat.com",
local=False, "ratword",
remote_id="https://example.com/users/rat", local=False,
inbox="https://example.com/users/rat/inbox", remote_id="https://example.com/users/rat",
outbox="https://example.com/users/rat/outbox", inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox",
)
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
) )
self.local_user = models.User.objects.create_user( self.local_user.remote_id = "http://local.com/user/mouse"
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" self.local_user.save(broadcast=False)
)
self.local_user.remote_id = "http://local.com/user/mouse"
self.local_user.save(broadcast=False)
def test_user_follows_from_request(self): def test_user_follows_from_request(self):
"""convert a follow request into a follow""" """convert a follow request into a follow"""

View file

@ -12,11 +12,15 @@ class Shelf(TestCase):
def setUp(self): def setUp(self):
"""look, a shelf""" """look, a shelf"""
self.local_user = models.User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" self.local_user = models.User.objects.create_user(
) "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
work = models.Work.objects.create(title="Test Work") )
self.book = models.Edition.objects.create(title="test book", parent_work=work) with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
work = models.Work.objects.create(title="Test 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,34 +16,37 @@ 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"""
self.local_user = models.User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" self.local_user = models.User.objects.create_user(
) "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=False,
remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox",
) )
self.book = models.Edition.objects.create(title="Test Edition") with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=False,
remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox",
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")
image_file = pathlib.Path(__file__).parent.joinpath( image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/default_avi.jpg" "../../static/images/default_avi.jpg"
) )
image = Image.open(image_file) image = Image.open(image_file)
output = BytesIO() output = BytesIO()
with patch("bookwyrm.models.Status.broadcast"): with patch("bookwyrm.models.Status.broadcast"):
image.save(output, format=image.format) image.save(output, format=image.format)
self.book.cover.save("test.jpg", ContentFile(output.getvalue())) self.book.cover.save("test.jpg", ContentFile(output.getvalue()))
def test_status_generated_fields(self, *_): def test_status_generated_fields(self, *_):
"""setting remote id""" """setting remote id"""
@ -58,9 +61,10 @@ 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
) )
models.Review.objects.create( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
content="hey", reply_parent=parent, user=self.local_user, book=self.book models.Review.objects.create(
) content="hey", reply_parent=parent, user=self.local_user, book=self.book
)
models.Status.objects.create( models.Status.objects.create(
content="hi hello", reply_parent=child, user=self.local_user content="hi hello", reply_parent=child, user=self.local_user
) )
@ -92,9 +96,10 @@ 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
) )
models.Review.objects.create( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
content="hey", reply_parent=parent, user=self.local_user, book=self.book models.Review.objects.create(
) content="hey", reply_parent=parent, user=self.local_user, book=self.book
)
models.Status.objects.create( models.Status.objects.create(
content="hi hello", reply_parent=child, user=self.local_user content="hi hello", reply_parent=child, user=self.local_user
) )
@ -251,14 +256,15 @@ 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"""
status = models.Review.objects.create( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
name="Review name", status = models.Review.objects.create(
content="test content", name="Review name",
rating=3.0, content="test content",
user=self.local_user, rating=3.0,
book=self.book, user=self.local_user,
) book=self.book,
activity = status.to_activity() )
activity = status.to_activity()
self.assertEqual(activity["id"], status.remote_id) self.assertEqual(activity["id"], status.remote_id)
self.assertEqual(activity["type"], "Review") self.assertEqual(activity["type"], "Review")
self.assertEqual(activity["rating"], 3) self.assertEqual(activity["rating"], 3)
@ -268,14 +274,15 @@ 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"""
status = models.Review.objects.create( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
name="Review's name", status = models.Review.objects.create(
content="test content", name="Review's name",
rating=3.0, content="test content",
user=self.local_user, rating=3.0,
book=self.book, user=self.local_user,
) book=self.book,
activity = status.to_activity(pure=True) )
activity = status.to_activity(pure=True)
self.assertEqual(activity["id"], status.remote_id) self.assertEqual(activity["id"], status.remote_id)
self.assertEqual(activity["type"], "Article") self.assertEqual(activity["type"], "Article")
self.assertEqual( self.assertEqual(
@ -292,13 +299,14 @@ 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"""
status = models.Review.objects.create( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
name="Review name", status = models.Review.objects.create(
content="test content", name="Review name",
user=self.local_user, content="test content",
book=self.book, user=self.local_user,
) book=self.book,
activity = status.to_activity(pure=True) )
activity = status.to_activity(pure=True)
self.assertEqual(activity["id"], status.remote_id) self.assertEqual(activity["id"], status.remote_id)
self.assertEqual(activity["type"], "Article") self.assertEqual(activity["type"], "Article")
self.assertEqual( self.assertEqual(
@ -314,12 +322,13 @@ 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"""
status = models.ReviewRating.objects.create( with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
rating=3.0, status = models.ReviewRating.objects.create(
user=self.local_user, rating=3.0,
book=self.book, user=self.local_user,
) book=self.book,
activity = status.to_activity(pure=True) )
activity = status.to_activity(pure=True)
self.assertEqual(activity["id"], status.remote_id) self.assertEqual(activity["id"], status.remote_id)
self.assertEqual(activity["type"], "Note") self.assertEqual(activity["type"], "Note")
self.assertEqual( self.assertEqual(
@ -348,11 +357,12 @@ 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
) )
fav = models.Favorite.objects.create(status=status, 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)
# can't fav a status twice # can't fav a status twice
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
models.Favorite.objects.create(status=status, user=self.local_user) models.Favorite.objects.create(status=status, user=self.local_user)
activity = fav.to_activity() activity = fav.to_activity()
self.assertEqual(activity["type"], "Like") self.assertEqual(activity["type"], "Like")
@ -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,15 +11,16 @@ 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):
self.user = models.User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse@%s" % DOMAIN, self.user = models.User.objects.create_user(
"mouse@mouse.mouse", "mouse@%s" % DOMAIN,
"mouseword", "mouse@mouse.mouse",
local=True, "mouseword",
localname="mouse", local=True,
name="hi", localname="mouse",
bookwyrm_user=False, name="hi",
) bookwyrm_user=False,
)
def test_computed_fields(self): def test_computed_fields(self):
"""username instead of id here""" """username instead of id here"""
@ -34,15 +35,16 @@ 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.models.user.set_remote_server.delay"): with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
user = models.User.objects.create_user( with patch("bookwyrm.models.user.set_remote_server.delay"):
"rat", user = models.User.objects.create_user(
"rat@rat.rat", "rat",
"ratword", "rat@rat.rat",
local=False, "ratword",
remote_id="https://example.com/dfjkg", local=False,
bookwyrm_user=False, remote_id="https://example.com/dfjkg",
) bookwyrm_user=False,
)
self.assertEqual(user.username, "rat@example.com") self.assertEqual(user.username, "rat@example.com")
def test_user_shelves(self): def test_user_shelves(self):

View file

@ -11,23 +11,29 @@ class Activitystreams(TestCase):
def setUp(self): def setUp(self):
"""use a test csv""" """use a test csv"""
self.local_user = models.User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse" self.local_user = models.User.objects.create_user(
) "mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse"
self.another_user = models.User.objects.create_user(
"nutria", "nutria@nutria.nutria", "password", local=True, localname="nutria"
)
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=False,
remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox",
) )
self.book = models.Edition.objects.create(title="test book") self.another_user = models.User.objects.create_user(
"nutria",
"nutria@nutria.nutria",
"password",
local=True,
localname="nutria",
)
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=False,
remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox",
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")
class TestStream(activitystreams.ActivityStream): class TestStream(activitystreams.ActivityStream):
"""test stream, don't have to do anything here""" """test stream, don't have to do anything here"""

View file

@ -15,14 +15,16 @@ 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()
self.local_user = models.User.objects.create_user( with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
"mouse@local.com", self.local_user = models.User.objects.create_user(
"mouse@mouse.mouse", "mouse@local.com",
"password", "mouse@mouse.mouse",
local=True, "password",
localname="mouse", local=True,
) localname="mouse",
models.SiteSettings.objects.create() )
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"):
models.SiteSettings.objects.create()
def test_invite_email(self, email_mock): def test_invite_email(self, email_mock):
"""load the invite email""" """load the invite email"""

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