diff --git a/README.md b/README.md index 2564d7af0..41bd70653 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Code contributions are gladly welcomed! If you're not sure where to start, take If you have questions about the project or contributing, you can set up a video call during BookWyrm ["office hours"](https://calendly.com/mouse-reeve/30min). ### Translation -Do you speak a language besides English? BookWyrm needs localization! If you're comfortable using git and want to get into the code, there are [instructions](#workin-with-translations-and-locale-files) on how to create and edit localization files. If you feel more comfortable working in a regular text editor and would prefer not to run the application, get in touch directly and we can figure out a system, like emailing a text file, that works best. +Do you speak a language besides English? BookWyrm needs localization! If you're comfortable using git and want to get into the code, there are [instructions](#working-with-translations-and-locale-files) on how to create and edit localization files. If you feel more comfortable working in a regular text editor and would prefer not to run the application, get in touch directly and we can figure out a system, like emailing a text file, that works best. ### Financial Support BookWyrm is an ad-free passion project with no intentions of seeking out venture funding or corporate financial relationships. If you want to help keep the project going, you can donate to the [Patreon](https://www.patreon.com/bookwyrm), or make a one time gift via [PayPal](https://paypal.me/oulipo). @@ -118,7 +118,7 @@ If you edit the CSS or JavaScript, you will need to run Django's `collectstatic` ./bw-dev collectstatic ``` -### Workin with translations and locale files +### Working with translations and locale files Text in the html files are wrapped in translation tags (`{% trans %}` and `{% blocktrans %}`), and Django generates locale files for all the strings in which you can add translations for the text. You can find existing translations in the `locale/` directory. The application's language is set by a request header sent by your browser to the application, so to change the language of the application, you can change the default language requested by your browser. @@ -132,7 +132,10 @@ To start translation into a language which is currently supported, run the djang #### Editing a locale When you have a locale file, open the `django.po` in the directory for the language (for example, if you were adding German, `locale/de/LC_MESSAGES/django.po`. All the the text in the application will be shown in paired strings, with `msgid` as the original text, and `msgstr` as the translation (by default, this is set to an empty string, and will display the original text). -Add you translations to the `msgstr` strings, and when you're ready, compile the locale by running: +Add your translations to the `msgstr` strings. As the messages in the application are updated, `gettext` will sometimes add best-guess fuzzy matched options for those translations. When a message is marked as fuzzy, it will not be used in the application, so be sure to remove it when you translate that line. + +When you're done, compile the locale by running: + ``` bash ./bw-dev compilemessages ``` diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index 87c40c90a..8c32be967 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -26,7 +26,7 @@ class Book(ActivityObject): librarythingKey: str = '' goodreadsKey: str = '' - cover: Image = field(default_factory=lambda: {}) + cover: Image = None type: str = 'Book' diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 68ff2a483..e6372438e 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -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 ''' diff --git a/bookwyrm/connectors/bookwyrm_connector.py b/bookwyrm/connectors/bookwyrm_connector.py index 00e6c62f1..96b72f267 100644 --- a/bookwyrm/connectors/bookwyrm_connector.py +++ b/bookwyrm/connectors/bookwyrm_connector.py @@ -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) + diff --git a/bookwyrm/connectors/connector_manager.py b/bookwyrm/connectors/connector_manager.py index a63a788eb..053e1f9ef 100644 --- a/bookwyrm/connectors/connector_manager.py +++ b/bookwyrm/connectors/connector_manager.py @@ -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(): diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index a767a45ac..8d227eef1 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -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) diff --git a/bookwyrm/connectors/self_connector.py b/bookwyrm/connectors/self_connector.py index f57fbc1cc..b3a4d6f9f 100644 --- a/bookwyrm/connectors/self_connector.py +++ b/bookwyrm/connectors/self_connector.py @@ -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 diff --git a/bookwyrm/management/commands/initdb.py b/bookwyrm/management/commands/initdb.py index 9fd117871..5759abfcc 100644 --- a/bookwyrm/management/commands/initdb.py +++ b/bookwyrm/management/commands/initdb.py @@ -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, ) diff --git a/bookwyrm/migrations/0047_connector_isbn_search_url.py b/bookwyrm/migrations/0047_connector_isbn_search_url.py new file mode 100644 index 000000000..617a89d9d --- /dev/null +++ b/bookwyrm/migrations/0047_connector_isbn_search_url.py @@ -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), + ), + ] diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index f1f208303..6a1a18b1e 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -91,7 +91,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 diff --git a/bookwyrm/models/connector.py b/bookwyrm/models/connector.py index 6f64cdf3e..c1fbf58bc 100644 --- a/bookwyrm/models/connector.py +++ b/bookwyrm/models/connector.py @@ -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) diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index f137236c0..bbeb10ccb 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -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) diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 499e72a76..45fdbd9da 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -141,6 +141,7 @@ LANGUAGE_CODE = 'en-us' LANGUAGES = [ ('en-us', _('English')), ('de-de', _('German')), + ('es', _('Spanish')), ('fr-fr', _('French')), ('zh-cn', _('Simplified Chinese')), ] diff --git a/bookwyrm/static/css/format.css b/bookwyrm/static/css/format.css index 9d4b3105f..435d8eb9e 100644 --- a/bookwyrm/static/css/format.css +++ b/bookwyrm/static/css/format.css @@ -1,3 +1,8 @@ +html { + scroll-behavior: smooth; + scroll-padding-top: 20%; +} + /* --- --- */ .image { overflow: hidden; diff --git a/bookwyrm/static/js/shared.js b/bookwyrm/static/js/shared.js index 7d65ee039..65f0dd65b 100644 --- a/bookwyrm/static/js/shared.js +++ b/bookwyrm/static/js/shared.js @@ -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) { diff --git a/bookwyrm/templates/book.html b/bookwyrm/templates/book.html index d80daca24..06578e894 100644 --- a/bookwyrm/templates/book.html +++ b/bookwyrm/templates/book.html @@ -35,7 +35,7 @@
-
+
{% 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 @@
-
+

{% include 'snippets/stars.html' with rating=rating %} @@ -201,7 +201,7 @@

-
+
{% if book.subjects %}

{% trans "Subjects" %}

@@ -217,7 +217,7 @@

{% trans "Places" %}

    - {% for place in book.subject_placess %} + {% for place in book.subject_places %}
  • {{ place }}
  • {% endfor %}
@@ -254,8 +254,8 @@
{% include 'snippets/username.html' with user=rating.user %}
-
-
{% trans "rated it" %}
+
+

{% trans "rated it" %}

{% include 'snippets/stars.html' with rating=rating.rating %}
diff --git a/bookwyrm/templates/components/dropdown.html b/bookwyrm/templates/components/dropdown.html index 1e45fe51a..72582ddc3 100644 --- a/bookwyrm/templates/components/dropdown.html +++ b/bookwyrm/templates/components/dropdown.html @@ -5,7 +5,7 @@ {% block dropdown-trigger %}{% endblock %} diff --git a/bookwyrm/templates/components/modal.html b/bookwyrm/templates/components/modal.html index 554f9ccde..9ffbc2b35 100644 --- a/bookwyrm/templates/components/modal.html +++ b/bookwyrm/templates/components/modal.html @@ -1,8 +1,15 @@ - diff --git a/bookwyrm/templates/discover/large-book.html b/bookwyrm/templates/discover/large-book.html index 7881a33ab..44b91b946 100644 --- a/bookwyrm/templates/discover/large-book.html +++ b/bookwyrm/templates/discover/large-book.html @@ -1,4 +1,5 @@ {% load bookwyrm_tags %} +{% load i18n %} {% if book %}
@@ -8,7 +9,7 @@

{{ book.title }}

{% if book.authors %} -

by {% include 'snippets/authors.html' with book=book %}

+

{% trans "by" %} {% include 'snippets/authors.html' with book=book %}

{% endif %} {% if book|book_description %}
{{ book|book_description|to_markdown|safe|truncatewords_html:50 }}
diff --git a/bookwyrm/templates/discover/small-book.html b/bookwyrm/templates/discover/small-book.html index 72108c309..6df277466 100644 --- a/bookwyrm/templates/discover/small-book.html +++ b/bookwyrm/templates/discover/small-book.html @@ -1,11 +1,12 @@ {% load bookwyrm_tags %} +{% load i18n %} {% if book %} {% include 'snippets/book_cover.html' with book=book %} {% include 'snippets/stars.html' with rating=book|rating:request.user %}

{{ book.title }}

{% if book.authors %} -

by {% include 'snippets/authors.html' with book=book %}

+

{% trans "by" %} {% include 'snippets/authors.html' with book=book %}

{% endif %} {% endif %} diff --git a/bookwyrm/templates/feed/feed.html b/bookwyrm/templates/feed/feed.html index 1eae24d4e..4eb363e4a 100644 --- a/bookwyrm/templates/feed/feed.html +++ b/bookwyrm/templates/feed/feed.html @@ -6,13 +6,13 @@

{% blocktrans %}{{ tab_title }} Timeline{% endblocktrans %}

diff --git a/bookwyrm/templates/feed/status.html b/bookwyrm/templates/feed/status.html index fc92f8556..9f90f355d 100644 --- a/bookwyrm/templates/feed/status.html +++ b/bookwyrm/templates/feed/status.html @@ -4,7 +4,7 @@ {% block panel %}
- + {% trans "Back" %}
diff --git a/bookwyrm/templates/import_status.html b/bookwyrm/templates/import_status.html index b28187b06..f9ba36bf4 100644 --- a/bookwyrm/templates/import_status.html +++ b/bookwyrm/templates/import_status.html @@ -5,7 +5,7 @@ {% block title %}{% trans "Import Status" %}{% endblock %} -{% block content %} +{% block content %}{% spaceless %}

{% trans "Import Status" %}

@@ -36,8 +36,19 @@ {% if not job.retry %}
{% csrf_token %} - -
-
- + + + + +
+ {% else %}
    {% for item in failed_items %} @@ -123,4 +147,4 @@
-{% endblock %} +{% endspaceless %}{% endblock %} diff --git a/bookwyrm/templates/isbn_search_results.html b/bookwyrm/templates/isbn_search_results.html new file mode 100644 index 000000000..a3861a68a --- /dev/null +++ b/bookwyrm/templates/isbn_search_results.html @@ -0,0 +1,33 @@ +{% extends 'layout.html' %} +{% load i18n %} + +{% block title %}{% trans "Search Results" %}{% endblock %} + +{% block content %} +{% with book_results|first as local_results %} +
+

{% blocktrans %}Search Results for "{{ query }}"{% endblocktrans %}

+
+ +
+
+

{% trans "Matching Books" %}

+
+ {% if not results %} +

{% blocktrans %}No books found for "{{ query }}"{% endblocktrans %}

+ {% else %} + + {% endif %} +
+ +
+
+
+{% endwith %} +{% endblock %} diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index bc2e7e090..8a708f633 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -22,7 +22,7 @@ -