Merge pull request #74 from mouse-reeve/book-data-overhaul

Store book data
This commit is contained in:
Mouse Reeve 2020-03-07 12:30:05 -08:00 committed by GitHub
commit 143c496272
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 585 additions and 200 deletions

View file

@ -120,7 +120,7 @@ def get_add_remove(user, book, shelf, action='Add'):
'object': { 'object': {
# TODO: document?? # TODO: document??
'type': 'Document', 'type': 'Document',
'name': book.data['title'], 'name': book.title,
'url': book.openlibrary_key 'url': book.openlibrary_key
}, },
'target': { 'target': {

View file

@ -16,7 +16,7 @@ def get_review_article(review):
''' a book review formatted for a non-fedireads isntance (mastodon) ''' ''' a book review formatted for a non-fedireads isntance (mastodon) '''
status = get_status(review) status = get_status(review)
name = 'Review of "%s" (%d stars): %s' % ( name = 'Review of "%s" (%d stars): %s' % (
review.book.data['title'], review.book.title,
review.rating, review.rating,
review.name review.name
) )

View file

@ -0,0 +1,11 @@
''' select and call a connector for whatever book task needs doing '''
from fedireads.connectors import OpenLibraryConnector
openlibrary = OpenLibraryConnector()
def get_or_create_book(key):
''' pull up a book record by whatever means possible '''
return openlibrary.get_or_create_book(key)
def search(query):
''' ya '''
return openlibrary.search(query)

View file

@ -0,0 +1,3 @@
''' bring connectors into the namespace '''
from .settings import CONNECTORS
from .openlibrary import OpenLibraryConnector

View file

@ -0,0 +1,60 @@
''' functionality outline for a book data connector '''
from abc import ABC, abstractmethod
from fedireads.connectors import CONNECTORS
class AbstractConnector(ABC):
''' generic book data connector '''
def __init__(self, connector_name):
# load connector settings
settings = CONNECTORS.get(connector_name)
if not settings:
raise ValueError('No connector with name "%s"' % connector_name)
try:
self.url = settings['BASE_URL']
self.covers_url = settings['COVERS_URL']
self.db_field = settings['DB_KEY_FIELD']
self.key_name = settings['KEY_NAME']
except KeyError:
raise KeyError('Invalid connector settings')
# TODO: politeness settings
@abstractmethod
def search(self, query):
''' free text search '''
# return list of search result objs
pass
@abstractmethod
def get_or_create_book(self, book_id):
''' request and format a book given an identifier '''
# return book model obj
pass
@abstractmethod
def get_or_create_author(self, book_id):
''' request and format a book given an identifier '''
# return book model obj
pass
@abstractmethod
def update_book(self, book_obj):
''' sync a book with the canonical remote copy '''
# return book model obj
pass
class SearchResult(object):
''' standardized search result object '''
def __init__(self, title, key, author, year):
self.title = title
self.key = key
self.author = author
self.year = year

View file

@ -0,0 +1,138 @@
''' openlibrary data connector '''
from django.core.exceptions import ObjectDoesNotExist
from django.core.files.base import ContentFile
import re
import requests
from fedireads import models
from .abstract_connector import AbstractConnector, SearchResult
class OpenLibraryConnector(AbstractConnector):
''' instantiate a connector for OL '''
def __init__(self):
super().__init__('openlibrary')
def search(self, query):
''' query openlibrary search '''
resp = requests.get('%s/search.json' % self.url, params={'q': query})
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'),
))
return results
def get_or_create_book(self, olkey):
''' pull up a book record by whatever means possible '''
if re.match(r'^OL\d+W$', olkey):
model = models.Work
elif re.match(r'^OL\d+M$', olkey):
model = models.Edition
else:
raise ValueError('Invalid OpenLibrary ID')
try:
book = models.Book.objects.get(openlibrary_key=olkey)
return book
except ObjectDoesNotExist:
# no book was found, so we start creating a new one
book = model(openlibrary_key=olkey)
# load the book json from openlibrary.org
response = requests.get('%s/works/%s.json' % (self.url, olkey))
if not response.ok:
response.raise_for_status()
data = response.json()
# great, we can update our book.
book.title = data['title']
description = data.get('description')
if description:
if isinstance(description, dict):
description = description.get('value')
book.description = description
book.pages = data.get('pages')
#book.published_date = data.get('publish_date')
# this book sure as heck better be an edition
if data.get('works'):
key = data.get('works')[0]['key']
key = key.split('/')[-1]
work = self.get_or_create_book(key)
book.parent_work = work
book.save()
# we also need to know the author get the cover
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_id = author_blob['key']
author_id = author_id.split('/')[-1]
book.authors.add(self.get_or_create_author(author_id))
if data.get('covers') and len(data['covers']):
book.cover.save(*self.get_cover(data['covers'][0]), save=True)
return book
def get_or_create_author(self, olkey):
''' load that author '''
if not re.match(r'^OL\d+A$', olkey):
raise ValueError('Invalid OpenLibrary author ID')
try:
author = models.Author.objects.get(openlibrary_key=olkey)
except ObjectDoesNotExist:
pass
response = requests.get('%s/authors/%s.json' % (self.url, olkey))
if not response.ok:
response.raise_for_status()
data = response.json()
author = models.Author(openlibrary_key=olkey)
bio = data.get('bio')
if bio:
if isinstance(bio, dict):
bio = bio.get('value')
author.bio = bio
name = data['name']
author.name = name
# TODO this is making some BOLD assumption
author.last_name = name.split(' ')[-1]
author.first_name = ' '.join(name.split(' ')[:-1])
#author.born = data.get('birth_date')
#author.died = data.get('death_date')
author.save()
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(requests.get(url).content)
return [image_name, image_content]
def update_book(self, book_obj):
pass

View file

@ -0,0 +1,28 @@
''' settings book data connectors '''
CONNECTORS = {
'openlibrary': {
'KEY_NAME': 'olkey',
'DB_KEY_FIELD': 'openlibrary_key',
'POLITENESS_DELAY': 0,
'MAX_DAILY_QUERIES': -1,
'BASE_URL': 'https://openlibrary.org',
'COVERS_URL': 'https://covers.openlibrary.org',
},
}
''' not implemented yet:
'librarything': {
'KEY_NAME': 'ltkey',
'DB_KEY_FIELD': 'librarything_key',
'POLITENESS_DELAY': 1,
'MAX_DAILY_QUERIES': 1000,
'BASE_URL': 'https://librarything.com',
},
'worldcat': {
'KEY_NAME': 'ocn',
'DB_KEY_FIELD': 'oclc_number',
'POLITENESS_DELAY': 0,
'MAX_DAILY_QUERIES': -1,
'BASE_URL': 'https://worldcat.org',
},
'''

View file

@ -0,0 +1,190 @@
# Generated by Django 3.0.3 on 2020-03-07 06:55
import datetime
from django.db import migrations, models
import django.db.models.deletion
import fedireads.utils.fields
import uuid
class Migration(migrations.Migration):
dependencies = [
('fedireads', '0009_status_published_date'),
]
operations = [
migrations.CreateModel(
name='Edition',
fields=[
('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fedireads.Book')),
('isbn', models.CharField(max_length=255, null=True, unique=True)),
('oclc_number', models.CharField(max_length=255, null=True, unique=True)),
('pages', models.IntegerField(null=True)),
],
options={
'abstract': False,
},
bases=('fedireads.book',),
),
migrations.CreateModel(
name='Work',
fields=[
('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fedireads.Book')),
('lccn', models.CharField(max_length=255, null=True, unique=True)),
],
options={
'abstract': False,
},
bases=('fedireads.book',),
),
migrations.RemoveField(
model_name='author',
name='data',
),
migrations.RemoveField(
model_name='book',
name='added_by',
),
migrations.RemoveField(
model_name='book',
name='data',
),
migrations.AddField(
model_name='author',
name='aliases',
field=fedireads.utils.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, size=None),
),
migrations.AddField(
model_name='author',
name='bio',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='author',
name='born',
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name='author',
name='died',
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name='author',
name='first_name',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='author',
name='last_name',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='author',
name='name',
field=models.CharField(default='Unknown', max_length=255),
preserve_default=False,
),
migrations.AddField(
model_name='author',
name='wikipedia_link',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='book',
name='description',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='book',
name='first_published_date',
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name='book',
name='language',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='book',
name='last_sync_date',
field=models.DateTimeField(default=datetime.datetime.now),
),
migrations.AddField(
model_name='book',
name='librarything_key',
field=models.CharField(max_length=255, null=True, unique=True),
),
migrations.AddField(
model_name='book',
name='local_edits',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='book',
name='local_key',
field=models.CharField(default=uuid.uuid4, max_length=255, unique=True),
),
migrations.AddField(
model_name='book',
name='misc_identifiers',
field=fedireads.utils.fields.JSONField(null=True),
),
migrations.AddField(
model_name='book',
name='origin',
field=models.CharField(max_length=255, null=True, unique=True),
),
migrations.AddField(
model_name='book',
name='published_date',
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name='book',
name='series',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='book',
name='series_number',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='book',
name='sort_title',
field=models.CharField(max_length=255, null=True),
),
migrations.AddField(
model_name='book',
name='subtitle',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='book',
name='sync',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='book',
name='title',
field=models.CharField(default='Unknown', max_length=255),
preserve_default=False,
),
migrations.AlterField(
model_name='author',
name='openlibrary_key',
field=models.CharField(max_length=255, null=True, unique=True),
),
migrations.AlterField(
model_name='book',
name='openlibrary_key',
field=models.CharField(max_length=255, null=True, unique=True),
),
migrations.AddField(
model_name='book',
name='parent_work',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='fedireads.Work'),
),
]

View file

@ -1,5 +1,5 @@
''' bring all the models into the app namespace ''' ''' bring all the models into the app namespace '''
from .book import Shelf, ShelfBook, Book, Author from .book import Book, Work, Edition, Author
from .user import User, UserRelationship, FederatedServer from .shelf import Shelf, ShelfBook
from .status import Status, Review, Favorite, Tag from .status import Status, Review, Favorite, Tag
from .user import User, UserRelationship, FederatedServer

View file

@ -1,68 +1,65 @@
''' database schema for books and shelves ''' ''' database schema for books and shelves '''
from datetime import datetime
from django.db import models from django.db import models
from uuid import uuid4
from fedireads.settings import DOMAIN from fedireads.settings import DOMAIN
from fedireads.utils.fields import JSONField from fedireads.utils.fields import JSONField, ArrayField
from fedireads.utils.models import FedireadsModel from fedireads.utils.models import FedireadsModel
class Shelf(FedireadsModel):
name = models.CharField(max_length=100)
identifier = models.CharField(max_length=100)
user = models.ForeignKey('User', on_delete=models.PROTECT)
editable = models.BooleanField(default=True)
books = models.ManyToManyField(
'Book',
symmetrical=False,
through='ShelfBook',
through_fields=('shelf', 'book')
)
@property
def absolute_id(self):
''' use shelf identifier as absolute id '''
base_path = self.user.absolute_id
model_name = type(self).__name__.lower()
return '%s/%s/%s' % (base_path, model_name, self.identifier)
class Meta:
unique_together = ('user', 'identifier')
class ShelfBook(FedireadsModel):
# many to many join table for books and shelves
book = models.ForeignKey('Book', on_delete=models.PROTECT)
shelf = models.ForeignKey('Shelf', on_delete=models.PROTECT)
added_by = models.ForeignKey(
'User',
blank=True,
null=True,
on_delete=models.PROTECT
)
class Meta:
unique_together = ('book', 'shelf')
class Book(FedireadsModel): class Book(FedireadsModel):
''' a non-canonical copy of a work (not book) from open library ''' ''' a generic book, which can mean either an edition or a work '''
openlibrary_key = models.CharField(max_length=255, unique=True) # these identifiers apply to both works and editions
data = JSONField() openlibrary_key = models.CharField(max_length=255, unique=True, null=True)
librarything_key = models.CharField(max_length=255, unique=True, null=True)
local_key = models.CharField(max_length=255, unique=True, default=uuid4)
misc_identifiers = JSONField(null=True)
# info about where the data comes from and where/if to sync
origin = models.CharField(max_length=255, unique=True, null=True)
local_edits = models.BooleanField(default=False)
sync = models.BooleanField(default=True)
last_sync_date = models.DateTimeField(default=datetime.now)
# TODO: edit history
# book/work metadata
title = models.CharField(max_length=255)
sort_title = models.CharField(max_length=255, null=True)
subtitle = models.TextField(blank=True, null=True)
description = models.TextField(blank=True, null=True)
language = models.CharField(max_length=255, null=True)
series = models.CharField(max_length=255, blank=True, null=True)
series_number = models.CharField(max_length=255, blank=True, null=True)
# TODO: include an annotation about the type of authorship (ie, translator)
authors = models.ManyToManyField('Author') authors = models.ManyToManyField('Author')
# TODO: also store cover thumbnail # TODO: also store cover thumbnail
cover = models.ImageField(upload_to='covers/', blank=True, null=True) cover = models.ImageField(upload_to='covers/', blank=True, null=True)
first_published_date = models.DateTimeField(null=True)
published_date = models.DateTimeField(null=True)
shelves = models.ManyToManyField( shelves = models.ManyToManyField(
'Shelf', 'Shelf',
symmetrical=False, symmetrical=False,
through='ShelfBook', through='ShelfBook',
through_fields=('book', 'shelf') through_fields=('book', 'shelf')
) )
added_by = models.ForeignKey( # TODO: why can't I just call this work????
'User', parent_work = models.ForeignKey('Work', on_delete=models.PROTECT, null=True)
blank=True,
null=True,
on_delete=models.PROTECT class Work(Book):
) ''' a work (an abstract concept of a book that manifests in an edition) '''
# library of congress catalog control number
lccn = models.CharField(max_length=255, unique=True, null=True)
class Edition(Book):
''' an edition of a book '''
# these identifiers only apply to work
isbn = models.CharField(max_length=255, unique=True, null=True)
oclc_number = models.CharField(max_length=255, unique=True, null=True)
pages = models.IntegerField(null=True)
@property @property
def absolute_id(self): def absolute_id(self):
@ -74,6 +71,14 @@ class Book(FedireadsModel):
class Author(FedireadsModel): class Author(FedireadsModel):
''' copy of an author from OL ''' ''' copy of an author from OL '''
openlibrary_key = models.CharField(max_length=255) openlibrary_key = models.CharField(max_length=255, null=True, unique=True)
data = JSONField() wikipedia_link = models.CharField(max_length=255, blank=True, null=True)
# idk probably other keys would be useful here?
born = models.DateTimeField(null=True)
died = models.DateTimeField(null=True)
name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255, null=True)
first_name = models.CharField(max_length=255, null=True)
aliases = ArrayField(models.CharField(max_length=255), blank=True)
bio = models.TextField(null=True, blank=True)

42
fedireads/models/shelf.py Normal file
View file

@ -0,0 +1,42 @@
''' puttin' books on shelves '''
from django.db import models
from fedireads.utils.models import FedireadsModel
class Shelf(FedireadsModel):
name = models.CharField(max_length=100)
identifier = models.CharField(max_length=100)
user = models.ForeignKey('User', on_delete=models.PROTECT)
editable = models.BooleanField(default=True)
books = models.ManyToManyField(
'Book',
symmetrical=False,
through='ShelfBook',
through_fields=('shelf', 'book')
)
@property
def absolute_id(self):
''' use shelf identifier as absolute id '''
base_path = self.user.absolute_id
model_name = type(self).__name__.lower()
return '%s/%s/%s' % (base_path, model_name, self.identifier)
class Meta:
unique_together = ('user', 'identifier')
class ShelfBook(FedireadsModel):
# many to many join table for books and shelves
book = models.ForeignKey('Book', on_delete=models.PROTECT)
shelf = models.ForeignKey('Shelf', on_delete=models.PROTECT)
added_by = models.ForeignKey(
'User',
blank=True,
null=True,
on_delete=models.PROTECT
)
class Meta:
unique_together = ('book', 'shelf')

View file

@ -1,109 +0,0 @@
''' activitystream api and books '''
from django.core.exceptions import ObjectDoesNotExist
from django.core.files.base import ContentFile
import re
import requests
from fedireads.models import Author, Book
from fedireads.settings import OL_URL
def book_search(query):
''' look up a book '''
response = requests.get('%s/search.json' % OL_URL, params={'q': query})
if not response.ok:
response.raise_for_status()
data = response.json()
results = []
for doc in data['docs'][:5]:
key = doc['key']
key = key.split('/')[-1]
author = doc.get('author_name') or ['Unknown']
results.append({
'title': doc.get('title'),
'olkey': key,
'year': doc.get('first_publish_year'),
'author': author[0],
})
return results
def get_or_create_book(olkey, user=None, update=False):
''' add a book by looking up its open library "work" key. I'm conflating
"book" and "work" here a bit; the table is called "book" in fedireads, but
in open library parlance, it's a "work," which is the canonical umbrella
item that contains all the editions ("book"s) '''
# check if this is in the format of an OL book identifier
if not re.match(r'^OL\d+W$', olkey):
raise ValueError('Invalid OpenLibrary work ID')
# get the existing entry from our db, if it exists
try:
book = Book.objects.get(openlibrary_key=olkey)
if not update:
return book
# we have the book, but still want to update it from OL
except ObjectDoesNotExist:
# no book was found, so we start creating a new one
book = Book(openlibrary_key=olkey)
# load the book json from openlibrary.org
response = requests.get('%s/works/%s.json' % (OL_URL, olkey))
if not response.ok:
response.raise_for_status()
data = response.json()
book.data = data
if user and user.is_authenticated:
book.added_by = user
# great, we can update our book.
book.save()
# we also need to know the author get the cover
for author_blob in data['authors']:
# this id starts as "/authors/OL1234567A" and we want just "OL1234567A"
author_id = author_blob['author']['key']
author_id = author_id.split('/')[-1]
book.authors.add(get_or_create_author(author_id))
if data.get('covers') and len(data['covers']):
book.cover.save(*get_cover(data['covers'][0]), save=True)
return book
def get_cover(cover_id):
''' ask openlibrary for the cover '''
# TODO: get medium and small versions
image_name = '%s-M.jpg' % cover_id
url = 'https://covers.openlibrary.org/b/id/%s' % image_name
response = requests.get(url)
if not response.ok:
response.raise_for_status()
image_content = ContentFile(requests.get(url).content)
return [image_name, image_content]
def get_or_create_author(olkey, update=False):
''' load that author '''
if not re.match(r'^OL\d+A$', olkey):
raise ValueError('Invalid OpenLibrary author ID')
try:
author = Author.objects.get(openlibrary_key=olkey)
if not update:
return author
except ObjectDoesNotExist:
pass
response = requests.get('%s/authors/%s.json' % (OL_URL, olkey))
if not response.ok:
response.raise_for_status()
data = response.json()
author = Author(openlibrary_key=olkey, data=data)
author.save()
return author

View file

@ -126,7 +126,7 @@ def handle_shelve(user, book, shelf):
'read': 'finished reading' 'read': 'finished reading'
}[shelf.identifier] }[shelf.identifier]
name = user.name if user.name else user.localname name = user.name if user.name else user.localname
message = '%s %s %s' % (name, verb, book.data['title']) message = '%s %s %s' % (name, verb, book.title)
status = create_status(user, message, mention_books=[book]) status = create_status(user, message, mention_books=[book])
activity = activitypub.get_status(status) activity = activitypub.get_status(status)
@ -150,7 +150,7 @@ def handle_unshelve(user, book, shelf):
def handle_review(user, book, name, content, rating): def handle_review(user, book, name, content, rating):
''' post a review ''' ''' post a review '''
# validated and saves the review in the database so it has an id # validated and saves the review in the database so it has an id
review = create_review(user, book, name, content, rating) review = create_review(user, book, name, content, rating, None)
review_activity = activitypub.get_review(review) review_activity = activitypub.get_review(review)
review_create_activity = activitypub.get_create(user, review_activity) review_create_activity = activitypub.get_create(user, review_activity)

View file

@ -1,6 +1,6 @@
''' Handle user activity ''' ''' Handle user activity '''
from fedireads import models from fedireads import models
from fedireads.openlibrary import get_or_create_book from fedireads.books_manager import get_or_create_book
from fedireads.sanitize_html import InputHtmlParser from fedireads.sanitize_html import InputHtmlParser
from django.db import IntegrityError from django.db import IntegrityError
@ -15,14 +15,17 @@ def create_review(user, possible_book, name, content, rating, published):
# no ratings outside of 0-5 # no ratings outside of 0-5
rating = rating if 0 <= rating <= 5 else 0 rating = rating if 0 <= rating <= 5 else 0
return models.Review.objects.create( review = models.Review(
user=user, user=user,
book=book, book=book,
name=name, name=name,
rating=rating, rating=rating,
content=content, content=content,
published_date=published,
) )
if published:
review.published_date = published
review.save()
return review
def create_status(user, content, reply_parent=None, mention_books=None): def create_status(user, content, reply_parent=None, mention_books=None):

View file

@ -3,9 +3,9 @@
{% block content %} {% block content %}
<div id="content"> <div id="content">
<div> <div>
<h2>{{ author.data.name }}</h2> <h2>{{ author.name }}</h2>
{% if author.data.bio %} {% if author.bio %}
<blockquote>{{ author.data.bio | author_bio }} <blockquote>{{ author.bio | author_bio }}
</blockquote> </blockquote>
{% endif %} {% endif %}
{% for book in books %} {% for book in books %}

View file

@ -3,7 +3,7 @@
{% block content %} {% block content %}
<div id="sidebar"> <div id="sidebar">
<div> <div>
<h2><q>{{ book.data.title }}</q> and You</h2> <h2><q>{{ book.title }}</q> and You</h2>
<p>{% if shelf %}On shelf <q>{{ shelf.name }}</q>{% endif %}</p> <p>{% if shelf %}On shelf <q>{{ shelf.name }}</q>{% endif %}</p>
{% include 'snippets/shelve-button.html' with book=book pulldown=True %} {% include 'snippets/shelve-button.html' with book=book pulldown=True %}
@ -27,13 +27,15 @@
<div id="content"> <div id="content">
<div> <div>
<h2><q>{{ book.data.title }}</q> by <h2><q>{{ book.title }}</q> by
{% include 'snippets/authors.html' with book=book %}</h2> {% include 'snippets/authors.html' with book=book %}</h2>
{% if book.parent_work %}<p>Edition of <a href="/book/{{ book.parent_work.openlibrary_key }}">{{ book.parent_work.title }}</a></p>{% endif %}
<div class="book-preview"> <div class="book-preview">
{% include 'snippets/book_cover.html' with book=book size=large %} {% include 'snippets/book_cover.html' with book=book size=large %}
<p>{{ active_tab }} rating: {{ rating | stars }}</p> <p>{{ active_tab }} rating: {{ rating | stars }}</p>
{% if description %} {% if description %}
<blockquote>{{ book.data.description | description }}</blockquote> <blockquote>{{ book.description | description }}</blockquote>
{% endif %} {% endif %}
<div> <div>
<div id="tag-cloud"> <div id="tag-cloud">

View file

@ -5,7 +5,7 @@
<h1>Search results</h1> <h1>Search results</h1>
{% for result in results %} {% for result in results %}
<div> <div>
<a href="/book/{{ result.olkey }}">{{ result.title }}</a> by {{ result.author }} ({{ result.year }}) <a href="/book/{{ result.key }}">{{ result.title }}</a> by {{ result.author }} ({{ result.year }})
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View file

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

View file

@ -1,10 +1,10 @@
{% load fr_display %} {% load fr_display %}
{% include 'snippets/book_cover.html' with book=book %} {% include 'snippets/book_cover.html' with book=book %}
<p class="title"> <p class="title">
<a href="/book/{{ book.openlibrary_key }}">{{ book.data.title }}</a> <a href="/book/{{ book.openlibrary_key }}">{{ book.title }}</a>
</p> </p>
<p> <p>
{% include 'snippets/authors.html' with book=book %} by {% include 'snippets/authors.html' with book=book %}
</p> </p>
{% if rating %} {% if rating %}
@ -12,7 +12,7 @@
{% endif %} {% endif %}
{% if description %} {% if description %}
<blockquote>{{ book.data.description | description }}</blockquote> <blockquote>{{ book.description | description }}</blockquote>
{% endif %} {% endif %}
{% include 'snippets/shelve-button.html' with book=book pulldown=shelf_pulldown %} {% include 'snippets/shelve-button.html' with book=book pulldown=shelf_pulldown %}

View file

@ -33,19 +33,19 @@
{% include 'snippets/book_cover.html' with book=book %} {% include 'snippets/book_cover.html' with book=book %}
</td> </td>
<td> <td>
<a href="/book/{{ book.openlibrary_key }}">{{ book.data.title }}</a> <a href="/book/{{ book.openlibrary_key }}">{{ book.title }}</a>
</td> </td>
<td> <td>
{{ book.authors.first.data.name }} {{ book.authors.first.data.name }}
</td> </td>
<td> <td>
{{ book.data.first_publish_date }} {{ book.first_publish_date }}
</td> </td>
<td> <td>
{{ book.added_date | naturalday }} {{ book.added_date | naturalday }}
</td> </td>
<td> <td>
<a href="https://openlibrary.org{{ book.data.key }}" target="_blank">OpenLibrary</a> <a href="https://openlibrary.org{{ book.key }}" target="_blank">OpenLibrary</a>
</td> </td>
{% if ratings %} {% if ratings %}
<td> <td>

View file

@ -1,7 +1,7 @@
{% load fr_display %} {% load fr_display %}
<div class="update"> <div class="update">
{% if activity.status_type == 'Review' %} {% if activity.status_type == 'Review' %}
{% include 'snippets/status_banner.html' with content="reviewed <i>"|add:activity.book.data.title|add:"</i>" %} {% include 'snippets/status_banner.html' with content="reviewed <i>"|add:activity.book.title|add:"</i>" %}
<div class="book-preview review"> <div class="book-preview review">
{% include 'snippets/book.html' with book=activity.book size=large %} {% include 'snippets/book.html' with book=activity.book size=large %}

View file

@ -11,6 +11,7 @@ def dict_key(d, k):
'''Returns the given key from a dictionary.''' '''Returns the given key from a dictionary.'''
return d.get(k) or 0 return d.get(k) or 0
@register.filter(name='stars') @register.filter(name='stars')
def stars(number): def stars(number):
''' turn integers into stars ''' ''' turn integers into stars '''
@ -20,16 +21,19 @@ def stars(number):
number = 0 number = 0
return ('' * number) + '' * (5 - number) return ('' * number) + '' * (5 - number)
@register.filter(name='description') @register.filter(name='description')
def description_format(description): def description_format(description):
''' handle the various OL description formats ''' ''' handle the various OL description formats '''
if isinstance(description, dict) and 'value' in description: if not description:
description = description['value'] return ''
if '----------' in description: if '----------' in description:
description = description.split('----------')[0] description = description.split('----------')[0]
return description.strip() return description.strip()
@register.filter(name='author_bio') @register.filter(name='author_bio')
def bio_format(bio): def bio_format(bio):
''' clean up OL author bios ''' ''' clean up OL author bios '''

View file

@ -1,3 +1,4 @@
''' base model with default fields '''
from django.db import models from django.db import models
from fedireads.settings import DOMAIN from fedireads.settings import DOMAIN

View file

@ -5,7 +5,7 @@ from django.shortcuts import redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
import re import re
from fedireads import forms, models, openlibrary, outgoing from fedireads import forms, models, books_manager, outgoing
from fedireads.views import get_user_from_username from fedireads.views import get_user_from_username
@ -150,8 +150,8 @@ def search(request):
results = [outgoing.handle_account_search(query)] results = [outgoing.handle_account_search(query)]
template = 'user_results.html' template = 'user_results.html'
else: else:
# just send the question over to openlibrary for book search # just send the question over to book search
results = openlibrary.book_search(query) results = books_manager.search(query)
template = 'book_results.html' template = 'book_results.html'
return TemplateResponse(request, template, {'results': results}) return TemplateResponse(request, template, {'results': results})

View file

@ -6,7 +6,7 @@ from django.http import 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, openlibrary, incoming from fedireads import forms, models, books_manager, incoming
from fedireads.settings import DOMAIN from fedireads.settings import DOMAIN
@ -188,28 +188,35 @@ def edit_profile_page(request, username):
@login_required @login_required
def book_page(request, book_identifier, tab='friends'): def book_page(request, book_identifier, tab='friends'):
''' info about a book ''' ''' info about a book '''
book = openlibrary.get_or_create_book(book_identifier) book = books_manager.get_or_create_book(book_identifier)
user_reviews = models.Review.objects.filter(user=request.user, book=book).all() if isinstance(book, models.Work):
book_reviews = models.Review.objects.filter(
Q(book=book) | Q(book__parent_work=book),
)
else:
book_reviews = models.Review.objects.filter(book=book)
user_reviews = book_reviews.filter(
user=request.user,
).all()
if tab == 'friends': if tab == 'friends':
reviews = models.Review.objects.filter( reviews = book_reviews.filter(
Q(user__followers=request.user, privacy='public') | \ Q(user__followers=request.user, privacy='public') | \
Q(user=request.user) | \
Q(mention_users=request.user), Q(mention_users=request.user),
book=book,
) )
elif tab == 'local': elif tab == 'local':
reviews = models.Review.objects.filter( reviews = book_reviews.filter(
Q(privacy='public') | \ Q(privacy='public') | \
Q(mention_users=request.user), Q(mention_users=request.user),
user__local=True, user__local=True,
book=book,
) )
else: else:
reviews = models.Review.objects.filter( reviews = book_reviews.filter(
Q(privacy='public') | \ Q(privacy='public') | \
Q(mention_users=request.user), Q(mention_users=request.user),
book=book,
) )
try: try:
@ -251,7 +258,7 @@ def book_page(request, book_identifier, tab='friends'):
def author_page(request, author_identifier): def author_page(request, author_identifier):
''' landing page for an author ''' ''' landing page for an author '''
try: try:
author = models.Author.objects.get(openlibrary_key=author_identifier) author = models.Author.objects.get(books_manager_key=author_identifier)
except ValueError: except ValueError:
return HttpResponseNotFound() return HttpResponseNotFound()

View file

@ -1,5 +1,5 @@
from fedireads.models import User from fedireads.models import User
from fedireads.openlibrary import get_or_create_book from fedireads.books_manager import get_or_create_book
User.objects.create_user('mouse', 'mouse.reeve@gmail.com', 'password123') User.objects.create_user('mouse', 'mouse.reeve@gmail.com', 'password123')
User.objects.create_user('rat', 'rat@rat.com', 'ratword') User.objects.create_user('rat', 'rat@rat.com', 'ratword')