Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-10-02 18:26:38 -07:00
commit 1b987542a4
124 changed files with 6808 additions and 5993 deletions

View file

@ -24,5 +24,5 @@ jobs:
--rule 'meta_viewport: true' \ --rule 'meta_viewport: true' \
--rule 'no_autofocus: true' \ --rule 'no_autofocus: true' \
--rule 'tabindex_no_positive: 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 bookwyrm/templates

View file

@ -54,6 +54,7 @@ class Edition(Book):
asin: str = "" asin: str = ""
pages: int = None pages: int = None
physicalFormat: str = "" physicalFormat: str = ""
physicalFormatDetail: str = ""
publishers: List[str] = field(default_factory=lambda: []) publishers: List[str] = field(default_factory=lambda: [])
editionRank: int = 0 editionRank: int = 0

156
bookwyrm/book_search.py Normal file
View 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

View file

@ -3,4 +3,4 @@ from .settings import CONNECTORS
from .abstract_connector import ConnectorException from .abstract_connector import ConnectorException
from .abstract_connector import get_data, get_image 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

View file

@ -1,6 +1,5 @@
""" functionality outline for a book data connector """ """ functionality outline for a book data connector """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import asdict, dataclass
import logging import logging
from django.db import transaction from django.db import transaction
@ -9,6 +8,7 @@ from requests.exceptions import RequestException
from bookwyrm import activitypub, models, settings from bookwyrm import activitypub, models, settings
from .connector_manager import load_more_data, ConnectorException from .connector_manager import load_more_data, ConnectorException
from .format_mappings import format_mappings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,7 +31,6 @@ class AbstractMinimalConnector(ABC):
"isbn_search_url", "isbn_search_url",
"name", "name",
"identifier", "identifier",
"local",
] ]
for field in self_fields: for field in self_fields:
setattr(self, field, getattr(info, field)) setattr(self, field, getattr(info, field))
@ -267,32 +266,6 @@ def get_image(url, timeout=10):
return resp 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: class Mapping:
"""associate a local database field with a field in an external dataset""" """associate a local database field with a field in an external dataset"""
@ -312,3 +285,25 @@ class Mapping:
return self.formatter(value) return self.formatter(value)
except: # pylint: disable=bare-except except: # pylint: disable=bare-except
return None 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

View file

@ -1,6 +1,7 @@
""" using another bookwyrm instance as a source of book data """ """ using another bookwyrm instance as a source of book data """
from bookwyrm import activitypub, models 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): class Connector(AbstractMinimalConnector):

View file

@ -10,7 +10,7 @@ from django.db.models import signals
from requests import HTTPError from requests import HTTPError
from bookwyrm import models from bookwyrm import book_search, models
from bookwyrm.tasks import app from bookwyrm.tasks import app
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -55,7 +55,7 @@ def search(query, min_confidence=0.1, return_first=False):
# if we found anything, return it # if we found anything, return it
return result_set[0] return result_set[0]
if result_set or connector.local: if result_set:
results.append( results.append(
{ {
"connector": connector, "connector": connector,
@ -71,22 +71,13 @@ def search(query, min_confidence=0.1, return_first=False):
return results 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): def first_search_result(query, min_confidence=0.1):
"""search until you find a result that fits""" """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 return search(query, min_confidence=min_confidence, return_first=True) or None

View 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",
}

View file

@ -2,13 +2,14 @@
import re import re
from bookwyrm import models 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 .abstract_connector import get_data
from .connector_manager import ConnectorException from .connector_manager import ConnectorException
class Connector(AbstractConnector): class Connector(AbstractConnector):
"""instantiate a connector for OL""" """instantiate a connector for inventaire"""
def __init__(self, identifier): def __init__(self, identifier):
super().__init__(identifier) super().__init__(identifier)

View file

@ -2,8 +2,9 @@
import re import re
from bookwyrm import models from bookwyrm import models
from .abstract_connector import AbstractConnector, SearchResult, Mapping from bookwyrm.book_search import SearchResult
from .abstract_connector import get_data from .abstract_connector import AbstractConnector, Mapping
from .abstract_connector import get_data, infer_physical_format, unique_physical_format
from .connector_manager import ConnectorException from .connector_manager import ConnectorException
from .openlibrary_languages import languages from .openlibrary_languages import languages
@ -43,7 +44,16 @@ class Connector(AbstractConnector):
), ),
Mapping("publishedDate", remote_field="publish_date"), Mapping("publishedDate", remote_field="publish_date"),
Mapping("pages", remote_field="number_of_pages"), 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"), Mapping("publishers"),
] ]

View file

@ -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()

View file

@ -1,3 +1,3 @@
""" settings book data connectors """ """ settings book data connectors """
CONNECTORS = ["openlibrary", "inventaire", "self_connector", "bookwyrm_connector"] CONNECTORS = ["openlibrary", "inventaire", "bookwyrm_connector"]

View file

@ -4,7 +4,6 @@ from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from bookwyrm.models import Connector, FederatedServer, SiteSettings, User from bookwyrm.models import Connector, FederatedServer, SiteSettings, User
from bookwyrm.settings import DOMAIN
def init_groups(): def init_groups():
@ -73,19 +72,6 @@ def init_permissions():
def init_connectors(): def init_connectors():
"""access book data sources""" """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( Connector.objects.create(
identifier="bookwyrm.social", identifier="bookwyrm.social",
name="BookWyrm dot Social", name="BookWyrm dot Social",

View 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),
]

View 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),
]

View 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",
),
]

View 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),
]

View file

@ -6,6 +6,7 @@ from django.contrib.postgres.indexes import GinIndex
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Prefetch from django.db.models import Prefetch
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from model_utils import FieldTracker from model_utils import FieldTracker
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from imagekit.models import ImageSpecField from imagekit.models import ImageSpecField
@ -226,6 +227,16 @@ class Work(OrderedCollectionPageMixin, Book):
deserialize_reverse_fields = [("editions", "editions")] deserialize_reverse_fields = [("editions", "editions")]
# https://schema.org/BookFormatType
FormatChoices = [
("AudiobookFormat", _("Audiobook")),
("EBook", _("eBook")),
("GraphicNovel", _("Graphic novel")),
("Hardcover", _("Hardcover")),
("Paperback", _("Paperback")),
]
class Edition(Book): class Edition(Book):
"""an edition of a book""" """an edition of a book"""
@ -243,7 +254,10 @@ class Edition(Book):
max_length=255, blank=True, null=True, deduplication_field=True max_length=255, blank=True, null=True, deduplication_field=True
) )
pages = fields.IntegerField(blank=True, null=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( publishers = fields.ArrayField(
models.CharField(max_length=255), blank=True, default=list models.CharField(max_length=255), blank=True, default=list
) )

View file

@ -14,7 +14,6 @@ class Connector(BookWyrmModel):
identifier = models.CharField(max_length=255, unique=True) identifier = models.CharField(max_length=255, unique=True)
priority = models.IntegerField(default=2) priority = models.IntegerField(default=2)
name = models.CharField(max_length=255, null=True, blank=True) 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) connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)
api_key = models.CharField(max_length=255, null=True, blank=True) api_key = models.CharField(max_length=255, null=True, blank=True)
active = models.BooleanField(default=True) active = models.BooleanField(default=True)

