mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-02 22:36:33 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
1b987542a4
124 changed files with 6808 additions and 5993 deletions
2
.github/workflows/curlylint.yaml
vendored
2
.github/workflows/curlylint.yaml
vendored
|
@ -24,5 +24,5 @@ jobs:
|
|||
--rule 'meta_viewport: true' \
|
||||
--rule 'no_autofocus: true' \
|
||||
--rule 'tabindex_no_positive: true' \
|
||||
--exclude '_modal.html|create_status/layout.html' \
|
||||
--exclude '_modal.html|create_status/layout.html|reading_modals/layout.html' \
|
||||
bookwyrm/templates
|
||||
|
|
|
@ -54,6 +54,7 @@ class Edition(Book):
|
|||
asin: str = ""
|
||||
pages: int = None
|
||||
physicalFormat: str = ""
|
||||
physicalFormatDetail: str = ""
|
||||
publishers: List[str] = field(default_factory=lambda: [])
|
||||
editionRank: int = 0
|
||||
|
||||
|
|
156
bookwyrm/book_search.py
Normal file
156
bookwyrm/book_search.py
Normal file
|
@ -0,0 +1,156 @@
|
|||
""" using a bookwyrm instance as a source of book data """
|
||||
from dataclasses import asdict, dataclass
|
||||
from functools import reduce
|
||||
import operator
|
||||
|
||||
from django.contrib.postgres.search import SearchRank, SearchQuery
|
||||
from django.db.models import OuterRef, Subquery, F, Q
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.settings import MEDIA_FULL_URL
|
||||
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def search(query, min_confidence=0, filters=None, return_first=False):
|
||||
"""search your local database"""
|
||||
filters = filters or []
|
||||
if not query:
|
||||
return []
|
||||
# first, try searching unqiue identifiers
|
||||
results = search_identifiers(query, *filters, return_first=return_first)
|
||||
if not results:
|
||||
# then try searching title/author
|
||||
results = search_title_author(
|
||||
query, min_confidence, *filters, return_first=return_first
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def isbn_search(query):
|
||||
"""search your local database"""
|
||||
if not query:
|
||||
return []
|
||||
|
||||
filters = [{f: query} for f in ["isbn_10", "isbn_13"]]
|
||||
results = models.Edition.objects.filter(
|
||||
reduce(operator.or_, (Q(**f) for f in filters))
|
||||
).distinct()
|
||||
|
||||
# when there are multiple editions of the same work, pick the default.
|
||||
# it would be odd for this to happen.
|
||||
|
||||
default_editions = models.Edition.objects.filter(
|
||||
parent_work=OuterRef("parent_work")
|
||||
).order_by("-edition_rank")
|
||||
results = (
|
||||
results.annotate(default_id=Subquery(default_editions.values("id")[:1])).filter(
|
||||
default_id=F("id")
|
||||
)
|
||||
or results
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def format_search_result(search_result):
|
||||
"""convert a book object into a search result object"""
|
||||
cover = None
|
||||
if search_result.cover:
|
||||
cover = f"{MEDIA_FULL_URL}{search_result.cover}"
|
||||
|
||||
return SearchResult(
|
||||
title=search_result.title,
|
||||
key=search_result.remote_id,
|
||||
author=search_result.author_text,
|
||||
year=search_result.published_date.year
|
||||
if search_result.published_date
|
||||
else None,
|
||||
cover=cover,
|
||||
confidence=search_result.rank if hasattr(search_result, "rank") else 1,
|
||||
connector="",
|
||||
).json()
|
||||
|
||||
|
||||
def search_identifiers(query, *filters, return_first=False):
|
||||
"""tries remote_id, isbn; defined as dedupe fields on the model"""
|
||||
# pylint: disable=W0212
|
||||
or_filters = [
|
||||
{f.name: query}
|
||||
for f in models.Edition._meta.get_fields()
|
||||
if hasattr(f, "deduplication_field") and f.deduplication_field
|
||||
]
|
||||
results = models.Edition.objects.filter(
|
||||
*filters, reduce(operator.or_, (Q(**f) for f in or_filters))
|
||||
).distinct()
|
||||
if results.count() <= 1:
|
||||
return results
|
||||
|
||||
# when there are multiple editions of the same work, pick the default.
|
||||
# it would be odd for this to happen.
|
||||
default_editions = models.Edition.objects.filter(
|
||||
parent_work=OuterRef("parent_work")
|
||||
).order_by("-edition_rank")
|
||||
results = (
|
||||
results.annotate(default_id=Subquery(default_editions.values("id")[:1])).filter(
|
||||
default_id=F("id")
|
||||
)
|
||||
or results
|
||||
)
|
||||
if return_first:
|
||||
return results.first()
|
||||
return results
|
||||
|
||||
|
||||
def search_title_author(query, min_confidence, *filters, return_first=False):
|
||||
"""searches for title and author"""
|
||||
query = SearchQuery(query, config="simple") | SearchQuery(query, config="english")
|
||||
results = (
|
||||
models.Edition.objects.filter(*filters, search_vector=query)
|
||||
.annotate(rank=SearchRank(F("search_vector"), query))
|
||||
.filter(rank__gt=min_confidence)
|
||||
.order_by("-rank")
|
||||
)
|
||||
|
||||
# when there are multiple editions of the same work, pick the closest
|
||||
editions_of_work = results.values("parent_work__id").values_list("parent_work__id")
|
||||
|
||||
# filter out multiple editions of the same work
|
||||
list_results = []
|
||||
for work_id in set(editions_of_work):
|
||||
editions = results.filter(parent_work=work_id)
|
||||
default = editions.order_by("-edition_rank").first()
|
||||
default_rank = default.rank if default else 0
|
||||
# if mutliple books have the top rank, pick the default edition
|
||||
if default_rank == editions.first().rank:
|
||||
result = default
|
||||
else:
|
||||
result = editions.first()
|
||||
if return_first:
|
||||
return result
|
||||
list_results.append(result)
|
||||
return list_results
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchResult:
|
||||
"""standardized search result object"""
|
||||
|
||||
title: str
|
||||
key: str
|
||||
connector: object
|
||||
view_link: str = None
|
||||
author: str = None
|
||||
year: str = None
|
||||
cover: str = None
|
||||
confidence: int = 1
|
||||
|
||||
def __repr__(self):
|
||||
# pylint: disable=consider-using-f-string
|
||||
return "<SearchResult key={!r} title={!r} author={!r}>".format(
|
||||
self.key, self.title, self.author
|
||||
)
|
||||
|
||||
def json(self):
|
||||
"""serialize a connector for json response"""
|
||||
serialized = asdict(self)
|
||||
del serialized["connector"]
|
||||
return serialized
|
|
@ -3,4 +3,4 @@ from .settings import CONNECTORS
|
|||
from .abstract_connector import ConnectorException
|
||||
from .abstract_connector import get_data, get_image
|
||||
|
||||
from .connector_manager import search, local_search, first_search_result
|
||||
from .connector_manager import search, first_search_result
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
""" functionality outline for a book data connector """
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import asdict, dataclass
|
||||
import logging
|
||||
|
||||
from django.db import transaction
|
||||
|
@ -9,6 +8,7 @@ from requests.exceptions import RequestException
|
|||
|
||||
from bookwyrm import activitypub, models, settings
|
||||
from .connector_manager import load_more_data, ConnectorException
|
||||
from .format_mappings import format_mappings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -31,7 +31,6 @@ class AbstractMinimalConnector(ABC):
|
|||
"isbn_search_url",
|
||||
"name",
|
||||
"identifier",
|
||||
"local",
|
||||
]
|
||||
for field in self_fields:
|
||||
setattr(self, field, getattr(info, field))
|
||||
|
@ -267,32 +266,6 @@ def get_image(url, timeout=10):
|
|||
return resp
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchResult:
|
||||
"""standardized search result object"""
|
||||
|
||||
title: str
|
||||
key: str
|
||||
connector: object
|
||||
view_link: str = None
|
||||
author: str = None
|
||||
year: str = None
|
||||
cover: str = None
|
||||
confidence: int = 1
|
||||
|
||||
def __repr__(self):
|
||||
# pylint: disable=consider-using-f-string
|
||||
return "<SearchResult key={!r} title={!r} author={!r}>".format(
|
||||
self.key, self.title, self.author
|
||||
)
|
||||
|
||||
def json(self):
|
||||
"""serialize a connector for json response"""
|
||||
serialized = asdict(self)
|
||||
del serialized["connector"]
|
||||
return serialized
|
||||
|
||||
|
||||
class Mapping:
|
||||
"""associate a local database field with a field in an external dataset"""
|
||||
|
||||
|
@ -312,3 +285,25 @@ class Mapping:
|
|||
return self.formatter(value)
|
||||
except: # pylint: disable=bare-except
|
||||
return None
|
||||
|
||||
|
||||
def infer_physical_format(format_text):
|
||||
"""try to figure out what the standardized format is from the free value"""
|
||||
format_text = format_text.lower()
|
||||
if format_text in format_mappings:
|
||||
# try a direct match
|
||||
return format_mappings[format_text]
|
||||
# failing that, try substring
|
||||
matches = [v for k, v in format_mappings.items() if k in format_text]
|
||||
if not matches:
|
||||
return None
|
||||
return matches[0]
|
||||
|
||||
|
||||
def unique_physical_format(format_text):
|
||||
"""only store the format if it isn't diretly in the format mappings"""
|
||||
format_text = format_text.lower()
|
||||
if format_text in format_mappings:
|
||||
# try a direct match, so saving this would be redundant
|
||||
return None
|
||||
return format_text
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
""" using another bookwyrm instance as a source of book data """
|
||||
from bookwyrm import activitypub, models
|
||||
from .abstract_connector import AbstractMinimalConnector, SearchResult
|
||||
from bookwyrm.book_search import SearchResult
|
||||
from .abstract_connector import AbstractMinimalConnector
|
||||
|
||||
|
||||
class Connector(AbstractMinimalConnector):
|
||||
|
|
|
@ -10,7 +10,7 @@ from django.db.models import signals
|
|||
|
||||
from requests import HTTPError
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm import book_search, models
|
||||
from bookwyrm.tasks import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -55,7 +55,7 @@ def search(query, min_confidence=0.1, return_first=False):
|
|||
# if we found anything, return it
|
||||
return result_set[0]
|
||||
|
||||
if result_set or connector.local:
|
||||
if result_set:
|
||||
results.append(
|
||||
{
|
||||
"connector": connector,
|
||||
|
@ -71,22 +71,13 @@ def search(query, min_confidence=0.1, return_first=False):
|
|||
return results
|
||||
|
||||
|
||||
def local_search(query, min_confidence=0.1, raw=False, filters=None):
|
||||
"""only look at local search results"""
|
||||
connector = load_connector(models.Connector.objects.get(local=True))
|
||||
return connector.search(
|
||||
query, min_confidence=min_confidence, raw=raw, filters=filters
|
||||
)
|
||||
|
||||
|
||||
def isbn_local_search(query, raw=False):
|
||||
"""only look at local search results"""
|
||||
connector = load_connector(models.Connector.objects.get(local=True))
|
||||
return connector.isbn_search(query, raw=raw)
|
||||
|
||||
|
||||
def first_search_result(query, min_confidence=0.1):
|
||||
"""search until you find a result that fits"""
|
||||
# try local search first
|
||||
result = book_search.search(query, min_confidence=min_confidence, return_first=True)
|
||||
if result:
|
||||
return result
|
||||
# otherwise, try remote endpoints
|
||||
return search(query, min_confidence=min_confidence, return_first=True) or None
|
||||
|
||||
|
||||
|
|
43
bookwyrm/connectors/format_mappings.py
Normal file
43
bookwyrm/connectors/format_mappings.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
""" comparing a free text format to the standardized one """
|
||||
format_mappings = {
|
||||
"paperback": "Paperback",
|
||||
"soft": "Paperback",
|
||||
"pamphlet": "Paperback",
|
||||
"peperback": "Paperback",
|
||||
"tapa blanda": "Paperback",
|
||||
"turtleback": "Paperback",
|
||||
"pocket": "Paperback",
|
||||
"spiral": "Paperback",
|
||||
"ring": "Paperback",
|
||||
"平装": "Paperback",
|
||||
"简装": "Paperback",
|
||||
"hardcover": "Hardcover",
|
||||
"hardcocer": "Hardcover",
|
||||
"hardover": "Hardcover",
|
||||
"hardback": "Hardcover",
|
||||
"library": "Hardcover",
|
||||
"tapa dura": "Hardcover",
|
||||
"leather": "Hardcover",
|
||||
"clothbound": "Hardcover",
|
||||
"精装": "Hardcover",
|
||||
"ebook": "EBook",
|
||||
"e-book": "EBook",
|
||||
"digital": "EBook",
|
||||
"computer file": "EBook",
|
||||
"epub": "EBook",
|
||||
"online": "EBook",
|
||||
"pdf": "EBook",
|
||||
"elektronische": "EBook",
|
||||
"electronic": "EBook",
|
||||
"audiobook": "AudiobookFormat",
|
||||
"audio": "AudiobookFormat",
|
||||
"cd": "AudiobookFormat",
|
||||
"dvd": "AudiobookFormat",
|
||||
"mp3": "AudiobookFormat",
|
||||
"cassette": "AudiobookFormat",
|
||||
"kindle": "AudiobookFormat",
|
||||
"talking": "AudiobookFormat",
|
||||
"sound": "AudiobookFormat",
|
||||
"comic": "GraphicNovel",
|
||||
"graphic": "GraphicNovel",
|
||||
}
|
|
@ -2,13 +2,14 @@
|
|||
import re
|
||||
|
||||
from bookwyrm import models
|
||||
from .abstract_connector import AbstractConnector, SearchResult, Mapping
|
||||
from bookwyrm.book_search import SearchResult
|
||||
from .abstract_connector import AbstractConnector, Mapping
|
||||
from .abstract_connector import get_data
|
||||
from .connector_manager import ConnectorException
|
||||
|
||||
|
||||
class Connector(AbstractConnector):
|
||||
"""instantiate a connector for OL"""
|
||||
"""instantiate a connector for inventaire"""
|
||||
|
||||
def __init__(self, identifier):
|
||||
super().__init__(identifier)
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
import re
|
||||
|
||||
from bookwyrm import models
|
||||
from .abstract_connector import AbstractConnector, SearchResult, Mapping
|
||||
from .abstract_connector import get_data
|
||||
from bookwyrm.book_search import SearchResult
|
||||
from .abstract_connector import AbstractConnector, Mapping
|
||||
from .abstract_connector import get_data, infer_physical_format, unique_physical_format
|
||||
from .connector_manager import ConnectorException
|
||||
from .openlibrary_languages import languages
|
||||
|
||||
|
@ -43,7 +44,16 @@ class Connector(AbstractConnector):
|
|||
),
|
||||
Mapping("publishedDate", remote_field="publish_date"),
|
||||
Mapping("pages", remote_field="number_of_pages"),
|
||||
Mapping("physicalFormat", remote_field="physical_format"),
|
||||
Mapping(
|
||||
"physicalFormat",
|
||||
remote_field="physical_format",
|
||||
formatter=infer_physical_format,
|
||||
),
|
||||
Mapping(
|
||||
"physicalFormatDetail",
|
||||
remote_field="physical_format",
|
||||
formatter=unique_physical_format,
|
||||
),
|
||||
Mapping("publishers"),
|
||||
]
|
||||
|
||||
|
|
|
@ -1,164 +0,0 @@
|
|||
""" using a bookwyrm instance as a source of book data """
|
||||
from functools import reduce
|
||||
import operator
|
||||
|
||||
from django.contrib.postgres.search import SearchRank, SearchQuery
|
||||
from django.db.models import OuterRef, Subquery, F, Q
|
||||
|
||||
from bookwyrm import models
|
||||
from .abstract_connector import AbstractConnector, SearchResult
|
||||
|
||||
|
||||
class Connector(AbstractConnector):
|
||||
"""instantiate a connector"""
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def search(self, query, min_confidence=0, raw=False, filters=None):
|
||||
"""search your local database"""
|
||||
filters = filters or []
|
||||
if not query:
|
||||
return []
|
||||
# first, try searching unqiue identifiers
|
||||
results = search_identifiers(query, *filters)
|
||||
if not results:
|
||||
# then try searching title/author
|
||||
results = search_title_author(query, min_confidence, *filters)
|
||||
search_results = []
|
||||
for result in results:
|
||||
if raw:
|
||||
search_results.append(result)
|
||||
else:
|
||||
search_results.append(self.format_search_result(result))
|
||||
if len(search_results) >= 10:
|
||||
break
|
||||
if not raw:
|
||||
search_results.sort(key=lambda r: r.confidence, reverse=True)
|
||||
return search_results
|
||||
|
||||
def isbn_search(self, query, raw=False):
|
||||
"""search your local database"""
|
||||
if not query:
|
||||
return []
|
||||
|
||||
filters = [{f: query} for f in ["isbn_10", "isbn_13"]]
|
||||
results = models.Edition.objects.filter(
|
||||
reduce(operator.or_, (Q(**f) for f in filters))
|
||||
).distinct()
|
||||
|
||||
# when there are multiple editions of the same work, pick the default.
|
||||
# it would be odd for this to happen.
|
||||
|
||||
default_editions = models.Edition.objects.filter(
|
||||
parent_work=OuterRef("parent_work")
|
||||
).order_by("-edition_rank")
|
||||
results = (
|
||||
results.annotate(
|
||||
default_id=Subquery(default_editions.values("id")[:1])
|
||||
).filter(default_id=F("id"))
|
||||
or results
|
||||
)
|
||||
|
||||
search_results = []
|
||||
for result in results:
|
||||
if raw:
|
||||
search_results.append(result)
|
||||
else:
|
||||
search_results.append(self.format_search_result(result))
|
||||
if len(search_results) >= 10:
|
||||
break
|
||||
return search_results
|
||||
|
||||
def format_search_result(self, search_result):
|
||||
cover = None
|
||||
if search_result.cover:
|
||||
cover = f"{self.covers_url}{search_result.cover}"
|
||||
|
||||
return SearchResult(
|
||||
title=search_result.title,
|
||||
key=search_result.remote_id,
|
||||
author=search_result.author_text,
|
||||
year=search_result.published_date.year
|
||||
if search_result.published_date
|
||||
else None,
|
||||
connector=self,
|
||||
cover=cover,
|
||||
confidence=search_result.rank if hasattr(search_result, "rank") else 1,
|
||||
)
|
||||
|
||||
def format_isbn_search_result(self, search_result):
|
||||
return self.format_search_result(search_result)
|
||||
|
||||
def is_work_data(self, data):
|
||||
pass
|
||||
|
||||
def get_edition_from_work_data(self, data):
|
||||
pass
|
||||
|
||||
def get_work_from_edition_data(self, data):
|
||||
pass
|
||||
|
||||
def get_authors_from_data(self, data):
|
||||
return None
|
||||
|
||||
def parse_isbn_search_data(self, data):
|
||||
"""it's already in the right format, don't even worry about it"""
|
||||
return data
|
||||
|
||||
def parse_search_data(self, data):
|
||||
"""it's already in the right format, don't even worry about it"""
|
||||
return data
|
||||
|
||||
def expand_book_data(self, book):
|
||||
pass
|
||||
|
||||
|
||||
def search_identifiers(query, *filters):
|
||||
"""tries remote_id, isbn; defined as dedupe fields on the model"""
|
||||
# pylint: disable=W0212
|
||||
or_filters = [
|
||||
{f.name: query}
|
||||
for f in models.Edition._meta.get_fields()
|
||||
if hasattr(f, "deduplication_field") and f.deduplication_field
|
||||
]
|
||||
results = models.Edition.objects.filter(
|
||||
*filters, reduce(operator.or_, (Q(**f) for f in or_filters))
|
||||
).distinct()
|
||||
if results.count() <= 1:
|
||||
return results
|
||||
|
||||
# when there are multiple editions of the same work, pick the default.
|
||||
# it would be odd for this to happen.
|
||||
default_editions = models.Edition.objects.filter(
|
||||
parent_work=OuterRef("parent_work")
|
||||
).order_by("-edition_rank")
|
||||
return (
|
||||
results.annotate(default_id=Subquery(default_editions.values("id")[:1])).filter(
|
||||
default_id=F("id")
|
||||
)
|
||||
or results
|
||||
)
|
||||
|
||||
|
||||
def search_title_author(query, min_confidence, *filters):
|
||||
"""searches for title and author"""
|
||||
query = SearchQuery(query, config="simple") | SearchQuery(query, config="english")
|
||||
results = (
|
||||
models.Edition.objects.filter(*filters, search_vector=query)
|
||||
.annotate(rank=SearchRank(F("search_vector"), query))
|
||||
.filter(rank__gt=min_confidence)
|
||||
.order_by("-rank")
|
||||
)
|
||||
|
||||
# when there are multiple editions of the same work, pick the closest
|
||||
editions_of_work = results.values("parent_work__id").values_list("parent_work__id")
|
||||
|
||||
# filter out multiple editions of the same work
|
||||
for work_id in set(editions_of_work):
|
||||
editions = results.filter(parent_work=work_id)
|
||||
default = editions.order_by("-edition_rank").first()
|
||||
default_rank = default.rank if default else 0
|
||||
# if mutliple books have the top rank, pick the default edition
|
||||
if default_rank == editions.first().rank:
|
||||
yield default
|
||||
else:
|
||||
yield editions.first()
|
|
@ -1,3 +1,3 @@
|
|||
""" settings book data connectors """
|
||||
|
||||
CONNECTORS = ["openlibrary", "inventaire", "self_connector", "bookwyrm_connector"]
|
||||
CONNECTORS = ["openlibrary", "inventaire", "bookwyrm_connector"]
|
||||
|
|
|
@ -4,7 +4,6 @@ from django.contrib.auth.models import Group, Permission
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from bookwyrm.models import Connector, FederatedServer, SiteSettings, User
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
|
||||
def init_groups():
|
||||
|
@ -73,19 +72,6 @@ def init_permissions():
|
|||
|
||||
def init_connectors():
|
||||
"""access book data sources"""
|
||||
Connector.objects.create(
|
||||
identifier=DOMAIN,
|
||||
name="Local",
|
||||
local=True,
|
||||
connector_file="self_connector",
|
||||
base_url="https://%s" % DOMAIN,
|
||||
books_url="https://%s/book" % DOMAIN,
|
||||
covers_url="https://%s/images/" % DOMAIN,
|
||||
search_url="https://%s/search?q=" % DOMAIN,
|
||||
isbn_search_url="https://%s/isbn/" % DOMAIN,
|
||||
priority=1,
|
||||
)
|
||||
|
||||
Connector.objects.create(
|
||||
identifier="bookwyrm.social",
|
||||
name="BookWyrm dot Social",
|
||||
|
|
56
bookwyrm/migrations/0101_auto_20210929_1847.py
Normal file
56
bookwyrm/migrations/0101_auto_20210929_1847.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
# Generated by Django 3.2 on 2021-05-21 00:17
|
||||
|
||||
from django.db import migrations
|
||||
import bookwyrm
|
||||
from bookwyrm.connectors.abstract_connector import infer_physical_format
|
||||
|
||||
|
||||
def infer_format(app_registry, schema_editor):
|
||||
"""set the new phsyical format field based on existing format data"""
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
editions = (
|
||||
app_registry.get_model("bookwyrm", "Edition")
|
||||
.objects.using(db_alias)
|
||||
.filter(physical_format_detail__isnull=False)
|
||||
)
|
||||
for edition in editions:
|
||||
free_format = edition.physical_format_detail.lower()
|
||||
edition.physical_format = infer_physical_format(free_format)
|
||||
edition.save()
|
||||
|
||||
|
||||
def reverse(app_registry, schema_editor):
|
||||
"""doesn't need to do anything"""
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0100_shelf_description"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="edition",
|
||||
old_name="physical_format",
|
||||
new_name="physical_format_detail",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="edition",
|
||||
name="physical_format",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("AudiobookFormat", "Audiobook"),
|
||||
("EBook", "eBook"),
|
||||
("GraphicNovel", "Graphic novel"),
|
||||
("Hardcover", "Hardcover"),
|
||||
("Paperback", "Paperback"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(infer_format, reverse),
|
||||
]
|
41
bookwyrm/migrations/0102_remove_connector_local.py
Normal file
41
bookwyrm/migrations/0102_remove_connector_local.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
# Generated by Django 3.2.5 on 2021-09-30 17:46
|
||||
|
||||
from django.db import migrations
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
|
||||
def remove_self_connector(app_registry, schema_editor):
|
||||
"""set the new phsyical format field based on existing format data"""
|
||||
db_alias = schema_editor.connection.alias
|
||||
app_registry.get_model("bookwyrm", "Connector").objects.using(db_alias).filter(
|
||||
connector_file="self_connector"
|
||||
).delete()
|
||||
|
||||
|
||||
def reverse(app_registry, schema_editor):
|
||||
"""doesn't need to do anything"""
|
||||
db_alias = schema_editor.connection.alias
|
||||
model = app_registry.get_model("bookwyrm", "Connector")
|
||||
model.objects.using(db_alias).create(
|
||||
identifier=DOMAIN,
|
||||
name="Local",
|
||||
local=True,
|
||||
connector_file="self_connector",
|
||||
base_url=f"https://{DOMAIN}",
|
||||
books_url=f"https://{DOMAIN}/book",
|
||||
covers_url=f"https://{DOMAIN}/images/",
|
||||
search_url=f"https://{DOMAIN}/search?q=",
|
||||
isbn_search_url=f"https://{DOMAIN}/isbn/",
|
||||
priority=1,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0101_auto_20210929_1847"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(remove_self_connector, reverse),
|
||||
]
|
17
bookwyrm/migrations/0103_remove_connector_local.py
Normal file
17
bookwyrm/migrations/0103_remove_connector_local.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.2.5 on 2021-09-30 18:03
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0102_remove_connector_local"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="connector",
|
||||
name="local",
|
||||
),
|
||||
]
|
53
bookwyrm/migrations/0104_auto_20211001_2012.py
Normal file
53
bookwyrm/migrations/0104_auto_20211001_2012.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Generated by Django 3.2.5 on 2021-10-01 20:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_thread_id(app_registry, schema_editor):
|
||||
"""set thread ids"""
|
||||
db_alias = schema_editor.connection.alias
|
||||
# set the thread id on parent nodes
|
||||
model = app_registry.get_model("bookwyrm", "Status")
|
||||
model.objects.using(db_alias).filter(reply_parent__isnull=True).update(
|
||||
thread_id=models.F("id")
|
||||
)
|
||||
|
||||
queryset = model.objects.using(db_alias).filter(
|
||||
reply_parent__isnull=False,
|
||||
reply_parent__thread_id__isnull=False,
|
||||
thread_id__isnull=True,
|
||||
)
|
||||
iters = 0
|
||||
while queryset.exists():
|
||||
queryset.update(
|
||||
thread_id=models.Subquery(
|
||||
model.objects.filter(id=models.OuterRef("reply_parent")).values_list(
|
||||
"thread_id"
|
||||
)[:1]
|
||||
)
|
||||
)
|
||||
print(iters)
|
||||
iters += 1
|
||||
if iters > 50:
|
||||
print("exceeded query depth")
|
||||
break
|
||||
|
||||
|
||||
def reverse(*_):
|
||||
"""do nothing"""
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0103_remove_connector_local"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="status",
|
||||
name="thread_id",
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.RunPython(set_thread_id, reverse),
|
||||
]
|
|
@ -6,6 +6,7 @@ from django.contrib.postgres.indexes import GinIndex
|
|||
from django.db import models, transaction
|
||||
from django.db.models import Prefetch
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from model_utils import FieldTracker
|
||||
from model_utils.managers import InheritanceManager
|
||||
from imagekit.models import ImageSpecField
|
||||
|
@ -226,6 +227,16 @@ class Work(OrderedCollectionPageMixin, Book):
|
|||
deserialize_reverse_fields = [("editions", "editions")]
|
||||
|
||||
|
||||
# https://schema.org/BookFormatType
|
||||
FormatChoices = [
|
||||
("AudiobookFormat", _("Audiobook")),
|
||||
("EBook", _("eBook")),
|
||||
("GraphicNovel", _("Graphic novel")),
|
||||
("Hardcover", _("Hardcover")),
|
||||
("Paperback", _("Paperback")),
|
||||
]
|
||||
|
||||
|
||||
class Edition(Book):
|
||||
"""an edition of a book"""
|
||||
|
||||
|
@ -243,7 +254,10 @@ class Edition(Book):
|
|||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
pages = fields.IntegerField(blank=True, null=True)
|
||||
physical_format = fields.CharField(max_length=255, blank=True, null=True)
|
||||
physical_format = fields.CharField(
|
||||
max_length=255, choices=FormatChoices, null=True, blank=True
|
||||
)
|
||||
physical_format_detail = fields.CharField(max_length=255, blank=True, null=True)
|
||||
publishers = fields.ArrayField(
|
||||
models.CharField(max_length=255), blank=True, default=list
|
||||
)
|
||||
|
|
|
@ -14,7 +14,6 @@ class Connector(BookWyrmModel):
|
|||
identifier = models.CharField(max_length=255, unique=True)
|
||||
priority = models.IntegerField(default=2)
|
||||
name = models.CharField(max_length=255, null=True, blank=True)
|
||||
local = models.BooleanField(default=False)
|
||||
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)
|
||||
api_key = models.CharField(max_length=255, null=True, blank=True)
|
||||
active = models.BooleanField(default=True)
|
||||
|
|
|
@ -57,6 +57,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
|||
on_delete=models.PROTECT,
|
||||
activitypub_field="inReplyTo",
|
||||
)
|
||||
thread_id = models.IntegerField(blank=True, null=True)
|
||||
objects = InheritanceManager()
|
||||
|
||||
activity_serializer = activitypub.Note
|
||||
|
@ -68,6 +69,17 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
|||
|
||||
ordering = ("-published_date",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""save and notify"""
|
||||
if self.reply_parent:
|
||||
self.thread_id = self.reply_parent.thread_id or self.reply_parent.id
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if not self.reply_parent:
|
||||
self.thread_id = self.id
|
||||
super().save(broadcast=False, update_fields=["thread_id"])
|
||||
|
||||
def delete(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
""" "delete" a status"""
|
||||
if hasattr(self, "boosted_status"):
|
||||
|
|
|
@ -13,7 +13,7 @@ VERSION = "0.0.1"
|
|||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||
|
||||
JS_CACHE = "e2bc0653"
|
||||
JS_CACHE = "c02929b1"
|
||||
|
||||
# email
|
||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||
|
|
|
@ -492,6 +492,23 @@ ol.ordered-list li::before {
|
|||
}
|
||||
}
|
||||
|
||||
/* Threads
|
||||
******************************************************************************/
|
||||
|
||||
.thread .is-main .card {
|
||||
box-shadow: 0 0.5em 1em -0.125em rgb(50 115 220 / 35%), 0 0 0 1px rgb(50 115 220 / 2%);
|
||||
}
|
||||
|
||||
.thread::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 2.5em;
|
||||
border-left: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* Dimensions
|
||||
* @todo These could be in rem.
|
||||
******************************************************************************/
|
||||
|
|
21
bookwyrm/static/js/block_href.js
Normal file
21
bookwyrm/static/js/block_href.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/* exported BlockHref */
|
||||
|
||||
let BlockHref = new class {
|
||||
constructor() {
|
||||
document.querySelectorAll('[data-href]')
|
||||
.forEach(t => t.addEventListener('click', this.followLink.bind(this)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow a fake link
|
||||
*
|
||||
* @param {Event} event
|
||||
* @return {undefined}
|
||||
*/
|
||||
followLink(event) {
|
||||
const url = event.currentTarget.dataset.href;
|
||||
|
||||
window.location.href = url;
|
||||
}
|
||||
}();
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block columns" itemscope itemtype="https://schema.org/Person">
|
||||
<div class="block columns content" itemscope itemtype="https://schema.org/Person">
|
||||
<meta itemprop="name" content="{{ author.name }}">
|
||||
|
||||
{% if author.aliases or author.born or author.died or author.wikipedia_link or author.openlibrary_key or author.inventaire_id %}
|
||||
|
|
|
@ -203,7 +203,9 @@
|
|||
<hr aria-hidden="true">
|
||||
|
||||
<section class="box">
|
||||
{% with 0|uuid as controls_uid %}
|
||||
{% include 'snippets/create_status.html' with book=book hide_cover=True %}
|
||||
{% endwith %}
|
||||
</section>
|
||||
{% endif %}
|
||||
<div class="block" id="reviews">
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{% spaceless %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if book.isbn13 or book.oclc_number or book.asin %}
|
||||
<dl>
|
||||
{% if book.isbn_13 %}
|
||||
<div class="is-flex">
|
||||
|
@ -23,4 +24,5 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% endif %}
|
||||
{% endspaceless %}
|
||||
|
|
116
bookwyrm/templates/book/edit/edit_book.html
Normal file
116
bookwyrm/templates/book/edit/edit_book.html
Normal file
|
@ -0,0 +1,116 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block title %}{% if book %}{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}{% else %}{% trans "Add Book" %}{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header class="block">
|
||||
<h1 class="title level-left">
|
||||
{% if book %}
|
||||
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "Add Book" %}
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if book %}
|
||||
<dl>
|
||||
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Added:" %}</dt>
|
||||
<dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
|
||||
|
||||
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Updated:" %}</dt>
|
||||
<dd class="ml-2">{{ book.updated_date | naturaltime }}</dd>
|
||||
|
||||
{% if book.last_edited_by %}
|
||||
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Last edited by:" %}</dt>
|
||||
<dd class="ml-2"><a href="{{ book.last_edited_by.remote_id }}">{{ book.last_edited_by.display_name }}</a></dd>
|
||||
{% endif %}
|
||||
|
||||
</dl>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
<form
|
||||
class="block"
|
||||
{% if book %}
|
||||
name="edit-book"
|
||||
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
|
||||
{% else %}
|
||||
name="create-book"
|
||||
action="/create-book{% if confirm_mode %}/confirm{% endif %}"
|
||||
{% endif %}
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
{% if confirm_mode %}
|
||||
<div class="box">
|
||||
<h2 class="title is-4">{% trans "Confirm Book Info" %}</h2>
|
||||
<div class="columns mb-4">
|
||||
{% if author_matches %}
|
||||
<input type="hidden" name="author-match-count" value="{{ author_matches|length }}">
|
||||
<div class="column is-half">
|
||||
{% for author in author_matches %}
|
||||
<fieldset>
|
||||
<legend class="title is-5 mb-1">
|
||||
{% blocktrans with name=author.name %}Is "{{ name }}" an existing author?{% endblocktrans %}
|
||||
</legend>
|
||||
{% with forloop.counter0 as counter %}
|
||||
{% for match in author.matches %}
|
||||
<label class="label mb-2">
|
||||
<input type="radio" name="author_match-{{ counter }}" value="{{ match.id }}" required>
|
||||
{{ match.name }}
|
||||
</label>
|
||||
<p class="help">
|
||||
<a href="{{ match.local_path }}" target="_blank">{% blocktrans with book_title=match.book_set.first.title %}Author of <em>{{ book_title }}</em>{% endblocktrans %}</a>
|
||||
</p>
|
||||
{% endfor %}
|
||||
<label class="label">
|
||||
<input type="radio" name="author_match-{{ counter }}" value="{{ author.name }}" required> {% trans "This is a new author" %}
|
||||
</label>
|
||||
{% endwith %}
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="column is-half">{% blocktrans with name=add_author %}Creating a new author: {{ name }}{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if not book %}
|
||||
<div class="column is-half">
|
||||
<fieldset>
|
||||
<legend class="title is-5 mb-1">
|
||||
{% trans "Is this an edition of an existing work?" %}
|
||||
</legend>
|
||||
{% for match in book_matches %}
|
||||
<label class="label">
|
||||
<input type="radio" name="parent_work" value="{{ match.parent_work.id }}"> {{ match.parent_work.title }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
<label>
|
||||
<input type="radio" name="parent_work" value="0" required> {% trans "This is a new work" %}
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
|
||||
<a href="#" class="button" data-back>
|
||||
<span>{% trans "Back" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<hr class="block">
|
||||
{% endif %}
|
||||
|
||||
{% include "book/edit/edit_book_form.html" %}
|
||||
|
||||
{% if not confirm_mode %}
|
||||
<div class="block">
|
||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||
<a class="button" href="{{ book.local_path }}">{% trans "Cancel" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -1,40 +1,4 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block title %}{% if book %}{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}{% else %}{% trans "Add Book" %}{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header class="block">
|
||||
<h1 class="title level-left">
|
||||
{% if book %}
|
||||
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "Add Book" %}
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if book %}
|
||||
<dl>
|
||||
<div class="is-flex">
|
||||
<dt class="has-text-weight-semibold">{% trans "Added:" %}</dt>
|
||||
<dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
|
||||
</div>
|
||||
|
||||
<div class="is-flex">
|
||||
<dt class="has-text-weight-semibold">{% trans "Updated:" %}</dt>
|
||||
<dd class="ml-2">{{ book.updated_date | naturaltime }}</dd>
|
||||
</div>
|
||||
|
||||
{% if book.last_edited_by %}
|
||||
<div class="is-flex">
|
||||
<dt class="has-text-weight-semibold">{% trans "Last edited by:" %}</dt>
|
||||
<dd class="ml-2"><a href="{{ book.last_edited_by.remote_id }}">{{ book.last_edited_by.display_name }}</a></dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</dl>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="block">
|
||||
|
@ -42,87 +6,14 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form
|
||||
class="block"
|
||||
{% if book %}
|
||||
name="edit-book"
|
||||
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
|
||||
{% else %}
|
||||
name="create-book"
|
||||
action="/create-book{% if confirm_mode %}/confirm{% endif %}"
|
||||
{% endif %}
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
|
||||
{% csrf_token %}
|
||||
{% if confirm_mode %}
|
||||
<div class="box">
|
||||
<h2 class="title is-4">{% trans "Confirm Book Info" %}</h2>
|
||||
<div class="columns mb-4">
|
||||
{% if author_matches %}
|
||||
<input type="hidden" name="author-match-count" value="{{ author_matches|length }}">
|
||||
<div class="column is-half">
|
||||
{% for author in author_matches %}
|
||||
<fieldset>
|
||||
<legend class="title is-5 mb-1">
|
||||
{% blocktrans with name=author.name %}Is "{{ name }}" an existing author?{% endblocktrans %}
|
||||
</legend>
|
||||
{% with forloop.counter0 as counter %}
|
||||
{% for match in author.matches %}
|
||||
<label class="label mb-2">
|
||||
<input type="radio" name="author_match-{{ counter }}" value="{{ match.id }}" required>
|
||||
{{ match.name }}
|
||||
</label>
|
||||
<p class="help">
|
||||
<a href="{{ match.local_path }}" target="_blank">{% blocktrans with book_title=match.book_set.first.title %}Author of <em>{{ book_title }}</em>{% endblocktrans %}</a>
|
||||
</p>
|
||||
{% endfor %}
|
||||
<label class="label">
|
||||
<input type="radio" name="author_match-{{ counter }}" value="{{ author.name }}" required> {% trans "This is a new author" %}
|
||||
</label>
|
||||
{% endwith %}
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="column is-half">{% blocktrans with name=add_author %}Creating a new author: {{ name }}{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if not book %}
|
||||
<div class="column is-half">
|
||||
<fieldset>
|
||||
<legend class="title is-5 mb-1">
|
||||
{% trans "Is this an edition of an existing work?" %}
|
||||
</legend>
|
||||
{% for match in book_matches %}
|
||||
<label class="label">
|
||||
<input type="radio" name="parent_work" value="{{ match.parent_work.id }}"> {{ match.parent_work.title }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
<label>
|
||||
<input type="radio" name="parent_work" value="0" required> {% trans "This is a new work" %}
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
|
||||
<a href="#" class="button" data-back>
|
||||
<span>{% trans "Back" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<hr class="block">
|
||||
{% endif %}
|
||||
|
||||
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Metadata" %}</h2>
|
||||
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="label" for="id_title">{% trans "Title:" %}</label>
|
||||
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title">
|
||||
|
@ -147,6 +38,8 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds">
|
||||
<div class="field">
|
||||
<label class="label" for="id_series">{% trans "Series:" %}</label>
|
||||
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}">
|
||||
|
@ -154,7 +47,8 @@
|
|||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="column is-one-third">
|
||||
<div class="field">
|
||||
<label class="label" for="id_series_number">{% trans "Series number:" %}</label>
|
||||
{{ form.series_number }}
|
||||
|
@ -162,6 +56,8 @@
|
|||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_languages">{% trans "Languages:" %}</label>
|
||||
|
@ -171,7 +67,12 @@
|
|||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Publication" %}</h2>
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
|
||||
{{ form.publishers }}
|
||||
|
@ -196,10 +97,12 @@
|
|||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Authors" %}</h2>
|
||||
<div class="box">
|
||||
{% if book.authors.exists %}
|
||||
<fieldset>
|
||||
{% for author in book.authors.all %}
|
||||
|
@ -220,18 +123,22 @@
|
|||
<input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'John Doe, Jane Smith' %}" value="{{ add_author }}" {% if confirm_mode %}readonly{% endif %}>
|
||||
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="column is-half">
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Cover" %}</h2>
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
{% if book.cover %}
|
||||
<div class="column is-3 is-cover">
|
||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-xl-mobile is-w-auto-tablet' size_mobile='xlarge' size='large' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="column">
|
||||
<div class="block">
|
||||
<div class="field">
|
||||
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
|
||||
{{ form.cover }}
|
||||
|
@ -248,16 +155,33 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="block">
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Physical Properties" %}</h2>
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column is-one-third">
|
||||
<div class="field">
|
||||
<label class="label" for="id_physical_format">{% trans "Format:" %}</label>
|
||||
<div class="select">
|
||||
{{ form.physical_format }}
|
||||
</div>
|
||||
{% for error in form.physical_format.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label" for="id_physical_format_detail">{% trans "Format details:" %}</label>
|
||||
{{ form.physical_format_detail }}
|
||||
{% for error in form.physical_format_detail.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_pages">{% trans "Pages:" %}</label>
|
||||
|
@ -267,9 +191,11 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="block">
|
||||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Book Identifiers" %}</h2>
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="label" for="id_isbn_13">{% trans "ISBN 13:" %}</label>
|
||||
{{ form.isbn_13 }}
|
||||
|
@ -318,15 +244,6 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not confirm_mode %}
|
||||
<div class="block">
|
||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||
<a class="button" href="{{ book.local_path }}">{% trans "Cancel" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -1,7 +0,0 @@
|
|||
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||
|
||||
{% block filter_fields %}
|
||||
{% include 'book/search_filter.html' %}
|
||||
{% include 'book/language_filter.html' %}
|
||||
{% include 'book/format_filter.html' %}
|
||||
{% endblock %}
|
7
bookwyrm/templates/book/editions/edition_filters.html
Normal file
7
bookwyrm/templates/book/editions/edition_filters.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||
|
||||
{% block filter_fields %}
|
||||
{% include 'book/editions/search_filter.html' %}
|
||||
{% include 'book/editions/language_filter.html' %}
|
||||
{% include 'book/editions/format_filter.html' %}
|
||||
{% endblock %}
|
|
@ -8,7 +8,7 @@
|
|||
<h1 class="title">{% blocktrans with work_path=work.local_path work_title=work|book_title %}Editions of <a href="{{ work_path }}">"{{ work_title }}"</a>{% endblocktrans %}</h1>
|
||||
</div>
|
||||
|
||||
{% include 'book/edition_filters.html' %}
|
||||
{% include 'book/editions/edition_filters.html' %}
|
||||
|
||||
<div class="block">
|
||||
{% for book in editions %}
|
|
@ -3,20 +3,20 @@
|
|||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
<p>
|
||||
{% with format=book.physical_format pages=book.pages %}
|
||||
{% if format %}
|
||||
{% comment %}
|
||||
@todo The bookFormat property is limited to a list of values whereas the book edition is free text.
|
||||
@see https://schema.org/bookFormat
|
||||
{% endcomment %}
|
||||
<meta itemprop="bookFormat" content="{{ format }}">
|
||||
{% firstof book.physical_format_detail book.physical_format as format %}
|
||||
{% firstof book.physical_format book.physical_format_detail as format_property %}
|
||||
{% with pages=book.pages %}
|
||||
{% if format or pages %}
|
||||
|
||||
{% if format_property %}
|
||||
<meta itemprop="bookFormat" content="{{ format_property }}">
|
||||
{% endif %}
|
||||
|
||||
{% if pages %}
|
||||
<meta itemprop="numberOfPages" content="{{ pages }}">
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
{% if format and not pages %}
|
||||
{% blocktrans %}{{ format }}{% endblocktrans %}
|
||||
{% elif format and pages %}
|
||||
|
@ -24,8 +24,9 @@
|
|||
{% elif pages %}
|
||||
{% blocktrans %}{{ pages }} pages{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% if book.languages %}
|
||||
{% for language in book.languages %}
|
||||
|
@ -39,14 +40,15 @@
|
|||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
{% with date=book.published_date|naturalday publisher=book.publishers|join:', ' %}
|
||||
{% if date or book.first_published_date or book.publishers %}
|
||||
{% if date or book.first_published_date %}
|
||||
<meta
|
||||
itemprop="datePublished"
|
||||
content="{{ book.first_published_date|default:book.published_date|date:'Y-m-d' }}"
|
||||
>
|
||||
{% endif %}
|
||||
<p>
|
||||
|
||||
{% comment %}
|
||||
@todo The publisher property needs to be an Organization or a Person. We’ll be using Thing which is the more generic ancestor.
|
||||
|
@ -65,6 +67,7 @@
|
|||
{% elif publisher %}
|
||||
{% blocktrans %}Published by {{ publisher }}.{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endspaceless %}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{% load status_display %}
|
||||
<div class="block">
|
||||
|
||||
<div class="thread-parent is-relative block">
|
||||
<div class="thread">
|
||||
{% with depth=depth|add:1 %}
|
||||
{% if depth <= max_depth and status.reply_parent and direction <= 0 %}
|
||||
{% with direction=-1 %}
|
||||
|
@ -8,7 +9,9 @@
|
|||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
<div{% if is_root %} class="block mt-5 is-main"{% endif %}>
|
||||
{% include 'snippets/status/status.html' with status=status main=is_root %}
|
||||
</div>
|
||||
|
||||
{% if depth <= max_depth and direction >= 0 %}
|
||||
{% for reply in status|replies %}
|
||||
|
@ -18,5 +21,5 @@
|
|||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,160 +0,0 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block title %}{% trans "Notifications" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header class="columns">
|
||||
<div class="column">
|
||||
<h1 class="title">{% trans "Notifications" %}</h1>
|
||||
</div>
|
||||
|
||||
<form name="clear" action="/notifications" method="POST" class="column is-narrow">
|
||||
{% csrf_token %}
|
||||
<button class="button is-danger is-light" type="submit" class="secondary">{% trans "Delete notifications" %}</button>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<div class="block">
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
{% url 'notifications' as tab_url %}
|
||||
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||
<a href="{{ tab_url }}">{% trans "All" %}</a>
|
||||
</li>
|
||||
{% url 'notifications' 'mentions' as tab_url %}
|
||||
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||
<a href="{{ tab_url }}">{% trans "Mentions" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
{% for notification in notifications %}
|
||||
{% related_status notification as related_status %}
|
||||
<div class="notification {% if notification.id in unread %} is-primary{% endif %}">
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-narrow is-size-3 {% if notification.id in unread%}has-text-white{% else %}has-text-grey{% endif %}">
|
||||
{% if notification.notification_type == 'MENTION' %}
|
||||
<span class="icon icon-comment"></span>
|
||||
{% elif notification.notification_type == 'REPLY' %}
|
||||
<span class="icon icon-comments"></span>
|
||||
{% elif notification.notification_type == 'FOLLOW' or notification.notification_type == 'FOLLOW_REQUEST' %}
|
||||
<span class="icon icon-local"></span>
|
||||
{% elif notification.notification_type == 'BOOST' %}
|
||||
<span class="icon icon-boost"></span>
|
||||
{% elif notification.notification_type == 'FAVORITE' %}
|
||||
<span class="icon icon-heart"></span>
|
||||
{% elif notification.notification_type == 'IMPORT' %}
|
||||
<span class="icon icon-list"></span>
|
||||
{% elif notification.notification_type == 'ADD' %}
|
||||
<span class="icon icon-plus"></span>
|
||||
{% elif notification.notification_type == 'REPORT' %}
|
||||
<span class="icon icon-warning"></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="column is-clipped">
|
||||
<div class="block">
|
||||
<p>
|
||||
{# DESCRIPTION #}
|
||||
{% if notification.related_user %}
|
||||
<a href="{{ notification.related_user.local_path }}">
|
||||
{% include 'snippets/avatar.html' with user=notification.related_user %}
|
||||
{{ notification.related_user.display_name }}
|
||||
</a>
|
||||
{% if notification.notification_type == 'FAVORITE' %}
|
||||
{% if related_status.status_type == 'Review' %}
|
||||
{% blocktrans with book_title=related_status.book.title related_path=related_status.local_path %}favorited your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Comment' %}
|
||||
{% blocktrans with book_title=related_status.book.title related_path=related_status.local_path %}favorited your <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Quotation' %}
|
||||
{% blocktrans with book_title=related_status.book.title related_path=related_status.local_path %}favorited your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans with related_path=related_status.local_path %}favorited your <a href="{{ related_path }}">status</a>{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
{% elif notification.notification_type == 'MENTION' %}
|
||||
{% if related_status.status_type == 'Review' %}
|
||||
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}mentioned you in a <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Comment' %}
|
||||
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}mentioned you in a <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Quotation' %}
|
||||
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}mentioned you in a <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans with related_path=related_status.local_path %}mentioned you in a <a href="{{ related_path }}">status</a>{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
{% elif notification.notification_type == 'REPLY' %}
|
||||
{% if related_status.status_type == 'Review' %}
|
||||
{% blocktrans with related_path=related_status.local_path parent_path=related_status.reply_parent.local_path book_title=related_status.reply_parent.book.title %}<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">review of <em>{{ book_title }}</em></a>{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Comment' %}
|
||||
{% blocktrans with related_path=related_status.local_path parent_path=related_status.reply_parent.local_path book_title=related_status.reply_parent.book.title %}<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">comment on <em>{{ book_title }}</em></a>{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Quotation' %}
|
||||
{% blocktrans with related_path=related_status.local_path parent_path=related_status.reply_parent.local_path book_title=related_status.reply_parent.book.title %}<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">quote from <em>{{ book_title }}</em></a>{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans with related_path=related_status.local_path parent_path=related_status.reply_parent.local_path %}<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">status</a>{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
{% elif notification.notification_type == 'FOLLOW' %}
|
||||
{% trans "followed you" %}
|
||||
{% include 'snippets/follow_button.html' with user=notification.related_user %}
|
||||
{% elif notification.notification_type == 'FOLLOW_REQUEST' %}
|
||||
{% trans "sent you a follow request" %}
|
||||
<div class="row shrink">
|
||||
{% include 'snippets/follow_request_buttons.html' with user=notification.related_user %}
|
||||
</div>
|
||||
{% elif notification.notification_type == 'BOOST' %}
|
||||
{% if related_status.status_type == 'Review' %}
|
||||
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}boosted your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Comment' %}
|
||||
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}boosted your <a href="{{ related_path }}">comment on<em>{{ book_title }}</em></a>{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Quotation' %}
|
||||
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}boosted your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans with related_path=related_status.local_path %}boosted your <a href="{{ related_path }}">status</a>{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% elif notification.notification_type == 'ADD' %}
|
||||
{% if notification.related_list_item.approved %}
|
||||
{% blocktrans with book_path=notification.related_list_item.book.local_path book_title=notification.related_list_item.book.title list_path=notification.related_list_item.book_list.local_path list_name=notification.related_list_item.book_list.name %} added <em><a href="{{ book_path }}">{{ book_title }}</a></em> to your list "<a href="{{ list_path }}">{{ list_name }}</a>"{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans with book_path=notification.related_list_item.book.local_path book_title=notification.related_list_item.book.title list_path=notification.related_list_item.book_list.local_path list_name=notification.related_list_item.book_list.name %} suggested adding <em><a href="{{ book_path }}">{{ book_title }}</a></em> to your list "<a href="{{ list_path }}/curate">{{ list_name }}</a>"{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% elif notification.related_import %}
|
||||
{% url 'import-status' notification.related_import.id as url %}
|
||||
{% blocktrans %}Your <a href="{{ url }}">import</a> completed.{% endblocktrans %}
|
||||
{% elif notification.related_report %}
|
||||
{% url 'settings-report' notification.related_report.id as path %}
|
||||
{% blocktrans with related_id=path %}A new <a href="{{ path }}">report</a> needs moderation.{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% if related_status %}
|
||||
<div class="block">
|
||||
{# 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="columns">
|
||||
<div class="column is-clipped">
|
||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||
</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 %}">
|
||||
{{ related_status.published_date|timesince }}
|
||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if not notifications %}
|
||||
<p>{% trans "You're all caught up!" %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
20
bookwyrm/templates/notifications/item.html
Normal file
20
bookwyrm/templates/notifications/item.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{# load the right template #}
|
||||
{% if notification.notification_type == 'MENTION' %}
|
||||
{% include 'notifications/items/mention.html' %}
|
||||
{% elif notification.notification_type == 'REPLY' %}
|
||||
{% include 'notifications/items/reply.html' %}
|
||||
{% elif notification.notification_type == 'BOOST' %}
|
||||
{% include 'notifications/items/boost.html' %}
|
||||
{% elif notification.notification_type == 'FAVORITE' %}
|
||||
{% include 'notifications/items/fav.html' %}
|
||||
{% elif notification.notification_type == 'FOLLOW' %}
|
||||
{% include 'notifications/items/follow.html' %}
|
||||
{% elif notification.notification_type == 'FOLLOW_REQUEST' %}
|
||||
{% include 'notifications/items/follow_request.html' %}
|
||||
{% elif notification.notification_type == 'IMPORT' %}
|
||||
{% include 'notifications/items/import.html' %}
|
||||
{% elif notification.notification_type == 'ADD' %}
|
||||
{% include 'notifications/items/add.html' %}
|
||||
{% elif notification.notification_type == 'REPORT' %}
|
||||
{% include 'notifications/items/report.html' %}
|
||||
{% endif %}
|
42
bookwyrm/templates/notifications/items/add.html
Normal file
42
bookwyrm/templates/notifications/items/add.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
{% extends 'notifications/items/item_layout.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block primary_link %}{% spaceless %}
|
||||
{% if notification.related_list_item.approved %}
|
||||
{{ notification.related_list_item.book_list.local_path }}
|
||||
{% else %}
|
||||
{% url 'list-curate' notification.related_list_item.book_list.id %}
|
||||
{% endif %}
|
||||
{% endspaceless %}{% endblock %}
|
||||
|
||||
{% block icon %}
|
||||
<span class="icon icon-plus"></span>
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
{% with book_path=notification.related_list_item.book.local_path %}
|
||||
{% with book_title=notification.related_list_item.book|book_title %}
|
||||
{% with list_name=notification.related_list_item.book_list.name %}
|
||||
|
||||
{% if notification.related_list_item.approved %}
|
||||
{% blocktrans trimmed with list_path=notification.related_list_item.book_list.local_path %}
|
||||
|
||||
added <em><a href="{{ book_path }}">{{ book_title }}</a></em> to your list "<a href="{{ list_path }}">{{ list_name }}</a>"
|
||||
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% url 'list-curate' notification.related_list_item.book_list.id as list_path %}
|
||||
{% blocktrans trimmed with list_path=list_path %}
|
||||
|
||||
suggested adding <em><a href="{{ book_path }}">{{ book_title }}</a></em> to your list "<a href="{{ list_path }}">{{ list_name }}</a>"
|
||||
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
61
bookwyrm/templates/notifications/items/boost.html
Normal file
61
bookwyrm/templates/notifications/items/boost.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
{% extends 'notifications/items/item_layout.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block primary_link %}{% spaceless %}
|
||||
{{ notification.related_status.local_path }}
|
||||
{% endspaceless %}{% endblock %}
|
||||
|
||||
{% block icon %}
|
||||
<span class="icon icon-boost"></span>
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
{% with related_status.book|book_title as book_title %}
|
||||
{% with related_status.local_path as related_path %}
|
||||
|
||||
{% if related_status.status_type == 'Review' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
boosted your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Comment' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
boosted your <a href="{{ related_path }}">comment on<em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Quotation' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
boosted your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
boosted your <a href="{{ related_path }}">status</a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block preview %}
|
||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white has-text-grey-dark{% endif %}">
|
||||
<div class="columns">
|
||||
<div class="column is-clipped">
|
||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||
</div>
|
||||
<div class="column is-narrow has-grey-dark">
|
||||
{{ related_status.published_date|timesince }}
|
||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
61
bookwyrm/templates/notifications/items/fav.html
Normal file
61
bookwyrm/templates/notifications/items/fav.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
{% extends 'notifications/items/item_layout.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block primary_link %}{% spaceless %}
|
||||
{{ notification.related_status.local_path }}
|
||||
{% endspaceless %}{% endblock %}
|
||||
|
||||
{% block icon %}
|
||||
<span class="icon icon-heart"></span>
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
{% with related_status.book|book_title as book_title %}
|
||||
{% with related_status.local_path as related_path %}
|
||||
|
||||
{% if related_status.status_type == 'Review' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
favorited your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Comment' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
favorited your <a href="{{ related_path }}">comment on<em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Quotation' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
favorited your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
favorited your <a href="{{ related_path }}">status</a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block preview %}
|
||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white has-text-grey-dark{% endif %}">
|
||||
<div class="columns">
|
||||
<div class="column is-clipped">
|
||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||
</div>
|
||||
<div class="column is-narrow has-grey-dark">
|
||||
{{ related_status.published_date|timesince }}
|
||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
17
bookwyrm/templates/notifications/items/follow.html
Normal file
17
bookwyrm/templates/notifications/items/follow.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{% extends 'notifications/items/item_layout.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block primary_link %}{% spaceless %}
|
||||
{{ notification.related_user.local_path }}
|
||||
{% endspaceless %}{% endblock %}
|
||||
|
||||
{% block icon %}
|
||||
<span class="icon icon-local"></span>
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
{% trans "followed you" %}
|
||||
{% include 'snippets/follow_button.html' with user=notification.related_user %}
|
||||
{% endblock %}
|
15
bookwyrm/templates/notifications/items/follow_request.html
Normal file
15
bookwyrm/templates/notifications/items/follow_request.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends 'notifications/items/item_layout.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block icon %}
|
||||
<span class="icon icon-local"></span>
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
{% trans "sent you a follow request" %}
|
||||
<div class="row shrink">
|
||||
{% include 'snippets/follow_request_buttons.html' with user=notification.related_user %}
|
||||
</div>
|
||||
{% endblock %}
|
15
bookwyrm/templates/notifications/items/import.html
Normal file
15
bookwyrm/templates/notifications/items/import.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends 'notifications/items/item_layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block primary_link %}{% spaceless %}
|
||||
{% url 'import-status' notification.related_import.id %}
|
||||
{% endspaceless %}{% endblock %}
|
||||
|
||||
{% block icon %}
|
||||
<span class="icon icon-list"></span>
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
{% url 'import-status' notification.related_import.id as url %}
|
||||
{% blocktrans %}Your <a href="{{ url }}">import</a> completed.{% endblocktrans %}
|
||||
{% endblock %}
|
29
bookwyrm/templates/notifications/items/item_layout.html
Normal file
29
bookwyrm/templates/notifications/items/item_layout.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
{% load humanize %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% related_status notification as related_status %}
|
||||
<div class="notification is-clickable {% if notification.id in unread %} is-primary{% endif %}" data-href="{% block primary_link %}{% endblock %}">
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-narrow is-size-3 {% if notification.id in unread%}has-text-white{% else %}has-text-grey{% endif %}">
|
||||
{% block icon %}{% endblock %}
|
||||
</div>
|
||||
<div class="column is-clipped">
|
||||
<div class="block">
|
||||
<p>
|
||||
{% if notification.related_user %}
|
||||
<a href="{{ notification.related_user.local_path }}">
|
||||
{% include 'snippets/avatar.html' with user=notification.related_user %}
|
||||
{{ notification.related_user.display_name }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% block description %}{% endblock %}
|
||||
</p>
|
||||
</div>
|
||||
{% if related_status %}
|
||||
<div class="block">
|
||||
{% block preview %}{% endblock %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
62
bookwyrm/templates/notifications/items/mention.html
Normal file
62
bookwyrm/templates/notifications/items/mention.html
Normal file
|
@ -0,0 +1,62 @@
|
|||
{% extends 'notifications/items/item_layout.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block primary_link %}{% spaceless %}
|
||||
{{ notification.related_status.local_path }}
|
||||
{% endspaceless %}{% endblock %}
|
||||
|
||||
{% block icon %}
|
||||
<span class="icon icon-comment"></span>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block description %}
|
||||
{% with related_status.book|book_title as book_title %}
|
||||
{% with related_status.local_path as related_path %}
|
||||
|
||||
{% if related_status.status_type == 'Review' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
mentioned you in a <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Comment' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
mentioned you in a <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Quotation' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
mentioned you in a <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
mentioned you in a <a href="{{ related_path }}">status</a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block preview %}
|
||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white has-text-black{% endif %}">
|
||||
<div class="columns">
|
||||
<div class="column is-clipped">
|
||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||
</div>
|
||||
<div class="column is-narrow has-text-black">
|
||||
{{ related_status.published_date|timesince }}
|
||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
65
bookwyrm/templates/notifications/items/reply.html
Normal file
65
bookwyrm/templates/notifications/items/reply.html
Normal file
|
@ -0,0 +1,65 @@
|
|||
{% extends 'notifications/items/item_layout.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block primary_link %}{% spaceless %}
|
||||
{{ notification.related_status.local_path }}
|
||||
{% endspaceless %}{% endblock %}
|
||||
|
||||
{% block icon %}
|
||||
<span class="icon icon-comments"></span>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block description %}
|
||||
{% with related_status.reply_parent.book|book_title as book_title %}
|
||||
{% with related_status.local_path as related_path %}
|
||||
{% with related_status.reply_parent.local_path as parent_path %}
|
||||
|
||||
{% if related_status.reply_parent.status_type == 'Review' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">review of <em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% elif related_status.reply_parent.status_type == 'Comment' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">comment on <em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% elif related_status.reply_parent.status_type == 'Quotation' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">quote from <em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">status</a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block preview %}
|
||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white has-text-black{% endif %}">
|
||||
<div class="columns">
|
||||
<div class="column is-clipped">
|
||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||
</div>
|
||||
<div class="column is-narrow has-text-black">
|
||||
{{ related_status.published_date|timesince }}
|
||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
16
bookwyrm/templates/notifications/items/report.html
Normal file
16
bookwyrm/templates/notifications/items/report.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% extends 'notifications/items/item_layout.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block primary_link %}{% spaceless %}
|
||||
{% url 'settings-report' notification.related_report.id %}
|
||||
{% endspaceless %}{% endblock %}
|
||||
|
||||
{% block icon %}
|
||||
<span class="icon icon-warning"></span>
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
{% url 'settings-report' notification.related_report.id as path %}
|
||||
{% blocktrans %}A new <a href="{{ path }}">report</a> needs moderation.{% endblocktrans %}
|
||||
{% endblock %}
|
52
bookwyrm/templates/notifications/notifications_page.html
Normal file
52
bookwyrm/templates/notifications/notifications_page.html
Normal file
|
@ -0,0 +1,52 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% trans "Notifications" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header class="columns is-mobile">
|
||||
<div class="column">
|
||||
<h1 class="title">{% trans "Notifications" %}</h1>
|
||||
</div>
|
||||
|
||||
<form name="clear" action="/notifications" method="POST" class="column is-narrow">
|
||||
{% csrf_token %}
|
||||
{% spaceless %}
|
||||
<button class="button is-danger is-light" type="submit">
|
||||
<span class="icon icon-x m-0-mobile" aria-hidden="true"></span>
|
||||
<span class="is-sr-only-mobile">{% trans "Delete notifications" %}</span>
|
||||
</button>
|
||||
{% endspaceless %}
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<div class="block">
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
{% url 'notifications' as tab_url %}
|
||||
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||
<a href="{{ tab_url }}">{% trans "All" %}</a>
|
||||
</li>
|
||||
{% url 'notifications' 'mentions' as tab_url %}
|
||||
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||
<a href="{{ tab_url }}">{% trans "Mentions" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
{% for notification in notifications %}
|
||||
{% include 'notifications/item.html' %}
|
||||
{% endfor %}
|
||||
|
||||
{% if not notifications %}
|
||||
<p>{% trans "You're all caught up!" %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{% static "js/block_href.js" %}?v={{ js_cache }}"></script>
|
||||
{% endblock %}
|
|
@ -8,7 +8,24 @@
|
|||
<ul class="block">
|
||||
{% for result in local_results.results %}
|
||||
<li class="pd-4 mb-5">
|
||||
{% include 'snippets/search_result_text.html' with result=result %}
|
||||
<div class="columns is-mobile is-gapless">
|
||||
<div class="column is-cover">
|
||||
{% include 'snippets/book_cover.html' with book=result cover_class='is-w-xs is-h-xs' %}
|
||||
</div>
|
||||
|
||||
<div class="column is-10 ml-3">
|
||||
<p>
|
||||
<strong>
|
||||
{% include "snippets/book_titleby.html" with book=result %}
|
||||
</strong>
|
||||
</p>
|
||||
<p>
|
||||
{% if result.first_published_date or result.published_date %}
|
||||
({% firstof result.first_published_date.year result.published_date.year %})
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
@ -43,7 +60,33 @@
|
|||
<ul class="is-flex-grow-1">
|
||||
{% for result in result_set.results %}
|
||||
<li class="mb-5">
|
||||
{% include 'snippets/search_result_text.html' with result=result remote_result=True %}
|
||||
<div class="columns is-mobile is-gapless">
|
||||
<div class="columns is-mobile is-gapless">
|
||||
{% include 'snippets/book_cover.html' with book=result cover_class='is-w-xs is-h-xs' external_path=True %}
|
||||
</div>
|
||||
<div class="column is-10 ml-3">
|
||||
<p>
|
||||
<strong>
|
||||
<a
|
||||
href="{{ result.view_link|default:result.key }}"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>{{ result.title }}</a>
|
||||
</strong>
|
||||
</p>
|
||||
<p>
|
||||
{{ result.author }}
|
||||
{% if result.year %}({{ result.year }}){% endif %}
|
||||
</p>
|
||||
<form class="mt-1" action="/resolve-book" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="remote_id" value="{{ result.key }}">
|
||||
<button type="submit" class="button is-small is-link">
|
||||
{% trans "Import book" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
|
@ -12,7 +12,7 @@ draft: an existing Status object that is providing default values for input fiel
|
|||
name="content"
|
||||
class="textarea save-draft"
|
||||
data-cache-draft="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
|
||||
id="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
|
||||
id="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}{{ uuid }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}"
|
||||
{% if not optional and type != "quotation" and type != "review" %}required{% endif %}
|
||||
|
|
|
@ -19,7 +19,7 @@ reply_parent: the Status object this post will be in reply to, if applicable
|
|||
name="{{ type }}"
|
||||
action="/post/{{ type }}"
|
||||
method="post"
|
||||
id="tab_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
|
||||
id="form_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
|
||||
>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -36,7 +36,7 @@ reply_parent: the Status object this post will be in reply to, if applicable
|
|||
{# fields that go between the content warnings and the content field (ie, quote) #}
|
||||
{% block pre_content_additions %}{% endblock %}
|
||||
|
||||
<label class="label" for="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}">
|
||||
<label class="label" for="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}{{ uuid }}">
|
||||
{% block content_label %}
|
||||
{% trans "Comment:" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
{% load i18n %}
|
||||
<div class="content">
|
||||
<p>{% blocktrans %}Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.{% endblocktrans %}</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<form method="post" name="goal" action="{% url 'user-goal' request.user.localname year %}">
|
||||
{% csrf_token %}
|
||||
|
@ -20,7 +24,7 @@
|
|||
|
||||
<div class="column">
|
||||
<label class="label" for="privacy_{{ goal.id }}">{% trans "Goal privacy:" %}</label>
|
||||
{% include 'snippets/privacy_select.html' with no_label=True current=goal.privacy uuid=goal.id %}
|
||||
{% include 'snippets/privacy_select.html' with no_label=True current=goal.privacy privacy_uuid=goal.id %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
<div class="select {{ class }}">
|
||||
{% firstof uuid 0|uuid as uuid %}
|
||||
{% firstof privacy_uuid 0|uuid as uuid %}
|
||||
{% if not no_label %}
|
||||
<label class="is-sr-only" for="privacy_{{ uuid }}">{% trans "Post privacy" %}</label>
|
||||
{% endif %}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
type="number"
|
||||
name="progress"
|
||||
class="input"
|
||||
id="id_progress_{{ readthrough.id }}"
|
||||
id="id_progress_{{ readthrough.id }}{{ controls_uid }}"
|
||||
value="{{ readthrough.progress }}"
|
||||
{% if progress_required %}required{% endif %}
|
||||
>
|
||||
|
|
|
@ -35,3 +35,7 @@ Finish "<em>{{ book_title }}</em>"
|
|||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
{% include "snippets/reading_modals/form.html" with optional=True type="finish_modal" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -15,3 +15,5 @@
|
|||
<input type="hidden" name="mention_books" value="{{ book.id }}">
|
||||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block form_close %}{% endblock %}
|
||||
|
|
|
@ -23,10 +23,11 @@
|
|||
<div id="reading_content_{{ local_uuid }}_{{ uuid }}">
|
||||
<hr aria-hidden="true">
|
||||
<fieldset id="reading_content_fieldset_{{ local_uuid }}_{{ uuid }}">
|
||||
{% comparison_bool controls_text "progress_update" True as optional %}
|
||||
{% include "snippets/reading_modals/form.html" with optional=optional %}
|
||||
{% block form %}{% endblock %}
|
||||
</fieldset>
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
||||
|
|
|
@ -12,6 +12,10 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block reading-dates %}
|
||||
<label for="id_progress_{{ readthrough.id }}" class="label">{% trans "Progress:" %}</label>
|
||||
<label for="id_progress_{{ readthrough.id }}{{ controls_uid }}" class="label">{% trans "Progress:" %}</label>
|
||||
{% include "snippets/progress_field.html" with progress_required=True %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
{% include "snippets/reading_modals/form.html" with optional=False type="update_modal" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -22,3 +22,7 @@ Start "<em>{{ book_title }}</em>"
|
|||
<input type="date" name="start_date" class="input" id="start_id_start_date_{{ uuid }}" value="{% now "Y-m-d" %}">
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
{% include "snippets/reading_modals/form.html" with optional=True type="start_modal" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -13,3 +13,7 @@ Want to Read "<em>{{ book_title }}</em>"
|
|||
<input type="hidden" name="reading_status" value="to-read">
|
||||
{% csrf_token %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
{% include "snippets/reading_modals/form.html" with optional=True type="want_modal" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
</div>
|
||||
{# Only show progress for editing existing readthroughs #}
|
||||
{% if readthrough.id and not readthrough.finish_date %}
|
||||
<label class="label" for="id_progress_{{ readthrough.id }}">
|
||||
<label class="label" for="id_progress_{{ readthrough.id }}{{ controls_uid }}">
|
||||
{% trans "Progress" %}
|
||||
</label>
|
||||
{% include "snippets/progress_field.html" %}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
{% load i18n %}
|
||||
<div class="columns is-mobile is-gapless">
|
||||
<div class="column is-cover">
|
||||
{% include 'snippets/book_cover.html' with book=result cover_class='is-w-xs is-h-xs' external_path=True %}
|
||||
</div>
|
||||
|
||||
<div class="column is-10 ml-3">
|
||||
<p>
|
||||
<strong>
|
||||
<a
|
||||
href="{{ result.view_link|default:result.key }}"
|
||||
{% if remote_result %}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
{% endif %}
|
||||
>{{ result.title }}</a>
|
||||
</strong>
|
||||
</p>
|
||||
<p>
|
||||
{% if result.author %}
|
||||
{{ result.author }}
|
||||
{% endif %}
|
||||
|
||||
{% if result.year %}
|
||||
({{ result.year }})
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if remote_result %}
|
||||
<form class="mt-1" action="/resolve-book" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<input type="hidden" name="remote_id" value="{{ result.key }}">
|
||||
|
||||
<button type="submit" class="button is-small is-link">
|
||||
{% trans "Import book" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -32,7 +32,7 @@
|
|||
{% include "snippets/status/header_content.html" %}
|
||||
</h3>
|
||||
<p class="is-size-7 is-flex is-align-items-center">
|
||||
<a href="{{ status.remote_id }}">{{ status.published_date|published_date }}</a>
|
||||
<a href="{{ status.remote_id }}{% if status.user.local %}#anchor-{{ status.id }}{% endif %}">{{ status.published_date|published_date }}</a>
|
||||
{% if status.progress %}
|
||||
<span class="ml-1">
|
||||
{% if status.progress_mode == 'PG' %}
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
{% load utilities %}
|
||||
|
||||
{% block card-header %}
|
||||
<div class="card-header-title has-background-white-ter is-block">
|
||||
<div
|
||||
class="card-header-title has-background-white-ter is-block"
|
||||
{% if main %}id="anchor-{{ status.id }}"{% endif %}
|
||||
>
|
||||
{% include 'snippets/status/header.html' with status=status %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
{% if status.user == request.user %}
|
||||
{# things you can do to your own statuses #}
|
||||
<li role="menuitem" class="dropdown-item p-0">
|
||||
<form name="delete-{{ status.id }}" action="/delete-status/{{ status.id }}" method="post">
|
||||
<form name="delete-{{ status.id|uuid }}" action="/delete-status/{{ status.id }}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="button is-radiusless is-danger is-light is-fullwidth is-small" type="submit">
|
||||
{% trans "Delete status" %}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{% load utilities %}
|
||||
{% if fallback_url %}
|
||||
<form name="fallback-form-{{ controls_uuid}}" method="GET" action="{{ fallback_url }}">
|
||||
<form name="fallback_form_{{ 0|uuid }}" method="GET" action="{{ fallback_url }}">
|
||||
{% endif %}
|
||||
<button
|
||||
{% if not fallback_url %}
|
||||
|
|
|
@ -119,12 +119,10 @@ class AbstractConnector(TestCase):
|
|||
@responses.activate
|
||||
def test_get_or_create_author(self):
|
||||
"""load an author"""
|
||||
self.connector.author_mappings = (
|
||||
[ # pylint: disable=attribute-defined-outside-init
|
||||
self.connector.author_mappings = [ # pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
|
||||
Mapping("id"),
|
||||
Mapping("name"),
|
||||
]
|
||||
)
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
|
|
|
@ -4,7 +4,7 @@ import responses
|
|||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.connectors import abstract_connector
|
||||
from bookwyrm.connectors.abstract_connector import Mapping, SearchResult
|
||||
from bookwyrm.connectors.abstract_connector import Mapping
|
||||
|
||||
|
||||
class AbstractConnector(TestCase):
|
||||
|
@ -53,7 +53,6 @@ class AbstractConnector(TestCase):
|
|||
self.assertEqual(connector.isbn_search_url, "https://example.com/isbn?q=")
|
||||
self.assertIsNone(connector.name)
|
||||
self.assertEqual(connector.identifier, "example.com")
|
||||
self.assertFalse(connector.local)
|
||||
|
||||
@responses.activate
|
||||
def test_search(self):
|
||||
|
@ -94,19 +93,6 @@ class AbstractConnector(TestCase):
|
|||
results = self.test_connector.isbn_search("123456")
|
||||
self.assertEqual(len(results), 10)
|
||||
|
||||
def test_search_result(self):
|
||||
"""a class that stores info about a search result"""
|
||||
result = SearchResult(
|
||||
title="Title",
|
||||
key="https://example.com/book/1",
|
||||
author="Author Name",
|
||||
year="1850",
|
||||
connector=self.test_connector,
|
||||
)
|
||||
# there's really not much to test here, it's just a dataclass
|
||||
self.assertEqual(result.confidence, 1)
|
||||
self.assertEqual(result.title, "Title")
|
||||
|
||||
def test_create_mapping(self):
|
||||
"""maps remote fields for book data to bookwyrm activitypub fields"""
|
||||
mapping = Mapping("isbn")
|
||||
|
|
|
@ -4,8 +4,8 @@ import pathlib
|
|||
from django.test import TestCase
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.book_search import SearchResult
|
||||
from bookwyrm.connectors.bookwyrm_connector import Connector
|
||||
from bookwyrm.connectors.abstract_connector import SearchResult
|
||||
|
||||
|
||||
class BookWyrmConnector(TestCase):
|
||||
|
|
|
@ -5,7 +5,6 @@ import responses
|
|||
from bookwyrm import models
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm.connectors.bookwyrm_connector import Connector as BookWyrmConnector
|
||||
from bookwyrm.connectors.self_connector import Connector as SelfConnector
|
||||
|
||||
|
||||
class ConnectorManager(TestCase):
|
||||
|
@ -15,28 +14,16 @@ class ConnectorManager(TestCase):
|
|||
"""we'll need some books and a connector info entry"""
|
||||
self.work = models.Work.objects.create(title="Example Work")
|
||||
|
||||
self.edition = models.Edition.objects.create(
|
||||
models.Edition.objects.create(
|
||||
title="Example Edition", parent_work=self.work, isbn_10="0000000000"
|
||||
)
|
||||
self.edition = models.Edition.objects.create(
|
||||
title="Another Edition", parent_work=self.work, isbn_10="1111111111"
|
||||
)
|
||||
|
||||
self.connector = models.Connector.objects.create(
|
||||
identifier="test_connector",
|
||||
priority=1,
|
||||
local=True,
|
||||
connector_file="self_connector",
|
||||
base_url="http://test.com/",
|
||||
books_url="http://test.com/",
|
||||
covers_url="http://test.com/",
|
||||
isbn_search_url="http://test.com/isbn/",
|
||||
)
|
||||
|
||||
self.remote_connector = models.Connector.objects.create(
|
||||
identifier="test_connector_remote",
|
||||
priority=1,
|
||||
local=False,
|
||||
connector_file="bookwyrm_connector",
|
||||
base_url="http://fake.ciom/",
|
||||
books_url="http://fake.ciom/",
|
||||
|
@ -59,23 +46,22 @@ class ConnectorManager(TestCase):
|
|||
def test_get_connectors(self):
|
||||
"""load all connectors"""
|
||||
connectors = list(connector_manager.get_connectors())
|
||||
self.assertEqual(len(connectors), 2)
|
||||
self.assertIsInstance(connectors[0], SelfConnector)
|
||||
self.assertIsInstance(connectors[1], BookWyrmConnector)
|
||||
self.assertEqual(len(connectors), 1)
|
||||
self.assertIsInstance(connectors[0], BookWyrmConnector)
|
||||
|
||||
@responses.activate
|
||||
def test_search(self):
|
||||
def test_search_plaintext(self):
|
||||
"""search all connectors"""
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"http://fake.ciom/search/Example?min_confidence=0.1",
|
||||
json={},
|
||||
json=[{"title": "Hello", "key": "https://www.example.com/search/1"}],
|
||||
)
|
||||
results = connector_manager.search("Example")
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertIsInstance(results[0]["connector"], SelfConnector)
|
||||
self.assertEqual(len(results[0]["results"]), 1)
|
||||
self.assertEqual(results[0]["results"][0].title, "Example Edition")
|
||||
self.assertEqual(results[0]["connector"].identifier, "test_connector_remote")
|
||||
self.assertEqual(results[0]["results"][0].title, "Hello")
|
||||
|
||||
def test_search_empty_query(self):
|
||||
"""don't panic on empty queries"""
|
||||
|
@ -88,19 +74,13 @@ class ConnectorManager(TestCase):
|
|||
responses.add(
|
||||
responses.GET,
|
||||
"http://fake.ciom/isbn/0000000000",
|
||||
json={},
|
||||
json=[{"title": "Hello", "key": "https://www.example.com/search/1"}],
|
||||
)
|
||||
results = connector_manager.search("0000000000")
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertIsInstance(results[0]["connector"], SelfConnector)
|
||||
self.assertEqual(len(results[0]["results"]), 1)
|
||||
self.assertEqual(results[0]["results"][0].title, "Example Edition")
|
||||
|
||||
def test_local_search(self):
|
||||
"""search only the local database"""
|
||||
results = connector_manager.local_search("Example")
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0].title, "Example Edition")
|
||||
self.assertEqual(results[0]["connector"].identifier, "test_connector_remote")
|
||||
self.assertEqual(results[0]["results"][0].title, "Hello")
|
||||
|
||||
def test_first_search_result(self):
|
||||
"""only get one search result"""
|
||||
|
@ -125,6 +105,5 @@ class ConnectorManager(TestCase):
|
|||
|
||||
def test_load_connector(self):
|
||||
"""load a connector object from the database entry"""
|
||||
connector = connector_manager.load_connector(self.connector)
|
||||
self.assertIsInstance(connector, SelfConnector)
|
||||
self.assertEqual(connector.identifier, "test_connector")
|
||||
connector = connector_manager.load_connector(self.remote_connector)
|
||||
self.assertEqual(connector.identifier, "test_connector_remote")
|
||||
|
|
|
@ -7,11 +7,11 @@ from django.test import TestCase
|
|||
import responses
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.book_search import SearchResult
|
||||
from bookwyrm.connectors.openlibrary import Connector
|
||||
from bookwyrm.connectors.openlibrary import ignore_edition
|
||||
from bookwyrm.connectors.openlibrary import get_languages, get_description
|
||||
from bookwyrm.connectors.openlibrary import pick_default_edition, get_openlibrary_key
|
||||
from bookwyrm.connectors.abstract_connector import SearchResult
|
||||
from bookwyrm.connectors.connector_manager import ConnectorException
|
||||
|
||||
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
""" testing book data connectors """
|
||||
import datetime
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.connectors.self_connector import Connector
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
|
||||
class SelfConnector(TestCase):
|
||||
"""just uses local data"""
|
||||
|
||||
def setUp(self):
|
||||
"""creating the connector"""
|
||||
models.Connector.objects.create(
|
||||
identifier=DOMAIN,
|
||||
name="Local",
|
||||
local=True,
|
||||
connector_file="self_connector",
|
||||
base_url="https://%s" % DOMAIN,
|
||||
books_url="https://%s/book" % DOMAIN,
|
||||
covers_url="https://%s/images/covers" % DOMAIN,
|
||||
search_url="https://%s/search?q=" % DOMAIN,
|
||||
priority=1,
|
||||
)
|
||||
self.connector = Connector(DOMAIN)
|
||||
|
||||
def test_format_search_result(self):
|
||||
"""create a SearchResult"""
|
||||
author = models.Author.objects.create(name="Anonymous")
|
||||
edition = models.Edition.objects.create(
|
||||
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]
|
||||
self.assertEqual(result.title, "Edition of Example Work")
|
||||
self.assertEqual(result.key, edition.remote_id)
|
||||
self.assertEqual(result.author, "Anonymous")
|
||||
self.assertEqual(result.year, 1980)
|
||||
self.assertEqual(result.connector, self.connector)
|
||||
|
||||
def test_search_rank(self):
|
||||
"""prioritize certain results"""
|
||||
author = models.Author.objects.create(name="Anonymous")
|
||||
edition = models.Edition.objects.create(
|
||||
title="Edition of Example Work",
|
||||
published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc),
|
||||
parent_work=models.Work.objects.create(title=""),
|
||||
)
|
||||
# author text is rank B
|
||||
edition.authors.add(author)
|
||||
|
||||
# series is rank D
|
||||
models.Edition.objects.create(
|
||||
title="Another Edition",
|
||||
series="Anonymous",
|
||||
parent_work=models.Work.objects.create(title=""),
|
||||
)
|
||||
# subtitle is rank B
|
||||
models.Edition.objects.create(
|
||||
title="More Editions",
|
||||
subtitle="The Anonymous Edition",
|
||||
parent_work=models.Work.objects.create(title=""),
|
||||
)
|
||||
# title is rank A
|
||||
models.Edition.objects.create(title="Anonymous")
|
||||
# doesn't rank in this search
|
||||
models.Edition.objects.create(
|
||||
title="An Edition", parent_work=models.Work.objects.create(title="")
|
||||
)
|
||||
|
||||
results = self.connector.search("Anonymous")
|
||||
self.assertEqual(len(results), 4)
|
||||
self.assertEqual(results[0].title, "Anonymous")
|
||||
self.assertEqual(results[1].title, "More Editions")
|
||||
self.assertEqual(results[2].title, "Edition of Example Work")
|
||||
self.assertEqual(results[3].title, "Another Edition")
|
||||
|
||||
def test_search_multiple_editions(self):
|
||||
"""it should get rid of duplicate editions for the same work"""
|
||||
work = models.Work.objects.create(title="Work Title")
|
||||
edition_1 = models.Edition.objects.create(
|
||||
title="Edition 1 Title", parent_work=work
|
||||
)
|
||||
edition_2 = models.Edition.objects.create(
|
||||
title="Edition 2 Title",
|
||||
parent_work=work,
|
||||
isbn_13="123456789", # this is now the defualt edition
|
||||
)
|
||||
edition_3 = models.Edition.objects.create(title="Fish", parent_work=work)
|
||||
|
||||
# pick the best edition
|
||||
results = self.connector.search("Edition 1 Title")
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0].key, edition_1.remote_id)
|
||||
|
||||
# pick the default edition when no match is best
|
||||
results = self.connector.search("Edition Title")
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0].key, edition_2.remote_id)
|
||||
|
||||
# only matches one edition, so no deduplication takes place
|
||||
results = self.connector.search("Fish")
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0].key, edition_3.remote_id)
|
|
@ -12,7 +12,6 @@ import responses
|
|||
from bookwyrm import models
|
||||
from bookwyrm.importers import GoodreadsImporter
|
||||
from bookwyrm.importers.importer import import_data, handle_imported_book
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
|
||||
def make_date(*args):
|
||||
|
@ -39,17 +38,6 @@ class GoodreadsImport(TestCase):
|
|||
"mouse", "mouse@mouse.mouse", "password", local=True
|
||||
)
|
||||
|
||||
models.Connector.objects.create(
|
||||
identifier=DOMAIN,
|
||||
name="Local",
|
||||
local=True,
|
||||
connector_file="self_connector",
|
||||
base_url="https://%s" % DOMAIN,
|
||||
books_url="https://%s/book" % DOMAIN,
|
||||
covers_url="https://%s/images/covers" % DOMAIN,
|
||||
search_url="https://%s/search?q=" % DOMAIN,
|
||||
priority=1,
|
||||
)
|
||||
work = models.Work.objects.create(title="Test Work")
|
||||
self.book = models.Edition.objects.create(
|
||||
title="Example Edition",
|
||||
|
@ -125,7 +113,7 @@ class GoodreadsImport(TestCase):
|
|||
|
||||
import_job = models.ImportJob.objects.create(user=self.user)
|
||||
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
|
||||
csv_file = open(datafile, "r")
|
||||
csv_file = open(datafile, "r") # pylint: disable=unspecified-encoding
|
||||
for index, entry in enumerate(list(csv.DictReader(csv_file))):
|
||||
entry = self.importer.parse_fields(entry)
|
||||
import_item = models.ImportItem.objects.create(
|
||||
|
@ -162,7 +150,7 @@ class GoodreadsImport(TestCase):
|
|||
|
||||
import_job = models.ImportJob.objects.create(user=self.user)
|
||||
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
|
||||
csv_file = open(datafile, "r")
|
||||
csv_file = open(datafile, "r") # pylint: disable=unspecified-encoding
|
||||
for index, entry in enumerate(list(csv.DictReader(csv_file))):
|
||||
entry = self.importer.parse_fields(entry)
|
||||
import_item = models.ImportItem.objects.create(
|
||||
|
@ -192,7 +180,7 @@ class GoodreadsImport(TestCase):
|
|||
shelf = self.user.shelf_set.filter(identifier="read").first()
|
||||
import_job = models.ImportJob.objects.create(user=self.user)
|
||||
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
|
||||
csv_file = open(datafile, "r")
|
||||
csv_file = open(datafile, "r") # pylint: disable=unspecified-encoding
|
||||
for index, entry in enumerate(list(csv.DictReader(csv_file))):
|
||||
entry = self.importer.parse_fields(entry)
|
||||
import_item = models.ImportItem.objects.create(
|
||||
|
@ -224,7 +212,7 @@ class GoodreadsImport(TestCase):
|
|||
"""goodreads review import"""
|
||||
import_job = models.ImportJob.objects.create(user=self.user)
|
||||
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
|
||||
csv_file = open(datafile, "r")
|
||||
csv_file = open(datafile, "r") # pylint: disable=unspecified-encoding
|
||||
entry = list(csv.DictReader(csv_file))[2]
|
||||
entry = self.importer.parse_fields(entry)
|
||||
import_item = models.ImportItem.objects.create(
|
||||
|
@ -248,7 +236,7 @@ class GoodreadsImport(TestCase):
|
|||
datafile = pathlib.Path(__file__).parent.joinpath(
|
||||
"../data/goodreads-rating.csv"
|
||||
)
|
||||
csv_file = open(datafile, "r")
|
||||
csv_file = open(datafile, "r") # pylint: disable=unspecified-encoding
|
||||
entry = list(csv.DictReader(csv_file))[2]
|
||||
entry = self.importer.parse_fields(entry)
|
||||
import_item = models.ImportItem.objects.create(
|
||||
|
@ -269,7 +257,7 @@ class GoodreadsImport(TestCase):
|
|||
"""goodreads review import"""
|
||||
import_job = models.ImportJob.objects.create(user=self.user)
|
||||
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
|
||||
csv_file = open(datafile, "r")
|
||||
csv_file = open(datafile, "r") # pylint: disable=unspecified-encoding
|
||||
entry = list(csv.DictReader(csv_file))[2]
|
||||
entry = self.importer.parse_fields(entry)
|
||||
import_item = models.ImportItem.objects.create(
|
||||
|
|
|
@ -11,7 +11,6 @@ import responses
|
|||
from bookwyrm import models
|
||||
from bookwyrm.importers import LibrarythingImporter
|
||||
from bookwyrm.importers.importer import import_data, handle_imported_book
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
|
||||
def make_date(*args):
|
||||
|
@ -39,18 +38,6 @@ class LibrarythingImport(TestCase):
|
|||
self.user = models.User.objects.create_user(
|
||||
"mmai", "mmai@mmai.mmai", "password", local=True
|
||||
)
|
||||
|
||||
models.Connector.objects.create(
|
||||
identifier=DOMAIN,
|
||||
name="Local",
|
||||
local=True,
|
||||
connector_file="self_connector",
|
||||
base_url="https://%s" % DOMAIN,
|
||||
books_url="https://%s/book" % DOMAIN,
|
||||
covers_url="https://%s/images/covers" % DOMAIN,
|
||||
search_url="https://%s/search?q=" % DOMAIN,
|
||||
priority=1,
|
||||
)
|
||||
work = models.Work.objects.create(title="Test Work")
|
||||
self.book = models.Edition.objects.create(
|
||||
title="Example Edition",
|
||||
|
|
|
@ -9,8 +9,8 @@ from django.test import TestCase
|
|||
import responses
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.book_search import SearchResult
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm.connectors.abstract_connector import SearchResult
|
||||
|
||||
|
||||
class ImportJob(TestCase):
|
||||
|
|
|
@ -14,6 +14,7 @@ from bookwyrm import activitypub, models, settings
|
|||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
# pylint: disable=line-too-long
|
||||
@patch("bookwyrm.models.Status.broadcast")
|
||||
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
||||
@patch("bookwyrm.activitystreams.remove_status_task.delay")
|
||||
|
@ -52,22 +53,26 @@ class Status(TestCase):
|
|||
def test_status_generated_fields(self, *_):
|
||||
"""setting remote id"""
|
||||
status = models.Status.objects.create(content="bleh", user=self.local_user)
|
||||
expected_id = "https://%s/user/mouse/status/%d" % (settings.DOMAIN, status.id)
|
||||
expected_id = f"https://{settings.DOMAIN}/user/mouse/status/{status.id}"
|
||||
self.assertEqual(status.remote_id, expected_id)
|
||||
self.assertEqual(status.privacy, "public")
|
||||
|
||||
def test_replies(self, *_):
|
||||
"""get a list of replies"""
|
||||
parent = models.Status.objects.create(content="hi", user=self.local_user)
|
||||
child = models.Status.objects.create(
|
||||
parent = models.Status(content="hi", user=self.local_user)
|
||||
parent.save(broadcast=False)
|
||||
child = models.Status(
|
||||
content="hello", reply_parent=parent, user=self.local_user
|
||||
)
|
||||
models.Review.objects.create(
|
||||
child.save(broadcast=False)
|
||||
sibling = models.Review(
|
||||
content="hey", reply_parent=parent, user=self.local_user, book=self.book
|
||||
)
|
||||
models.Status.objects.create(
|
||||
sibling.save(broadcast=False)
|
||||
grandchild = models.Status(
|
||||
content="hi hello", reply_parent=child, user=self.local_user
|
||||
)
|
||||
grandchild.save(broadcast=False)
|
||||
|
||||
replies = models.Status.replies(parent)
|
||||
self.assertEqual(replies.count(), 2)
|
||||
|
@ -75,6 +80,11 @@ class Status(TestCase):
|
|||
# should select subclasses
|
||||
self.assertIsInstance(replies.last(), models.Review)
|
||||
|
||||
self.assertEqual(parent.thread_id, parent.id)
|
||||
self.assertEqual(child.thread_id, parent.id)
|
||||
self.assertEqual(sibling.thread_id, parent.id)
|
||||
self.assertEqual(grandchild.thread_id, parent.id)
|
||||
|
||||
def test_status_type(self, *_):
|
||||
"""class name"""
|
||||
self.assertEqual(models.Status().status_type, "Note")
|
||||
|
@ -104,7 +114,7 @@ class Status(TestCase):
|
|||
)
|
||||
|
||||
replies = parent.to_replies()
|
||||
self.assertEqual(replies["id"], "%s/replies" % parent.remote_id)
|
||||
self.assertEqual(replies["id"], f"{parent.remote_id}/replies")
|
||||
self.assertEqual(replies["totalItems"], 2)
|
||||
|
||||
def test_status_to_activity(self, *_):
|
||||
|
@ -168,7 +178,7 @@ class Status(TestCase):
|
|||
self.assertEqual(activity["id"], status.remote_id)
|
||||
self.assertEqual(
|
||||
activity["content"],
|
||||
'mouse test content <a href="%s">"Test Edition"</a>' % self.book.remote_id,
|
||||
f'mouse test content <a href="{self.book.remote_id}">"Test Edition"</a>',
|
||||
)
|
||||
self.assertEqual(len(activity["tag"]), 2)
|
||||
self.assertEqual(activity["type"], "Note")
|
||||
|
@ -177,7 +187,7 @@ class Status(TestCase):
|
|||
self.assertEqual(activity["attachment"][0].type, "Document")
|
||||
self.assertEqual(
|
||||
activity["attachment"][0].url,
|
||||
"https://%s%s" % (settings.DOMAIN, self.book.cover.url),
|
||||
f"https://{settings.DOMAIN}{self.book.cover.url}",
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].name, "Test Edition")
|
||||
|
||||
|
@ -202,13 +212,12 @@ class Status(TestCase):
|
|||
self.assertEqual(activity["type"], "Note")
|
||||
self.assertEqual(
|
||||
activity["content"],
|
||||
'test content<p>(comment on <a href="%s">"Test Edition"</a>)</p>'
|
||||
% self.book.remote_id,
|
||||
f'test content<p>(comment on <a href="{self.book.remote_id}">"Test Edition"</a>)</p>',
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].type, "Document")
|
||||
self.assertEqual(
|
||||
activity["attachment"][0].url,
|
||||
"https://%s%s" % (settings.DOMAIN, self.book.cover.url),
|
||||
f"https://{settings.DOMAIN}{self.book.cover.url}",
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].name, "Test Edition")
|
||||
|
||||
|
@ -240,13 +249,12 @@ class Status(TestCase):
|
|||
self.assertEqual(activity["type"], "Note")
|
||||
self.assertEqual(
|
||||
activity["content"],
|
||||
'a sickening sense <p>-- <a href="%s">"Test Edition"</a></p>'
|
||||
"test content" % self.book.remote_id,
|
||||
f'a sickening sense <p>-- <a href="{self.book.remote_id}">"Test Edition"</a></p>test content',
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].type, "Document")
|
||||
self.assertEqual(
|
||||
activity["attachment"][0].url,
|
||||
"https://%s%s" % (settings.DOMAIN, self.book.cover.url),
|
||||
f"https://{settings.DOMAIN}{self.book.cover.url}",
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].name, "Test Edition")
|
||||
|
||||
|
@ -281,13 +289,13 @@ class Status(TestCase):
|
|||
self.assertEqual(activity["type"], "Article")
|
||||
self.assertEqual(
|
||||
activity["name"],
|
||||
'Review of "%s" (3 stars): Review\'s name' % self.book.title,
|
||||
f'Review of "{self.book.title}" (3 stars): Review\'s name',
|
||||
)
|
||||
self.assertEqual(activity["content"], "test content")
|
||||
self.assertEqual(activity["attachment"][0].type, "Document")
|
||||
self.assertEqual(
|
||||
activity["attachment"][0].url,
|
||||
"https://%s%s" % (settings.DOMAIN, self.book.cover.url),
|
||||
f"https://{settings.DOMAIN}{self.book.cover.url}",
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].name, "Test Edition")
|
||||
|
||||
|
@ -303,13 +311,13 @@ class Status(TestCase):
|
|||
self.assertEqual(activity["id"], status.remote_id)
|
||||
self.assertEqual(activity["type"], "Article")
|
||||
self.assertEqual(
|
||||
activity["name"], 'Review of "%s": Review name' % self.book.title
|
||||
activity["name"], f'Review of "{self.book.title}": Review name'
|
||||
)
|
||||
self.assertEqual(activity["content"], "test content")
|
||||
self.assertEqual(activity["attachment"][0].type, "Document")
|
||||
self.assertEqual(
|
||||
activity["attachment"][0].url,
|
||||
"https://%s%s" % (settings.DOMAIN, self.book.cover.url),
|
||||
f"https://{settings.DOMAIN}{self.book.cover.url}",
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].name, "Test Edition")
|
||||
|
||||
|
@ -325,13 +333,12 @@ class Status(TestCase):
|
|||
self.assertEqual(activity["type"], "Note")
|
||||
self.assertEqual(
|
||||
activity["content"],
|
||||
'rated <em><a href="%s">%s</a></em>: 3 stars'
|
||||
% (self.book.remote_id, self.book.title),
|
||||
f'rated <em><a href="{self.book.remote_id}">{self.book.title}</a></em>: 3 stars',
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].type, "Document")
|
||||
self.assertEqual(
|
||||
activity["attachment"][0].url,
|
||||
"https://%s%s" % (settings.DOMAIN, self.book.cover.url),
|
||||
f"https://{settings.DOMAIN}{self.book.cover.url}",
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].name, "Test Edition")
|
||||
|
||||
|
|
118
bookwyrm/tests/test_book_search.py
Normal file
118
bookwyrm/tests/test_book_search.py
Normal file
|
@ -0,0 +1,118 @@
|
|||
""" test searching for books """
|
||||
import datetime
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from bookwyrm import book_search, models
|
||||
from bookwyrm.connectors.abstract_connector import AbstractMinimalConnector
|
||||
|
||||
|
||||
class BookSearch(TestCase):
|
||||
"""look for some books"""
|
||||
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
self.work = models.Work.objects.create(title="Example Work")
|
||||
|
||||
self.first_edition = models.Edition.objects.create(
|
||||
title="Example Edition",
|
||||
parent_work=self.work,
|
||||
isbn_10="0000000000",
|
||||
physical_format="Paperback",
|
||||
published_date=datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
self.second_edition = models.Edition.objects.create(
|
||||
title="Another Edition",
|
||||
parent_work=self.work,
|
||||
isbn_10="1111111111",
|
||||
openlibrary_key="hello",
|
||||
)
|
||||
|
||||
def test_search(self):
|
||||
"""search for a book in the db"""
|
||||
# title/author
|
||||
results = book_search.search("Example")
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0], self.first_edition)
|
||||
|
||||
# isbn
|
||||
results = book_search.search("0000000000")
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0], self.first_edition)
|
||||
|
||||
# identifier
|
||||
results = book_search.search("hello")
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0], self.second_edition)
|
||||
|
||||
def test_isbn_search(self):
|
||||
"""test isbn search"""
|
||||
results = book_search.isbn_search("0000000000")
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0], self.first_edition)
|
||||
|
||||
def test_search_identifiers(self):
|
||||
"""search by unique identifiers"""
|
||||
results = book_search.search_identifiers("hello")
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0], self.second_edition)
|
||||
|
||||
def test_search_title_author(self):
|
||||
"""search by unique identifiers"""
|
||||
results = book_search.search_title_author("Another", min_confidence=0)
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0], self.second_edition)
|
||||
|
||||
def test_format_search_result(self):
|
||||
"""format a search result"""
|
||||
result = book_search.format_search_result(self.first_edition)
|
||||
self.assertEqual(result["title"], "Example Edition")
|
||||
self.assertEqual(result["key"], self.first_edition.remote_id)
|
||||
self.assertEqual(result["year"], 2019)
|
||||
|
||||
result = book_search.format_search_result(self.second_edition)
|
||||
self.assertEqual(result["title"], "Another Edition")
|
||||
self.assertEqual(result["key"], self.second_edition.remote_id)
|
||||
self.assertIsNone(result["year"])
|
||||
|
||||
def test_search_result(self):
|
||||
"""a class that stores info about a search result"""
|
||||
models.Connector.objects.create(
|
||||
identifier="example.com",
|
||||
connector_file="openlibrary",
|
||||
base_url="https://example.com",
|
||||
books_url="https://example.com/books",
|
||||
covers_url="https://example.com/covers",
|
||||
search_url="https://example.com/search?q=",
|
||||
isbn_search_url="https://example.com/isbn?q=",
|
||||
)
|
||||
|
||||
class TestConnector(AbstractMinimalConnector):
|
||||
"""nothing added here"""
|
||||
|
||||
def format_search_result(self, search_result):
|
||||
return search_result
|
||||
|
||||
def get_or_create_book(self, remote_id):
|
||||
pass
|
||||
|
||||
def parse_search_data(self, data):
|
||||
return data
|
||||
|
||||
def format_isbn_search_result(self, search_result):
|
||||
return search_result
|
||||
|
||||
def parse_isbn_search_data(self, data):
|
||||
return data
|
||||
|
||||
test_connector = TestConnector("example.com")
|
||||
result = book_search.SearchResult(
|
||||
title="Title",
|
||||
key="https://example.com/book/1",
|
||||
author="Author Name",
|
||||
year="1850",
|
||||
connector=test_connector,
|
||||
)
|
||||
# there's really not much to test here, it's just a dataclass
|
||||
self.assertEqual(result.confidence, 1)
|
||||
self.assertEqual(result.title, "Title")
|
21
bookwyrm/tests/validate_html.py
Normal file
21
bookwyrm/tests/validate_html.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
""" html validation on rendered templates """
|
||||
from tidylib import tidy_document
|
||||
|
||||
|
||||
def validate_html(html):
|
||||
"""run tidy on html"""
|
||||
_, errors = tidy_document(
|
||||
html.content,
|
||||
options={
|
||||
"drop-empty-elements": False,
|
||||
"warn-proprietary-attributes": False,
|
||||
},
|
||||
)
|
||||
# idk how else to filter out these unescape amp errs
|
||||
errors = "\n".join(
|
||||
e
|
||||
for e in errors.split("\n")
|
||||
if "&book" not in e and "id and name attribute" not in e
|
||||
)
|
||||
if errors:
|
||||
raise Exception(errors)
|
|
@ -1,11 +1,11 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
from tidylib import tidy_document
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class DashboardViews(TestCase):
|
||||
|
@ -35,8 +35,5 @@ class DashboardViews(TestCase):
|
|||
request.user.is_superuser = True
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content)
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
from tidylib import tidy_document
|
||||
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class EmailBlocklistViews(TestCase):
|
||||
|
@ -38,10 +38,7 @@ class EmailBlocklistViews(TestCase):
|
|||
result = view(request)
|
||||
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_blocklist_page_post(self):
|
||||
|
@ -54,10 +51,7 @@ class EmailBlocklistViews(TestCase):
|
|||
result = view(request)
|
||||
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
self.assertTrue(
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
""" test for app action functionality """
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
from tidylib import tidy_document
|
||||
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.template.response import TemplateResponse
|
||||
|
@ -9,6 +8,7 @@ from django.test import TestCase
|
|||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import forms, models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class FederationViews(TestCase):
|
||||
|
@ -48,16 +48,7 @@ class FederationViews(TestCase):
|
|||
request.user.is_superuser = True
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(
|
||||
html.content,
|
||||
options={
|
||||
"drop-empty-elements": False,
|
||||
"warn-proprietary-attributes": False,
|
||||
},
|
||||
)
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_instance_page(self):
|
||||
|
@ -70,10 +61,7 @@ class FederationViews(TestCase):
|
|||
|
||||
result = view(request, server.id)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_server_page_block(self):
|
||||
|
@ -162,10 +150,7 @@ class FederationViews(TestCase):
|
|||
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content)
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_add_view_post_create(self):
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
from tidylib import tidy_document
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class IPBlocklistViews(TestCase):
|
||||
|
@ -37,8 +37,5 @@ class IPBlocklistViews(TestCase):
|
|||
result = view(request)
|
||||
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
""" test for app action functionality """
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
from tidylib import tidy_document
|
||||
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import forms, models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class ReportViews(TestCase):
|
||||
|
@ -44,16 +44,7 @@ class ReportViews(TestCase):
|
|||
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(
|
||||
html.content,
|
||||
options={
|
||||
"drop-empty-elements": False,
|
||||
"warn-proprietary-attributes": False,
|
||||
},
|
||||
)
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_reports_page_with_data(self):
|
||||
|
@ -66,16 +57,7 @@ class ReportViews(TestCase):
|
|||
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(
|
||||
html.content,
|
||||
options={
|
||||
"drop-empty-elements": False,
|
||||
"warn-proprietary-attributes": False,
|
||||
},
|
||||
)
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_report_page(self):
|
||||
|
@ -89,10 +71,7 @@ class ReportViews(TestCase):
|
|||
result = view(request, report.id)
|
||||
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_report_comment(self):
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
from tidylib import tidy_document
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.template.response import TemplateResponse
|
||||
|
@ -8,6 +7,7 @@ from django.test import TestCase
|
|||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class UserAdminViews(TestCase):
|
||||
|
@ -36,10 +36,7 @@ class UserAdminViews(TestCase):
|
|||
request.user.is_superuser = True
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_user_admin_page(self):
|
||||
|
@ -52,10 +49,7 @@ class UserAdminViews(TestCase):
|
|||
result = view(request, self.local_user.id)
|
||||
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||
|
@ -77,10 +71,7 @@ class UserAdminViews(TestCase):
|
|||
result = view(request, self.local_user.id)
|
||||
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
|
||||
self.assertEqual(
|
||||
list(self.local_user.groups.values_list("name", flat=True)), ["editor"]
|
||||
|
|
1
bookwyrm/tests/views/books/__init__.py
Normal file
1
bookwyrm/tests/views/books/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from . import *
|
228
bookwyrm/tests/views/books/test_book.py
Normal file
228
bookwyrm/tests/views/books/test_book.py
Normal file
|
@ -0,0 +1,228 @@
|
|||
""" test for app action functionality """
|
||||
from io import BytesIO
|
||||
import pathlib
|
||||
from unittest.mock import patch
|
||||
from PIL import Image
|
||||
|
||||
import responses
|
||||
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.http import Http404
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import timezone
|
||||
|
||||
from bookwyrm import forms, models, views
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class BookViews(TestCase):
|
||||
"""books books books"""
|
||||
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
):
|
||||
self.local_user = models.User.objects.create_user(
|
||||
"mouse@local.com",
|
||||
"mouse@mouse.com",
|
||||
"mouseword",
|
||||
local=True,
|
||||
localname="mouse",
|
||||
remote_id="https://example.com/users/mouse",
|
||||
)
|
||||
self.group = Group.objects.create(name="editor")
|
||||
self.group.permissions.add(
|
||||
Permission.objects.create(
|
||||
name="edit_book",
|
||||
codename="edit_book",
|
||||
content_type=ContentType.objects.get_for_model(models.User),
|
||||
).id
|
||||
)
|
||||
self.work = models.Work.objects.create(title="Test Work")
|
||||
self.book = models.Edition.objects.create(
|
||||
title="Example Edition",
|
||||
remote_id="https://example.com/book/1",
|
||||
parent_work=self.work,
|
||||
)
|
||||
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_book_page(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Book.as_view()
|
||||
models.ReadThrough.objects.create(
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
start_date=timezone.now(),
|
||||
)
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.book.id)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
|
||||
is_api.return_value = True
|
||||
result = view(request, self.book.id)
|
||||
self.assertIsInstance(result, ActivitypubResponse)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
|
||||
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
||||
def test_book_page_statuses(self, *_):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Book.as_view()
|
||||
|
||||
review = models.Review.objects.create(
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
content="hi",
|
||||
)
|
||||
|
||||
comment = models.Comment.objects.create(
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
content="hi",
|
||||
)
|
||||
|
||||
quote = models.Quotation.objects.create(
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
content="hi",
|
||||
quote="wow",
|
||||
)
|
||||
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.book.id, user_statuses="review")
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(result.context_data["statuses"].object_list[0], review)
|
||||
|
||||
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.book.id, user_statuses="comment")
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(result.context_data["statuses"].object_list[0], comment)
|
||||
|
||||
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.book.id, user_statuses="quotation")
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(result.context_data["statuses"].object_list[0], quote)
|
||||
|
||||
def test_book_page_invalid_id(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Book.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
with self.assertRaises(Http404):
|
||||
view(request, 0)
|
||||
|
||||
def test_book_page_work_id(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Book.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.work.id)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(result.context_data["book"], self.book)
|
||||
|
||||
def test_upload_cover_file(self):
|
||||
"""add a cover via file upload"""
|
||||
self.assertFalse(self.book.cover)
|
||||
image_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../../../static/images/default_avi.jpg"
|
||||
)
|
||||
|
||||
form = forms.CoverForm(instance=self.book)
|
||||
# pylint: disable=consider-using-with
|
||||
form.data["cover"] = SimpleUploadedFile(
|
||||
image_file, open(image_file, "rb").read(), content_type="image/jpeg"
|
||||
)
|
||||
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
with patch(
|
||||
"bookwyrm.models.activitypub_mixin.broadcast_task.delay"
|
||||
) as delay_mock:
|
||||
views.upload_cover(request, self.book.id)
|
||||
self.assertEqual(delay_mock.call_count, 1)
|
||||
|
||||
self.book.refresh_from_db()
|
||||
self.assertTrue(self.book.cover)
|
||||
|
||||
@responses.activate
|
||||
def test_upload_cover_url(self):
|
||||
"""add a cover via url"""
|
||||
self.assertFalse(self.book.cover)
|
||||
form = forms.CoverForm(instance=self.book)
|
||||
form.data["cover-url"] = _setup_cover_url()
|
||||
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
with patch(
|
||||
"bookwyrm.models.activitypub_mixin.broadcast_task.delay"
|
||||
) as delay_mock:
|
||||
views.upload_cover(request, self.book.id)
|
||||
self.assertEqual(delay_mock.call_count, 1)
|
||||
|
||||
self.book.refresh_from_db()
|
||||
self.assertTrue(self.book.cover)
|
||||
|
||||
def test_add_description(self):
|
||||
"""add a book description"""
|
||||
self.local_user.groups.add(self.group)
|
||||
request = self.factory.post("", {"description": "new description hi"})
|
||||
request.user = self.local_user
|
||||
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||
views.add_description(request, self.book.id)
|
||||
|
||||
self.book.refresh_from_db()
|
||||
self.assertEqual(self.book.description, "new description hi")
|
||||
self.assertEqual(self.book.last_edited_by, self.local_user)
|
||||
|
||||
|
||||
def _setup_cover_url():
|
||||
"""creates cover url mock"""
|
||||
cover_url = "http://example.com"
|
||||
image_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../../../static/images/default_avi.jpg"
|
||||
)
|
||||
image = Image.open(image_file)
|
||||
output = BytesIO()
|
||||
image.save(output, format=image.format)
|
||||
responses.add(
|
||||
responses.GET,
|
||||
cover_url,
|
||||
body=output.getvalue(),
|
||||
status=200,
|
||||
)
|
||||
return cover_url
|
|
@ -1,25 +1,19 @@
|
|||
""" test for app action functionality """
|
||||
from io import BytesIO
|
||||
import pathlib
|
||||
from unittest.mock import patch
|
||||
|
||||
from PIL import Image
|
||||
import responses
|
||||
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.http import Http404
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import timezone
|
||||
|
||||
from bookwyrm import forms, models, views
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
from bookwyrm.tests.views.books.test_book import _setup_cover_url
|
||||
|
||||
|
||||
class BookViews(TestCase):
|
||||
class EditBookViews(TestCase):
|
||||
"""books books books"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -53,102 +47,6 @@ class BookViews(TestCase):
|
|||
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_book_page(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Book.as_view()
|
||||
models.ReadThrough.objects.create(
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
start_date=timezone.now(),
|
||||
)
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.views.books.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.book.id)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
result.render()
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
with patch("bookwyrm.views.books.is_api_request") as is_api:
|
||||
is_api.return_value = True
|
||||
result = view(request, self.book.id)
|
||||
self.assertIsInstance(result, ActivitypubResponse)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
|
||||
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
||||
def test_book_page_statuses(self, *_):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Book.as_view()
|
||||
|
||||
review = models.Review.objects.create(
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
content="hi",
|
||||
)
|
||||
|
||||
comment = models.Comment.objects.create(
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
content="hi",
|
||||
)
|
||||
|
||||
quote = models.Quotation.objects.create(
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
content="hi",
|
||||
quote="wow",
|
||||
)
|
||||
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.views.books.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.book.id, user_statuses="review")
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
result.render()
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(result.context_data["statuses"].object_list[0], review)
|
||||
|
||||
with patch("bookwyrm.views.books.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.book.id, user_statuses="comment")
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
result.render()
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(result.context_data["statuses"].object_list[0], comment)
|
||||
|
||||
with patch("bookwyrm.views.books.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.book.id, user_statuses="quotation")
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
result.render()
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(result.context_data["statuses"].object_list[0], quote)
|
||||
|
||||
def test_book_page_invalid_id(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Book.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.views.books.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
with self.assertRaises(Http404):
|
||||
view(request, 0)
|
||||
|
||||
def test_book_page_work_id(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Book.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.views.books.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.work.id)
|
||||
result.render()
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(result.context_data["book"], self.book)
|
||||
|
||||
def test_edit_book_page(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.EditBook.as_view()
|
||||
|
@ -157,7 +55,7 @@ class BookViews(TestCase):
|
|||
request.user.is_superuser = True
|
||||
result = view(request, self.book.id)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
result.render()
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_edit_book(self):
|
||||
|
@ -188,7 +86,7 @@ class BookViews(TestCase):
|
|||
request.user = self.local_user
|
||||
|
||||
result = view(request, self.book.id)
|
||||
result.render()
|
||||
validate_html(result.render())
|
||||
|
||||
# the changes haven't been saved yet
|
||||
self.book.refresh_from_db()
|
||||
|
@ -283,29 +181,12 @@ class BookViews(TestCase):
|
|||
self.assertEqual(book.authors.first().name, "Sappho")
|
||||
self.assertEqual(book.authors.first(), book.parent_work.authors.first())
|
||||
|
||||
def _setup_cover_url(self):
|
||||
cover_url = "http://example.com"
|
||||
image_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../../static/images/default_avi.jpg"
|
||||
)
|
||||
image = Image.open(image_file)
|
||||
output = BytesIO()
|
||||
image.save(output, format=image.format)
|
||||
responses.add(
|
||||
responses.GET,
|
||||
cover_url,
|
||||
body=output.getvalue(),
|
||||
status=200,
|
||||
)
|
||||
return cover_url
|
||||
|
||||
@responses.activate
|
||||
def test_create_book_upload_cover_url(self):
|
||||
"""create an entirely new book and work with cover url"""
|
||||
self.assertFalse(self.book.cover)
|
||||
view = views.ConfirmEditBook.as_view()
|
||||
self.local_user.groups.add(self.group)
|
||||
cover_url = self._setup_cover_url()
|
||||
cover_url = _setup_cover_url()
|
||||
|
||||
form = forms.EditionForm()
|
||||
form.data["title"] = "New Title"
|
||||
|
@ -322,59 +203,3 @@ class BookViews(TestCase):
|
|||
|
||||
self.book.refresh_from_db()
|
||||
self.assertTrue(self.book.cover)
|
||||
|
||||
def test_upload_cover_file(self):
|
||||
"""add a cover via file upload"""
|
||||
self.assertFalse(self.book.cover)
|
||||
image_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../../static/images/default_avi.jpg"
|
||||
)
|
||||
|
||||
form = forms.CoverForm(instance=self.book)
|
||||
form.data["cover"] = SimpleUploadedFile(
|
||||
image_file, open(image_file, "rb").read(), content_type="image/jpeg"
|
||||
)
|
||||
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
with patch(
|
||||
"bookwyrm.models.activitypub_mixin.broadcast_task.delay"
|
||||
) as delay_mock:
|
||||
views.upload_cover(request, self.book.id)
|
||||
self.assertEqual(delay_mock.call_count, 1)
|
||||
|
||||
self.book.refresh_from_db()
|
||||
self.assertTrue(self.book.cover)
|
||||
|
||||
@responses.activate
|
||||
def test_upload_cover_url(self):
|
||||
"""add a cover via url"""
|
||||
self.assertFalse(self.book.cover)
|
||||
form = forms.CoverForm(instance=self.book)
|
||||
form.data["cover-url"] = self._setup_cover_url()
|
||||
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
with patch(
|
||||
"bookwyrm.models.activitypub_mixin.broadcast_task.delay"
|
||||
) as delay_mock:
|
||||
views.upload_cover(request, self.book.id)
|
||||
self.assertEqual(delay_mock.call_count, 1)
|
||||
|
||||
self.book.refresh_from_db()
|
||||
self.assertTrue(self.book.cover)
|
||||
|
||||
def test_add_description(self):
|
||||
"""add a book description"""
|
||||
self.local_user.groups.add(self.group)
|
||||
request = self.factory.post("", {"description": "new description hi"})
|
||||
request.user = self.local_user
|
||||
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||
views.add_description(request, self.book.id)
|
||||
|
||||
self.book.refresh_from_db()
|
||||
self.assertEqual(self.book.description, "new description hi")
|
||||
self.assertEqual(self.book.last_edited_by, self.local_user)
|
|
@ -7,6 +7,7 @@ from django.test.client import RequestFactory
|
|||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class BookViews(TestCase):
|
||||
|
@ -40,11 +41,11 @@ class BookViews(TestCase):
|
|||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Editions.as_view()
|
||||
request = self.factory.get("")
|
||||
with patch("bookwyrm.views.editions.is_api_request") as is_api:
|
||||
with patch("bookwyrm.views.books.editions.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.work.id)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
result.render()
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertTrue("paperback" in result.context_data["formats"])
|
||||
|
||||
|
@ -57,11 +58,11 @@ class BookViews(TestCase):
|
|||
)
|
||||
view = views.Editions.as_view()
|
||||
request = self.factory.get("")
|
||||
with patch("bookwyrm.views.editions.is_api_request") as is_api:
|
||||
with patch("bookwyrm.views.books.editions.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.work.id)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
result.render()
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(len(result.context_data["editions"].object_list), 2)
|
||||
self.assertEqual(len(result.context_data["formats"]), 2)
|
||||
|
@ -69,26 +70,26 @@ class BookViews(TestCase):
|
|||
self.assertTrue("okay" in result.context_data["formats"])
|
||||
|
||||
request = self.factory.get("", {"q": "fish"})
|
||||
with patch("bookwyrm.views.editions.is_api_request") as is_api:
|
||||
with patch("bookwyrm.views.books.editions.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.work.id)
|
||||
result.render()
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(len(result.context_data["editions"].object_list), 1)
|
||||
|
||||
request = self.factory.get("", {"q": "okay"})
|
||||
with patch("bookwyrm.views.editions.is_api_request") as is_api:
|
||||
with patch("bookwyrm.views.books.editions.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.work.id)
|
||||
result.render()
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(len(result.context_data["editions"].object_list), 1)
|
||||
|
||||
request = self.factory.get("", {"format": "okay"})
|
||||
with patch("bookwyrm.views.editions.is_api_request") as is_api:
|
||||
with patch("bookwyrm.views.books.editions.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
result = view(request, self.work.id)
|
||||
result.render()
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(len(result.context_data["editions"].object_list), 1)
|
||||
|
||||
|
@ -96,7 +97,7 @@ class BookViews(TestCase):
|
|||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Editions.as_view()
|
||||
request = self.factory.get("")
|
||||
with patch("bookwyrm.views.editions.is_api_request") as is_api:
|
||||
with patch("bookwyrm.views.books.editions.is_api_request") as is_api:
|
||||
is_api.return_value = True
|
||||
result = view(request, self.work.id)
|
||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
@ -72,6 +72,7 @@ class InboxCreate(TestCase):
|
|||
self.assertEqual(status.quote, "quote body")
|
||||
self.assertEqual(status.content, "commentary")
|
||||
self.assertEqual(status.user, self.local_user)
|
||||
self.assertEqual(status.thread_id, status.id)
|
||||
|
||||
# while we're here, lets ensure we avoid dupes
|
||||
views.inbox.activity_task(activity)
|
||||
|
@ -144,6 +145,7 @@ class InboxCreate(TestCase):
|
|||
status = models.Status.objects.last()
|
||||
self.assertEqual(status.content, "test content in note")
|
||||
self.assertEqual(status.reply_parent, parent_status)
|
||||
self.assertEqual(status.thread_id, parent_status.id)
|
||||
self.assertTrue(models.Notification.objects.filter(user=self.local_user))
|
||||
self.assertEqual(models.Notification.objects.get().notification_type, "REPLY")
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
from tidylib import tidy_document
|
||||
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
|
||||
|
@ -46,10 +46,7 @@ class BlockViews(TestCase):
|
|||
request.user = self.local_user
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content)
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_block_post(self, _):
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
from tidylib import tidy_document
|
||||
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class ChangePasswordViews(TestCase):
|
||||
|
@ -35,10 +35,7 @@ class ChangePasswordViews(TestCase):
|
|||
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content)
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_password_change(self):
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
""" test for app action functionality """
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
from tidylib import tidy_document
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.template.response import TemplateResponse
|
||||
|
@ -9,6 +8,7 @@ from django.test import TestCase
|
|||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import forms, models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
@patch("bookwyrm.suggested_users.remove_user_task.delay")
|
||||
|
@ -53,10 +53,7 @@ class DeleteUserViews(TestCase):
|
|||
request.user = self.local_user
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content)
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task")
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import pathlib
|
||||
from unittest.mock import patch
|
||||
from PIL import Image
|
||||
from tidylib import tidy_document
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.files.base import ContentFile
|
||||
|
@ -12,6 +11,7 @@ from django.test import TestCase
|
|||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import forms, models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
@patch("bookwyrm.suggested_users.remove_user_task.delay")
|
||||
|
@ -58,10 +58,7 @@ class EditUserViews(TestCase):
|
|||
request.user = self.local_user
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content)
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_edit_user(self, _):
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
from tidylib import tidy_document
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.template.response import TemplateResponse
|
||||
|
@ -8,6 +7,7 @@ from django.test import TestCase
|
|||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
class DirectoryViews(TestCase):
|
||||
|
@ -52,16 +52,7 @@ class DirectoryViews(TestCase):
|
|||
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(
|
||||
html.content,
|
||||
options={
|
||||
"drop-empty-elements": False,
|
||||
"warn-proprietary-attributes": False,
|
||||
},
|
||||
)
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_directory_page_empty(self):
|
||||
|
@ -72,10 +63,7 @@ class DirectoryViews(TestCase):
|
|||
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_directory_page_logged_out(self):
|
||||
|
|
|
@ -29,9 +29,6 @@ class GetStartedViews(TestCase):
|
|||
title="Example Edition",
|
||||
remote_id="https://example.com/book/1",
|
||||
)
|
||||
models.Connector.objects.create(
|
||||
identifier="self", connector_file="self_connector", local=True
|
||||
)
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_profile_view(self, *_):
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
from tidylib import tidy_document
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.http import Http404
|
||||
|
@ -10,6 +9,7 @@ from django.test.client import RequestFactory
|
|||
from django.utils import timezone
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class GoalViews(TestCase):
|
||||
|
@ -62,16 +62,7 @@ class GoalViews(TestCase):
|
|||
request.user = self.local_user
|
||||
|
||||
result = view(request, self.local_user.localname, self.year)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(
|
||||
html.content,
|
||||
options={
|
||||
"drop-empty-elements": False,
|
||||
"warn-proprietary-attributes": False,
|
||||
},
|
||||
)
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
|
||||
def test_goal_page_anonymous(self):
|
||||
|
@ -102,16 +93,7 @@ class GoalViews(TestCase):
|
|||
request.user = self.rat
|
||||
|
||||
result = view(request, self.local_user.localname, timezone.now().year)
|
||||
html = result.render()
|
||||
_, errors = tidy_document(
|
||||
html.content,
|
||||
options={
|
||||
"drop-empty-elements": False,
|
||||
"warn-proprietary-attributes": False,
|
||||
},
|
||||
)
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
validate_html(result.render())
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
|
||||
def test_goal_page_private(self):
|
||||
|
|
|
@ -34,9 +34,6 @@ class IsbnViews(TestCase):
|
|||
remote_id="https://example.com/book/1",
|
||||
parent_work=self.work,
|
||||
)
|
||||
models.Connector.objects.create(
|
||||
identifier="self", connector_file="self_connector", local=True
|
||||
)
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_isbn_json_response(self):
|
||||
|
@ -51,4 +48,4 @@ class IsbnViews(TestCase):
|
|||
data = json.loads(response.content)
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(data[0]["title"], "Test Book")
|
||||
self.assertEqual(data[0]["key"], "https://%s/book/%d" % (DOMAIN, self.book.id))
|
||||
self.assertEqual(data[0]["key"], f"https://{DOMAIN}/book/{self.book.id}")
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.test.client import RequestFactory
|
|||
|
||||
from bookwyrm import models
|
||||
from bookwyrm import views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class NotificationViews(TestCase):
|
||||
|
@ -24,16 +25,53 @@ class NotificationViews(TestCase):
|
|||
local=True,
|
||||
localname="mouse",
|
||||
)
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||
self.status = models.Status.objects.create(
|
||||
content="hi",
|
||||
user=self.local_user,
|
||||
)
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_notifications_page(self):
|
||||
def test_notifications_page_empty(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Notifications.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
result.render()
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_notifications_page_notifications(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
models.Notification.objects.create(
|
||||
user=self.local_user,
|
||||
notification_type="FAVORITE",
|
||||
related_status=self.status,
|
||||
)
|
||||
models.Notification.objects.create(
|
||||
user=self.local_user,
|
||||
notification_type="BOOST",
|
||||
related_status=self.status,
|
||||
)
|
||||
models.Notification.objects.create(
|
||||
user=self.local_user,
|
||||
notification_type="MENTION",
|
||||
related_status=self.status,
|
||||
)
|
||||
self.status.reply_parent = self.status
|
||||
self.status.save(broadcast=False)
|
||||
models.Notification.objects.create(
|
||||
user=self.local_user,
|
||||
notification_type="REPLY",
|
||||
related_status=self.status,
|
||||
)
|
||||
view = views.Notifications.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_clear_notifications(self):
|
||||
|
|
|
@ -58,6 +58,9 @@ class ReadingViews(TestCase):
|
|||
"post-status": True,
|
||||
"privacy": "followers",
|
||||
"start_date": "2020-01-05",
|
||||
"book": self.book.id,
|
||||
"mention_books": self.book.id,
|
||||
"user": self.local_user.id,
|
||||
},
|
||||
)
|
||||
request.user = self.local_user
|
||||
|
@ -77,6 +80,45 @@ class ReadingViews(TestCase):
|
|||
self.assertEqual(readthrough.user, self.local_user)
|
||||
self.assertEqual(readthrough.book, self.book)
|
||||
|
||||
def test_start_reading_with_comment(self, *_):
|
||||
"""begin a book"""
|
||||
shelf = self.local_user.shelf_set.get(identifier=models.Shelf.READING)
|
||||
self.assertFalse(shelf.books.exists())
|
||||
self.assertFalse(models.Status.objects.exists())
|
||||
|
||||
request = self.factory.post(
|
||||
"",
|
||||
{
|
||||
"post-status": True,
|
||||
"privacy": "followers",
|
||||
"start_date": "2020-01-05",
|
||||
"content": "hello hello",
|
||||
"book": self.book.id,
|
||||
"mention_books": self.book.id,
|
||||
"user": self.local_user.id,
|
||||
"reading_status": "reading",
|
||||
},
|
||||
)
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||
views.ReadingStatus.as_view()(request, "start", self.book.id)
|
||||
|
||||
self.assertEqual(shelf.books.get(), self.book)
|
||||
|
||||
status = models.Comment.objects.get()
|
||||
self.assertEqual(status.user, self.local_user)
|
||||
self.assertEqual(status.book, self.book)
|
||||
self.assertFalse(status.mention_books.exists())
|
||||
self.assertEqual(status.privacy, "followers")
|
||||
self.assertEqual(status.content, "<p>hello hello</p>")
|
||||
self.assertEqual(status.reading_status, "reading")
|
||||
|
||||
readthrough = models.ReadThrough.objects.get()
|
||||
self.assertIsNotNone(readthrough.start_date)
|
||||
self.assertIsNone(readthrough.finish_date)
|
||||
self.assertEqual(readthrough.user, self.local_user)
|
||||
self.assertEqual(readthrough.book, self.book)
|
||||
|
||||
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
|
||||
@patch("bookwyrm.activitystreams.remove_book_statuses_task.delay")
|
||||
def test_start_reading_reshelve(self, *_):
|
||||
|
@ -203,3 +245,42 @@ class ReadingViews(TestCase):
|
|||
self.assertEqual(readthrough.finish_date.day, 7)
|
||||
self.assertEqual(readthrough.book, self.book)
|
||||
self.assertEqual(readthrough.user, self.local_user)
|
||||
|
||||
def test_update_progress_comment(self, *_):
|
||||
"""update progress with commentary"""
|
||||
readthrough = models.ReadThrough.objects.create(
|
||||
user=self.local_user, start_date=timezone.now(), book=self.book
|
||||
)
|
||||
request = self.factory.post(
|
||||
"",
|
||||
{
|
||||
"post-status": True,
|
||||
"privacy": "followers",
|
||||
"start_date": "2020-01-05",
|
||||
"content": "hello hello",
|
||||
"book": self.book.id,
|
||||
"mention_books": self.book.id,
|
||||
"user": self.local_user.id,
|
||||
"id": readthrough.id,
|
||||
"progress": 23,
|
||||
"progress_mode": "PCT",
|
||||
},
|
||||
)
|
||||
request.user = self.local_user
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||
views.update_progress(request, self.book.id)
|
||||
|
||||
status = models.Comment.objects.get()
|
||||
self.assertEqual(status.user, self.local_user)
|
||||
self.assertEqual(status.book, self.book)
|
||||
self.assertFalse(status.mention_books.exists())
|
||||
self.assertEqual(status.privacy, "followers")
|
||||
self.assertEqual(status.content, "<p>hello hello</p>")
|
||||
self.assertIsNone(status.reading_status)
|
||||
self.assertEqual(status.progress, 23)
|
||||
self.assertEqual(status.progress_mode, "PCT")
|
||||
|
||||
self.assertIsNotNone(readthrough.start_date)
|
||||
self.assertIsNone(readthrough.finish_date)
|
||||
self.assertEqual(readthrough.user, self.local_user)
|
||||
self.assertEqual(readthrough.book, self.book)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue