Merge pull request #162 from mouse-reeve/fedireads_connector

Fedireads connector
This commit is contained in:
Mouse Reeve 2020-05-10 15:07:14 -07:00 committed by GitHub
commit 64903157ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 715 additions and 475 deletions

View file

@ -1,6 +1,6 @@
''' bring activitypub functions into the namespace ''' ''' bring activitypub functions into the namespace '''
from .actor import get_actor from .actor import get_actor
from .book import get_book from .book import get_book, get_author
from .create import get_create, get_update from .create import get_create, get_update
from .follow import get_following, get_followers from .follow import get_following, get_followers
from .follow import get_follow_request, get_unfollow, get_accept, get_reject from .follow import get_follow_request, get_unfollow, get_accept, get_reject

View file

@ -1,25 +1,23 @@
''' federate book data ''' ''' federate book data '''
from fedireads.settings import DOMAIN from fedireads.settings import DOMAIN
def get_book(book): def get_book(book, recursive=True):
''' activitypub serialize a book ''' ''' activitypub serialize a book '''
fields = [ fields = [
'title',
'sort_title', 'sort_title',
'subtitle', 'subtitle',
'isbn_13', 'isbn_13',
'oclc_number', 'oclc_number',
'openlibrary_key', 'openlibrary_key',
'librarything_key', 'librarything_key',
'fedireads_key',
'lccn', 'lccn',
'oclc_number', 'oclc_number',
'pages', 'pages',
'physical_format', 'physical_format',
'misc_identifiers', 'misc_identifiers',
'source_url',
'description', 'description',
'languages', 'languages',
'series', 'series',
@ -30,21 +28,28 @@ def get_book(book):
'physical_format', 'physical_format',
] ]
book_type = type(book).__name__
activity = { activity = {
'@context': 'https://www.w3.org/ns/activitystreams', '@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Document', 'type': 'Document',
'book_type': type(book).__name__, 'book_type': book_type,
'name': book.title, 'name': book.title,
'url': book.absolute_id, 'url': book.absolute_id,
'authors': [get_author(a) for a in book.authors.all()], 'authors': [a.absolute_id for a in book.authors.all()],
'first_published_date': book.first_published_date.isoformat() if \ 'first_published_date': book.first_published_date.isoformat() if \
book.first_published_date else None, book.first_published_date else None,
'published_date': book.published_date.isoformat() if \ 'published_date': book.published_date.isoformat() if \
book.published_date else None, book.published_date else None,
'parent_work': book.parent_work.absolute_id if \
hasattr(book, 'parent_work') else None,
} }
if recursive:
if book_type == 'Edition':
activity['work'] = get_book(book.parent_work, recursive=False)
else:
editions = book.edition_set.order_by('default')
activity['editions'] = [
get_book(b, recursive=False) for b in editions]
for field in fields: for field in fields:
if hasattr(book, field): if hasattr(book, field):
activity[field] = book.__getattribute__(field) activity[field] = book.__getattribute__(field)
@ -63,7 +68,21 @@ def get_book(book):
def get_author(author): def get_author(author):
''' serialize an author ''' ''' serialize an author '''
return { fields = [
'name': author.name, 'name',
'born',
'died',
'aliases',
'bio'
'openlibrary_key',
'wikipedia_link',
]
activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
'url': author.absolute_id, 'url': author.absolute_id,
'type': 'Person',
} }
for field in fields:
if hasattr(author, field):
activity[field] = author.__getattribute__(field)
return activity

View file