View file

@ -57,6 +57,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
on_delete=models.PROTECT, on_delete=models.PROTECT,
activitypub_field="inReplyTo", activitypub_field="inReplyTo",
) )
thread_id = models.IntegerField(blank=True, null=True)
objects = InheritanceManager() objects = InheritanceManager()
activity_serializer = activitypub.Note activity_serializer = activitypub.Note
@ -68,6 +69,17 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
ordering = ("-published_date",) 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 def delete(self, *args, **kwargs): # pylint: disable=unused-argument
""" "delete" a status""" """ "delete" a status"""
if hasattr(self, "boosted_status"): if hasattr(self, "boosted_status"):

View file

@ -13,7 +13,7 @@ VERSION = "0.0.1"
PAGE_LENGTH = env("PAGE_LENGTH", 15) PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English") DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "e2bc0653" JS_CACHE = "c02929b1"
# email # email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend") EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")

View file

@ -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 /* Dimensions
* @todo These could be in rem. * @todo These could be in rem.
******************************************************************************/ ******************************************************************************/

View 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;
}
}();

View file

@ -22,7 +22,7 @@
</div> </div>
</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 }}"> <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 %} {% if author.aliases or author.born or author.died or author.wikipedia_link or author.openlibrary_key or author.inventaire_id %}

View file

@ -203,7 +203,9 @@
<hr aria-hidden="true"> <hr aria-hidden="true">
<section class="box"> <section class="box">
{% with 0|uuid as controls_uid %}
{% include 'snippets/create_status.html' with book=book hide_cover=True %} {% include 'snippets/create_status.html' with book=book hide_cover=True %}
{% endwith %}
</section> </section>
{% endif %} {% endif %}
<div class="block" id="reviews"> <div class="block" id="reviews">

View file

@ -1,6 +1,7 @@
{% spaceless %} {% spaceless %}
{% load i18n %} {% load i18n %}
{% if book.isbn13 or book.oclc_number or book.asin %}
<dl> <dl>
{% if book.isbn_13 %} {% if book.isbn_13 %}
<div class="is-flex"> <div class="is-flex">
@ -23,4 +24,5 @@
</div> </div>
{% endif %} {% endif %}
</dl> </dl>
{% endif %}
{% endspaceless %} {% endspaceless %}

View 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 %}

View file

@ -1,40 +1,4 @@
{% extends 'layout.html' %}
{% load i18n %} {% 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 %} {% if form.non_field_errors %}
<div class="block"> <div class="block">
@ -42,87 +6,14 @@
</div> </div>
{% endif %} {% endif %}
<form {% csrf_token %}
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>
<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"> <div class="field">
<label class="label" for="id_title">{% trans "Title:" %}</label> <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"> <input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title">
@ -147,20 +38,25 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="field"> <div class="columns">
<label class="label" for="id_series">{% trans "Series:" %}</label> <div class="column is-two-thirds">
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}"> <div class="field">
{% for error in form.series.errors %} <label class="label" for="id_series">{% trans "Series:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> <input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}">
{% endfor %} {% for error in form.series.errors %}
</div> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<div class="field"> </div>
<label class="label" for="id_series_number">{% trans "Series number:" %}</label> </div>
{{ form.series_number }} <div class="column is-one-third">
{% for error in form.series_number.errors %} <div class="field">
<p class="help is-danger">{{ error | escape }}</p> <label class="label" for="id_series_number">{% trans "Series number:" %}</label>
{% endfor %} {{ form.series_number }}
{% for error in form.series_number.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
</div> </div>
<div class="field"> <div class="field">
@ -171,7 +67,12 @@
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</div> </div>
</div>
</section>
<section class="block">
<h2 class="title is-4">{% trans "Publication" %}</h2>
<div class="box">
<div class="field"> <div class="field">
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label> <label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
{{ form.publishers }} {{ form.publishers }}
@ -196,10 +97,12 @@
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</div> </div>
</section> </div>
</section>
<section class="block"> <section class="block">
<h2 class="title is-4">{% trans "Authors" %}</h2> <h2 class="title is-4">{% trans "Authors" %}</h2>
<div class="box">
{% if book.authors.exists %} {% if book.authors.exists %}
<fieldset> <fieldset>
{% for author in book.authors.all %} {% 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 %}> <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> <span class="help">{% trans "Separate multiple values with commas." %}</span>
</div> </div>
</section> </div>
</div> </section>
</div>
<div class="column is-half"> <div class="column is-half">
<section class="block">
<h2 class="title is-4">{% trans "Cover" %}</h2> <h2 class="title is-4">{% trans "Cover" %}</h2>
<div class="columns"> <div class="box">
<div class="column is-3 is-cover"> <div class="columns">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-xl-mobile is-w-auto-tablet' size_mobile='xlarge' size='large' %} {% if book.cover %}
</div> <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="column">
<div class="block">
<div class="field"> <div class="field">
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label> <label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
{{ form.cover }} {{ form.cover }}
@ -248,15 +155,32 @@
</div> </div>
</div> </div>
</div> </div>
</section>
<div class="block"> <section class="block">
<h2 class="title is-4">{% trans "Physical Properties" %}</h2> <h2 class="title is-4">{% trans "Physical Properties" %}</h2>
<div class="field"> <div class="box">
<label class="label" for="id_physical_format">{% trans "Format:" %}</label> <div class="columns">
{{ form.physical_format }} <div class="column is-one-third">
{% for error in form.physical_format.errors %} <div class="field">
<p class="help is-danger">{{ error | escape }}</p> <label class="label" for="id_physical_format">{% trans "Format:" %}</label>
{% endfor %} <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>
<div class="field"> <div class="field">
@ -267,9 +191,11 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</section>
<div class="block"> <section class="block">
<h2 class="title is-4">{% trans "Book Identifiers" %}</h2> <h2 class="title is-4">{% trans "Book Identifiers" %}</h2>
<div class="box">
<div class="field"> <div class="field">
<label class="label" for="id_isbn_13">{% trans "ISBN 13:" %}</label> <label class="label" for="id_isbn_13">{% trans "ISBN 13:" %}</label>
{{ form.isbn_13 }} {{ form.isbn_13 }}
@ -318,15 +244,6 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div> </section>
</div> </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 %}

View file

@ -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 %}

View 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 %}

