Merge branch 'main' into frontend-book-cover

This commit is contained in:
Fabien Basmaison
2021-04-30 09:54:42 +02:00
48 changed files with 1378 additions and 352 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -80,6 +80,9 @@
{% if book.openlibrary_key %}
<p><a href="https://openlibrary.org/books/{{ book.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a></p>
{% endif %}
{% if book.inventaire_id %}
<p><a href="https://inventaire.io/entity/{{ book.inventaire_id }}" target="_blank" rel="noopener">{% trans "View on Inventaire" %}</a></p>
{% endif %}
</section>
</div>

View File

@ -11,10 +11,15 @@
<div class="block columns">
<div class="column">
<h2 class="title">{% trans "Matching Books" %}</h2>
<h2 class="title is-4">{% trans "Matching Books" %}</h2>
<section class="block">
{% if not local_results.results %}
<p>{% blocktrans %}No books found for "{{ query }}"{% endblocktrans %}</p>
<p><em>{% blocktrans %}No books found for "{{ query }}"{% endblocktrans %}</em></p>
{% if not user.is_authenticated %}
<p>
<a href="{% url 'login' %}">{% trans "Log in to import or add books." %}</a>
</p>
{% endif %}
{% else %}
<ul>
{% for result in local_results.results %}
@ -29,39 +34,57 @@
{% if request.user.is_authenticated %}
{% if book_results|slice:":1" and local_results.results %}
<div class="block">
<p>
<h3 class="title is-6">
{% trans "Didn't find what you were looking for?" %}
</p>
</h3>
{% trans "Show results from other catalogues" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text small=True controls_text="more-results" %}
{% if local_results.results %}
{% trans "Hide results from other catalogues" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text small=True controls_text="more-results" %}
{% endif %}
</div>
{% endif %}
<div class="{% if local_results.results %}is-hidden{% endif %}" id="more-results">
{% for result_set in book_results|slice:"1:" %}
{% if result_set.results %}
<section class="block">
<section class="box has-background-white-bis">
{% if not result_set.connector.local %}
<h3 class="title is-5">
Results from <a href="{{ result_set.connector.base_url }}" target="_blank">{% if result_set.connector.name %}{{ result_set.connector.name }}{% else %}{{ result_set.connector.identifier }}{% endif %}</a>
</h3>
<header class="columns is-mobile">
<div class="column">
<h3 class="title is-5">
Results from
<a href="{{ result_set.connector.base_url }}" target="_blank">{{ result_set.connector.name|default:result_set.connector.identifier }}</a>
</h3>
</div>
<div class="column is-narrow">
{% trans "Show" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text small=True controls_text="more-results-panel" controls_uid=result_set.connector.identifier class="is-small" icon="arrow-down" pressed=forloop.first %}
</div>
</header>
{% endif %}
<ul>
{% for result in result_set.results %}
<li class="mb-5">
{% include 'snippets/search_result_text.html' with result=result remote_result=True %}
</li>
{% endfor %}
</ul>
<div class="box has-background-white is-shadowless{% if not forloop.first %} is-hidden{% endif %}" id="more-results-panel-{{ result_set.connector.identifier }}">
<div class="is-flex is-flex-direction-row-reverse">
<div>
{% trans "Close" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with label=button_text class="delete" nonbutton=True controls_text="more-results-panel" controls_uid=result_set.connector.identifier pressed=forloop.first %}
</div>
<ul class="is-flex-grow-1">
{% for result in result_set.results %}
<li class="mb-5">
{% include 'snippets/search_result_text.html' with result=result remote_result=True %}
</li>
{% endfor %}
</ul>
</div>
</div>
</section>
{% endif %}
{% endfor %}
{% if local_results.results %}
{% trans "Hide results from other catalogues" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text small=True controls_text="more-results" %}
{% endif %}
</div>
<div class="block">
@ -70,10 +93,11 @@
{% endif %}
</div>
<div class="column">
<section class="block">
<h2 class="title">{% trans "Matching Users" %}</h2>
{% if request.user.is_authenticated %}
<section class="box">
<h2 class="title is-4">{% trans "Matching Users" %}</h2>
{% if not user_results %}
<p>{% blocktrans %}No users found for "{{ query }}"{% endblocktrans %}</p>
<p><em>{% blocktrans %}No users found for "{{ query }}"{% endblocktrans %}</em></p>
{% endif %}
<ul>
{% for result in user_results %}
@ -87,10 +111,11 @@
{% endfor %}
</ul>
</section>
<section class="block">
<h2 class="title">{% trans "Lists" %}</h2>
{% endif %}
<section class="box">
<h2 class="title is-4">{% trans "Lists" %}</h2>
{% if not list_results %}
<p>{% blocktrans %}No lists found for "{{ query }}"{% endblocktrans %}</p>
<p><em>{% blocktrans %}No lists found for "{{ query }}"{% endblocktrans %}</em></p>
{% endif %}
{% for result in list_results %}
<div class="block">

