forked from mirrors/bookwyrm
isbn search
This commit is contained in:
parent
79c3ae1c44
commit
703ff60271
18 changed files with 358 additions and 4 deletions
bookwyrm
|
@ -26,6 +26,7 @@ class AbstractMinimalConnector(ABC):
|
||||||
'books_url',
|
'books_url',
|
||||||
'covers_url',
|
'covers_url',
|
||||||
'search_url',
|
'search_url',
|
||||||
|
'isbn_search_url',
|
||||||
'max_query_count',
|
'max_query_count',
|
||||||
'name',
|
'name',
|
||||||
'identifier',
|
'identifier',
|
||||||
|
@ -61,6 +62,30 @@ class AbstractMinimalConnector(ABC):
|
||||||
results.append(self.format_search_result(doc))
|
results.append(self.format_search_result(doc))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
def isbn_search(self, query):
|
||||||
|
''' isbn search '''
|
||||||
|
params = {}
|
||||||
|
resp = requests.get(
|
||||||
|
'%s%s' % (self.isbn_search_url, query),
|
||||||
|
params=params,
|
||||||
|
headers={
|
||||||
|
'Accept': 'application/json; charset=utf-8',
|
||||||
|
'User-Agent': settings.USER_AGENT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if not resp.ok:
|
||||||
|
resp.raise_for_status()
|
||||||
|
try:
|
||||||
|
data = resp.json()
|
||||||
|
except ValueError as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise ConnectorException('Unable to parse json response', e)
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for doc in self.parse_isbn_search_data(data):
|
||||||
|
results.append(self.format_isbn_search_result(doc))
|
||||||
|
return results
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_or_create_book(self, remote_id):
|
def get_or_create_book(self, remote_id):
|
||||||
''' pull up a book record by whatever means possible '''
|
''' pull up a book record by whatever means possible '''
|
||||||
|
@ -73,6 +98,14 @@ class AbstractMinimalConnector(ABC):
|
||||||
def format_search_result(self, search_result):
|
def format_search_result(self, search_result):
|
||||||
''' create a SearchResult obj from json '''
|
''' create a SearchResult obj from json '''
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def parse_isbn_search_data(self, data):
|
||||||
|
''' turn the result json from a search into a list '''
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
''' create a SearchResult obj from json '''
|
||||||
|
|
||||||
|
|
||||||
class AbstractConnector(AbstractMinimalConnector):
|
class AbstractConnector(AbstractMinimalConnector):
|
||||||
''' generic book data connector '''
|
''' generic book data connector '''
|
||||||
|
|
|
@ -19,3 +19,11 @@ class Connector(AbstractMinimalConnector):
|
||||||
def format_search_result(self, search_result):
|
def format_search_result(self, search_result):
|
||||||
search_result['connector'] = self
|
search_result['connector'] = self
|
||||||
return SearchResult(**search_result)
|
return SearchResult(**search_result)
|
||||||
|
|
||||||
|
def parse_isbn_search_data(self, data):
|
||||||
|
return data
|
||||||
|
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
search_result['connector'] = self
|
||||||
|
return SearchResult(**search_result)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
''' interface with whatever connectors the app has '''
|
''' interface with whatever connectors the app has '''
|
||||||
import importlib
|
import importlib
|
||||||
|
import re
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from requests import HTTPError
|
from requests import HTTPError
|
||||||
|
@ -15,13 +16,31 @@ class ConnectorException(HTTPError):
|
||||||
def search(query, min_confidence=0.1):
|
def search(query, min_confidence=0.1):
|
||||||
''' find books based on arbitary keywords '''
|
''' find books based on arbitary keywords '''
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
|
# Have we got a ISBN ?
|
||||||
|
isbn = re.sub('[\W_]', '', query)
|
||||||
|
maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13
|
||||||
|
|
||||||
dedup_slug = lambda r: '%s/%s/%s' % (r.title, r.author, r.year)
|
dedup_slug = lambda r: '%s/%s/%s' % (r.title, r.author, r.year)
|
||||||
result_index = set()
|
result_index = set()
|
||||||
for connector in get_connectors():
|
for connector in get_connectors():
|
||||||
try:
|
result_set = None
|
||||||
result_set = connector.search(query, min_confidence=min_confidence)
|
if maybe_isbn:
|
||||||
except (HTTPError, ConnectorException):
|
# Search on ISBN
|
||||||
continue
|
if not connector.isbn_search_url or connector.isbn_search_url == '':
|
||||||
|
result_set = []
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
result_set = connector.isbn_search(isbn)
|
||||||
|
except (HTTPError, ConnectorException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# if no isbn search or results, we fallback to generic search
|
||||||
|
if result_set == None or result_set == []:
|
||||||
|
try:
|
||||||
|
result_set = connector.search(query, min_confidence=min_confidence)
|
||||||
|
except (HTTPError, ConnectorException):
|
||||||
|
continue
|
||||||
|
|
||||||
result_set = [r for r in result_set \
|
result_set = [r for r in result_set \
|
||||||
if dedup_slug(r) not in result_index]
|
if dedup_slug(r) not in result_index]
|
||||||
|
@ -41,6 +60,12 @@ def local_search(query, min_confidence=0.1, raw=False):
|
||||||
return connector.search(query, min_confidence=min_confidence, raw=raw)
|
return connector.search(query, min_confidence=min_confidence, raw=raw)
|
||||||
|
|
||||||
|
|
||||||
|
def isbn_local_search(query, raw=False):
|
||||||
|
''' only look at local search results '''
|
||||||
|
connector = load_connector(models.Connector.objects.get(local=True))
|
||||||
|
return connector.isbn_search(query, raw=raw)
|
||||||
|
|
||||||
|
|
||||||
def first_search_result(query, min_confidence=0.1):
|
def first_search_result(query, min_confidence=0.1):
|
||||||
''' search until you find a result that fits '''
|
''' search until you find a result that fits '''
|
||||||
for connector in get_connectors():
|
for connector in get_connectors():
|
||||||
|
|
|
@ -129,6 +129,22 @@ class Connector(AbstractConnector):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_isbn_search_data(self, data):
|
||||||
|
return list(data.values())
|
||||||
|
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
# build the remote id from the openlibrary key
|
||||||
|
key = self.books_url + search_result['key']
|
||||||
|
authors = search_result.get('authors') or [{'name': 'Unknown'}]
|
||||||
|
author_names = [ author.get('name') for author in authors]
|
||||||
|
return SearchResult(
|
||||||
|
title=search_result.get('title'),
|
||||||
|
key=key,
|
||||||
|
author=', '.join(author_names),
|
||||||
|
connector=self,
|
||||||
|
year=search_result.get('publish_date'),
|
||||||
|
)
|
||||||
|
|
||||||
def load_edition_data(self, olkey):
|
def load_edition_data(self, olkey):
|
||||||
''' query openlibrary for editions of a work '''
|
''' query openlibrary for editions of a work '''
|
||||||
url = '%s/works/%s/editions' % (self.books_url, olkey)
|
url = '%s/works/%s/editions' % (self.books_url, olkey)
|
||||||
|
|
|
@ -33,6 +33,31 @@ class Connector(AbstractConnector):
|
||||||
search_results.sort(key=lambda r: r.confidence, reverse=True)
|
search_results.sort(key=lambda r: r.confidence, reverse=True)
|
||||||
return search_results
|
return search_results
|
||||||
|
|
||||||
|
def isbn_search(self, query, raw=False):
|
||||||
|
''' search your local database '''
|
||||||
|
if not query:
|
||||||
|
return []
|
||||||
|
|
||||||
|
filters = [{f: query} for f in ['isbn_10', 'isbn_13']]
|
||||||
|
results = models.Edition.objects.filter(
|
||||||
|
reduce(operator.or_, (Q(**f) for f in filters))
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
# when there are multiple editions of the same work, pick the default.
|
||||||
|
# it would be odd for this to happen.
|
||||||
|
results = results.filter(parent_work__default_edition__id=F('id')) \
|
||||||
|
or results
|
||||||
|
|
||||||
|
search_results = []
|
||||||
|
for result in results:
|
||||||
|
if raw:
|
||||||
|
search_results.append(result)
|
||||||
|
else:
|
||||||
|
search_results.append(self.format_search_result(result))
|
||||||
|
if len(search_results) >= 10:
|
||||||
|
break
|
||||||
|
return search_results
|
||||||
|
|
||||||
|
|
||||||
def format_search_result(self, search_result):
|
def format_search_result(self, search_result):
|
||||||
return SearchResult(
|
return SearchResult(
|
||||||
|
@ -47,6 +72,19 @@ class Connector(AbstractConnector):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
return SearchResult(
|
||||||
|
title=search_result.title,
|
||||||
|
key=search_result.remote_id,
|
||||||
|
author=search_result.author_text,
|
||||||
|
year=search_result.published_date.year if \
|
||||||
|
search_result.published_date else None,
|
||||||
|
connector=self,
|
||||||
|
confidence=search_result.rank if \
|
||||||
|
hasattr(search_result, 'rank') else 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_work_data(self, data):
|
def is_work_data(self, data):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -59,6 +97,10 @@ class Connector(AbstractConnector):
|
||||||
def get_authors_from_data(self, data):
|
def get_authors_from_data(self, data):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def parse_isbn_search_data(self, data):
|
||||||
|
''' it's already in the right format, don't even worry about it '''
|
||||||
|
return data
|
||||||
|
|
||||||
def parse_search_data(self, data):
|
def parse_search_data(self, data):
|
||||||
''' it's already in the right format, don't even worry about it '''
|
''' it's already in the right format, don't even worry about it '''
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -66,6 +66,7 @@ def init_connectors():
|
||||||
books_url='https://%s/book' % DOMAIN,
|
books_url='https://%s/book' % DOMAIN,
|
||||||
covers_url='https://%s/images/covers' % DOMAIN,
|
covers_url='https://%s/images/covers' % DOMAIN,
|
||||||
search_url='https://%s/search?q=' % DOMAIN,
|
search_url='https://%s/search?q=' % DOMAIN,
|
||||||
|
isbn_search_url='https://%s/isbn/' % DOMAIN,
|
||||||
priority=1,
|
priority=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -77,6 +78,7 @@ def init_connectors():
|
||||||
books_url='https://bookwyrm.social/book',
|
books_url='https://bookwyrm.social/book',
|
||||||
covers_url='https://bookwyrm.social/images/covers',
|
covers_url='https://bookwyrm.social/images/covers',
|
||||||
search_url='https://bookwyrm.social/search?q=',
|
search_url='https://bookwyrm.social/search?q=',
|
||||||
|
isbn_search_url='https://bookwyrm.social/isbn/',
|
||||||
priority=2,
|
priority=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -88,6 +90,7 @@ def init_connectors():
|
||||||
books_url='https://openlibrary.org',
|
books_url='https://openlibrary.org',
|
||||||
covers_url='https://covers.openlibrary.org',
|
covers_url='https://covers.openlibrary.org',
|
||||||
search_url='https://openlibrary.org/search?q=',
|
search_url='https://openlibrary.org/search?q=',
|
||||||
|
isbn_search_url='https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:',
|
||||||
priority=3,
|
priority=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
18
bookwyrm/migrations/0047_connector_isbn_search_url.py
Normal file
18
bookwyrm/migrations/0047_connector_isbn_search_url.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.0.7 on 2021-02-28 16:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bookwyrm', '0046_sitesettings_privacy_policy'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='connector',
|
||||||
|
name='isbn_search_url',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -22,6 +22,7 @@ class Connector(BookWyrmModel):
|
||||||
books_url = models.CharField(max_length=255)
|
books_url = models.CharField(max_length=255)
|
||||||
covers_url = models.CharField(max_length=255)
|
covers_url = models.CharField(max_length=255)
|
||||||
search_url = models.CharField(max_length=255, null=True, blank=True)
|
search_url = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
isbn_search_url = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
|
||||||
politeness_delay = models.IntegerField(null=True, blank=True) #seconds
|
politeness_delay = models.IntegerField(null=True, blank=True) #seconds
|
||||||
max_query_count = models.IntegerField(null=True, blank=True)
|
max_query_count = models.IntegerField(null=True, blank=True)
|
||||||
|
|
33
bookwyrm/templates/isbn_search_results.html
Normal file
33
bookwyrm/templates/isbn_search_results.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Search Results" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% with book_results|first as local_results %}
|
||||||
|
<div class="block">
|
||||||
|
<h1 class="title">{% blocktrans %}Search Results for "{{ query }}"{% endblocktrans %}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block columns">
|
||||||
|
<div class="column">
|
||||||
|
<h2 class="title">{% trans "Matching Books" %}</h2>
|
||||||
|
<section class="block">
|
||||||
|
{% if not results %}
|
||||||
|
<p>{% blocktrans %}No books found for "{{ query }}"{% endblocktrans %}</p>
|
||||||
|
{% else %}
|
||||||
|
<ul>
|
||||||
|
{% for result in results %}
|
||||||
|
<li class="pd-4">
|
||||||
|
<a href="{{ result.key }}">{% include 'snippets/search_result_text.html' with result=result link=True %}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
|
@ -42,6 +42,10 @@ class AbstractConnector(TestCase):
|
||||||
return search_result
|
return search_result
|
||||||
def parse_search_data(self, data):
|
def parse_search_data(self, data):
|
||||||
return data
|
return data
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
return search_result
|
||||||
|
def parse_isbn_search_data(self, data):
|
||||||
|
return data
|
||||||
def is_work_data(self, data):
|
def is_work_data(self, data):
|
||||||
return data['type'] == 'work'
|
return data['type'] == 'work'
|
||||||
def get_edition_from_work_data(self, data):
|
def get_edition_from_work_data(self, data):
|
||||||
|
|
|
@ -18,6 +18,7 @@ class AbstractConnector(TestCase):
|
||||||
books_url='https://example.com/books',
|
books_url='https://example.com/books',
|
||||||
covers_url='https://example.com/covers',
|
covers_url='https://example.com/covers',
|
||||||
search_url='https://example.com/search?q=',
|
search_url='https://example.com/search?q=',
|
||||||
|
isbn_search_url='https://example.com/isbn',
|
||||||
)
|
)
|
||||||
|
|
||||||
class TestConnector(abstract_connector.AbstractMinimalConnector):
|
class TestConnector(abstract_connector.AbstractMinimalConnector):
|
||||||
|
@ -28,6 +29,10 @@ class AbstractConnector(TestCase):
|
||||||
pass
|
pass
|
||||||
def parse_search_data(self, data):
|
def parse_search_data(self, data):
|
||||||
return data
|
return data
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
return search_result
|
||||||
|
def parse_isbn_search_data(self, data):
|
||||||
|
return data
|
||||||
self.test_connector = TestConnector('example.com')
|
self.test_connector = TestConnector('example.com')
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,6 +44,7 @@ class AbstractConnector(TestCase):
|
||||||
self.assertEqual(connector.books_url, 'https://example.com/books')
|
self.assertEqual(connector.books_url, 'https://example.com/books')
|
||||||
self.assertEqual(connector.covers_url, 'https://example.com/covers')
|
self.assertEqual(connector.covers_url, 'https://example.com/covers')
|
||||||
self.assertEqual(connector.search_url, 'https://example.com/search?q=')
|
self.assertEqual(connector.search_url, 'https://example.com/search?q=')
|
||||||
|
self.assertEqual(connector.isbn_search_url, 'https://example.com/isbn')
|
||||||
self.assertIsNone(connector.name)
|
self.assertIsNone(connector.name)
|
||||||
self.assertEqual(connector.identifier, 'example.com')
|
self.assertEqual(connector.identifier, 'example.com')
|
||||||
self.assertIsNone(connector.max_query_count)
|
self.assertIsNone(connector.max_query_count)
|
||||||
|
|
|
@ -27,6 +27,7 @@ class Openlibrary(TestCase):
|
||||||
books_url='https://openlibrary.org',
|
books_url='https://openlibrary.org',
|
||||||
covers_url='https://covers.openlibrary.org',
|
covers_url='https://covers.openlibrary.org',
|
||||||
search_url='https://openlibrary.org/search?q=',
|
search_url='https://openlibrary.org/search?q=',
|
||||||
|
isbn_search_url='https://openlibrary.org/isbn',
|
||||||
)
|
)
|
||||||
self.connector = Connector('openlibrary.org')
|
self.connector = Connector('openlibrary.org')
|
||||||
|
|
||||||
|
@ -149,6 +150,34 @@ class Openlibrary(TestCase):
|
||||||
self.assertEqual(result.connector, self.connector)
|
self.assertEqual(result.connector, self.connector)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_isbn_search_result(self):
|
||||||
|
''' extract the results from the search json response '''
|
||||||
|
datafile = pathlib.Path(__file__).parent.joinpath(
|
||||||
|
'../data/ol_isbn_search.json')
|
||||||
|
search_data = json.loads(datafile.read_bytes())
|
||||||
|
result = self.connector.parse_isbn_search_data(search_data)
|
||||||
|
self.assertIsInstance(result, list)
|
||||||
|
self.assertEqual(len(result), 1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_isbn_search_result(self):
|
||||||
|
''' translate json from openlibrary into SearchResult '''
|
||||||
|
datafile = pathlib.Path(__file__).parent.joinpath(
|
||||||
|
'../data/ol_isbn_search.json')
|
||||||
|
search_data = json.loads(datafile.read_bytes())
|
||||||
|
results = self.connector.parse_isbn_search_data(search_data)
|
||||||
|
self.assertIsInstance(results, list)
|
||||||
|
|
||||||
|
result = self.connector.format_isbn_search_result(results[0])
|
||||||
|
self.assertIsInstance(result, SearchResult)
|
||||||
|
self.assertEqual(result.title, 'Les ombres errantes')
|
||||||
|
self.assertEqual(
|
||||||
|
result.key, 'https://openlibrary.org/books/OL16262504M')
|
||||||
|
self.assertEqual(result.author, 'Pascal Quignard')
|
||||||
|
self.assertEqual(result.year, '2002')
|
||||||
|
self.assertEqual(result.connector, self.connector)
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_load_edition_data(self):
|
def test_load_edition_data(self):
|
||||||
''' format url from key and make request '''
|
''' format url from key and make request '''
|
||||||
|
|
45
bookwyrm/tests/data/ol_isbn_search.json
Normal file
45
bookwyrm/tests/data/ol_isbn_search.json
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"ISBN:9782070427796": {
|
||||||
|
"url": "https://openlibrary.org/books/OL16262504M/Les_ombres_errantes",
|
||||||
|
"key": "/books/OL16262504M",
|
||||||
|
"title": "Les ombres errantes",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"url": "https://openlibrary.org/authors/OL269675A/Pascal_Quignard",
|
||||||
|
"name": "Pascal Quignard"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"by_statement": "Pascal Quignard.",
|
||||||
|
"identifiers": {
|
||||||
|
"goodreads": [
|
||||||
|
"1835483"
|
||||||
|
],
|
||||||
|
"librarything": [
|
||||||
|
"983474"
|
||||||
|
],
|
||||||
|
"isbn_10": [
|
||||||
|
"207042779X"
|
||||||
|
],
|
||||||
|
"openlibrary": [
|
||||||
|
"OL16262504M"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"classifications": {
|
||||||
|
"dewey_decimal_class": [
|
||||||
|
"848/.91403"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"publishers": [
|
||||||
|
{
|
||||||
|
"name": "Gallimard"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"publish_places": [
|
||||||
|
{
|
||||||
|
"name": "Paris"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"publish_date": "2002",
|
||||||
|
"notes": "Hardback published Grasset, 2002."
|
||||||
|
}
|
||||||
|
}
|
54
bookwyrm/tests/views/test_isbn.py
Normal file
54
bookwyrm/tests/views/test_isbn.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models, views
|
||||||
|
from bookwyrm.connectors import abstract_connector
|
||||||
|
from bookwyrm.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class IsbnViews(TestCase):
|
||||||
|
''' tag views'''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True, localname='mouse',
|
||||||
|
remote_id='https://example.com/users/mouse',
|
||||||
|
)
|
||||||
|
self.work = models.Work.objects.create(title='Test Work')
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Test Book',
|
||||||
|
isbn_13='1234567890123',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
parent_work=self.work
|
||||||
|
)
|
||||||
|
models.Connector.objects.create(
|
||||||
|
identifier='self',
|
||||||
|
connector_file='self_connector',
|
||||||
|
local=True
|
||||||
|
)
|
||||||
|
models.SiteSettings.objects.create()
|
||||||
|
|
||||||
|
|
||||||
|
def test_isbn_json_response(self):
|
||||||
|
''' searches local data only and returns book data in json format '''
|
||||||
|
view = views.Isbn.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
with patch('bookwyrm.views.isbn.is_api_request') as is_api:
|
||||||
|
is_api.return_value = True
|
||||||
|
response = view(request, isbn='1234567890123')
|
||||||
|
self.assertIsInstance(response, JsonResponse)
|
||||||
|
|
||||||
|
data = json.loads(response.content)
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
self.assertEqual(data[0]['title'], 'Test Book')
|
||||||
|
self.assertEqual(
|
||||||
|
data[0]['key'], 'https://%s/book/%d' % (DOMAIN, self.book.id))
|
||||||
|
|
|
@ -64,6 +64,10 @@ class ShelfViews(TestCase):
|
||||||
pass
|
pass
|
||||||
def parse_search_data(self, data):
|
def parse_search_data(self, data):
|
||||||
pass
|
pass
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
return search_result
|
||||||
|
def parse_isbn_search_data(self, data):
|
||||||
|
return data
|
||||||
models.Connector.objects.create(
|
models.Connector.objects.create(
|
||||||
identifier='example.com',
|
identifier='example.com',
|
||||||
connector_file='openlibrary',
|
connector_file='openlibrary',
|
||||||
|
|
|
@ -135,6 +135,9 @@ urlpatterns = [
|
||||||
re_path(r'^resolve-book/?$', views.resolve_book),
|
re_path(r'^resolve-book/?$', views.resolve_book),
|
||||||
re_path(r'^switch-edition/?$', views.switch_edition),
|
re_path(r'^switch-edition/?$', views.switch_edition),
|
||||||
|
|
||||||
|
# isbn
|
||||||
|
re_path(r'^isbn/(?P<isbn>\d+)(.json)?/?$', views.Isbn.as_view()),
|
||||||
|
|
||||||
# author
|
# author
|
||||||
re_path(r'^author/(?P<author_id>\d+)(.json)?/?$', views.Author.as_view()),
|
re_path(r'^author/(?P<author_id>\d+)(.json)?/?$', views.Author.as_view()),
|
||||||
re_path(r'^author/(?P<author_id>\d+)/edit/?$', views.EditAuthor.as_view()),
|
re_path(r'^author/(?P<author_id>\d+)/edit/?$', views.EditAuthor.as_view()),
|
||||||
|
|
|
@ -31,3 +31,4 @@ from .site import Site
|
||||||
from .status import CreateStatus, DeleteStatus
|
from .status import CreateStatus, DeleteStatus
|
||||||
from .updates import Updates
|
from .updates import Updates
|
||||||
from .user import User, EditUser, Followers, Following
|
from .user import User, EditUser, Followers, Following
|
||||||
|
from .isbn import Isbn
|
||||||
|
|
29
bookwyrm/views/isbn.py
Normal file
29
bookwyrm/views/isbn.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
''' isbn search view '''
|
||||||
|
from django.http import HttpResponseNotFound
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
|
from bookwyrm import forms, models
|
||||||
|
from bookwyrm.connectors import connector_manager
|
||||||
|
from .helpers import is_api_request
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
class Isbn(View):
|
||||||
|
''' search a book by isbn '''
|
||||||
|
def get(self, request, isbn):
|
||||||
|
''' info about a book '''
|
||||||
|
book_results = connector_manager.isbn_local_search(isbn)
|
||||||
|
|
||||||
|
if is_api_request(request):
|
||||||
|
return JsonResponse([r.json() for r in book_results], safe=False)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': 'ISBN Search Results',
|
||||||
|
'results': book_results,
|
||||||
|
'query': isbn,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'isbn_search_results.html', data)
|
Loading…
Reference in a new issue