View file

@ -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> <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> </div>
{% include 'book/edition_filters.html' %} {% include 'book/editions/edition_filters.html' %}
<div class="block"> <div class="block">
{% for book in editions %} {% for book in editions %}

View file

@ -3,29 +3,30 @@
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
{% 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> <p>
{% with format=book.physical_format pages=book.pages %} {% if format and not pages %}
{% if format %} {% blocktrans %}{{ format }}{% endblocktrans %}
{% comment %} {% elif format and pages %}
@todo The bookFormat property is limited to a list of values whereas the book edition is free text. {% blocktrans %}{{ format }}, {{ pages }} pages{% endblocktrans %}
@see https://schema.org/bookFormat {% elif pages %}
{% endcomment %} {% blocktrans %}{{ pages }} pages{% endblocktrans %}
<meta itemprop="bookFormat" content="{{ format }}"> {% endif %}
{% endif %}
{% if pages %}
<meta itemprop="numberOfPages" content="{{ pages }}">
{% endif %}
{% if format and not pages %}
{% blocktrans %}{{ format }}{% endblocktrans %}
{% elif format and pages %}
{% blocktrans %}{{ format }}, {{ pages }} pages{% endblocktrans %}
{% elif pages %}
{% blocktrans %}{{ pages }} pages{% endblocktrans %}
{% endif %}
{% endwith %}
</p> </p>
{% endif %}
{% endwith %}
{% if book.languages %} {% if book.languages %}
{% for language in book.languages %} {% for language in book.languages %}
@ -39,32 +40,34 @@
</p> </p>
{% endif %} {% endif %}
{% 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> <p>
{% with date=book.published_date|naturalday publisher=book.publishers|join:', ' %}
{% if date or book.first_published_date %}
<meta
itemprop="datePublished"
content="{{ book.first_published_date|default:book.published_date|date:'Y-m-d' }}"
>
{% endif %}
{% comment %} {% comment %}
@todo The publisher property needs to be an Organization or a Person. Well be using Thing which is the more generic ancestor. @todo The publisher property needs to be an Organization or a Person. Well be using Thing which is the more generic ancestor.
@see https://schema.org/Publisher @see https://schema.org/Publisher
{% endcomment %} {% endcomment %}
{% if book.publishers %} {% if book.publishers %}
{% for publisher in book.publishers %} {% for publisher in book.publishers %}
<meta itemprop="publisher" content="{{ publisher }}"> <meta itemprop="publisher" content="{{ publisher }}">
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if date and publisher %} {% if date and publisher %}
{% blocktrans %}Published {{ date }} by {{ publisher }}.{% endblocktrans %} {% blocktrans %}Published {{ date }} by {{ publisher }}.{% endblocktrans %}
{% elif date %} {% elif date %}
{% blocktrans %}Published {{ date }}{% endblocktrans %} {% blocktrans %}Published {{ date }}{% endblocktrans %}
{% elif publisher %} {% elif publisher %}
{% blocktrans %}Published by {{ publisher }}.{% endblocktrans %} {% blocktrans %}Published by {{ publisher }}.{% endblocktrans %}
{% endif %} {% endif %}
{% endwith %}
</p> </p>
{% endif %}
{% endwith %}
{% endspaceless %} {% endspaceless %}

View file

@ -1,6 +1,7 @@
{% load status_display %} {% load status_display %}
<div class="block">
<div class="thread-parent is-relative block">
<div class="thread">
{% with depth=depth|add:1 %} {% with depth=depth|add:1 %}
{% if depth <= max_depth and status.reply_parent and direction <= 0 %} {% if depth <= max_depth and status.reply_parent and direction <= 0 %}
{% with direction=-1 %} {% with direction=-1 %}
@ -8,7 +9,9 @@
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% include 'snippets/status/status.html' with status=status main=is_root %} <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 %} {% if depth <= max_depth and direction >= 0 %}
{% for reply in status|replies %} {% for reply in status|replies %}
@ -18,5 +21,5 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endwith %} {% endwith %}
</div>
</div> </div>

View file

@ -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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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>

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View file

@ -8,7 +8,24 @@
<ul class="block"> <ul class="block">
{% for result in local_results.results %} {% for result in local_results.results %}
<li class="pd-4 mb-5"> <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> </li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -43,7 +60,33 @@
<ul class="is-flex-grow-1"> <ul class="is-flex-grow-1">
{% for result in result_set.results %} {% for result in result_set.results %}
<li class="mb-5"> <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> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View file

@ -12,7 +12,7 @@ draft: an existing Status object that is providing default values for input fiel
name="content" name="content"
class="textarea save-draft" class="textarea save-draft"
data-cache-draft="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}" 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 }}" placeholder="{{ placeholder }}"
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}" aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}"
{% if not optional and type != "quotation" and type != "review" %}required{% endif %} {% if not optional and type != "quotation" and type != "review" %}required{% endif %}

View file

@ -19,7 +19,7 @@ reply_parent: the Status object this post will be in reply to, if applicable
name="{{ type }}" name="{{ type }}"
action="/post/{{ type }}" action="/post/{{ type }}"
method="post" method="post"
id="tab_{{ type }}_{{ book.id }}{{ reply_parent.id }}" id="form_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
> >
{% endblock %} {% 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) #} {# fields that go between the content warnings and the content field (ie, quote) #}
{% block pre_content_additions %}{% endblock %} {% 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 %} {% block content_label %}
{% trans "Comment:" %} {% trans "Comment:" %}
{% endblock %} {% endblock %}

View file

@ -1,6 +1,10 @@
{% load i18n %} {% load i18n %}
<div class="content"> <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 %}"> <form method="post" name="goal" action="{% url 'user-goal' request.user.localname year %}">
{% csrf_token %} {% csrf_token %}
@ -20,7 +24,7 @@
<div class="column"> <div class="column">
<label class="label" for="privacy_{{ goal.id }}">{% trans "Goal privacy:" %}</label> <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>
</div> </div>

View file

@ -1,7 +1,7 @@
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}
<div class="select {{ class }}"> <div class="select {{ class }}">
{% firstof uuid 0|uuid as uuid %} {% firstof privacy_uuid 0|uuid as uuid %}
{% if not no_label %} {% if not no_label %}
<label class="is-sr-only" for="privacy_{{ uuid }}">{% trans "Post privacy" %}</label> <label class="is-sr-only" for="privacy_{{ uuid }}">{% trans "Post privacy" %}</label>
{% endif %} {% endif %}

View file

@ -5,7 +5,7 @@
type="number" type="number"
name="progress" name="progress"
class="input" class="input"
id="id_progress_{{ readthrough.id }}" id="id_progress_{{ readthrough.id }}{{ controls_uid }}"
value="{{ readthrough.progress }}" value="{{ readthrough.progress }}"
{% if progress_required %}required{% endif %} {% if progress_required %}required{% endif %}
> >

