Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-06-17 19:17:43 -07:00
commit fd0e4c6e13
22 changed files with 188 additions and 130 deletions

View file

@ -201,6 +201,19 @@ def add_status_on_create(sender, instance, created, *args, **kwargs):
for stream in streams.values():
stream.add_status(instance)
if sender != models.Boost:
return
# remove the original post and other, earlier boosts
boosted = instance.boost.boosted_status
old_versions = models.Boost.objects.filter(
boosted_status__id=boosted.id,
created_date__lt=instance.created_date,
)
for stream in streams.values():
stream.remove_object_from_related_stores(boosted)
for status in old_versions:
stream.remove_object_from_related_stores(status)
@receiver(signals.post_delete, sender=models.Boost)
# pylint: disable=unused-argument
@ -208,7 +221,10 @@ def remove_boost_on_delete(sender, instance, *args, **kwargs):
"""boosts are deleted"""
# we're only interested in new statuses
for stream in streams.values():
# remove the boost
stream.remove_object_from_related_stores(instance)
# re-add the original status
stream.add_status(instance.boosted_status)
@receiver(signals.post_save, sender=models.UserFollows)

View file

@ -37,7 +37,7 @@ class AbstractMinimalConnector(ABC):
for field in self_fields:
setattr(self, field, getattr(info, field))
def search(self, query, min_confidence=None):
def search(self, query, min_confidence=None, timeout=5):
"""free text search"""
params = {}
if min_confidence:
@ -46,6 +46,7 @@ class AbstractMinimalConnector(ABC):
data = self.get_search_data(
"%s%s" % (self.search_url, query),
params=params,
timeout=timeout,
)
results = []
@ -218,7 +219,7 @@ def dict_from_mappings(data, mappings):
return result
def get_data(url, params=None):
def get_data(url, params=None, timeout=10):
"""wrapper for request.get"""
# check if the url is blocked
if models.FederatedServer.is_blocked(url):
@ -234,6 +235,7 @@ def get_data(url, params=None):
"Accept": "application/json; charset=utf-8",
"User-Agent": settings.USER_AGENT,
},
timeout=timeout,
)
except (RequestError, SSLError, ConnectionError) as e:
logger.exception(e)
@ -250,7 +252,7 @@ def get_data(url, params=None):
return data
def get_image(url):
def get_image(url, timeout=10):
"""wrapper for requesting an image"""
try:
resp = requests.get(
@ -258,6 +260,7 @@ def get_image(url):
headers={
"User-Agent": settings.USER_AGENT,
},
timeout=timeout,
)
except (RequestError, SSLError) as e:
logger.exception(e)

View file

@ -1,4 +1,5 @@
""" interface with whatever connectors the app has """
from datetime import datetime
import importlib
import logging
import re
@ -29,9 +30,11 @@ def search(query, min_confidence=0.1, return_first=False):
isbn = re.sub(r"[\W_]", "", query)
maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13
timeout = 15
start_time = datetime.now()
for connector in get_connectors():
result_set = None
if maybe_isbn and connector.isbn_search_url and connector.isbn_search_url == "":
if maybe_isbn and connector.isbn_search_url and connector.isbn_search_url != "":
# Search on ISBN
try:
result_set = connector.isbn_search(isbn)
@ -59,6 +62,8 @@ def search(query, min_confidence=0.1, return_first=False):
"results": result_set,
}
)
if (datetime.now() - start_time).seconds >= timeout:
break
if return_first:
return None

View file

@ -3,7 +3,7 @@ from functools import reduce
import operator
from django.contrib.postgres.search import SearchRank, SearchVector
from django.db.models import Count, OuterRef, Subquery, F, Q
from django.db.models import OuterRef, Subquery, F, Q
from bookwyrm import models
from .abstract_connector import AbstractConnector, SearchResult
@ -122,6 +122,8 @@ def search_identifiers(query, *filters):
results = models.Edition.objects.filter(
*filters, reduce(operator.or_, (Q(**f) for f in or_filters))
).distinct()
if results.count() <= 1:
return results
# when there are multiple editions of the same work, pick the default.
# it would be odd for this to happen.
@ -146,19 +148,15 @@ def search_title_author(query, min_confidence, *filters):
)
results = (
models.Edition.objects.annotate(search=vector)
.annotate(rank=SearchRank(vector, query))
models.Edition.objects.annotate(rank=SearchRank(vector, query))
.filter(*filters, rank__gt=min_confidence)
.order_by("-rank")
)
# when there are multiple editions of the same work, pick the closest
editions_of_work = (
results.values("parent_work")
.annotate(Count("parent_work"))
.values_list("parent_work")
)
editions_of_work = results.values("parent_work__id").values_list("parent_work__id")
# filter out multiple editions of the same work
for work_id in set(editions_of_work):
editions = results.filter(parent_work=work_id)
default = editions.order_by("-edition_rank").first()

View file

@ -1,5 +1,6 @@
""" activitypub-aware django model fields """
from dataclasses import MISSING
import imghdr
import re
from uuid import uuid4
@ -9,7 +10,7 @@ from django.contrib.postgres.fields import ArrayField as DjangoArrayField
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.db import models
from django.forms import ClearableFileInput, ImageField
from django.forms import ClearableFileInput, ImageField as DjangoImageField
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from bookwyrm import activitypub
@ -334,10 +335,14 @@ class TagField(ManyToManyField):
class ClearableFileInputWithWarning(ClearableFileInput):
"""max file size warning"""
template_name = "widgets/clearable_file_input_with_warning.html"
class CustomImageField(ImageField):
class CustomImageField(DjangoImageField):
"""overwrites image field for form"""
widget = ClearableFileInputWithWarning
@ -400,11 +405,12 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
if not response:
return None
image_name = str(uuid4()) + "." + url.split(".")[-1]
image_content = ContentFile(response.content)
image_name = str(uuid4()) + "." + imghdr.what(None, image_content.read())
return [image_name, image_content]
def formfield(self, **kwargs):
"""special case for forms"""
return super().formfield(
**{
"form_class": CustomImageField,

View file

@ -15,7 +15,7 @@
<div class="column is-narrow">
<a href="{{ author.local_path }}/edit">
<span class="icon icon-pencil" title="{% trans 'Edit Author' %}" aria-hidden="True"></span>
<span>{% trans "Edit Author" %}</span>
<span class="is-hidden-mobile">{% trans "Edit Author" %}</span>
</a>
</div>
{% endif %}

View file

@ -8,27 +8,36 @@
<div class="block" itemscope itemtype="https://schema.org/Book">
<div class="columns is-mobile">
<div class="column">
<h1 class="title">
<span itemprop="name">
{{ book.title }}{% if book.subtitle %}:
<small>{{ book.subtitle }}</small>
{% endif %}
<h1 class="title" itemprop="name">
{{ book.title }}
</h1>
{% if book.subtitle or book.series %}
<p class="subtitle title is-5">
{% if book.subtitle %}
<meta
itemprop="alternativeHeadline"
content="{{ book.subtitle | escape }}"
>
<span class="has-text-weight-bold">
{{ book.subtitle }}
</span>
{% endif %}
{% if book.series %}
<meta itemprop="isPartOf" content="{{ book.series }}">
<meta itemprop="isPartOf" content="{{ book.series | escape }}">
<meta itemprop="volumeNumber" content="{{ book.series_number }}">
<small class="has-text-grey-dark">
({{ book.series }}{% if book.series_number %} #{{ book.series_number }}{% endif %})
</small>
<br>
{% endif %}
</h1>
</p>
{% endif %}
{% if book.authors %}
<h2 class="subtitle">
<div class="subtitle">
{% trans "by" %} {% include 'snippets/authors.html' with book=book %}
</h2>
</div>
{% endif %}
</div>
@ -36,7 +45,7 @@
<div class="column is-narrow">
<a href="{{ book.id }}/edit">
<span class="icon icon-pencil" title="{% trans "Edit Book" %}" aria-hidden=True></span>
<span>{% trans "Edit Book" %}</span>
<span class="is-hidden-mobile">{% trans "Edit Book" %}</span>
</a>
</div>
{% endif %}
@ -84,7 +93,7 @@
<div class="column is-three-fifths">
<div class="block">
<h3
<div
class="field is-grouped"
itemprop="aggregateRating"
itemscope
@ -102,7 +111,7 @@
{% plural %}
({{ review_count }} reviews)
{% endblocktrans %}
</h3>
</div>
{% with full=book|book_description itemprop='abstract' %}
{% include 'snippets/trimmed_text.html' %}
@ -180,7 +189,7 @@
<p>{% trans "You don't have any reading activity for this book." %}</p>
{% endif %}
{% for readthrough in readthroughs %}
{% include 'snippets/readthrough.html' with readthrough=readthrough %}
{% include 'book/readthrough.html' with readthrough=readthrough %}
{% endfor %}
</section>
<hr aria-hidden="true">

View file

@ -40,7 +40,7 @@
</div>
<div class="column is-narrow">
{% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True %}
{% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True right=True %}
</div>
</div>
{% endfor %}

View file

@ -1,8 +1,8 @@
{% load i18n %}
{% load humanize %}
{% load tz %}
<div class="content box is-shadowless has-background-white-bis">
<div id="hide-edit-readthrough-{{ readthrough.id }}">
<div class="content">
<div id="hide-edit-readthrough-{{ readthrough.id }}" class="box is-shadowless has-background-white-bis">
<div class="columns">
<div class="column">
{% trans "Progress Updates:" %}

View file

@ -20,8 +20,8 @@
{% csrf_token %}
<button class="button is-primary" type="submit">Join Directory</button>
<p class="help">
{% url 'settings-profile' as path %}
{% blocktrans %}You can opt-out at any time in your <a href="{{ path }}">profile settings.</a>{% endblocktrans %}
{% url 'prefs-profile' as path %}
{% blocktrans with path=path %}You can opt-out at any time in your <a href="{{ path }}">profile settings.</a>{% endblocktrans %}
</p>
</form>
</div>

View file

@ -56,7 +56,7 @@
<span class="icon icon-warning"></span>
{% endif %}
</div>
<div class="column">
<div class="column is-clipped">
<div class="block">
<p>
{# DESCRIPTION #}
@ -137,7 +137,7 @@
{# PREVIEW #}
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white{% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %} has-text-black{% else %}-bis has-text-grey-dark{% endif %}{% endif %}">
<div class="columns">
<div class="column">
<div class="column is-clipped">
{% include 'snippets/status_preview.html' with status=related_status %}
</div>
<div class="column is-narrow {% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %}has-text-black{% else %}has-text-grey-dark{% endif %}">

View file

@ -7,7 +7,7 @@
{% block edit-button %}
<a href="{% url 'settings-import-blocklist' %}">
<span class="icon icon-plus" title="{% trans 'Add instance' %}" aria-hidden="True"></span>
<span>{% trans "Add instance" %}</span>
<span class="is-hidden-mobile">{% trans "Add instance" %}</span>
</a>
{% endblock %}

View file

@ -11,7 +11,7 @@
class="is-sr-only"
type="radio"
name="rating"
value="0"
value=""
{% if default_rating == 0 or not default_rating %}checked{% endif %}
>

View file

@ -13,7 +13,7 @@
<div class="column is-narrow">
<a href="{% url 'prefs-profile' %}">
<span class="icon icon-pencil" title="Edit profile" aria-hidden="true"></span>
<span>{% trans "Edit profile" %}</span>
<span class="is-hidden-mobile">{% trans "Edit profile" %}</span>
</a>
</div>
{% endif %}
@ -26,7 +26,7 @@
<h2 class="title">
{% include 'user/shelf/books_header.html' %}
</h2>
<div class="columns">
<div class="columns is-mobile scroll-x">
{% for shelf in shelves %}
<div class="column is-narrow">
<h3>{{ shelf.name }}
@ -60,7 +60,7 @@
<div class="column is-narrow">
<a target="_blank" href="{{ user.local_path }}/rss">
<span class="icon icon-rss" aria-hidden="true"></span>
<span>{% trans "RSS feed" %}</span>
<span class="is-hidden-mobile">{% trans "RSS feed" %}</span>
</a>
</div>
</div>

View file

@ -2,6 +2,7 @@
import os
from uuid import uuid4
from django import template
from django.utils.translation import gettext_lazy as _
register = template.Library()
@ -20,13 +21,16 @@ def get_user_identifier(user):
@register.filter(name="book_title")
def get_title(book):
def get_title(book, too_short=5):
"""display the subtitle if the title is short"""
if not book:
return ""
title = book.title
if len(title) < 6 and book.subtitle:
title = "{:s}: {:s}".format(title, book.subtitle)
if len(title) <= too_short and book.subtitle:
title = _("%(title)s: %(subtitle)s") % {
"title": title,
"subtitle": book.subtitle,
}
return title

View file

@ -16,6 +16,7 @@ from bookwyrm import activitypub, models, settings
# pylint: disable=too-many-public-methods
@patch("bookwyrm.models.Status.broadcast")
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
@patch("bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores")
class Status(TestCase):
"""lotta types of statuses"""
@ -384,7 +385,8 @@ class Status(TestCase):
user=self.local_user, notification_type="GLORB"
)
def test_create_broadcast(self, _, broadcast_mock):
# pylint: disable=unused-argument
def test_create_broadcast(self, one, two, broadcast_mock, *_):
"""should send out two verions of a status on create"""
models.Comment.objects.create(
content="hi", user=self.local_user, book=self.book

View file

@ -16,6 +16,7 @@ from bookwyrm.templatetags import (
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
@patch("bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores")
class TemplateTags(TestCase):
"""lotta different things here"""
@ -38,28 +39,28 @@ class TemplateTags(TestCase):
)
self.book = models.Edition.objects.create(title="Test Book")
def test_get_user_rating(self, _):
def test_get_user_rating(self, *_):
"""get a user's most recent rating of a book"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.Review.objects.create(user=self.user, book=self.book, rating=3)
self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 3)
def test_get_user_rating_doesnt_exist(self, _):
def test_get_user_rating_doesnt_exist(self, *_):
"""there is no rating available"""
self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 0)
def test_get_user_identifer_local(self, _):
def test_get_user_identifer_local(self, *_):
"""fall back to the simplest uid available"""
self.assertNotEqual(self.user.username, self.user.localname)
self.assertEqual(utilities.get_user_identifier(self.user), "mouse")
def test_get_user_identifer_remote(self, _):
def test_get_user_identifer_remote(self, *_):
"""for a remote user, should be their full username"""
self.assertEqual(
utilities.get_user_identifier(self.remote_user), "rat@example.com"
)
def test_get_replies(self, _):
def test_get_replies(self, *_):
"""direct replies to a status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
parent = models.Review.objects.create(
@ -87,7 +88,7 @@ class TemplateTags(TestCase):
self.assertTrue(second_child in replies)
self.assertFalse(third_child in replies)
def test_get_parent(self, _):
def test_get_parent(self, *_):
"""get the reply parent of a status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
parent = models.Review.objects.create(
@ -101,7 +102,7 @@ class TemplateTags(TestCase):
self.assertEqual(result, parent)
self.assertIsInstance(result, models.Review)
def test_get_user_liked(self, _):
def test_get_user_liked(self, *_):
"""did a user like a status"""
status = models.Review.objects.create(user=self.remote_user, book=self.book)
@ -110,7 +111,7 @@ class TemplateTags(TestCase):
models.Favorite.objects.create(user=self.user, status=status)
self.assertTrue(interaction.get_user_liked(self.user, status))
def test_get_user_boosted(self, _):
def test_get_user_boosted(self, *_):
"""did a user boost a status"""
status = models.Review.objects.create(user=self.remote_user, book=self.book)
@ -119,7 +120,7 @@ class TemplateTags(TestCase):
models.Boost.objects.create(user=self.user, boosted_status=status)
self.assertTrue(interaction.get_user_boosted(self.user, status))
def test_get_boosted(self, _):
def test_get_boosted(self, *_):
"""load a boosted status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Review.objects.create(user=self.remote_user, book=self.book)
@ -128,7 +129,7 @@ class TemplateTags(TestCase):
self.assertIsInstance(boosted, models.Review)
self.assertEqual(boosted, status)
def test_get_book_description(self, _):
def test_get_book_description(self, *_):
"""grab it from the edition or the parent"""
work = models.Work.objects.create(title="Test Work")
self.book.parent_work = work
@ -144,12 +145,12 @@ class TemplateTags(TestCase):
self.book.save()
self.assertEqual(bookwyrm_tags.get_book_description(self.book), "hello")
def test_get_uuid(self, _):
def test_get_uuid(self, *_):
"""uuid functionality"""
uuid = utilities.get_uuid("hi")
self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid))
def test_get_markdown(self, _):
def test_get_markdown(self, *_):
"""mardown format data"""
result = markdown.get_markdown("_hi_")
self.assertEqual(result, "<p><em>hi</em></p>")
@ -157,13 +158,13 @@ class TemplateTags(TestCase):
result = markdown.get_markdown("<marquee>_hi_</marquee>")
self.assertEqual(result, "<p><em>hi</em></p>")
def test_get_mentions(self, _):
def test_get_mentions(self, *_):
"""list of people mentioned"""
status = models.Status.objects.create(content="hi", user=self.remote_user)
result = status_display.get_mentions(status, self.user)
self.assertEqual(result, "@rat@example.com ")
def test_related_status(self, _):
def test_related_status(self, *_):
"""gets the subclass model for a notification status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(content="hi", user=self.user)

View file

@ -51,7 +51,8 @@ class InboxActivities(TestCase):
models.SiteSettings.objects.create()
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_boost(self, redis_mock):
@patch("bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores")
def test_boost(self, redis_mock, _):
"""boost a status"""
self.assertEqual(models.Notification.objects.count(), 0)
activity = {
@ -81,7 +82,8 @@ class InboxActivities(TestCase):
@responses.activate
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_boost_remote_status(self, redis_mock):
@patch("bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores")
def test_boost_remote_status(self, redis_mock, _):
"""boost a status from a remote server"""
work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create(
@ -153,9 +155,10 @@ class InboxActivities(TestCase):
views.inbox.activity_task(activity)
self.assertEqual(models.Boost.objects.count(), 0)
def test_unboost(self):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
@patch("bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores")
def test_unboost(self, *_):
"""undo a boost"""
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
boost = models.Boost.objects.create(
boosted_status=self.status, user=self.remote_user
)
@ -175,11 +178,7 @@ class InboxActivities(TestCase):
"published": "Mon, 25 May 2020 19:31:20 GMT",
},
}
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
self.assertFalse(models.Boost.objects.exists())
def test_unboost_unknown_boost(self):

View file

@ -8,6 +8,7 @@ from bookwyrm import models, views
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
@patch("bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores")
class InteractionViews(TestCase):
"""viewing and creating statuses"""
@ -40,7 +41,7 @@ class InteractionViews(TestCase):
parent_work=work,
)
def test_favorite(self, _):
def test_favorite(self, *_):
"""create and broadcast faving a status"""
view = views.Favorite.as_view()
request = self.factory.post("")
@ -58,7 +59,7 @@ class InteractionViews(TestCase):
self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.related_user, self.remote_user)
def test_unfavorite(self, _):
def test_unfavorite(self, *_):
"""unfav a status"""
view = views.Unfavorite.as_view()
request = self.factory.post("")
@ -75,7 +76,7 @@ class InteractionViews(TestCase):
self.assertEqual(models.Favorite.objects.count(), 0)
self.assertEqual(models.Notification.objects.count(), 0)
def test_boost(self, _):
def test_boost(self, *_):
"""boost a status"""
view = views.Boost.as_view()
request = self.factory.post("")
@ -97,7 +98,7 @@ class InteractionViews(TestCase):
self.assertEqual(notification.related_user, self.remote_user)
self.assertEqual(notification.related_status, status)
def test_self_boost(self, _):
def test_self_boost(self, *_):
"""boost your own status"""
view = views.Boost.as_view()
request = self.factory.post("")
@ -121,7 +122,7 @@ class InteractionViews(TestCase):
self.assertFalse(models.Notification.objects.exists())
def test_boost_unlisted(self, _):
def test_boost_unlisted(self, *_):
"""boost a status"""
view = views.Boost.as_view()
request = self.factory.post("")
@ -136,7 +137,7 @@ class InteractionViews(TestCase):
boost = models.Boost.objects.get()
self.assertEqual(boost.privacy, "unlisted")
def test_boost_private(self, _):
def test_boost_private(self, *_):
"""boost a status"""
view = views.Boost.as_view()
request = self.factory.post("")
@ -149,7 +150,7 @@ class InteractionViews(TestCase):
view(request, status.id)
self.assertFalse(models.Boost.objects.exists())
def test_boost_twice(self, _):
def test_boost_twice(self, *_):
"""boost a status"""
view = views.Boost.as_view()
request = self.factory.post("")
@ -161,13 +162,17 @@ class InteractionViews(TestCase):
view(request, status.id)
self.assertEqual(models.Boost.objects.count(), 1)
def test_unboost(self, _):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_unboost(self, *_):
"""undo a boost"""
view = views.Unboost.as_view()
request = self.factory.post("")
request.user = self.remote_user
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.Status.objects.create(user=self.local_user, content="hi")
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
):
views.Boost.as_view()(request, status.id)
self.assertEqual(models.Boost.objects.count(), 1)

View file

@ -37,8 +37,12 @@ class ManageInvites(View):
PAGE_LENGTH,
)
page = paginated.get_page(request.GET.get("page"))
data = {
"invites": paginated.get_page(request.GET.get("page")),
"invites": page,
"page_range": paginated.get_elided_page_range(
page.number, on_each_side=2, on_ends=1
),
"form": forms.CreateInviteForm(),
}
return TemplateResponse(request, "settings/manage_invites.html", data)
@ -118,15 +122,16 @@ class ManageInviteRequests(View):
reduce(operator.or_, (Q(**f) for f in filters))
).distinct()
paginated = Paginator(
requests,
PAGE_LENGTH,
)
paginated = Paginator(requests, PAGE_LENGTH)
page = paginated.get_page(request.GET.get("page"))
data = {
"ignored": ignored,
"count": paginated.count,
"requests": paginated.get_page(request.GET.get("page")),
"requests": page,
"page_range": paginated.get_elided_page_range(
page.number, on_each_side=2, on_ends=1
),
"sort": sort,
}
return TemplateResponse(request, "settings/manage_invite_requests.html", data)

Binary file not shown.

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.1.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-06-07 14:09+0000\n"
"POT-Creation-Date: 2021-06-09 16:46+0000\n"
"PO-Revision-Date: 2021-04-05 12:44+0100\n"
"Last-Translator: Fabien Basmaison <contact@arkhi.org>\n"
"Language-Team: Mouse Reeve <LL@li.org>\n"
@ -91,23 +91,23 @@ msgstr "nom du compte:"
msgid "A user with that username already exists."
msgstr "Ce nom est déjà associé à un compte."
#: bookwyrm/settings.py:160
#: bookwyrm/settings.py:156
msgid "English"
msgstr "English"
#: bookwyrm/settings.py:161
#: bookwyrm/settings.py:157
msgid "German"
msgstr "Deutsch"
#: bookwyrm/settings.py:162
#: bookwyrm/settings.py:158
msgid "Spanish"
msgstr "Español"
#: bookwyrm/settings.py:163
#: bookwyrm/settings.py:159
msgid "French"
msgstr "Français"
#: bookwyrm/settings.py:164
#: bookwyrm/settings.py:160
msgid "Simplified Chinese"
msgstr "简化字"
@ -154,12 +154,12 @@ msgid "Wikipedia"
msgstr "Wikipedia"
#: bookwyrm/templates/author/author.html:69
#: bookwyrm/templates/book/book.html:78
#: bookwyrm/templates/book/book.html:87
msgid "View on OpenLibrary"
msgstr "Voir sur OpenLibrary"
#: bookwyrm/templates/author/author.html:77
#: bookwyrm/templates/book/book.html:81
#: bookwyrm/templates/book/book.html:90
msgid "View on Inventaire"
msgstr "Voir sur Inventaire"
@ -252,7 +252,7 @@ msgid "Goodreads key:"
msgstr "Clé Goodreads:"
#: bookwyrm/templates/author/edit_author.html:116
#: bookwyrm/templates/book/book.html:124
#: bookwyrm/templates/book/book.html:133
#: bookwyrm/templates/book/edit_book.html:321
#: bookwyrm/templates/lists/form.html:42
#: bookwyrm/templates/preferences/edit_user.html:70
@ -269,7 +269,7 @@ msgid "Save"
msgstr "Enregistrer"
#: bookwyrm/templates/author/edit_author.html:117
#: bookwyrm/templates/book/book.html:125 bookwyrm/templates/book/book.html:174
#: bookwyrm/templates/book/book.html:134 bookwyrm/templates/book/book.html:183
#: bookwyrm/templates/book/cover_modal.html:32
#: bookwyrm/templates/book/edit_book.html:322
#: bookwyrm/templates/moderation/report_modal.html:34
@ -284,98 +284,98 @@ msgstr "Enregistrer"
msgid "Cancel"
msgstr "Annuler"
#: bookwyrm/templates/book/book.html:31
#: bookwyrm/templates/book/book.html:40
#: bookwyrm/templates/discover/large-book.html:25
#: bookwyrm/templates/discover/small-book.html:19
msgid "by"
msgstr "par"
#: bookwyrm/templates/book/book.html:39 bookwyrm/templates/book/book.html:40
#: bookwyrm/templates/book/book.html:48 bookwyrm/templates/book/book.html:49
msgid "Edit Book"
msgstr "Modifier le livre"
#: bookwyrm/templates/book/book.html:57
#: bookwyrm/templates/book/book.html:66
#: bookwyrm/templates/book/cover_modal.html:5
msgid "Add cover"
msgstr "Ajouter une couverture"
#: bookwyrm/templates/book/book.html:61
#: bookwyrm/templates/book/book.html:70
msgid "Failed to load cover"
msgstr "La couverture na pu être chargée"
#: bookwyrm/templates/book/book.html:101
#: bookwyrm/templates/book/book.html:110
#, python-format
msgid "(%(review_count)s review)"
msgid_plural "(%(review_count)s reviews)"
msgstr[0] "(%(review_count)s critique)"
msgstr[1] "(%(review_count)s critiques)"
#: bookwyrm/templates/book/book.html:113
#: bookwyrm/templates/book/book.html:122
msgid "Add Description"
msgstr "Ajouter une description"
#: bookwyrm/templates/book/book.html:120
#: bookwyrm/templates/book/book.html:129
#: bookwyrm/templates/book/edit_book.html:136
#: bookwyrm/templates/lists/form.html:12
msgid "Description:"
msgstr "Description:"
#: bookwyrm/templates/book/book.html:134
#: bookwyrm/templates/book/book.html:143
#, python-format
msgid "<a href=\"%(path)s/editions\">%(count)s editions</a>"
msgstr "<a href=\"%(path)s/editions\">%(count)s éditions</a>"
#: bookwyrm/templates/book/book.html:142
#: bookwyrm/templates/book/book.html:151
#, python-format
msgid "This edition is on your <a href=\"%(path)s\">%(shelf_name)s</a> shelf."
msgstr "Cette édition est sur votre étagère <a href=\"%(path)s\">%(shelf_name)s</a>."
#: bookwyrm/templates/book/book.html:148
#: bookwyrm/templates/book/book.html:157
#, 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 "Une <a href=\"%(book_path)s\">édition différente</a> de ce livre existe sur votre étagère <a href=\"%(shelf_path)s\">%(shelf_name)s</a>."
#: bookwyrm/templates/book/book.html:159
#: bookwyrm/templates/book/book.html:168
msgid "Your reading activity"
msgstr "Votre activité de lecture"
#: bookwyrm/templates/book/book.html:162
#: bookwyrm/templates/book/book.html:171
msgid "Add read dates"
msgstr "Ajouter des dates de lecture"
#: bookwyrm/templates/book/book.html:171
#: bookwyrm/templates/book/book.html:180
msgid "Create"
msgstr "Créer"
#: bookwyrm/templates/book/book.html:181
#: bookwyrm/templates/book/book.html:190
msgid "You don't have any reading activity for this book."
msgstr "Vous navez aucune activité de lecture pour ce livre"
#: bookwyrm/templates/book/book.html:200
#: bookwyrm/templates/book/book.html:209
msgid "Reviews"
msgstr "Critiques"
#: bookwyrm/templates/book/book.html:205
#: bookwyrm/templates/book/book.html:214
msgid "Your reviews"
msgstr "Vos critiques"
#: bookwyrm/templates/book/book.html:211
#: bookwyrm/templates/book/book.html:220
msgid "Your comments"
msgstr "Vos commentaires"
#: bookwyrm/templates/book/book.html:217
#: bookwyrm/templates/book/book.html:226
msgid "Your quotes"
msgstr "Vos citations"
#: bookwyrm/templates/book/book.html:253
#: bookwyrm/templates/book/book.html:262
msgid "Subjects"
msgstr "Sujets"
#: bookwyrm/templates/book/book.html:265
#: bookwyrm/templates/book/book.html:274
msgid "Places"
msgstr "Lieux"
#: bookwyrm/templates/book/book.html:276 bookwyrm/templates/layout.html:61
#: bookwyrm/templates/book/book.html:285 bookwyrm/templates/layout.html:61
#: bookwyrm/templates/lists/lists.html:5 bookwyrm/templates/lists/lists.html:12
#: bookwyrm/templates/search/layout.html:25
#: bookwyrm/templates/search/layout.html:50
@ -383,11 +383,11 @@ msgstr "Lieux"
msgid "Lists"
msgstr "Listes"
#: bookwyrm/templates/book/book.html:287
#: bookwyrm/templates/book/book.html:296
msgid "Add to list"
msgstr "Ajouter à la liste"
#: bookwyrm/templates/book/book.html:297
#: bookwyrm/templates/book/book.html:306
#: bookwyrm/templates/book/cover_modal.html:31
#: bookwyrm/templates/lists/list.html:179
msgid "Add"
@ -2921,6 +2921,11 @@ msgstr "Niveau daccès:"
msgid "File exceeds maximum size: 10MB"
msgstr "Ce fichier dépasse la taille limite: 10Mo"
#: bookwyrm/templatetags/utilities.py:30
#, python-format
msgid "%(title)s: %(subtitle)s"
msgstr "%(title)s (%(subtitle)s)"
#: bookwyrm/views/import_data.py:67
msgid "Not a valid csv file"
msgstr "Fichier CSV non valide"