@ -1,31 +1,100 @@
''' select and call a connector for whatever book task needs doing ''' ''' select and call a connector for whatever book task needs doing '''
import importlib from requests import HTTPError
from fedireads import models import importlib
from urllib.parse import urlparse
from fedireads import models, settings
from fedireads.tasks import app from fedireads.tasks import app
def get_or_create_book(key): def get_or_create_book(value, key='id', connector_id=None):
''' pull up a book record by whatever means possible ''' ''' pull up a book record by whatever means possible '''
try: book = models.Book.objects.select_subclasses().filter(
book = models.Book.objects.select_subclasses().get( **{key: value}
fedireads_key=key ).first()
) if book:
if not isinstance(book, models.Edition):
return book.default_edition
return book return book
except models.Book.DoesNotExist:
pass
connector = get_connector() if key == 'remote_id':
book = connector.get_or_create_book(key) book = get_by_absolute_id(value, models.Book)
if book:
return book
if connector_id:
connector_info = models.Connector.objects.get(id=connector_id)
connector = load_connector(connector_info)
else:
connector = get_or_create_connector(value)
book = connector.get_or_create_book(value)
load_more_data.delay(book.id) load_more_data.delay(book.id)
return book return book
def get_or_create_connector(remote_id):
''' get the connector related to the author's server '''
url = urlparse(remote_id)
identifier = url.netloc
if not identifier:
raise ValueError('Invalid remote id')
try:
connector_info = models.Connector.objects.get(identifier=identifier)
except models.Connector.DoesNotExist:
connector_info = models.Connector.objects.create(
identifier=identifier,
connector_file='fedireads_connector',
base_url='https://%s' % identifier,
books_url='https://%s/book' % identifier,
covers_url='https://%s/images/covers' % identifier,
search_url='https://%s/search?q=' % identifier,
key_name='remote_id',
priority=3
)
return load_connector(connector_info)
def get_by_absolute_id(absolute_id, model):
''' generalized function to get from a model with a remote_id field '''
if not absolute_id:
return None
# check if it's a remote status
try:
return model.objects.get(remote_id=absolute_id)
except model.DoesNotExist:
pass
url = urlparse(absolute_id)
if url.netloc != settings.DOMAIN:
return None
# try finding a local status with that id
local_id = absolute_id.split('/')[-1]
try:
if hasattr(model.objects, 'select_subclasses'):
possible_match = model.objects.select_subclasses().get(id=local_id)
else:
possible_match = model.objects.get(id=local_id)
except model.DoesNotExist:
return None
# make sure it's not actually a remote status with an id that
# clashes with a local id
if possible_match.absolute_id == absolute_id:
return possible_match
return None
@app.task @app.task
def load_more_data(book_id): def load_more_data(book_id):
''' background the work of getting all 10,000 editions of LoTR ''' ''' background the work of getting all 10,000 editions of LoTR '''
book = models.Book.objects.select_subclasses().get(id=book_id) book = models.Book.objects.select_subclasses().get(id=book_id)
connector = get_connector(book) connector = load_connector(book.connector)
connector.expand_book_data(book) connector.expand_book_data(book)
@ -35,7 +104,10 @@ def search(query):
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 = connector.search(query) result_set = connector.search(query)
except HTTPError:
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]
@ -49,6 +121,13 @@ def search(query):
return results return results
def local_search(query):
''' only look at local search results '''
connector = load_connector(models.Connector.objects.get(local=True))
return connector.search(query)
def first_search_result(query): def first_search_result(query):
''' 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():
@ -58,10 +137,10 @@ def first_search_result(query):
return None return None
def update_book(book): def update_book(book, data=None):
''' re-sync with the original data source ''' ''' re-sync with the original data source '''
connector = get_connector(book) connector = load_connector(book.connector)
connector.update_book(book) connector.update_book(book, data=data)
def get_connectors(): def get_connectors():
@ -70,18 +149,6 @@ def get_connectors():
return [load_connector(c) for c in connectors_info] return [load_connector(c) for c in connectors_info]
def get_connector(book=None):
''' pick a book data connector '''
if book and book.connector:
connector_info = book.connector
else:
# only select from external connectors
connector_info = models.Connector.objects.filter(
local=False
).order_by('priority').first()
return load_connector(connector_info)
def load_connector(connector_info): def load_connector(connector_info):
''' instantiate the connector class ''' ''' instantiate the connector class '''
connector = importlib.import_module( connector = importlib.import_module(

View file

@ -2,6 +2,9 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dateutil import parser from dateutil import parser
import pytz import pytz
import requests
from django.db import transaction
from fedireads import models from fedireads import models
@ -14,34 +17,233 @@ class AbstractConnector(ABC):
info = models.Connector.objects.get(identifier=identifier) info = models.Connector.objects.get(identifier=identifier)
self.connector = info self.connector = info
self.base_url = info.base_url self.book_mappings = {}
self.books_url = info.books_url self.key_mappings = {
self.covers_url = info.covers_url 'isbn_13': ('isbn_13', None),
self.search_url = info.search_url 'isbn_10': ('isbn_10', None),
self.key_name = info.key_name 'oclc_numbers': ('oclc_number', None),
self.max_query_count = info.max_query_count 'lccn': ('lccn', None),
self.name = info.name }
self.local = info.local
fields = [
'base_url',
'books_url',
'covers_url',
'search_url',
'key_name',
'max_query_count',
'name',
'identifier',
'local'
]
for field in fields:
setattr(self, field, getattr(info, field))
def is_available(self): def is_available(self):
''' check if you're allowed to use this connector ''' ''' check if you're allowed to use this connector '''
if self.connector.max_query_count is not None: if self.max_query_count is not None:
if self.connector.query_count >= self.connector.max_query_count: if self.connector.query_count >= self.max_query_count:
return False return False
return True return True
@abstractmethod
def search(self, query): def search(self, query):
''' free text search ''' ''' free text search '''
# return list of search result objs resp = requests.get(
'%s%s' % (self.search_url, query),
headers={
'Accept': 'application/json; charset=utf-8',
},
)
if not resp.ok:
resp.raise_for_status()
data = resp.json()
results = []
for doc in self.parse_search_data(data)[:10]:
results.append(self.format_search_result(doc))
return results
def get_or_create_book(self, remote_id):
''' pull up a book record by whatever means possible '''
# try to load the book
book = models.Book.objects.select_subclasses().filter(
remote_id=remote_id
).first()
if book:
if isinstance(book, models.Work):
return book.default_edition
return book
# no book was found, so we start creating a new one
data = get_data(remote_id)
work = None
edition = None
if self.is_work_data(data):
work_data = data
# if we requested a work and there's already an edition, we're set
work = self.match_from_mappings(work_data)
if work and work.default_edition:
return work.default_edition
# no such luck, we need more information.
try:
edition_data = self.get_edition_from_work_data(work_data)
except KeyError:
# hack: re-use the work data as the edition data
# this is why remote ids aren't necessarily unique
edition_data = data
else:
edition_data = data
edition = self.match_from_mappings(edition_data)
# no need to figure out about the work if we already know about it
if edition and edition.parent_work:
return edition
# no such luck, we need more information.
try:
work_data = self.get_work_from_edition_date(edition_data)
except KeyError:
# remember this hack: re-use the work data as the edition data
work_data = data
# at this point, we need to figure out the work, edition, or both
# atomic so that we don't save a work with no edition for vice versa
with transaction.atomic():
if not work:
work_key = work_data.get('url')
work = self.create_book(work_key, work_data, models.Work)
if not edition:
ed_key = edition_data.get('url')
edition = self.create_book(ed_key, edition_data, models.Edition)
edition.default = True
edition.parent_work = work
edition.save()
# now's our change to fill in author gaps
if not edition.authors and work.authors:
edition.authors.set(work.authors.all())
edition.author_text = work.author_text
edition.save()
return edition
def create_book(self, remote_id, data, model):
''' create a work or edition from data '''
book = model.objects.create(
remote_id=remote_id,
title=data['title'],
connector=self.connector,
)
return self.update_book_from_data(book, data)
def update_book_from_data(self, book, data, update_cover=True):
''' for creating a new book or syncing with data '''
book = update_from_mappings(book, data, self.book_mappings)
for author in self.get_authors_from_data(data):
book.authors.add(author)
book.author_text = ', '.join(a.name for a in book.authors.all())
book.save()
if not update_cover:
return book
cover = self.get_cover_from_data(data)
if cover:
book.cover.save(*cover, save=True)
return book
def update_book(self, book, data=None):
''' load new data '''
if not book.sync and not book.sync_cover:
return
if not data:
key = getattr(book, self.key_name)
data = self.load_book_data(key)
if book.sync:
book = self.update_book_from_data(
book, data, update_cover=book.sync_cover)
else:
cover = self.get_cover_from_data(data)
if cover:
book.cover.save(*cover, save=True)
return book
def match_from_mappings(self, data):
''' try to find existing copies of this book using various keys '''
keys = [
('openlibrary_key', models.Book),
('librarything_key', models.Book),
('goodreads_key', models.Book),
('lccn', models.Work),
('isbn_10', models.Edition),
('isbn_13', models.Edition),
('oclc_number', models.Edition),
('asin', models.Edition),
]
noop = lambda x: x
for key, model in keys:
formatter = None
if key in self.key_mappings:
key, formatter = self.key_mappings[key]
if not formatter:
formatter = noop
value = data.get(key)
if not value:
continue
value = formatter(value)
match = model.objects.select_subclasses().filter(
**{key: value}).first()
if match:
return match
@abstractmethod @abstractmethod
def get_or_create_book(self, book_id): def is_work_data(self, data):
''' request and format a book given an identifier ''' ''' differentiate works and editions '''
# return book model obj
@abstractmethod
def get_edition_from_work_data(self, data):
''' every work needs at least one edition '''
@abstractmethod
def get_work_from_edition_date(self, data):
''' every edition needs a work '''
@abstractmethod
def get_authors_from_data(self, data):
''' load author data '''
@abstractmethod
def get_cover_from_data(self, data):
''' load cover '''
@abstractmethod
def parse_search_data(self, data):
''' turn the result json from a search into a list '''
@abstractmethod
def format_search_result(self, search_result):
''' create a SearchResult obj from json '''
@abstractmethod @abstractmethod
@ -49,18 +251,6 @@ class AbstractConnector(ABC):
''' get more info on a book ''' ''' get more info on a book '''
@abstractmethod
def get_or_create_author(self, book_id):
''' request and format a book given an identifier '''
# return book model obj
@abstractmethod
def update_book(self, book_obj):
''' sync a book with the canonical remote copy '''
# return book model obj
def update_from_mappings(obj, data, mappings): def update_from_mappings(obj, data, mappings):
''' assign data to model with mappings ''' ''' assign data to model with mappings '''
noop = lambda x: x noop = lambda x: x
@ -91,20 +281,35 @@ def has_attr(obj, key):
def get_date(date_string): def get_date(date_string):
''' helper function to try to interpret dates ''' ''' helper function to try to interpret dates '''
if not date_string:
return None
try: try:
return pytz.utc.localize(parser.parse(date_string)) return pytz.utc.localize(parser.parse(date_string))
except ValueError: except ValueError:
return None return None
class SearchResult: def get_data(url):
''' wrapper for request.get '''
resp = requests.get(
url,
headers={
'Accept': 'application/json; charset=utf-8',
},
)
if not resp.ok:
resp.raise_for_status()
data = resp.json()
return data
class SearchResult(object):
''' standardized search result object ''' ''' standardized search result object '''
def __init__(self, title, key, author, year, raw_data): def __init__(self, title, key, author, year):
self.title = title self.title = title
self.key = key self.key = key
self.author = author self.author = author
self.year = year self.year = year
self.raw_data = raw_data
def __repr__(self): def __repr__(self):
return "<SearchResult key={!r} title={!r} author={!r}>".format( return "<SearchResult key={!r} title={!r} author={!r}>".format(

View file

@ -1,101 +1,68 @@
''' using another fedireads instance as a source of book data ''' ''' using another fedireads instance as a source of book data '''
from uuid import uuid4
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
import requests import requests
from fedireads import models from fedireads import models
from .abstract_connector import AbstractConnector from .abstract_connector import AbstractConnector, SearchResult
from .abstract_connector import update_from_mappings, get_date from .abstract_connector import update_from_mappings, get_date, get_data
class Connector(AbstractConnector): class Connector(AbstractConnector):
def search(self, query): ''' interact with other instances '''
''' right now you can't search fedireads, but... ''' def __init__(self, identifier):
resp = requests.get( super().__init__(identifier)
'%s%s' % (self.search_url, query), self.book_mappings = self.key_mappings.copy()
headers={ self.book_mappings.update({
'Accept': 'application/activity+json; charset=utf-8', 'published_date': ('published_date', get_date),
}, 'first_published_date': ('first_published_date', get_date),
) })
if not resp.ok:
resp.raise_for_status()
return resp.json()
def get_or_create_book(self, fedireads_key): def is_work_data(self, data):
''' pull up a book record by whatever means possible ''' return data['book_type'] == 'Work'
try:
book = models.Book.objects.select_subclasses().get(
fedireads_key=fedireads_key def get_edition_from_work_data(self, data):
) return data['editions'][0]
return book
except ObjectDoesNotExist:
if self.model.is_self: def get_work_from_edition_date(self, data):
# we can't load a book from a remote server, this is it return data['work']
def get_authors_from_data(self, data):
for author_url in data.get('authors', []):
yield self.get_or_create_author(author_url)
def get_cover_from_data(self, data):
cover_data = data.get('attachment')
if not cover_data:
return None return None
# no book was found, so we start creating a new one cover_url = cover_data[0].get('url')
book = models.Book(fedireads_key=fedireads_key) response = requests.get(cover_url)
def update_book(self, book):
''' add remote data to a local book '''
fedireads_key = book.fedireads_key
response = requests.get(
'%s/%s' % (self.base_url, fedireads_key),
headers={
'Accept': 'application/activity+json; charset=utf-8',
},
)
if not response.ok: if not response.ok:
response.raise_for_status() response.raise_for_status()
data = response.json() image_name = str(uuid4()) + cover_url.split('.')[-1]
image_content = ContentFile(response.content)
# great, we can update our book. return [image_name, image_content]
mappings = {
'published_date': ('published_date', get_date),
'first_published_date': ('first_published_date', get_date),
}
book = update_from_mappings(book, data, mappings)
if not book.source_url:
book.source_url = response.url
if not book.connector:
book.connector = self.connector
book.save()
if data.get('parent_work'):
work = self.get_or_create_book(data.get('parent_work'))
book.parent_work = work
for author_blob in data.get('authors', []):
author_blob = author_blob.get('author', author_blob)
author_id = author_blob['key']
author_id = author_id.split('/')[-1]
book.authors.add(self.get_or_create_author(author_id))
if book.sync_cover and data.get('covers') and data['covers']:
book.cover.save(*get_cover(data['covers'][0]), save=True)
return book
def get_or_create_author(self, fedireads_key): def get_or_create_author(self, remote_id):
''' load that author ''' ''' load that author '''
try: try:
return models.Author.objects.get(fedireads_key=fedireads_key) return models.Author.objects.get(remote_id=remote_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
resp = requests.get('%s/authors/%s.json' % (self.url, fedireads_key)) data = get_data(remote_id)
if not resp.ok:
resp.raise_for_status()
data = resp.json()
# ingest a new author # ingest a new author
author = models.Author(fedireads_key=fedireads_key) author = models.Author(remote_id=remote_id)
mappings = { mappings = {
'born': ('born', get_date), 'born': ('born', get_date),
'died': ('died', get_date), 'died': ('died', get_date),
@ -106,11 +73,14 @@ class Connector(AbstractConnector):
return author return author
def get_cover(cover_url): def parse_search_data(self, data):
''' ask openlibrary for the cover ''' return data
image_name = cover_url.split('/')[-1]
response = requests.get(cover_url)
if not response.ok: def format_search_result(self, search_result):
response.raise_for_status() return SearchResult(**search_result)
image_content = ContentFile(response.content)
return [image_name, image_content]
def expand_book_data(self, book):
# TODO
pass

View file

@ -3,184 +3,105 @@ import re
import requests import requests
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import transaction
from fedireads import models from fedireads import models
from .abstract_connector import AbstractConnector, SearchResult from .abstract_connector import AbstractConnector, SearchResult
from .abstract_connector import update_from_mappings, get_date from .abstract_connector import update_from_mappings
from .abstract_connector import get_date, get_data
from .openlibrary_languages import languages from .openlibrary_languages import languages
class Connector(AbstractConnector): class Connector(AbstractConnector):
''' instantiate a connector for OL ''' ''' instantiate a connector for OL '''
def __init__(self, identifier): def __init__(self, identifier):
super().__init__(identifier)
get_first = lambda a: a[0] get_first = lambda a: a[0]
self.book_mappings = { self.key_mappings = {
'isbn_13': ('isbn_13', get_first),
'isbn_10': ('isbn_10', get_first),
'oclc_numbers': ('oclc_number', get_first),
'lccn': ('lccn', get_first),
}
self.book_mappings = self.key_mappings.copy()
self.book_mappings.update({
'publish_date': ('published_date', get_date), 'publish_date': ('published_date', get_date),
'first_publish_date': ('first_published_date', get_date), 'first_publish_date': ('first_published_date', get_date),
'description': ('description', get_description), 'description': ('description', get_description),
'isbn_13': ('isbn_13', get_first),
'oclc_numbers': ('oclc_number', get_first),
'lccn': ('lccn', get_first),
'languages': ('languages', get_languages), 'languages': ('languages', get_languages),
'number_of_pages': ('pages', None), 'number_of_pages': ('pages', None),
'series': ('series', get_first), 'series': ('series', get_first),
} })
super().__init__(identifier)
def search(self, query): def is_work_data(self, data):
''' query openlibrary search ''' return not re.match(r'^OL\d+M$', data['key'])
resp = requests.get(
'%s%s' % (self.search_url, query),
headers={
'Accept': 'application/json; charset=utf-8',
},
)
if not resp.ok:
resp.raise_for_status()
data = resp.json()
results = []
for doc in data['docs'][:5]:
key = doc['key']
key = key.split('/')[-1]
author = doc.get('author_name') or ['Unknown']
results.append(SearchResult(
doc.get('title'),
key,
author[0],
doc.get('first_publish_year'),
doc
))
return results
def get_or_create_book(self, olkey): def get_edition_from_work_data(self, data):
''' pull up a book record by whatever means possible. try:
if you give a work key, it should give you the default edition, key = data['key']
annotated with work data. ''' except KeyError:
return False
book = models.Book.objects.select_subclasses().filter( url = '%s/%s/editions' % (self.books_url, key)
openlibrary_key=olkey data = get_data(url)
).first() return pick_default_edition(data['entries'])
if book:
if isinstance(book, models.Work):
return book.default_edition
return book
# no book was found, so we start creating a new one
if re.match(r'^OL\d+W$', olkey):
with transaction.atomic():
# create both work and a default edition
work_data = self.load_book_data(olkey)
work = self.create_book(olkey, work_data, models.Work)
edition_options = self.load_edition_data(olkey).get('entries')
edition_data = pick_default_edition(edition_options)
if not edition_data:
# hack: re-use the work data as the edition data
edition_data = work_data
key = edition_data.get('key').split('/')[-1]
edition = self.create_book(key, edition_data, models.Edition)
edition.default = True
edition.parent_work = work
edition.save()
else:
with transaction.atomic():
edition_data = self.load_book_data(olkey)
edition = self.create_book(olkey, edition_data, models.Edition)
work_data = edition_data.get('works')
if not work_data:
# hack: we're re-using the edition data as the work data
work_key = olkey
else:
work_key = work_data[0]['key'].split('/')[-1]
work = models.Work.objects.filter(
openlibrary_key=work_key
).first()
if not work:
work_data = self.load_book_data(work_key)
work = self.create_book(work_key, work_data, models.Work)
edition.parent_work = work
edition.save()
if not edition.authors and work.authors:
edition.authors.set(work.authors.all())
edition.author_text = ', '.join(a.name for a in edition.authors)
return edition
def create_book(self, key, data, model): def get_work_from_edition_date(self, data):
''' create a work or edition from data ''' try:
book = model.objects.create( key = data['works'][0]['key']
openlibrary_key=key, except (IndexError, KeyError):
title=data['title'], return False
connector=self.connector, url = '%s/%s' % (self.books_url, key)
) return get_data(url)
return self.update_book_from_data(book, data)
def update_book_from_data(self, book, data):
''' updaet a book model instance from ol data '''
# populate the simple data fields
update_from_mappings(book, data, self.book_mappings)
book.save()
authors = self.get_authors_from_data(data)
for author in authors:
book.authors.add(author)
if authors:
book.author_text = ', '.join(a.name for a in authors)
if data.get('covers'):
book.cover.save(*self.get_cover(data['covers'][0]), save=True)
return book
def update_book(self, book):
''' load new data '''
if not book.sync and not book.sync_cover:
return
data = self.load_book_data(book.openlibrary_key)
if book.sync_cover and data.get('covers'):
book.cover.save(*self.get_cover(data['covers'][0]), save=True)
if book.sync:
book = self.update_book_from_data(book, data)
return book
def get_authors_from_data(self, data): def get_authors_from_data(self, data):
''' parse author json and load or create authors ''' ''' parse author json and load or create authors '''
authors = []
for author_blob in data.get('authors', []): for author_blob in data.get('authors', []):
# this id is "/authors/OL1234567A" and we want just "OL1234567A"
author_blob = author_blob.get('author', author_blob) author_blob = author_blob.get('author', author_blob)
# this id is "/authors/OL1234567A" and we want just "OL1234567A"
author_id = author_blob['key'].split('/')[-1] author_id = author_blob['key'].split('/')[-1]
authors.append(self.get_or_create_author(author_id)) yield self.get_or_create_author(author_id)
return authors
def load_book_data(self, olkey): def get_cover_from_data(self, data):
''' query openlibrary for data on a book ''' ''' ask openlibrary for the cover '''
response = requests.get('%s/works/%s.json' % (self.books_url, olkey)) if not data.get('covers'):
return None
cover_id = data.get('covers')[0]
image_name = '%s-M.jpg' % cover_id
url = '%s/b/id/%s' % (self.covers_url, image_name)
response = requests.get(url)
if not response.ok: if not response.ok:
response.raise_for_status() response.raise_for_status()
data = response.json() image_content = ContentFile(response.content)
return data return [image_name, image_content]
def parse_search_data(self, data):
return data.get('docs')
def format_search_result(self, doc):
key = doc['key']
# build the absolute id from the openlibrary key
key = self.books_url + key
author = doc.get('author_name') or ['Unknown']
return SearchResult(
doc.get('title'),
key,
author[0],
doc.get('first_publish_year'),
)
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 '''
response = requests.get( url = '%s/works/%s/editions.json' % (self.books_url, olkey)
'%s/works/%s/editions.json' % (self.books_url, olkey)) return get_data(url)
if not response.ok:
response.raise_for_status()
data = response.json()
return data
def expand_book_data(self, book): def expand_book_data(self, book):
@ -209,11 +130,9 @@ class Connector(AbstractConnector):
except models.Author.DoesNotExist: except models.Author.DoesNotExist:
pass pass
response = requests.get('%s/authors/%s.json' % (self.base_url, olkey)) url = '%s/authors/%s.json' % (self.base_url, olkey)
if not response.ok: data = get_data(url)
response.raise_for_status()
data = response.json()
author = models.Author(openlibrary_key=olkey) author = models.Author(openlibrary_key=olkey)
mappings = { mappings = {
'birth_date': ('born', get_date), 'birth_date': ('born', get_date),
@ -221,8 +140,8 @@ class Connector(AbstractConnector):
'bio': ('bio', get_description), 'bio': ('bio', get_description),
} }
author = update_from_mappings(author, data, mappings) author = update_from_mappings(author, data, mappings)
# TODO this is making some BOLD assumption
name = data.get('name') name = data.get('name')
# TODO this is making some BOLD assumption
if name: if name:
author.last_name = name.split(' ')[-1] author.last_name = name.split(' ')[-1]
author.first_name = ' '.join(name.split(' ')[:-1]) author.first_name = ' '.join(name.split(' ')[:-1])
@ -231,18 +150,6 @@ class Connector(AbstractConnector):
return author return author
def get_cover(self, cover_id):
''' ask openlibrary for the cover '''
# TODO: get medium and small versions
image_name = '%s-M.jpg' % cover_id
url = '%s/b/id/%s' % (self.covers_url, image_name)
response = requests.get(url)
if not response.ok:
response.raise_for_status()
image_content = ContentFile(response.content)
return [image_name, image_content]
def get_description(description_blob): def get_description(description_blob):
''' descriptions can be a string or a dict ''' ''' descriptions can be a string or a dict '''
if isinstance(description_blob, dict): if isinstance(description_blob, dict):

View file

@ -23,9 +23,9 @@ class Connector(AbstractConnector):
SearchVector('isbn_10', weight='A') +\ SearchVector('isbn_10', weight='A') +\
SearchVector('openlibrary_key', weight='B') +\ SearchVector('openlibrary_key', weight='B') +\
SearchVector('goodreads_key', weight='B') +\ SearchVector('goodreads_key', weight='B') +\
SearchVector('source_url', weight='B') +\
SearchVector('asin', weight='B') +\ SearchVector('asin', weight='B') +\
SearchVector('oclc_number', weight='B') +\ SearchVector('oclc_number', weight='B') +\
SearchVector('remote_id', weight='B') +\
SearchVector('description', weight='C') +\ SearchVector('description', weight='C') +\
SearchVector('series', weight='C') SearchVector('series', weight='C')
).filter(search=query) ).filter(search=query)
@ -34,39 +34,49 @@ class Connector(AbstractConnector):
search_results = [] search_results = []
for book in results[:10]: for book in results[:10]:
search_results.append( search_results.append(
SearchResult( self.format_search_result(book)
book.title,
book.fedireads_key,
book.author_text,
book.published_date.year if book.published_date else None,
None
)
) )
return search_results return search_results
def get_or_create_book(self, fedireads_key): def format_search_result(self, book):
return SearchResult(
book.title,
book.absolute_id,
book.author_text,
book.published_date.year if book.published_date else None,
)
def get_or_create_book(self, book_id):
''' since this is querying its own data source, it can only ''' since this is querying its own data source, it can only
get a book, not load one from an external source ''' get a book, not load one from an external source '''
try: try:
return models.Book.objects.select_subclasses().get( return models.Book.objects.select_subclasses().get(
fedireads_key=fedireads_key id=book_id
) )
except ObjectDoesNotExist: except ObjectDoesNotExist:
return None return None
def get_or_create_author(self, fedireads_key): def is_work_data(self, data):
''' load that author '''
try:
return models.Author.objects.get(fedreads_key=fedireads_key)
except ObjectDoesNotExist:
pass pass
def get_edition_from_work_data(self, data):
def update_book(self, book_obj):
pass pass
def get_work_from_edition_date(self, data):
pass
def get_authors_from_data(self, data):
return None
def get_cover_from_data(self, data):
return None
def parse_search_data(self, data):
''' it's already in the right format, don't even worry about it '''
return data
def expand_book_data(self, book): def expand_book_data(self, book):
pass pass

View file

@ -109,7 +109,6 @@ class EditionForm(ModelForm):
'subjects',# TODO 'subjects',# TODO
'subject_places',# TODO 'subject_places',# TODO
'source_url',
'connector', 'connector',
] ]

View file

@ -10,7 +10,7 @@ from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
import requests import requests
from fedireads import models, outgoing from fedireads import books_manager, models, outgoing
from fedireads import status as status_builder from fedireads import status as status_builder
from fedireads.remote_user import get_or_create_remote_user from fedireads.remote_user import get_or_create_remote_user
from fedireads.tasks import app from fedireads.tasks import app
@ -63,7 +63,7 @@ def shared_inbox(request):
}, },
'Update': { 'Update': {
'Person': None,# TODO: handle_update_user 'Person': None,# TODO: handle_update_user
'Document': None# TODO: handle_update_book 'Document': handle_update_book,
}, },
} }
activity_type = activity['type'] activity_type = activity['type']
@ -320,3 +320,18 @@ def handle_tag(activity):
if not user.local: if not user.local:
book = activity['target']['id'].split('/')[-1] book = activity['target']['id'].split('/')[-1]
status_builder.create_tag(user, book, activity['object']['name']) status_builder.create_tag(user, book, activity['object']['name'])
@app.task
def handle_update_book(activity):
''' a remote instance changed a book (Document) '''
document = activity['object']
# check if we have their copy and care about their updates
book = models.Book.objects.select_subclasses().filter(
remote_id=document['url'],
sync=True,
).first()
if not book:
return
books_manager.update_book(book, data=document)

View file

@ -0,0 +1,41 @@
# Generated by Django 3.0.3 on 2020-05-04 01:54
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('fedireads', '0036_auto_20200503_2007'),
]
operations = [
migrations.RemoveField(
model_name='author',
name='fedireads_key',
),
migrations.RemoveField(
model_name='book',
name='fedireads_key',
),
migrations.RemoveField(
model_name='book',
name='source_url',
),
migrations.AddField(
model_name='author',
name='last_sync_date',
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name='author',
name='sync',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='book',
name='remote_id',
field=models.CharField(max_length=255, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.0.3 on 2020-05-09 19:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fedireads', '0037_auto_20200504_0154'),
]
operations = [
migrations.AddField(
model_name='author',
name='remote_id',
field=models.CharField(max_length=255, null=True),
),
]

View file

@ -1,6 +1,4 @@
''' database schema for books and shelves ''' ''' database schema for books and shelves '''
from uuid import uuid4
from django.utils import timezone from django.utils import timezone
from django.db import models from django.db import models
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
@ -50,15 +48,14 @@ class Connector(FedireadsModel):
class Book(FedireadsModel): class Book(FedireadsModel):
''' a generic book, which can mean either an edition or a work ''' ''' a generic book, which can mean either an edition or a work '''
remote_id = models.CharField(max_length=255, null=True)
# these identifiers apply to both works and editions # these identifiers apply to both works and editions
fedireads_key = models.CharField(max_length=255, unique=True, default=uuid4)
openlibrary_key = models.CharField(max_length=255, blank=True, null=True) openlibrary_key = models.CharField(max_length=255, blank=True, null=True)
librarything_key = models.CharField(max_length=255, blank=True, null=True) librarything_key = models.CharField(max_length=255, blank=True, null=True)
goodreads_key = models.CharField(max_length=255, blank=True, null=True) goodreads_key = models.CharField(max_length=255, blank=True, null=True)
misc_identifiers = JSONField(null=True) misc_identifiers = JSONField(null=True)
# info about where the data comes from and where/if to sync # info about where the data comes from and where/if to sync
source_url = models.CharField(max_length=255, unique=True, null=True)
sync = models.BooleanField(default=True) sync = models.BooleanField(default=True)
sync_cover = models.BooleanField(default=True) sync_cover = models.BooleanField(default=True)
last_sync_date = models.DateTimeField(default=timezone.now) last_sync_date = models.DateTimeField(default=timezone.now)
@ -95,9 +92,10 @@ class Book(FedireadsModel):
@property @property
def absolute_id(self): def absolute_id(self):
''' constructs the absolute reference to any db object ''' ''' constructs the absolute reference to any db object '''
if self.sync and self.remote_id:
return self.remote_id
base_path = 'https://%s' % DOMAIN base_path = 'https://%s' % DOMAIN
model_name = type(self).__name__.lower() return '%s/book/%d' % (base_path, self.id)
return '%s/book/%s' % (base_path, self.openlibrary_key)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
''' can't be abstract for query reasons, but you shouldn't USE it ''' ''' can't be abstract for query reasons, but you shouldn't USE it '''
@ -130,6 +128,7 @@ class Edition(Book):
''' an edition of a book ''' ''' an edition of a book '''
# default -> this is what gets displayed for a work # default -> this is what gets displayed for a work
default = models.BooleanField(default=False) default = models.BooleanField(default=False)
# these identifiers only apply to editions, not works # these identifiers only apply to editions, not works
isbn_10 = models.CharField(max_length=255, blank=True, null=True) isbn_10 = models.CharField(max_length=255, blank=True, null=True)
isbn_13 = models.CharField(max_length=255, blank=True, null=True) isbn_13 = models.CharField(max_length=255, blank=True, null=True)
@ -151,8 +150,10 @@ class Edition(Book):
class Author(FedireadsModel): class Author(FedireadsModel):
''' copy of an author from OL ''' ''' copy of an author from OL '''
fedireads_key = models.CharField(max_length=255, unique=True, default=uuid4) remote_id = models.CharField(max_length=255, null=True)
openlibrary_key = models.CharField(max_length=255, blank=True, null=True) openlibrary_key = models.CharField(max_length=255, blank=True, null=True)
sync = models.BooleanField(default=True)
last_sync_date = models.DateTimeField(default=timezone.now)
wikipedia_link = models.CharField(max_length=255, blank=True, null=True) wikipedia_link = models.CharField(max_length=255, blank=True, null=True)
# idk probably other keys would be useful here? # idk probably other keys would be useful here?
born = models.DateTimeField(blank=True, null=True) born = models.DateTimeField(blank=True, null=True)

View file

@ -7,7 +7,7 @@ from django.http import HttpResponseNotFound, JsonResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
import requests import requests
from fedireads import activitypub from fedireads import activitypub, books_manager
from fedireads import models from fedireads import models
from fedireads.broadcast import broadcast from fedireads.broadcast import broadcast
from fedireads.status import create_review, create_status from fedireads.status import create_review, create_status
@ -260,9 +260,10 @@ def handle_comment(user, book, content):
user, book, builder, fr_serializer, ap_serializer, content) user, book, builder, fr_serializer, ap_serializer, content)
def handle_status(user, book, \ def handle_status(user, book_id, \
builder, fr_serializer, ap_serializer, *args): builder, fr_serializer, ap_serializer, *args):
''' generic handler for statuses ''' ''' generic handler for statuses '''
book = books_manager.get_or_create_book(book_id)
status = builder(user, book, *args) status = builder(user, book, *args)
activity = fr_serializer(status) activity = fr_serializer(status)
@ -286,7 +287,7 @@ def handle_tag(user, book, name):
def handle_untag(user, book, name): def handle_untag(user, book, name):
''' tag a book ''' ''' tag a book '''
book = models.Book.objects.get(fedireads_key=book) book = models.Book.objects.get(id=book)
tag = models.Tag.objects.get(name=name, book=book, user=user) tag = models.Tag.objects.get(name=name, book=book, user=user)
tag_activity = activitypub.get_remove_tag(tag) tag_activity = activitypub.get_remove_tag(tag)
tag.delete() tag.delete()

View file

@ -2,22 +2,20 @@
from django.db import IntegrityError from django.db import IntegrityError
from fedireads import models from fedireads import models
from fedireads.books_manager import get_or_create_book from fedireads.books_manager import get_or_create_book, get_by_absolute_id
from fedireads.sanitize_html import InputHtmlParser from fedireads.sanitize_html import InputHtmlParser
def create_review_from_activity(author, activity): def create_review_from_activity(author, activity):
''' parse an activity json blob into a status ''' ''' parse an activity json blob into a status '''
book_id = activity['inReplyToBook'] book_id = activity['inReplyToBook']
book_id = book_id.split('/')[-1] book = get_or_create_book(book_id, key='remote_id')
name = activity.get('name') name = activity.get('name')
rating = activity.get('rating') rating = activity.get('rating')
content = activity.get('content') content = activity.get('content')
published = activity.get('published') published = activity.get('published')
remote_id = activity['id'] remote_id = activity['id']
book = get_or_create_book(book_id)
review = create_review(author, book, name, content, rating) review = create_review(author, book, name, content, rating)
review.published_date = published review.published_date = published
review.remote_id = remote_id review.remote_id = remote_id
@ -58,8 +56,8 @@ def create_review(user, book, name, content, rating):
def create_quotation_from_activity(author, activity): def create_quotation_from_activity(author, activity):
''' parse an activity json blob into a status ''' ''' parse an activity json blob into a status '''
book = activity['inReplyToBook'] book_id = activity['inReplyToBook']
book = book.split('/')[-1] book = get_or_create_book(book_id, key='remote_id')
quote = activity.get('quote') quote = activity.get('quote')
content = activity.get('content') content = activity.get('content')
published = activity.get('published') published = activity.get('published')
@ -72,10 +70,9 @@ def create_quotation_from_activity(author, activity):
return quotation return quotation
def create_quotation(user, possible_book, content, quote): def create_quotation(user, book, content, quote):
''' a quotation has been added ''' ''' a quotation has been added '''
# throws a value error if the book is not found # throws a value error if the book is not found
book = get_or_create_book(possible_book)
content = sanitize(content) content = sanitize(content)
quote = sanitize(quote) quote = sanitize(quote)
@ -87,11 +84,10 @@ def create_quotation(user, possible_book, content, quote):
) )
def create_comment_from_activity(author, activity): def create_comment_from_activity(author, activity):
''' parse an activity json blob into a status ''' ''' parse an activity json blob into a status '''
book = activity['inReplyToBook'] book_id = activity['inReplyToBook']
book = book.split('/')[-1] book = get_or_create_book(book_id, key='remote_id')
content = activity.get('content') content = activity.get('content')
published = activity.get('published') published = activity.get('published')
remote_id = activity['id'] remote_id = activity['id']
@ -103,10 +99,9 @@ def create_comment_from_activity(author, activity):
return comment return comment
def create_comment(user, possible_book, content): def create_comment(user, book, content):
''' a book comment has been added ''' ''' a book comment has been added '''
# throws a value error if the book is not found # throws a value error if the book is not found
book = get_or_create_book(possible_book)
content = sanitize(content) content = sanitize(content)
return models.Comment.objects.create( return models.Comment.objects.create(
@ -170,34 +165,6 @@ def get_favorite(absolute_id):
return get_by_absolute_id(absolute_id, models.Favorite) return get_by_absolute_id(absolute_id, models.Favorite)
def get_by_absolute_id(absolute_id, model):
''' generalized function to get from a model with a remote_id field '''
if not absolute_id:
return None
# check if it's a remote status
try:
return model.objects.get(remote_id=absolute_id)
except model.DoesNotExist:
pass
# try finding a local status with that id
local_id = absolute_id.split('/')[-1]
try:
if hasattr(model.objects, 'select_subclasses'):
possible_match = model.objects.select_subclasses().get(id=local_id)
else:
possible_match = model.objects.get(id=local_id)
except model.DoesNotExist:
return None
# make sure it's not actually a remote status with an id that
# clashes with a local id
if possible_match.absolute_id == absolute_id:
return possible_match
return None
def create_status(user, content, reply_parent=None, mention_books=None, def create_status(user, content, reply_parent=None, mention_books=None,
remote_id=None): remote_id=None):
''' a status update ''' ''' a status update '''
@ -224,7 +191,7 @@ def create_status(user, content, reply_parent=None, mention_books=None,
def create_tag(user, possible_book, name): def create_tag(user, possible_book, name):
''' add a tag to a book ''' ''' add a tag to a book '''
book = get_or_create_book(possible_book) book = get_or_create_book(possible_book, key='remote_id')
try: try:
tag = models.Tag.objects.create(name=name, book=book, user=user) tag = models.Tag.objects.create(name=name, book=book, user=user)

View file

@ -16,7 +16,7 @@
<div class="book-grid row shrink wrap"> <div class="book-grid row shrink wrap">
{% for book in books %} {% for book in books %}
<div class="book-preview"> <div class="book-preview">
<a href="{{ book.absolute_id }}"> <a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %} {% include 'snippets/book_cover.html' with book=book %}
</a> </a>
{% include 'snippets/shelve_button.html' with book=book %} {% include 'snippets/shelve_button.html' with book=book %}

View file

@ -6,7 +6,7 @@
{% include 'snippets/book_titleby.html' with book=book %} {% include 'snippets/book_titleby.html' with book=book %}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<a href="{{ book.fedireads_key }}/edit" class="edit-link">edit <a href="{{ book.id }}/edit" class="edit-link">edit
<span class="icon icon-pencil"> <span class="icon icon-pencil">
<span class="hidden-text">Edit Book</span> <span class="hidden-text">Edit Book</span>
</span> </span>
@ -22,7 +22,7 @@
{% include 'snippets/shelve_button.html' %} {% include 'snippets/shelve_button.html' %}
{% if request.user.is_authenticated and not book.cover %} {% if request.user.is_authenticated and not book.cover %}
<form name="add-cover" method="POST" action="/upload_cover/{{book.id}}" enctype="multipart/form-data"> <form name="add-cover" method="POST" action="/upload_cover/{{ book.id }}" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ cover_form.as_p }} {{ cover_form.as_p }}
<button type="submit">Add cover</button> <button type="submit">Add cover</button>
@ -57,7 +57,7 @@
<h3>Tags</h3> <h3>Tags</h3>
<form name="tag" action="/tag/" method="post"> <form name="tag" action="/tag/" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.fedireads_key }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="text" name="name"> <input type="text" name="name">
<button type="submit">Add tag</button> <button type="submit">Add tag</button>
</form> </form>

View file

@ -3,19 +3,25 @@
<div class="content-container"> <div class="content-container">
<h2>Search results</h2> <h2>Search results</h2>
{% for result_set in results %} {% for result_set in results %}
{% if result_set.results %}
<section> <section>
{% if not result_set.connector.local %} {% if not result_set.connector.local %}
<h3> <h3>
Results from <a href="{{ result_set.connector.base_url }}" target="_blank">{{ result_set.connector.name }}</a> 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> </h3>
{% endif %} {% endif %}
{% for result in result_set.results %} {% for result in result_set.results %}
<div> <div>
<a href="/book/{{ result.key }}">{{ result.title }}</a> by {{ result.author }} ({{ result.year }}) <form action="/resolve_book" method="POST">
{% csrf_token %}
<input type="hidden" name="remote_id" value="{{ result.key }}">
<button type="submit">{{ result.title }} by {{ result.author }} ({{ result.year }})</button>
</form>
</div> </div>
{% endfor %} {% endfor %}
</section> </section>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -6,7 +6,7 @@
<div class="book-grid row wrap shrink"> <div class="book-grid row wrap shrink">
{% for book in books %} {% for book in books %}
<div class="cover-container"> <div class="cover-container">
<a href="{{ book.absolute_id }}"> <a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %} {% include 'snippets/book_cover.html' with book=book %}
</a> </a>
{% include 'snippets/shelve_button.html' with book=book %} {% include 'snippets/shelve_button.html' with book=book %}

View file

@ -4,7 +4,7 @@
<div class="content-container"> <div class="content-container">
<h2> <h2>
Edit "{{ book.title }}" Edit "{{ book.title }}"
<a href="/book/{{ book.fedireads_key }}"> <a href="/book/{{ book.id }}">
<span class="edit-link icon icon-close"> <span class="edit-link icon icon-close">
<span class="hidden-text">Close</span> <span class="hidden-text">Close</span>
</span> </span>
@ -40,8 +40,8 @@
<h3>Book Identifiers</h2> <h3>Book Identifiers</h2>
<div> <div>
<p><label for="id_isbn">ISBN 13:</label> {{ form.isbn_13 }} </p> <p><label for="id_isbn_13">ISBN 13:</label> {{ form.isbn_13 }} </p>
<p><label for="id_fedireads_key">Fedireads key:</label> {{ form.fedireads_key }} </p> <p><label for="id_isbn_10">ISBN 10:</label> {{ form.isbn_10 }} </p>
<p><label for="id_openlibrary_key">Openlibrary key:</label> {{ form.openlibrary_key }} </p> <p><label for="id_openlibrary_key">Openlibrary key:</label> {{ form.openlibrary_key }} </p>
<p><label for="id_librarything_key">Librarything key:</label> {{ form.librarything_key }} </p> <p><label for="id_librarything_key">Librarything key:</label> {{ form.librarything_key }} </p>
<p><label for="id_goodreads_key">Goodreads key:</label> {{ form.goodreads_key }} </p> <p><label for="id_goodreads_key">Goodreads key:</label> {{ form.goodreads_key }} </p>

View file

@ -2,11 +2,11 @@
{% load fr_display %} {% load fr_display %}
{% block content %} {% block content %}
<div class="content-container"> <div class="content-container">
<h2>Editions of <a href="/book/{{ work.fedireads_key }}">"{{ work.title }}"</a></h2> <h2>Editions of <a href="/book/{{ work.id }}">"{{ work.title }}"</a></h2>
<ol class="book-grid row wrap"> <ol class="book-grid row wrap">
{% for book in editions %} {% for book in editions %}
<li class="book-preview"> <li class="book-preview">
<a href="{{ book.absolute_id }}"> <a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %} {% include 'snippets/book_cover.html' with book=book %}
</a> </a>
{% include 'snippets/shelve_button.html' with book=book %} {% include 'snippets/shelve_button.html' with book=book %}

View file

@ -44,7 +44,7 @@
<tr> <tr>
<td> <td>
{% if item.book %} {% if item.book %}
<a href="{{ item.book.absolute_id }}"> <a href="/book/{{ item.book.id }}">
{% include 'snippets/book_cover.html' with book=item.book size='small' %} {% include 'snippets/book_cover.html' with book=item.book size='small' %}
</a> </a>
{% endif %} {% endif %}

View file

@ -1 +1 @@
<a href="/author/{{ book.authors.first.fedireads_key }}" class="author">{{ book.authors.first.name }}</a> <a href="/author/{{ book.authors.first.id }}" class="author">{{ book.authors.first.name }}</a>

View file

@ -1,5 +1,5 @@
<span class="title"> <span class="title">
<a href="/book/{{ book.fedireads_key }}">{{ book.title }}</a> <a href="/book/{{ book.id }}">{{ book.title }}</a>
</span> </span>
{% if book.authors %} {% if book.authors %}
<span class="author"> <span class="author">

View file

@ -37,7 +37,7 @@
<h2> <h2>
{% include 'snippets/avatar.html' with user=user %} {% include 'snippets/avatar.html' with user=user %}
Your thoughts on Your thoughts on
a <a href="/book/{{ book.fedireads_key }}">{{ book.title }}</a> a <a href="/book/{{ book.id }}">{{ book.title }}</a>
by {% include 'snippets/authors.html' with book=book %} by {% include 'snippets/authors.html' with book=book %}
</h2> </h2>

View file

@ -3,13 +3,13 @@
<div class="tabs secondary"> <div class="tabs secondary">
<div class="tab active" data-id="tab-review-{{ book.id }}" data-category="tab-option-{{ book.id }}"> <div class="tab active" data-id="tab-review-{{ book.id }}" data-category="tab-option-{{ book.id }}">
<a href="{{ book.absolute_id }}/review" onclick="tabChange(event)">Review</a> <a href="/book/{{ book.id }}/review" onclick="tabChange(event)">Review</a>
</div> </div>
<div class="tab" data-id="tab-comment-{{ book.id }}" data-category="tab-option-{{ book.id }}"> <div class="tab" data-id="tab-comment-{{ book.id }}" data-category="tab-option-{{ book.id }}">
<a href="{{ book.absolute_id }}/comment" onclick="tabChange(event)">Comment</a> <a href="/book/{{ book.id }}/comment" onclick="tabChange(event)">Comment</a>
</div> </div>
<div class="tab" data-id="tab-quotation-{{ book.id }}" data-category="tab-option-{{ book.id }}"> <div class="tab" data-id="tab-quotation-{{ book.id }}" data-category="tab-option-{{ book.id }}">
<a href="{{ book.absolute_id }}/quotation" onclick="tabChange(event)">Quote</a> <a href="/book/{{ book.id }}/quotation" onclick="tabChange(event)">Quote</a>
</div> </div>
</div> </div>
@ -21,7 +21,7 @@
{% endif %} {% endif %}
<form class="tab-option-{{ book.id }} review-form" name="review" action="/review/" method="post" id="tab-review-{{ book.id }}"> <form class="tab-option-{{ book.id }} review-form" name="review" action="/review/" method="post" id="tab-review-{{ book.id }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.fedireads_key }}"> <input type="hidden" name="book" value="{{ book.id }}">
{% include 'snippets/rate_form.html' with book=book %} {% include 'snippets/rate_form.html' with book=book %}
{{ review_form.as_p }} {{ review_form.as_p }}
<button type="submit">post review</button> <button type="submit">post review</button>
@ -29,14 +29,14 @@
<form class="hidden tab-option-{{ book.id }} review-form" name="comment" action="/comment/" method="post" id="tab-comment-{{ book.id }}"> <form class="hidden tab-option-{{ book.id }} review-form" name="comment" action="/comment/" method="post" id="tab-comment-{{ book.id }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.fedireads_key }}"> <input type="hidden" name="book" value="{{ book.id }}">
{{ comment_form.as_p }} {{ comment_form.as_p }}
<button type="submit">post comment</button> <button type="submit">post comment</button>
</form> </form>
<form class="hidden tab-option-{{ book.id }} review-form quote-form" name="quotation" action="/quotate/" method="post" id="tab-quotation-{{ book.id }}"> <form class="hidden tab-option-{{ book.id }} review-form quote-form" name="quotation" action="/quotate/" method="post" id="tab-quotation-{{ book.id }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.fedireads_key }}"> <input type="hidden" name="book" value="{{ book.id }}">
{{ quotation_form.as_p }} {{ quotation_form.as_p }}
<button typr="submit">post quote</button> <button typr="submit">post quote</button>
</form> </form>

View file

@ -4,7 +4,7 @@
{% for i in '12345'|make_list %} {% for i in '12345'|make_list %}
<form name="rate" action="/rate/" method="POST" onsubmit="return rate_stars(event)"> <form name="rate" action="/rate/" method="POST" onsubmit="return rate_stars(event)">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.fedireads_key }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="rating" value="{{ forloop.counter }}"> <input type="hidden" name="rating" value="{{ forloop.counter }}">
<button type="submit" class="icon icon-star-{% if book|rating:user < forloop.counter %}empty{% else %}full{% endif %}"> <button type="submit" class="icon icon-star-{% if book|rating:user < forloop.counter %}empty{% else %}full{% endif %}">
<span class="hidden-text">{{ forloop.counter }} star{{ forloop.counter | pluralize }}</span> <span class="hidden-text">{{ forloop.counter }} star{{ forloop.counter | pluralize }}</span>

View file

@ -39,7 +39,7 @@
{% include 'snippets/book_cover.html' with book=book size="small" %} {% include 'snippets/book_cover.html' with book=book size="small" %}
</td> </td>
<td> <td>
<a href="/book/{{ book.fedireads_key }}">{{ book.title }}</a> <a href="/book/{{ book.id }}">{{ book.title }}</a>
</td> </td>
<td> <td>
{{ book.authors.first.name }} {{ book.authors.first.name }}

View file

@ -6,13 +6,13 @@
{% if status.status_type == 'Update' %} {% if status.status_type == 'Update' %}
{{ status.content | safe }} {{ status.content | safe }}
{% elif status.status_type == 'Review' and not status.name and not status.content%} {% elif status.status_type == 'Review' and not status.name and not status.content%}
rated <a href="{{ status.book.absolute_id }}">{{ status.book.title }}</a> rated <a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>
{% elif status.status_type == 'Review' %} {% elif status.status_type == 'Review' %}
reviewed <a href="{{ status.book.absolute_id }}">{{ status.book.title }}</a> reviewed <a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>
{% elif status.status_type == 'Comment' %} {% elif status.status_type == 'Comment' %}
commented on <a href="{{ status.book.absolute_id }}">{{ status.book.title }}</a> commented on <a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>
{% elif status.status_type == 'Quotation' %} {% elif status.status_type == 'Quotation' %}
quoted <a href="{{ status.book.absolute_id }}">{{ status.book.title }}</a> quoted <a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>
{% elif status.status_type == 'Boost' %} {% elif status.status_type == 'Boost' %}
boosted boosted
{% elif status.reply_parent %} {% elif status.reply_parent %}

View file

@ -3,14 +3,14 @@
{% if tag.identifier in user_tags %} {% if tag.identifier in user_tags %}
<form class="tag-form" name="tag" action="/untag/" method="post"> <form class="tag-form" name="tag" action="/untag/" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.fedireads_key }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="name" value="{{ tag.name }}"> <input type="hidden" name="name" value="{{ tag.name }}">
<button type="submit">x</button> <button type="submit">x</button>
</form> </form>
{% else %} {% else %}
<form class="tag-form" name="tag" action="/tag/" method="post"> <form class="tag-form" name="tag" action="/tag/" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.fedireads_key }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="name" value="{{ tag.name }}"> <input type="hidden" name="name" value="{{ tag.name }}">
<button type="submit">+</button> <button type="submit">+</button>
</form> </form>

View file

@ -6,7 +6,7 @@
<div class="book-grid row wrap shrink"> <div class="book-grid row wrap shrink">
{% for book in books.all %} {% for book in books.all %}
<div class="cover-container"> <div class="cover-container">
<a href="{{ book.absolute_id }}"> <a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %} {% include 'snippets/book_cover.html' with book=book %}
</a> </a>
{% include 'snippets/rate_action.html' with user=request.user book=book %} {% include 'snippets/rate_action.html' with user=request.user book=book %}

View file

@ -11,7 +11,7 @@ localname_regex = r'(?P<username>[\w\-_]+)'
user_path = r'^user/%s' % username_regex user_path = r'^user/%s' % username_regex
local_user_path = r'^user/%s' % localname_regex local_user_path = r'^user/%s' % localname_regex
status_path = r'%s/(status|review|comment)/(?P<status_id>\d+)' % local_user_path status_path = r'%s/(status|review|comment)/(?P<status_id>\d+)' % local_user_path
book_path = r'^book/(?P<book_identifier>[\w\-]+)' book_path = r'^book/(?P<book_id>\d+)'
handler404 = 'fedireads.views.not_found_page' handler404 = 'fedireads.views.not_found_page'
handler500 = 'fedireads.views.server_error_page' handler500 = 'fedireads.views.server_error_page'
@ -63,19 +63,21 @@ urlpatterns = [
re_path(r'%s/edit/?$' % book_path, views.edit_book_page), re_path(r'%s/edit/?$' % book_path, views.edit_book_page),
re_path(r'^editions/(?P<work_id>\d+)/?$', views.editions_page), re_path(r'^editions/(?P<work_id>\d+)/?$', views.editions_page),
re_path(r'^author/(?P<author_identifier>[\w\-]+)/?$', views.author_page), re_path(r'^author/(?P<author_id>[\w\-]+)(.json)?/?$', views.author_page),
re_path(r'^tag/(?P<tag_id>.+)/?$', views.tag_page), re_path(r'^tag/(?P<tag_id>.+)/?$', views.tag_page),
re_path(r'^shelf/%s/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % username_regex, views.shelf_page), re_path(r'^shelf/%s/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % username_regex, views.shelf_page),
re_path(r'^shelf/%s/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % localname_regex, views.shelf_page), re_path(r'^shelf/%s/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % localname_regex, views.shelf_page),
re_path(r'^search/?$', views.search),
# internal action endpoints # internal action endpoints
re_path(r'^logout/?$', actions.user_logout), re_path(r'^logout/?$', actions.user_logout),
re_path(r'^user-login/?$', actions.user_login), re_path(r'^user-login/?$', actions.user_login),
re_path(r'^register/?$', actions.register), re_path(r'^register/?$', actions.register),
re_path(r'^edit_profile/?$', actions.edit_profile), re_path(r'^edit_profile/?$', actions.edit_profile),
re_path(r'^search/?$', actions.search),
re_path(r'^import_data/?', actions.import_data), re_path(r'^import_data/?', actions.import_data),
re_path(r'^resolve_book/?', actions.resolve_book),
re_path(r'^edit_book/(?P<book_id>\d+)/?', actions.edit_book), re_path(r'^edit_book/(?P<book_id>\d+)/?', actions.edit_book),
re_path(r'^upload_cover/(?P<book_id>\d+)/?', actions.upload_cover), re_path(r'^upload_cover/(?P<book_id>\d+)/?', actions.upload_cover),

View file

@ -1,6 +1,5 @@
''' views for actions you can take in the application ''' ''' views for actions you can take in the application '''
from io import BytesIO, TextIOWrapper from io import BytesIO, TextIOWrapper
import re
from PIL import Image from PIL import Image
from django.contrib.auth import authenticate, login, logout from django.contrib.auth import authenticate, login, logout
@ -10,7 +9,7 @@ from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from fedireads import forms, models, books_manager, outgoing from fedireads import forms, models, outgoing
from fedireads import goodreads_import from fedireads import goodreads_import
from fedireads.settings import DOMAIN from fedireads.settings import DOMAIN
from fedireads.views import get_user_from_username from fedireads.views import get_user_from_username
@ -116,6 +115,13 @@ def edit_profile(request):
return redirect('/user/%s' % request.user.localname) return redirect('/user/%s' % request.user.localname)
def resolve_book(request):
''' figure out the local path to a book from a remote_id '''
remote_id = request.POST.get('remote_id')
book = get_or_create_book(remote_id, key='remote_id')
return redirect('/book/%d' % book.id)
@login_required @login_required
def edit_book(request, book_id): def edit_book(request, book_id):
''' edit a book cool ''' ''' edit a book cool '''
@ -133,7 +139,7 @@ def edit_book(request, book_id):
form.save() form.save()
outgoing.handle_update_book(request.user, book) outgoing.handle_update_book(request.user, book)
return redirect('/book/%s' % book.fedireads_key) return redirect('/book/%s' % book.id)
@login_required @login_required
@ -157,7 +163,7 @@ def upload_cover(request, book_id):
book.save() book.save()
outgoing.handle_update_book(request.user, book) outgoing.handle_update_book(request.user, book)
return redirect('/book/%s' % book.fedireads_key) return redirect('/book/%s' % book.id)
@login_required @login_required
@ -190,27 +196,25 @@ def shelve(request):
def rate(request): def rate(request):
''' just a star rating for a book ''' ''' just a star rating for a book '''
form = forms.RatingForm(request.POST) form = forms.RatingForm(request.POST)
book_identifier = request.POST.get('book') book_id = request.POST.get('book')
# TODO: better failure behavior # TODO: better failure behavior
if not form.is_valid(): if not form.is_valid():
return redirect('/book/%s' % book_identifier) return redirect('/book/%s' % book_id)
rating = form.cleaned_data.get('rating') rating = form.cleaned_data.get('rating')
# throws a value error if the book is not found # throws a value error if the book is not found
book = get_or_create_book(book_identifier)
outgoing.handle_rate(request.user, book, rating) outgoing.handle_rate(request.user, book_id, rating)
return redirect('/book/%s' % book_identifier) return redirect('/book/%s' % book_id)
@login_required @login_required
def review(request): def review(request):
''' create a book review ''' ''' create a book review '''
form = forms.ReviewForm(request.POST) form = forms.ReviewForm(request.POST)
book_identifier = request.POST.get('book') book_id = request.POST.get('book')
# TODO: better failure behavior
if not form.is_valid(): if not form.is_valid():
return redirect('/book/%s' % book_identifier) return redirect('/book/%s' % book_id)
# TODO: validation, htmlification # TODO: validation, htmlification
name = form.cleaned_data.get('name') name = form.cleaned_data.get('name')
@ -221,42 +225,39 @@ def review(request):
except ValueError: except ValueError:
rating = None rating = None
# throws a value error if the book is not found outgoing.handle_review(request.user, book_id, name, content, rating)
book = get_or_create_book(book_identifier) return redirect('/book/%s' % book_id)
outgoing.handle_review(request.user, book, name, content, rating)
return redirect('/book/%s' % book_identifier)
@login_required @login_required
def quotate(request): def quotate(request):
''' create a book quotation ''' ''' create a book quotation '''
form = forms.QuotationForm(request.POST) form = forms.QuotationForm(request.POST)
book_identifier = request.POST.get('book') book_id = request.POST.get('book')
if not form.is_valid(): if not form.is_valid():
return redirect('/book/%s' % book_identifier) return redirect('/book/%s' % book_id)
quote = form.cleaned_data.get('quote') quote = form.cleaned_data.get('quote')
content = form.cleaned_data.get('content') content = form.cleaned_data.get('content')
outgoing.handle_quotation(request.user, book_identifier, content, quote) outgoing.handle_quotation(request.user, book_id, content, quote)
return redirect('/book/%s' % book_identifier) return redirect('/book/%s' % book_id)
@login_required @login_required
def comment(request): def comment(request):
''' create a book comment ''' ''' create a book comment '''
form = forms.CommentForm(request.POST) form = forms.CommentForm(request.POST)
book_identifier = request.POST.get('book') book_id = request.POST.get('book')
# TODO: better failure behavior # TODO: better failure behavior
if not form.is_valid(): if not form.is_valid():
return redirect('/book/%s' % book_identifier) return redirect('/book/%s' % book_id)
# TODO: validation, htmlification # TODO: validation, htmlification
content = form.data.get('content') content = form.data.get('content')
outgoing.handle_comment(request.user, book_identifier, content) outgoing.handle_comment(request.user, book_id, content)
return redirect('/book/%s' % book_identifier) return redirect('/book/%s' % book_id)
@login_required @login_required
@ -265,20 +266,20 @@ def tag(request):
# I'm not using a form here because sometimes "name" is sent as a hidden # I'm not using a form here because sometimes "name" is sent as a hidden
# field which doesn't validate # field which doesn't validate
name = request.POST.get('name') name = request.POST.get('name')
book_identifier = request.POST.get('book') book_id = request.POST.get('book')
outgoing.handle_tag(request.user, book_identifier, name) outgoing.handle_tag(request.user, book_id, name)
return redirect('/book/%s' % book_identifier) return redirect('/book/%s' % book_id)
@login_required @login_required
def untag(request): def untag(request):
''' untag a book ''' ''' untag a book '''
name = request.POST.get('name') name = request.POST.get('name')
book_identifier = request.POST.get('book') book_id = request.POST.get('book')
outgoing.handle_untag(request.user, book_identifier, name) outgoing.handle_untag(request.user, book_id, name)
return redirect('/book/%s' % book_identifier) return redirect('/book/%s' % book_id)
@login_required @login_required
@ -346,22 +347,6 @@ def unfollow(request):
return redirect('/user/%s' % user_slug) return redirect('/user/%s' % user_slug)
@login_required
def search(request):
''' that search bar up top '''
query = request.GET.get('q')
if re.match(r'\w+@\w+.\w+', query):
# if something looks like a username, search with webfinger
results = [outgoing.handle_account_search(query)]
template = 'user_results.html'
else:
# just send the question over to book search
results = books_manager.search(query)
template = 'book_results.html'
return TemplateResponse(request, template, {'results': results})
@login_required @login_required
def clear_notifications(request): def clear_notifications(request):
''' permanently delete notification for user ''' ''' permanently delete notification for user '''

View file

@ -1,15 +1,19 @@
''' views for pages you can go to in the application ''' ''' views for pages you can go to in the application '''
import re
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Avg, Q from django.db.models import Avg, Q
from django.http import HttpResponseBadRequest, HttpResponseNotFound,\ from django.http import HttpResponseBadRequest, HttpResponseNotFound,\
JsonResponse JsonResponse
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from fedireads import activitypub from fedireads import activitypub, outgoing
from fedireads import forms, models, books_manager from fedireads import forms, models, books_manager
from fedireads import goodreads_import from fedireads import goodreads_import
from fedireads.books_manager import get_or_create_book
from fedireads.tasks import app from fedireads.tasks import app
@ -139,6 +143,27 @@ def get_activity_feed(user, filter_level, model=models.Status):
return activities return activities
def search(request):
''' that search bar up top '''
query = request.GET.get('q')
if re.match(r'\w+@\w+.\w+', query):
# if something looks like a username, search with webfinger
results = [outgoing.handle_account_search(query)]
return TemplateResponse(
request, 'user_results.html', {'results': results}
)
# or just send the question over to book search
if is_api_request(request):
# only return local results via json so we don't cause a cascade
results = books_manager.local_search(query)
return JsonResponse([r.__dict__ for r in results], safe=False)
results = books_manager.search(query)
return TemplateResponse(request, 'book_results.html', {'results': results})
def books_page(request): def books_page(request):
''' discover books ''' ''' discover books '''
recent_books = models.Work.objects recent_books = models.Work.objects
@ -363,10 +388,9 @@ def edit_profile_page(request):
return TemplateResponse(request, 'edit_user.html', data) return TemplateResponse(request, 'edit_user.html', data)
def book_page(request, book_identifier, tab='friends'): def book_page(request, book_id, tab='friends'):
''' info about a book ''' ''' info about a book '''
book = books_manager.get_or_create_book(book_identifier) book = get_or_create_book(book_id)
if is_api_request(request): if is_api_request(request):
return JsonResponse(activitypub.get_book(book)) return JsonResponse(activitypub.get_book(book))
@ -430,7 +454,7 @@ def book_page(request, book_identifier, tab='friends'):
{'id': 'federated', 'display': 'Federated'} {'id': 'federated', 'display': 'Federated'}
], ],
'active_tab': tab, 'active_tab': tab,
'path': '/book/%s' % book_identifier, 'path': '/book/%s' % book_id,
'cover_form': forms.CoverForm(instance=book), 'cover_form': forms.CoverForm(instance=book),
'info_fields': [ 'info_fields': [
{'name': 'ISBN', 'value': book.isbn_13}, {'name': 'ISBN', 'value': book.isbn_13},
@ -445,9 +469,9 @@ def book_page(request, book_identifier, tab='friends'):
@login_required @login_required
def edit_book_page(request, book_identifier): def edit_book_page(request, book_id):
''' info about a book ''' ''' info about a book '''
book = books_manager.get_or_create_book(book_identifier) book = get_or_create_book(book_id)
if not book.description: if not book.description:
book.description = book.parent_work.description book.description = book.parent_work.description
data = { data = {
@ -468,13 +492,16 @@ def editions_page(request, work_id):
return TemplateResponse(request, 'editions.html', data) return TemplateResponse(request, 'editions.html', data)
def author_page(request, author_identifier): def author_page(request, author_id):
''' landing page for an author ''' ''' landing page for an author '''
try: try:
author = models.Author.objects.get(fedireads_key=author_identifier) author = models.Author.objects.get(id=author_id)
except ValueError: except ValueError:
return HttpResponseNotFound() return HttpResponseNotFound()
if is_api_request(request):
return JsonResponse(activitypub.get_author(author))
books = models.Work.objects.filter(authors=author) books = models.Work.objects.filter(authors=author)
data = { data = {
'author': author, 'author': author,

View file

@ -31,10 +31,10 @@ Connector.objects.create(
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,
key_name='openlibrary_key', key_name='id',
priority=1, priority=1,
) )
get_or_create_book('OL1715344W') get_or_create_book('OL1715344W', key='openlibrary_key', connector_id=1)
get_or_create_book('OL102749W') get_or_create_book('OL102749W', key='openlibrary_key', connector_id=1)