View file

@ -35,3 +35,7 @@ Finish "<em>{{ book_title }}</em>"
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block form %}
{% include "snippets/reading_modals/form.html" with optional=True type="finish_modal" %}
{% endblock %}

View file

@ -15,3 +15,5 @@
<input type="hidden" name="mention_books" value="{{ book.id }}"> <input type="hidden" name="mention_books" value="{{ book.id }}">
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
{% endblock %} {% endblock %}
{% block form_close %}{% endblock %}

View file

@ -23,10 +23,11 @@
<div id="reading_content_{{ local_uuid }}_{{ uuid }}"> <div id="reading_content_{{ local_uuid }}_{{ uuid }}">
<hr aria-hidden="true"> <hr aria-hidden="true">
<fieldset id="reading_content_fieldset_{{ local_uuid }}_{{ uuid }}"> <fieldset id="reading_content_fieldset_{{ local_uuid }}_{{ uuid }}">
{% comparison_bool controls_text "progress_update" True as optional %} {% block form %}{% endblock %}
{% include "snippets/reading_modals/form.html" with optional=optional %}
</fieldset> </fieldset>
</div> </div>
{% endwith %} {% endwith %}
{% endblock %} {% endblock %}
{% block modal-form-close %}</form>{% endblock %}

View file

@ -12,6 +12,10 @@
{% endblock %} {% endblock %}
{% block reading-dates %} {% 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 %} {% include "snippets/progress_field.html" with progress_required=True %}
{% endblock %} {% endblock %}
{% block form %}
{% include "snippets/reading_modals/form.html" with optional=False type="update_modal" %}
{% endblock %}

View file

@ -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" %}"> <input type="date" name="start_date" class="input" id="start_id_start_date_{{ uuid }}" value="{% now "Y-m-d" %}">
</div> </div>
{% endblock %} {% endblock %}
{% block form %}
{% include "snippets/reading_modals/form.html" with optional=True type="start_modal" %}
{% endblock %}

View file

@ -13,3 +13,7 @@ Want to Read "<em>{{ book_title }}</em>"
<input type="hidden" name="reading_status" value="to-read"> <input type="hidden" name="reading_status" value="to-read">
{% csrf_token %} {% csrf_token %}
{% endblock %} {% endblock %}
{% block form %}
{% include "snippets/reading_modals/form.html" with optional=True type="want_modal" %}
{% endblock %}

View file

