Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-12-07 15:37:22 -08:00
commit b53d45a19a
55 changed files with 4045 additions and 1640 deletions

View file

@ -111,7 +111,7 @@ class AbstractConnector(AbstractMinimalConnector):
return existing.default_edition return existing.default_edition
return existing return existing
# load the json # load the json data from the remote data source
data = self.get_book_data(remote_id) data = self.get_book_data(remote_id)
if self.is_work_data(data): if self.is_work_data(data):
try: try:
@ -150,27 +150,37 @@ class AbstractConnector(AbstractMinimalConnector):
"""this allows connectors to override the default behavior""" """this allows connectors to override the default behavior"""
return get_data(remote_id) 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""" """if we already have the work, we're ready"""
mapped_data = dict_from_mappings(edition_data, self.book_mappings) mapped_data = dict_from_mappings(edition_data, self.book_mappings)
mapped_data["work"] = work.remote_id mapped_data["work"] = work.remote_id
edition_activity = activitypub.Edition(**mapped_data) edition_activity = activitypub.Edition(**mapped_data)
edition = edition_activity.to_model(model=models.Edition, overwrite=False) edition = edition_activity.to_model(
edition.connector = self.connector model=models.Edition, overwrite=False, instance=instance
edition.save() )
# 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): for author in self.get_authors_from_data(edition_data):
edition.authors.add(author) 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(): if not edition.authors.exists() and work.authors.exists():
edition.authors.set(work.authors.all()) edition.authors.set(work.authors.all())
return edition return edition
def get_or_create_author(self, remote_id): def get_or_create_author(self, remote_id, instance=None):
"""load that author""" """load that author"""
existing = models.Author.find_existing_by_remote_id(remote_id) if not instance:
if existing: existing = models.Author.find_existing_by_remote_id(remote_id)
return existing if existing:
return existing
data = self.get_book_data(remote_id) data = self.get_book_data(remote_id)
@ -181,7 +191,24 @@ class AbstractConnector(AbstractMinimalConnector):
return None return None
# this will dedupe # 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 @abstractmethod
def is_work_data(self, data): def is_work_data(self, data):

View file

@ -11,6 +11,8 @@ from .connector_manager import ConnectorException
class Connector(AbstractConnector): class Connector(AbstractConnector):
"""instantiate a connector for inventaire""" """instantiate a connector for inventaire"""
generated_remote_link_field = "inventaire_id"
def __init__(self, identifier): def __init__(self, identifier):
super().__init__(identifier) super().__init__(identifier)
@ -210,6 +212,11 @@ class Connector(AbstractConnector):
return "" return ""
return data.get("extract") 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"): def get_language_code(options, code="en"):
"""when there are a bunch of translation but we need a single field""" """when there are a bunch of translation but we need a single field"""

View file

@ -12,6 +12,8 @@ from .openlibrary_languages import languages
class Connector(AbstractConnector): class Connector(AbstractConnector):
"""instantiate a connector for OL""" """instantiate a connector for OL"""
generated_remote_link_field = "openlibrary_link"
def __init__(self, identifier): def __init__(self, identifier):
super().__init__(identifier) super().__init__(identifier)
@ -66,6 +68,7 @@ class Connector(AbstractConnector):
Mapping("born", remote_field="birth_date"), Mapping("born", remote_field="birth_date"),
Mapping("died", remote_field="death_date"), Mapping("died", remote_field="death_date"),
Mapping("bio", formatter=get_description), Mapping("bio", formatter=get_description),
Mapping("isni", remote_field="remote_ids", formatter=get_isni),
] ]
def get_book_data(self, remote_id): def get_book_data(self, remote_id):
@ -224,6 +227,13 @@ def get_languages(language_blob):
return langs 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): def pick_default_edition(options):
"""favor physical copies with covers in english""" """favor physical copies with covers in english"""
if not options: if not options:

View file

@ -1,4 +1,5 @@
""" database schema for info about authors """ """ database schema for info about authors """
import re
from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.indexes import GinIndex
from django.db import models from django.db import models
@ -33,6 +34,17 @@ class Author(BookDataModel):
) )
bio = fields.HtmlField(null=True, blank=True) 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): def get_remote_id(self):
"""editions and works both use "book" instead of model_name""" """editions and works both use "book" instead of model_name"""
return f"https://{DOMAIN}/author/{self.id}" return f"https://{DOMAIN}/author/{self.id}"

View file

@ -52,6 +52,16 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
null=True, 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: class Meta:
"""can't initialize this model, that wouldn't make sense""" """can't initialize this model, that wouldn't make sense"""

View file

