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(): for stream in streams.values():
stream.add_status(instance) 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) @receiver(signals.post_delete, sender=models.Boost)
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -208,7 +221,10 @@ def remove_boost_on_delete(sender, instance, *args, **kwargs):
"""boosts are deleted""" """boosts are deleted"""
# we're only interested in new statuses # we're only interested in new statuses
for stream in streams.values(): for stream in streams.values():
# remove the boost
stream.remove_object_from_related_stores(instance) 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) @receiver(signals.post_save, sender=models.UserFollows)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,7 +40,7 @@
</div> </div>
<div class="column is-narrow"> <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>
</div> </div>
{% endfor %} {% endfor %}

View file

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

View file

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

View file

@ -56,7 +56,7 @@
<span class="icon icon-warning"></span> <span class="icon icon-warning"></span>
{% endif %} {% endif %}
</div> </div>
<div class="column"> <div class="column is-clipped">
<div class="block"> <div class="block">
<p> <p>
{# DESCRIPTION #} {# DESCRIPTION #}
@ -137,7 +137,7 @@
{# PREVIEW #} {# 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="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="columns">
<div class="column"> <div class="column is-clipped">
{% include 'snippets/status_preview.html' with status=related_status %} {% include 'snippets/status_preview.html' with status=related_status %}
</div> </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 %}"> <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 %} {% block edit-button %}
<a href="{% url 'settings-import-blocklist' %}"> <a href="{% url 'settings-import-blocklist' %}">
<span class="icon icon-plus" title="{% trans 'Add instance' %}" aria-hidden="True"></span> <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> </a>
{% endblock %} {% endblock %}

View file

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

View file

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

View file

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

View file

@ -16,6 +16,7 @@ from bookwyrm import activitypub, models, settings
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
@patch("bookwyrm.models.Status.broadcast") @patch("bookwyrm.models.Status.broadcast")
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
@patch("bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores")
class Status(TestCase): class Status(TestCase):
"""lotta types of statuses""" """lotta types of statuses"""
@ -384,7 +385,8 @@ class Status(TestCase):
user=self.local_user, notification_type="GLORB" 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""" """should send out two verions of a status on create"""
models.Comment.objects.create( models.Comment.objects.create(
content="hi", user=self.local_user, book=self.book 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.add_status")
@patch("bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores")
class TemplateTags(TestCase): class TemplateTags(TestCase):
"""lotta different things here""" """lotta different things here"""
@ -38,28 +39,28 @@ class TemplateTags(TestCase):
) )
self.book = models.Edition.objects.create(title="Test Book") 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""" """get a user's most recent rating of a book"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.Review.objects.create(user=self.user, book=self.book, rating=3) models.Review.objects.create(user=self.user, book=self.book, rating=3)
self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 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""" """there is no rating available"""
self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 0) 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""" """fall back to the simplest uid available"""
self.assertNotEqual(self.user.username, self.user.localname) self.assertNotEqual(self.user.username, self.user.localname)
self.assertEqual(utilities.get_user_identifier(self.user), "mouse") 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""" """for a remote user, should be their full username"""
self.assertEqual( self.assertEqual(
utilities.get_user_identifier(self.remote_user), "rat@example.com" 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""" """direct replies to a status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
parent = models.Review.objects.create( parent = models.Review.objects.create(
@ -87,7 +88,7 @@ class TemplateTags(TestCase):
self.assertTrue(second_child in replies) self.assertTrue(second_child in replies)
self.assertFalse(third_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""" """get the reply parent of a status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
parent = models.Review.objects.create( parent = models.Review.objects.create(
@ -101,7 +102,7 @@ class TemplateTags(TestCase):
self.assertEqual(result, parent) self.assertEqual(result, parent)
self.assertIsInstance(result, models.Review) self.assertIsInstance(result, models.Review)
def test_get_user_liked(self, _): def test_get_user_liked(self, *_):
"""did a user like a status""" """did a user like a status"""
status = models.Review.objects.create(user=self.remote_user, book=self.book) 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) models.Favorite.objects.create(user=self.user, status=status)
self.assertTrue(interaction.get_user_liked(self.user, 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""" """did a user boost a status"""
status = models.Review.objects.create(user=self.remote_user, book=self.book) 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) models.Boost.objects.create(user=self.user, boosted_status=status)
self.assertTrue(interaction.get_user_boosted(self.user, status)) self.assertTrue(interaction.get_user_boosted(self.user, status))
def test_get_boosted(self, _): def test_get_boosted(self, *_):
"""load a boosted status""" """load a boosted status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Review.objects.create(user=self.remote_user, book=self.book) 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.assertIsInstance(boosted, models.Review)
self.assertEqual(boosted, status) self.assertEqual(boosted, status)
def test_get_book_description(self, _): def test_get_book_description(self, *_):
"""grab it from the edition or the parent""" """grab it from the edition or the parent"""
work = models.Work.objects.create(title="Test Work") work = models.Work.objects.create(title="Test Work")
self.book.parent_work = work self.book.parent_work = work
@ -144,12 +145,12 @@ class TemplateTags(TestCase):
self.book.save() self.book.save()
self.assertEqual(bookwyrm_tags.get_book_description(self.book), "hello") self.assertEqual(bookwyrm_tags.get_book_description(self.book), "hello")
def test_get_uuid(self, _): def test_get_uuid(self, *_):
"""uuid functionality""" """uuid functionality"""
uuid = utilities.get_uuid("hi") uuid = utilities.get_uuid("hi")
self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid)) self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid))
def test_get_markdown(self, _): def test_get_markdown(self, *_):
"""mardown format data""" """mardown format data"""
result = markdown.get_markdown("_hi_") result = markdown.get_markdown("_hi_")
self.assertEqual(result, "<p><em>hi</em></p>") self.assertEqual(result, "<p><em>hi</em></p>")
@ -157,13 +158,13 @@ class TemplateTags(TestCase):
result = markdown.get_markdown("<marquee>_hi_</marquee>") result = markdown.get_markdown("<marquee>_hi_</marquee>")
self.assertEqual(result, "<p><em>hi</em></p>") self.assertEqual(result, "<p><em>hi</em></p>")
def test_get_mentions(self, _): def test_get_mentions(self, *_):
"""list of people mentioned""" """list of people mentioned"""
status = models.Status.objects.create(content="hi", user=self.remote_user) status = models.Status.objects.create(content="hi", user=self.remote_user)
result = status_display.get_mentions(status, self.user) result = status_display.get_mentions(status, self.user)
self.assertEqual(result, "@rat@example.com ") self.assertEqual(result, "@rat@example.com ")
def test_related_status(self, _): def test_related_status(self, *_):
"""gets the subclass model for a notification status""" """gets the subclass model for a notification status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(content="hi", user=self.user) status = models.Status.objects.create(content="hi", user=self.user)

View file

@ -51,7 +51,8 @@ class InboxActivities(TestCase):
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @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""" """boost a status"""
self.assertEqual(models.Notification.objects.count(), 0) self.assertEqual(models.Notification.objects.count(), 0)
activity = { activity = {
@ -81,7 +82,8 @@ class InboxActivities(TestCase):
@responses.activate @responses.activate
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @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""" """boost a status from a remote server"""
work = models.Work.objects.create(title="work title") work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create( book = models.Edition.objects.create(
@ -153,12 +155,13 @@ class InboxActivities(TestCase):
views.inbox.activity_task(activity) views.inbox.activity_task(activity)
self.assertEqual(models.Boost.objects.count(), 0) 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""" """undo a boost"""
with patch("bookwyrm.activitystreams.ActivityStream.add_status"): boost = models.Boost.objects.create(
boost = models.Boost.objects.create( boosted_status=self.status, user=self.remote_user
boosted_status=self.status, user=self.remote_user )
)
activity = { activity = {
"type": "Undo", "type": "Undo",
"actor": "hi", "actor": "hi",
@ -175,11 +178,7 @@ class InboxActivities(TestCase):
"published": "Mon, 25 May 2020 19:31:20 GMT", "published": "Mon, 25 May 2020 19:31:20 GMT",
}, },
} }
with patch( views.inbox.activity_task(activity)
"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()) self.assertFalse(models.Boost.objects.exists())
def test_unboost_unknown_boost(self): 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.models.activitypub_mixin.broadcast_task.delay")
@patch("bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores")
class InteractionViews(TestCase): class InteractionViews(TestCase):
"""viewing and creating statuses""" """viewing and creating statuses"""
@ -40,7 +41,7 @@ class InteractionViews(TestCase):
parent_work=work, parent_work=work,
) )
def test_favorite(self, _): def test_favorite(self, *_):
"""create and broadcast faving a status""" """create and broadcast faving a status"""
view = views.Favorite.as_view() view = views.Favorite.as_view()
request = self.factory.post("") request = self.factory.post("")
@ -58,7 +59,7 @@ class InteractionViews(TestCase):
self.assertEqual(notification.user, self.local_user) self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.related_user, self.remote_user) self.assertEqual(notification.related_user, self.remote_user)
def test_unfavorite(self, _): def test_unfavorite(self, *_):
"""unfav a status""" """unfav a status"""
view = views.Unfavorite.as_view() view = views.Unfavorite.as_view()
request = self.factory.post("") request = self.factory.post("")
@ -75,7 +76,7 @@ class InteractionViews(TestCase):
self.assertEqual(models.Favorite.objects.count(), 0) self.assertEqual(models.Favorite.objects.count(), 0)
self.assertEqual(models.Notification.objects.count(), 0) self.assertEqual(models.Notification.objects.count(), 0)
def test_boost(self, _): def test_boost(self, *_):
"""boost a status""" """boost a status"""
view = views.Boost.as_view() view = views.Boost.as_view()
request = self.factory.post("") request = self.factory.post("")
@ -97,7 +98,7 @@ class InteractionViews(TestCase):
self.assertEqual(notification.related_user, self.remote_user) self.assertEqual(notification.related_user, self.remote_user)
self.assertEqual(notification.related_status, status) self.assertEqual(notification.related_status, status)
def test_self_boost(self, _): def test_self_boost(self, *_):
"""boost your own status""" """boost your own status"""
view = views.Boost.as_view() view = views.Boost.as_view()
request = self.factory.post("") request = self.factory.post("")
@ -121,7 +122,7 @@ class InteractionViews(TestCase):
self.assertFalse(models.Notification.objects.exists()) self.assertFalse(models.Notification.objects.exists())
def test_boost_unlisted(self, _): def test_boost_unlisted(self, *_):
"""boost a status""" """boost a status"""
view = views.Boost.as_view() view = views.Boost.as_view()
request = self.factory.post("") request = self.factory.post("")
@ -136,7 +137,7 @@ class InteractionViews(TestCase):
boost = models.Boost.objects.get() boost = models.Boost.objects.get()
self.assertEqual(boost.privacy, "unlisted") self.assertEqual(boost.privacy, "unlisted")
def test_boost_private(self, _): def test_boost_private(self, *_):
"""boost a status""" """boost a status"""
view = views.Boost.as_view() view = views.Boost.as_view()
request = self.factory.post("") request = self.factory.post("")
@ -149,7 +150,7 @@ class InteractionViews(TestCase):
view(request, status.id) view(request, status.id)
self.assertFalse(models.Boost.objects.exists()) self.assertFalse(models.Boost.objects.exists())
def test_boost_twice(self, _): def test_boost_twice(self, *_):
"""boost a status""" """boost a status"""
view = views.Boost.as_view() view = views.Boost.as_view()
request = self.factory.post("") request = self.factory.post("")
@ -161,13 +162,17 @@ class InteractionViews(TestCase):
view(request, status.id) view(request, status.id)
self.assertEqual(models.Boost.objects.count(), 1) self.assertEqual(models.Boost.objects.count(), 1)
def test_unboost(self, _): @patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_unboost(self, *_):
"""undo a boost""" """undo a boost"""
view = views.Unboost.as_view() view = views.Unboost.as_view()
request = self.factory.post("") request = self.factory.post("")
request.user = self.remote_user request.user = self.remote_user
with patch("bookwyrm.activitystreams.ActivityStream.add_status"): status = models.Status.objects.create(user=self.local_user, content="hi")
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) views.Boost.as_view()(request, status.id)
self.assertEqual(models.Boost.objects.count(), 1) self.assertEqual(models.Boost.objects.count(), 1)

View file

@ -37,8 +37,12 @@ class ManageInvites(View):
PAGE_LENGTH, PAGE_LENGTH,
) )
page = paginated.get_page(request.GET.get("page"))
data = { 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(), "form": forms.CreateInviteForm(),
} }
return TemplateResponse(request, "settings/manage_invites.html", data) return TemplateResponse(request, "settings/manage_invites.html", data)
@ -118,15 +122,16 @@ class ManageInviteRequests(View):
reduce(operator.or_, (Q(**f) for f in filters)) reduce(operator.or_, (Q(**f) for f in filters))
).distinct() ).distinct()
paginated = Paginator( paginated = Paginator(requests, PAGE_LENGTH)
requests,
PAGE_LENGTH,
)
page = paginated.get_page(request.GET.get("page"))
data = { data = {
"ignored": ignored, "ignored": ignored,
"count": paginated.count, "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, "sort": sort,
} }
return TemplateResponse(request, "settings/manage_invite_requests.html", data) return TemplateResponse(request, "settings/manage_invite_requests.html", data)

Binary file not shown.

View file

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