Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-04-04 16:44:46 -07:00
commit a8052c2dd0
29 changed files with 724 additions and 521 deletions

View file

@ -8,6 +8,6 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: psf/black@stable
- uses: psf/black@20.8b1
with:
args: ". --check -l 80 -S"

View file

@ -26,15 +26,21 @@ class ActivityStream(ABC):
""" the redis key for this user's unread count for this stream """
return "{}-unread".format(self.stream_id(user))
def get_value(self, status): # pylint: disable=no-self-use
""" the status id and the rank (ie, published date) """
return {status.id: status.published_date.timestamp()}
def add_status(self, status):
""" add a status to users' feeds """
value = self.get_value(status)
# we want to do this as a bulk operation, hence "pipeline"
pipeline = r.pipeline()
for user in self.stream_users(status):
# add the status to the feed
pipeline.lpush(self.stream_id(user), status.id)
pipeline.ltrim(self.stream_id(user), 0, settings.MAX_STREAM_LENGTH)
pipeline.zadd(self.stream_id(user), value)
pipeline.zremrangebyrank(
self.stream_id(user), settings.MAX_STREAM_LENGTH, -1
)
# add to the unread status count
pipeline.incr(self.unread_id(user))
# and go!
@ -44,14 +50,19 @@ class ActivityStream(ABC):
""" remove a status from all feeds """
pipeline = r.pipeline()
for user in self.stream_users(status):
pipeline.lrem(self.stream_id(user), -1, status.id)
pipeline.zrem(self.stream_id(user), -1, status.id)
pipeline.execute()
def add_user_statuses(self, viewer, user):
""" add a user's statuses to another user's feed """
pipeline = r.pipeline()
for status in user.status_set.all()[: settings.MAX_STREAM_LENGTH]:
pipeline.lpush(self.stream_id(viewer), status.id)
statuses = user.status_set.all()[: settings.MAX_STREAM_LENGTH]
for status in statuses:
pipeline.zadd(self.stream_id(viewer), self.get_value(status))
if statuses:
pipeline.zremrangebyrank(
self.stream_id(user), settings.MAX_STREAM_LENGTH, -1
)
pipeline.execute()
def remove_user_statuses(self, viewer, user):
@ -66,7 +77,7 @@ class ActivityStream(ABC):
# clear unreads for this feed
r.set(self.unread_id(user), 0)
statuses = r.lrange(self.stream_id(user), 0, -1)
statuses = r.zrevrange(self.stream_id(user), 0, -1)
return (
models.Status.objects.select_subclasses()
.filter(id__in=statuses)
@ -75,7 +86,7 @@ class ActivityStream(ABC):
def get_unread_count(self, user):
""" get the unread status count for this user's feed """
return int(r.get(self.unread_id(user)))
return int(r.get(self.unread_id(user)) or 0)
def populate_stream(self, user):
""" go from zero to a timeline """
@ -84,7 +95,11 @@ class ActivityStream(ABC):
stream_id = self.stream_id(user)
for status in statuses.all()[: settings.MAX_STREAM_LENGTH]:
pipeline.lpush(stream_id, status.id)
pipeline.zadd(stream_id, self.get_value(status))
# only trim the stream if statuses were added
if statuses.exists():
pipeline.zremrangebyrank(stream_id, settings.MAX_STREAM_LENGTH, -1)
pipeline.execute()
def stream_users(self, status): # pylint: disable=no-self-use
@ -274,7 +289,7 @@ def add_statuses_on_unblock(sender, instance, *args, **kwargs):
@receiver(signals.post_save, sender=models.User)
# pylint: disable=unused-argument
def populate_feed_on_account_create(sender, instance, created, *args, **kwargs):
def populate_streams_on_account_create(sender, instance, created, *args, **kwargs):
""" build a user's feeds when they join """
if not created or not instance.local:
return

View file

@ -6,7 +6,7 @@ from django import forms
from django.forms import ModelForm, PasswordInput, widgets
from django.forms.widgets import Textarea
from django.utils import timezone
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from bookwyrm import models

View file

@ -25,6 +25,16 @@ html {
min-width: 75% !important;
}
/* --- "disabled" for non-buttons --- */
.is-disabled {
background-color: #dbdbdb;
border-color: #dbdbdb;
box-shadow: none;
color: #7a7a7a;
opacity: 0.5;
cursor: not-allowed;
}
/* --- SHELVING --- */
/** @todo Replace icons with SVG symbols.
@ -80,52 +90,34 @@ html {
}
}
/* --- STARS --- */
.rate-stars button.icon {
background: none;
border: none;
padding: 0;
margin: 0;
display: inline;
/** Stars in a review form
*
* Specificity makes hovering taking over checked inputs.
*
* \e9d9: filled star
* \e9d7: empty star;
******************************************************************************/
.form-rate-stars {
width: max-content;
}
.rate-stars:hover .icon::before {
/* All stars are visually filled by default. */
.form-rate-stars .icon::before {
content: '\e9d9';
}
.rate-stars form:hover ~ form .icon::before {
/* Icons directly following inputs that follow the checked input are emptied. */
.form-rate-stars input:checked ~ input + .icon::before {
content: '\e9d7';
}
/** stars in a review form
*
* @todo Simplify the logic for those icons.
*/
.form-rate-stars input + .icon.icon::before {
content: '\e9d9';
}
/* When a label is hovered, repeat the fill-all-then-empty-following pattern. */
.form-rate-stars:hover .icon.icon::before {
content: '\e9d9';
}
.form-rate-stars input:checked + .icon.icon::before {
content: '\e9d9';
}
.form-rate-stars input:checked + * ~ .icon.icon::before {
content: '\e9d7';
}
.form-rate-stars:hover label.icon.icon::before {
content: '\e9d9';
}
.form-rate-stars label.icon:hover::before {
content: '\e9d9';
}
.form-rate-stars label.icon:hover ~ label.icon.icon::before {
.form-rate-stars .icon:hover ~ .icon::before {
content: '\e9d7';
}

View file

@ -1,18 +1,10 @@
""" Handle user activity """
from django.db import transaction
from django.utils import timezone
from bookwyrm import models
from bookwyrm.sanitize_html import InputHtmlParser
def delete_status(status):
""" replace the status with a tombstone """
status.deleted = True
status.deleted_date = timezone.now()
status.save()
def create_generated_note(user, content, mention_books=None, privacy="public"):
""" a note created by the app about user activity """
# sanitize input html

View file

@ -122,12 +122,18 @@
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2"><label class="label" for="id_first_published_date">{% trans "First published date:" %}</label> {{ form.first_published_date }} </p>
<p class="mb-2">
<label class="label" for="id_first_published_date">{% trans "First published date:" %}</label>
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if book.first_published_date %} value="{{ book.first_published_date|date:'Y-m-d' }}"{% endif %}>
</p>
{% for error in form.first_published_date.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2"><label class="label" for="id_published_date">{% trans "Published date:" %}</label> {{ form.published_date }} </p>
<p class="mb-2">
<label class="label" for="id_published_date">{% trans "Published date:" %}</label>
<input type="date" name="published_date" class="input" id="id_published_date"{% if book.published_date %} value="{{ book.published_date|date:'Y-m-d' }}"{% endif %}>
</p>
{% for error in form.published_date.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}

View file

@ -0,0 +1,34 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block title %}{% trans "Compose status" %}{% endblock %}
{% block content %}
<header class="block content">
<h1>{% trans "Compose status" %}</h1>
</header>
{% with 0|uuid as uuid %}
<div class="box columns">
{% if book %}
<div class="column is-one-third">
<div class="block">
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
</div>
<h3 class="title is-6">{% include 'snippets/book_titleby.html' with book=book %}</h3>
</div>
{% endif %}
<div class="column is-two-thirds">
{% if draft.reply_parent %}
{% include 'snippets/status/status.html' with status=draft.reply_parent no_interact=True %}
{% endif %}
{% if not draft %}
{% include 'snippets/create_status.html' %}
{% else %}
{% include 'snippets/create_status_form.html' %}
{% endif %}
</div>
</div>
{% endwith %}
{% endblock %}

View file

@ -12,6 +12,7 @@
{% if not suggested_books %}
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
{% else %}
{% with active_book=request.GET.book %}
<div class="tab-group">
<div class="tabs is-small">
<ul role="tablist">
@ -28,8 +29,14 @@
<div class="tabs is-small is-toggle">
<ul>
{% for book in shelf.books %}
<li{% if shelf_counter == 1 and forloop.first %} class="is-active"{% endif %}>
<a href="#book-{{ book.id }}" id="tab-book-{{ book.id }}" role="tab" aria-label="{{ book.title }}" aria-selected="{% if shelf_counter == 1 and forloop.first %}true{% else %}false{% endif %}" aria-controls="book-{{ book.id }}">
<li class="{% if active_book == book.id|stringformat:'d' %}is-active{% elif not active_book and shelf_counter == 1 and forloop.first %}is-active{% endif %}">
<a
href="{{ request.path }}?book={{ book.id }}"
id="tab-book-{{ book.id }}"
role="tab"
aria-label="{{ book.title }}"
aria-selected="{% if active_book == book.id|stringformat:'d' %}true{% elif shelf_counter == 1 and forloop.first %}true{% else %}false{% endif %}"
aria-controls="book-{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book size="medium" %}
</a>
</li>
@ -45,7 +52,13 @@
{% for shelf in suggested_books %}
{% with shelf_counter=forloop.counter %}
{% for book in shelf.books %}
<div class="suggested-tabs card" role="tabpanel" id="book-{{ book.id }}"{% if shelf_counter != 1 or not forloop.first %} hidden{% endif %} aria-labelledby="tab-book-{{ book.id }}">
<div
class="suggested-tabs card"
role="tabpanel"
id="book-{{ book.id }}"
{% if active_book and active_book == book.id|stringformat:'d' %}{% elif not active_book and shelf_counter == 1 and forloop.first %}{% else %} hidden{% endif %}
aria-labelledby="tab-book-{{ book.id }}">
<div class="card-header">
<div class="card-header-title">
<div>
@ -66,6 +79,7 @@
{% endwith %}
{% endfor %}
</div>
{% endwith %}
{% endif %}
{% if goal %}

View file

@ -57,7 +57,7 @@
{% for book in goal.books %}
<div class="column is-one-fifth">
<div class="is-clipped">
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book.book %}</a>
<a href="{{ book.book.local_path }}">{% include 'snippets/book_cover.html' with book=book.book %}</a>
</div>
</div>
{% endfor %}

View file

@ -28,13 +28,17 @@
{% include 'settings/invite_request_filters.html' %}
<table class="table is-striped">
<table class="table is-striped is-fullwidth">
{% url 'settings-invite-requests' as url %}
<tr>
<th>
{% trans "Date" as text %}
{% trans "Date requested" as text %}
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
</th>
<th>
{% trans "Date accepted" as text %}
{% include 'snippets/table-sort-header.html' with field="invite__invitees__created_date" sort=sort text=text %}
</th>
<th>{% trans "Email" %}</th>
<th>
{% trans "Status" as text %}
@ -48,6 +52,7 @@
{% for req in requests %}
<tr>
<td>{{ req.created_date | naturaltime }}</td>
<td>{{ req.invite.invitees.first.created_date | naturaltime }}</td>
<td>{{ req.email }}</td>
<td>
{% if req.invite.times_used %}
@ -58,7 +63,7 @@
{% trans "Requested" %}
{% endif %}
</td>
<td class="field is-grouped">
<td><div class="field is-grouped">
{# no invite OR invite not yet used #}
{% if not req.invite.times_used %}
<form name="send-invite" method="post">
@ -93,7 +98,7 @@
{% endif %}
</form>
{% endif %}
</td>
</div></td>
</tr>
{% endfor %}
</table>

View file

@ -1,5 +1,12 @@
{% load i18n %}
<div class="control{% if not parent_status.content_warning %} hidden{% endif %}" id="spoilers-{{ uuid }}">
<div class="control{% if not parent_status.content_warning and not draft.content_warning %} hidden{% endif %}" id="spoilers-{{ uuid }}">
<label class="is-sr-only" for="id_content_warning-{{ uuid }}">{% trans "Spoiler alert:" %}</label>
<input type="text" name="content_warning" maxlength="255" class="input" id="id_content_warning-{{ uuid }}" placeholder="{% trans 'Spoilers ahead!' %}"{% if parent_status.content_warning %} value="{{ parent_status.content_warning }}"{% endif %}>
<input
type="text"
name="content_warning"
maxlength="255"
class="input"
id="id_content_warning-{{ uuid }}"
placeholder="{% trans 'Spoilers ahead!' %}"
value="{% firstof draft.content_warning parent_status.content_warning '' %}">
</div>

View file

@ -2,36 +2,62 @@
{% load i18n %}
{% load bookwyrm_tags %}
{% with status_type=request.GET.status_type %}
<div class="tab-group">
<div class="tabs is-boxed" role="tablist">
<ul>
<li class="is-active">
<a href="#review-{{ book.id }}" id="tab-review-{{ book.id }}" role="tab" aria-selected="true" aria-controls="review-{{ book.id }}" data-category="tab-option-{{ book.id }}">{% trans "Review" %}</a>
<li class="{% if status_type == 'review' or not status_type %}is-active{% endif %}">
<a
href="{{ request.path }}?status_type=review&book={{ book.id }}"
id="tab-review-{{ book.id }}"
role="tab"
aria-selected="{% if status_type == 'review' or not status_type %}true{% else %}false{% endif %}"
aria-controls="review-{{ book.id }}"
data-category="tab-option-{{ book.id }}">
{% trans "Review" %}
</a>
</li>
<li>
<a href="#comment-{{ book.id}}" id="tab-comment-{{ book.id }}" role="tab" aria-selected="false" aria-controls="comment-{{ book.id}}" data-category="tab-option-{{ book.id }}">{% trans "Comment" %}</a>
<li class="{% if status_type == 'comment' %}is-active{% endif %}">
<a
href="{{ request.path }}?status_type=comment&book={{ book.id}}"
id="tab-comment-{{ book.id }}"
role="tab"
aria-selected="{% if status_type == 'comment' %}true{% else %}false{% endif %}"
aria-controls="comment-{{ book.id}}"
data-category="tab-option-{{ book.id }}">
{% trans "Comment" %}
</a>
</li>
<li>
<a href="#quote-{{ book.id }}" id="tab-quote-{{ book.id }}" role="tab" aria-selected="false" aria-controls="quote-{{ book.id }}" data-category="tab-option-{{ book.id }}">{% trans "Quote" %}</a>
<li class="{% if status_type == 'quote' %}is-active{% endif %}">
<a
href="{{ request.path }}?status_type=quote&book={{ book.id }}"
id="tab-quote-{{ book.id }}"
role="tab"
aria-selected="{% if status_type == 'quote' %}true{% else %}false{% endif %}"
aria-controls="quote-{{ book.id }}"
data-category="tab-option-{{ book.id }}">
{% trans "Quote" %}
</a>
</li>
</ul>
</div>
<div class="tab-option-{{ book.id }}" id="review-{{ book.id }}" role="tabpanel" aria-labelledby="tab-review-{{ book.id }}">
<div class="tab-option-{{ book.id }}" id="review-{{ book.id }}" role="tabpanel" aria-labelledby="tab-review-{{ book.id }}" {% if status_type and status_type != "review" %}hidden{% endif %}>
{% with 0|uuid as uuid %}
{% include 'snippets/create_status_form.html' with type='review' %}
{% endwith %}
</div>
<div class="tab-option-{{ book.id }}" id="comment-{{ book.id }}" role="tabpanel" aria-labelledby="tab-comment-{{ book.id }}" hidden>
<div class="tab-option-{{ book.id }}" id="comment-{{ book.id }}" role="tabpanel" aria-labelledby="tab-comment-{{ book.id }}" {% if status_type != "comment" %}hidden{% endif %}>
{% with 0|uuid as uuid %}
{% include 'snippets/create_status_form.html' with type="comment" placeholder="Some thoughts on '"|add:book.title|add:"'" %}
{% endwith %}
</div>
<div class="tab-option-{{ book.id }}" id="quote-{{ book.id }}" role="tabpanel" aria-labelledby="tab-quote-{{ book.id }}" hidden>
<div class="tab-option-{{ book.id }}" id="quote-{{ book.id }}" role="tabpanel" aria-labelledby="tab-quote-{{ book.id }}" {% if status_type != "quote" %}hidden{% endif %}>
{% with 0|uuid as uuid %}
{% include 'snippets/create_status_form.html' with type="quotation" placeholder="An excerpt from '"|add:book.title|add:"'" %}
{% endwith %}
</div>
</div>
{% endwith %}

View file

@ -4,11 +4,11 @@
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="user" value="{{ request.user.id }}">
<input type="hidden" name="reply_parent" value="{{ reply_parent.id }}">
<input type="hidden" name="reply_parent" value="{% firstof draft.reply_parent.id reply_parent.id %}">
{% if type == 'review' %}
<div class="control">
<label class="label" for="id_name_{{ book.id }}_{{ type }}">{% trans "Title" %}:</label>
<input type="text" name="name" maxlength="255" class="input" required="" id="id_name_{{ book.id }}_{{ type }}" placeholder="My {{ type }} of '{{ book.title }}'">
<input type="text" name="name" maxlength="255" class="input" required="" id="id_name_{{ book.id }}_{{ type }}" placeholder="My {{ type }} of '{{ book.title }}'" value="{% firstof draft.name ''%}">
</div>
{% endif %}
<div class="control">
@ -27,31 +27,23 @@
{% if type == 'review' %}
<fieldset>
<legend class="is-sr-only">{% trans "Rating" %}</legend>
<div class="field is-grouped stars form-rate-stars">
<label class="is-sr-only" for="no-rating-{{ book.id }}">{% trans "No rating" %}</label>
<input class="is-sr-only" type="radio" name="rating" value="" id="no-rating-{{ book.id }}" checked>
{% for i in '12345'|make_list %}
<input class="is-sr-only" id="book{{book.id}}-star-{{ forloop.counter }}" type="radio" name="rating" value="{{ forloop.counter }}">
<label class="icon icon-star-empty" for="book{{book.id}}-star-{{ forloop.counter }}">
<span class="is-sr-only">{{ forloop.counter }} star{{ forloop.counter | pluralize }}</span>
</label>
{% endfor %}
</div>
{% include 'snippets/form_rate_stars.html' with book=book type=type|default:'summary' default_rating=draft.rating %}
</fieldset>
{% endif %}
{% if type == 'quotation' %}
<textarea name="quote" class="textarea" id="id_quote_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required></textarea>
<textarea name="quote" class="textarea" id="id_quote_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required>{{ draft.quote|default:'' }}</textarea>
{% else %}
{% include 'snippets/content_warning_field.html' with parent_status=status %}
<textarea name="content" class="textarea" id="id_content_{{ type }}-{{ book.id }}{{reply_parent.id}}" placeholder="{{ placeholder }}" {% if type == 'reply' %} aria-label="Reply"{% endif %} required>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mentions %}@{{ mentions|username }} {% endif %}</textarea>
<textarea name="content" class="textarea" id="id_content_{{ type }}-{{ book.id }}{{reply_parent.id}}" placeholder="{{ placeholder }}" {% if type == 'reply' %} aria-label="Reply"{% endif %} required>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mentions %}@{{ mentions|username }} {% endif %}{{ draft.content|default:'' }}</textarea>
{% endif %}
</div>
{% if type == 'quotation' %}
<div class="control">
<label class="label" for="id_content_quote-{{ book.id }}">{% trans "Comment" %}:</label>
{% include 'snippets/content_warning_field.html' with parent_status=status %}
<textarea name="content" class="textarea is-small" id="id_content_quote-{{ book.id }}"></textarea>
<textarea name="content" class="textarea is-small" id="id_content_quote-{{ book.id }}">{{ draft.content|default:'' }}</textarea>
</div>
{% elif type == 'comment' %}
<div class="control">
@ -64,12 +56,12 @@
<label class="label" for="progress-{{ uuid }}">{% trans "Progress:" %}</label>
<div class="field has-addons mb-0">
<div class="control">
<input aria-label="{% if readthrough.progress_mode == 'PG' %}Current page{% else %}Percent read{% endif %}" class="input" type="number" min="0" name="progress" size="3" value="{{ readthrough.progress|default:'' }}" id="progress-{{ uuid }}">
<input aria-label="{% if draft.progress_mode == 'PG' or readthrough.progress_mode == 'PG' %}Current page{% else %}Percent read{% endif %}" class="input" type="number" min="0" name="progress" size="3" value="{% firstof draft.progress readthrough.progress '' %}" id="progress-{{ uuid }}">
</div>
<div class="control select">
<select name="progress_mode" aria-label="Progress mode">
<option value="PG" {% if readthrough.progress_mode == 'PG' %}selected{% endif %}>{% trans "pages" %}</option>
<option value="PCT" {% if readthrough.progress_mode == 'PCT' %}selected{% endif %}>{% trans "percent" %}</option>
<option value="PG" {% if draft.progress_mode == 'PG' or readthrough.progress_mode == 'PG' %}selected{% endif %}>{% trans "pages" %}</option>
<option value="PCT" {% if draft.progress_mode == 'PCT' or readthrough.progress_mode == 'PCT' %}selected{% endif %}>{% trans "percent" %}</option>
</select>
</div>
</div>
@ -81,20 +73,25 @@
{% endif %}
</div>
{% endif %}
<input type="checkbox" class="hidden" name="sensitive" id="id_show_spoilers-{{ uuid }}" {% if status.content_warning %}checked{% endif %} aria-hidden="true">
<input type="checkbox" class="hidden" name="sensitive" id="id_show_spoilers-{{ uuid }}" {% if draft.content_warning or status.content_warning %}checked{% endif %} aria-hidden="true">
{# bottom bar #}
<div class="columns pt-1">
<div class="field has-addons column">
<div class="control">
{% trans "Include spoiler alert" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with text=button_text icon="warning is-size-4" controls_text="spoilers" controls_uid=uuid focus="id_content_warning" checkbox="id_show_spoilers" class="toggle-button" pressed=status.content_warning %}
{% firstof draft.content_warning status.content_warning as pressed %}
{% include 'snippets/toggle/toggle_button.html' with text=button_text icon="warning is-size-4" controls_text="spoilers" controls_uid=uuid focus="id_content_warning" checkbox="id_show_spoilers" class="toggle-button" pressed=pressed %}
</div>
<div class="control">
{% if type == 'direct' %}
<input type="hidden" name="privacy" value="direct">
<button type="button" class="button" aria-label="Privacy" disabled>{% trans "Private" %}</button>
{% else %}
{% if draft %}
{% include 'snippets/privacy_select.html' with current=draft.privacy %}
{% else %}
{% include 'snippets/privacy_select.html' with current=reply_parent.privacy %}
{% endif %}
{% endif %}
</div>
</div>

View file

@ -0,0 +1,54 @@
{% spaceless %}
{% load i18n %}
{% load bookwyrm_tags %}
<div class="
field is-grouped
stars form-rate-stars
{% if classes %}{{classes}}{% endif%}
">
<input
id="{{ type|slugify }}-{{ book.id }}-no-rating"
class="is-sr-only"
type="radio"
name="rating"
value="0"
{% if default_rating == 0 or not default_rating %}checked{% endif %}
>
<label class="is-sr-only" for="{{ type|slugify }}-{{ book.id }}-no-rating">
{% trans "No rating" %}
</label>
{% for i in '12345'|make_list %}
<input
id="{{ type|slugify }}-book{{ book.id }}-star-{{ forloop.counter }}"
class="is-sr-only"
type="radio"
name="rating"
value="{{ forloop.counter }}"
{% if default_rating == forloop.counter %}checked{% endif %}
/>
<label
class="
icon
{% if forloop.counter <= default_rating %}
icon-star-full
{% else %}
icon-star-empty
{% endif %}
"
for="{{ type|slugify }}-book{{ book.id }}-star-{{ forloop.counter }}"
>
<span class="is-sr-only">
{% blocktranslate trimmed count rating=forloop.counter %}
{{ rating }} star
{% plural %}
{{ rating }} stars
{% endblocktranslate %}
</span>
</label>
{% endfor %}
</div>
{% endspaceless %}

View file

@ -1,12 +1,26 @@
{% load i18n %}
<nav class="pagination" aria-label="pagination">
<a class="pagination-previous" {% if page.has_previous %}href="{{ path }}?{% for k, v in request.GET.items %}{% if k != 'page' %}{{ k }}={{ v }}&{% endif %}{% endfor %}page={{ page.previous_page_number }}{{ anchor }}" {% else %}disabled{% endif %}>
<span class="icon icon-arrow-left"></span>
<a
class="pagination-previous {% if not page.has_previous %}is-disabled{% endif %}"
{% if page.has_previous %}
href="{{ path }}?{% for k, v in request.GET.items %}{% if k != 'page' %}{{ k }}={{ v }}&{% endif %}{% endfor %}page={{ page.previous_page_number }}{{ anchor }}"
{% else %}
aria-hidden="true"
{% endif %}>
<span class="icon icon-arrow-left" aria-hidden="true"></span>
{% trans "Previous" %}
</a>
<a class="pagination-next" {% if page.has_next %}href="{{ path }}?{% for k, v in request.GET.items %}{% if k != 'page' %}{{ k }}={{ v }}&{% endif %}{% endfor %}page={{ page.next_page_number }}{{ anchor }}"{% else %} disabled{% endif %}>
<a
class="pagination-next {% if not page.has_next %}is-disabled{% endif %}"
{% if page.has_next %}
href="{{ path }}?{% for k, v in request.GET.items %}{% if k != 'page' %}{{ k }}={{ v }}&{% endif %}{% endfor %}page={{ page.next_page_number }}{{ anchor }}"
{% else %}
aria-hidden="true"
{% endif %}>
{% trans "Next" %}
<span class="icon icon-arrow-right"></span>
<span class="icon icon-arrow-right" aria-hidden="true"></span>
</a>
</nav>

View file

@ -8,18 +8,8 @@
<input type="hidden" name="user" value="{{ request.user.id }}">
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="privacy" value="public">
<input type="hidden" name="rating" value="{{ forloop.counter }}">
<div class="field is-grouped stars form-rate-stars mb-1 has-text-warning-dark">
<label class="is-sr-only" for="rating-no-rating-{{ book.id }}">{% trans "No rating" %}</label>
<input class="is-sr-only" type="radio" name="rating" value="" id="rating-no-rating-{{ book.id }}" checked>
{% for i in '12345'|make_list %}
<input class="is-sr-only" id="rating-book{{book.id}}-star-{{ forloop.counter }}" type="radio" name="rating" value="{{ forloop.counter }}" {% if book|user_rating:user == forloop.counter %}checked{% endif %}>
<label class="icon icon-star-empty" for="rating-book{{book.id}}-star-{{ forloop.counter }}">
<span class="is-sr-only">{{ forloop.counter }} star{{ forloop.counter | pluralize }}</span>
</label>
{% endfor %}
</div>
{% include 'snippets/form_rate_stars.html' with book=book classes='mb-1 has-text-warning-dark' default_rating=book|user_rating:request.user %}
<div class="field has-addons hidden">
<div class="control">

View file

@ -1,8 +1,27 @@
{% spaceless %}
{% load i18n %}
<p class="stars">
<span class="is-sr-only">{% if rating %}{{ rating|floatformat }} star{{ rating|floatformat | pluralize }}{% else %}{% trans "No rating" %}{% endif %}</span>
{% for i in '12345'|make_list %}
<span class="icon is-small mr-1 icon-star-{% if rating >= forloop.counter %}full{% elif rating|floatformat:0 >= forloop.counter|floatformat:0 %}half{% else %}empty{% endif %}" aria-hidden="true">
<span class="is-sr-only">
{% if rating %}
{% blocktranslate trimmed with rating=rating|floatformat count counter=rating|length %}
{{ rating }} star
{% plural %}
{{ rating }} stars
{% endblocktranslate %}
{% else %}
{% trans "No rating" %}
{% endif %}
</span>
{% for i in '12345'|make_list %}
<span
class="
icon is-small mr-1
icon-star-{% if rating >= forloop.counter %}full{% elif rating|floatformat:0 >= forloop.counter|floatformat:0 %}half{% else %}empty{% endif %}
"
aria-hidden="true"
></span>
{% endfor %}
</p>
{% endspaceless %}

View file

@ -27,7 +27,8 @@
{% trans "Delete status" %}
</button>
</form>
{% elif no_interact %}
{# nothing here #}
{% elif request.user.is_authenticated %}
<div class="field has-addons">
<div class="control">

View file

@ -19,6 +19,16 @@
</button>
</form>
</li>
{% if status.status_type != 'GeneratedNote' and status.status_type != 'Rating' %}
<li role="menuitem">
<form class="dropdown-item pt-0 pb-0" name="delete-{{status.id}}" action="{% url 'redraft' status.id %}" method="post">
{% csrf_token %}
<button class="button is-danger is-light is-fullwidth is-small" type="submit">
{% trans "Delete & re-draft" %}
</button>
</form>
</li>
{% endif %}
{% else %}
{# things you can do to other people's statuses #}
<li role="menuitem">

View file

@ -40,6 +40,7 @@ class StatusViews(TestCase):
remote_id="https://example.com/book/1",
parent_work=work,
)
models.SiteSettings.objects.create()
def test_handle_status(self, _):
""" create a status """
@ -166,6 +167,61 @@ class StatusViews(TestCase):
self.assertFalse(self.remote_user in reply.mention_users.all())
self.assertTrue(self.local_user in reply.mention_users.all())
def test_delete_and_redraft(self, _):
""" delete and re-draft a status """
view = views.DeleteAndRedraft.as_view()
request = self.factory.post("")
request.user = self.local_user
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.Comment.objects.create(
content="hi", book=self.book, user=self.local_user
)
with patch("bookwyrm.activitystreams.ActivityStream.remove_status") as mock:
result = view(request, status.id)
self.assertTrue(mock.called)
result.render()
# make sure it was deleted
status.refresh_from_db()
self.assertTrue(status.deleted)
def test_delete_and_redraft_invalid_status_type_rating(self, _):
""" you can't redraft generated statuses """
view = views.DeleteAndRedraft.as_view()
request = self.factory.post("")
request.user = self.local_user
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.ReviewRating.objects.create(
book=self.book, rating=2.0, user=self.local_user
)
with patch("bookwyrm.activitystreams.ActivityStream.remove_status") as mock:
result = view(request, status.id)
self.assertFalse(mock.called)
self.assertEqual(result.status_code, 400)
status.refresh_from_db()
self.assertFalse(status.deleted)
def test_delete_and_redraft_invalid_status_type_generated_note(self, _):
""" you can't redraft generated statuses """
view = views.DeleteAndRedraft.as_view()
request = self.factory.post("")
request.user = self.local_user
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.GeneratedNote.objects.create(
content="hi", user=self.local_user
)
with patch("bookwyrm.activitystreams.ActivityStream.remove_status") as mock:
result = view(request, status.id)
self.assertFalse(mock.called)
self.assertEqual(result.status_code, 400)
status.refresh_from_db()
self.assertFalse(status.deleted)
def test_find_mentions(self, _):
""" detect and look up @ mentions of users """
user = models.User.objects.create_user(

View file

@ -197,11 +197,31 @@ urlpatterns = [
re_path(r"^block/(?P<user_id>\d+)/?$", views.Block.as_view()),
re_path(r"^unblock/(?P<user_id>\d+)/?$", views.unblock),
# statuses
re_path(r"%s(.json)?/?$" % status_path, views.Status.as_view()),
re_path(r"%s/activity/?$" % status_path, views.Status.as_view()),
re_path(r"%s/replies(.json)?/?$" % status_path, views.Replies.as_view()),
re_path(r"^post/(?P<status_type>\w+)/?$", views.CreateStatus.as_view()),
re_path(r"^delete-status/(?P<status_id>\d+)/?$", views.DeleteStatus.as_view()),
re_path(r"%s(.json)?/?$" % status_path, views.Status.as_view(), name="status"),
re_path(r"%s/activity/?$" % status_path, views.Status.as_view(), name="status"),
re_path(
r"%s/replies(.json)?/?$" % status_path, views.Replies.as_view(), name="replies"
),
re_path(
r"^post/?$",
views.CreateStatus.as_view(),
name="create-status",
),
re_path(
r"^post/(?P<status_type>\w+)/?$",
views.CreateStatus.as_view(),
name="create-status",
),
re_path(
r"^delete-status/(?P<status_id>\d+)/?$",
views.DeleteStatus.as_view(),
name="delete-status",
),
re_path(
r"^redraft-status/(?P<status_id>\d+)/?$",
views.DeleteAndRedraft.as_view(),
name="redraft",
),
# interact
re_path(r"^favorite/(?P<status_id>\d+)/?$", views.Favorite.as_view()),
re_path(r"^unfavorite/(?P<status_id>\d+)/?$", views.Unfavorite.as_view()),

View file

@ -31,7 +31,7 @@ from .shelf import Shelf
from .shelf import create_shelf, delete_shelf
from .shelf import shelve, unshelve
from .site import Site
from .status import CreateStatus, DeleteStatus
from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
from .tag import Tag, AddTag, RemoveTag
from .updates import get_notification_count, get_unread_status_count
from .user import User, EditUser, Followers, Following

View file

@ -99,7 +99,11 @@ class ManageInviteRequests(View):
page = 1
sort = request.GET.get("sort")
sort_fields = ["created_date", "invite__times_used"]
sort_fields = [
"created_date",
"invite__times_used",
"invite__invitees__created_date",
]
if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
sort = "-created_date"
@ -115,7 +119,7 @@ class ManageInviteRequests(View):
if "requested" in status_filters:
filters.append({"invite__isnull": True})
if "sent" in status_filters:
filters.append({"invite__isnull": False})
filters.append({"invite__isnull": False, "invite__times_used": 0})
if "accepted" in status_filters:
filters.append({"invite__isnull": False, "invite__times_used__gte": 1})

View file

@ -3,6 +3,7 @@ import re
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest
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 markdown import markdown
@ -10,7 +11,6 @@ from markdown import markdown
from bookwyrm import forms, models
from bookwyrm.sanitize_html import InputHtmlParser
from bookwyrm.settings import DOMAIN
from bookwyrm.status import delete_status
from bookwyrm.utils import regex
from .helpers import handle_remote_webfinger
from .reading import edit_readthrough
@ -21,6 +21,12 @@ from .reading import edit_readthrough
class CreateStatus(View):
""" the view for *posting* """
def get(self, request):
""" compose view (used for delete-and-redraft """
book = get_object_or_404(models.Edition, id=request.GET.get("book"))
data = {"book": book}
return TemplateResponse(request, "compose.html", data)
def post(self, request, status_type):
""" create status of whatever type """
status_type = status_type[0].upper() + status_type[1:]
@ -69,9 +75,10 @@ class CreateStatus(View):
# update a readthorugh, if needed
edit_readthrough(request)
return redirect(request.headers.get("Referer", "/"))
return redirect("/")
@method_decorator(login_required, name="dispatch")
class DeleteStatus(View):
""" tombstone that bad boy """
@ -84,10 +91,44 @@ class DeleteStatus(View):
return HttpResponseBadRequest()
# perform deletion
delete_status(status)
status.delete()
return redirect(request.headers.get("Referer", "/"))
@method_decorator(login_required, name="dispatch")
class DeleteAndRedraft(View):
""" delete a status but let the user re-create it """
def post(self, request, status_id):
""" delete and tombstone a status """
status = get_object_or_404(
models.Status.objects.select_subclasses(), id=status_id
)
if isinstance(status, (models.GeneratedNote, models.ReviewRating)):
return HttpResponseBadRequest()
# don't let people redraft other people's statuses
if status.user != request.user:
return HttpResponseBadRequest()
status_type = status.status_type.lower()
if status.reply_parent:
status_type = "reply"
data = {
"draft": status,
"type": status_type,
}
if hasattr(status, "book"):
data["book"] = status.book
elif status.mention_books:
data["book"] = status.mention_books.first()
# perform deletion
status.delete()
return TemplateResponse(request, "compose.html", data)
def find_mentions(content):
""" detect @mentions in raw status content """
if not content:

2
bw-dev
View file

@ -76,7 +76,7 @@ case "$CMD" in
docker-compose exec web python manage.py collectstatic --no-input
docker-compose restart
;;
populate_feeds)
populate_streams)
execweb python manage.py populate_streams
;;
*)

Binary file not shown.

View file

@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.1.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-01 13:14-0700\n"
"PO-Revision-Date: 2021-03-02 12:37+0100\n"
"POT-Creation-Date: 2021-04-04 13:04+0000\n"
"PO-Revision-Date: 2021-04-04 14:43+0100\n"
"Last-Translator: Fabien Basmaison <contact@arkhi.org>\n"
"Language-Team: Mouse Reeve <LL@li.org>\n"
"Language: fr_FR\n"
@ -59,7 +59,7 @@ msgstr ""
msgid "%(value)s is not a valid username"
msgstr ""
#: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:157
#: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:152
#, fuzzy
#| msgid "Username:"
msgid "username"
@ -185,7 +185,7 @@ msgstr "Description:"
#: bookwyrm/templates/edit_author.html:78 bookwyrm/templates/lists/form.html:42
#: bookwyrm/templates/preferences/edit_user.html:70
#: bookwyrm/templates/settings/site.html:93
#: bookwyrm/templates/snippets/readthrough.html:65
#: bookwyrm/templates/snippets/readthrough.html:75
#: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:42
#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:42
#: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:34
@ -199,7 +199,7 @@ msgstr "Enregistrer"
#: bookwyrm/templates/moderation/report_modal.html:32
#: bookwyrm/templates/snippets/delete_readthrough_modal.html:17
#: bookwyrm/templates/snippets/goal_form.html:32
#: bookwyrm/templates/snippets/readthrough.html:66
#: bookwyrm/templates/snippets/readthrough.html:76
#: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:43
#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:43
#: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:35
@ -537,7 +537,7 @@ msgstr "Fédéré"
#: bookwyrm/templates/directory/directory.html:6
#: bookwyrm/templates/directory/directory.html:11
#: bookwyrm/templates/layout.html:97
#: bookwyrm/templates/layout.html:92
msgid "Directory"
msgstr ""
@ -1082,7 +1082,7 @@ msgid "%(username)s's %(year)s Books"
msgstr "Livres de %(username)s en %(year)s"
#: bookwyrm/templates/import.html:5 bookwyrm/templates/import.html:9
#: bookwyrm/templates/layout.html:102
#: bookwyrm/templates/layout.html:97
msgid "Import Books"
msgstr "Importer des livres"
@ -1223,18 +1223,13 @@ msgstr "Menu de navigation principal "
msgid "Feed"
msgstr "Fil dactualité"
#: bookwyrm/templates/layout.html:92
#: bookwyrm/templates/preferences/preferences_layout.html:14
msgid "Profile"
msgstr "Profil"
#: bookwyrm/templates/layout.html:107
#: bookwyrm/templates/layout.html:102
#, fuzzy
#| msgid "Instance Settings"
msgid "Settings"
msgstr "Paramètres de linstance"
#: bookwyrm/templates/layout.html:116
#: bookwyrm/templates/layout.html:111
#: bookwyrm/templates/settings/admin_layout.html:24
#: bookwyrm/templates/settings/manage_invite_requests.html:15
#: bookwyrm/templates/settings/manage_invites.html:3
@ -1242,59 +1237,59 @@ msgstr "Paramètres de linstance"
msgid "Invites"
msgstr "Invitations"
#: bookwyrm/templates/layout.html:123
#: bookwyrm/templates/layout.html:118
msgid "Admin"
msgstr ""
#: bookwyrm/templates/layout.html:130
#: bookwyrm/templates/layout.html:125
msgid "Log out"
msgstr "Se déconnecter"
#: bookwyrm/templates/layout.html:138 bookwyrm/templates/layout.html:139
#: bookwyrm/templates/layout.html:133 bookwyrm/templates/layout.html:134
#: bookwyrm/templates/notifications.html:6
#: bookwyrm/templates/notifications.html:10
msgid "Notifications"
msgstr "Notifications"
#: bookwyrm/templates/layout.html:156 bookwyrm/templates/layout.html:160
#: bookwyrm/templates/layout.html:151 bookwyrm/templates/layout.html:155
#: bookwyrm/templates/login.html:17
#: bookwyrm/templates/snippets/register_form.html:4
msgid "Username:"
msgstr "Nom dutilisateur:"
#: bookwyrm/templates/layout.html:161
#: bookwyrm/templates/layout.html:156
#, fuzzy
#| msgid "Password:"
msgid "password"
msgstr "Mot de passe:"
#: bookwyrm/templates/layout.html:162 bookwyrm/templates/login.html:36
#: bookwyrm/templates/layout.html:157 bookwyrm/templates/login.html:36
msgid "Forgot your password?"
msgstr "Mot de passe oublié?"
#: bookwyrm/templates/layout.html:165 bookwyrm/templates/login.html:10
#: bookwyrm/templates/layout.html:160 bookwyrm/templates/login.html:10
#: bookwyrm/templates/login.html:33
msgid "Log in"
msgstr "Se connecter"
#: bookwyrm/templates/layout.html:173
#: bookwyrm/templates/layout.html:168
msgid "Join"
msgstr ""
#: bookwyrm/templates/layout.html:196
#: bookwyrm/templates/layout.html:191
msgid "About this server"
msgstr "À propos de ce serveur"
#: bookwyrm/templates/layout.html:200
#: bookwyrm/templates/layout.html:195
msgid "Contact site admin"
msgstr "Contacter ladministrateur du site"
#: bookwyrm/templates/layout.html:207
#: bookwyrm/templates/layout.html:202
#, python-format
msgid "Support %(site_name)s on <a href=\"%(support_link)s\" target=\"_blank\">%(support_title)s</a>"
msgstr ""
#: bookwyrm/templates/layout.html:211
#: bookwyrm/templates/layout.html:206
msgid "BookWyrm is open source software. You can contribute or report issues on <a href=\"https://github.com/mouse-reeve/bookwyrm\">GitHub</a>."
msgstr "Bookwyrm est un logiciel libre. Vous pouvez contribuer ou faire des rapports de bogues via <a href=\"https://github.com/mouse-reeve/bookwyrm\">GitHub</a>."
@ -1384,7 +1379,7 @@ msgid "Added by <a href=\"%(user_path)s\">%(username)s</a>"
msgstr "Messages directs avec <a href=\"%(path)s\">%(username)s</a>"
#: bookwyrm/templates/lists/list.html:41
#: bookwyrm/templates/snippets/shelf_selector.html:28
#: bookwyrm/templates/snippets/shelf_selector.html:26
msgid "Remove"
msgstr "Supprimer"
@ -1473,7 +1468,7 @@ msgstr ""
#: bookwyrm/templates/moderation/report.html:54
#: bookwyrm/templates/snippets/create_status.html:12
#: bookwyrm/templates/snippets/create_status_form.html:52
#: bookwyrm/templates/snippets/create_status_form.html:44
msgid "Comment"
msgstr "Commentaire"
@ -1730,6 +1725,10 @@ msgstr ""
msgid "Account"
msgstr "Compte"
#: bookwyrm/templates/preferences/preferences_layout.html:14
msgid "Profile"
msgstr "Profil"
#: bookwyrm/templates/preferences/preferences_layout.html:20
msgid "Relationships"
msgstr "Relations"
@ -1888,7 +1887,8 @@ msgid "Software"
msgstr "Logiciel"
#: bookwyrm/templates/settings/federation.html:24
#: bookwyrm/templates/settings/manage_invite_requests.html:33
#: bookwyrm/templates/settings/manage_invite_requests.html:40
#: bookwyrm/templates/settings/status_filter.html:5
#: bookwyrm/templates/settings/user_admin.html:32
msgid "Status"
msgstr "Statut"
@ -1906,61 +1906,64 @@ msgstr "Invitations"
msgid "Ignored Invite Requests"
msgstr ""
#: bookwyrm/templates/settings/manage_invite_requests.html:31
#: bookwyrm/templates/settings/manage_invite_requests.html:35
msgid "Date"
msgstr ""
#: bookwyrm/templates/settings/manage_invite_requests.html:32
#: bookwyrm/templates/settings/manage_invite_requests.html:38
msgid "Email"
msgstr ""
#: bookwyrm/templates/settings/manage_invite_requests.html:34
#: bookwyrm/templates/settings/manage_invite_requests.html:43
#, fuzzy
#| msgid "Notifications"
msgid "Action"
msgstr "Notifications"
#: bookwyrm/templates/settings/manage_invite_requests.html:37
#: bookwyrm/templates/settings/manage_invite_requests.html:46
#, fuzzy
#| msgid "Follow Requests"
msgid "No requests"
msgstr "Demandes dabonnement"
#: bookwyrm/templates/settings/manage_invite_requests.html:45
#: bookwyrm/templates/settings/manage_invite_requests.html:54
#: bookwyrm/templates/settings/status_filter.html:16
#, fuzzy
#| msgid "Accept"
msgid "Accepted"
msgstr "Accepter"
#: bookwyrm/templates/settings/manage_invite_requests.html:47
#: bookwyrm/templates/settings/manage_invite_requests.html:56
#: bookwyrm/templates/settings/status_filter.html:12
msgid "Sent"
msgstr ""
#: bookwyrm/templates/settings/manage_invite_requests.html:49
#: bookwyrm/templates/settings/manage_invite_requests.html:58
#: bookwyrm/templates/settings/status_filter.html:8
msgid "Requested"
msgstr ""
#: bookwyrm/templates/settings/manage_invite_requests.html:57
#: bookwyrm/templates/settings/manage_invite_requests.html:68
msgid "Send invite"
msgstr ""
#: bookwyrm/templates/settings/manage_invite_requests.html:59
#: bookwyrm/templates/settings/manage_invite_requests.html:70
msgid "Re-send invite"
msgstr ""
#: bookwyrm/templates/settings/manage_invite_requests.html:70
#: bookwyrm/templates/settings/manage_invite_requests.html:90
msgid "Ignore"
msgstr ""
#: bookwyrm/templates/settings/manage_invite_requests.html:72
msgid "Un-gnore"
#: bookwyrm/templates/settings/manage_invite_requests.html:92
msgid "Un-ignore"
msgstr ""
#: bookwyrm/templates/settings/manage_invite_requests.html:83
#: bookwyrm/templates/settings/manage_invite_requests.html:103
msgid "Back to pending requests"
msgstr ""
#: bookwyrm/templates/settings/manage_invite_requests.html:85
#: bookwyrm/templates/settings/manage_invite_requests.html:105
msgid "View ignored requests"
msgstr ""
@ -2161,47 +2164,41 @@ msgstr "Critique"
msgid "Rating"
msgstr "Note"
#: bookwyrm/templates/snippets/create_status_form.html:31
#: bookwyrm/templates/snippets/rate_action.html:14
#: bookwyrm/templates/snippets/stars.html:3
msgid "No rating"
msgstr "Aucune note"
#: bookwyrm/templates/snippets/create_status_form.html:64
#: bookwyrm/templates/snippets/create_status_form.html:56
#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:16
msgid "Progress:"
msgstr "Progression:"
#: bookwyrm/templates/snippets/create_status_form.html:71
#: bookwyrm/templates/snippets/create_status_form.html:63
#: bookwyrm/templates/snippets/readthrough_form.html:22
#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:30
msgid "pages"
msgstr "pages"
#: bookwyrm/templates/snippets/create_status_form.html:72
#: bookwyrm/templates/snippets/create_status_form.html:64
#: bookwyrm/templates/snippets/readthrough_form.html:23
#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:31
msgid "percent"
msgstr "pourcent"
#: bookwyrm/templates/snippets/create_status_form.html:77
#: bookwyrm/templates/snippets/create_status_form.html:69
#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:36
#, python-format
msgid "of %(pages)s pages"
msgstr "sur %(pages)s pages"
#: bookwyrm/templates/snippets/create_status_form.html:89
#: bookwyrm/templates/snippets/create_status_form.html:81
msgid "Include spoiler alert"
msgstr "Afficher une alerte spoiler"
#: bookwyrm/templates/snippets/create_status_form.html:95
#: bookwyrm/templates/snippets/create_status_form.html:87
#: bookwyrm/templates/snippets/privacy-icons.html:15
#: bookwyrm/templates/snippets/privacy-icons.html:16
#: bookwyrm/templates/snippets/privacy_select.html:19
msgid "Private"
msgstr "Privé"
#: bookwyrm/templates/snippets/create_status_form.html:102
#: bookwyrm/templates/snippets/create_status_form.html:94
msgid "Post"
msgstr "Publier"
@ -2241,11 +2238,11 @@ msgstr "Replier"
msgid "Hide filters"
msgstr ""
#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:19
#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:22
msgid "Apply filters"
msgstr ""
#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:23
#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:26
#, fuzzy
#| msgid "Clear search"
msgid "Clear filters"
@ -2269,6 +2266,19 @@ msgstr "Se désabonner"
msgid "Accept"
msgstr "Accepter"
#: bookwyrm/templates/snippets/form_rate_stars.html:20
#: bookwyrm/templates/snippets/stars.html:13
msgid "No rating"
msgstr "Aucune note"
#: bookwyrm/templates/snippets/form_rate_stars.html:45
#: bookwyrm/templates/snippets/stars.html:7
#, python-format
msgid "%(rating)s star"
msgid_plural "%(rating)s stars"
msgstr[0] "%(rating)s étoile"
msgstr[1] "%(rating)s étoiles"
#: bookwyrm/templates/snippets/generated_status/goal.html:1
#, python-format
msgid "set a goal to read %(counter)s book in %(year)s"
@ -2343,11 +2353,23 @@ msgstr "Vous avez lu <a href=\"%(path)s\">%(read_count)s sur %(goal_count)s livr
msgid "%(username)s has read <a href=\"%(path)s\">%(read_count)s of %(goal_count)s books</a>."
msgstr "%(username)s a lu <a href=\"%(path)s\">%(read_count)s sur %(goal_count)s livres</a>."
#: bookwyrm/templates/snippets/pagination.html:7
#: bookwyrm/templates/snippets/page_text.html:4
#, fuzzy, python-format
#| msgid "of %(pages)s pages"
msgid "page %(page)s of %(total_pages)s"
msgstr "sur %(pages)s pages"
#: bookwyrm/templates/snippets/page_text.html:6
#, fuzzy, python-format
#| msgid "of %(book.pages)s pages"
msgid "page %(page)s"
msgstr "sur %(book.pages)s pages"
#: bookwyrm/templates/snippets/pagination.html:5
msgid "Previous"
msgstr "Précédente"
#: bookwyrm/templates/snippets/pagination.html:15
#: bookwyrm/templates/snippets/pagination.html:9
msgid "Next"
msgstr "Suivante"
@ -2380,7 +2402,7 @@ msgstr "Abonnements"
msgid "Leave a rating"
msgstr "Laisser une note"
#: bookwyrm/templates/snippets/rate_action.html:29
#: bookwyrm/templates/snippets/rate_action.html:19
msgid "Rate"
msgstr "Noter"
@ -2388,28 +2410,28 @@ msgstr "Noter"
msgid "Progress Updates:"
msgstr "Progression:"
#: bookwyrm/templates/snippets/readthrough.html:12
#: bookwyrm/templates/snippets/readthrough.html:14
msgid "finished"
msgstr "terminé"
#: bookwyrm/templates/snippets/readthrough.html:15
#: bookwyrm/templates/snippets/readthrough.html:25
msgid "Show all updates"
msgstr "Montrer toutes les progressions"
#: bookwyrm/templates/snippets/readthrough.html:31
#: bookwyrm/templates/snippets/readthrough.html:41
msgid "Delete this progress update"
msgstr "Supprimer cette mise à jour"
#: bookwyrm/templates/snippets/readthrough.html:41
#: bookwyrm/templates/snippets/readthrough.html:51
msgid "started"
msgstr "commencé"
#: bookwyrm/templates/snippets/readthrough.html:47
#: bookwyrm/templates/snippets/readthrough.html:61
#: bookwyrm/templates/snippets/readthrough.html:57
#: bookwyrm/templates/snippets/readthrough.html:71
msgid "Edit read dates"
msgstr "Modifier les date de lecture"
#: bookwyrm/templates/snippets/readthrough.html:51
#: bookwyrm/templates/snippets/readthrough.html:61
#, fuzzy
#| msgid "Delete these read dates?"
msgid "Delete these read dates"
@ -2696,11 +2718,11 @@ msgstr "Commencé"
msgid "Finished"
msgstr "Terminé"
#: bookwyrm/templates/user/shelf.html:127
#: bookwyrm/templates/user/shelf.html:129
msgid "This shelf is empty."
msgstr "Cette étagère est vide"
#: bookwyrm/templates/user/shelf.html:133
#: bookwyrm/templates/user/shelf.html:135
msgid "Delete shelf"
msgstr "Supprimer létagère"
@ -2781,7 +2803,7 @@ msgstr ""
#~ msgid "Getting Started"
#~ msgstr "Commencé"
#, fuzzy, python-format
#, fuzzy
#~| msgid "No users found for \"%(query)s\""
#~ msgid "No users were found for \"%(query)s\""
#~ msgstr "Aucun compte trouvé pour « %(query)s»"
@ -2795,7 +2817,7 @@ msgstr ""
#~ msgid "Your lists"
#~ msgstr "Vos listes"
#, fuzzy, python-format
#, fuzzy
#~| msgid "See all %(size)s"
#~ msgid "See all %(size)s lists"
#~ msgstr "Voir les %(size)s"
@ -2821,14 +2843,12 @@ msgstr ""
#~ msgid "Your Shelves"
#~ msgstr "Vos étagères"
#, python-format
#~ msgid "%(username)s: Shelves"
#~ msgstr "%(username)s: Étagères"
#~ msgid "Shelves"
#~ msgstr "Étagères"
#, python-format
#~ msgid "See all %(shelf_count)s shelves"
#~ msgstr "Voir les %(shelf_count)s étagères"

Binary file not shown.

File diff suppressed because it is too large Load diff