@ -46,4 +46,5 @@
<glyph unicode="&#xe9d9;" 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="&#xe9d9;" 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="&#xe9da;" 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="&#xe9da;" 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="&#xea0a;" 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="&#xea0a;" 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="&#xea36;" glyph-name="download" d="M512-32l480 480h-288v512h-384v-512h-288z" />
</font></defs></svg> </font></defs></svg>

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -1,10 +1,10 @@
@font-face { @font-face {
font-family: 'icomoon'; font-family: 'icomoon';
src: url('../fonts/icomoon.eot?36x4a3'); src: url('../fonts/icomoon.eot?r7jc98');
src: url('../fonts/icomoon.eot?36x4a3#iefix') format('embedded-opentype'), src: url('../fonts/icomoon.eot?r7jc98#iefix') format('embedded-opentype'),
url('../fonts/icomoon.ttf?36x4a3') format('truetype'), url('../fonts/icomoon.ttf?r7jc98') format('truetype'),
url('../fonts/icomoon.woff?36x4a3') format('woff'), url('../fonts/icomoon.woff?r7jc98') format('woff'),
url('../fonts/icomoon.svg?36x4a3#icomoon') format('svg'); url('../fonts/icomoon.svg?r7jc98#icomoon') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: block; font-display: block;
@ -142,3 +142,6 @@
.icon-spinner:before { .icon-spinner:before {
content: "\e97a"; content: "\e97a";
} }
.icon-download:before {
content: "\ea36";
}

View file

@ -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 ) { duplicateInput (event ) {
const trigger = event.currentTarget; const trigger = event.currentTarget;
const input_id = trigger.dataset['duplicate'] const input_id = trigger.dataset['duplicate']

View file

@ -14,7 +14,7 @@
</div> </div>
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %} {% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
<div class="column is-narrow"> <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="icon icon-pencil" title="{% trans 'Edit Author' %}" aria-hidden="True"></span>
<span class="is-hidden-mobile">{% trans "Edit Author" %}</span> <span class="is-hidden-mobile">{% trans "Edit Author" %}</span>
</a> </a>
@ -23,102 +23,130 @@
</div> </div>
</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 }}"> <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="column is-two-fifths">
<div class="box py-2"> {% if details %}
<dl> <section class="block content">
<h2 class="title is-4">{% trans "Author details" %}</h2>
<dl class="box">
{% if author.aliases %} {% 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> <dt class="has-text-weight-bold mr-1">{% trans "Aliases:" %}</dt>
{% for alias in author.aliases %} <dd>
<dd itemprop="alternateName" content="{{alias}}"> {% include "snippets/trimmed_list.html" with items=author.aliases itemprop="alternateName" %}
{{alias}}{% if not forloop.last %},&nbsp;{% endif %} </dd>
</dd>
{% endfor %}
</div> </div>
{% endif %} {% endif %}
{% if author.born %} {% 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> <dt class="has-text-weight-bold mr-1">{% trans "Born:" %}</dt>
<dd itemprop="birthDate">{{ author.born|naturalday }}</dd> <dd itemprop="birthDate">{{ author.born|naturalday }}</dd>
</div> </div>
{% endif %} {% endif %}
{% if author.died %} {% 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> <dt class="has-text-weight-bold mr-1">{% trans "Died:" %}</dt>
<dd itemprop="deathDate">{{ author.died|naturalday }}</dd> <dd itemprop="deathDate">{{ author.died|naturalday }}</dd>
</div> </div>
{% endif %} {% endif %}
</dl> </dl>
</section>
{% endif %}
{% if author.wikipedia_link %} {% if links %}
<p class="my-1"> <section>
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener" target="_blank"> <h2 class="title is-4">{% trans "External links" %}</h2>
{% trans "Wikipedia" %} <div class="box">
</a> {% if author.wikipedia_link %}
</p> <div>
{% endif %} <a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener" target="_blank">
{% trans "Wikipedia" %}
</a>
</div>
{% endif %}
{% if author.isni %} {% if author.isni %}
<p class="my-1"> <div class="mt-1">
<a itemprop="sameAs" href="https://isni.org/isni/{{ author.isni|remove_spaces }}" rel="noopener" target="_blank"> <a itemprop="sameAs" href="{{ author.isni_link }}" rel="noopener" target="_blank">
{% trans "View ISNI record" %} {% trans "View ISNI record" %}
</a> </a>
</p> </div>
{% endif %} {% endif %}
{% if author.openlibrary_key %} {% trans "Load data" as button_text %}
<p class="my-1"> {% if author.openlibrary_key %}
<a itemprop="sameAs" href="https://openlibrary.org/authors/{{ author.openlibrary_key }}" target="_blank" rel="noopener"> <div class="mt-1 is-flex">
{% trans "View on OpenLibrary" %} <a class="mr-3" itemprop="sameAs" href="{{ author.openlibrary_link }}" target="_blank" rel="noopener">
</a> {% trans "View on OpenLibrary" %}
</p> </a>
{% endif %} {% 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 %} {% if author.inventaire_id %}
<p class="my-1"> <div class="mt-1 is-flex">
<a itemprop="sameAs" href="https://inventaire.io/entity/{{ author.inventaire_id }}" target="_blank" rel="noopener"> <a class="mr-3" itemprop="sameAs" href="{{ author.inventaire_link }}" target="_blank" rel="noopener">
{% trans "View on Inventaire" %} {% trans "View on Inventaire" %}
</a> </a>
</p>
{% endif %}
{% if author.librarything_key %} {% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
<p class="my-1"> {% with controls_text="iv_sync" controls_uid=author.id %}
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener"> {% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_iv_sync" class="is-small" icon_with_text="download" %}
{% trans "View on LibraryThing" %} {% include "author/sync_modal.html" with source="inventaire.io" source_name="Inventaire" %}
</a> {% endwith %}
</p> {% endif %}
{% endif %} </div>
{% endif %}
{% if author.goodreads_key %} {% if author.librarything_key %}
<p class="my-1"> <div class="mt-1">
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener"> <a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener">
{% trans "View on Goodreads" %} {% trans "View on LibraryThing" %}
</a> </a>
</p> </div>
{% endif %} {% endif %}
</div>
</div> {% if author.goodreads_key %}
{% endif %} <div>
<div class="column"> <a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener">
{% if author.bio %} {% trans "View on Goodreads" %}
{{ author.bio|to_markdown|safe }} </a>
</div>
{% endif %}
</div>
</section>
{% endif %} {% endif %}
</div> </div>
{% endif %}
</div> </div>
<hr aria-hidden="true">
<div class="block"> <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"> <div class="columns is-multiline is-mobile">
{% for book in books %} {% for book in books %}
<div class="column is-one-fifth"> <div class="column is-one-fifth-tablet is-half-mobile is-flex is-flex-direction-column">
{% include 'landing/small-book.html' with book=book %} <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 %} {% include 'snippets/shelve_button/shelve_button.html' with book=book %}
</div> </div>
{% endfor %} {% endfor %}

View 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 %}

View file

@ -90,11 +90,28 @@
</div> </div>
{% endwith %} {% endwith %}
{% trans "Load data" as button_text %}
{% if book.openlibrary_key %} {% 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 %} {% endif %}
{% if book.inventaire_id %} {% 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 %} {% endif %}
</section> </section>
</div> </div>

View 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 %}

View file

@ -267,5 +267,6 @@
<script src="{% static "js/status_cache.js" %}?v={{ js_cache }}"></script> <script src="{% static "js/status_cache.js" %}?v={{ js_cache }}"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View 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 %}

View 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 %}

View 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 %}

View 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 %}&nbsp;{% endif %}
</div>
</div>
</div>
{% endif %}
{% endblock %}

View 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 %}

View 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>

View 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 %}

View file

@ -39,6 +39,9 @@
{% if not is_self and request.user.is_authenticated %} {% if not is_self and request.user.is_authenticated %}
{% include 'snippets/follow_button.html' with user=user %} {% include 'snippets/follow_button.html' with user=user %}
{% endif %} {% endif %}
{% if not is_self %}
{% include 'ostatus/remote_follow_button.html' with user=user %}
{% endif %}
{% if is_self and user.follower_requests.all %} {% if is_self and user.follower_requests.all %}
<div class="follow-requests"> <div class="follow-requests">

View file

@ -5,7 +5,6 @@ from uuid import uuid4
from django import template from django import template
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.template.defaultfilters import stringfilter
from django.templatetags.static import static 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>' f'<input type="text" name="isni-for-{author.id}" value="{isni}" hidden>'
) )
return "" return ""
@register.filter(name="remove_spaces")
@stringfilter
def remove_spaces(arg):
"""Removes spaces from argument passed in"""
return re.sub(r"\s", "", str(arg))

View file

@ -40,6 +40,8 @@ class AbstractConnector(TestCase):
class TestConnector(abstract_connector.AbstractConnector): class TestConnector(abstract_connector.AbstractConnector):
"""nothing added here""" """nothing added here"""
generated_remote_link_field = "openlibrary_link"
def format_search_result(self, search_result): def format_search_result(self, search_result):
return search_result return search_result
@ -87,9 +89,7 @@ class AbstractConnector(TestCase):
def test_get_or_create_book_existing(self): def test_get_or_create_book_existing(self):
"""find an existing book by remote/origin id""" """find an existing book by remote/origin id"""
self.assertEqual(models.Book.objects.count(), 1) self.assertEqual(models.Book.objects.count(), 1)
self.assertEqual( self.assertEqual(self.book.remote_id, f"https://{DOMAIN}/book/{self.book.id}")
self.book.remote_id, "https://%s/book/%d" % (DOMAIN, self.book.id)
)
self.assertEqual(self.book.origin_id, "https://example.com/book/1234") self.assertEqual(self.book.origin_id, "https://example.com/book/1234")
# dedupe by origin id # dedupe by origin id
@ -99,7 +99,7 @@ class AbstractConnector(TestCase):
# dedupe by remote id # dedupe by remote id
result = self.connector.get_or_create_book( 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(models.Book.objects.count(), 1)
self.assertEqual(result, self.book) self.assertEqual(result, self.book)
@ -119,7 +119,8 @@ class AbstractConnector(TestCase):
@responses.activate @responses.activate
def test_get_or_create_author(self): def test_get_or_create_author(self):
"""load an author""" """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("id"),
Mapping("name"), Mapping("name"),
] ]
@ -139,3 +140,26 @@ class AbstractConnector(TestCase):
author = models.Author.objects.create(name="Test Author") author = models.Author.objects.create(name="Test Author")
result = self.connector.get_or_create_author(author.remote_id) result = self.connector.get_or_create_author(author.remote_id)
self.assertEqual(author, result) 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")

View file

@ -306,3 +306,11 @@ class Inventaire(TestCase):
extract = self.connector.get_description({"enwiki": "test_path"}) extract = self.connector.get_description({"enwiki": "test_path"})
self.assertEqual(extract, "hi hi") 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",
)

View file

@ -98,6 +98,9 @@ class Openlibrary(TestCase):
"type": "/type/datetime", "type": "/type/datetime",
"value": "2008-08-31 10:09:33.413686", "value": "2008-08-31 10:09:33.413686",
}, },
"remote_ids": {
"isni": "000111",
},
"key": "/authors/OL453734A", "key": "/authors/OL453734A",
"type": {"key": "/type/author"}, "type": {"key": "/type/author"},
"id": 1259965, "id": 1259965,
@ -110,6 +113,7 @@ class Openlibrary(TestCase):
self.assertIsInstance(result, models.Author) self.assertIsInstance(result, models.Author)
self.assertEqual(result.name, "George Elliott") self.assertEqual(result.name, "George Elliott")
self.assertEqual(result.openlibrary_key, "OL453734A") self.assertEqual(result.openlibrary_key, "OL453734A")
self.assertEqual(result.isni, "000111")
def test_get_cover_url(self): def test_get_cover_url(self):
"""formats a url that should contain the cover image""" """formats a url that should contain the cover image"""

View file

@ -209,6 +209,28 @@ class BookViews(TestCase):
self.assertEqual(self.book.description, "new description hi") self.assertEqual(self.book.description, "new description hi")
self.assertEqual(self.book.last_edited_by, self.local_user) 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(): def _setup_cover_url():
"""creates cover url mock""" """creates cover url mock"""

View file

@ -148,3 +148,26 @@ class AuthorViews(TestCase):
self.assertEqual(author.name, "Test Author") self.assertEqual(author.name, "Test Author")
validate_html(resp.render()) validate_html(resp.render())
self.assertEqual(resp.status_code, 200) 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)

View file

@ -4,10 +4,12 @@ from unittest.mock import patch
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
@patch("bookwyrm.activitystreams.add_user_statuses_task.delay") @patch("bookwyrm.activitystreams.add_user_statuses_task.delay")
@ -16,6 +18,7 @@ class FollowViews(TestCase):
def setUp(self): def setUp(self):
"""we need basic test data and mocks""" """we need basic test data and mocks"""
models.SiteSettings.objects.create()
self.factory = RequestFactory() self.factory = RequestFactory()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay" "bookwyrm.activitystreams.populate_stream_task.delay"
@ -174,3 +177,43 @@ class FollowViews(TestCase):
self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0) self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0)
# follow relationship should not exist # follow relationship should not exist
self.assertEqual(models.UserFollows.objects.filter(id=rel.id).count(), 0) 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)

View file

@ -44,6 +44,7 @@ urlpatterns = [
re_path(r"^api/v1/instance/?$", views.instance_info), re_path(r"^api/v1/instance/?$", views.instance_info),
re_path(r"^api/v1/instance/peers/?$", views.peers), re_path(r"^api/v1/instance/peers/?$", views.peers),
re_path(r"^opensearch.xml$", views.opensearch, name="opensearch"), re_path(r"^opensearch.xml$", views.opensearch, name="opensearch"),
re_path(r"^ostatus_subscribe/?$", views.ostatus_follow_request),
# polling updates # polling updates
re_path("^api/updates/notifications/?$", views.get_notification_count), re_path("^api/updates/notifications/?$", views.get_notification_count),
re_path("^api/updates/stream/(?P<stream>[a-z]+)/?$", views.get_unread_status_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" 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"^add-description/(?P<book_id>\d+)/?$", views.add_description),
re_path(r"^resolve-book/?$", views.resolve_book), re_path(r"^resolve-book/?$", views.resolve_book, name="resolve-book"),
re_path(r"^switch-edition/?$", views.switch_edition), 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 # isbn
re_path(r"^isbn/(?P<isbn>\d+)(.json)?/?$", views.Isbn.as_view()), re_path(r"^isbn/(?P<isbn>\d+)(.json)?/?$", views.Isbn.as_view()),
# author # author
re_path(r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view()), re_path(
re_path(r"^author/(?P<author_id>\d+)/edit/?$", views.EditAuthor.as_view()), 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 # reading progress
re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"), re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"),
re_path(r"^delete-readthrough/?$", views.delete_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"^unfollow/?$", views.unfollow, name="unfollow"),
re_path(r"^accept-follow-request/?$", views.accept_follow_request), re_path(r"^accept-follow-request/?$", views.accept_follow_request),
re_path(r"^delete-follow-request/?$", views.delete_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) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -29,6 +29,7 @@ from .preferences.block import Block, unblock
# books # books
from .books.books import Book, upload_cover, add_description, resolve_book 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.edit_book import EditBook, ConfirmEditBook
from .books.editions import Editions, switch_edition from .books.editions import Editions, switch_edition
@ -54,11 +55,18 @@ from .imports.manually_review import (
) )
# misc views # misc views
from .author import Author, EditAuthor from .author import Author, EditAuthor, update_author_from_remote
from .directory import Directory from .directory import Directory
from .discover import Discover from .discover import Discover
from .feed import DirectMessage, Feed, Replies, Status 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 .follow import accept_follow_request, delete_follow_request
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
from .goal import Goal, hide_goal from .goal import Goal, hide_goal

View file

@ -6,9 +6,11 @@ from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views.helpers import is_api_request from bookwyrm.views.helpers import is_api_request
@ -73,3 +75,19 @@ class EditAuthor(View):
author = form.save() author = form.save()
return redirect(f"/author/{author.id}") 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)

View file

@ -178,3 +178,19 @@ def resolve_book(request):
book = connector.get_or_create_book(remote_id) book = connector.get_or_create_book(remote_id)
return redirect("book", book.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)

View file

@ -1,11 +1,19 @@
""" views for actions you can take in the application """ """ views for actions you can take in the application """
import urllib.parse
import re
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db import IntegrityError from django.db import IntegrityError
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from bookwyrm import models 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 @login_required
@ -23,6 +31,9 @@ def follow(request):
except IntegrityError: except IntegrityError:
pass pass
if request.GET.get("next"):
return redirect(request.GET.get("next", "/"))
return redirect(to_follow.local_path) return redirect(to_follow.local_path)
@ -84,3 +95,91 @@ def delete_follow_request(request):
follow_request.delete() follow_request.delete()
return redirect(f"/user/{request.user.localname}") 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)

View file

@ -16,6 +16,13 @@ from bookwyrm.status import create_generated_note
from bookwyrm.utils import regex 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): def get_user_from_username(viewer, username):
"""helper function to resolve a localname or a username to a user""" """helper function to resolve a localname or a username to a user"""
if viewer.is_authenticated and viewer.localname == username: 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 # usernames could be @user@domain or user@domain
if not query: if not query:
return None return None
if query[0] == "@": if query[0] == "@":
query = query[1:] query = query[1:]
try: try:
domain = query.split("@")[1] domain = query.split("@")[1]
except IndexError: except IndexError:
@ -86,6 +91,35 @@ def handle_remote_webfinger(query):
return user 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): def get_edition(book_id):
"""look up a book in the db and return an edition""" """look up a book in the db and return an edition"""
book = models.Book.objects.select_subclasses().get(id=book_id) book = models.Book.objects.select_subclasses().get(id=book_id)

View file

@ -30,7 +30,11 @@ def webfinger(request):
"rel": "self", "rel": "self",
"type": "application/activity+json", "type": "application/activity+json",
"href": user.remote_id, "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

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 0.0.1\n" "Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2021-02-28 17:19-0800\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n" "Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: English <LL@li.org>\n" "Language-Team: English <LL@li.org>\n"
@ -18,58 +18,58 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\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." msgid "A user with this email already exists."
msgstr "" msgstr ""
#: bookwyrm/forms.py:364 #: bookwyrm/forms.py:365
msgid "One Day" msgid "One Day"
msgstr "" msgstr ""
#: bookwyrm/forms.py:365 #: bookwyrm/forms.py:366
msgid "One Week" msgid "One Week"
msgstr "" msgstr ""
#: bookwyrm/forms.py:366 #: bookwyrm/forms.py:367
msgid "One Month" msgid "One Month"
msgstr "" msgstr ""
#: bookwyrm/forms.py:367 #: bookwyrm/forms.py:368
msgid "Does Not Expire" msgid "Does Not Expire"
msgstr "" msgstr ""
#: bookwyrm/forms.py:371 #: bookwyrm/forms.py:372
#, python-brace-format #, python-brace-format
msgid "{i} uses" msgid "{i} uses"
msgstr "" msgstr ""
#: bookwyrm/forms.py:372 #: bookwyrm/forms.py:373
msgid "Unlimited" msgid "Unlimited"
msgstr "" msgstr ""
#: bookwyrm/forms.py:468 #: bookwyrm/forms.py:469
msgid "List Order" msgid "List Order"
msgstr "" msgstr ""
#: bookwyrm/forms.py:469 #: bookwyrm/forms.py:470
msgid "Book Title" msgid "Book Title"
msgstr "" 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/shelf/shelf.html:184
#: bookwyrm/templates/snippets/create_status/review.html:33 #: bookwyrm/templates/snippets/create_status/review.html:33
msgid "Rating" msgid "Rating"
msgstr "" msgstr ""
#: bookwyrm/forms.py:472 bookwyrm/templates/lists/list.html:110 #: bookwyrm/forms.py:473 bookwyrm/templates/lists/list.html:110
msgid "Sort By" msgid "Sort By"
msgstr "" msgstr ""
#: bookwyrm/forms.py:476 #: bookwyrm/forms.py:477
msgid "Ascending" msgid "Ascending"
msgstr "" msgstr ""
#: bookwyrm/forms.py:477 #: bookwyrm/forms.py:478
msgid "Descending" msgid "Descending"
msgstr "" msgstr ""
@ -102,23 +102,23 @@ msgstr ""
msgid "Domain block" msgid "Domain block"
msgstr "" msgstr ""
#: bookwyrm/models/book.py:233 #: bookwyrm/models/book.py:243
msgid "Audiobook" msgid "Audiobook"
msgstr "" msgstr ""
#: bookwyrm/models/book.py:234 #: bookwyrm/models/book.py:244
msgid "eBook" msgid "eBook"
msgstr "" msgstr ""
#: bookwyrm/models/book.py:235 #: bookwyrm/models/book.py:245
msgid "Graphic novel" msgid "Graphic novel"
msgstr "" msgstr ""
#: bookwyrm/models/book.py:236 #: bookwyrm/models/book.py:246
msgid "Hardcover" msgid "Hardcover"
msgstr "" msgstr ""
#: bookwyrm/models/book.py:237 #: bookwyrm/models/book.py:247
msgid "Paperback" msgid "Paperback"
msgstr "" msgstr ""
@ -146,6 +146,7 @@ msgid "%(value)s is not a valid username"
msgstr "" msgstr ""
#: bookwyrm/models/fields.py:183 bookwyrm/templates/layout.html:171 #: bookwyrm/models/fields.py:183 bookwyrm/templates/layout.html:171
#: bookwyrm/templates/ostatus/error.html:29
msgid "username" msgid "username"
msgstr "" msgstr ""
@ -153,7 +154,7 @@ msgstr ""
msgid "A user with that username already exists." msgid "A user with that username already exists."
msgstr "" 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" msgid "Reviews"
msgstr "" msgstr ""
@ -183,7 +184,7 @@ msgstr ""
#: bookwyrm/settings.py:119 bookwyrm/templates/search/layout.html:21 #: bookwyrm/settings.py:119 bookwyrm/templates/search/layout.html:21
#: bookwyrm/templates/search/layout.html:42 #: bookwyrm/templates/search/layout.html:42
#: bookwyrm/templates/user/layout.html:88 #: bookwyrm/templates/user/layout.html:91
msgid "Books" msgid "Books"
msgstr "" msgstr ""
@ -248,46 +249,61 @@ msgstr ""
msgid "Edit Author" msgid "Edit Author"
msgstr "" 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 #: bookwyrm/templates/author/edit_author.html:42
msgid "Aliases:" msgid "Aliases:"
msgstr "" msgstr ""
#: bookwyrm/templates/author/author.html:46 #: bookwyrm/templates/author/author.html:53
msgid "Born:" msgid "Born:"
msgstr "" msgstr ""
#: bookwyrm/templates/author/author.html:53 #: bookwyrm/templates/author/author.html:60
msgid "Died:" msgid "Died:"
msgstr "" 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" msgid "Wikipedia"
msgstr "" msgstr ""
#: bookwyrm/templates/author/author.html:70 #: bookwyrm/templates/author/author.html:83
msgid "View ISNI record" msgid "View ISNI record"
msgstr "" msgstr ""
#: bookwyrm/templates/author/author.html:78 #: bookwyrm/templates/author/author.html:88
#: bookwyrm/templates/book/book.html:94 #: 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" msgid "View on OpenLibrary"
msgstr "" msgstr ""
#: bookwyrm/templates/author/author.html:86 #: bookwyrm/templates/author/author.html:106
#: bookwyrm/templates/book/book.html:97 #: bookwyrm/templates/book/book.html:107
msgid "View on Inventaire" msgid "View on Inventaire"
msgstr "" msgstr ""
#: bookwyrm/templates/author/author.html:94 #: bookwyrm/templates/author/author.html:121
msgid "View on LibraryThing" msgid "View on LibraryThing"
msgstr "" msgstr ""
#: bookwyrm/templates/author/author.html:102 #: bookwyrm/templates/author/author.html:129
msgid "View on Goodreads" msgid "View on Goodreads"
msgstr "" msgstr ""
#: bookwyrm/templates/author/author.html:117 #: bookwyrm/templates/author/author.html:143
#, python-format #, python-format
msgid "Books by %(name)s" msgid "Books by %(name)s"
msgstr "" msgstr ""
@ -364,8 +380,12 @@ msgstr ""
msgid "Goodreads key:" msgid "Goodreads key:"
msgstr "" msgstr ""
#: bookwyrm/templates/author/edit_author.html:108 #: bookwyrm/templates/author/edit_author.html:105
#: bookwyrm/templates/book/book.html:140 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/edit/edit_book.html:121
#: bookwyrm/templates/book/readthrough.html:76 #: bookwyrm/templates/book/readthrough.html:76
#: bookwyrm/templates/groups/form.html:24 #: bookwyrm/templates/groups/form.html:24
@ -382,12 +402,14 @@ msgstr ""
msgid "Save" msgid "Save"
msgstr "" msgstr ""
#: bookwyrm/templates/author/edit_author.html:109 #: bookwyrm/templates/author/edit_author.html:116
#: bookwyrm/templates/book/book.html:141 bookwyrm/templates/book/book.html:199 #: 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/cover_modal.html:32
#: bookwyrm/templates/book/edit/edit_book.html:123 #: bookwyrm/templates/book/edit/edit_book.html:123
#: bookwyrm/templates/book/edit/edit_book.html:126 #: bookwyrm/templates/book/edit/edit_book.html:126
#: bookwyrm/templates/book/readthrough.html:77 #: bookwyrm/templates/book/readthrough.html:77
#: bookwyrm/templates/book/sync_modal.html:26
#: bookwyrm/templates/groups/delete_group_modal.html:17 #: bookwyrm/templates/groups/delete_group_modal.html:17
#: bookwyrm/templates/lists/delete_list_modal.html:17 #: bookwyrm/templates/lists/delete_list_modal.html:17
#: bookwyrm/templates/settings/federation/instance.html:88 #: bookwyrm/templates/settings/federation/instance.html:88
@ -396,6 +418,20 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" 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/book/book.html:47
#: bookwyrm/templates/discover/large-book.html:22 #: bookwyrm/templates/discover/large-book.html:22
#: bookwyrm/templates/landing/large-book.html:25 #: bookwyrm/templates/landing/large-book.html:25
@ -416,86 +452,86 @@ msgstr ""
msgid "Failed to load cover" msgid "Failed to load cover"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:117 #: bookwyrm/templates/book/book.html:134
#, python-format #, python-format
msgid "(%(review_count)s review)" msgid "(%(review_count)s review)"
msgid_plural "(%(review_count)s reviews)" msgid_plural "(%(review_count)s reviews)"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: bookwyrm/templates/book/book.html:129 #: bookwyrm/templates/book/book.html:146
msgid "Add Description" msgid "Add Description"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:136 #: bookwyrm/templates/book/book.html:153
#: bookwyrm/templates/book/edit/edit_book_form.html:39 #: bookwyrm/templates/book/edit/edit_book_form.html:39
#: bookwyrm/templates/lists/form.html:13 bookwyrm/templates/shelf/form.html:17 #: bookwyrm/templates/lists/form.html:13 bookwyrm/templates/shelf/form.html:17
msgid "Description:" msgid "Description:"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:150 #: bookwyrm/templates/book/book.html:167
#, python-format #, python-format
msgid "<a href=\"%(path)s/editions\">%(count)s editions</a>" msgid "<a href=\"%(path)s/editions\">%(count)s editions</a>"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:158 #: bookwyrm/templates/book/book.html:175
msgid "You have shelved this edition in:" msgid "You have shelved this edition in:"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:173 #: bookwyrm/templates/book/book.html:190
#, python-format #, 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." 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 "" msgstr ""
#: bookwyrm/templates/book/book.html:184 #: bookwyrm/templates/book/book.html:201
msgid "Your reading activity" msgid "Your reading activity"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:187 #: bookwyrm/templates/book/book.html:204
msgid "Add read dates" msgid "Add read dates"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:196 #: bookwyrm/templates/book/book.html:213
msgid "Create" msgid "Create"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:206 #: bookwyrm/templates/book/book.html:223
msgid "You don't have any reading activity for this book." msgid "You don't have any reading activity for this book."
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:232 #: bookwyrm/templates/book/book.html:249
msgid "Your reviews" msgid "Your reviews"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:238 #: bookwyrm/templates/book/book.html:255
msgid "Your comments" msgid "Your comments"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:244 #: bookwyrm/templates/book/book.html:261
msgid "Your quotes" msgid "Your quotes"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:280 #: bookwyrm/templates/book/book.html:297
msgid "Subjects" msgid "Subjects"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:292 #: bookwyrm/templates/book/book.html:309
msgid "Places" msgid "Places"
msgstr "" 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/lists/lists.html:5 bookwyrm/templates/lists/lists.html:12
#: bookwyrm/templates/search/layout.html:25 #: bookwyrm/templates/search/layout.html:25
#: bookwyrm/templates/search/layout.html:50 #: bookwyrm/templates/search/layout.html:50
#: bookwyrm/templates/user/layout.html:82 #: bookwyrm/templates/user/layout.html:85
msgid "Lists" msgid "Lists"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:314 #: bookwyrm/templates/book/book.html:331
msgid "Add to list" msgid "Add to list"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:324 #: bookwyrm/templates/book/book.html:341
#: bookwyrm/templates/book/cover_modal.html:31 #: bookwyrm/templates/book/cover_modal.html:31
#: bookwyrm/templates/lists/list.html:182 #: bookwyrm/templates/lists/list.html:182
#: bookwyrm/templates/settings/email_blocklist/domain_form.html:24 #: bookwyrm/templates/settings/email_blocklist/domain_form.html:24
@ -573,13 +609,6 @@ msgstr ""
msgid "This is a new work" msgid "This is a new work"
msgstr "" 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/book/edit/edit_book.html:110
#: bookwyrm/templates/feed/status.html:21 #: bookwyrm/templates/feed/status.html:21
msgid "Back" msgid "Back"
@ -779,6 +808,11 @@ msgstr ""
msgid "Delete these read dates" msgid "Delete these read dates"
msgstr "" 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/inline_form.html:8
#: bookwyrm/templates/components/modal.html:11 #: bookwyrm/templates/components/modal.html:11
#: bookwyrm/templates/components/tooltip.html:7 #: bookwyrm/templates/components/tooltip.html:7
@ -898,6 +932,12 @@ msgstr ""
#: bookwyrm/templates/directory/user_card.html:17 #: bookwyrm/templates/directory/user_card.html:17
#: bookwyrm/templates/directory/user_card.html:18 #: 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:16
#: bookwyrm/templates/user/user_preview.html:17 #: bookwyrm/templates/user/user_preview.html:17
msgid "Locked account" msgid "Locked account"
@ -1717,6 +1757,7 @@ msgstr ""
#: bookwyrm/templates/landing/login.html:7 #: bookwyrm/templates/landing/login.html:7
#: bookwyrm/templates/landing/login.html:36 bookwyrm/templates/layout.html:179 #: bookwyrm/templates/landing/login.html:36 bookwyrm/templates/layout.html:179
#: bookwyrm/templates/ostatus/error.html:37
msgid "Log in" msgid "Log in"
msgstr "" msgstr ""
@ -1725,18 +1766,20 @@ msgid "Success! Email address confirmed."
msgstr "" msgstr ""
#: bookwyrm/templates/landing/login.html:21 bookwyrm/templates/layout.html:170 #: bookwyrm/templates/landing/login.html:21 bookwyrm/templates/layout.html:170
#: bookwyrm/templates/ostatus/error.html:28
#: bookwyrm/templates/snippets/register_form.html:4 #: bookwyrm/templates/snippets/register_form.html:4
msgid "Username:" msgid "Username:"
msgstr "" msgstr ""
#: bookwyrm/templates/landing/login.html:27 #: bookwyrm/templates/landing/login.html:27
#: bookwyrm/templates/landing/password_reset.html:26 #: 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 #: bookwyrm/templates/snippets/register_form.html:20
msgid "Password:" msgid "Password:"
msgstr "" msgstr ""
#: bookwyrm/templates/landing/login.html:39 bookwyrm/templates/layout.html:176 #: bookwyrm/templates/landing/login.html:39 bookwyrm/templates/layout.html:176
#: bookwyrm/templates/ostatus/error.html:34
msgid "Forgot your password?" msgid "Forgot your password?"
msgstr "" msgstr ""
@ -1801,7 +1844,7 @@ msgstr ""
msgid "Notifications" msgid "Notifications"
msgstr "" msgstr ""
#: bookwyrm/templates/layout.html:175 #: bookwyrm/templates/layout.html:175 bookwyrm/templates/ostatus/error.html:33
msgid "password" msgid "password"
msgstr "" msgstr ""
@ -2193,6 +2236,120 @@ msgstr ""
msgid "You're all caught up!" msgid "You're all caught up!"
msgstr "" 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:4
#: bookwyrm/templates/preferences/blocks.html:7 #: bookwyrm/templates/preferences/blocks.html:7
#: bookwyrm/templates/preferences/layout.html:31 #: bookwyrm/templates/preferences/layout.html:31
@ -2618,7 +2775,7 @@ msgid "Details"
msgstr "" msgstr ""
#: bookwyrm/templates/settings/federation/instance.html:35 #: bookwyrm/templates/settings/federation/instance.html:35
#: bookwyrm/templates/user/layout.html:64 #: bookwyrm/templates/user/layout.html:67
msgid "Activity" msgid "Activity"
msgstr "" msgstr ""
@ -3260,6 +3417,7 @@ msgid "Posted by <a href=\"%(user_path)s\">%(username)s</a>"
msgstr "" msgstr ""
#: bookwyrm/templates/snippets/authors.html:22 #: bookwyrm/templates/snippets/authors.html:22
#: bookwyrm/templates/snippets/trimmed_list.html:14
#, python-format #, python-format
msgid "and %(remainder_count_display)s other" msgid "and %(remainder_count_display)s other"
msgid_plural "and %(remainder_count_display)s others" msgid_plural "and %(remainder_count_display)s others"
@ -3847,15 +4005,15 @@ msgstr ""
msgid "User Profile" msgid "User Profile"
msgstr "" msgstr ""
#: bookwyrm/templates/user/layout.html:45 #: bookwyrm/templates/user/layout.html:48
msgid "Follow Requests" msgid "Follow Requests"
msgstr "" msgstr ""
#: bookwyrm/templates/user/layout.html:70 #: bookwyrm/templates/user/layout.html:73
msgid "Reading Goal" msgid "Reading Goal"
msgstr "" msgstr ""
#: bookwyrm/templates/user/layout.html:76 #: bookwyrm/templates/user/layout.html:79
msgid "Groups" msgid "Groups"
msgstr "" msgstr ""
@ -3945,7 +4103,7 @@ msgstr ""
msgid "File exceeds maximum size: 10MB" msgid "File exceeds maximum size: 10MB"
msgstr "" msgstr ""
#: bookwyrm/templatetags/utilities.py:34 #: bookwyrm/templatetags/utilities.py:33
#, python-format #, python-format
msgid "%(title)s: %(subtitle)s" msgid "%(title)s: %(subtitle)s"
msgstr "" 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