mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-10-31 22:19:00 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
b53d45a19a
55 changed files with 4045 additions and 1640 deletions
|
@ -111,7 +111,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
return existing.default_edition
|
||||
return existing
|
||||
|
||||
# load the json
|
||||
# load the json data from the remote data source
|
||||
data = self.get_book_data(remote_id)
|
||||
if self.is_work_data(data):
|
||||
try:
|
||||
|
@ -150,27 +150,37 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
"""this allows connectors to override the default behavior"""
|
||||
return get_data(remote_id)
|
||||
|
||||
def create_edition_from_data(self, work, edition_data):
|
||||
def create_edition_from_data(self, work, edition_data, instance=None):
|
||||
"""if we already have the work, we're ready"""
|
||||
mapped_data = dict_from_mappings(edition_data, self.book_mappings)
|
||||
mapped_data["work"] = work.remote_id
|
||||
edition_activity = activitypub.Edition(**mapped_data)
|
||||
edition = edition_activity.to_model(model=models.Edition, overwrite=False)
|
||||
edition.connector = self.connector
|
||||
edition.save()
|
||||
edition = edition_activity.to_model(
|
||||
model=models.Edition, overwrite=False, instance=instance
|
||||
)
|
||||
|
||||
# if we're updating an existing instance, we don't need to load authors
|
||||
if instance:
|
||||
return edition
|
||||
|
||||
if not edition.connector:
|
||||
edition.connector = self.connector
|
||||
edition.save(broadcast=False, update_fields=["connector"])
|
||||
|
||||
for author in self.get_authors_from_data(edition_data):
|
||||
edition.authors.add(author)
|
||||
# use the authors from the work if none are found for the edition
|
||||
if not edition.authors.exists() and work.authors.exists():
|
||||
edition.authors.set(work.authors.all())
|
||||
|
||||
return edition
|
||||
|
||||
def get_or_create_author(self, remote_id):
|
||||
def get_or_create_author(self, remote_id, instance=None):
|
||||
"""load that author"""
|
||||
existing = models.Author.find_existing_by_remote_id(remote_id)
|
||||
if existing:
|
||||
return existing
|
||||
if not instance:
|
||||
existing = models.Author.find_existing_by_remote_id(remote_id)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
data = self.get_book_data(remote_id)
|
||||
|
||||
|
@ -181,7 +191,24 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
return None
|
||||
|
||||
# this will dedupe
|
||||
return activity.to_model(model=models.Author, overwrite=False)
|
||||
return activity.to_model(
|
||||
model=models.Author, overwrite=False, instance=instance
|
||||
)
|
||||
|
||||
def get_remote_id_from_model(self, obj):
|
||||
"""given the data stored, how can we look this up"""
|
||||
return getattr(obj, getattr(self, "generated_remote_link_field"))
|
||||
|
||||
def update_author_from_remote(self, obj):
|
||||
"""load the remote data from this connector and add it to an existing author"""
|
||||
remote_id = self.get_remote_id_from_model(obj)
|
||||
return self.get_or_create_author(remote_id, instance=obj)
|
||||
|
||||
def update_book_from_remote(self, obj):
|
||||
"""load the remote data from this connector and add it to an existing book"""
|
||||
remote_id = self.get_remote_id_from_model(obj)
|
||||
data = self.get_book_data(remote_id)
|
||||
return self.create_edition_from_data(obj.parent_work, data, instance=obj)
|
||||
|
||||
@abstractmethod
|
||||
def is_work_data(self, data):
|
||||
|
|
|
@ -11,6 +11,8 @@ from .connector_manager import ConnectorException
|
|||
class Connector(AbstractConnector):
|
||||
"""instantiate a connector for inventaire"""
|
||||
|
||||
generated_remote_link_field = "inventaire_id"
|
||||
|
||||
def __init__(self, identifier):
|
||||
super().__init__(identifier)
|
||||
|
||||
|
@ -210,6 +212,11 @@ class Connector(AbstractConnector):
|
|||
return ""
|
||||
return data.get("extract")
|
||||
|
||||
def get_remote_id_from_model(self, obj):
|
||||
"""use get_remote_id to figure out the link from a model obj"""
|
||||
remote_id_value = obj.inventaire_id
|
||||
return self.get_remote_id(remote_id_value)
|
||||
|
||||
|
||||
def get_language_code(options, code="en"):
|
||||
"""when there are a bunch of translation but we need a single field"""
|
||||
|
|
|
@ -12,6 +12,8 @@ from .openlibrary_languages import languages
|
|||
class Connector(AbstractConnector):
|
||||
"""instantiate a connector for OL"""
|
||||
|
||||
generated_remote_link_field = "openlibrary_link"
|
||||
|
||||
def __init__(self, identifier):
|
||||
super().__init__(identifier)
|
||||
|
||||
|
@ -66,6 +68,7 @@ class Connector(AbstractConnector):
|
|||
Mapping("born", remote_field="birth_date"),
|
||||
Mapping("died", remote_field="death_date"),
|
||||
Mapping("bio", formatter=get_description),
|
||||
Mapping("isni", remote_field="remote_ids", formatter=get_isni),
|
||||
]
|
||||
|
||||
def get_book_data(self, remote_id):
|
||||
|
@ -224,6 +227,13 @@ def get_languages(language_blob):
|
|||
return langs
|
||||
|
||||
|
||||
def get_isni(remote_ids_blob):
|
||||
"""extract the isni from the remote id data for the author"""
|
||||
if not remote_ids_blob or not isinstance(remote_ids_blob, dict):
|
||||
return None
|
||||
return remote_ids_blob.get("isni")
|
||||
|
||||
|
||||
def pick_default_edition(options):
|
||||
"""favor physical copies with covers in english"""
|
||||
if not options:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" database schema for info about authors """
|
||||
import re
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.db import models
|
||||
|
||||
|
@ -33,6 +34,17 @@ class Author(BookDataModel):
|
|||
)
|
||||
bio = fields.HtmlField(null=True, blank=True)
|
||||
|
||||
@property
|
||||
def isni_link(self):
|
||||
"""generate the url from the isni id"""
|
||||
clean_isni = re.sub(r"\s", "", self.isni)
|
||||
return f"https://isni.org/isni/{clean_isni}"
|
||||
|
||||
@property
|
||||
def openlibrary_link(self):
|
||||
"""generate the url from the openlibrary id"""
|
||||
return f"https://openlibrary.org/authors/{self.openlibrary_key}"
|
||||
|
||||
def get_remote_id(self):
|
||||
"""editions and works both use "book" instead of model_name"""
|
||||
return f"https://{DOMAIN}/author/{self.id}"
|
||||
|
|
|
@ -52,6 +52,16 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
|
|||
null=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def openlibrary_link(self):
|
||||
"""generate the url from the openlibrary id"""
|
||||
return f"https://openlibrary.org/books/{self.openlibrary_key}"
|
||||
|
||||
@property
|
||||
def inventaire_link(self):
|
||||
"""generate the url from the inventaire id"""
|
||||
return f"https://inventaire.io/entity/{self.inventaire_id}"
|
||||
|
||||
class Meta:
|
||||
"""can't initialize this model, that wouldn't make sense"""
|
||||
|
||||
|
|
Binary file not shown.
|
@ -46,4 +46,5 @@
|
|||
<glyph unicode="" glyph-name="star-full" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538z" />
|
||||
<glyph unicode="" glyph-name="heart" d="M755.188 896c-107.63 0-200.258-87.554-243.164-179-42.938 91.444-135.578 179-243.216 179-148.382 0-268.808-120.44-268.808-268.832 0-301.846 304.5-380.994 512.022-679.418 196.154 296.576 511.978 387.206 511.978 679.418 0 148.392-120.43 268.832-268.812 268.832z" />
|
||||
<glyph unicode="" glyph-name="plus" d="M992 576h-352v352c0 17.672-14.328 32-32 32h-192c-17.672 0-32-14.328-32-32v-352h-352c-17.672 0-32-14.328-32-32v-192c0-17.672 14.328-32 32-32h352v-352c0-17.672 14.328-32 32-32h192c17.672 0 32 14.328 32 32v352h352c17.672 0 32 14.328 32 32v192c0 17.672-14.328 32-32 32z" />
|
||||
<glyph unicode="" glyph-name="download" d="M512-32l480 480h-288v512h-384v-512h-288z" />
|
||||
</font></defs></svg>
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Binary file not shown.
Binary file not shown.
13
bookwyrm/static/css/vendor/icons.css
vendored
13
bookwyrm/static/css/vendor/icons.css
vendored
|
@ -1,10 +1,10 @@
|
|||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src: url('../fonts/icomoon.eot?36x4a3');
|
||||
src: url('../fonts/icomoon.eot?36x4a3#iefix') format('embedded-opentype'),
|
||||
url('../fonts/icomoon.ttf?36x4a3') format('truetype'),
|
||||
url('../fonts/icomoon.woff?36x4a3') format('woff'),
|
||||
url('../fonts/icomoon.svg?36x4a3#icomoon') format('svg');
|
||||
src: url('../fonts/icomoon.eot?r7jc98');
|
||||
src: url('../fonts/icomoon.eot?r7jc98#iefix') format('embedded-opentype'),
|
||||
url('../fonts/icomoon.ttf?r7jc98') format('truetype'),
|
||||
url('../fonts/icomoon.woff?r7jc98') format('woff'),
|
||||
url('../fonts/icomoon.svg?r7jc98#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
|
@ -142,3 +142,6 @@
|
|||
.icon-spinner:before {
|
||||
content: "\e97a";
|
||||
}
|
||||
.icon-download:before {
|
||||
content: "\ea36";
|
||||
}
|
||||
|
|
|
@ -411,6 +411,21 @@ let BookWyrm = new class {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display pop up window.
|
||||
*
|
||||
* @param {string} url Url to open
|
||||
* @param {string} windowName windowName
|
||||
* @return {undefined}
|
||||
*/
|
||||
displayPopUp(url, windowName) {
|
||||
window.open(
|
||||
url,
|
||||
windowName,
|
||||
"left=100,top=100,width=430,height=600"
|
||||
);
|
||||
}
|
||||
|
||||
duplicateInput (event ) {
|
||||
const trigger = event.currentTarget;
|
||||
const input_id = trigger.dataset['duplicate']
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
<div class="column is-narrow">
|
||||
<a href="{{ author.local_path }}/edit">
|
||||
<a href="{% url 'edit-author' author.id %}">
|
||||
<span class="icon icon-pencil" title="{% trans 'Edit Author' %}" aria-hidden="True"></span>
|
||||
<span class="is-hidden-mobile">{% trans "Edit Author" %}</span>
|
||||
</a>
|
||||
|
@ -23,102 +23,130 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block columns content" itemscope itemtype="https://schema.org/Person">
|
||||
<div class="block columns is-flex-direction-row-reverse" itemscope itemtype="https://schema.org/Person">
|
||||
<meta itemprop="name" content="{{ author.name }}">
|
||||
{% if author.bio %}
|
||||
<div class="column">
|
||||
{% include "snippets/trimmed_text.html" with full=author.bio trim_length=200 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.aliases or author.born or author.died or author.wikipedia_link or author.openlibrary_key or author.inventaire_id or author.isni %}
|
||||
{% firstof author.aliases author.born author.died as details %}
|
||||
{% firstof author.wikipedia_link author.openlibrary_key author.inventaire_id author.isni as links %}
|
||||
{% if details or links %}
|
||||
<div class="column is-two-fifths">
|
||||
<div class="box py-2">
|
||||
<dl>
|
||||
{% if details %}
|
||||
<section class="block content">
|
||||
<h2 class="title is-4">{% trans "Author details" %}</h2>
|
||||
<dl class="box">
|
||||
{% if author.aliases %}
|
||||
<div class="is-flex is-flex-wrap-wrap my-1">
|
||||
<div class="is-flex is-flex-wrap-wrap mr-1">
|
||||
<dt class="has-text-weight-bold mr-1">{% trans "Aliases:" %}</dt>
|
||||
{% for alias in author.aliases %}
|
||||
<dd itemprop="alternateName" content="{{alias}}">
|
||||
{{alias}}{% if not forloop.last %}, {% endif %}
|
||||
</dd>
|
||||
{% endfor %}
|
||||
<dd>
|
||||
{% include "snippets/trimmed_list.html" with items=author.aliases itemprop="alternateName" %}
|
||||
</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.born %}
|
||||
<div class="is-flex my-1">
|
||||
<div class="is-flex mt-1">
|
||||
<dt class="has-text-weight-bold mr-1">{% trans "Born:" %}</dt>
|
||||
<dd itemprop="birthDate">{{ author.born|naturalday }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.died %}
|
||||
<div class="is-flex my-1">
|
||||
<div class="is-flex mt-1">
|
||||
<dt class="has-text-weight-bold mr-1">{% trans "Died:" %}</dt>
|
||||
<dd itemprop="deathDate">{{ author.died|naturalday }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if author.wikipedia_link %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener" target="_blank">
|
||||
{% trans "Wikipedia" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if links %}
|
||||
<section>
|
||||
<h2 class="title is-4">{% trans "External links" %}</h2>
|
||||
<div class="box">
|
||||
{% if author.wikipedia_link %}
|
||||
<div>
|
||||
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener" target="_blank">
|
||||
{% trans "Wikipedia" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.isni %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://isni.org/isni/{{ author.isni|remove_spaces }}" rel="noopener" target="_blank">
|
||||
{% trans "View ISNI record" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if author.isni %}
|
||||
<div class="mt-1">
|
||||
<a itemprop="sameAs" href="{{ author.isni_link }}" rel="noopener" target="_blank">
|
||||
{% trans "View ISNI record" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.openlibrary_key %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://openlibrary.org/authors/{{ author.openlibrary_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on OpenLibrary" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% trans "Load data" as button_text %}
|
||||
{% if author.openlibrary_key %}
|
||||
<div class="mt-1 is-flex">
|
||||
<a class="mr-3" itemprop="sameAs" href="{{ author.openlibrary_link }}" target="_blank" rel="noopener">
|
||||
{% trans "View on OpenLibrary" %}
|
||||
</a>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
{% with controls_text="ol_sync" controls_uid=author.id %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_ol_sync" class="is-small" icon_with_text="download" %}
|
||||
{% include "author/sync_modal.html" with source="openlibrary.org" source_name="OpenLibrary" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.inventaire_id %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://inventaire.io/entity/{{ author.inventaire_id }}" target="_blank" rel="noopener">
|
||||
{% trans "View on Inventaire" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if author.inventaire_id %}
|
||||
<div class="mt-1 is-flex">
|
||||
<a class="mr-3" itemprop="sameAs" href="{{ author.inventaire_link }}" target="_blank" rel="noopener">
|
||||
{% trans "View on Inventaire" %}
|
||||
</a>
|
||||
|
||||
{% if author.librarything_key %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on LibraryThing" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
{% with controls_text="iv_sync" controls_uid=author.id %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_iv_sync" class="is-small" icon_with_text="download" %}
|
||||
{% include "author/sync_modal.html" with source="inventaire.io" source_name="Inventaire" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.goodreads_key %}
|
||||
<p class="my-1">
|
||||
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on Goodreads" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="column">
|
||||
{% if author.bio %}
|
||||
{{ author.bio|to_markdown|safe }}
|
||||
{% if author.librarything_key %}
|
||||
<div class="mt-1">
|
||||
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on LibraryThing" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if author.goodreads_key %}
|
||||
<div>
|
||||
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener">
|
||||
{% trans "View on Goodreads" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr aria-hidden="true">
|
||||
|
||||
<div class="block">
|
||||
<h3 class="title is-4">{% blocktrans with name=author.name %}Books by {{ name }}{% endblocktrans %}</h3>
|
||||
<h2 class="title is-4">{% blocktrans with name=author.name %}Books by {{ name }}{% endblocktrans %}</h2>
|
||||
<div class="columns is-multiline is-mobile">
|
||||
{% for book in books %}
|
||||
<div class="column is-one-fifth">
|
||||
{% include 'landing/small-book.html' with book=book %}
|
||||
<div class="column is-one-fifth-tablet is-half-mobile is-flex is-flex-direction-column">
|
||||
<div class="is-flex-grow-1">
|
||||
{% include 'landing/small-book.html' with book=book %}
|
||||
</div>
|
||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
30
bookwyrm/templates/author/sync_modal.html
Normal file
30
bookwyrm/templates/author/sync_modal.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% extends 'components/modal.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-title %}
|
||||
{% trans "Load data" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-open %}
|
||||
<form name="{{ source }}-update" method="POST" action="{% url 'author-update-remote' author.id source %}">
|
||||
{% csrf_token %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Loading data will connect to <strong>{{ source_name }}</strong> and check for any metadata about this author which aren't present here. Existing metadata will not be overwritten.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button is-primary" type="submit">
|
||||
<span>{% trans "Confirm" %}</span>
|
||||
</button>
|
||||
|
||||
{% trans "Cancel" as button_text %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
|
@ -90,11 +90,28 @@
|
|||
</div>
|
||||
{% endwith %}
|
||||
|
||||
{% trans "Load data" as button_text %}
|
||||
{% if book.openlibrary_key %}
|
||||
<p><a href="https://openlibrary.org/books/{{ book.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a></p>
|
||||
<p>
|
||||
<a href="{{ book.openlibrary_link }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
{% with controls_text="ol_sync" controls_uid=book.id %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_ol_sync" class="is-small" icon_with_text="download" %}
|
||||
{% include "book/sync_modal.html" with source="openlibrary.org" source_name="OpenLibrary" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if book.inventaire_id %}
|
||||
<p><a href="https://inventaire.io/entity/{{ book.inventaire_id }}" target="_blank" rel="noopener">{% trans "View on Inventaire" %}</a></p>
|
||||
<p>
|
||||
<a href="{{ book.inventaire_link }}" target="_blank" rel="noopener">{% trans "View on Inventaire" %}</a>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
{% with controls_text="iv_sync" controls_uid=book.id %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_iv_sync" class="is-small" icon_with_text="download" %}
|
||||
{% include "book/sync_modal.html" with source="inventaire.io" source_name="Inventaire" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
|
30
bookwyrm/templates/book/sync_modal.html
Normal file
30
bookwyrm/templates/book/sync_modal.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% extends 'components/modal.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-title %}
|
||||
{% trans "Load data" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-open %}
|
||||
<form name="{{ source }}-update" method="POST" action="{% url 'book-update-remote' book.id source %}">
|
||||
{% csrf_token %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Loading data will connect to <strong>{{ source_name }}</strong> and check for any metadata about this book which aren't present here. Existing metadata will not be overwritten.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button is-primary" type="submit">
|
||||
<span>{% trans "Confirm" %}</span>
|
||||
</button>
|
||||
|
||||
{% trans "Cancel" as button_text %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
|
@ -267,5 +267,6 @@
|
|||
<script src="{% static "js/status_cache.js" %}?v={{ js_cache }}"></script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
70
bookwyrm/templates/ostatus/error.html
Normal file
70
bookwyrm/templates/ostatus/error.html
Normal file
|
@ -0,0 +1,70 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block">
|
||||
{% if error == 'invalid_username' %}
|
||||
<div class="notification is-warning has-text-centered" role="status">
|
||||
<p>{% blocktrans %}<strong>{{ account }}</strong> is not a valid username{% endblocktrans %}.</p>
|
||||
<p>{% trans 'Check you have the correct username before trying again' %}.</p>
|
||||
</div>
|
||||
{% elif error == 'user_not_found' %}
|
||||
<div class="notification is-warning has-text-centered" role="status">
|
||||
<p>{% blocktrans %}<strong>{{ account }}</strong> could not be found or <code>{{ remote_domain }}</code> does not support identity discovery{% endblocktrans %}.</p>
|
||||
<p>{% trans 'Check you have the correct username before trying again' %}.</p>
|
||||
</div>
|
||||
{% elif error == 'not_supported' %}
|
||||
<div class="notification is-warning has-text-centered" role="status">
|
||||
<p>{% blocktrans %}<strong>{{ account }}</strong> was found but <code>{{ remote_domain }}</code> does not support 'remote follow'{% endblocktrans %}.</p>
|
||||
<p>{% blocktrans %}Try searching for <strong>{{ user }}</strong> on <code>{{ remote_domain }}</code> instead{% endblocktrans %}.</p>
|
||||
</div>
|
||||
{% elif not request.user.is_authenticated %}
|
||||
<div class="navbar-item">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<form name="login" method="post" action="{% url 'login' %}?next={{ request.path }}?acct={{ user.remote_id }}">
|
||||
{% csrf_token %}
|
||||
<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="{% trans 'username' %}">
|
||||
</div>
|
||||
<div class="column">
|
||||
<label class="is-sr-only" for="id_password">{% trans "Password:" %}</label>
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password" placeholder="{% trans 'password' %}">
|
||||
<p class="help"><a href="{% url '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>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif error == 'ostatus_subscribe' %}
|
||||
<div class="notification is-warning has-text-centered" role="status">
|
||||
<p>{% blocktrans %}Something went wrong trying to follow <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||
<p>{% trans 'Check you have the correct username before trying again.' %}</p>
|
||||
</div>
|
||||
{% elif error == 'is_blocked' %}
|
||||
<div class="notification is-danger has-text-centered" role="status">
|
||||
<p>{% blocktrans %}You have blocked <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% elif error == 'has_blocked' %}
|
||||
<div class="notification is-danger has-text-centered" role="status">
|
||||
<p>{% blocktrans %}<strong>{{ account }}</strong> has blocked you{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% elif error == 'already_following' %}
|
||||
<div class="notification is-success has-text-centered" role="status">
|
||||
<p>{% blocktrans %}You are already following <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% elif error == 'already_requested' %}
|
||||
<div class="notification is-success has-text-centered" role="status">
|
||||
<p>{% blocktrans %}You have already requested to follow <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="block is-pulled-right">
|
||||
<button type="button" class="button" onclick="closeWindow()">Close window</button>
|
||||
</div>
|
||||
{% endblock %}
|
46
bookwyrm/templates/ostatus/remote_follow.html
Normal file
46
bookwyrm/templates/ostatus/remote_follow.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
{% extends 'ostatus/template.html' %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block heading %}
|
||||
{% blocktrans with username=user.localname sitename=site.name %}Follow {{ username }} on the fediverse{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block card">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<a href="{{ user.local_path }}" class="media-left">
|
||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||
</a>
|
||||
<div class="media-content">
|
||||
<a href="{{ user.local_path }}" class="is-block mb-2">
|
||||
<span class="title is-4 is-block">
|
||||
{{ user.display_name }}
|
||||
{% if user.manually_approves_followers %}
|
||||
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
|
||||
<span class="is-sr-only">{% trans "Locked account" %}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<p>{% blocktrans with username=user.display_name %}Follow {{ username }} from another Fediverse account like BookWyrm, Mastodon, or Pleroma.{% endblocktrans %}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<section class="card-content content">
|
||||
<form name="remote-follow" method="post" action="{% url 'remote-follow' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="user" value="{{ user.id }}">
|
||||
<label class="label" for="remote_user">{% trans 'User handle to follow from:' %}</label>
|
||||
<input class="input" type="text" name="remote_user" id="remote_user" placeholder="user@example.social" required>
|
||||
<button class="button mt-1 is-primary" type="submit">{% trans 'Follow!' %}</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
15
bookwyrm/templates/ostatus/remote_follow_button.html
Normal file
15
bookwyrm/templates/ostatus/remote_follow_button.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% load i18n %}
|
||||
{% if request.user == user %}
|
||||
{% else %}
|
||||
|
||||
<div class="field mb-0">
|
||||
<div class="control">
|
||||
<a class="button is-small is-link" href="{% url 'remote-follow-page' %}?user={{ user.username }}" target="_blank" rel="noopener noreferrer" onclick="BookWyrm.displayPopUp(`{% url 'remote-follow-page' %}?user={{ user.username }}`, `remoteFollow`); return false;" aria-describedby="remote_follow_warning">
|
||||
{% blocktrans with username=user.localname %}Follow on Fediverse{% endblocktrans %}
|
||||
</a>
|
||||
</div>
|
||||
<p id="remote_follow_warning" class="mt-1 is-size-7 has-text-weight-light">
|
||||
{% trans 'This link opens in a pop-up window' %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
63
bookwyrm/templates/ostatus/subscribe.html
Normal file
63
bookwyrm/templates/ostatus/subscribe.html
Normal file
|
@ -0,0 +1,63 @@
|
|||
{% extends 'ostatus/template.html' %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
{% load markdown %}
|
||||
|
||||
{% block title %}
|
||||
{% if not request.user.is_authenticated %}
|
||||
{% blocktrans with sitename=site.name %}Log in to {{ sitename }}{% endblocktrans %}
|
||||
{% elif error %}
|
||||
{% blocktrans with sitename=site.name %}Error following from {{ sitename }}{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans with sitename=site.name %}Follow from {{ sitename }}{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% if error %}
|
||||
{% trans 'Uh oh...' %}
|
||||
{% elif not request.user.is_authenticated %}
|
||||
{% trans "Let's log in first..." %}
|
||||
{% else %}
|
||||
{% blocktrans with sitename=site.name %}Follow from {{ sitename }}{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if error or not request.user.is_authenticated %}
|
||||
{% include 'ostatus/error.html' with error=error user=user account=account %}
|
||||
{% else %}
|
||||
<div class="block card">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<a href="{{ user.local_path }}" class="media-left">
|
||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||
</a>
|
||||
<div class="media-content">
|
||||
<a href="{{ user.local_path }}" class="is-block mb-2">
|
||||
<span class="title is-4 is-block">
|
||||
{{ user.display_name }}
|
||||
{% if user.manually_approves_followers %}
|
||||
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
|
||||
<span class="is-sr-only">{% trans 'Locked account' %}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
||||
</a>
|
||||
<form name="follow" method="post" action="{% url 'follow' %}/?next={% url 'ostatus-success' %}?following={{ user.username }}">
|
||||
{% csrf_token %}
|
||||
<input name="user" value="{{ user.username }}" hidden>
|
||||
<button class="button is-link" type="submit">{% blocktrans with username=user.display_name %}Follow {{ username }}{% endblocktrans %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{% if user.summary %}
|
||||
{{ user.summary|to_markdown|safe|truncatechars_html:120 }}
|
||||
{% else %} {% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
35
bookwyrm/templates/ostatus/success.html
Normal file
35
bookwyrm/templates/ostatus/success.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% extends 'ostatus/template.html' %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block card">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<a href="{{ user.local_path }}" class="media-left">
|
||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||
</a>
|
||||
<div class="media-content">
|
||||
<a href="{{ user.local_path }}" class="is-block mb-2">
|
||||
<span class="title is-4 is-block">
|
||||
{{ user.display_name }}
|
||||
{% if user.manually_approves_followers %}
|
||||
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
|
||||
<span class="is-sr-only">{% trans "Locked account" %}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="notification is-success">
|
||||
<span class="icon icon-check m-0-mobile" aria-hidden="true"></span>
|
||||
<span>{% blocktrans with display_name=user.display_name %}You are now following {{ display_name }}!{% endblocktrans %}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block is-pulled-right">
|
||||
<button type="button" class="button" onclick="closeWindow()">Close window</button>
|
||||
</div>
|
||||
{% endblock %}
|
41
bookwyrm/templates/ostatus/template.html
Normal file
41
bookwyrm/templates/ostatus/template.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
{% load layout %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load utilities %}
|
||||
{% load markdown %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="{% get_lang %}">
|
||||
<head>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="{% static 'css/vendor/bulma.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/vendor/icons.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bookwyrm.css' %}">
|
||||
<script>
|
||||
function closeWindow() {
|
||||
window.close();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<img class="image logo navbar-item" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static 'images/logo-small.png' %}{% endif %}" alt="Home page">
|
||||
<h2 class="navbar-item subtitle">{% block heading %}{% endblock %}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="section is-flex-grow-1 columns is-centered">
|
||||
<div class="block column is-one-third">
|
||||
{% block content%}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var csrf_token = '{{ csrf_token }}';
|
||||
</script>
|
||||
<script src="{% static 'js/bookwyrm.js' %}?v={{ js_cache }}"></script>
|
||||
</body>
|
||||
</html>
|
34
bookwyrm/templates/snippets/trimmed_list.html
Normal file
34
bookwyrm/templates/snippets/trimmed_list.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
{% spaceless %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% firstof limit 3 as limit %}
|
||||
{% with subtraction_value='-'|add:limit %}
|
||||
{% with remainder_count=items|length|add:subtraction_value %}
|
||||
{% with remainder_count_display=remainder_count|intcomma %}
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
{% for item in items|slice:limit %}
|
||||
<span
|
||||
{% if itemprop %}itemprop="{{ itemprop }}"{% endif %}
|
||||
>{{ item }}</span>{% if not forloop.last %}, {% elif remainder_count > 0 %}, {% blocktrans trimmed count counter=remainder_count %}
|
||||
and {{ remainder_count_display }} other
|
||||
{% plural %}
|
||||
and {{ remainder_count_display }} others
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</summary>
|
||||
|
||||
{% for item in items|slice:"3:" %}
|
||||
<span
|
||||
{% if itemprop %}itemprop="{{ itemprop }}"{% endif %}
|
||||
>{{ item }}</span>{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</details>
|
||||
|
||||
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endspaceless %}
|
|
@ -39,6 +39,9 @@
|
|||
{% if not is_self and request.user.is_authenticated %}
|
||||
{% include 'snippets/follow_button.html' with user=user %}
|
||||
{% endif %}
|
||||
{% if not is_self %}
|
||||
{% include 'ostatus/remote_follow_button.html' with user=user %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_self and user.follower_requests.all %}
|
||||
<div class="follow-requests">
|
||||
|
|
|
@ -5,7 +5,6 @@ from uuid import uuid4
|
|||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.template.defaultfilters import stringfilter
|
||||
from django.templatetags.static import static
|
||||
|
||||
|
||||
|
@ -98,10 +97,3 @@ def get_isni(existing, author, autoescape=True):
|
|||
f'<input type="text" name="isni-for-{author.id}" value="{isni}" hidden>'
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
@register.filter(name="remove_spaces")
|
||||
@stringfilter
|
||||
def remove_spaces(arg):
|
||||
"""Removes spaces from argument passed in"""
|
||||
return re.sub(r"\s", "", str(arg))
|
||||
|
|
|
@ -40,6 +40,8 @@ class AbstractConnector(TestCase):
|
|||
class TestConnector(abstract_connector.AbstractConnector):
|
||||
"""nothing added here"""
|
||||
|
||||
generated_remote_link_field = "openlibrary_link"
|
||||
|
||||
def format_search_result(self, search_result):
|
||||
return search_result
|
||||
|
||||
|
@ -87,9 +89,7 @@ class AbstractConnector(TestCase):
|
|||
def test_get_or_create_book_existing(self):
|
||||
"""find an existing book by remote/origin id"""
|
||||
self.assertEqual(models.Book.objects.count(), 1)
|
||||
self.assertEqual(
|
||||
self.book.remote_id, "https://%s/book/%d" % (DOMAIN, self.book.id)
|
||||
)
|
||||
self.assertEqual(self.book.remote_id, f"https://{DOMAIN}/book/{self.book.id}")
|
||||
self.assertEqual(self.book.origin_id, "https://example.com/book/1234")
|
||||
|
||||
# dedupe by origin id
|
||||
|
@ -99,7 +99,7 @@ class AbstractConnector(TestCase):
|
|||
|
||||
# dedupe by remote id
|
||||
result = self.connector.get_or_create_book(
|
||||
"https://%s/book/%d" % (DOMAIN, self.book.id)
|
||||
f"https://{DOMAIN}/book/{self.book.id}"
|
||||
)
|
||||
self.assertEqual(models.Book.objects.count(), 1)
|
||||
self.assertEqual(result, self.book)
|
||||
|
@ -119,7 +119,8 @@ class AbstractConnector(TestCase):
|
|||
@responses.activate
|
||||
def test_get_or_create_author(self):
|
||||
"""load an author"""
|
||||
self.connector.author_mappings = [ # pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.connector.author_mappings = [
|
||||
Mapping("id"),
|
||||
Mapping("name"),
|
||||
]
|
||||
|
@ -139,3 +140,26 @@ class AbstractConnector(TestCase):
|
|||
author = models.Author.objects.create(name="Test Author")
|
||||
result = self.connector.get_or_create_author(author.remote_id)
|
||||
self.assertEqual(author, result)
|
||||
|
||||
@responses.activate
|
||||
def test_update_author_from_remote(self):
|
||||
"""trigger the function that looks up the remote data"""
|
||||
author = models.Author.objects.create(name="Test", openlibrary_key="OL123A")
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.connector.author_mappings = [
|
||||
Mapping("id"),
|
||||
Mapping("name"),
|
||||
Mapping("isni"),
|
||||
]
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://openlibrary.org/authors/OL123A",
|
||||
json={"id": "https://www.example.com/author", "name": "Beep", "isni": "hi"},
|
||||
)
|
||||
|
||||
self.connector.update_author_from_remote(author)
|
||||
|
||||
author.refresh_from_db()
|
||||
self.assertEqual(author.name, "Test")
|
||||
self.assertEqual(author.isni, "hi")
|
||||
|
|
|
@ -306,3 +306,11 @@ class Inventaire(TestCase):
|
|||
|
||||
extract = self.connector.get_description({"enwiki": "test_path"})
|
||||
self.assertEqual(extract, "hi hi")
|
||||
|
||||
def test_remote_id_from_model(self):
|
||||
"""figure out a url from an id"""
|
||||
obj = models.Author.objects.create(name="hello", inventaire_id="123")
|
||||
self.assertEqual(
|
||||
self.connector.get_remote_id_from_model(obj),
|
||||
"https://inventaire.io?action=by-uris&uris=123",
|
||||
)
|
||||
|
|
|
@ -98,6 +98,9 @@ class Openlibrary(TestCase):
|
|||
"type": "/type/datetime",
|
||||
"value": "2008-08-31 10:09:33.413686",
|
||||
},
|
||||
"remote_ids": {
|
||||
"isni": "000111",
|
||||
},
|
||||
"key": "/authors/OL453734A",
|
||||
"type": {"key": "/type/author"},
|
||||
"id": 1259965,
|
||||
|
@ -110,6 +113,7 @@ class Openlibrary(TestCase):
|
|||
self.assertIsInstance(result, models.Author)
|
||||
self.assertEqual(result.name, "George Elliott")
|
||||
self.assertEqual(result.openlibrary_key, "OL453734A")
|
||||
self.assertEqual(result.isni, "000111")
|
||||
|
||||
def test_get_cover_url(self):
|
||||
"""formats a url that should contain the cover image"""
|
||||
|
|
|
@ -209,6 +209,28 @@ class BookViews(TestCase):
|
|||
self.assertEqual(self.book.description, "new description hi")
|
||||
self.assertEqual(self.book.last_edited_by, self.local_user)
|
||||
|
||||
def test_update_book_from_remote(self):
|
||||
"""call out to sync with remote connector"""
|
||||
models.Connector.objects.create(
|
||||
identifier="openlibrary.org",
|
||||
name="OpenLibrary",
|
||||
connector_file="openlibrary",
|
||||
base_url="https://openlibrary.org",
|
||||
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.local_user.groups.add(self.group)
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
|
||||
with patch(
|
||||
"bookwyrm.connectors.openlibrary.Connector.update_book_from_remote"
|
||||
) as mock:
|
||||
views.update_book_from_remote(request, self.book.id, "openlibrary.org")
|
||||
self.assertEqual(mock.call_count, 1)
|
||||
|
||||
|
||||
def _setup_cover_url():
|
||||
"""creates cover url mock"""
|
||||
|
|
|
@ -148,3 +148,26 @@ class AuthorViews(TestCase):
|
|||
self.assertEqual(author.name, "Test Author")
|
||||
validate_html(resp.render())
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_update_author_from_remote(self):
|
||||
"""call out to sync with remote connector"""
|
||||
author = models.Author.objects.create(name="Test Author")
|
||||
models.Connector.objects.create(
|
||||
identifier="openlibrary.org",
|
||||
name="OpenLibrary",
|
||||
connector_file="openlibrary",
|
||||
base_url="https://openlibrary.org",
|
||||
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.local_user.groups.add(self.group)
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
|
||||
with patch(
|
||||
"bookwyrm.connectors.openlibrary.Connector.update_author_from_remote"
|
||||
) as mock:
|
||||
views.update_author_from_remote(request, author.id, "openlibrary.org")
|
||||
self.assertEqual(mock.call_count, 1)
|
||||
|
|
|
@ -4,10 +4,12 @@ from unittest.mock import patch
|
|||
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
@patch("bookwyrm.activitystreams.add_user_statuses_task.delay")
|
||||
|
@ -16,6 +18,7 @@ class FollowViews(TestCase):
|
|||
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
models.SiteSettings.objects.create()
|
||||
self.factory = RequestFactory()
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
|
@ -174,3 +177,43 @@ class FollowViews(TestCase):
|
|||
self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0)
|
||||
# follow relationship should not exist
|
||||
self.assertEqual(models.UserFollows.objects.filter(id=rel.id).count(), 0)
|
||||
|
||||
def test_ostatus_follow_request(self, _):
|
||||
"""check ostatus subscribe template loads"""
|
||||
request = self.factory.get(
|
||||
"", {"acct": "https%3A%2F%2Fexample.com%2Fusers%2Frat"}
|
||||
)
|
||||
request.user = self.local_user
|
||||
result = views.ostatus_follow_request(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_remote_follow_page(self, _):
|
||||
"""check remote follow page loads"""
|
||||
request = self.factory.get("", {"acct": "mouse@local.com"})
|
||||
request.user = self.remote_user
|
||||
result = views.remote_follow_page(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_ostatus_follow_success(self, _):
|
||||
"""check remote follow success page loads"""
|
||||
request = self.factory.get("")
|
||||
request.user = self.remote_user
|
||||
request.following = "mouse@local.com"
|
||||
result = views.ostatus_follow_success(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_remote_follow(self, _):
|
||||
"""check follow from remote page loads"""
|
||||
request = self.factory.post("", {"user": self.remote_user.id})
|
||||
request.user = self.remote_user
|
||||
request.remote_user = "mouse@local.com"
|
||||
result = views.remote_follow(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
|
|
@ -44,6 +44,7 @@ urlpatterns = [
|
|||
re_path(r"^api/v1/instance/?$", views.instance_info),
|
||||
re_path(r"^api/v1/instance/peers/?$", views.peers),
|
||||
re_path(r"^opensearch.xml$", views.opensearch, name="opensearch"),
|
||||
re_path(r"^ostatus_subscribe/?$", views.ostatus_follow_request),
|
||||
# polling updates
|
||||
re_path("^api/updates/notifications/?$", views.get_notification_count),
|
||||
re_path("^api/updates/stream/(?P<stream>[a-z]+)/?$", views.get_unread_status_count),
|
||||
|
@ -422,13 +423,29 @@ urlpatterns = [
|
|||
r"^upload-cover/(?P<book_id>\d+)/?$", views.upload_cover, name="upload-cover"
|
||||
),
|
||||
re_path(r"^add-description/(?P<book_id>\d+)/?$", views.add_description),
|
||||
re_path(r"^resolve-book/?$", views.resolve_book),
|
||||
re_path(r"^switch-edition/?$", views.switch_edition),
|
||||
re_path(r"^resolve-book/?$", views.resolve_book, name="resolve-book"),
|
||||
re_path(r"^switch-edition/?$", views.switch_edition, name="switch-edition"),
|
||||
re_path(
|
||||
rf"{BOOK_PATH}/update/(?P<connector_identifier>[\w\.]+)/?$",
|
||||
views.update_book_from_remote,
|
||||
name="book-update-remote",
|
||||
),
|
||||
re_path(
|
||||
r"^author/(?P<author_id>\d+)/update/(?P<connector_identifier>[\w\.]+)/?$",
|
||||
views.update_author_from_remote,
|
||||
name="author-update-remote",
|
||||
),
|
||||
# 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()),
|
||||
re_path(
|
||||
r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view(), name="author"
|
||||
),
|
||||
re_path(
|
||||
r"^author/(?P<author_id>\d+)/edit/?$",
|
||||
views.EditAuthor.as_view(),
|
||||
name="edit-author",
|
||||
),
|
||||
# reading progress
|
||||
re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"),
|
||||
re_path(r"^delete-readthrough/?$", views.delete_readthrough),
|
||||
|
@ -450,4 +467,9 @@ urlpatterns = [
|
|||
re_path(r"^unfollow/?$", views.unfollow, name="unfollow"),
|
||||
re_path(r"^accept-follow-request/?$", views.accept_follow_request),
|
||||
re_path(r"^delete-follow-request/?$", views.delete_follow_request),
|
||||
re_path(r"^ostatus_follow/?$", views.remote_follow, name="remote-follow"),
|
||||
re_path(r"^remote_follow/?$", views.remote_follow_page, name="remote-follow-page"),
|
||||
re_path(
|
||||
r"^ostatus_success/?$", views.ostatus_follow_success, name="ostatus-success"
|
||||
),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
|
|
@ -29,6 +29,7 @@ from .preferences.block import Block, unblock
|
|||
|
||||
# books
|
||||
from .books.books import Book, upload_cover, add_description, resolve_book
|
||||
from .books.books import update_book_from_remote
|
||||
from .books.edit_book import EditBook, ConfirmEditBook
|
||||
from .books.editions import Editions, switch_edition
|
||||
|
||||
|
@ -54,11 +55,18 @@ from .imports.manually_review import (
|
|||
)
|
||||
|
||||
# misc views
|
||||
from .author import Author, EditAuthor
|
||||
from .author import Author, EditAuthor, update_author_from_remote
|
||||
from .directory import Directory
|
||||
from .discover import Discover
|
||||
from .feed import DirectMessage, Feed, Replies, Status
|
||||
from .follow import follow, unfollow
|
||||
from .follow import (
|
||||
follow,
|
||||
unfollow,
|
||||
ostatus_follow_request,
|
||||
ostatus_follow_success,
|
||||
remote_follow,
|
||||
remote_follow_page,
|
||||
)
|
||||
from .follow import accept_follow_request, delete_follow_request
|
||||
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
|
||||
from .goal import Goal, hide_goal
|
||||
|
|
|
@ -6,9 +6,11 @@ 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.activitypub import ActivitypubResponse
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.views.helpers import is_api_request
|
||||
|
||||
|
@ -73,3 +75,19 @@ class EditAuthor(View):
|
|||
author = form.save()
|
||||
|
||||
return redirect(f"/author/{author.id}")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_book", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def update_author_from_remote(request, author_id, connector_identifier):
|
||||
"""load the remote data for this author"""
|
||||
connector = connector_manager.load_connector(
|
||||
get_object_or_404(models.Connector, identifier=connector_identifier)
|
||||
)
|
||||
author = get_object_or_404(models.Author, id=author_id)
|
||||
|
||||
connector.update_author_from_remote(author)
|
||||
|
||||
return redirect("author", author.id)
|
||||
|
|
|
@ -178,3 +178,19 @@ def resolve_book(request):
|
|||
book = connector.get_or_create_book(remote_id)
|
||||
|
||||
return redirect("book", book.id)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_book", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def update_book_from_remote(request, book_id, connector_identifier):
|
||||
"""load the remote data for this book"""
|
||||
connector = connector_manager.load_connector(
|
||||
get_object_or_404(models.Connector, identifier=connector_identifier)
|
||||
)
|
||||
book = get_object_or_404(models.Book.objects.select_subclasses(), id=book_id)
|
||||
|
||||
connector.update_book_from_remote(book)
|
||||
|
||||
return redirect("book", book.id)
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
""" views for actions you can take in the application """
|
||||
import urllib.parse
|
||||
import re
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db import IntegrityError
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import models
|
||||
from .helpers import get_user_from_username
|
||||
from .helpers import (
|
||||
get_user_from_username,
|
||||
handle_remote_webfinger,
|
||||
subscribe_remote_webfinger,
|
||||
WebFingerError,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -23,6 +31,9 @@ def follow(request):
|
|||
except IntegrityError:
|
||||
pass
|
||||
|
||||
if request.GET.get("next"):
|
||||
return redirect(request.GET.get("next", "/"))
|
||||
|
||||
return redirect(to_follow.local_path)
|
||||
|
||||
|
||||
|
@ -84,3 +95,91 @@ def delete_follow_request(request):
|
|||
|
||||
follow_request.delete()
|
||||
return redirect(f"/user/{request.user.localname}")
|
||||
|
||||
|
||||
def ostatus_follow_request(request):
|
||||
"""prepare an outgoing remote follow request"""
|
||||
uri = urllib.parse.unquote(request.GET.get("acct"))
|
||||
username_parts = re.search(
|
||||
r"(?:^http(?:s?):\/\/)([\w\-\.]*)(?:.)*(?:(?:\/)([\w]*))", uri
|
||||
)
|
||||
account = f"{username_parts[2]}@{username_parts[1]}"
|
||||
user = handle_remote_webfinger(account)
|
||||
error = None
|
||||
|
||||
if user is None or user == "":
|
||||
error = "ostatus_subscribe"
|
||||
|
||||
# don't do these checks for AnonymousUser before they sign in
|
||||
if request.user.is_authenticated:
|
||||
|
||||
# you have blocked them so you probably don't want to follow
|
||||
if hasattr(request.user, "blocks") and user in request.user.blocks.all():
|
||||
error = "is_blocked"
|
||||
# they have blocked you
|
||||
if hasattr(user, "blocks") and request.user in user.blocks.all():
|
||||
error = "has_blocked"
|
||||
# you're already following them
|
||||
if hasattr(user, "followers") and request.user in user.followers.all():
|
||||
error = "already_following"
|
||||
# you're not following yet but you already asked
|
||||
if (
|
||||
hasattr(user, "follower_requests")
|
||||
and request.user in user.follower_requests.all()
|
||||
):
|
||||
error = "already_requested"
|
||||
|
||||
data = {"account": account, "user": user, "error": error}
|
||||
|
||||
return TemplateResponse(request, "ostatus/subscribe.html", data)
|
||||
|
||||
|
||||
@login_required
|
||||
def ostatus_follow_success(request):
|
||||
"""display success message for remote follow"""
|
||||
user = get_user_from_username(request.user, request.GET.get("following"))
|
||||
data = {"account": user.name, "user": user, "error": None}
|
||||
return TemplateResponse(request, "ostatus/success.html", data)
|
||||
|
||||
|
||||
def remote_follow_page(request):
|
||||
"""display remote follow page"""
|
||||
user = get_user_from_username(request.user, request.GET.get("user"))
|
||||
data = {"user": user}
|
||||
return TemplateResponse(request, "ostatus/remote_follow.html", data)
|
||||
|
||||
|
||||
@require_POST
|
||||
def remote_follow(request):
|
||||
"""direct user to follow from remote account using ostatus subscribe protocol"""
|
||||
remote_user = request.POST.get("remote_user")
|
||||
try:
|
||||
if remote_user[0] == "@":
|
||||
remote_user = remote_user[1:]
|
||||
remote_domain = remote_user.split("@")[1]
|
||||
except (TypeError, IndexError):
|
||||
remote_domain = None
|
||||
|
||||
wf_response = subscribe_remote_webfinger(remote_user)
|
||||
user = get_object_or_404(models.User, id=request.POST.get("user"))
|
||||
|
||||
if wf_response is None:
|
||||
data = {
|
||||
"account": remote_user,
|
||||
"user": user,
|
||||
"error": "not_supported",
|
||||
"remote_domain": remote_domain,
|
||||
}
|
||||
return TemplateResponse(request, "ostatus/subscribe.html", data)
|
||||
|
||||
if isinstance(wf_response, WebFingerError):
|
||||
data = {
|
||||
"account": remote_user,
|
||||
"user": user,
|
||||
"error": str(wf_response),
|
||||
"remote_domain": remote_domain,
|
||||
}
|
||||
return TemplateResponse(request, "ostatus/subscribe.html", data)
|
||||
|
||||
url = wf_response.replace("{uri}", urllib.parse.quote(user.remote_id))
|
||||
return redirect(url)
|
||||
|
|
|
@ -16,6 +16,13 @@ from bookwyrm.status import create_generated_note
|
|||
from bookwyrm.utils import regex
|
||||
|
||||
|
||||
# pylint: disable=unnecessary-pass
|
||||
class WebFingerError(Exception):
|
||||
"""empty error class for problems finding user information with webfinger"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def get_user_from_username(viewer, username):
|
||||
"""helper function to resolve a localname or a username to a user"""
|
||||
if viewer.is_authenticated and viewer.localname == username:
|
||||
|
@ -57,10 +64,8 @@ def handle_remote_webfinger(query):
|
|||
# usernames could be @user@domain or user@domain
|
||||
if not query:
|
||||
return None
|
||||
|
||||
if query[0] == "@":
|
||||
query = query[1:]
|
||||
|
||||
try:
|
||||
domain = query.split("@")[1]
|
||||
except IndexError:
|
||||
|
@ -86,6 +91,35 @@ def handle_remote_webfinger(query):
|
|||
return user
|
||||
|
||||
|
||||
def subscribe_remote_webfinger(query):
|
||||
"""get subscribe template from other servers"""
|
||||
template = None
|
||||
# usernames could be @user@domain or user@domain
|
||||
if not query:
|
||||
return WebFingerError("invalid_username")
|
||||
|
||||
if query[0] == "@":
|
||||
query = query[1:]
|
||||
|
||||
try:
|
||||
domain = query.split("@")[1]
|
||||
except IndexError:
|
||||
return WebFingerError("invalid_username")
|
||||
|
||||
url = f"https://{domain}/.well-known/webfinger?resource=acct:{query}"
|
||||
|
||||
try:
|
||||
data = get_data(url)
|
||||
except (ConnectorException, HTTPError):
|
||||
return WebFingerError("user_not_found")
|
||||
|
||||
for link in data.get("links"):
|
||||
if link.get("rel") == "http://ostatus.org/schema/1.0/subscribe":
|
||||
template = link["template"]
|
||||
|
||||
return template
|
||||
|
||||
|
||||
def get_edition(book_id):
|
||||
"""look up a book in the db and return an edition"""
|
||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
||||
|
|
|
@ -30,7 +30,11 @@ def webfinger(request):
|
|||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": user.remote_id,
|
||||
}
|
||||
},
|
||||
{
|
||||
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||
"template": f"https://{DOMAIN}/ostatus_subscribe?acct={{uri}}",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-12-04 22:38+0000\n"
|
||||
"POT-Creation-Date: 2021-12-07 22:16+0000\n"
|
||||
"PO-Revision-Date: 2021-02-28 17:19-0800\n"
|
||||
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
|
||||
"Language-Team: English <LL@li.org>\n"
|
||||
|
@ -18,58 +18,58 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: bookwyrm/forms.py:350
|
||||
#: bookwyrm/forms.py:351
|
||||
msgid "A user with this email already exists."
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/forms.py:364
|
||||
#: bookwyrm/forms.py:365
|
||||
msgid "One Day"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/forms.py:365
|
||||
#: bookwyrm/forms.py:366
|
||||
msgid "One Week"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/forms.py:366
|
||||
#: bookwyrm/forms.py:367
|
||||
msgid "One Month"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/forms.py:367
|
||||
#: bookwyrm/forms.py:368
|
||||
msgid "Does Not Expire"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/forms.py:371
|
||||
#: bookwyrm/forms.py:372
|
||||
#, python-brace-format
|
||||
msgid "{i} uses"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/forms.py:372
|
||||
#: bookwyrm/forms.py:373
|
||||
msgid "Unlimited"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/forms.py:468
|
||||
#: bookwyrm/forms.py:469
|
||||
msgid "List Order"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/forms.py:469
|
||||
#: bookwyrm/forms.py:470
|
||||
msgid "Book Title"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/forms.py:470 bookwyrm/templates/shelf/shelf.html:152
|
||||
#: bookwyrm/forms.py:471 bookwyrm/templates/shelf/shelf.html:152
|
||||
#: bookwyrm/templates/shelf/shelf.html:184
|
||||
#: bookwyrm/templates/snippets/create_status/review.html:33
|
||||
msgid "Rating"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/forms.py:472 bookwyrm/templates/lists/list.html:110
|
||||
#: bookwyrm/forms.py:473 bookwyrm/templates/lists/list.html:110
|
||||
msgid "Sort By"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/forms.py:476
|
||||
#: bookwyrm/forms.py:477
|
||||
msgid "Ascending"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/forms.py:477
|
||||
#: bookwyrm/forms.py:478
|
||||
msgid "Descending"
|
||||
msgstr ""
|
||||
|
||||
|
@ -102,23 +102,23 @@ msgstr ""
|
|||
msgid "Domain block"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/models/book.py:233
|
||||
#: bookwyrm/models/book.py:243
|
||||
msgid "Audiobook"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/models/book.py:234
|
||||
#: bookwyrm/models/book.py:244
|
||||
msgid "eBook"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/models/book.py:235
|
||||
#: bookwyrm/models/book.py:245
|
||||
msgid "Graphic novel"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/models/book.py:236
|
||||
#: bookwyrm/models/book.py:246
|
||||
msgid "Hardcover"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/models/book.py:237
|
||||
#: bookwyrm/models/book.py:247
|
||||
msgid "Paperback"
|
||||
msgstr ""
|
||||
|
||||
|
@ -146,6 +146,7 @@ msgid "%(value)s is not a valid username"
|
|||
msgstr ""
|
||||
|
||||
#: bookwyrm/models/fields.py:183 bookwyrm/templates/layout.html:171
|
||||
#: bookwyrm/templates/ostatus/error.html:29
|
||||
msgid "username"
|
||||
msgstr ""
|
||||
|
||||
|
@ -153,7 +154,7 @@ msgstr ""
|
|||
msgid "A user with that username already exists."
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/models/user.py:32 bookwyrm/templates/book/book.html:227
|
||||
#: bookwyrm/models/user.py:32 bookwyrm/templates/book/book.html:244
|
||||
msgid "Reviews"
|
||||
msgstr ""
|
||||
|
||||
|
@ -183,7 +184,7 @@ msgstr ""
|
|||
|
||||
#: bookwyrm/settings.py:119 bookwyrm/templates/search/layout.html:21
|
||||
#: bookwyrm/templates/search/layout.html:42
|
||||
#: bookwyrm/templates/user/layout.html:88
|
||||
#: bookwyrm/templates/user/layout.html:91
|
||||
msgid "Books"
|
||||
msgstr ""
|
||||
|
||||
|
@ -248,46 +249,61 @@ msgstr ""
|
|||
msgid "Edit Author"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/author.html:35
|
||||
#: bookwyrm/templates/author/author.html:40
|
||||
msgid "Author details"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/author.html:44
|
||||
#: bookwyrm/templates/author/edit_author.html:42
|
||||
msgid "Aliases:"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/author.html:46
|
||||
#: bookwyrm/templates/author/author.html:53
|
||||
msgid "Born:"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/author.html:53
|
||||
#: bookwyrm/templates/author/author.html:60
|
||||
msgid "Died:"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/author.html:62
|
||||
#: bookwyrm/templates/author/author.html:70
|
||||
msgid "External links"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/author.html:75
|
||||
msgid "Wikipedia"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/author.html:70
|
||||
#: bookwyrm/templates/author/author.html:83
|
||||
msgid "View ISNI record"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/author.html:78
|
||||
#: bookwyrm/templates/book/book.html:94
|
||||
#: bookwyrm/templates/author/author.html:88
|
||||
#: bookwyrm/templates/author/sync_modal.html:5
|
||||
#: bookwyrm/templates/book/book.html:93
|
||||
#: bookwyrm/templates/book/sync_modal.html:5
|
||||
msgid "Load data"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/author.html:92
|
||||
#: bookwyrm/templates/book/book.html:96
|
||||
msgid "View on OpenLibrary"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/author.html:86
|
||||
#: bookwyrm/templates/book/book.html:97
|
||||
#: bookwyrm/templates/author/author.html:106
|
||||
#: bookwyrm/templates/book/book.html:107
|
||||
msgid "View on Inventaire"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/author.html:94
|
||||
#: bookwyrm/templates/author/author.html:121
|
||||
msgid "View on LibraryThing"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/author.html:102
|
||||
#: bookwyrm/templates/author/author.html:129
|
||||
msgid "View on Goodreads"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/author.html:117
|
||||
#: bookwyrm/templates/author/author.html:143
|
||||
#, python-format
|
||||
msgid "Books by %(name)s"
|
||||
msgstr ""
|
||||
|
@ -364,8 +380,12 @@ msgstr ""
|
|||
msgid "Goodreads key:"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/edit_author.html:108
|
||||
#: bookwyrm/templates/book/book.html:140
|
||||
#: bookwyrm/templates/author/edit_author.html:105
|
||||
msgid "ISNI:"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/edit_author.html:115
|
||||
#: bookwyrm/templates/book/book.html:157
|
||||
#: bookwyrm/templates/book/edit/edit_book.html:121
|
||||
#: bookwyrm/templates/book/readthrough.html:76
|
||||
#: bookwyrm/templates/groups/form.html:24
|
||||
|
@ -382,12 +402,14 @@ msgstr ""
|
|||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/edit_author.html:109
|
||||
#: bookwyrm/templates/book/book.html:141 bookwyrm/templates/book/book.html:199
|
||||
#: bookwyrm/templates/author/edit_author.html:116
|
||||
#: bookwyrm/templates/author/sync_modal.html:26
|
||||
#: bookwyrm/templates/book/book.html:158 bookwyrm/templates/book/book.html:216
|
||||
#: bookwyrm/templates/book/cover_modal.html:32
|
||||
#: bookwyrm/templates/book/edit/edit_book.html:123
|
||||
#: bookwyrm/templates/book/edit/edit_book.html:126
|
||||
#: bookwyrm/templates/book/readthrough.html:77
|
||||
#: bookwyrm/templates/book/sync_modal.html:26
|
||||
#: bookwyrm/templates/groups/delete_group_modal.html:17
|
||||
#: bookwyrm/templates/lists/delete_list_modal.html:17
|
||||
#: bookwyrm/templates/settings/federation/instance.html:88
|
||||
|
@ -396,6 +418,20 @@ msgstr ""
|
|||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/sync_modal.html:15
|
||||
#, python-format
|
||||
msgid "Loading data will connect to <strong>%(source_name)s</strong> and check for any metadata about this author which aren't present here. Existing metadata will not be overwritten."
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/author/sync_modal.html:23
|
||||
#: bookwyrm/templates/book/edit/edit_book.html:108
|
||||
#: bookwyrm/templates/book/sync_modal.html:23
|
||||
#: bookwyrm/templates/groups/members.html:16
|
||||
#: bookwyrm/templates/landing/password_reset.html:42
|
||||
#: bookwyrm/templates/snippets/remove_from_group_button.html:16
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:47
|
||||
#: bookwyrm/templates/discover/large-book.html:22
|
||||
#: bookwyrm/templates/landing/large-book.html:25
|
||||
|
@ -416,86 +452,86 @@ msgstr ""
|
|||
msgid "Failed to load cover"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:117
|
||||
#: bookwyrm/templates/book/book.html:134
|
||||
#, python-format
|
||||
msgid "(%(review_count)s review)"
|
||||
msgid_plural "(%(review_count)s reviews)"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:129
|
||||
#: bookwyrm/templates/book/book.html:146
|
||||
msgid "Add Description"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:136
|
||||
#: bookwyrm/templates/book/book.html:153
|
||||
#: bookwyrm/templates/book/edit/edit_book_form.html:39
|
||||
#: bookwyrm/templates/lists/form.html:13 bookwyrm/templates/shelf/form.html:17
|
||||
msgid "Description:"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:150
|
||||
#: bookwyrm/templates/book/book.html:167
|
||||
#, python-format
|
||||
msgid "<a href=\"%(path)s/editions\">%(count)s editions</a>"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:158
|
||||
#: bookwyrm/templates/book/book.html:175
|
||||
msgid "You have shelved this edition in:"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:173
|
||||
#: bookwyrm/templates/book/book.html:190
|
||||
#, python-format
|
||||
msgid "A <a href=\"%(book_path)s\">different edition</a> of this book is on your <a href=\"%(shelf_path)s\">%(shelf_name)s</a> shelf."
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:184
|
||||
#: bookwyrm/templates/book/book.html:201
|
||||
msgid "Your reading activity"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:187
|
||||
#: bookwyrm/templates/book/book.html:204
|
||||
msgid "Add read dates"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:196
|
||||
#: bookwyrm/templates/book/book.html:213
|
||||
msgid "Create"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:206
|
||||
#: bookwyrm/templates/book/book.html:223
|
||||
msgid "You don't have any reading activity for this book."
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:232
|
||||
#: bookwyrm/templates/book/book.html:249
|
||||
msgid "Your reviews"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:238
|
||||
#: bookwyrm/templates/book/book.html:255
|
||||
msgid "Your comments"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:244
|
||||
#: bookwyrm/templates/book/book.html:261
|
||||
msgid "Your quotes"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:280
|
||||
#: bookwyrm/templates/book/book.html:297
|
||||
msgid "Subjects"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:292
|
||||
#: bookwyrm/templates/book/book.html:309
|
||||
msgid "Places"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:303 bookwyrm/templates/layout.html:75
|
||||
#: bookwyrm/templates/book/book.html:320 bookwyrm/templates/layout.html:75
|
||||
#: bookwyrm/templates/lists/lists.html:5 bookwyrm/templates/lists/lists.html:12
|
||||
#: bookwyrm/templates/search/layout.html:25
|
||||
#: bookwyrm/templates/search/layout.html:50
|
||||
#: bookwyrm/templates/user/layout.html:82
|
||||
#: bookwyrm/templates/user/layout.html:85
|
||||
msgid "Lists"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:314
|
||||
#: bookwyrm/templates/book/book.html:331
|
||||
msgid "Add to list"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/book.html:324
|
||||
#: bookwyrm/templates/book/book.html:341
|
||||
#: bookwyrm/templates/book/cover_modal.html:31
|
||||
#: bookwyrm/templates/lists/list.html:182
|
||||
#: bookwyrm/templates/settings/email_blocklist/domain_form.html:24
|
||||
|
@ -573,13 +609,6 @@ msgstr ""
|
|||
msgid "This is a new work"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/edit/edit_book.html:108
|
||||
#: bookwyrm/templates/groups/members.html:16
|
||||
#: bookwyrm/templates/landing/password_reset.html:42
|
||||
#: bookwyrm/templates/snippets/remove_from_group_button.html:16
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/edit/edit_book.html:110
|
||||
#: bookwyrm/templates/feed/status.html:21
|
||||
msgid "Back"
|
||||
|
@ -779,6 +808,11 @@ msgstr ""
|
|||
msgid "Delete these read dates"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/book/sync_modal.html:15
|
||||
#, python-format
|
||||
msgid "Loading data will connect to <strong>%(source_name)s</strong> and check for any metadata about this book which aren't present here. Existing metadata will not be overwritten."
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/components/inline_form.html:8
|
||||
#: bookwyrm/templates/components/modal.html:11
|
||||
#: bookwyrm/templates/components/tooltip.html:7
|
||||
|
@ -898,6 +932,12 @@ msgstr ""
|
|||
|
||||
#: bookwyrm/templates/directory/user_card.html:17
|
||||
#: bookwyrm/templates/directory/user_card.html:18
|
||||
#: bookwyrm/templates/ostatus/remote_follow.html:21
|
||||
#: bookwyrm/templates/ostatus/remote_follow.html:22
|
||||
#: bookwyrm/templates/ostatus/subscribe.html:41
|
||||
#: bookwyrm/templates/ostatus/subscribe.html:42
|
||||
#: bookwyrm/templates/ostatus/success.html:17
|
||||
#: bookwyrm/templates/ostatus/success.html:18
|
||||
#: bookwyrm/templates/user/user_preview.html:16
|
||||
#: bookwyrm/templates/user/user_preview.html:17
|
||||
msgid "Locked account"
|
||||
|
@ -1717,6 +1757,7 @@ msgstr ""
|
|||
|
||||
#: bookwyrm/templates/landing/login.html:7
|
||||
#: bookwyrm/templates/landing/login.html:36 bookwyrm/templates/layout.html:179
|
||||
#: bookwyrm/templates/ostatus/error.html:37
|
||||
msgid "Log in"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1725,18 +1766,20 @@ msgid "Success! Email address confirmed."
|
|||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/landing/login.html:21 bookwyrm/templates/layout.html:170
|
||||
#: bookwyrm/templates/ostatus/error.html:28
|
||||
#: bookwyrm/templates/snippets/register_form.html:4
|
||||
msgid "Username:"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/landing/login.html:27
|
||||
#: bookwyrm/templates/landing/password_reset.html:26
|
||||
#: bookwyrm/templates/layout.html:174
|
||||
#: bookwyrm/templates/layout.html:174 bookwyrm/templates/ostatus/error.html:32
|
||||
#: bookwyrm/templates/snippets/register_form.html:20
|
||||
msgid "Password:"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/landing/login.html:39 bookwyrm/templates/layout.html:176
|
||||
#: bookwyrm/templates/ostatus/error.html:34
|
||||
msgid "Forgot your password?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1801,7 +1844,7 @@ msgstr ""
|
|||
msgid "Notifications"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/layout.html:175
|
||||
#: bookwyrm/templates/layout.html:175 bookwyrm/templates/ostatus/error.html:33
|
||||
msgid "password"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2193,6 +2236,120 @@ msgstr ""
|
|||
msgid "You're all caught up!"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/error.html:7
|
||||
#, python-format
|
||||
msgid "<strong>%(account)s</strong> is not a valid username"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/error.html:8
|
||||
#: bookwyrm/templates/ostatus/error.html:13
|
||||
msgid "Check you have the correct username before trying again"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/error.html:12
|
||||
#, python-format
|
||||
msgid "<strong>%(account)s</strong> could not be found or <code>%(remote_domain)s</code> does not support identity discovery"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/error.html:17
|
||||
#, python-format
|
||||
msgid "<strong>%(account)s</strong> was found but <code>%(remote_domain)s</code> does not support 'remote follow'"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/error.html:18
|
||||
#, python-format
|
||||
msgid "Try searching for <strong>%(user)s</strong> on <code>%(remote_domain)s</code> instead"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/error.html:46
|
||||
#, python-format
|
||||
msgid "Something went wrong trying to follow <strong>%(account)s</strong>"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/error.html:47
|
||||
msgid "Check you have the correct username before trying again."
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/error.html:51
|
||||
#, python-format
|
||||
msgid "You have blocked <strong>%(account)s</strong>"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/error.html:55
|
||||
#, python-format
|
||||
msgid "<strong>%(account)s</strong> has blocked you"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/error.html:59
|
||||
#, python-format
|
||||
msgid "You are already following <strong>%(account)s</strong>"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/error.html:63
|
||||
#, python-format
|
||||
msgid "You have already requested to follow <strong>%(account)s</strong>"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/remote_follow.html:6
|
||||
#, python-format
|
||||
msgid "Follow %(username)s on the fediverse"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/remote_follow.html:33
|
||||
#, python-format
|
||||
msgid "Follow %(username)s from another Fediverse account like BookWyrm, Mastodon, or Pleroma."
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/remote_follow.html:40
|
||||
msgid "User handle to follow from:"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/remote_follow.html:42
|
||||
msgid "Follow!"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/remote_follow_button.html:8
|
||||
msgid "Follow on Fediverse"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/remote_follow_button.html:12
|
||||
msgid "This link opens in a pop-up window"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/subscribe.html:8
|
||||
#, python-format
|
||||
msgid "Log in to %(sitename)s"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/subscribe.html:10
|
||||
#, python-format
|
||||
msgid "Error following from %(sitename)s"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/subscribe.html:12
|
||||
#: bookwyrm/templates/ostatus/subscribe.html:22
|
||||
#, python-format
|
||||
msgid "Follow from %(sitename)s"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/subscribe.html:18
|
||||
msgid "Uh oh..."
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/subscribe.html:20
|
||||
msgid "Let's log in first..."
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/subscribe.html:51
|
||||
#, python-format
|
||||
msgid "Follow %(username)s"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/ostatus/success.html:28
|
||||
#, python-format
|
||||
msgid "You are now following %(display_name)s!"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/preferences/blocks.html:4
|
||||
#: bookwyrm/templates/preferences/blocks.html:7
|
||||
#: bookwyrm/templates/preferences/layout.html:31
|
||||
|
@ -2618,7 +2775,7 @@ msgid "Details"
|
|||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/settings/federation/instance.html:35
|
||||
#: bookwyrm/templates/user/layout.html:64
|
||||
#: bookwyrm/templates/user/layout.html:67
|
||||
msgid "Activity"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3260,6 +3417,7 @@ msgid "Posted by <a href=\"%(user_path)s\">%(username)s</a>"
|
|||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/snippets/authors.html:22
|
||||
#: bookwyrm/templates/snippets/trimmed_list.html:14
|
||||
#, python-format
|
||||
msgid "and %(remainder_count_display)s other"
|
||||
msgid_plural "and %(remainder_count_display)s others"
|
||||
|
@ -3847,15 +4005,15 @@ msgstr ""
|
|||
msgid "User Profile"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/user/layout.html:45
|
||||
#: bookwyrm/templates/user/layout.html:48
|
||||
msgid "Follow Requests"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/user/layout.html:70
|
||||
#: bookwyrm/templates/user/layout.html:73
|
||||
msgid "Reading Goal"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/user/layout.html:76
|
||||
#: bookwyrm/templates/user/layout.html:79
|
||||
msgid "Groups"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3945,7 +4103,7 @@ msgstr ""
|
|||
msgid "File exceeds maximum size: 10MB"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templatetags/utilities.py:34
|
||||
#: bookwyrm/templatetags/utilities.py:33
|
||||
#, python-format
|
||||
msgid "%(title)s: %(subtitle)s"
|
||||
msgstr ""
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue