diff --git a/.env.dev.example b/.env.dev.example index 5e605d74..538d1611 100644 --- a/.env.dev.example +++ b/.env.dev.example @@ -7,6 +7,9 @@ DEBUG=true DOMAIN=your.domain.here #EMAIL=your@email.here +# Used for deciding which editions to prefer +DEFAULT_LANGUAGE="English" + ## Leave unset to allow all hosts # ALLOWED_HOSTS="localhost,127.0.0.1,[::1]" diff --git a/.env.prod.example b/.env.prod.example index 0013bf9d..ac9fe70f 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -7,6 +7,9 @@ DEBUG=false DOMAIN=your.domain.here EMAIL=your@email.here +# Used for deciding which editions to prefer +DEFAULT_LANGUAGE="English" + ## Leave unset to allow all hosts # ALLOWED_HOSTS="localhost,127.0.0.1,[::1]" diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index f6ebf913..1599b408 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -7,11 +7,22 @@ from .image import Document @dataclass(init=False) -class Book(ActivityObject): +class BookData(ActivityObject): + """shared fields for all book data and authors""" + + openlibraryKey: str = None + inventaireId: str = None + librarythingKey: str = None + goodreadsKey: str = None + bnfId: str = None + lastEditedBy: str = None + + +@dataclass(init=False) +class Book(BookData): """serializes an edition or work, abstract""" title: str - lastEditedBy: str = None sortTitle: str = "" subtitle: str = "" description: str = "" @@ -25,10 +36,6 @@ class Book(ActivityObject): firstPublishedDate: str = "" publishedDate: str = "" - openlibraryKey: str = "" - librarythingKey: str = "" - goodreadsKey: str = "" - cover: Document = None type: str = "Book" @@ -55,23 +62,21 @@ class Work(Book): """work instance of a book object""" lccn: str = "" - defaultEdition: str = "" editions: List[str] = field(default_factory=lambda: []) type: str = "Work" @dataclass(init=False) -class Author(ActivityObject): +class Author(BookData): """author of a book""" name: str - lastEditedBy: str = None + isni: str = None + viafId: str = None + gutenbergId: str = None born: str = None died: str = None aliases: List[str] = field(default_factory=lambda: []) bio: str = "" - openlibraryKey: str = "" - librarythingKey: str = "" - goodreadsKey: str = "" wikipediaLink: str = "" type: str = "Author" diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index b501c3d6..ea2e92b6 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -83,4 +83,5 @@ class Rating(Comment): rating: int content: str = None + name: str = None # not used, but the model inherits from Review type: str = "Rating" diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 264b5a38..76718823 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -44,7 +44,7 @@ class AbstractMinimalConnector(ABC): if min_confidence: params["min_confidence"] = min_confidence - data = get_data( + data = self.get_search_data( "%s%s" % (self.search_url, query), params=params, ) @@ -57,7 +57,7 @@ class AbstractMinimalConnector(ABC): def isbn_search(self, query): """isbn search""" params = {} - data = get_data( + data = self.get_search_data( "%s%s" % (self.isbn_search_url, query), params=params, ) @@ -68,6 +68,10 @@ class AbstractMinimalConnector(ABC): results.append(self.format_isbn_search_result(doc)) return results + def get_search_data(self, remote_id, **kwargs): # pylint: disable=no-self-use + """this allows connectors to override the default behavior""" + return get_data(remote_id, **kwargs) + @abstractmethod def get_or_create_book(self, remote_id): """pull up a book record by whatever means possible""" @@ -112,12 +116,12 @@ class AbstractConnector(AbstractMinimalConnector): remote_id ) or models.Work.find_existing_by_remote_id(remote_id) if existing: - if hasattr(existing, "get_default_editon"): - return existing.get_default_editon() + if hasattr(existing, "default_edition"): + return existing.default_edition return existing # load the json - data = get_data(remote_id) + data = self.get_book_data(remote_id) mapped_data = dict_from_mappings(data, self.book_mappings) if self.is_work_data(data): try: @@ -128,12 +132,12 @@ class AbstractConnector(AbstractMinimalConnector): edition_data = data work_data = mapped_data else: + edition_data = data try: work_data = self.get_work_from_edition_data(data) work_data = dict_from_mappings(work_data, self.book_mappings) except (KeyError, ConnectorException): work_data = mapped_data - edition_data = data if not work_data or not edition_data: raise ConnectorException("Unable to load book data: %s" % remote_id) @@ -150,6 +154,10 @@ class AbstractConnector(AbstractMinimalConnector): load_more_data.delay(self.connector.id, work.id) return edition + def get_book_data(self, remote_id): # pylint: disable=no-self-use + """this allows connectors to override the default behavior""" + return get_data(remote_id) + def create_edition_from_data(self, work, edition_data): """if we already have the work, we're ready""" mapped_data = dict_from_mappings(edition_data, self.book_mappings) @@ -159,10 +167,6 @@ class AbstractConnector(AbstractMinimalConnector): edition.connector = self.connector edition.save() - if not work.default_edition: - work.default_edition = edition - work.save() - for author in self.get_authors_from_data(edition_data): edition.authors.add(author) if not edition.authors.exists() and work.authors.exists(): @@ -176,7 +180,7 @@ class AbstractConnector(AbstractMinimalConnector): if existing: return existing - data = get_data(remote_id) + data = self.get_book_data(remote_id) mapped_data = dict_from_mappings(data, self.author_mappings) try: @@ -273,6 +277,7 @@ class SearchResult: title: str key: str connector: object + view_link: str = None author: str = None year: str = None cover: str = None diff --git a/bookwyrm/connectors/bookwyrm_connector.py b/bookwyrm/connectors/bookwyrm_connector.py index 640a0bca..10a633b2 100644 --- a/bookwyrm/connectors/bookwyrm_connector.py +++ b/bookwyrm/connectors/bookwyrm_connector.py @@ -7,11 +7,7 @@ class Connector(AbstractMinimalConnector): """this is basically just for search""" def get_or_create_book(self, remote_id): - edition = activitypub.resolve_remote_id(remote_id, model=models.Edition) - work = edition.parent_work - work.default_edition = work.get_default_edition() - work.save() - return edition + return activitypub.resolve_remote_id(remote_id, model=models.Edition) def parse_search_data(self, data): return data diff --git a/bookwyrm/connectors/inventaire.py b/bookwyrm/connectors/inventaire.py new file mode 100644 index 00000000..dc27f2c0 --- /dev/null +++ b/bookwyrm/connectors/inventaire.py @@ -0,0 +1,214 @@ +""" inventaire data connector """ +import re + +from bookwyrm import models +from .abstract_connector import AbstractConnector, SearchResult, Mapping +from .abstract_connector import get_data +from .connector_manager import ConnectorException + + +class Connector(AbstractConnector): + """instantiate a connector for OL""" + + def __init__(self, identifier): + super().__init__(identifier) + + get_first = lambda a: a[0] + shared_mappings = [ + Mapping("id", remote_field="uri", formatter=self.get_remote_id), + Mapping("bnfId", remote_field="wdt:P268", formatter=get_first), + Mapping("openlibraryKey", remote_field="wdt:P648", formatter=get_first), + ] + self.book_mappings = [ + Mapping("title", remote_field="wdt:P1476", formatter=get_first), + Mapping("subtitle", remote_field="wdt:P1680", formatter=get_first), + Mapping("inventaireId", remote_field="uri"), + Mapping( + "description", remote_field="sitelinks", formatter=self.get_description + ), + Mapping("cover", remote_field="image", formatter=self.get_cover_url), + Mapping("isbn13", remote_field="wdt:P212", formatter=get_first), + Mapping("isbn10", remote_field="wdt:P957", formatter=get_first), + Mapping("oclcNumber", remote_field="wdt:P5331", formatter=get_first), + Mapping("goodreadsKey", remote_field="wdt:P2969", formatter=get_first), + Mapping("librarythingKey", remote_field="wdt:P1085", formatter=get_first), + Mapping("languages", remote_field="wdt:P407", formatter=self.resolve_keys), + Mapping("publishers", remote_field="wdt:P123", formatter=self.resolve_keys), + Mapping("publishedDate", remote_field="wdt:P577", formatter=get_first), + Mapping("pages", remote_field="wdt:P1104", formatter=get_first), + Mapping( + "subjectPlaces", remote_field="wdt:P840", formatter=self.resolve_keys + ), + Mapping("subjects", remote_field="wdt:P921", formatter=self.resolve_keys), + Mapping("asin", remote_field="wdt:P5749", formatter=get_first), + ] + shared_mappings + # TODO: P136: genre, P674 characters, P950 bne + + self.author_mappings = [ + Mapping("id", remote_field="uri", formatter=self.get_remote_id), + Mapping("name", remote_field="labels", formatter=get_language_code), + Mapping("bio", remote_field="sitelinks", formatter=self.get_description), + Mapping("goodreadsKey", remote_field="wdt:P2963", formatter=get_first), + Mapping("isni", remote_field="wdt:P213", formatter=get_first), + Mapping("viafId", remote_field="wdt:P214", formatter=get_first), + Mapping("gutenberg_id", remote_field="wdt:P1938", formatter=get_first), + Mapping("born", remote_field="wdt:P569", formatter=get_first), + Mapping("died", remote_field="wdt:P570", formatter=get_first), + ] + shared_mappings + + def get_remote_id(self, value): + """convert an id/uri into a url""" + return "{:s}?action=by-uris&uris={:s}".format(self.books_url, value) + + def get_book_data(self, remote_id): + data = get_data(remote_id) + extracted = list(data.get("entities").values()) + try: + data = extracted[0] + except KeyError: + raise ConnectorException("Invalid book data") + # flatten the data so that images, uri, and claims are on the same level + return { + **data.get("claims", {}), + **{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks"]}, + } + + def parse_search_data(self, data): + return data.get("results") + + def format_search_result(self, search_result): + images = search_result.get("image") + cover = ( + "{:s}/img/entities/{:s}".format(self.covers_url, images[0]) + if images + else None + ) + return SearchResult( + title=search_result.get("label"), + key=self.get_remote_id(search_result.get("uri")), + author=search_result.get("description"), + view_link="{:s}/entity/{:s}".format( + self.base_url, search_result.get("uri") + ), + cover=cover, + connector=self, + ) + + def parse_isbn_search_data(self, data): + """got some daaaata""" + results = data.get("entities") + if not results: + return [] + return list(results.values()) + + def format_isbn_search_result(self, search_result): + """totally different format than a regular search result""" + title = search_result.get("claims", {}).get("wdt:P1476", []) + if not title: + return None + return SearchResult( + title=title[0], + key=self.get_remote_id(search_result.get("uri")), + author=search_result.get("description"), + view_link="{:s}/entity/{:s}".format( + self.base_url, search_result.get("uri") + ), + cover=self.get_cover_url(search_result.get("image")), + connector=self, + ) + + def is_work_data(self, data): + return data.get("type") == "work" + + def load_edition_data(self, work_uri): + """get a list of editions for a work""" + url = "{:s}?action=reverse-claims&property=wdt:P629&value={:s}".format( + self.books_url, work_uri + ) + return get_data(url) + + def get_edition_from_work_data(self, data): + data = self.load_edition_data(data.get("uri")) + try: + uri = data["uris"][0] + except KeyError: + raise ConnectorException("Invalid book data") + return self.get_book_data(self.get_remote_id(uri)) + + def get_work_from_edition_data(self, data): + try: + uri = data["claims"]["wdt:P629"] + except KeyError: + raise ConnectorException("Invalid book data") + return self.get_book_data(self.get_remote_id(uri)) + + def get_authors_from_data(self, data): + authors = data.get("wdt:P50", []) + for author in authors: + yield self.get_or_create_author(self.get_remote_id(author)) + + def expand_book_data(self, book): + work = book + # go from the edition to the work, if necessary + if isinstance(book, models.Edition): + work = book.parent_work + + try: + edition_options = self.load_edition_data(work.inventaire_id) + except ConnectorException: + # who knows, man + return + + for edition_uri in edition_options.get("uris"): + remote_id = self.get_remote_id(edition_uri) + try: + data = self.get_book_data(remote_id) + except ConnectorException: + # who, indeed, knows + continue + self.create_edition_from_data(work, data) + + def get_cover_url(self, cover_blob, *_): + """format the relative cover url into an absolute one: + {"url": "/img/entities/e794783f01b9d4f897a1ea9820b96e00d346994f"} + """ + # covers may or may not be a list + if isinstance(cover_blob, list) and len(cover_blob) > 0: + cover_blob = cover_blob[0] + cover_id = cover_blob.get("url") + if not cover_id: + return None + # cover may or may not be an absolute url already + if re.match(r"^http", cover_id): + return cover_id + return "%s%s" % (self.covers_url, cover_id) + + def resolve_keys(self, keys): + """cool, it's "wd:Q3156592" now what the heck does that mean""" + results = [] + for uri in keys: + try: + data = self.get_book_data(self.get_remote_id(uri)) + except ConnectorException: + continue + results.append(get_language_code(data.get("labels"))) + return results + + def get_description(self, links): + """grab an extracted excerpt from wikipedia""" + link = links.get("enwiki") + if not link: + return "" + url = "{:s}/api/data?action=wp-extract&lang=en&title={:s}".format( + self.base_url, link + ) + try: + data = get_data(url) + except ConnectorException: + return "" + return data.get("extract") + + +def get_language_code(options, code="en"): + """when there are a bunch of translation but we need a single field""" + return options.get(code) diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index a7c30b66..69d498b8 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -14,8 +14,8 @@ class Connector(AbstractConnector): def __init__(self, identifier): super().__init__(identifier) - get_first = lambda a: a[0] - get_remote_id = lambda a: self.base_url + a + get_first = lambda a, *args: a[0] + get_remote_id = lambda a, *args: self.base_url + a self.book_mappings = [ Mapping("title"), Mapping("id", remote_field="key", formatter=get_remote_id), diff --git a/bookwyrm/connectors/self_connector.py b/bookwyrm/connectors/self_connector.py index 0dc922a5..a8f85834 100644 --- a/bookwyrm/connectors/self_connector.py +++ b/bookwyrm/connectors/self_connector.py @@ -3,7 +3,7 @@ from functools import reduce import operator from django.contrib.postgres.search import SearchRank, SearchVector -from django.db.models import Count, F, Q +from django.db.models import Count, OuterRef, Subquery, F, Q from bookwyrm import models from .abstract_connector import AbstractConnector, SearchResult @@ -47,7 +47,16 @@ class Connector(AbstractConnector): # when there are multiple editions of the same work, pick the default. # it would be odd for this to happen. - results = results.filter(parent_work__default_edition__id=F("id")) or results + + 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: @@ -60,6 +69,10 @@ class Connector(AbstractConnector): return search_results def format_search_result(self, search_result): + cover = None + if search_result.cover: + cover = "%s%s" % (self.covers_url, search_result.cover) + return SearchResult( title=search_result.title, key=search_result.remote_id, @@ -68,7 +81,7 @@ class Connector(AbstractConnector): if search_result.published_date else None, connector=self, - cover="%s%s" % (self.covers_url, search_result.cover), + cover=cover, confidence=search_result.rank if hasattr(search_result, "rank") else 1, ) @@ -112,7 +125,15 @@ def search_identifiers(query, *filters): # when there are multiple editions of the same work, pick the default. # it would be odd for this to happen. - return results.filter(parent_work__default_edition__id=F("id")) or results + 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): @@ -140,10 +161,10 @@ def search_title_author(query, min_confidence, *filters): for work_id in set(editions_of_work): editions = results.filter(parent_work=work_id) - default = editions.filter(parent_work__default_edition=F("id")) - default_rank = default.first().rank if default.exists() else 0 + 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.first() + yield default else: yield editions.first() diff --git a/bookwyrm/connectors/settings.py b/bookwyrm/connectors/settings.py index f1674cf7..4cc98da7 100644 --- a/bookwyrm/connectors/settings.py +++ b/bookwyrm/connectors/settings.py @@ -1,3 +1,3 @@ """ settings book data connectors """ -CONNECTORS = ["openlibrary", "self_connector", "bookwyrm_connector"] +CONNECTORS = ["openlibrary", "inventaire", "self_connector", "bookwyrm_connector"] diff --git a/bookwyrm/management/commands/initdb.py b/bookwyrm/management/commands/initdb.py index 0c0cc61f..9033249d 100644 --- a/bookwyrm/management/commands/initdb.py +++ b/bookwyrm/management/commands/initdb.py @@ -94,6 +94,18 @@ def init_connectors(): priority=2, ) + Connector.objects.create( + identifier="inventaire.io", + name="Inventaire", + connector_file="inventaire", + base_url="https://inventaire.io", + books_url="https://inventaire.io/api/entities", + covers_url="https://inventaire.io", + search_url="https://inventaire.io/api/search?types=works&types=works&search=", + isbn_search_url="https://inventaire.io/api/entities?action=by-uris&uris=isbn%3A", + priority=3, + ) + Connector.objects.create( identifier="openlibrary.org", name="OpenLibrary", diff --git a/bookwyrm/migrations/0062_auto_20210406_1731.py b/bookwyrm/migrations/0062_auto_20210406_1731.py new file mode 100644 index 00000000..5db176ec --- /dev/null +++ b/bookwyrm/migrations/0062_auto_20210406_1731.py @@ -0,0 +1,30 @@ +# Generated by Django 3.1.6 on 2021-04-06 17:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0061_auto_20210402_1435"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="connector", + name="connector_file_valid", + ), + migrations.AlterField( + model_name="connector", + name="connector_file", + field=models.CharField( + choices=[ + ("openlibrary", "Openlibrary"), + ("inventaire", "Inventaire"), + ("self_connector", "Self Connector"), + ("bookwyrm_connector", "Bookwyrm Connector"), + ], + max_length=255, + ), + ), + ] diff --git a/bookwyrm/migrations/0063_auto_20210407_0045.py b/bookwyrm/migrations/0063_auto_20210407_0045.py new file mode 100644 index 00000000..cd87dd97 --- /dev/null +++ b/bookwyrm/migrations/0063_auto_20210407_0045.py @@ -0,0 +1,63 @@ +# Generated by Django 3.1.6 on 2021-04-07 00:45 + +import bookwyrm.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0062_auto_20210406_1731"), + ] + + operations = [ + migrations.AddField( + model_name="author", + name="bnf_id", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), + ), + migrations.AddField( + model_name="author", + name="gutenberg_id", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), + ), + migrations.AddField( + model_name="author", + name="inventaire_id", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), + ), + migrations.AddField( + model_name="author", + name="isni", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), + ), + migrations.AddField( + model_name="author", + name="viaf_id", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), + ), + migrations.AddField( + model_name="book", + name="bnf_id", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), + ), + migrations.AddField( + model_name="book", + name="inventaire_id", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), + ), + ] diff --git a/bookwyrm/migrations/0071_merge_0063_auto_20210407_0045_0070_auto_20210423_0121.py b/bookwyrm/migrations/0071_merge_0063_auto_20210407_0045_0070_auto_20210423_0121.py new file mode 100644 index 00000000..b6489b80 --- /dev/null +++ b/bookwyrm/migrations/0071_merge_0063_auto_20210407_0045_0070_auto_20210423_0121.py @@ -0,0 +1,13 @@ +# Generated by Django 3.2 on 2021-04-26 21:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0063_auto_20210407_0045"), + ("bookwyrm", "0070_auto_20210423_0121"), + ] + + operations = [] diff --git a/bookwyrm/migrations/0072_remove_work_default_edition.py b/bookwyrm/migrations/0072_remove_work_default_edition.py new file mode 100644 index 00000000..1c05c95e --- /dev/null +++ b/bookwyrm/migrations/0072_remove_work_default_edition.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2 on 2021-04-28 22:16 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0071_merge_0063_auto_20210407_0045_0070_auto_20210423_0121"), + ] + + operations = [ + migrations.RemoveField( + model_name="work", + name="default_edition", + ), + ] diff --git a/bookwyrm/models/author.py b/bookwyrm/models/author.py index b9a4b146..c4e26c5a 100644 --- a/bookwyrm/models/author.py +++ b/bookwyrm/models/author.py @@ -14,6 +14,15 @@ class Author(BookDataModel): wikipedia_link = fields.CharField( max_length=255, blank=True, null=True, deduplication_field=True ) + isni = fields.CharField( + max_length=255, blank=True, null=True, deduplication_field=True + ) + viaf_id = fields.CharField( + max_length=255, blank=True, null=True, deduplication_field=True + ) + gutenberg_id = fields.CharField( + max_length=255, blank=True, null=True, deduplication_field=True + ) # idk probably other keys would be useful here? born = fields.DateTimeField(blank=True, null=True) died = fields.DateTimeField(blank=True, null=True) diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index dd098e56..869ff04d 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -1,11 +1,11 @@ """ database schema for books and shelves """ import re -from django.db import models, transaction +from django.db import models from model_utils.managers import InheritanceManager from bookwyrm import activitypub -from bookwyrm.settings import DOMAIN +from bookwyrm.settings import DOMAIN, DEFAULT_LANGUAGE from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin from .base_model import BookWyrmModel @@ -19,12 +19,18 @@ class BookDataModel(ObjectMixin, BookWyrmModel): openlibrary_key = fields.CharField( max_length=255, blank=True, null=True, deduplication_field=True ) + inventaire_id = fields.CharField( + max_length=255, blank=True, null=True, deduplication_field=True + ) librarything_key = fields.CharField( max_length=255, blank=True, null=True, deduplication_field=True ) goodreads_key = fields.CharField( max_length=255, blank=True, null=True, deduplication_field=True ) + bnf_id = fields.CharField( # Bibliothèque nationale de France + max_length=255, blank=True, null=True, deduplication_field=True + ) last_edited_by = fields.ForeignKey( "User", @@ -137,10 +143,6 @@ class Work(OrderedCollectionPageMixin, Book): lccn = fields.CharField( max_length=255, blank=True, null=True, deduplication_field=True ) - # this has to be nullable but should never be null - default_edition = fields.ForeignKey( - "Edition", on_delete=models.PROTECT, null=True, load_remote=False - ) def save(self, *args, **kwargs): """set some fields on the edition object""" @@ -149,18 +151,10 @@ class Work(OrderedCollectionPageMixin, Book): edition.save() return super().save(*args, **kwargs) - def get_default_edition(self): + @property + def default_edition(self): """in case the default edition is not set""" - return self.default_edition or self.editions.order_by("-edition_rank").first() - - @transaction.atomic() - def reset_default_edition(self): - """sets a new default edition based on computed rank""" - self.default_edition = None - # editions are re-ranked implicitly - self.save() - self.default_edition = self.get_default_edition() - self.save() + return self.editions.order_by("-edition_rank").first() def to_edition_list(self, **kwargs): """an ordered collection of editions""" @@ -214,17 +208,20 @@ class Edition(Book): activity_serializer = activitypub.Edition name_field = "title" - def get_rank(self, ignore_default=False): + def get_rank(self): """calculate how complete the data is on this edition""" - if ( - not ignore_default - and self.parent_work - and self.parent_work.default_edition == self - ): - # default edition has the highest rank - return 20 rank = 0 + # big ups for havinga cover rank += int(bool(self.cover)) * 3 + # is it in the instance's preferred language? + rank += int(bool(DEFAULT_LANGUAGE in self.languages)) + # prefer print editions + if self.physical_format: + rank += int( + bool(self.physical_format.lower() in ["paperback", "hardcover"]) + ) + + # does it have metadata? rank += int(bool(self.isbn_13)) rank += int(bool(self.isbn_10)) rank += int(bool(self.oclc_number)) @@ -242,6 +239,12 @@ class Edition(Book): if self.isbn_10 and not self.isbn_13: self.isbn_13 = isbn_10_to_13(self.isbn_10) + # normalize isbn format + if self.isbn_10: + self.isbn_10 = re.sub(r"[^0-9X]", "", self.isbn_10) + if self.isbn_13: + self.isbn_13 = re.sub(r"[^0-9X]", "", self.isbn_13) + # set rank self.edition_rank = self.get_rank() diff --git a/bookwyrm/models/connector.py b/bookwyrm/models/connector.py index 6043fc02..625cdbed 100644 --- a/bookwyrm/models/connector.py +++ b/bookwyrm/models/connector.py @@ -31,16 +31,6 @@ class Connector(BookWyrmModel): # when to reset the query count back to 0 (ie, after 1 day) query_count_expiry = models.DateTimeField(auto_now_add=True, blank=True) - class Meta: - """check that there's code to actually use this connector""" - - constraints = [ - models.CheckConstraint( - check=models.Q(connector_file__in=ConnectorFiles), - name="connector_file_valid", - ) - ] - def __str__(self): return "{} ({})".format( self.identifier, diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index b679e2d4..1bc3c587 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -11,6 +11,7 @@ DOMAIN = env("DOMAIN") VERSION = "0.0.1" PAGE_LENGTH = env("PAGE_LENGTH", 15) +DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English") # celery CELERY_BROKER = env("CELERY_BROKER") diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index a9932910..98006184 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -80,6 +80,9 @@ {% if book.openlibrary_key %}
{% trans "View on OpenLibrary" %}
{% endif %} + {% if book.inventaire_id %} +{% trans "View on Inventaire" %}
+ {% endif %} diff --git a/bookwyrm/templates/search_results.html b/bookwyrm/templates/search_results.html index cb1fae39..fdb77f72 100644 --- a/bookwyrm/templates/search_results.html +++ b/bookwyrm/templates/search_results.html @@ -11,10 +11,15 @@{% blocktrans %}No books found for "{{ query }}"{% endblocktrans %}
+{% blocktrans %}No books found for "{{ query }}"{% endblocktrans %}
+ {% if not user.is_authenticated %} ++ {% trans "Log in to import or add books." %} +
+ {% endif %} {% else %}+
{% blocktrans %}No users found for "{{ query }}"{% endblocktrans %}
+{% blocktrans %}No users found for "{{ query }}"{% endblocktrans %}
{% endif %}{% blocktrans %}No lists found for "{{ query }}"{% endblocktrans %}
+{% blocktrans %}No lists found for "{{ query }}"{% endblocktrans %}
{% endif %} {% for result in list_results %}
{% if result.author %}
- {% blocktrans with author=result.author %}by {{ author }}{% endblocktrans %}
+ {{ result.author }}
{% endif %}
{% if result.year %}
diff --git a/bookwyrm/tests/connectors/test_connector_manager.py b/bookwyrm/tests/connectors/test_connector_manager.py
index feded616..34abbeaf 100644
--- a/bookwyrm/tests/connectors/test_connector_manager.py
+++ b/bookwyrm/tests/connectors/test_connector_manager.py
@@ -17,8 +17,6 @@ class ConnectorManager(TestCase):
self.edition = models.Edition.objects.create(
title="Example Edition", parent_work=self.work, isbn_10="0000000000"
)
- self.work.default_edition = self.edition
- self.work.save()
self.connector = models.Connector.objects.create(
identifier="test_connector",
diff --git a/bookwyrm/tests/connectors/test_inventaire_connector.py b/bookwyrm/tests/connectors/test_inventaire_connector.py
new file mode 100644
index 00000000..4058b067
--- /dev/null
+++ b/bookwyrm/tests/connectors/test_inventaire_connector.py
@@ -0,0 +1,158 @@
+""" testing book data connectors """
+import json
+import pathlib
+from django.test import TestCase
+import responses
+
+from bookwyrm import models
+from bookwyrm.connectors.inventaire import Connector
+
+
+class Inventaire(TestCase):
+ """test loading data from inventaire.io"""
+
+ def setUp(self):
+ """creates the connector we'll use"""
+ models.Connector.objects.create(
+ identifier="inventaire.io",
+ name="Inventaire",
+ connector_file="inventaire",
+ base_url="https://inventaire.io",
+ books_url="https://inventaire.io",
+ covers_url="https://covers.inventaire.io",
+ search_url="https://inventaire.io/search?q=",
+ isbn_search_url="https://inventaire.io/isbn",
+ )
+ self.connector = Connector("inventaire.io")
+
+ @responses.activate
+ def test_get_book_data(self):
+ """flattens the default structure to make it easier to parse"""
+ responses.add(
+ responses.GET,
+ "https://test.url/ok",
+ json={
+ "entities": {
+ "isbn:9780375757853": {
+ "claims": {
+ "wdt:P31": ["wd:Q3331189"],
+ },
+ "uri": "isbn:9780375757853",
+ }
+ },
+ "redirects": {},
+ },
+ )
+
+ result = self.connector.get_book_data("https://test.url/ok")
+ self.assertEqual(result["wdt:P31"], ["wd:Q3331189"])
+ self.assertEqual(result["uri"], "isbn:9780375757853")
+
+ def test_format_search_result(self):
+ """json to search result objs"""
+ search_file = pathlib.Path(__file__).parent.joinpath(
+ "../data/inventaire_search.json"
+ )
+ search_results = json.loads(search_file.read_bytes())
+
+ results = self.connector.parse_search_data(search_results)
+ formatted = self.connector.format_search_result(results[0])
+
+ self.assertEqual(formatted.title, "The Stories of Vladimir Nabokov")
+ self.assertEqual(
+ formatted.key, "https://inventaire.io?action=by-uris&uris=wd:Q7766679"
+ )
+ self.assertEqual(
+ formatted.cover,
+ "https://covers.inventaire.io/img/entities/ddb32e115a28dcc0465023869ba19f6868ec4042",
+ )
+
+ def test_get_cover_url(self):
+ """figure out where the cover image is"""
+ cover_blob = {"url": "/img/entities/d46a8"}
+ result = self.connector.get_cover_url(cover_blob)
+ self.assertEqual(result, "https://covers.inventaire.io/img/entities/d46a8")
+
+ cover_blob = {
+ "url": "https://commons.wikimedia.org/wiki/Special:FilePath/The%20Moonstone%201st%20ed.jpg?width=1000",
+ "file": "The Moonstone 1st ed.jpg",
+ "credits": {
+ "text": "Wikimedia Commons",
+ "url": "https://commons.wikimedia.org/wiki/File:The Moonstone 1st ed.jpg",
+ },
+ }
+
+ result = self.connector.get_cover_url(cover_blob)
+ self.assertEqual(
+ result,
+ "https://commons.wikimedia.org/wiki/Special:FilePath/The%20Moonstone%201st%20ed.jpg?width=1000",
+ )
+
+ @responses.activate
+ def test_resolve_keys(self):
+ """makes an http request"""
+ responses.add(
+ responses.GET,
+ "https://inventaire.io?action=by-uris&uris=wd:Q465821",
+ json={
+ "entities": {
+ "wd:Q465821": {
+ "type": "genre",
+ "labels": {
+ "nl": "briefroman",
+ "en": "epistolary novel",
+ "de-ch": "Briefroman",
+ "en-ca": "Epistolary novel",
+ "nb": "brev- og dagbokroman",
+ },
+ "descriptions": {
+ "en": "novel written as a series of documents",
+ "es": "novela escrita como una serie de documentos",
+ "eo": "romano en la formo de serio de leteroj",
+ },
+ },
+ "redirects": {},
+ }
+ },
+ )
+ responses.add(
+ responses.GET,
+ "https://inventaire.io?action=by-uris&uris=wd:Q208505",
+ json={
+ "entities": {
+ "wd:Q208505": {
+ "type": "genre",
+ "labels": {
+ "en": "crime novel",
+ },
+ },
+ }
+ },
+ )
+
+ keys = [
+ "wd:Q465821",
+ "wd:Q208505",
+ ]
+ result = self.connector.resolve_keys(keys)
+ self.assertEqual(result, ["epistolary novel", "crime novel"])
+
+ def test_isbn_search(self):
+ """another search type"""
+ search_file = pathlib.Path(__file__).parent.joinpath(
+ "../data/inventaire_isbn_search.json"
+ )
+ search_results = json.loads(search_file.read_bytes())
+
+ results = self.connector.parse_isbn_search_data(search_results)
+ formatted = self.connector.format_isbn_search_result(results[0])
+
+ self.assertEqual(formatted.title, "L'homme aux cercles bleus")
+ self.assertEqual(
+ formatted.key,
+ "https://inventaire.io?action=by-uris&uris=isbn:9782290349229",
+ )
+ self.assertEqual(
+ formatted.cover,
+ "https://covers.inventaire.io/img/entities/12345",
+ )
diff --git a/bookwyrm/tests/connectors/test_self_connector.py b/bookwyrm/tests/connectors/test_self_connector.py
index eee7d00c..db97b65a 100644
--- a/bookwyrm/tests/connectors/test_self_connector.py
+++ b/bookwyrm/tests/connectors/test_self_connector.py
@@ -84,11 +84,11 @@ class SelfConnector(TestCase):
title="Edition 1 Title", parent_work=work
)
edition_2 = models.Edition.objects.create(
- title="Edition 2 Title", parent_work=work
+ title="Edition 2 Title",
+ parent_work=work,
+ edition_rank=20, # that's default babey
)
edition_3 = models.Edition.objects.create(title="Fish", parent_work=work)
- work.default_edition = edition_2
- work.save()
# pick the best edition
results = self.connector.search("Edition 1 Title")
diff --git a/bookwyrm/tests/data/inventaire_edition.json b/bookwyrm/tests/data/inventaire_edition.json
new file mode 100644
index 00000000..1150bc9b
--- /dev/null
+++ b/bookwyrm/tests/data/inventaire_edition.json
@@ -0,0 +1,45 @@
+{
+ "entities": {
+ "isbn:9780375757853": {
+ "_id": "7beee121a8d9ac345cdf4e9128577723",
+ "_rev": "2-ac318b04b953ca3894deb77fee28211c",
+ "type": "edition",
+ "labels": {},
+ "claims": {
+ "wdt:P31": [
+ "wd:Q3331189"
+ ],
+ "wdt:P212": [
+ "978-0-375-75785-3"
+ ],
+ "wdt:P957": [
+ "0-375-75785-6"
+ ],
+ "wdt:P407": [
+ "wd:Q1860"
+ ],
+ "wdt:P1476": [
+ "The Moonstone"
+ ],
+ "wdt:P577": [
+ "2001"
+ ],
+ "wdt:P629": [
+ "wd:Q2362563"
+ ],
+ "invp:P2": [
+ "d46a8eac7555afa479b8bbb5149f35858e8e19c4"
+ ]
+ },
+ "created": 1495452670475,
+ "updated": 1541032981834,
+ "version": 3,
+ "uri": "isbn:9780375757853",
+ "originalLang": "en",
+ "image": {
+ "url": "/img/entities/d46a8eac7555afa479b8bbb5149f35858e8e19c4"
+ }
+ }
+ },
+ "redirects": {}
+}
diff --git a/bookwyrm/tests/data/inventaire_isbn_search.json b/bookwyrm/tests/data/inventaire_isbn_search.json
new file mode 100644
index 00000000..7328a78f
--- /dev/null
+++ b/bookwyrm/tests/data/inventaire_isbn_search.json
@@ -0,0 +1,48 @@
+{
+ "entities": {
+ "isbn:9782290349229": {
+ "_id": "d59e3e64f92c6340fbb10c5dcf7c0abf",
+ "_rev": "3-079ed51158a001dc74caafb21cff1c22",
+ "type": "edition",
+ "labels": {},
+ "claims": {
+ "wdt:P31": [
+ "wd:Q3331189"
+ ],
+ "wdt:P212": [
+ "978-2-290-34922-9"
+ ],
+ "wdt:P957": [
+ "2-290-34922-4"
+ ],
+ "wdt:P407": [
+ "wd:Q150"
+ ],
+ "wdt:P1476": [
+ "L'homme aux cercles bleus"
+ ],
+ "wdt:P629": [
+ "wd:Q3203603"
+ ],
+ "wdt:P123": [
+ "wd:Q3156592"
+ ],
+ "invp:P2": [
+ "57883743aa7c6ad25885a63e6e94349ec4f71562"
+ ],
+ "wdt:P577": [
+ "2005-05-01"
+ ]
+ },
+ "created": 1485023383338,
+ "updated": 1609171008418,
+ "version": 5,
+ "uri": "isbn:9782290349229",
+ "originalLang": "fr",
+ "image": {
+ "url": "/img/entities/12345"
+ }
+ }
+ },
+ "redirects": {}
+}
diff --git a/bookwyrm/tests/data/inventaire_search.json b/bookwyrm/tests/data/inventaire_search.json
new file mode 100644
index 00000000..e80e593a
--- /dev/null
+++ b/bookwyrm/tests/data/inventaire_search.json
@@ -0,0 +1,111 @@
+{
+ "results": [
+ {
+ "id": "Q7766679",
+ "type": "works",
+ "uri": "wd:Q7766679",
+ "label": "The Stories of Vladimir Nabokov",
+ "description": "book by Vladimir Nabokov",
+ "image": [
+ "ddb32e115a28dcc0465023869ba19f6868ec4042"
+ ],
+ "_score": 25.180836,
+ "_popularity": 4
+ },
+ {
+ "id": "Q47407212",
+ "type": "works",
+ "uri": "wd:Q47407212",
+ "label": "Conversations with Vladimir Nabokov",
+ "description": "book edited by Robert Golla",
+ "image": [],
+ "_score": 24.41498,
+ "_popularity": 2
+ },
+ {
+ "id": "Q6956987",
+ "type": "works",
+ "uri": "wd:Q6956987",
+ "label": "Nabokov's Congeries",
+ "description": "book by Vladimir Nabokov",
+ "image": [],
+ "_score": 22.343866,
+ "_popularity": 2
+ },
+ {
+ "id": "Q6956986",
+ "type": "works",
+ "uri": "wd:Q6956986",
+ "label": "Nabokov's Butterflies",
+ "description": "book by Brian Boyd",
+ "image": [],
+ "_score": 22.343866,
+ "_popularity": 2
+ },
+ {
+ "id": "Q47472170",
+ "type": "works",
+ "uri": "wd:Q47472170",
+ "label": "A Reader's Guide to Nabokov's \"Lolita\"",
+ "description": "book by Julian W. Connolly",
+ "image": [],
+ "_score": 19.482553,
+ "_popularity": 2
+ },
+ {
+ "id": "Q7936323",
+ "type": "works",
+ "uri": "wd:Q7936323",
+ "label": "Visiting Mrs Nabokov: And Other Excursions",
+ "description": "book by Martin Amis",
+ "image": [],
+ "_score": 18.684965,
+ "_popularity": 2
+ },
+ {
+ "id": "1732d81bf7376e04da27568a778561a4",
+ "type": "works",
+ "uri": "inv:1732d81bf7376e04da27568a778561a4",
+ "label": "Nabokov's Dark Cinema",
+ "image": [
+ "7512805a53da569b11bf29cc3fb272c969619749"
+ ],
+ "_score": 16.56681,
+ "_popularity": 1
+ },
+ {
+ "id": "00f118336b02219e1bddc8fa93c56050",
+ "type": "works",
+ "uri": "inv:00f118336b02219e1bddc8fa93c56050",
+ "label": "The Cambridge Companion to Nabokov",
+ "image": [
+ "0683a059fb95430cfa73334f9eff2ef377f3ae3d"
+ ],
+ "_score": 15.502292,
+ "_popularity": 1
+ },
+ {
+ "id": "6e59f968a1cd00dbedeb1964dec47507",
+ "type": "works",
+ "uri": "inv:6e59f968a1cd00dbedeb1964dec47507",
+ "label": "Vladimir Nabokov : selected letters, 1940-1977",
+ "image": [
+ "e3ce8c0ee89d576adf2651a6e5ce55fc6d9f8cb3"
+ ],
+ "_score": 15.019735,
+ "_popularity": 1
+ },
+ {
+ "id": "Q127149",
+ "type": "works",
+ "uri": "wd:Q127149",
+ "label": "Lolita",
+ "description": "novel by Vladimir Nabokov",
+ "image": [
+ "51cbfdbf7257b1a6bb3ea3fbb167dbce1fb44a0e"
+ ],
+ "_score": 13.458428,
+ "_popularity": 32
+ }
+ ]
+}
diff --git a/bookwyrm/tests/data/inventaire_work.json b/bookwyrm/tests/data/inventaire_work.json
new file mode 100644
index 00000000..635c52f3
--- /dev/null
+++ b/bookwyrm/tests/data/inventaire_work.json
@@ -0,0 +1,155 @@
+{
+ "entities": {
+ "wd:Q2362563": {
+ "type": "work",
+ "labels": {
+ "zh-hans": "月亮宝石",
+ "zh-hant": "月亮寶石",
+ "zh-hk": "月光石",
+ "zh-tw": "月光石",
+ "cy": "The Moonstone",
+ "ml": "ദ മൂൺസ്റ്റോൺ",
+ "ja": "月長石",
+ "te": "ది మూన్ స్టోన్",
+ "ru": "Лунный камень",
+ "fr": "La Pierre de lune",
+ "en": "The Moonstone",
+ "es": "La piedra lunar",
+ "it": "La Pietra di Luna",
+ "zh": "月亮宝石",
+ "pl": "Kamień Księżycowy",
+ "sr": "2 Јн",
+ "ta": "moon stone",
+ "ar": "حجر القمر",
+ "fa": "ماهالماس",
+ "uk": "Місячний камінь",
+ "nl": "The Moonstone",
+ "de": "Der Monddiamant",
+ "sl": "Diamant",
+ "sv": "Månstenen",
+ "he": "אבן הירח",
+ "eu": "Ilargi-harriak",
+ "bg": "Лунният камък",
+ "ka": "მთვარის ქვა",
+ "eo": "La Lunŝtono",
+ "hy": "Լուսնաքար",
+ "ro": "Piatra Lunii",
+ "ca": "The Moonstone",
+ "is": "The Moonstone"
+ },
+ "descriptions": {
+ "it": "romanzo scritto da Wilkie Collins",
+ "en": "novel by Wilkie Collins",
+ "de": "Buch von Wilkie Collins",
+ "nl": "boek van Wilkie Collins",
+ "ru": "роман Уилки Коллинза",
+ "he": "רומן מאת וילקי קולינס",
+ "ar": "رواية من تأليف ويلكي كولينز",
+ "fr": "livre de Wilkie Collins",
+ "es": "libro de Wilkie Collins",
+ "bg": "роман на Уилки Колинс",
+ "ka": "უილკი კოლინსის რომანი",
+ "eo": "angalingva romano far Wilkie Collins",
+ "ro": "roman de Wilkie Collins"
+ },
+ "aliases": {
+ "zh": [
+ "月光石"
+ ],
+ "ml": [
+ "The Moonstone"
+ ],
+ "fr": [
+ "The Moonstone"
+ ],
+ "it": [
+ "Il diamante indiano",
+ "La pietra della luna",
+ "La maledizione del diamante indiano"
+ ],
+ "ro": [
+ "The Moonstone"
+ ]
+ },
+ "claims": {
+ "wdt:P18": [
+ "The Moonstone 1st ed.jpg"
+ ],
+ "wdt:P31": [
+ "wd:Q7725634"
+ ],
+ "wdt:P50": [
+ "wd:Q210740"
+ ],
+ "wdt:P123": [
+ "wd:Q4457856"
+ ],
+ "wdt:P136": [
+ "wd:Q465821",
+ "wd:Q208505",
+ "wd:Q10992055"
+ ],
+ "wdt:P156": [
+ "wd:Q7228798"
+ ],
+ "wdt:P268": [
+ "12496407z"
+ ],
+ "wdt:P407": [
+ "wd:Q7979"
+ ],
+ "wdt:P577": [
+ "1868"
+ ],
+ "wdt:P1433": [
+ "wd:Q21"
+ ],
+ "wdt:P1476": [
+ "The Moonstone"
+ ],
+ "wdt:P1680": [
+ "A Romance"
+ ],
+ "wdt:P2034": [
+ "155"
+ ]
+ },
+ "sitelinks": {
+ "arwiki": "حجر القمر (رواية)",
+ "bgwiki": "Лунният камък (роман)",
+ "cywiki": "The Moonstone",
+ "dewiki": "Der Monddiamant",
+ "enwiki": "The Moonstone",
+ "enwikisource": "The Moonstone",
+ "eswiki": "La piedra lunar",
+ "euwiki": "Ilargi-harria",
+ "fawiki": "ماهالماس",
+ "frwiki": "La Pierre de lune (roman de Wilkie Collins)",
+ "hewiki": "אבן הירח",
+ "hywiki": "Լուսնաքար",
+ "iswiki": "The Moonstone",
+ "itwiki": "La pietra di Luna",
+ "jawiki": "月長石 (小説)",
+ "mlwiki": "ദ മൂൺസ്റ്റോൺ",
+ "plwiki": "Kamień Księżycowy (powieść)",
+ "ruwiki": "Лунный камень (роман)",
+ "slwiki": "Diamant (roman)",
+ "srwikisource": "Нови завјет (Караџић) / 2. Јованова",
+ "svwiki": "Månstenen",
+ "tewiki": "ది మూన్స్టోన్",
+ "ukwiki": "Місячний камінь (роман)",
+ "zhwiki": "月亮宝石"
+ },
+ "uri": "wd:Q2362563",
+ "image": {
+ "url": "https://commons.wikimedia.org/wiki/Special:FilePath/The%20Moonstone%201st%20ed.jpg?width=1000",
+ "file": "The Moonstone 1st ed.jpg",
+ "credits": {
+ "text": "Wikimedia Commons",
+ "url": "https://commons.wikimedia.org/wiki/File:The Moonstone 1st ed.jpg"
+ }
+ }
+ }
+ },
+ "redirects": {}
+}
diff --git a/bookwyrm/tests/models/test_book_model.py b/bookwyrm/tests/models/test_book_model.py
index c80cc4a8..cad00d43 100644
--- a/bookwyrm/tests/models/test_book_model.py
+++ b/bookwyrm/tests/models/test_book_model.py
@@ -84,9 +84,3 @@ class Book(TestCase):
self.first_edition.description = "hi"
self.first_edition.save()
self.assertEqual(self.first_edition.edition_rank, 1)
-
- # default edition
- self.work.default_edition = self.first_edition
- self.work.save()
- self.first_edition.refresh_from_db()
- self.assertEqual(self.first_edition.edition_rank, 20)
diff --git a/bookwyrm/tests/models/test_readthrough_model.py b/bookwyrm/tests/models/test_readthrough_model.py
index 93e9e654..986b739b 100644
--- a/bookwyrm/tests/models/test_readthrough_model.py
+++ b/bookwyrm/tests/models/test_readthrough_model.py
@@ -2,7 +2,7 @@
from django.test import TestCase
from django.core.exceptions import ValidationError
-from bookwyrm import models, settings
+from bookwyrm import models
class ReadThrough(TestCase):
@@ -19,8 +19,6 @@ class ReadThrough(TestCase):
self.edition = models.Edition.objects.create(
title="Example Edition", parent_work=self.work
)
- self.work.default_edition = self.edition
- self.work.save()
self.readthrough = models.ReadThrough.objects.create(
user=self.user, book=self.edition
diff --git a/bookwyrm/tests/views/inbox/test_inbox_create.py b/bookwyrm/tests/views/inbox/test_inbox_create.py
index e7a12024..958dfee8 100644
--- a/bookwyrm/tests/views/inbox/test_inbox_create.py
+++ b/bookwyrm/tests/views/inbox/test_inbox_create.py
@@ -127,6 +127,43 @@ class InboxCreate(TestCase):
self.assertTrue(models.Notification.objects.filter(user=self.local_user))
self.assertEqual(models.Notification.objects.get().notification_type, "REPLY")
+ def test_create_rating(self):
+ """a remote rating activity"""
+ book = models.Edition.objects.create(
+ title="Test Book", remote_id="https://example.com/book/1"
+ )
+ activity = self.create_json
+ activity["object"] = {
+ "id": "https://example.com/user/mouse/reviewrating/12",
+ "type": "Rating",
+ "published": "2021-04-29T21:27:30.014235+00:00",
+ "attributedTo": "https://example.com/user/mouse",
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc": ["https://example.com/user/mouse/followers"],
+ "replies": {
+ "id": "https://example.com/user/mouse/reviewrating/12/replies",
+ "type": "OrderedCollection",
+ "totalItems": 0,
+ "first": "https://example.com/user/mouse/reviewrating/12/replies?page=1",
+ "last": "https://example.com/user/mouse/reviewrating/12/replies?page=1",
+ "@context": "https://www.w3.org/ns/activitystreams",
+ },
+ "inReplyTo": "",
+ "summary": "",
+ "tag": [],
+ "attachment": [],
+ "sensitive": False,
+ "inReplyToBook": "https://example.com/book/1",
+ "rating": 3,
+ "@context": "https://www.w3.org/ns/activitystreams",
+ }
+ with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
+ views.inbox.activity_task(activity)
+ self.assertTrue(redis_mock.called)
+ rating = models.ReviewRating.objects.first()
+ self.assertEqual(rating.book, book)
+ self.assertEqual(rating.rating, 3.0)
+
def test_create_list(self):
"""a new list"""
activity = self.create_json
diff --git a/bookwyrm/tests/views/test_helpers.py b/bookwyrm/tests/views/test_helpers.py
index e2e041e9..0dddd2a1 100644
--- a/bookwyrm/tests/views/test_helpers.py
+++ b/bookwyrm/tests/views/test_helpers.py
@@ -219,7 +219,7 @@ class ViewsHelpers(TestCase):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
# 1 shared follow
self.local_user.following.add(user_2)
- user_1.following.add(user_2)
+ user_1.followers.add(user_2)
# 1 shared book
models.ShelfBook.objects.create(
@@ -264,7 +264,7 @@ class ViewsHelpers(TestCase):
local=True,
localname=i,
)
- user.followers.add(user_1)
+ user.following.add(user_1)
user.followers.add(self.local_user)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
diff --git a/bookwyrm/tests/views/test_readthrough.py b/bookwyrm/tests/views/test_readthrough.py
index c9ebf216..882c7929 100644
--- a/bookwyrm/tests/views/test_readthrough.py
+++ b/bookwyrm/tests/views/test_readthrough.py
@@ -20,8 +20,6 @@ class ReadThrough(TestCase):
self.edition = models.Edition.objects.create(
title="Example Edition", parent_work=self.work
)
- self.work.default_edition = self.edition
- self.work.save()
self.user = models.User.objects.create_user(
"cinco", "cinco@example.com", "seissiete", local=True, localname="cinco"
diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py
index 53ceeaa8..24c80b04 100644
--- a/bookwyrm/urls.py
+++ b/bookwyrm/urls.py
@@ -43,7 +43,7 @@ urlpatterns = [
re_path("^api/updates/notifications/?$", views.get_notification_count),
re_path("^api/updates/stream/(?P