@ -10,7 +10,7 @@
</div> </div>
{# Only show progress for editing existing readthroughs #} {# Only show progress for editing existing readthroughs #}
{% if readthrough.id and not readthrough.finish_date %} {% 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" %} {% trans "Progress" %}
</label> </label>
{% include "snippets/progress_field.html" %} {% include "snippets/progress_field.html" %}

View file

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

View file

@ -32,7 +32,7 @@
{% include "snippets/status/header_content.html" %} {% include "snippets/status/header_content.html" %}
</h3> </h3>
<p class="is-size-7 is-flex is-align-items-center"> <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 %} {% if status.progress %}
<span class="ml-1"> <span class="ml-1">
{% if status.progress_mode == 'PG' %} {% if status.progress_mode == 'PG' %}

View file

@ -3,7 +3,10 @@
{% load utilities %} {% load utilities %}
{% block card-header %} {% 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 %} {% include 'snippets/status/header.html' with status=status %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -11,7 +11,7 @@
{% if status.user == request.user %} {% if status.user == request.user %}
{# things you can do to your own statuses #} {# things you can do to your own statuses #}
<li role="menuitem" class="dropdown-item p-0"> <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 %} {% csrf_token %}
<button class="button is-radiusless is-danger is-light is-fullwidth is-small" type="submit"> <button class="button is-radiusless is-danger is-light is-fullwidth is-small" type="submit">
{% trans "Delete status" %} {% trans "Delete status" %}

View file

@ -1,5 +1,6 @@
{% load utilities %}
{% if fallback_url %} {% 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 %} {% endif %}
<button <button
{% if not fallback_url %} {% if not fallback_url %}

View file

@ -119,12 +119,10 @@ class AbstractConnector(TestCase):
@responses.activate @responses.activate
def test_get_or_create_author(self): def test_get_or_create_author(self):
"""load an author""" """load an author"""
self.connector.author_mappings = ( self.connector.author_mappings = [ # pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
[ # pylint: disable=attribute-defined-outside-init Mapping("id"),
Mapping("id"), Mapping("name"),
Mapping("name"), ]
]
)
responses.add( responses.add(
responses.GET, responses.GET,

View file

@ -4,7 +4,7 @@ import responses
from bookwyrm import models from bookwyrm import models
from bookwyrm.connectors import abstract_connector from bookwyrm.connectors import abstract_connector
from bookwyrm.connectors.abstract_connector import Mapping, SearchResult from bookwyrm.connectors.abstract_connector import Mapping
class AbstractConnector(TestCase): class AbstractConnector(TestCase):
@ -53,7 +53,6 @@ class AbstractConnector(TestCase):
self.assertEqual(connector.isbn_search_url, "https://example.com/isbn?q=") self.assertEqual(connector.isbn_search_url, "https://example.com/isbn?q=")
self.assertIsNone(connector.name) self.assertIsNone(connector.name)
self.assertEqual(connector.identifier, "example.com") self.assertEqual(connector.identifier, "example.com")
self.assertFalse(connector.local)
@responses.activate @responses.activate
def test_search(self): def test_search(self):
@ -94,19 +93,6 @@ class AbstractConnector(TestCase):
results = self.test_connector.isbn_search("123456") results = self.test_connector.isbn_search("123456")
self.assertEqual(len(results), 10) 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): def test_create_mapping(self):
"""maps remote fields for book data to bookwyrm activitypub fields""" """maps remote fields for book data to bookwyrm activitypub fields"""
mapping = Mapping("isbn") mapping = Mapping("isbn")

View file

@ -4,8 +4,8 @@ import pathlib
from django.test import TestCase from django.test import TestCase
from bookwyrm import models from bookwyrm import models
from bookwyrm.book_search import SearchResult
from bookwyrm.connectors.bookwyrm_connector import Connector from bookwyrm.connectors.bookwyrm_connector import Connector
from bookwyrm.connectors.abstract_connector import SearchResult
class BookWyrmConnector(TestCase): class BookWyrmConnector(TestCase):

View file

@ -5,7 +5,6 @@ import responses
from bookwyrm import models from bookwyrm import models
from bookwyrm.connectors import connector_manager from bookwyrm.connectors import connector_manager
from bookwyrm.connectors.bookwyrm_connector import Connector as BookWyrmConnector from bookwyrm.connectors.bookwyrm_connector import Connector as BookWyrmConnector
from bookwyrm.connectors.self_connector import Connector as SelfConnector
class ConnectorManager(TestCase): class ConnectorManager(TestCase):
@ -15,28 +14,16 @@ class ConnectorManager(TestCase):
"""we'll need some books and a connector info entry""" """we'll need some books and a connector info entry"""
self.work = models.Work.objects.create(title="Example Work") 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" title="Example Edition", parent_work=self.work, isbn_10="0000000000"
) )
self.edition = models.Edition.objects.create( self.edition = models.Edition.objects.create(
title="Another Edition", parent_work=self.work, isbn_10="1111111111" title="Another Edition", parent_work=self.work, isbn_10="1111111111"
) )
self.connector = models.Connector.objects.create(
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( self.remote_connector = models.Connector.objects.create(
identifier="test_connector_remote", identifier="test_connector_remote",
priority=1, priority=1,
local=False,
connector_file="bookwyrm_connector", connector_file="bookwyrm_connector",
base_url="http://fake.ciom/", base_url="http://fake.ciom/",
books_url="http://fake.ciom/", books_url="http://fake.ciom/",
@ -59,23 +46,22 @@ class ConnectorManager(TestCase):
def test_get_connectors(self): def test_get_connectors(self):
"""load all connectors""" """load all connectors"""
connectors = list(connector_manager.get_connectors()) connectors = list(connector_manager.get_connectors())
self.assertEqual(len(connectors), 2) self.assertEqual(len(connectors), 1)
self.assertIsInstance(connectors[0], SelfConnector) self.assertIsInstance(connectors[0], BookWyrmConnector)
self.assertIsInstance(connectors[1], BookWyrmConnector)
@responses.activate @responses.activate
def test_search(self): def test_search_plaintext(self):
"""search all connectors""" """search all connectors"""
responses.add( responses.add(
responses.GET, responses.GET,
"http://fake.ciom/search/Example?min_confidence=0.1", "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") results = connector_manager.search("Example")
self.assertEqual(len(results), 1) self.assertEqual(len(results), 1)
self.assertIsInstance(results[0]["connector"], SelfConnector)
self.assertEqual(len(results[0]["results"]), 1) 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): def test_search_empty_query(self):
"""don't panic on empty queries""" """don't panic on empty queries"""
@ -88,19 +74,13 @@ class ConnectorManager(TestCase):
responses.add( responses.add(
responses.GET, responses.GET,
"http://fake.ciom/isbn/0000000000", "http://fake.ciom/isbn/0000000000",
json={}, json=[{"title": "Hello", "key": "https://www.example.com/search/1"}],
) )
results = connector_manager.search("0000000000") results = connector_manager.search("0000000000")
self.assertEqual(len(results), 1) self.assertEqual(len(results), 1)
self.assertIsInstance(results[0]["connector"], SelfConnector)
self.assertEqual(len(results[0]["results"]), 1) 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_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")
def test_first_search_result(self): def test_first_search_result(self):
"""only get one search result""" """only get one search result"""
@ -125,6 +105,5 @@ class ConnectorManager(TestCase):
def test_load_connector(self): def test_load_connector(self):
"""load a connector object from the database entry""" """load a connector object from the database entry"""
connector = connector_manager.load_connector(self.connector) connector = connector_manager.load_connector(self.remote_connector)
self.assertIsInstance(connector, SelfConnector) self.assertEqual(connector.identifier, "test_connector_remote")
self.assertEqual(connector.identifier, "test_connector")

View file

@ -7,11 +7,11 @@ from django.test import TestCase
import responses import responses
from bookwyrm import models from bookwyrm import models
from bookwyrm.book_search import SearchResult
from bookwyrm.connectors.openlibrary import Connector from bookwyrm.connectors.openlibrary import Connector
from bookwyrm.connectors.openlibrary import ignore_edition from bookwyrm.connectors.openlibrary import ignore_edition
from bookwyrm.connectors.openlibrary import get_languages, get_description from bookwyrm.connectors.openlibrary import get_languages, get_description
from bookwyrm.connectors.openlibrary import pick_default_edition, get_openlibrary_key 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 from bookwyrm.connectors.connector_manager import ConnectorException

View file

@ -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)

View file

@ -12,7 +12,6 @@ import responses
from bookwyrm import models from bookwyrm import models
from bookwyrm.importers import GoodreadsImporter from bookwyrm.importers import GoodreadsImporter
from bookwyrm.importers.importer import import_data, handle_imported_book from bookwyrm.importers.importer import import_data, handle_imported_book
from bookwyrm.settings import DOMAIN
def make_date(*args): def make_date(*args):
@ -39,17 +38,6 @@ class GoodreadsImport(TestCase):
"mouse", "mouse@mouse.mouse", "password", local=True "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") work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create( self.book = models.Edition.objects.create(
title="Example Edition", title="Example Edition",
@ -125,7 +113,7 @@ class GoodreadsImport(TestCase):
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") 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))): for index, entry in enumerate(list(csv.DictReader(csv_file))):
entry = self.importer.parse_fields(entry) entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create( import_item = models.ImportItem.objects.create(
@ -162,7 +150,7 @@ class GoodreadsImport(TestCase):
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") 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))): for index, entry in enumerate(list(csv.DictReader(csv_file))):
entry = self.importer.parse_fields(entry) entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create( import_item = models.ImportItem.objects.create(
@ -192,7 +180,7 @@ class GoodreadsImport(TestCase):
shelf = self.user.shelf_set.filter(identifier="read").first() shelf = self.user.shelf_set.filter(identifier="read").first()
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") 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))): for index, entry in enumerate(list(csv.DictReader(csv_file))):
entry = self.importer.parse_fields(entry) entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create( import_item = models.ImportItem.objects.create(
@ -224,7 +212,7 @@ class GoodreadsImport(TestCase):
"""goodreads review import""" """goodreads review import"""
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") 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 = list(csv.DictReader(csv_file))[2]
entry = self.importer.parse_fields(entry) entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create( import_item = models.ImportItem.objects.create(
@ -248,7 +236,7 @@ class GoodreadsImport(TestCase):
datafile = pathlib.Path(__file__).parent.joinpath( datafile = pathlib.Path(__file__).parent.joinpath(
"../data/goodreads-rating.csv" "../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 = list(csv.DictReader(csv_file))[2]
entry = self.importer.parse_fields(entry) entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create( import_item = models.ImportItem.objects.create(
@ -269,7 +257,7 @@ class GoodreadsImport(TestCase):
"""goodreads review import""" """goodreads review import"""
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") 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 = list(csv.DictReader(csv_file))[2]
entry = self.importer.parse_fields(entry) entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create( import_item = models.ImportItem.objects.create(

View file

@ -11,7 +11,6 @@ import responses
from bookwyrm import models from bookwyrm import models
from bookwyrm.importers import LibrarythingImporter from bookwyrm.importers import LibrarythingImporter
from bookwyrm.importers.importer import import_data, handle_imported_book from bookwyrm.importers.importer import import_data, handle_imported_book
from bookwyrm.settings import DOMAIN
def make_date(*args): def make_date(*args):
@ -39,18 +38,6 @@ class LibrarythingImport(TestCase):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
"mmai", "mmai@mmai.mmai", "password", local=True "mmai", "mmai@mmai.mmai", "password", local=True
) )
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") work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create( self.book = models.Edition.objects.create(
title="Example Edition", title="Example Edition",

View file

@ -9,8 +9,8 @@ from django.test import TestCase
import responses import responses
from bookwyrm import models from bookwyrm import models
from bookwyrm.book_search import SearchResult
from bookwyrm.connectors import connector_manager from bookwyrm.connectors import connector_manager
from bookwyrm.connectors.abstract_connector import SearchResult
class ImportJob(TestCase): class ImportJob(TestCase):

View file

@ -14,6 +14,7 @@ from bookwyrm import activitypub, models, settings
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
# pylint: disable=line-too-long
@patch("bookwyrm.models.Status.broadcast") @patch("bookwyrm.models.Status.broadcast")
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
@patch("bookwyrm.activitystreams.remove_status_task.delay") @patch("bookwyrm.activitystreams.remove_status_task.delay")
@ -52,22 +53,26 @@ class Status(TestCase):
def test_status_generated_fields(self, *_): def test_status_generated_fields(self, *_):
"""setting remote id""" """setting remote id"""
status = models.Status.objects.create(content="bleh", user=self.local_user) 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.remote_id, expected_id)
self.assertEqual(status.privacy, "public") self.assertEqual(status.privacy, "public")
def test_replies(self, *_): def test_replies(self, *_):
"""get a list of replies""" """get a list of replies"""
parent = models.Status.objects.create(content="hi", user=self.local_user) parent = models.Status(content="hi", user=self.local_user)
child = models.Status.objects.create( parent.save(broadcast=False)
child = models.Status(
content="hello", reply_parent=parent, user=self.local_user 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 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 content="hi hello", reply_parent=child, user=self.local_user
) )
grandchild.save(broadcast=False)
replies = models.Status.replies(parent) replies = models.Status.replies(parent)
self.assertEqual(replies.count(), 2) self.assertEqual(replies.count(), 2)
@ -75,6 +80,11 @@ class Status(TestCase):
# should select subclasses # should select subclasses
self.assertIsInstance(replies.last(), models.Review) 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, *_): def test_status_type(self, *_):
"""class name""" """class name"""
self.assertEqual(models.Status().status_type, "Note") self.assertEqual(models.Status().status_type, "Note")
@ -104,7 +114,7 @@ class Status(TestCase):
) )
replies = parent.to_replies() 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) self.assertEqual(replies["totalItems"], 2)
def test_status_to_activity(self, *_): def test_status_to_activity(self, *_):
@ -168,7 +178,7 @@ class Status(TestCase):
self.assertEqual(activity["id"], status.remote_id) self.assertEqual(activity["id"], status.remote_id)
self.assertEqual( self.assertEqual(
activity["content"], 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(len(activity["tag"]), 2)
self.assertEqual(activity["type"], "Note") self.assertEqual(activity["type"], "Note")
@ -177,7 +187,7 @@ class Status(TestCase):
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertEqual(
activity["attachment"][0].url, 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") self.assertEqual(activity["attachment"][0].name, "Test Edition")
@ -202,13 +212,12 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Note") self.assertEqual(activity["type"], "Note")
self.assertEqual( self.assertEqual(
activity["content"], activity["content"],
'test content<p>(comment on <a href="%s">"Test Edition"</a>)</p>' f'test content<p>(comment on <a href="{self.book.remote_id}">"Test Edition"</a>)</p>',
% self.book.remote_id,
) )
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertEqual(
activity["attachment"][0].url, 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") self.assertEqual(activity["attachment"][0].name, "Test Edition")
@ -240,13 +249,12 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Note") self.assertEqual(activity["type"], "Note")
self.assertEqual( self.assertEqual(
activity["content"], activity["content"],
'a sickening sense <p>-- <a href="%s">"Test Edition"</a></p>' f'a sickening sense <p>-- <a href="{self.book.remote_id}">"Test Edition"</a></p>test content',
"test content" % self.book.remote_id,
) )
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertEqual(
activity["attachment"][0].url, 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") self.assertEqual(activity["attachment"][0].name, "Test Edition")
@ -281,13 +289,13 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Article") self.assertEqual(activity["type"], "Article")
self.assertEqual( self.assertEqual(
activity["name"], 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["content"], "test content")
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertEqual(
activity["attachment"][0].url, 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") 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["id"], status.remote_id)
self.assertEqual(activity["type"], "Article") self.assertEqual(activity["type"], "Article")
self.assertEqual( 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["content"], "test content")
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertEqual(
activity["attachment"][0].url, 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") self.assertEqual(activity["attachment"][0].name, "Test Edition")
@ -325,13 +333,12 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Note") self.assertEqual(activity["type"], "Note")
self.assertEqual( self.assertEqual(
activity["content"], activity["content"],
'rated <em><a href="%s">%s</a></em>: 3 stars' f'rated <em><a href="{self.book.remote_id}">{self.book.title}</a></em>: 3 stars',
% (self.book.remote_id, self.book.title),
) )
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertEqual(
activity["attachment"][0].url, 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") self.assertEqual(activity["attachment"][0].name, "Test Edition")

View 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")

View 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)

View file

@ -1,11 +1,11 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
class DashboardViews(TestCase): class DashboardViews(TestCase):
@ -35,8 +35,5 @@ class DashboardViews(TestCase):
request.user.is_superuser = True request.user.is_superuser = True
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)

View file

@ -1,12 +1,12 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
class EmailBlocklistViews(TestCase): class EmailBlocklistViews(TestCase):
@ -38,10 +38,7 @@ class EmailBlocklistViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_blocklist_page_post(self): def test_blocklist_page_post(self):
@ -54,10 +51,7 @@ class EmailBlocklistViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertTrue( self.assertTrue(

View file

@ -1,7 +1,6 @@
""" test for app action functionality """ """ test for app action functionality """
import json import json
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@ -9,6 +8,7 @@ from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import forms, models, views from bookwyrm import forms, models, views
from bookwyrm.tests.validate_html import validate_html
class FederationViews(TestCase): class FederationViews(TestCase):
@ -48,16 +48,7 @@ class FederationViews(TestCase):
request.user.is_superuser = True request.user.is_superuser = True
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_instance_page(self): def test_instance_page(self):
@ -70,10 +61,7 @@ class FederationViews(TestCase):
result = view(request, server.id) result = view(request, server.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_server_page_block(self): def test_server_page_block(self):
@ -162,10 +150,7 @@ class FederationViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_add_view_post_create(self): def test_add_view_post_create(self):

View file

@ -1,11 +1,11 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
class IPBlocklistViews(TestCase): class IPBlocklistViews(TestCase):
@ -37,8 +37,5 @@ class IPBlocklistViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)

View file

@ -1,13 +1,13 @@
""" test for app action functionality """ """ test for app action functionality """
import json import json
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import forms, models, views from bookwyrm import forms, models, views
from bookwyrm.tests.validate_html import validate_html
class ReportViews(TestCase): class ReportViews(TestCase):
@ -44,16 +44,7 @@ class ReportViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_reports_page_with_data(self): def test_reports_page_with_data(self):
@ -66,16 +57,7 @@ class ReportViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_report_page(self): def test_report_page(self):
@ -89,10 +71,7 @@ class ReportViews(TestCase):
result = view(request, report.id) result = view(request, report.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_report_comment(self): def test_report_comment(self):

View file

@ -1,6 +1,5 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@ -8,6 +7,7 @@ from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
class UserAdminViews(TestCase): class UserAdminViews(TestCase):
@ -36,10 +36,7 @@ class UserAdminViews(TestCase):
request.user.is_superuser = True request.user.is_superuser = True
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_user_admin_page(self): def test_user_admin_page(self):
@ -52,10 +49,7 @@ class UserAdminViews(TestCase):
result = view(request, self.local_user.id) result = view(request, self.local_user.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@ -77,10 +71,7 @@ class UserAdminViews(TestCase):
result = view(request, self.local_user.id) result = view(request, self.local_user.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual( self.assertEqual(
list(self.local_user.groups.values_list("name", flat=True)), ["editor"] list(self.local_user.groups.values_list("name", flat=True)), ["editor"]

View file

@ -0,0 +1 @@
from . import *

View 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

View file

@ -1,25 +1,19 @@
""" test for app action functionality """ """ test for app action functionality """
from io import BytesIO
import pathlib
from unittest.mock import patch from unittest.mock import patch
from PIL import Image
import responses import responses
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType 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.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.utils import timezone
from bookwyrm import forms, models, views 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""" """books books books"""
def setUp(self): def setUp(self):
@ -53,102 +47,6 @@ class BookViews(TestCase):
models.SiteSettings.objects.create() 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): def test_edit_book_page(self):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""
view = views.EditBook.as_view() view = views.EditBook.as_view()
@ -157,7 +55,7 @@ class BookViews(TestCase):
request.user.is_superuser = True request.user.is_superuser = True
result = view(request, self.book.id) result = view(request, self.book.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_edit_book(self): def test_edit_book(self):
@ -188,7 +86,7 @@ class BookViews(TestCase):
request.user = self.local_user request.user = self.local_user
result = view(request, self.book.id) result = view(request, self.book.id)
result.render() validate_html(result.render())
# the changes haven't been saved yet # the changes haven't been saved yet
self.book.refresh_from_db() self.book.refresh_from_db()
@ -283,29 +181,12 @@ class BookViews(TestCase):
self.assertEqual(book.authors.first().name, "Sappho") self.assertEqual(book.authors.first().name, "Sappho")
self.assertEqual(book.authors.first(), book.parent_work.authors.first()) 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 @responses.activate
def test_create_book_upload_cover_url(self): def test_create_book_upload_cover_url(self):
"""create an entirely new book and work with cover url""" """create an entirely new book and work with cover url"""
self.assertFalse(self.book.cover) self.assertFalse(self.book.cover)
view = views.ConfirmEditBook.as_view()
self.local_user.groups.add(self.group) self.local_user.groups.add(self.group)
cover_url = self._setup_cover_url() cover_url = _setup_cover_url()
form = forms.EditionForm() form = forms.EditionForm()
form.data["title"] = "New Title" form.data["title"] = "New Title"
@ -322,59 +203,3 @@ class BookViews(TestCase):
self.book.refresh_from_db() self.book.refresh_from_db()
self.assertTrue(self.book.cover) 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)

View file

@ -7,6 +7,7 @@ from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.tests.validate_html import validate_html
class BookViews(TestCase): class BookViews(TestCase):
@ -40,11 +41,11 @@ class BookViews(TestCase):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""
view = views.Editions.as_view() view = views.Editions.as_view()
request = self.factory.get("") 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 is_api.return_value = False
result = view(request, self.work.id) result = view(request, self.work.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertTrue("paperback" in result.context_data["formats"]) self.assertTrue("paperback" in result.context_data["formats"])
@ -57,11 +58,11 @@ class BookViews(TestCase):
) )
view = views.Editions.as_view() view = views.Editions.as_view()
request = self.factory.get("") 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 is_api.return_value = False
result = view(request, self.work.id) result = view(request, self.work.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 2) self.assertEqual(len(result.context_data["editions"].object_list), 2)
self.assertEqual(len(result.context_data["formats"]), 2) self.assertEqual(len(result.context_data["formats"]), 2)
@ -69,26 +70,26 @@ class BookViews(TestCase):
self.assertTrue("okay" in result.context_data["formats"]) self.assertTrue("okay" in result.context_data["formats"])
request = self.factory.get("", {"q": "fish"}) 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 is_api.return_value = False
result = view(request, self.work.id) result = view(request, self.work.id)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 1) self.assertEqual(len(result.context_data["editions"].object_list), 1)
request = self.factory.get("", {"q": "okay"}) 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 is_api.return_value = False
result = view(request, self.work.id) result = view(request, self.work.id)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 1) self.assertEqual(len(result.context_data["editions"].object_list), 1)
request = self.factory.get("", {"format": "okay"}) 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 is_api.return_value = False
result = view(request, self.work.id) result = view(request, self.work.id)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 1) 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""" """there are so many views, this just makes sure it LOADS"""
view = views.Editions.as_view() view = views.Editions.as_view()
request = self.factory.get("") 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 is_api.return_value = True
result = view(request, self.work.id) result = view(request, self.work.id)
self.assertIsInstance(result, ActivitypubResponse) self.assertIsInstance(result, ActivitypubResponse)

View file

@ -72,6 +72,7 @@ class InboxCreate(TestCase):
self.assertEqual(status.quote, "quote body") self.assertEqual(status.quote, "quote body")
self.assertEqual(status.content, "commentary") self.assertEqual(status.content, "commentary")
self.assertEqual(status.user, self.local_user) self.assertEqual(status.user, self.local_user)
self.assertEqual(status.thread_id, status.id)
# while we're here, lets ensure we avoid dupes # while we're here, lets ensure we avoid dupes
views.inbox.activity_task(activity) views.inbox.activity_task(activity)
@ -144,6 +145,7 @@ class InboxCreate(TestCase):
status = models.Status.objects.last() status = models.Status.objects.last()
self.assertEqual(status.content, "test content in note") self.assertEqual(status.content, "test content in note")
self.assertEqual(status.reply_parent, parent_status) 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.assertTrue(models.Notification.objects.filter(user=self.local_user))
self.assertEqual(models.Notification.objects.get().notification_type, "REPLY") self.assertEqual(models.Notification.objects.get().notification_type, "REPLY")

View file

@ -1,12 +1,12 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
@ -46,10 +46,7 @@ class BlockViews(TestCase):
request.user = self.local_user request.user = self.local_user
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_block_post(self, _): def test_block_post(self, _):

View file

@ -1,12 +1,12 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
class ChangePasswordViews(TestCase): class ChangePasswordViews(TestCase):
@ -35,10 +35,7 @@ class ChangePasswordViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_password_change(self): def test_password_change(self):

View file

@ -1,7 +1,6 @@
""" test for app action functionality """ """ test for app action functionality """
import json import json
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@ -9,6 +8,7 @@ from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import forms, models, views from bookwyrm import forms, models, views
from bookwyrm.tests.validate_html import validate_html
@patch("bookwyrm.suggested_users.remove_user_task.delay") @patch("bookwyrm.suggested_users.remove_user_task.delay")
@ -53,10 +53,7 @@ class DeleteUserViews(TestCase):
request.user = self.local_user request.user = self.local_user
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@patch("bookwyrm.suggested_users.rerank_suggestions_task") @patch("bookwyrm.suggested_users.rerank_suggestions_task")

View file

@ -2,7 +2,6 @@
import pathlib import pathlib
from unittest.mock import patch from unittest.mock import patch
from PIL import Image from PIL import Image
from tidylib import tidy_document
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
@ -12,6 +11,7 @@ from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import forms, models, views from bookwyrm import forms, models, views
from bookwyrm.tests.validate_html import validate_html
@patch("bookwyrm.suggested_users.remove_user_task.delay") @patch("bookwyrm.suggested_users.remove_user_task.delay")
@ -58,10 +58,7 @@ class EditUserViews(TestCase):
request.user = self.local_user request.user = self.local_user
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_edit_user(self, _): def test_edit_user(self, _):

View file

@ -1,6 +1,5 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@ -8,6 +7,7 @@ from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
# pylint: disable=unused-argument # pylint: disable=unused-argument
class DirectoryViews(TestCase): class DirectoryViews(TestCase):
@ -52,16 +52,7 @@ class DirectoryViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_directory_page_empty(self): def test_directory_page_empty(self):
@ -72,10 +63,7 @@ class DirectoryViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_directory_page_logged_out(self): def test_directory_page_logged_out(self):

View file

@ -29,9 +29,6 @@ class GetStartedViews(TestCase):
title="Example Edition", title="Example Edition",
remote_id="https://example.com/book/1", remote_id="https://example.com/book/1",
) )
models.Connector.objects.create(
identifier="self", connector_file="self_connector", local=True
)
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def test_profile_view(self, *_): def test_profile_view(self, *_):

View file

@ -1,6 +1,5 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.http import Http404 from django.http import Http404
@ -10,6 +9,7 @@ from django.test.client import RequestFactory
from django.utils import timezone from django.utils import timezone
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
class GoalViews(TestCase): class GoalViews(TestCase):
@ -62,16 +62,7 @@ class GoalViews(TestCase):
request.user = self.local_user request.user = self.local_user
result = view(request, self.local_user.localname, self.year) result = view(request, self.local_user.localname, self.year)
html = result.render() validate_html(result.render())
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
def test_goal_page_anonymous(self): def test_goal_page_anonymous(self):
@ -102,16 +93,7 @@ class GoalViews(TestCase):
request.user = self.rat request.user = self.rat
result = view(request, self.local_user.localname, timezone.now().year) result = view(request, self.local_user.localname, timezone.now().year)
html = result.render() validate_html(result.render())
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
def test_goal_page_private(self): def test_goal_page_private(self):

View file

@ -34,9 +34,6 @@ class IsbnViews(TestCase):
remote_id="https://example.com/book/1", remote_id="https://example.com/book/1",
parent_work=self.work, parent_work=self.work,
) )
models.Connector.objects.create(
identifier="self", connector_file="self_connector", local=True
)
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def test_isbn_json_response(self): def test_isbn_json_response(self):
@ -51,4 +48,4 @@ class IsbnViews(TestCase):
data = json.loads(response.content) data = json.loads(response.content)
self.assertEqual(len(data), 1) self.assertEqual(len(data), 1)
self.assertEqual(data[0]["title"], "Test Book") 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}")

View file

@ -6,6 +6,7 @@ from django.test.client import RequestFactory
from bookwyrm import models from bookwyrm import models
from bookwyrm import views from bookwyrm import views
from bookwyrm.tests.validate_html import validate_html
class NotificationViews(TestCase): class NotificationViews(TestCase):
@ -24,16 +25,53 @@ class NotificationViews(TestCase):
local=True, local=True,
localname="mouse", 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() 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""" """there are so many views, this just makes sure it LOADS"""
view = views.Notifications.as_view() view = views.Notifications.as_view()
request = self.factory.get("") request = self.factory.get("")
request.user = self.local_user request.user = self.local_user
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) 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) self.assertEqual(result.status_code, 200)
def test_clear_notifications(self): def test_clear_notifications(self):

View file

@ -58,6 +58,9 @@ class ReadingViews(TestCase):
"post-status": True, "post-status": True,
"privacy": "followers", "privacy": "followers",
"start_date": "2020-01-05", "start_date": "2020-01-05",
"book": self.book.id,
"mention_books": self.book.id,
"user": self.local_user.id,
}, },
) )
request.user = self.local_user request.user = self.local_user
@ -77,6 +80,45 @@ class ReadingViews(TestCase):
self.assertEqual(readthrough.user, self.local_user) self.assertEqual(readthrough.user, self.local_user)
self.assertEqual(readthrough.book, self.book) 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.add_book_statuses_task.delay")
@patch("bookwyrm.activitystreams.remove_book_statuses_task.delay") @patch("bookwyrm.activitystreams.remove_book_statuses_task.delay")
def test_start_reading_reshelve(self, *_): def test_start_reading_reshelve(self, *_):
@ -203,3 +245,42 @@ class ReadingViews(TestCase):
self.assertEqual(readthrough.finish_date.day, 7) self.assertEqual(readthrough.finish_date.day, 7)
self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.book, self.book)
self.assertEqual(readthrough.user, self.local_user) 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