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
# 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):

View file

@ -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"""

View file

@ -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:

View file

@ -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}"

View file

@ -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"""

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="&#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="&#xea36;" 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

View file

@ -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";
}

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

View file

@ -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 %},&nbsp;{% 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 %}

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>
{% 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>

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>
{% block scripts %}{% endblock %}
</body>
</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 %}
{% 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">

View file

@ -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))

View file

@ -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")

View file

@ -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",
)

View file

@ -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"""

View file

@ -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"""

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

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

View file

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