mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-10-31 22:19:00 +00:00
Merge remote-tracking branch 'upstream/main' into images-django-imagekit
This commit is contained in:
commit
e251b687dc
171 changed files with 3674 additions and 2175 deletions
|
@ -13,16 +13,10 @@ DEFAULT_LANGUAGE="English"
|
||||||
## Leave unset to allow all hosts
|
## Leave unset to allow all hosts
|
||||||
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
|
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
|
||||||
|
|
||||||
OL_URL=https://openlibrary.org
|
|
||||||
|
|
||||||
## Database backend to use.
|
|
||||||
## Default is postgres, sqlite is for dev quickstart only (NOT production!!!)
|
|
||||||
BOOKWYRM_DATABASE_BACKEND=postgres
|
|
||||||
|
|
||||||
MEDIA_ROOT=images/
|
MEDIA_ROOT=images/
|
||||||
|
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
POSTGRES_PASSWORD=fedireads
|
POSTGRES_PASSWORD=securedbypassword123
|
||||||
POSTGRES_USER=fedireads
|
POSTGRES_USER=fedireads
|
||||||
POSTGRES_DB=fedireads
|
POSTGRES_DB=fedireads
|
||||||
POSTGRES_HOST=db
|
POSTGRES_HOST=db
|
||||||
|
@ -51,5 +45,14 @@ EMAIL_USE_SSL=false
|
||||||
# Thumbnails Generation
|
# Thumbnails Generation
|
||||||
ENABLE_THUMBNAIL_GENERATION=false
|
ENABLE_THUMBNAIL_GENERATION=false
|
||||||
|
|
||||||
# Set this to true when initializing certbot for domain, false when not
|
# Preview image generation can be computing and storage intensive
|
||||||
CERTBOT_INIT=false
|
# ENABLE_PREVIEW_IMAGES=True
|
||||||
|
|
||||||
|
# Specify RGB tuple or RGB hex strings,
|
||||||
|
# or use_dominant_color_light / use_dominant_color_dark
|
||||||
|
PREVIEW_BG_COLOR=use_dominant_color_light
|
||||||
|
# Change to #FFF if you use use_dominant_color_dark
|
||||||
|
PREVIEW_TEXT_COLOR="#363636"
|
||||||
|
PREVIEW_IMG_WIDTH=1200
|
||||||
|
PREVIEW_IMG_HEIGHT=630
|
||||||
|
PREVIEW_DEFAULT_COVER_COLOR="#002549"
|
||||||
|
|
|
@ -13,16 +13,10 @@ DEFAULT_LANGUAGE="English"
|
||||||
## Leave unset to allow all hosts
|
## Leave unset to allow all hosts
|
||||||
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
|
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
|
||||||
|
|
||||||
OL_URL=https://openlibrary.org
|
|
||||||
|
|
||||||
## Database backend to use.
|
|
||||||
## Default is postgres, sqlite is for dev quickstart only (NOT production!!!)
|
|
||||||
BOOKWYRM_DATABASE_BACKEND=postgres
|
|
||||||
|
|
||||||
MEDIA_ROOT=images/
|
MEDIA_ROOT=images/
|
||||||
|
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
POSTGRES_PASSWORD=securedbpassword123
|
POSTGRES_PASSWORD=securedbypassword123
|
||||||
POSTGRES_USER=fedireads
|
POSTGRES_USER=fedireads
|
||||||
POSTGRES_DB=fedireads
|
POSTGRES_DB=fedireads
|
||||||
POSTGRES_HOST=db
|
POSTGRES_HOST=db
|
||||||
|
@ -51,5 +45,14 @@ EMAIL_USE_SSL=false
|
||||||
# Thumbnails Generation
|
# Thumbnails Generation
|
||||||
ENABLE_THUMBNAIL_GENERATION=false
|
ENABLE_THUMBNAIL_GENERATION=false
|
||||||
|
|
||||||
# Set this to true when initializing certbot for domain, false when not
|
# Preview image generation can be computing and storage intensive
|
||||||
CERTBOT_INIT=false
|
# ENABLE_PREVIEW_IMAGES=True
|
||||||
|
|
||||||
|
# Specify RGB tuple or RGB hex strings,
|
||||||
|
# or use_dominant_color_light / use_dominant_color_dark
|
||||||
|
PREVIEW_BG_COLOR=use_dominant_color_light
|
||||||
|
# Change to #FFF if you use use_dominant_color_dark
|
||||||
|
PREVIEW_TEXT_COLOR="#363636"
|
||||||
|
PREVIEW_IMG_WIDTH=1200
|
||||||
|
PREVIEW_IMG_HEIGHT=630
|
||||||
|
PREVIEW_DEFAULT_COVER_COLOR="#002549"
|
||||||
|
|
2
.github/workflows/black.yml
vendored
2
.github/workflows/black.yml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: Lint Python
|
name: Python Formatting (run ./bw-dev black to fix)
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
|
2
.github/workflows/django-tests.yml
vendored
2
.github/workflows/django-tests.yml
vendored
|
@ -50,7 +50,6 @@ jobs:
|
||||||
SECRET_KEY: beepbeep
|
SECRET_KEY: beepbeep
|
||||||
DEBUG: true
|
DEBUG: true
|
||||||
DOMAIN: your.domain.here
|
DOMAIN: your.domain.here
|
||||||
OL_URL: https://openlibrary.org
|
|
||||||
BOOKWYRM_DATABASE_BACKEND: postgres
|
BOOKWYRM_DATABASE_BACKEND: postgres
|
||||||
MEDIA_ROOT: images/
|
MEDIA_ROOT: images/
|
||||||
POSTGRES_PASSWORD: hunter2
|
POSTGRES_PASSWORD: hunter2
|
||||||
|
@ -65,5 +64,6 @@ jobs:
|
||||||
EMAIL_HOST_USER: ""
|
EMAIL_HOST_USER: ""
|
||||||
EMAIL_HOST_PASSWORD: ""
|
EMAIL_HOST_PASSWORD: ""
|
||||||
EMAIL_USE_TLS: true
|
EMAIL_USE_TLS: true
|
||||||
|
ENABLE_PREVIEW_IMAGES: true
|
||||||
run: |
|
run: |
|
||||||
python manage.py test
|
python manage.py test
|
||||||
|
|
24
.github/workflows/pylint.yml
vendored
Normal file
24
.github/workflows/pylint.yml
vendored
Normal 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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -4,8 +4,18 @@ from bookwyrm import models, settings
|
||||||
|
|
||||||
def site_settings(request): # pylint: disable=unused-argument
|
def site_settings(request): # pylint: disable=unused-argument
|
||||||
"""include the custom info about the site"""
|
"""include the custom info about the site"""
|
||||||
|
request_protocol = "https://"
|
||||||
|
if not request.is_secure():
|
||||||
|
request_protocol = "http://"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"site": models.SiteSettings.objects.get(),
|
"site": models.SiteSettings.objects.get(),
|
||||||
"active_announcements": models.Announcement.active_announcements(),
|
"active_announcements": models.Announcement.active_announcements(),
|
||||||
"enable_thumbnail_generation": settings.ENABLE_THUMBNAIL_GENERATION,
|
"enable_thumbnail_generation": settings.ENABLE_THUMBNAIL_GENERATION,
|
||||||
|
"static_url": settings.STATIC_URL,
|
||||||
|
"media_url": settings.MEDIA_URL,
|
||||||
|
"static_path": settings.STATIC_PATH,
|
||||||
|
"media_path": settings.MEDIA_PATH,
|
||||||
|
"preview_images_enabled": settings.ENABLE_PREVIEW_IMAGES,
|
||||||
|
"request_protocol": request_protocol,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
65
bookwyrm/management/commands/generate_preview_images.py
Normal file
65
bookwyrm/management/commands/generate_preview_images.py
Normal 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("🧑🎨 ⎨ I’m all done! ✧ Enjoy ✧")
|
32
bookwyrm/migrations/0076_preview_images.py
Normal file
32
bookwyrm/migrations/0076_preview_images.py
Normal 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/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,11 +2,14 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from model_utils import FieldTracker
|
||||||
from model_utils.managers import InheritanceManager
|
from model_utils.managers import InheritanceManager
|
||||||
from imagekit.models import ImageSpecField
|
from imagekit.models import ImageSpecField
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from bookwyrm.settings import DOMAIN, DEFAULT_LANGUAGE, ENABLE_THUMBNAIL_GENERATION
|
from bookwyrm.preview_images import generate_edition_preview_image_task
|
||||||
|
from bookwyrm.settings import DOMAIN, DEFAULT_LANGUAGE, ENABLE_PREVIEW_IMAGES, ENABLE_THUMBNAIL_GENERATION
|
||||||
|
|
||||||
from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
|
from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
@ -83,10 +86,14 @@ class Book(BookDataModel):
|
||||||
cover = fields.ImageField(
|
cover = fields.ImageField(
|
||||||
upload_to="covers/", blank=True, null=True, alt_field="alt_text"
|
upload_to="covers/", blank=True, null=True, alt_field="alt_text"
|
||||||
)
|
)
|
||||||
|
preview_image = models.ImageField(
|
||||||
|
upload_to="previews/covers/", blank=True, null=True
|
||||||
|
)
|
||||||
first_published_date = fields.DateTimeField(blank=True, null=True)
|
first_published_date = fields.DateTimeField(blank=True, null=True)
|
||||||
published_date = fields.DateTimeField(blank=True, null=True)
|
published_date = fields.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
|
field_tracker = FieldTracker(fields=["authors", "title", "subtitle", "cover"])
|
||||||
|
|
||||||
if ENABLE_THUMBNAIL_GENERATION:
|
if ENABLE_THUMBNAIL_GENERATION:
|
||||||
cover_bw_book_xsmall_webp = ImageSpecField(
|
cover_bw_book_xsmall_webp = ImageSpecField(
|
||||||
|
@ -328,3 +335,17 @@ def isbn_13_to_10(isbn_13):
|
||||||
if checkdigit == 10:
|
if checkdigit == 10:
|
||||||
checkdigit = "X"
|
checkdigit = "X"
|
||||||
return converted + str(checkdigit)
|
return converted + str(checkdigit)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
@receiver(models.signals.post_save, sender=Edition)
|
||||||
|
def preview_image(instance, *args, **kwargs):
|
||||||
|
"""create preview image on book create"""
|
||||||
|
if not ENABLE_PREVIEW_IMAGES:
|
||||||
|
return
|
||||||
|
changed_fields = {}
|
||||||
|
if instance.field_tracker:
|
||||||
|
changed_fields = instance.field_tracker.changed()
|
||||||
|
|
||||||
|
if len(changed_fields) > 0:
|
||||||
|
generate_edition_preview_image_task.delay(instance.id)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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",)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
424
bookwyrm/preview_images.py
Normal 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)
|
|
@ -42,6 +42,14 @@ LOCALE_PATHS = [
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||||
|
|
||||||
|
# Preview image
|
||||||
|
ENABLE_PREVIEW_IMAGES = env.bool("ENABLE_PREVIEW_IMAGES", False)
|
||||||
|
PREVIEW_BG_COLOR = env.str("PREVIEW_BG_COLOR", "use_dominant_color_light")
|
||||||
|
PREVIEW_TEXT_COLOR = env.str("PREVIEW_TEXT_COLOR", "#363636")
|
||||||
|
PREVIEW_IMG_WIDTH = env.int("PREVIEW_IMG_WIDTH", 1200)
|
||||||
|
PREVIEW_IMG_HEIGHT = env.int("PREVIEW_IMG_HEIGHT", 630)
|
||||||
|
PREVIEW_DEFAULT_COVER_COLOR = env.str("PREVIEW_DEFAULT_COVER_COLOR", "#002549")
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
@ -52,7 +60,6 @@ SECRET_KEY = env("SECRET_KEY")
|
||||||
DEBUG = env.bool("DEBUG", True)
|
DEBUG = env.bool("DEBUG", True)
|
||||||
|
|
||||||
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"])
|
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"])
|
||||||
OL_URL = env("OL_URL")
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
@ -115,10 +122,8 @@ STREAMS = ["home", "local", "federated"]
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||||
|
|
||||||
BOOKWYRM_DATABASE_BACKEND = env("BOOKWYRM_DATABASE_BACKEND", "postgres")
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
BOOKWYRM_DBS = {
|
|
||||||
"postgres": {
|
|
||||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
||||||
"NAME": env("POSTGRES_DB", "fedireads"),
|
"NAME": env("POSTGRES_DB", "fedireads"),
|
||||||
"USER": env("POSTGRES_USER", "fedireads"),
|
"USER": env("POSTGRES_USER", "fedireads"),
|
||||||
|
@ -128,8 +133,6 @@ BOOKWYRM_DBS = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
DATABASES = {"default": BOOKWYRM_DBS[BOOKWYRM_DATABASE_BACKEND]}
|
|
||||||
|
|
||||||
|
|
||||||
LOGIN_URL = "/login/"
|
LOGIN_URL = "/login/"
|
||||||
AUTH_USER_MODEL = "bookwyrm.User"
|
AUTH_USER_MODEL = "bookwyrm.User"
|
||||||
|
@ -137,6 +140,7 @@ AUTH_USER_MODEL = "bookwyrm.User"
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
# pylint: disable=line-too-long
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
|
@ -180,8 +184,10 @@ USE_TZ = True
|
||||||
|
|
||||||
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
|
STATIC_PATH = "%s/%s" % (DOMAIN, env("STATIC_ROOT", "static"))
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
|
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
|
||||||
MEDIA_URL = "/images/"
|
MEDIA_URL = "/images/"
|
||||||
|
MEDIA_PATH = "%s/%s" % (DOMAIN, env("MEDIA_ROOT", "images"))
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
|
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
|
||||||
|
|
||||||
USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % (
|
USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % (
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
94
bookwyrm/static/fonts/public_sans/OFL.txt
Normal file
94
bookwyrm/static/fonts/public_sans/OFL.txt
Normal 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.
|
BIN
bookwyrm/static/fonts/public_sans/PublicSans-Bold.ttf
Normal file
BIN
bookwyrm/static/fonts/public_sans/PublicSans-Bold.ttf
Normal file
Binary file not shown.
BIN
bookwyrm/static/fonts/public_sans/PublicSans-Light.ttf
Normal file
BIN
bookwyrm/static/fonts/public_sans/PublicSans-Light.ttf
Normal file
Binary file not shown.
BIN
bookwyrm/static/fonts/public_sans/PublicSans-Regular.ttf
Normal file
BIN
bookwyrm/static/fonts/public_sans/PublicSans-Regular.ttf
Normal file
Binary file not shown.
BIN
bookwyrm/static/images/icons/star-empty.png
Executable file
BIN
bookwyrm/static/images/icons/star-empty.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
bookwyrm/static/images/icons/star-full.png
Executable file
BIN
bookwyrm/static/images/icons/star-full.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 923 B |
BIN
bookwyrm/static/images/icons/star-half.png
Executable file
BIN
bookwyrm/static/images/icons/star-half.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -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)
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}();
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}();
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -1,35 +1,47 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% load i18n %}{% load bookwyrm_tags %}{% load humanize %}{% load utilities %}
|
{% load i18n %}{% load bookwyrm_tags %}{% load humanize %}{% load utilities %}{% load layout %}
|
||||||
|
|
||||||
{% block title %}{{ book|book_title }}{% endblock %}
|
{% block title %}{{ book|book_title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block opengraph_images %}
|
||||||
|
{% include 'snippets/opengraph_images.html' with image=book.preview_image %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% with user_authenticated=request.user.is_authenticated can_edit_book=perms.bookwyrm.edit_book %}
|
{% with user_authenticated=request.user.is_authenticated can_edit_book=perms.bookwyrm.edit_book %}
|
||||||
<div class="block" itemscope itemtype="https://schema.org/Book">
|
<div class="block" itemscope itemtype="https://schema.org/Book">
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<h1 class="title">
|
<h1 class="title" itemprop="name">
|
||||||
<span itemprop="name">
|
{{ book.title }}
|
||||||
{{ book.title }}{% if book.subtitle %}:
|
|
||||||
<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>
|
||||||
|
|
||||||
|
@ -37,7 +49,7 @@
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<a href="{{ book.id }}/edit">
|
<a href="{{ book.id }}/edit">
|
||||||
<span class="icon icon-pencil" title="{% trans "Edit Book" %}" aria-hidden=True></span>
|
<span class="icon icon-pencil" title="{% trans "Edit Book" %}" aria-hidden=True></span>
|
||||||
<span>{% trans "Edit Book" %}</span>
|
<span class="is-hidden-mobile">{% trans "Edit Book" %}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -85,7 +97,7 @@
|
||||||
|
|
||||||
<div class="column is-three-fifths">
|
<div class="column is-three-fifths">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h3
|
<div
|
||||||
class="field is-grouped"
|
class="field is-grouped"
|
||||||
itemprop="aggregateRating"
|
itemprop="aggregateRating"
|
||||||
itemscope
|
itemscope
|
||||||
|
@ -103,7 +115,7 @@
|
||||||
{% plural %}
|
{% plural %}
|
||||||
({{ review_count }} reviews)
|
({{ review_count }} reviews)
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</h3>
|
</div>
|
||||||
|
|
||||||
{% with full=book|book_description itemprop='abstract' %}
|
{% with full=book|book_description itemprop='abstract' %}
|
||||||
{% include 'snippets/trimmed_text.html' %}
|
{% include 'snippets/trimmed_text.html' %}
|
||||||
|
@ -181,7 +193,7 @@
|
||||||
<p>{% trans "You don't have any reading activity for this book." %}</p>
|
<p>{% trans "You don't have any reading activity for this book." %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for readthrough in readthroughs %}
|
{% for readthrough in readthroughs %}
|
||||||
{% include 'snippets/readthrough.html' with readthrough=readthrough %}
|
{% include 'book/readthrough.html' with readthrough=readthrough %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</section>
|
</section>
|
||||||
<hr aria-hidden="true">
|
<hr aria-hidden="true">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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:" %}
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
{% block content %}{% spaceless %}
|
{% block content %}{% spaceless %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h1 class="title">{% trans "Import Status" %}</h1>
|
<h1 class="title">{% trans "Import Status" %}</h1>
|
||||||
|
<a href="{% url 'import' %}" class="has-text-weight-normal help subtitle is-link">{% trans "Back to imports" %}</a>
|
||||||
|
|
||||||
<dl>
|
<dl>
|
||||||
<div class="is-flex">
|
<div class="is-flex">
|
||||||
|
@ -106,7 +107,11 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
|
{% if job.complete %}
|
||||||
<h2 class="title is-4">{% trans "Successfully imported" %}</h2>
|
<h2 class="title is-4">{% trans "Successfully imported" %}</h2>
|
||||||
|
{% else %}
|
||||||
|
<h2 class="title is-4">{% trans "Import Progress" %}</h2>
|
||||||
|
{% endif %}
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
|
|
|
@ -8,16 +8,21 @@
|
||||||
<link rel="stylesheet" href="/static/css/vendor/icons.css">
|
<link rel="stylesheet" href="/static/css/vendor/icons.css">
|
||||||
<link rel="stylesheet" href="/static/css/bookwyrm.css">
|
<link rel="stylesheet" href="/static/css/bookwyrm.css">
|
||||||
|
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}/images/{{ site.favicon }}{% else %}/static/images/favicon.ico{% endif %}">
|
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{{ media_url }}{{ site.favicon }}{% else %}/static/images/favicon.ico{% endif %}">
|
||||||
|
|
||||||
|
{% if preview_images_enabled is True %}
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
{% else %}
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
|
{% endif %}
|
||||||
<meta name="twitter:title" content="{% if title %}{{ title }} | {% endif %}{{ site.name }}">
|
<meta name="twitter:title" content="{% if title %}{{ title }} | {% endif %}{{ site.name }}">
|
||||||
<meta name="og:title" content="{% if title %}{{ title }} | {% endif %}{{ site.name }}">
|
<meta name="og:title" content="{% if title %}{{ title }} | {% endif %}{{ site.name }}">
|
||||||
<meta name="twitter:description" content="{{ site.instance_tagline }}">
|
<meta name="twitter:description" content="{{ site.instance_tagline }}">
|
||||||
<meta name="og:description" content="{{ site.instance_tagline }}">
|
<meta name="og:description" content="{{ site.instance_tagline }}">
|
||||||
|
|
||||||
<meta name="twitter:image" content="{% if site.logo %}/images/{{ site.logo }}{% else %}/static/images/logo.png{% endif %}">
|
{% block opengraph_images %}
|
||||||
<meta name="og:image" content="{% if site.logo %}/images/{{ site.logo }}{% else %}/static/images/logo.png{% endif %}">
|
{% include 'snippets/opengraph_images.html' %}
|
||||||
|
{% endblock %}
|
||||||
<meta name="twitter:image:alt" content="BookWyrm Logo">
|
<meta name="twitter:image:alt" content="BookWyrm Logo">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -25,7 +30,7 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<a class="navbar-item" href="/">
|
<a class="navbar-item" href="/">
|
||||||
<img class="image logo" src="{% if site.logo_small %}/images/{{ site.logo_small }}{% else %}/static/images/logo-small.png{% endif %}" alt="Home page">
|
<img class="image logo" src="{% if site.logo_small %}{{ media_url }}{{ site.logo_small }}{% else %}/static/images/logo-small.png{% endif %}" alt="Home page">
|
||||||
</a>
|
</a>
|
||||||
<form class="navbar-item column" action="/search/">
|
<form class="navbar-item column" action="/search/">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
30
bookwyrm/templates/preferences/delete_user.html
Normal file
30
bookwyrm/templates/preferences/delete_user.html
Normal 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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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">
|
14
bookwyrm/templates/reading_progress/finish.html
Normal file
14
bookwyrm/templates/reading_progress/finish.html
Normal 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 %}
|
14
bookwyrm/templates/reading_progress/start.html
Normal file
14
bookwyrm/templates/reading_progress/start.html
Normal 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 %}
|
14
bookwyrm/templates/reading_progress/want.html
Normal file
14
bookwyrm/templates/reading_progress/want.html
Normal 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 %}
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
12
bookwyrm/templates/snippets/opengraph_images.html
Normal file
12
bookwyrm/templates/snippets/opengraph_images.html
Normal 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 %}
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
119
bookwyrm/tests/test_preview_images.py
Normal file
119
bookwyrm/tests/test_preview_images.py
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
""" test generating preview images """
|
||||||
|
import pathlib
|
||||||
|
from unittest.mock import patch
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.db.models.fields.files import ImageFieldFile
|
||||||
|
|
||||||
|
from bookwyrm import models, settings
|
||||||
|
|
||||||
|
from bookwyrm.preview_images import (
|
||||||
|
generate_site_preview_image_task,
|
||||||
|
generate_edition_preview_image_task,
|
||||||
|
generate_user_preview_image_task,
|
||||||
|
generate_preview_image,
|
||||||
|
save_and_cleanup,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
# pylint: disable=missing-function-docstring
|
||||||
|
class PreviewImages(TestCase):
|
||||||
|
"""every response to a get request, html or json"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""we need basic test data and mocks"""
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"):
|
||||||
|
avatar_file = pathlib.Path(__file__).parent.joinpath(
|
||||||
|
"../static/images/no_cover.jpg"
|
||||||
|
)
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
"possum@local.com",
|
||||||
|
"possum@possum.possum",
|
||||||
|
"password",
|
||||||
|
local=True,
|
||||||
|
localname="possum",
|
||||||
|
avatar=SimpleUploadedFile(
|
||||||
|
avatar_file,
|
||||||
|
open(avatar_file, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
|
||||||
|
self.work = models.Work.objects.create(title="Test Work")
|
||||||
|
self.edition = models.Edition.objects.create(
|
||||||
|
title="Example Edition",
|
||||||
|
remote_id="https://example.com/book/1",
|
||||||
|
parent_work=self.work,
|
||||||
|
)
|
||||||
|
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"):
|
||||||
|
self.site = models.SiteSettings.objects.create()
|
||||||
|
|
||||||
|
def test_generate_preview_image(self, *args, **kwargs):
|
||||||
|
image_file = pathlib.Path(__file__).parent.joinpath(
|
||||||
|
"../static/images/no_cover.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
texts = {
|
||||||
|
"text_one": "Awesome Possum",
|
||||||
|
"text_three": "@possum@local.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = generate_preview_image(texts=texts, picture=image_file, rating=5)
|
||||||
|
self.assertIsInstance(result, Image.Image)
|
||||||
|
self.assertEqual(
|
||||||
|
result.size, (settings.PREVIEW_IMG_WIDTH, settings.PREVIEW_IMG_HEIGHT)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_store_preview_image(self, *args, **kwargs):
|
||||||
|
image = Image.new("RGB", (200, 200), color="#F00")
|
||||||
|
|
||||||
|
result = save_and_cleanup(image, instance=self.local_user)
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
|
self.local_user.refresh_from_db()
|
||||||
|
self.assertIsInstance(self.local_user.preview_image, ImageFieldFile)
|
||||||
|
self.assertIsNotNone(self.local_user.preview_image)
|
||||||
|
self.assertEqual(self.local_user.preview_image.width, 200)
|
||||||
|
self.assertEqual(self.local_user.preview_image.height, 200)
|
||||||
|
|
||||||
|
def test_site_preview(self, *args, **kwargs):
|
||||||
|
"""generate site preview"""
|
||||||
|
generate_site_preview_image_task()
|
||||||
|
|
||||||
|
self.site.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertIsInstance(self.site.preview_image, ImageFieldFile)
|
||||||
|
self.assertIsNotNone(self.site.preview_image)
|
||||||
|
self.assertEqual(self.site.preview_image.width, settings.PREVIEW_IMG_WIDTH)
|
||||||
|
self.assertEqual(self.site.preview_image.height, settings.PREVIEW_IMG_HEIGHT)
|
||||||
|
|
||||||
|
def test_edition_preview(self, *args, **kwargs):
|
||||||
|
"""generate edition preview"""
|
||||||
|
generate_edition_preview_image_task(self.edition.id)
|
||||||
|
|
||||||
|
self.edition.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertIsInstance(self.edition.preview_image, ImageFieldFile)
|
||||||
|
self.assertIsNotNone(self.edition.preview_image)
|
||||||
|
self.assertEqual(self.edition.preview_image.width, settings.PREVIEW_IMG_WIDTH)
|
||||||
|
self.assertEqual(self.edition.preview_image.height, settings.PREVIEW_IMG_HEIGHT)
|
||||||
|
|
||||||
|
def test_user_preview(self, *args, **kwargs):
|
||||||
|
"""generate user preview"""
|
||||||
|
generate_user_preview_image_task(self.local_user.id)
|
||||||
|
|
||||||
|
self.local_user.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertIsInstance(self.local_user.preview_image, ImageFieldFile)
|
||||||
|
self.assertIsNotNone(self.local_user.preview_image)
|
||||||
|
self.assertEqual(
|
||||||
|
self.local_user.preview_image.width, settings.PREVIEW_IMG_WIDTH
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.local_user.preview_image.height, settings.PREVIEW_IMG_HEIGHT
|
||||||
|
)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue