Merge branch 'main' into create-book

This commit is contained in:
Mouse Reeve
2021-03-07 15:49:00 -08:00
77 changed files with 3191 additions and 650 deletions

View File

@ -102,7 +102,7 @@ class ActivityObject:
if allow_create and \
hasattr(model, 'ignore_activity') and \
model.ignore_activity(self):
return None
raise ActivitySerializerError()
# check for an existing instance
instance = instance or model.find_existing(self.serialize())

View File

@ -26,7 +26,7 @@ class Book(ActivityObject):
librarythingKey: str = ''
goodreadsKey: str = ''
cover: Image = field(default_factory=lambda: {})
cover: Image = None
type: str = 'Book'

View File

@ -26,6 +26,7 @@ class AbstractMinimalConnector(ABC):
'books_url',
'covers_url',
'search_url',
'isbn_search_url',
'max_query_count',
'name',
'identifier',
@ -61,6 +62,30 @@ class AbstractMinimalConnector(ABC):
results.append(self.format_search_result(doc))
return results
def isbn_search(self, query):
''' isbn search '''
params = {}
resp = requests.get(
'%s%s' % (self.isbn_search_url, query),
params=params,
headers={
'Accept': 'application/json; charset=utf-8',
'User-Agent': settings.USER_AGENT,
},
)
if not resp.ok:
resp.raise_for_status()
try:
data = resp.json()
except ValueError as e:
logger.exception(e)
raise ConnectorException('Unable to parse json response', e)
results = []
for doc in self.parse_isbn_search_data(data):
results.append(self.format_isbn_search_result(doc))
return results
@abstractmethod
def get_or_create_book(self, remote_id):
''' pull up a book record by whatever means possible '''
@ -73,6 +98,14 @@ class AbstractMinimalConnector(ABC):
def format_search_result(self, search_result):
''' create a SearchResult obj from json '''
@abstractmethod
def parse_isbn_search_data(self, data):
''' turn the result json from a search into a list '''
@abstractmethod
def format_isbn_search_result(self, search_result):
''' create a SearchResult obj from json '''
class AbstractConnector(AbstractMinimalConnector):
''' generic book data connector '''

View File

@ -19,3 +19,11 @@ class Connector(AbstractMinimalConnector):
def format_search_result(self, search_result):
search_result['connector'] = self
return SearchResult(**search_result)
def parse_isbn_search_data(self, data):
return data
def format_isbn_search_result(self, search_result):
search_result['connector'] = self
return SearchResult(**search_result)

View File

@ -1,5 +1,6 @@
''' interface with whatever connectors the app has '''
import importlib
import re
from urllib.parse import urlparse
from requests import HTTPError
@ -15,13 +16,31 @@ class ConnectorException(HTTPError):
def search(query, min_confidence=0.1):
''' find books based on arbitary keywords '''
results = []
# Have we got a ISBN ?
isbn = re.sub('[\W_]', '', query)
maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13
dedup_slug = lambda r: '%s/%s/%s' % (r.title, r.author, r.year)
result_index = set()
for connector in get_connectors():
try:
result_set = connector.search(query, min_confidence=min_confidence)
except (HTTPError, ConnectorException):
continue
result_set = None
if maybe_isbn:
# Search on ISBN
if not connector.isbn_search_url or connector.isbn_search_url == '':
result_set = []
else:
try:
result_set = connector.isbn_search(isbn)
except (HTTPError, ConnectorException):
pass
# if no isbn search or results, we fallback to generic search
if result_set == None or result_set == []:
try:
result_set = connector.search(query, min_confidence=min_confidence)
except (HTTPError, ConnectorException):
continue
result_set = [r for r in result_set \
if dedup_slug(r) not in result_index]
@ -41,6 +60,12 @@ def local_search(query, min_confidence=0.1, raw=False):
return connector.search(query, min_confidence=min_confidence, raw=raw)
def isbn_local_search(query, raw=False):
''' only look at local search results '''
connector = load_connector(models.Connector.objects.get(local=True))
return connector.isbn_search(query, raw=raw)
def first_search_result(query, min_confidence=0.1):
''' search until you find a result that fits '''
for connector in get_connectors():

View File

@ -129,6 +129,22 @@ class Connector(AbstractConnector):
)
def parse_isbn_search_data(self, data):
return list(data.values())
def format_isbn_search_result(self, search_result):
# build the remote id from the openlibrary key
key = self.books_url + search_result['key']
authors = search_result.get('authors') or [{'name': 'Unknown'}]
author_names = [ author.get('name') for author in authors]
return SearchResult(
title=search_result.get('title'),
key=key,
author=', '.join(author_names),
connector=self,
year=search_result.get('publish_date'),
)
def load_edition_data(self, olkey):
''' query openlibrary for editions of a work '''
url = '%s/works/%s/editions' % (self.books_url, olkey)

View File

@ -33,6 +33,31 @@ class Connector(AbstractConnector):
search_results.sort(key=lambda r: r.confidence, reverse=True)
return search_results
def isbn_search(self, query, raw=False):
''' search your local database '''
if not query:
return []
filters = [{f: query} for f in ['isbn_10', 'isbn_13']]
results = models.Edition.objects.filter(
reduce(operator.or_, (Q(**f) for f in filters))
).distinct()
# when there are multiple editions of the same work, pick the default.
# it would be odd for this to happen.
results = results.filter(parent_work__default_edition__id=F('id')) \
or results
search_results = []
for result in results:
if raw:
search_results.append(result)
else:
search_results.append(self.format_search_result(result))
if len(search_results) >= 10:
break
return search_results
def format_search_result(self, search_result):
return SearchResult(
@ -47,6 +72,19 @@ class Connector(AbstractConnector):
)
def format_isbn_search_result(self, search_result):
return SearchResult(
title=search_result.title,
key=search_result.remote_id,
author=search_result.author_text,
year=search_result.published_date.year if \
search_result.published_date else None,
connector=self,
confidence=search_result.rank if \
hasattr(search_result, 'rank') else 1,
)
def is_work_data(self, data):
pass
@ -59,6 +97,10 @@ class Connector(AbstractConnector):
def get_authors_from_data(self, data):
return None
def parse_isbn_search_data(self, data):
''' it's already in the right format, don't even worry about it '''
return data
def parse_search_data(self, data):
''' it's already in the right format, don't even worry about it '''
return data

View File

@ -66,6 +66,7 @@ def init_connectors():
books_url='https://%s/book' % DOMAIN,
covers_url='https://%s/images/covers' % DOMAIN,
search_url='https://%s/search?q=' % DOMAIN,
isbn_search_url='https://%s/isbn/' % DOMAIN,
priority=1,
)
@ -77,6 +78,7 @@ def init_connectors():
books_url='https://bookwyrm.social/book',
covers_url='https://bookwyrm.social/images/covers',
search_url='https://bookwyrm.social/search?q=',
isbn_search_url='https://bookwyrm.social/isbn/',
priority=2,
)
@ -88,6 +90,7 @@ def init_connectors():
books_url='https://openlibrary.org',
covers_url='https://covers.openlibrary.org',
search_url='https://openlibrary.org/search?q=',
isbn_search_url='https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:',
priority=3,
)

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2021-02-28 16:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bookwyrm', '0046_sitesettings_privacy_policy'),
]
operations = [
migrations.AddField(
model_name='connector',
name='isbn_search_url',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@ -449,7 +449,7 @@ def broadcast_task(sender_id, activity, recipients):
for recipient in recipients:
try:
sign_and_send(sender, activity, recipient)
except (HTTPError, SSLError) as e:
except (HTTPError, SSLError, ConnectionError) as e:
logger.exception(e)

View File

@ -37,6 +37,10 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
self.remote_id = None
return super().save(*args, **kwargs)
def broadcast(self, activity, sender, software='bookwyrm'):
''' only send book data updates to other bookwyrm instances '''
super().broadcast(activity, sender, software=software)
class Book(BookDataModel):
''' a generic book, which can mean either an edition or a work '''
@ -91,7 +95,7 @@ class Book(BookDataModel):
@property
def alt_text(self):
''' image alt test '''
text = '%s cover' % self.title
text = '%s' % self.title
if self.edition_info:
text += ' (%s)' % self.edition_info
return text

View File

@ -22,6 +22,7 @@ class Connector(BookWyrmModel):
books_url = models.CharField(max_length=255)
covers_url = models.CharField(max_length=255)
search_url = models.CharField(max_length=255, null=True, blank=True)
isbn_search_url = models.CharField(max_length=255, null=True, blank=True)
politeness_delay = models.IntegerField(null=True, blank=True) #seconds
max_query_count = models.IntegerField(null=True, blank=True)

View File

@ -7,6 +7,7 @@ from bookwyrm import activitypub
from .activitypub_mixin import ActivityMixin
from .base_model import BookWyrmModel
from . import fields
from .status import Status
class Favorite(ActivityMixin, BookWyrmModel):
''' fav'ing a post '''
@ -17,6 +18,11 @@ class Favorite(ActivityMixin, BookWyrmModel):
activity_serializer = activitypub.Like
@classmethod
def ignore_activity(cls, activity):
''' don't bother with incoming favs of unknown statuses '''
return not Status.objects.filter(remote_id=activity.object).exists()
def save(self, *args, **kwargs):
''' update user active time '''
self.user.last_active_date = timezone.now()

View File

@ -3,7 +3,7 @@ import re
from urllib.parse import urlparse
from django.apps import apps
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import AbstractUser, Group
from django.core.validators import MinValueValidator
from django.db import models
from django.utils import timezone
@ -208,6 +208,13 @@ class User(OrderedCollectionPageMixin, AbstractUser):
# an id needs to be set before we can proceed with related models
super().save(*args, **kwargs)
# make users editors by default
try:
self.groups.add(Group.objects.get(name='editor'))
except Group.DoesNotExist:
# this should only happen in tests
pass
# create keys and shelves for new local users
self.key_pair = KeyPair.objects.create(
remote_id='%s/#main-key' % self.remote_id)

View File

@ -141,6 +141,7 @@ LANGUAGE_CODE = 'en-us'
LANGUAGES = [
('en-us', _('English')),
('de-de', _('German')),
('es', _('Spanish')),
('fr-fr', _('French')),
('zh-cn', _('Simplified Chinese')),
]

View File

@ -1,3 +1,8 @@
html {
scroll-behavior: smooth;
scroll-padding-top: 20%;
}
/* --- --- */
.image {
overflow: hidden;

View File

@ -8,9 +8,12 @@ window.onload = function() {
Array.from(document.getElementsByClassName('interaction'))
.forEach(t => t.onsubmit = interact);
// select all
Array.from(document.getElementsByClassName('select-all'))
.forEach(t => t.onclick = selectAll);
// Toggle all checkboxes.
document
.querySelectorAll('[data-action="toggle-all"]')
.forEach(input => {
input.addEventListener('change', toggleAllCheckboxes);
});
// tab groups
Array.from(document.getElementsByClassName('tab-group'))
@ -136,9 +139,20 @@ function interact(e) {
.forEach(t => addRemoveClass(t, 'hidden', t.className.indexOf('hidden') == -1));
}
function selectAll(e) {
e.target.parentElement.parentElement.querySelectorAll('[type="checkbox"]')
.forEach(t => t.checked=true);
/**
* Toggle all descendant checkboxes of a target.
*
* Use `data-target="ID_OF_TARGET"` on the node being listened to.
*
* @param {Event} event - change Event
* @return {undefined}
*/
function toggleAllCheckboxes(event) {
const mainCheckbox = event.target;
document
.querySelectorAll(`#${mainCheckbox.dataset.target} [type="checkbox"]`)
.forEach(checkbox => {checkbox.checked = mainCheckbox.checked;});
}
function toggleMenu(e) {

View File

@ -35,7 +35,7 @@
</div>
<div class="columns">
<div class="column is-narrow">
<div class="column is-one-fifth">
{% include 'snippets/book_cover.html' with book=book size=large %}
{% include 'snippets/rate_action.html' with user=request.user book=book %}
{% include 'snippets/shelve_button/shelve_button.html' %}
@ -93,7 +93,7 @@
</section>
</div>
<div class="column">
<div class="column is-three-fifths">
<div class="block">
<h3 class="field is-grouped">
{% include 'snippets/stars.html' with rating=rating %}
@ -201,7 +201,7 @@
</div>
</div>
<div class="column is-narrow">
<div class="column is-one-fifth">
{% if book.subjects %}
<section class="content block">
<h2 class="title is-5">{% trans "Subjects" %}</h2>
@ -217,7 +217,7 @@
<section class="content block">
<h2 class="title is-5">{% trans "Places" %}</h2>
<ul>
{% for place in book.subject_placess %}
{% for place in book.subject_places %}
<li>{{ place }}</li>
{% endfor %}
</ul>
@ -252,10 +252,10 @@
<div class="media-left">{% include 'snippets/avatar.html' with user=rating.user %}</div>
<div class="media-content">
<div>
{% include 'snippets/username.html' with user=rating.user %}
<a href="{{ rating.user.local_path }}">{{ rating.user.display_name }}</a>
</div>
<div class="field is-grouped mb-0">
<div>{% trans "rated it" %}</div>
<div class="is-flex">
<p class="mr-1">{% trans "rated it" %}</p>
{% include 'snippets/stars.html' with rating=rating.rating %}
</div>
<div>

View File

@ -5,7 +5,7 @@
{% block dropdown-trigger %}{% endblock %}
</button>
<div class="dropdown-menu">
<ul class="dropdown-content" role="menu" id="menu-options-{{ book.id }}">
<ul class="dropdown-content" role="menu" id="menu-options-{{ uuid }}">
{% block dropdown-list %}{% endblock %}
</ul>
</div>

View File

@ -1,8 +1,15 @@
<div class="modal hidden" id="{{ controls_text }}-{{ controls_uid }}">
<div
role="dialog"
class="modal hidden"
id="{{ controls_text }}-{{ controls_uid }}"
aria-labelledby="modal-card-title-{{ controls_text }}-{{ controls_uid }}"
aria-modal="true"
>
{# @todo Implement focus traps to prevent tabbing out of the modal. #}
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head" tabindex="0" id="modal-title-{{ controls_text }}-{{ controls_uid }}">
<h2 class="modal-card-title">
<h2 class="modal-card-title" id="modal-card-title-{{ controls_text }}-{{ controls_uid }}">
{% block modal-title %}{% endblock %}
</h2>
{% include 'snippets/toggle/toggle_button.html' with label="close" class="delete" nonbutton=True %}
@ -18,7 +25,6 @@
</footer>
{% block modal-form-close %}{% endblock %}
</div>
<label class="modal-close is-large" for="{{ controls_text }}-{{ controls_uid }}" aria-label="close"></label>
{% include 'snippets/toggle/toggle_button.html' with label="close" class="modal-close is-large" nonbutton=True %}
</div>

View File

@ -54,11 +54,11 @@
<div class="tile is-child box has-background-white-bis">
<h2 class="title is-4">{% trans "Your Account" %}</h2>
{% include 'user/user_preview.html' with user=request.user %}
{% if request.user.summary %}
<div class="box content">
{% if request.user.summary %}
{{ request.user.summary | to_markdown | safe }}
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
</div>

View File

@ -1,4 +1,5 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% if book %}
<div class="columns">
<div class="column is-narrow">
@ -8,7 +9,7 @@
<div class="column">
<h3 class="title is-5"><a href="/book/{{ book.id }}">{{ book.title }}</a></h3>
{% if book.authors %}
<p class="subtitle is-5">by {% include 'snippets/authors.html' with book=book %}</p>
<p class="subtitle is-5">{% trans "by" %} {% include 'snippets/authors.html' with book=book %}</p>
{% endif %}
{% if book|book_description %}
<blockquote class="content">{{ book|book_description|to_markdown|safe|truncatewords_html:50 }}</blockquote>

View File

@ -1,11 +1,12 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% if book %}
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
<h3 class="title is-6"><a href="/book/{{ book.id }}">{{ book.title }}</a></h3>
{% if book.authors %}
<p class="subtitle is-6">by {% include 'snippets/authors.html' with book=book %}</p>
<p class="subtitle is-6">{% trans "by" %} {% include 'snippets/authors.html' with book=book %}</p>
{% endif %}
{% endif %}

View File

@ -6,13 +6,13 @@
<h1 class="title">{% blocktrans %}{{ tab_title }} Timeline{% endblocktrans %}</h1>
<div class="tabs">
<ul>
<li class="{% if tab == 'home' %}is-active{% endif %}">
<li class="{% if tab == 'home' %}is-active{% endif %}"{% if tab == 'home' %} aria-current="page"{% endif %}>
<a href="/#feed">{% trans "Home" %}</a>
</li>
<li class="{% if tab == 'local' %}is-active{% endif %}">
<li class="{% if tab == 'local' %}is-active{% endif %}"{% if tab == 'local' %} aria-current="page"{% endif %}>
<a href="/local#feed">{% trans "Local" %}</a>
</li>
<li class="{% if tab == 'federated' %}is-active{% endif %}">
<li class="{% if tab == 'federated' %}is-active{% endif %}"{% if tab == 'federated' %} aria-current="page"{% endif %}>
<a href="/federated#feed">{% trans "Federated" %}</a>
</li>
</ul>

View File

@ -4,7 +4,7 @@
{% block panel %}
<header class="block">
<a href="/#feed" class="button" data-back>
<span class="icon icon-arrow-left" aira-hidden="true"></span>
<span class="icon icon-arrow-left" aria-hidden="true"></span>
<span>{% trans "Back" %}</span>
</a>
</header>

View File

@ -5,7 +5,7 @@
{% block title %}{% trans "Import Status" %}{% endblock %}
{% block content %}
{% block content %}{% spaceless %}
<div class="block">
<h1 class="title">{% trans "Import Status" %}</h1>
@ -36,8 +36,19 @@
{% if not job.retry %}
<form name="retry" action="/import/{{ job.id }}" method="post">
{% csrf_token %}
<ul>
<fieldset>
{% with failed_count=failed_items|length %}
{% if failed_count > 10 %}
<p class="block">
<a href="#select-all-failed-imports">
{% blocktrans %}Jump to the bottom of the list to select the {{ failed_count }} items which failed to import.{% endblocktrans %}
</a>
</p>
{% endif %}
{% endwith %}
<fieldset id="failed-imports">
<ul>
{% for item in failed_items %}
<li class="pb-1">
<input class="checkbox" type="checkbox" name="import_item" value="{{ item.id }}" id="import-item-{{ item.id }}">
@ -51,15 +62,28 @@
</p>
</li>
{% endfor %}
</fieldset>
</ul>
<div class="block pt-1 select-all">
<label class="label">
<input type="checkbox" class="checkbox">
</ul>
</fieldset>
<fieldset class="mt-3">
<a name="select-all-failed-imports"></a>
<label class="label is-inline">
<input
id="toggle-all-checkboxes-failed-imports"
class="checkbox"
type="checkbox"
data-action="toggle-all"
data-target="failed-imports"
/>
{% trans "Select all" %}
</label>
</div>
<button class="button" type="submit">{% trans "Retry items" %}</button>
<button class="button is-block mt-3" type="submit">{% trans "Retry items" %}</button>
</fieldset>
<hr>
{% else %}
<ul>
{% for item in failed_items %}
@ -123,4 +147,4 @@
</table>
</div>
</div>
{% endblock %}
{% endspaceless %}{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block title %}{% trans "Search Results" %}{% endblock %}
{% block content %}
{% with book_results|first as local_results %}
<div class="block">
<h1 class="title">{% blocktrans %}Search Results for "{{ query }}"{% endblocktrans %}</h1>
</div>
<div class="block columns">
<div class="column">
<h2 class="title">{% trans "Matching Books" %}</h2>
<section class="block">
{% if not results %}
<p>{% blocktrans %}No books found for "{{ query }}"{% endblocktrans %}</p>
{% else %}
<ul>
{% for result in results %}
<li class="pd-4">
<a href="{{ result.key }}">{% include 'snippets/search_result_text.html' with result=result link=True %}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</section>
<div class="column">
</div>
</div>
{% endwith %}
{% endblock %}

View File

@ -22,7 +22,7 @@
<meta name="twitter:image:alt" content="BookWyrm Logo">
</head>
<body>
<nav class="navbar container" role="navigation" aria-label="main navigation">
<nav class="navbar container" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<img class="image logo" src="{% if site.logo_small %}/images/{{ site.logo_small }}{% else %}/static/images/logo-small.png{% endif %}" alt="Home page">
@ -67,12 +67,20 @@
</div>
<div class="navbar-end">
{% if request.user.is_authenticated %}
{% if request.user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable">
<div class="navbar-link pulldown-menu" role="button" aria-expanded="false" tabindex="0" aria-haspopup="true" aria-controls="navbar-dropdown"><p>
<a
href="{{ user.local_path }}"
class="navbar-link pulldown-menu"
role="button"
aria-expanded="false"
tabindex="0"
aria-haspopup="true"
aria-controls="navbar-dropdown"
>
{% include 'snippets/avatar.html' with user=request.user %}
{% include 'snippets/username.html' with user=request.user %}
</p></div>
<span class="ml-2">{{ user.display_name }}</span>
</a>
<ul class="navbar-dropdown" id="navbar-dropdown">
<li>
<a href="/direct-messages" class="navbar-item">
@ -95,7 +103,7 @@
</a>
</li>
{% if perms.bookwyrm.create_invites or perms.bookwyrm.edit_instance_settings%}
<hr class="navbar-divider">
<li class="navbar-divider" role="presentation"></li>
{% endif %}
{% if perms.bookwyrm.create_invites %}
<li>
@ -111,7 +119,7 @@
</a>
</li>
{% endif %}
<hr class="navbar-divider">
<li class="navbar-divider" role="presentation"></li>
<li>
<a href="/logout" class="navbar-item">
{% trans 'Log out' %}
@ -141,12 +149,12 @@
<div class="columns is-variable is-1">
<div class="column">
<label class="is-sr-only" for="id_localname">{% trans "Username:" %}</label>
<input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname" placeholder="username">
<input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname" placeholder="{% trans 'username' %}">
</div>
<div class="column">
<label class="is-sr-only" for="id_password">{% trans "Username:" %}</label>
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password" placeholder="password">
<p class="help"><a href="/password-reset">Forgot your password?</a></p>
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password" placeholder="{% trans 'password' %}">
<p class="help"><a href="/password-reset">{% trans "Forgot your password?" %}</a></p>
</div>
<div class="column is-narrow">
<button class="button is-primary" type="submit">{% trans "Log in" %}</button>
@ -157,7 +165,7 @@
{% if site.allow_registration and request.path != '' and request.path != '/' %}
<div class="column is-narrow">
<a href="/" class="button is-link">
Join
{% trans "Join" %}
</a>
</div>
{% endif %}
@ -191,7 +199,7 @@
{% if site.support_link %}
<div class="column">
<span class="icon icon-heart"></span>
Support {{ site.name }} on <a href="{{ site.support_link }}" target="_blank">{{ site.support_title }}</a>
{% blocktrans with site_name=site.name support_link=site.support_link support_title=site.support_title %}Support {{ site_name }} on <a href="{{ support_link }}" target="_blank">{{ support_title }}</a>{% endblocktrans %}
</div>
{% endif %}
<div class="column">

View File

@ -0,0 +1,10 @@
{% load i18n %}
{% spaceless %}
{% if list.curation != 'open' %}
{% blocktrans with username=list.user.display_name path=list.user.local_path %}Created and curated by <a href="{{ path }}">{{ username }}</a>{% endblocktrans %}
{% else %}
{% blocktrans with username=list.user.display_name path=list.user.local_path %}Created by <a href="{{ path }}">{{ username }}</a>{% endblocktrans %}
{% endif %}
{% endspaceless %}

View File

@ -24,7 +24,7 @@
{% include 'snippets/book_titleby.html' with book=item.book %}
</td>
<td>
{% include 'snippets/username.html' with user=item.user %}
<a href="{{ item.user.local_path }}">{{ item.user.display_name }}</a>
</td>
<td>
<div class="field has-addons">

View File

@ -1,5 +1,4 @@
{% load bookwyrm_tags %}
{% load i18n %}
<div class="columns is-multiline">
{% for list in lists %}
<div class="column is-one-quarter">
@ -16,7 +15,9 @@
</div>
<div class="card-content is-flex-grow-0">
{% if list.description %}{{ list.description | to_markdown | safe | truncatewords_html:20 }}{% endif %}
<p class="subtitle help">{% if list.curation != 'open' %}{% trans "Created and curated by" %}{% else %}{% trans "Created by" %}{% endif %} {% include 'snippets/username.html' with user=list.user %}</p>
<p class="subtitle help">
{% include 'lists/created_text.html' with list=list %}
</p>
</div>
</div>
</div>

View File

@ -8,8 +8,9 @@
<header class="columns content is-mobile">
<div class="column">
<h1 class="title">{{ list.name }} <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span></h1>
<p class="subtitle help">{% if list.curation != 'open' %}{% trans "Created and curated by" %}{% else %}{% trans "Created by" %} {% include 'snippets/username.html' with user=list.user %}</p>
{% endif %}
<p class="subtitle help">
{% include 'lists/created_text.html' with list=list %}
</p>
{% include 'snippets/trimmed_text.html' with full=list.description %}
</div>
{% if request.user == list.user %}

View File

@ -6,6 +6,6 @@
{% block content %}
<div class="block">
<h1 class="title">{% trans "Not Found" %}</h1>
<p>{% trans "The page your requested doesn't seem to exist!" %}</p>
<p>{% trans "The page you requested doesn't seem to exist!" %}</p>
</div>
{% endblock %}

View File

@ -42,8 +42,10 @@
<p>
{# DESCRIPTION #}
{% if notification.related_user %}
{% include 'snippets/avatar.html' with user=notification.related_user %}
{% include 'snippets/username.html' with user=notification.related_user %}
<a href="{{ notification.related_user.local_path }}">
{% include 'snippets/avatar.html' with user=notification.related_user %}
{{ notification.related_user.display_name }}
</a>
{% if notification.notification_type == 'FAVORITE' %}
{% if related_status.status_type == 'Review' %}
{% blocktrans with book_title=related_status.book.title related_path=related_status.local_path %}favorited your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>{% endblocktrans %}
@ -87,11 +89,11 @@
</div>
{% elif notification.notification_type == 'BOOST' %}
{% if related_status.status_type == 'Review' %}
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}boosted your <a href="{{ related_path }}">review of <em>{{ book.title }}</em></a>{% endblocktrans %}
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}boosted your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>{% endblocktrans %}
{% elif related_status.status_type == 'Comment' %}
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}boosted your <a href="{{ related_path }}">comment on<em>{{ book.title }}</em></a>{% endblocktrans %}
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}boosted your <a href="{{ related_path }}">comment on<em>{{ book_title }}</em></a>{% endblocktrans %}
{% elif related_status.status_type == 'Quotation' %}
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}boosted your <a href="{{ related_path }}">quote from <em>{{ book.title }}</em></a>{% endblocktrans %}
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}boosted your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>{% endblocktrans %}
{% else %}
{% blocktrans with related_path=related_status.local_path %}boosted your <a href="{{ related_path }}">status</a>{% endblocktrans %}
{% endif %}
@ -114,7 +116,9 @@
<div class="columns">
<div class="column">
{% if related_status.content %}
<a href="{{ related_status.local_path }}">{{ related_status.content | safe | truncatewords_html:10 }}</a>
<a href="{{ related_status.local_path }}">
{{ related_status.content | safe | truncatewords_html:10 }}{% if related_status.mention_books %} <em>{{ related_status.mention_books.first.title }}</em>{% endif %}
</a>
{% elif related_status.quote %}
<a href="{{ related_status.local_path }}">{{ related_status.quote | safe | truncatewords_html:10 }}</a>
{% elif related_status.rating %}

View File

@ -15,7 +15,7 @@
{% for user in request.user.blocks.all %}
<li class="is-flex">
<p>
{% include 'snippets/avatar.html' with user=user %} {% include 'snippets/username.html' with user=user %}
<a href="{{ user.local_path }}">{% include 'snippets/avatar.html' with user=user %} {{ user.display_name }}</a>
</p>
<p class="mr-2">
{% include 'snippets/block_button.html' with user=user %}

View File

@ -79,8 +79,10 @@
<ul>
{% for result in user_results %}
<li class="block">
{% include 'snippets/avatar.html' with user=result %}</h2>
{% include 'snippets/username.html' with user=result show_full=True %}</h2>
<a href="{{ result.local_path }}">
{% include 'snippets/avatar.html' with user=result %}
{{ result.display_name }}
</a> ({{ result.username }})
{% include 'snippets/follow_button.html' with user=result %}
</li>
{% endfor %}

View File

@ -1,3 +1,3 @@
{% load bookwyrm_tags %}
<img class="avatar image {% if large %}is-96x96{% else %}is-32x32{% endif %}" src="{% if user.avatar %}/images/{{ user.avatar }}{% else %}/static/images/default_avi.jpg{% endif %}" alt="{{ user.alt_text }}">
<img class="avatar image {% if large %}is-96x96{% else %}is-32x32{% endif %}" src="{% if user.avatar %}/images/{{ user.avatar }}{% else %}/static/images/default_avi.jpg{% endif %}" {% if ariaHide %}aria-hidden="true"{% endif %} alt="{{ user.alt_text }}">

View File

@ -6,8 +6,7 @@
<div class="no-cover book-cover">
<img class="book-cover" src="/static/images/no_cover.jpg" alt="No cover">
<div>
<p>{{ book.title }}</p>
<p>({{ book.edition_info }})</p>
<p>{{ book.alt_text }}</p>
</div>
</div>
{% endif %}

View File

@ -1,5 +1,5 @@
{% load i18n %}
<nav class="pagination" role="navigation" aria-label="pagination">
<nav class="pagination" aria-label="pagination">
{% if page.has_previous %}
<p class="pagination-previous">
<a href="{{ path }}?page={{ page.previous_page_number }}{{ anchor }}">

View File

@ -67,4 +67,4 @@
</div>
</form>
</div>
{% include 'snippets/delete_readthrough_modal.html' with controls_text="delete-readthrough" controls_uid=readthrough.id %}
{% include 'snippets/delete_readthrough_modal.html' with controls_text="delete-readthrough" controls_uid=readthrough.id no_body=True %}

View File

@ -18,7 +18,7 @@
</li>
{% endif %}
{% endfor %}
<hr class="navbar-divider">
<li class="navbar-divider" role="presentation"></li>
<li>
<form class="dropdown-item pt-0 pb-0" name="shelve" action="/unshelve/" method="post">
{% csrf_token %}

View File

@ -2,8 +2,10 @@
{% load i18n %}
{% if not status.deleted %}
{% if status.status_type == 'Announce' %}
{% include 'snippets/avatar.html' with user=status.user %}
{% include 'snippets/username.html' with user=status.user %}
<a href="{{ status.user.local_path }}">
{% include 'snippets/avatar.html' with user=status.user %}
{{ status.user.display_name }}
</a>
{% trans "boosted" %}
{% include 'snippets/status/status_body.html' with status=status|boosted_status %}
{% else %}

View File

@ -3,10 +3,12 @@
<div class="block">
{% if status.status_type == 'Review' %}
<div>
<h3 class="title is-5 has-subtitle">
{% if status.name %}<span dir="auto">{{ status.name }}</span><br>{% endif %}
{% if status.name %}
<h3 class="title is-5 has-subtitle" dir="auto">
{{ status.name|escape }}
</h3>
<p class="subtitle">{% include 'snippets/stars.html' with rating=status.rating %}</p>
{% endif %}
{% include 'snippets/stars.html' with rating=status.rating %}
</div>
{% endif %}
@ -35,7 +37,7 @@
{% if status.content and status.status_type != 'GeneratedNote' and status.status_type != 'Announce' %}
{% include 'snippets/trimmed_text.html' with full=status.content|safe %}
{% endif %}
{% if status.attachments %}
{% if status.attachments.exists %}
<div class="block">
<div class="columns">
{% for attachment in status.attachments.all %}

View File

@ -1,7 +1,9 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% include 'snippets/avatar.html' with user=status.user %}
{% include 'snippets/username.html' with user=status.user %}
<a href="{{ status.user.local_path }}">
{% include 'snippets/avatar.html' with user=status.user ariaHide="true" %}
{{ status.user.display_name }}
</a>
{% if status.status_type == 'GeneratedNote' %}
{{ status.content | safe }}
@ -15,7 +17,17 @@
{% trans "quoted" %}
{% elif status.reply_parent %}
{% with parent_status=status|parent %}
replied to {% include 'snippets/username.html' with user=parent_status.user possessive=True %} <a href="{{parent_status.remote_id }}">{% if parent_status.status_type == 'GeneratedNote' %}update{% else %}{{ parent_status.status_type | lower }}{% endif %}</a>
{% if parent_status.status_type == 'Review' %}
{% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">review</a>{% endblocktrans %}
{% elif parent_status.status_type == 'Comment' %}
{% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">comment</a>{% endblocktrans %}
{% elif parent_status.status_type == 'Quotation' %}
{% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">quote</a>{% endblocktrans %}
{% else %}
{% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">status</a>{% endblocktrans %}
{% endif %}
{% endwith %}
{% endif %}
{% if status.book %}

View File

@ -8,19 +8,29 @@
{% with full|to_markdown|safe|truncatewords_html:60 as trimmed %}
{% if trimmed != full %}
<div id="hide-full-{{ uuid }}">
<div class="content" id="trimmed-{{ uuid }}"><span dir="auto">{{ trimmed }}</span>
{% trans "Show more" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text controls_text="full" controls_uid=uuid class="is-small" %}
<div class="content" id="trimmed-{{ uuid }}">
<p dir="auto">{{ trimmed }}</p>
<div>
{% trans "Show more" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text controls_text="full" controls_uid=uuid class="is-small" %}
</div>
</div>
</div>
<div id="full-{{ uuid }}" class="hidden">
<div class="content"><span dir="auto">{{ full }}</span>
{% trans "Show less" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="full" controls_uid=uuid class="is-small" %}
<div class="content">
<div dir="auto">{{ full }}</div>
<div>
{% trans "Show less" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="full" controls_uid=uuid class="is-small" %}
</div>
</div>
</div>
{% else %}
<div class="content"><span dir="auto">{{ full }}</span></div>
<div class="content">
<div dir="auto">{{ full }}</div>
</div>
{% endif %}
{% endwith %}

View File

@ -1,2 +0,0 @@
{% load bookwyrm_tags %}
<a href="{{ user.local_path }}" class="user">{% if user.name %}{{ user.name }}{% else %}{{ user | username }}{% endif %}</a>{% if possessive %}'s{% endif %}{% if show_full and user.name or show_full and user.localname %} ({{ user.username }}){% endif %}

View File

@ -11,19 +11,22 @@
{% block panel %}
<div class="block">
<h2 class="title">{% trans "Followers" %}</h2>
{% for followers in followers %}
{% for follower in followers %}
<div class="block columns">
<div class="column">
{% include 'snippets/avatar.html' with user=followers %}
{% include 'snippets/username.html' with user=followers show_full=True %}
<a href="{{ follower.local_path }}">
{% include 'snippets/avatar.html' with user=follower %}
{{ follower.display_name }}
</a>
({{ follower.username }})
</div>
<div class="column is-narrow">
{% include 'snippets/follow_button.html' with user=followers %}
{% include 'snippets/follow_button.html' with user=follower %}
</div>
</div>
{% endfor %}
{% if not followers.count %}
<div>{% blocktrans with username=user|username %}{{ username }} has no followers{% endblocktrans %}</div>
<div>{% blocktrans with username=user.display_name %}{{ username }} has no followers{% endblocktrans %}</div>
{% endif %}
</div>
{% endblock %}

View File

@ -14,8 +14,11 @@
{% for follower in user.following.all %}
<div class="block columns">
<div class="column">
{% include 'snippets/avatar.html' with user=follower %}
{% include 'snippets/username.html' with user=follower show_full=True %}
<a href="{{ follower.local_path }}">
{% include 'snippets/avatar.html' with user=follower %}
{{ follower.display_name }}
</a>
({{ follower.username }})
</div>
<div class="column">
{% include 'snippets/follow_button.html' with user=follower %}

View File

@ -1,7 +1,7 @@
{% extends 'user/user_layout.html' %}
{% load i18n %}
{% block title %}{{ user.name }}{% endblock %}
{% block title %}{{ user.display_name }}{% endblock %}
{% block header %}
<div class="columns is-mobile">
@ -67,7 +67,7 @@
</div>
</div>
{% for activity in activities %}
<div class="block" id="feed">
<div class="block" id="feed-{{ activity.id }}">
{% include 'snippets/status/status.html' with status=activity %}
</div>
{% endfor %}

View File

@ -17,11 +17,11 @@
{% include 'user/user_preview.html' with user=user %}
</div>
{% if user.summary %}
<div class="column box has-background-white-bis content">
{% if user.summary %}
{{ user.summary | to_markdown | safe }}
{% endif %}
</div>
{% endif %}
</div>
{% if not is_self and request.user.is_authenticated %}
{% include 'snippets/follow_button.html' with user=user %}
@ -33,7 +33,7 @@
{% for requester in user.follower_requests.all %}
<div class="row shrink">
<p>
{% include 'snippets/username.html' with user=requester show_full=True %}
<a href="{{ requester.local_path }}">{{ requester.display_name }}</a> ({{ requester.username }})
</p>
{% include 'snippets/follow_request_buttons.html' with user=requester %}
</div>
@ -56,7 +56,7 @@
<a href="{{ url }}">{% trans "Reading Goal" %}</a>
</li>
{% endif %}
{% if is_self or user.lists.exists %}
{% if is_self or user.list_set.exists %}
{% url 'user-lists' user|username as url %}
<li{% if url in request.path %} class="is-active"{% endif %}>
<a href="{{ url }}">{% trans "Lists" %}</a>

View File

@ -1 +0,0 @@
from . import *

View File

@ -208,7 +208,10 @@ class BaseActivity(TestCase):
# sets the celery task call to the function call
with patch(
'bookwyrm.activitypub.base_activity.set_related_field.delay'):
update_data.to_model(model=models.Status, instance=status)
with patch('bookwyrm.models.status.Status.ignore_activity') \
as discarder:
discarder.return_value = False
update_data.to_model(model=models.Status, instance=status)
self.assertIsNone(status.attachments.first())

View File

@ -42,6 +42,10 @@ class AbstractConnector(TestCase):
return search_result
def parse_search_data(self, data):
return data
def format_isbn_search_result(self, search_result):
return search_result
def parse_isbn_search_data(self, data):
return data
def is_work_data(self, data):
return data['type'] == 'work'
def get_edition_from_work_data(self, data):

View File

@ -18,6 +18,7 @@ class AbstractConnector(TestCase):
books_url='https://example.com/books',
covers_url='https://example.com/covers',
search_url='https://example.com/search?q=',
isbn_search_url='https://example.com/isbn',
)
class TestConnector(abstract_connector.AbstractMinimalConnector):
@ -28,6 +29,10 @@ class AbstractConnector(TestCase):
pass
def parse_search_data(self, data):
return data
def format_isbn_search_result(self, search_result):
return search_result
def parse_isbn_search_data(self, data):
return data
self.test_connector = TestConnector('example.com')
@ -39,6 +44,7 @@ class AbstractConnector(TestCase):
self.assertEqual(connector.books_url, 'https://example.com/books')
self.assertEqual(connector.covers_url, 'https://example.com/covers')
self.assertEqual(connector.search_url, 'https://example.com/search?q=')
self.assertEqual(connector.isbn_search_url, 'https://example.com/isbn')
self.assertIsNone(connector.name)
self.assertEqual(connector.identifier, 'example.com')
self.assertIsNone(connector.max_query_count)

View File

@ -27,6 +27,7 @@ class Openlibrary(TestCase):
books_url='https://openlibrary.org',
covers_url='https://covers.openlibrary.org',
search_url='https://openlibrary.org/search?q=',
isbn_search_url='https://openlibrary.org/isbn',
)
self.connector = Connector('openlibrary.org')
@ -149,6 +150,34 @@ class Openlibrary(TestCase):
self.assertEqual(result.connector, self.connector)
def test_parse_isbn_search_result(self):
''' extract the results from the search json response '''
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ol_isbn_search.json')
search_data = json.loads(datafile.read_bytes())
result = self.connector.parse_isbn_search_data(search_data)
self.assertIsInstance(result, list)
self.assertEqual(len(result), 1)
def test_format_isbn_search_result(self):
''' translate json from openlibrary into SearchResult '''
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ol_isbn_search.json')
search_data = json.loads(datafile.read_bytes())
results = self.connector.parse_isbn_search_data(search_data)
self.assertIsInstance(results, list)
result = self.connector.format_isbn_search_result(results[0])
self.assertIsInstance(result, SearchResult)
self.assertEqual(result.title, 'Les ombres errantes')
self.assertEqual(
result.key, 'https://openlibrary.org/books/OL16262504M')
self.assertEqual(result.author, 'Pascal Quignard')
self.assertEqual(result.year, '2002')
self.assertEqual(result.connector, self.connector)
@responses.activate
def test_load_edition_data(self):
''' format url from key and make request '''

View File

@ -0,0 +1,45 @@
{
"ISBN:9782070427796": {
"url": "https://openlibrary.org/books/OL16262504M/Les_ombres_errantes",
"key": "/books/OL16262504M",
"title": "Les ombres errantes",
"authors": [
{
"url": "https://openlibrary.org/authors/OL269675A/Pascal_Quignard",
"name": "Pascal Quignard"
}
],
"by_statement": "Pascal Quignard.",
"identifiers": {
"goodreads": [
"1835483"
],
"librarything": [
"983474"
],
"isbn_10": [
"207042779X"
],
"openlibrary": [
"OL16262504M"
]
},
"classifications": {
"dewey_decimal_class": [
"848/.91403"
]
},
"publishers": [
{
"name": "Gallimard"
}
],
"publish_places": [
{
"name": "Paris"
}
],
"publish_date": "2002",
"notes": "Hardback published Grasset, 2002."
}
}

View File

@ -81,7 +81,7 @@ class Book(TestCase):
book.save()
self.assertEqual(book.edition_info, 'worm, Glorbish language, 2020')
self.assertEqual(
book.alt_text, 'Test Edition cover (worm, Glorbish language, 2020)')
book.alt_text, 'Test Edition (worm, Glorbish language, 2020)')
def test_get_rank(self):

View File

@ -150,7 +150,7 @@ class Status(TestCase):
self.assertEqual(activity['attachment'][0].url, 'https://%s%s' % \
(settings.DOMAIN, self.book.cover.url))
self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover')
activity['attachment'][0].name, 'Test Edition')
def test_comment_to_activity(self, _):
''' subclass of the base model version with a "pure" serializer '''
@ -177,7 +177,7 @@ class Status(TestCase):
self.assertEqual(activity['attachment'][0].url, 'https://%s%s' % \
(settings.DOMAIN, self.book.cover.url))
self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover')
activity['attachment'][0].name, 'Test Edition')
def test_quotation_to_activity(self, _):
''' subclass of the base model version with a "pure" serializer '''
@ -207,7 +207,7 @@ class Status(TestCase):
self.assertEqual(activity['attachment'][0].url, 'https://%s%s' % \
(settings.DOMAIN, self.book.cover.url))
self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover')
activity['attachment'][0].name, 'Test Edition')
def test_review_to_activity(self, _):
''' subclass of the base model version with a "pure" serializer '''
@ -238,7 +238,7 @@ class Status(TestCase):
self.assertEqual(activity['attachment'][0].url, 'https://%s%s' % \
(settings.DOMAIN, self.book.cover.url))
self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover')
activity['attachment'][0].name, 'Test Edition')
def test_favorite(self, _):
''' fav a status '''

View File

@ -74,7 +74,7 @@ class Inbox(TestCase):
mock_valid.return_value = False
result = self.client.post(
'/user/mouse/inbox',
'{"type": "Test", "object": "exists"}',
'{"type": "Announce", "object": "exists"}',
content_type="application/json"
)
self.assertEqual(result.status_code, 401)
@ -484,7 +484,7 @@ class Inbox(TestCase):
'actor': 'https://example.com/users/rat',
'type': 'Like',
'published': 'Mon, 25 May 2020 19:31:20 GMT',
'object': 'https://example.com/status/1',
'object': self.status.remote_id,
}
views.inbox.activity_task(activity)
@ -494,6 +494,21 @@ class Inbox(TestCase):
self.assertEqual(fav.remote_id, 'https://example.com/fav/1')
self.assertEqual(fav.user, self.remote_user)
def test_ignore_favorite(self):
''' don't try to save an unknown status '''
activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://example.com/fav/1',
'actor': 'https://example.com/users/rat',
'type': 'Like',
'published': 'Mon, 25 May 2020 19:31:20 GMT',
'object': 'https://unknown.status/not-found',
}
views.inbox.activity_task(activity)
self.assertFalse(models.Favorite.objects.exists())
def test_handle_unfavorite(self):
''' fav a status '''
activity = {

View File

@ -0,0 +1,54 @@
''' test for app action functionality '''
import json
from unittest.mock import patch
from django.http import JsonResponse
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.connectors import abstract_connector
from bookwyrm.settings import DOMAIN
class IsbnViews(TestCase):
''' tag views'''
def setUp(self):
''' we need basic test data and mocks '''
self.factory = RequestFactory()
self.local_user = models.User.objects.create_user(
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse',
remote_id='https://example.com/users/mouse',
)
self.work = models.Work.objects.create(title='Test Work')
self.book = models.Edition.objects.create(
title='Test Book',
isbn_13='1234567890123',
remote_id='https://example.com/book/1',
parent_work=self.work
)
models.Connector.objects.create(
identifier='self',
connector_file='self_connector',
local=True
)
models.SiteSettings.objects.create()
def test_isbn_json_response(self):
''' searches local data only and returns book data in json format '''
view = views.Isbn.as_view()
request = self.factory.get('')
with patch('bookwyrm.views.isbn.is_api_request') as is_api:
is_api.return_value = True
response = view(request, isbn='1234567890123')
self.assertIsInstance(response, JsonResponse)
data = json.loads(response.content)
self.assertEqual(len(data), 1)
self.assertEqual(data[0]['title'], 'Test Book')
self.assertEqual(
data[0]['key'], 'https://%s/book/%d' % (DOMAIN, self.book.id))

View File

@ -64,6 +64,10 @@ class ShelfViews(TestCase):
pass
def parse_search_data(self, data):
pass
def format_isbn_search_result(self, search_result):
return search_result
def parse_isbn_search_data(self, data):
return data
models.Connector.objects.create(
identifier='example.com',
connector_file='openlibrary',

View File

@ -137,6 +137,9 @@ urlpatterns = [
re_path(r'^switch-edition/?$', views.switch_edition),
re_path(r'^create-book/?$', views.EditBook.as_view()),
# isbn
re_path(r'^isbn/(?P<isbn>\d+)(.json)?/?$', views.Isbn.as_view()),
# author
re_path(r'^author/(?P<author_id>\d+)(.json)?/?$', views.Author.as_view()),
re_path(r'^author/(?P<author_id>\d+)/edit/?$', views.EditAuthor.as_view()),

View File

@ -31,3 +31,4 @@ from .site import Site
from .status import CreateStatus, DeleteStatus
from .updates import Updates
from .user import User, EditUser, Followers, Following
from .isbn import Isbn

View File

@ -90,7 +90,7 @@ class Book(View):
'rating': reviews.aggregate(Avg('rating'))['rating__avg'],
'tags': models.UserTag.objects.filter(book=book),
'lists': privacy_filter(
request.user, book.list_set.all()
request.user, book.list_set.filter(listitem__approved=True)
),
'user_tags': user_tags,
'user_shelves': user_shelves,

View File

@ -20,7 +20,7 @@ class Inbox(View):
''' requests sent by outside servers'''
def post(self, request, username=None):
''' only works as POST request '''
# first let's do some basic checks to see if this is legible
# make sure the user's inbox even exists
if username:
try:
models.User.objects.get(localname=username)
@ -33,6 +33,11 @@ class Inbox(View):
except json.decoder.JSONDecodeError:
return HttpResponseBadRequest()
if not 'object' in activity_json or \
not 'type' in activity_json or \
not activity_json['type'] in activitypub.activity_objects:
return HttpResponseNotFound()
# verify the signature
if not has_valid_signature(request, activity_json):
if activity_json['type'] == 'Delete':
@ -42,12 +47,6 @@ class Inbox(View):
return HttpResponse()
return HttpResponse(status=401)
# just some quick smell tests before we try to parse the json
if not 'object' in activity_json or \
not 'type' in activity_json or \
not activity_json['type'] in activitypub.activity_objects:
return HttpResponseNotFound()
activity_task.delay(activity_json)
return HttpResponse()
@ -63,7 +62,11 @@ def activity_task(activity_json):
# cool that worked, now we should do the action described by the type
# (create, update, delete, etc)
activity.action()
try:
activity.action()
except activitypub.ActivitySerializerError:
# this is raised if the activity is discarded
return
def has_valid_signature(request, activity):

29
bookwyrm/views/isbn.py Normal file
View File

@ -0,0 +1,29 @@
''' isbn search view '''
from django.http import HttpResponseNotFound
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import forms, models
from bookwyrm.connectors import connector_manager
from .helpers import is_api_request
# pylint: disable= no-self-use
class Isbn(View):
''' search a book by isbn '''
def get(self, request, isbn):
''' info about a book '''
book_results = connector_manager.isbn_local_search(isbn)
if is_api_request(request):
return JsonResponse([r.json() for r in book_results], safe=False)
data = {
'title': 'ISBN Search Results',
'results': book_results,
'query': isbn,
}
return TemplateResponse(request, 'isbn_search_results.html', data)