View File

@ -8,17 +8,17 @@
<p>
<strong>
<a
href="{{ result.key }}"
href="{{ result.view_link|default:result.key }}"
{% if remote_result %}
rel=”noopener”
target="_blank"
{% endif %}
>{{ result.title }}</a>
</strong>
</p>
<p>
{% if result.author %}
{% blocktrans with author=result.author %}by {{ author }}{% endblocktrans %}
{{ result.author }}
{% endif %}
{% if result.year %}

View File

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

View File

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

View File

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

View File

@ -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": {}
}

View File

@ -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": {}
}

View File

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

View File

@ -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": {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -43,7 +43,7 @@ urlpatterns = [
re_path("^api/updates/notifications/?$", views.get_notification_count),
re_path("^api/updates/stream/(?P<stream>[a-z]+)/?$", views.get_unread_status_count),
# authentication
re_path(r"^login/?$", views.Login.as_view()),
re_path(r"^login/?$", views.Login.as_view(), name="login"),
re_path(r"^register/?$", views.Register.as_view()),
re_path(r"^logout/?$", views.Logout.as_view()),
re_path(r"^password-reset/?$", views.PasswordResetRequest.as_view()),

View File

@ -27,7 +27,7 @@ class Author(View):
).distinct()
data = {
"author": author,
"books": [b.get_default_edition() for b in books],
"books": [b.default_edition for b in books],
}
return TemplateResponse(request, "author.html", data)

View File

@ -39,7 +39,7 @@ class Book(View):
return ActivitypubResponse(book.to_activity())
if isinstance(book, models.Work):
book = book.get_default_edition()
book = book.default_edition
if not book or not book.parent_work:
return HttpResponseNotFound()
@ -156,7 +156,6 @@ class EditBook(View):
),
}
)
print(data["author_matches"])
# we're creating a new book
if not book:

View File

@ -123,7 +123,7 @@ def get_edition(book_id):
"""look up a book in the db and return an edition"""
book = models.Book.objects.select_subclasses().get(id=book_id)
if isinstance(book, models.Work):
book = book.get_default_edition()
book = book.default_edition
return book
@ -190,11 +190,11 @@ def get_annotated_users(user, *args, **kwargs):
.exclude(Q(id__in=user.blocks.all()) | Q(blocks=user))
.annotate(
mutuals=Count(
"following",
"followers",
filter=Q(
~Q(id=user.id),
~Q(id__in=user.following.all()),
following__in=user.following.all(),
followers__in=user.following.all(),
),
distinct=True,
),

View File

@ -30,27 +30,30 @@ class Search(View):
)
return JsonResponse([r.json() for r in book_results], safe=False)
data = {"query": query or ""}
# use webfinger for mastodon style account@domain.com username
if query and re.match(regex.full_username, query):
handle_remote_webfinger(query)
# do a user search
user_results = (
models.User.viewer_aware_objects(request.user)
.annotate(
similarity=Greatest(
TrigramSimilarity("username", query),
TrigramSimilarity("localname", query),
if request.user.is_authenticated:
data["user_results"] = (
models.User.viewer_aware_objects(request.user)
.annotate(
similarity=Greatest(
TrigramSimilarity("username", query),
TrigramSimilarity("localname", query),
)
)
.filter(
similarity__gt=0.5,
)
.order_by("-similarity")[:10]
)
.filter(
similarity__gt=0.5,
)
.order_by("-similarity")[:10]
)
# any relevent lists?
list_results = (
data["list_results"] = (
privacy_filter(
request.user,
models.List.objects,
@ -68,11 +71,7 @@ class Search(View):
.order_by("-similarity")[:10]
)
book_results = connector_manager.search(query, min_confidence=min_confidence)
data = {
"book_results": book_results,
"user_results": user_results,
"list_results": list_results,
"query": query or "",
}
data["book_results"] = connector_manager.search(
query, min_confidence=min_confidence
)
return TemplateResponse(request, "search_results.html", data)