Merge branch 'main' into prettier

This commit is contained in:
Mouse Reeve 2021-12-27 13:39:34 -08:00
commit 1be164425a
37 changed files with 1214 additions and 126 deletions

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.5 on 2021-12-22 11:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0120_list_embed_key"),
]
operations = [
migrations.AddField(
model_name="user",
name="summary_keys",
field=models.JSONField(null=True),
),
]

View file

@ -148,6 +148,8 @@ class User(OrderedCollectionPageMixin, AbstractUser):
size=8,
default=get_feed_filter_choices,
)
# annual summary keys
summary_keys = models.JSONField(null=True)
preferred_timezone = models.CharField(
choices=[(str(tz), str(tz)) for tz in pytz.all_timezones],

View file

@ -93,6 +93,9 @@ body {
display: inline !important;
}
/** File input styles
******************************************************************************/
input[type=file]::file-selector-button {
-moz-appearance: none;
-webkit-appearance: none;
@ -119,17 +122,15 @@ input[type=file]::file-selector-button:hover {
color: #363636;
}
details .dropdown-menu {
display: block !important;
/** General `details` element styles
******************************************************************************/
summary {
cursor: pointer;
}
details.dropdown[open] summary.dropdown-trigger::before {
content: "";
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
summary::-webkit-details-marker {
display: none;
}
summary::marker {
@ -147,6 +148,57 @@ summary::marker {
margin-top: 1em;
}
/** Details dropdown
******************************************************************************/
details.dropdown[open] summary.dropdown-trigger::before {
content: "";
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
details .dropdown-menu {
display: block !important;
}
details .dropdown-menu button {
/* Fix weird Safari defaults */
box-sizing: border-box;
}
details.dropdown .dropdown-menu button:focus-visible,
details.dropdown .dropdown-menu a:focus-visible {
outline-style: auto;
outline-offset: -2px;
}
@media only screen and (max-width: 768px) {
details.dropdown[open] summary.dropdown-trigger::before {
background-color: rgba(0, 0, 0, 0.5);
z-index: 30;
}
details .dropdown-menu {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex !important;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 100;
}
details .dropdown-menu > * {
pointer-events: all;
}
}
/** Shelving
******************************************************************************/
@ -555,6 +607,68 @@ ol.ordered-list li::before {
padding: 0 0.75em;
}
/* Breadcrumbs
******************************************************************************/
.books-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(10em, 1fr));
gap: 1.5rem;
align-items: end;
justify-items: center;
}
.books-grid > .is-big {
grid-column: span 2;
grid-row: span 2;
justify-self: stretch;
padding: 1.5rem 1.5rem 0;
}
.books-grid .book-cover {
width: 100%;
}
.books-grid .book-title {
--height-basis: 1.35rem;
display: block;
margin-top: 0.5rem;
line-height: var(--height-basis);
min-height: calc(2 * var(--height-basis));
}
/* Copy
******************************************************************************/
.horizontal-copy {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.75rem;
}
.horizontal-copy textarea {
min-width: initial;
white-space: nowrap;
}
.horizontal-copy button {
align-self: stretch;
height: unset;
}
.vertical-copy {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
.vertical-copy button {
width: 100%;
}
/* Dimensions
* @todo These could be in rem.
******************************************************************************/

View file

@ -0,0 +1,95 @@
Copyright 2014-2018 Adobe (http://www.adobe.com/), with Reserved Font Name
'Source'. All Rights Reserved. Source is a trademark of Adobe in the United
States and/or other countries. Copyright 2019 Google LLC.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -0,0 +1,30 @@
# Font Squirrel Font-face Generator Configuration File
# Upload this file to the generator to recreate the settings
# you used to create these fonts.
{
"mode": "optimal",
"formats":
[
"woff",
"woff2"
],
"tt_instructor": "default",
"fix_gasp": "xy",
"fix_vertical_metrics": "Y",
"metrics_ascent": "",
"metrics_descent": "",
"metrics_linegap": "",
"add_spaces": "Y",
"add_hyphens": "Y",
"fallback": "none",
"fallback_custom": "100",
"options_subset": "basic",
"subset_custom": "",
"subset_custom_range": "",
"subset_ot_features_list": "",
"css_stylesheet": "stylesheet.css",
"filename_suffix": "-webfont",
"emsquare": "2048",
"spacing_adjustment": "0"
}

View file

@ -0,0 +1,19 @@
@font-face {
font-family: 'dm_serif_display';
src: url('../fonts/dm_serif_display/dmserifdisplay-italic-webfont.woff2') format('woff2'),
url('../fonts/dm_serif_display/dmserifdisplay-italic-webfont.woff') format('woff');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: 'dm_serif_display';
src: url('../fonts/dm_serif_display/dmserifdisplay-regular-webfont.woff2') format('woff2'),
url('../fonts/dm_serif_display/dmserifdisplay-regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
.is-serif {
font-family: 'dm_serif_display', Georgia, serif;
}

View file

@ -37,6 +37,11 @@ let BookWyrm = new (class {
document
.querySelectorAll("[data-duplicate]")
.forEach((node) => node.addEventListener("click", this.duplicateInput.bind(this)));
document
.querySelectorAll("details.dropdown")
.forEach((node) =>
node.addEventListener("toggle", this.handleDetailsDropdown.bind(this))
);
}
/**
@ -441,14 +446,7 @@ let BookWyrm = new (class {
const copyButtonEl = document.createElement("button");
copyButtonEl.textContent = textareaEl.dataset.copytextLabel;
copyButtonEl.classList.add(
"mt-2",
"button",
"is-small",
"is-fullwidth",
"is-primary",
"is-light"
);
copyButtonEl.classList.add("button", "is-small", "is-primary", "is-light");
copyButtonEl.addEventListener("click", () => {
navigator.clipboard.writeText(text).then(function () {
textareaEl.classList.add("is-success");
@ -459,4 +457,101 @@ let BookWyrm = new (class {
textareaEl.parentNode.appendChild(copyButtonEl);
}
/**
* Handle the details dropdown component.
*
* @param {Event} event - Event fired by a `details` element
* with the `dropdown` class name, on toggle.
* @return {undefined}
*/
handleDetailsDropdown(event) {
const detailsElement = event.target;
const summaryElement = detailsElement.querySelector("summary");
const menuElement = detailsElement.querySelector(".dropdown-menu");
const htmlElement = document.querySelector("html");
if (detailsElement.open) {
// Focus first menu element
menuElement
.querySelectorAll("a[href]:not([disabled]), button:not([disabled])")[0]
.focus();
// Enable focus trap
menuElement.addEventListener("keydown", this.handleFocusTrap);
// Close on Esc
detailsElement.addEventListener("keydown", handleEscKey);
// Clip page if Mobile
if (this.isMobile()) {
htmlElement.classList.add("is-clipped");
}
} else {
summaryElement.focus();
// Disable focus trap
menuElement.removeEventListener("keydown", this.handleFocusTrap);
// Unclip page
if (this.isMobile()) {
htmlElement.classList.remove("is-clipped");
}
}
function handleEscKey(event) {
if (event.key !== "Escape") {
return;
}
summaryElement.click();
}
}
/**
* Check if windows matches mobile media query.
*
* @return {Boolean}
*/
isMobile() {
return window.matchMedia("(max-width: 768px)").matches;
}
/**
* Focus trap handler
*
* @param {Event} event - Keydown event.
* @return {undefined}
*/
handleFocusTrap(event) {
if (event.key !== "Tab") {
return;
}
const focusableEls = event.currentTarget.querySelectorAll(
[
"a[href]:not([disabled])",
"button:not([disabled])",
"textarea:not([disabled])",
'input:not([type="hidden"]):not([disabled])',
"select:not([disabled])",
"details:not([disabled])",
'[tabindex]:not([tabindex="-1"]):not([disabled])',
].join(",")
);
const firstFocusableEl = focusableEls[0];
const lastFocusableEl = focusableEls[focusableEls.length - 1];
if (event.shiftKey) {
/* Shift + tab */ if (document.activeElement === firstFocusableEl) {
lastFocusableEl.focus();
event.preventDefault();
}
} /* Tab */ else {
if (document.activeElement === lastFocusableEl) {
firstFocusableEl.focus();
event.preventDefault();
}
}
}
})();

View file

@ -1,31 +0,0 @@
(function () {
"use strict";
/**
* Toggle all descendant checkboxes of a target.
*
* Use `data-target="ID_OF_TARGET"` on the node on which the event is listened
* to (checkbox, button, link), where_ID_OF_TARGET_ should be the ID of an
* ancestor for the checkboxes.
*
* @example
* <input
* type="checkbox"
* data-action="toggle-all"
* data-target="failed-imports"
* >
* @param {Event} event
* @return {undefined}
*/
function toggleAllCheckboxes(event) {
const mainCheckbox = event.target;
document
.querySelectorAll(`#${mainCheckbox.dataset.target} [type="checkbox"]`)
.forEach((checkbox) => (checkbox.checked = mainCheckbox.checked));
}
document.querySelectorAll('[data-action="toggle-all"]').forEach((input) => {
input.addEventListener("change", toggleAllCheckboxes);
});
})();

View file

@ -0,0 +1,256 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load static %}
{% block title %}{% blocktrans %}{{ year }} in the books{% endblocktrans %}{% endblock %}
{% block head_links %}
<link rel="stylesheet" href="{% static "css/vendor/dm_serif_display.css" %}">
{% endblock %}
{% block content %}
{% with display_name=summary_user.display_name %}
{% if user == summary_user %}
<div class="columns">
{% with year=paginated_years|first %}
{% if year %}
<div class="column">
<a href="{% url 'annual-summary' summary_user.localname year %}">
<span class="icon icon-arrow-left" aria-hidden="true"></span>
{{ year }}
</a>
</div>
{% endif %}
{% endwith %}
{% with year=paginated_years|last %}
{% if year %}
<div class="column has-text-right">
<a href="{% url 'annual-summary' summary_user.localname year %}">
{{ year }}
<span class="icon icon-arrow-right" aria-hidden="true"></span>
</a>
</div>
{% endif %}
{% endwith %}
</div>
{% endif %}
<h1 class="title is-1 is-serif has-text-centered">
📚✨
{% blocktrans %}{{ year }} <em>in the books</em>{% endblocktrans %}
✨📚
</h1>
<p class="subtitle is-3 is-serif has-text-centered mb-5">
{% blocktrans %}<em>{{ display_name }}s</em> year of reading{% endblocktrans %}
</p>
<details>
<summary class="has-text-centered">
<span role="heading" aria-level="2" class="title is-6 has-text-success-dark">
{% trans "Share this page" %}
</span>
</summary>
<div class="columns mt-3">
<div class="column is-three-fifths is-offset-one-fifth">
{% if year_key %}
<div class="horizontal-copy mb-5">
<textarea rows="1" readonly class="textarea is-small" aria-labelledby="embed-label" data-copytext data-copytext-label="{% trans 'Copy address' %}" data-copytext-success="{% trans 'Copied!' %}">{{ request.scheme|add:"://"|add:request.get_host|add:request.path }}?key={{ year_key }}</textarea>
</div>
{% endif %}
{% if user == summary_user %}
{% if year_key %}
<div class="columns mb-2">
<div class="column pb-0">
<p>{% trans "Sharing status: <strong>public with key</strong>" %}</p>
<p>{% trans "The page can be seen by anyone with the complete address." %}</p>
</div>
<form class="column pb-0 is-narrow" method="post" action="{% url "summary-revoke-key" %}" id="revoke-key">
{% csrf_token %}
<input type="hidden" name="year" value="{{ year }}" />
<button class="button is-danger is-outlined" type="submit">{% trans "Make page private" %}</button>
</form>
</div>
{% else %}
<div class="columns">
<div class="column pb-0">
<p>{% trans "Sharing status: <strong>private</strong>" %}</p>
<p>{% trans "The page is private, only you can see it." %}</p>
</div>
<form class="column pb-0 is-narrow" method="post" action="{% url "summary-add-key" %}" id="add-key">
{% csrf_token %}
<input type="hidden" name="year" value="{{ year }}" />
<button class="button is-primary is-outlined" type="submit">{% trans "Make page public" %}</button>
</form>
</div>
{% endif %}
<p class="help">{% trans "When you make your page private, the old key wont give access to the page anymore. A new key will be created if the page is once again made public." %}</p>
{% endif %}
</div>
</div>
</details>
<div class="columns mt-1">
<div class="column is-one-fifth is-offset-two-fifths">
<hr />
</div>
</div>
{% if not books %}
<p class="has-text-centered is-size-5">{% blocktrans %}Sadly {{ display_name }} didnt finish any book in {{ year }}{% endblocktrans %}</p>
{% else %}
<div class="columns is-mobile">
<div class="column is-8 is-offset-2 has-text-centered">
<h2 class="title is-3 is-serif">
{% blocktrans %}In {{ year }}, {{ display_name }} read {{ books_total }} books<br />for a total of {{ pages_total }} pages!{% endblocktrans %}
</h2>
<p class="subtitle is-5">{% trans "Thats great!" %}</p>
<p class="title is-4 is-serif">
{% blocktrans %}That makes an average of {{ pages_average }} pages per book.{% endblocktrans %}
</p>
{% if no_page_number %}
<p class="subtitle is-6">
{% blocktrans trimmed count counter=no_page_number %}
({{ no_page_number }} book doesnt have pages)
{% plural %}
({{ no_page_number }} books dont have pages)
{% endblocktrans %}
</p>
{% endif %}
</div>
</div>
{% if book_pages_lowest and book_pages_highest %}
<div class="columns is-mobile is-align-items-center mt-5">
<div class="column is-2 is-offset-1">
<a href="{{ book_pages_lowest.local_path }}">{% include 'snippets/book_cover.html' with book=book_pages_lowest cover_class='is-w-auto-tablet is-h-l-mobile' %}</a>
</div>
<div class="column is-3">
{% trans "Their shortest read this year…" %}
<p class="title is-4 is-serif is-italic">
<a href="{{ book_pages_lowest.local_path }}" class="has-text-success-dark">
{{ book_pages_lowest.title }}
</a>
</p>
{% if book_pages_lowest.authors.exists %}
<p class="subtitle is-5 mb-2">{% trans "by" %}
{% include 'snippets/authors.html' with book=book_pages_lowest link_class="has-text-success-dark" %}
</p>
{% endif %}
<p class="subtitle is-6">
{% with pages=book_pages_lowest.pages %}
{% blocktrans %}<strong>{{ pages }}</strong> pages{% endblocktrans%}
{% endwith %}
</p>
</div>
<div class="column is-2">
<a href="{{ book_pages_highest.local_path }}">{% include 'snippets/book_cover.html' with book=book_pages_highest cover_class='is-w-auto-tablet is-h-l-mobile' %}</a>
</div>
<div class="column is-3">
{% trans "…and the longest" %}
<p class="title is-4 is-serif is-italic">
<a href="{{ book_pages_lowest.local_path }}" class="has-text-success-dark">
{{ book_pages_highest.title }}
</a>
</p>
{% if book_pages_highest.authors.exists %}
<p class="subtitle is-5 mb-2">{% trans "by" %}
{% include 'snippets/authors.html' with book=book_pages_highest link_class="has-text-success-dark" %}
</p>
{% endif %}
<p class="subtitle is-6">
{% with pages=book_pages_highest.pages %}
{% blocktrans %}<strong>{{ pages }}</strong> pages{% endblocktrans%}
{% endwith %}
</p>
</div>
</div>
{% endif %}
<div class="columns">
<div class="column is-one-fifth is-offset-two-fifths">
<hr />
</div>
</div>
{% if ratings_total > 0 %}
<div class="columns">
<div class="column has-text-centered">
<h2 class="title is-3 is-serif">
{% blocktrans %}{{ display_name }} left {{ ratings_total }} ratings, <br />their average rating is {{ rating_average }}{% endblocktrans %}
</h2>
</div>
</div>
<div class="columns is-align-items-center">
<div class="column is-3 is-offset-3">
<a href="{{ book_rating_highest.book.local_path }}">{% include 'snippets/book_cover.html' with book=book_rating_highest.book cover_class='is-w-auto-tablet is-h-l-mobile' %}</a>
</div>
{% if book_rating_highest %}
<div class="column is-4">
{% trans "Their best rated review" %}
<p class="title is-4 is-serif is-italic">
<a href="{{ book_rating_highest.book.local_path }}" class="has-text-success-dark">
{{ book_rating_highest.book.title }}
</a>
</p>
{% if book_rating_highest.book.authors.exists %}
<p class="subtitle is-5 mb-2">{% trans "by" %}
{% include 'snippets/authors.html' with book=book_rating_highest.book link_class="has-text-success-dark" %}
</p>
{% endif %}
<p class="subtitle is-6">
{% with rating=book_rating_highest.rating|floatformat %}
{% blocktrans %}Their rating: <strong>{{ rating }}</strong>{% endblocktrans%}
{% endwith %}
</p>
</div>
{% endif %}
</div>
<div class="columns">
<div class="column is-one-fifth is-offset-two-fifths">
<hr />
</div>
</div>
{% endif %}
<div class="columns">
<div class="column has-text-centered">
<h2 class="title is-3 is-serif">
{% blocktrans %}All the books {{ display_name }} read in 2021{% endblocktrans %}
</h2>
</div>
</div>
<div class="columns">
<div class="column is-10 is-offset-1">
<div class="books-grid">
{% for book in books %}
{% if book.id in best_ratings_books_ids %}
<a href="{{ book.local_path }}" class="has-text-centered is-big has-text-success-dark">
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto' %}
<span class="book-title is-serif is-size-5">
{{ book.title }}
</span>
</a>
{% else %}
<a href="{{ book.local_path }}" class="has-text-centered has-text-success-dark">
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto' %}
<span class="book-title is-serif is-size-6">
{{ book.title }}
</span>
</a>
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% endwith %}
{% endblock %}

View file

@ -7,6 +7,7 @@
class="
dropdown control
{% if right %}is-right{% endif %}
has-text-left
"
>
<summary
@ -15,7 +16,7 @@
{% block dropdown-trigger %}{% endblock %}
</summary>
<div class="dropdown-menu control">
<div class="dropdown-menu">
<ul
id="menu_options_{{ uuid }}"
class="dropdown-content p-0 is-clipped"

View file

@ -64,14 +64,20 @@
{{ allowed_status_types|json_script:"unread-notifications-wrapper" }}
</a>
{% if request.user.show_goal and not goal and tab.key == 'home' %}
{% now 'Y' as year %}
<section class="block">
{% include 'feed/goal_card.html' with year=year %}
<hr>
</section>
{% endif %}
{% if request.user.show_goal and not goal and tab.key == 'home' %}
{% now 'Y' as year %}
<section class="block">
{% include 'feed/goal_card.html' with year=year %}
<hr>
</section>
{% endif %}
{% if annual_summary_year and tab.key == 'home' %}
<section class="block">
{% include 'feed/summary_card.html' with year=annual_summary_year %}
<hr>
</section>
{% endif %}
{% endif %}
{# activity feed #}

View file

@ -0,0 +1,22 @@
{% extends 'components/card.html' %}
{% load i18n %}
{% block card-header %}
<h3 class="card-header-title has-background-success-dark has-text-white">
<span class="icon is-size-3 mr-2" aria-hidden="true">📚</span>
<span class="icon is-size-3 mr-2" aria-hidden="true"></span>
{% blocktrans %}{{ year }} in the books{% endblocktrans %}
</h3>
{% endblock %}
{% block card-content %}
<p class="mb-3">
{% blocktrans %}The end of the year is the best moment to take stock of all the books read during the last 12 months. How many pages have you read? Which book is your best-rated of the year? We compiled these stats, and more!{% endblocktrans %}
</p>
<p>
<a href="{% url 'annual-summary' request.user.localname year %}" class="button is-success has-background-success-dark">
{% blocktrans %}Discover your stats for {{ year }}!{% endblocktrans %}
</a>
</p>
{% endblock %}

View file

@ -234,7 +234,3 @@
</div>
{% endif %}
{% endspaceless %}{% endblock %}
{% block scripts %}
<script src="{% static "js/check_all.js" %}?v={{ js_cache }}"></script>
{% endblock %}

View file

@ -28,6 +28,8 @@
{% include 'snippets/opengraph_images.html' %}
{% endblock %}
<meta name="twitter:image:alt" content="BookWyrm Logo">
{% block head_links %}{% endblock %}
</head>
<body>
<nav class="navbar" aria-label="main navigation">

View file

@ -190,7 +190,9 @@
<h2 class="title is-5 mt-6" id="embed-label">
{% trans "Embed this list on a website" %}
</h2>
<textarea readonly class="textarea is-small" aria-labelledby="embed-label" data-copytext data-copytext-label="{% trans 'Copy embed code' %}" data-copytext-success="{% trans 'Copied!' %}"><iframe style="border-width:0;" id="bookwyrm_list_embed" width="400" height="600" title="{% blocktrans with list_name=list.name site_name=site.name owner=list.user.display_name %}{{ list_name }}, a list by {{owner}} on {{ site_name }}{% endblocktrans %}" src="{{ embed_url }}"></iframe></textarea>
<div class="vertical-copy">
<textarea readonly class="textarea is-small" aria-labelledby="embed-label" data-copytext data-copytext-label="{% trans 'Copy embed code' %}" data-copytext-success="{% trans 'Copied!' %}"><iframe style="border-width:0;" id="bookwyrm_list_embed" width="400" height="600" title="{% blocktrans with list_name=list.name site_name=site.name owner=list.user.display_name %}{{ list_name }}, a list by {{owner}} on {{ site_name }}{% endblocktrans %}" src="{{ embed_url }}"></iframe></textarea>
</div>
</div>
</section>

View file

@ -13,7 +13,7 @@
{% for author in book.authors.all|slice:limit %}
<a
href="{{ author.local_path }}"
class="author"
class="author {{ link_class }}"
itemprop="author"
itemscope
itemtype="https://schema.org/Thing"

View file

@ -0,0 +1,151 @@
"""testing the annual summary page"""
from datetime import datetime
import pytz
from unittest.mock import patch
from django.contrib.auth.models import AnonymousUser
from django.http import Http404
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
def make_date(*args):
"""helper function to easily generate a date obj"""
return datetime(*args, tzinfo=pytz.UTC)
class AnnualSummary(TestCase):
"""views"""
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
):
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
remote_id="https://example.com/users/mouse",
summary_keys={"2020": "0123456789"},
)
self.work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
parent_work=self.work,
pages=300,
)
self.anonymous_user = AnonymousUser
self.anonymous_user.is_authenticated = False
self.year = "2020"
models.SiteSettings.objects.create()
def test_annual_summary_not_authenticated(self, *_):
"""there are so many views, this just makes sure it DOESNT LOAD"""
view = views.AnnualSummary.as_view()
request = self.factory.get("")
request.user = self.anonymous_user
with self.assertRaises(Http404):
view(request, self.local_user.localname, self.year)
def test_annual_summary_not_authenticated_with_key(self, *_):
"""there are so many views, this just makes sure it DOES LOAD"""
key = self.local_user.summary_keys[self.year]
view = views.AnnualSummary.as_view()
request_url = (
f"user/{self.local_user.localname}/{self.year}-in-the-books?key={key}"
)
request = self.factory.get(request_url)
request.user = self.anonymous_user
result = view(request, self.local_user.localname, self.year)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_annual_summary_wrong_year(self, *_):
"""there are so many views, this just makes sure it DOESNT LOAD"""
view = views.AnnualSummary.as_view()
request = self.factory.get("")
request.user = self.anonymous_user
with self.assertRaises(Http404):
view(request, self.local_user.localname, self.year)
def test_annual_summary_empty_page(self, *_):
"""there are so many views, this just makes sure it LOADS"""
view = views.AnnualSummary.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request, self.local_user.localname, self.year)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
def test_annual_summary_page(self, *_):
"""there are so many views, this just makes sure it LOADS"""
shelf = self.local_user.shelf_set.filter(identifier="read").first()
models.ShelfBook.objects.create(
book=self.book,
user=self.local_user,
shelf=shelf,
shelved_date=make_date(2020, 1, 1),
)
view = views.AnnualSummary.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request, self.local_user.localname, self.year)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
def test_annual_summary_page_with_review(self, *_):
"""there are so many views, this just makes sure it LOADS"""
self.review = models.Review.objects.create(
name="Review name",
content="test content",
rating=3.0,
user=self.local_user,
book=self.book,
)
shelf = self.local_user.shelf_set.filter(identifier="read").first()
models.ShelfBook.objects.create(
book=self.book,
user=self.local_user,
shelf=shelf,
shelved_date=make_date(2020, 1, 1),
)
view = views.AnnualSummary.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request, self.local_user.localname, self.year)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)

View file

@ -477,4 +477,18 @@ urlpatterns = [
re_path(
r"^ostatus_success/?$", views.ostatus_follow_success, name="ostatus-success"
),
# annual summary
re_path(
r"^my-year-in-the-books/(?P<year>\d+)/?$",
views.personal_annual_summary,
),
re_path(
rf"{LOCAL_USER_PATH}/(?P<year>\d+)-in-the-books/?$",
views.AnnualSummary.as_view(),
name="annual-summary",
),
re_path(r"^summary_add_key/?$", views.summary_add_key, name="summary-add-key"),
re_path(
r"^summary_revoke_key/?$", views.summary_revoke_key, name="summary-revoke-key"
),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -96,3 +96,9 @@ from .status import edit_readthrough
from .updates import get_notification_count, get_unread_status_count
from .user import User, Followers, Following, hide_suggestions
from .wellknown import *
from .annual_summary import (
AnnualSummary,
personal_annual_summary,
summary_add_key,
summary_revoke_key,
)

View file

@ -0,0 +1,270 @@
"""end-of-year read books stats"""
from datetime import date
from uuid import uuid4
from django.contrib.auth.decorators import login_required
from django.db.models import Case, When, Avg, Sum
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import models
from .helpers import get_user_from_username
# December day of first availability
FIRST_DAY = 15
# January day of last availability, 0 for no availability in Jan.
LAST_DAY = 15
# pylint: disable= no-self-use
class AnnualSummary(View):
"""display a summary of the year for the current user"""
def get(self, request, username, year):
"""get response"""
user = get_user_from_username(request.user, username)
year_key = None
if user.summary_keys and year in user.summary_keys:
year_key = user.summary_keys[year]
privacy_verification(request, user, year, year_key)
paginated_years = (
int(year) - 1 if is_year_available(user, int(year) - 1) else None,
int(year) + 1 if is_year_available(user, int(year) + 1) else None,
)
# get data
read_book_ids_in_year = get_read_book_ids_in_year(user, year)
if len(read_book_ids_in_year) == 0:
data = {
"summary_user": user,
"year": year,
"year_key": year_key,
"book_total": 0,
"books": [],
"paginated_years": paginated_years,
}
return TemplateResponse(request, "annual_summary/layout.html", data)
read_books_in_year = get_books_from_shelfbooks(read_book_ids_in_year)
# pages stats queries
page_stats = read_books_in_year.aggregate(Sum("pages"), Avg("pages"))
book_list_by_pages = read_books_in_year.filter(pages__gte=0).order_by("pages")
# books with no pages
no_page_list = len(read_books_in_year.filter(pages__exact=None))
# rating stats queries
ratings = (
models.Review.objects.filter(user=user)
.exclude(deleted=True)
.exclude(rating=None)
.filter(book_id__in=read_book_ids_in_year)
)
ratings_stats = ratings.aggregate(Avg("rating"))
data = {
"summary_user": user,
"year": year,
"year_key": year_key,
"books_total": len(read_books_in_year),
"books": read_books_in_year,
"pages_total": page_stats["pages__sum"] or 0,
"pages_average": round(
page_stats["pages__avg"] if page_stats["pages__avg"] else 0
),
"book_pages_lowest": book_list_by_pages.first(),
"book_pages_highest": book_list_by_pages.last(),
"no_page_number": no_page_list,
"ratings_total": len(ratings),
"rating_average": round(
ratings_stats["rating__avg"] if ratings_stats["rating__avg"] else 0, 2
),
"book_rating_highest": ratings.order_by("-rating").first(),
"best_ratings_books_ids": [
review.book.id for review in ratings.filter(rating=5)
],
"paginated_years": paginated_years,
}
return TemplateResponse(request, "annual_summary/layout.html", data)
@login_required
def personal_annual_summary(request, year):
"""redirect simple URL to URL with username"""
return redirect("annual-summary", request.user.localname, year)
@login_required
@require_POST
def summary_add_key(request):
"""add summary key"""
year = request.POST["year"]
user = request.user
new_key = uuid4().hex
if not user.summary_keys:
user.summary_keys = {
year: new_key,
}
else:
user.summary_keys[year] = new_key
user.save()
response = redirect("annual-summary", user.localname, year)
response["Location"] += f"?key={str(new_key)}"
return response
@login_required
@require_POST
def summary_revoke_key(request):
"""revoke summary key"""
year = request.POST["year"]
user = request.user
if user.summary_keys and year in user.summary_keys:
user.summary_keys.pop(year)
user.save()
return redirect("annual-summary", user.localname, year)
def get_annual_summary_year():
"""return the latest available annual summary year or None"""
today = date.today()
if date(today.year, 12, FIRST_DAY) <= today <= date(today.year, 12, 31):
return today.year
if LAST_DAY > 0 and date(today.year, 1, 1) <= today <= date(
today.year, 1, LAST_DAY
):
return today.year - 1
return None
def privacy_verification(request, user, year, year_key):
"""raises a 404 error if the user should not access the page"""
if user != request.user:
request_key = None
if "key" in request.GET:
request_key = request.GET["key"]
if not request_key or request_key != year_key:
raise Http404(f"The summary for {year} is unavailable")
if not is_year_available(user, year):
raise Http404(f"The summary for {year} is unavailable")
def is_year_available(user, year):
"""return boolean"""
earliest_year = int(get_earliest_year(user, year))
today = date.today()
year = int(year)
if earliest_year <= year < today.year:
return True
if year == today.year and today >= date(today.year, 12, FIRST_DAY):
return True
return False
def get_earliest_year(user, year):
"""return the earliest finish_date or shelved_date year for user books in read shelf"""
read_shelfbooks = models.ShelfBook.objects.filter(user__id=user.id).filter(
shelf__identifier__exact="read"
)
read_shelfbooks_list = list(read_shelfbooks.values("book", "shelved_date"))
book_dates = []
for book in read_shelfbooks_list:
earliest_finished = (
models.ReadThrough.objects.filter(user__id=user.id)
.filter(book_id=book["book"])
.exclude(finish_date__exact=None)
.order_by("finish_date")
.values("finish_date")
.first()
)
if earliest_finished:
book_dates.append(
min(earliest_finished["finish_date"], book["shelved_date"])
)
else:
book_dates.append(book["shelved_date"])
if book_dates:
return min(book_dates).year
return year
def get_read_book_ids_in_year(user, year):
"""return an ordered QuerySet of the read book ids"""
read_shelf = get_object_or_404(user.shelf_set, identifier="read")
shelved_book_ids = (
models.ShelfBook.objects.filter(shelf=read_shelf)
.filter(user=user)
.values_list("book", "shelved_date")
)
book_dates = []
for book in shelved_book_ids:
finished_in_year = (
models.ReadThrough.objects.filter(user__id=user.id)
.filter(book_id=book[0])
.filter(finish_date__year=year)
.values("finish_date")
.first()
)
if finished_in_year:
# Finished a readthrough in the year
book_dates.append((book[0], finished_in_year["finish_date"]))
else:
has_other_year_readthrough = (
models.ReadThrough.objects.filter(user__id=user.id)
.filter(book_id=book[0])
.exists()
)
if not has_other_year_readthrough and book[1].year == int(year):
# No readthrough but shelved this year
book_dates.append(book)
book_dates = sorted(book_dates, key=lambda tup: tup[1])
return [book[0] for book in book_dates]
def get_books_from_shelfbooks(books_ids):
"""return an ordered QuerySet of books from a list"""
ordered = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(books_ids)])
books = models.Edition.objects.filter(id__in=books_ids).order_by(ordered)
return books

View file

@ -16,6 +16,7 @@ from bookwyrm.settings import PAGE_LENGTH, STREAMS
from bookwyrm.suggested_users import suggested_users
from .helpers import filter_stream_by_status_type, get_user_from_username
from .helpers import is_api_request, is_bookwyrm_request
from .annual_summary import get_annual_summary_year
# pylint: disable= no-self-use
@ -62,6 +63,7 @@ class Feed(View):
"allowed_status_types": request.user.feed_status_types,
"settings_saved": settings_saved,
"path": f"/{tab['key']}",
"annual_summary_year": get_annual_summary_year(),
},
}
return TemplateResponse(request, "feed/feed.html", data)

22
bw-dev
View file

@ -134,7 +134,7 @@ case "$CMD" in
npx prettier --write bookwyrm/static/js/*.js
;;
populate_streams)
runweb python manage.py populate_streams $@
runweb python manage.py populate_streams "$@"
;;
populate_suggestions)
runweb python manage.py populate_suggestions
@ -143,20 +143,33 @@ case "$CMD" in
runweb python manage.py generateimages
;;
generate_preview_images)
runweb python manage.py generate_preview_images $@
runweb python manage.py generate_preview_images "$@"
;;
copy_media_to_s3)
awscommand "bookwyrm_media_volume:/images"\
"s3 cp /images s3://${AWS_STORAGE_BUCKET_NAME}/images\
--endpoint-url ${AWS_S3_ENDPOINT_URL}\
--recursive --acl public-read"
--recursive --acl public-read" "$@"
;;
sync_media_to_s3)
awscommand "bookwyrm_media_volume:/images"\
"s3 sync /images s3://${AWS_STORAGE_BUCKET_NAME}/images\
--endpoint-url ${AWS_S3_ENDPOINT_URL}\
--acl public-read" "$@"
;;
set_cors_to_s3)
set +x
config_file=$1
if [ -z "$config_file" ]; then
echo "This command requires a JSON file containing a CORS configuration as an argument"
exit 1
fi
set -x
awscommand "$(pwd):/bw"\
"s3api put-bucket-cors\
--bucket ${AWS_STORAGE_BUCKET_NAME}\
--endpoint-url ${AWS_S3_ENDPOINT_URL}\
--cors-configuration file:///bw/$@"
--cors-configuration file:///bw/$config_file" "$@"
;;
runweb)
runweb "$@"
@ -187,6 +200,7 @@ case "$CMD" in
echo " generate_thumbnails"
echo " generate_preview_images [--all]"
echo " copy_media_to_s3"
echo " sync_media_to_s3"
echo " set_cors_to_s3 [cors file]"
echo " runweb [command]"
;;

Binary file not shown.

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-12-08 15:40+0000\n"
"PO-Revision-Date: 2021-12-09 18:56\n"
"PO-Revision-Date: 2021-12-16 14:48\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: German\n"
"Language: de\n"
@ -159,11 +159,11 @@ msgstr "Besprechungen"
#: bookwyrm/models/user.py:33
msgid "Comments"
msgstr ""
msgstr "Kommentare"
#: bookwyrm/models/user.py:34
msgid "Quotations"
msgstr ""
msgstr "Zitate"
#: bookwyrm/models/user.py:35
msgid "Everything else"
@ -671,7 +671,7 @@ msgstr "Autor*innen hinzufügen:"
#: bookwyrm/templates/book/edit/edit_book_form.html:145
#: bookwyrm/templates/book/edit/edit_book_form.html:148
msgid "Add Author"
msgstr ""
msgstr "Autor*in hinzufügen"
#: bookwyrm/templates/book/edit/edit_book_form.html:146
#: bookwyrm/templates/book/edit/edit_book_form.html:149
@ -1145,7 +1145,7 @@ msgstr "Administrator*in kontaktieren"
#: bookwyrm/templates/embed-layout.html:46
msgid "Join Bookwyrm"
msgstr ""
msgstr "Bookwyrm beitreten"
#: bookwyrm/templates/feed/direct_messages.html:8
#, python-format
@ -1171,11 +1171,11 @@ msgstr ""
#: bookwyrm/templates/feed/feed.html:39
msgid "Saved!"
msgstr ""
msgstr "Gespeichert!"
#: bookwyrm/templates/feed/feed.html:53
msgid "Save settings"
msgstr ""
msgstr "Einstellungen speichern"
#: bookwyrm/templates/feed/feed.html:63
#, python-format
@ -1267,7 +1267,7 @@ msgstr "Hast du %(book_title)s gelesen?"
#: bookwyrm/templates/get_started/book_preview.html:7
msgid "Add to your books"
msgstr ""
msgstr "Zu deinen Büchern hinzufügen"
#: bookwyrm/templates/get_started/books.html:6
msgid "What are you reading?"
@ -1555,7 +1555,7 @@ msgstr ""
#: bookwyrm/templates/import/import_status.html:50
msgid "Refresh"
msgstr ""
msgstr "Aktualisieren"
#: bookwyrm/templates/import/import_status.html:71
#, python-format
@ -1592,7 +1592,7 @@ msgstr "Titel"
#: bookwyrm/templates/import/import_status.html:106
msgid "ISBN"
msgstr ""
msgstr "ISBN"
#: bookwyrm/templates/import/import_status.html:109
#: bookwyrm/templates/shelf/shelf.html:145
@ -1602,7 +1602,7 @@ msgstr "Autor*in"
#: bookwyrm/templates/import/import_status.html:112
msgid "Shelf"
msgstr ""
msgstr "Regal"
#: bookwyrm/templates/import/import_status.html:115
#: bookwyrm/templates/import/manual_review.html:13
@ -1642,7 +1642,7 @@ msgstr ""
#: bookwyrm/templates/import/import_status.html:195
msgid "Retry"
msgstr ""
msgstr "Erneut versuchen"
#: bookwyrm/templates/import/import_status.html:213
msgid "This import is in an old format that is no longer supported. If you would like to troubleshoot missing items from this import, click the button below to update the import format."
@ -2310,7 +2310,7 @@ msgstr ""
#: bookwyrm/templates/ostatus/error.html:51
#, python-format
msgid "You have blocked <strong>%(account)s</strong>"
msgstr ""
msgstr "Du hast <strong>%(account)s</strong> blockiert"
#: bookwyrm/templates/ostatus/error.html:55
#, python-format
@ -2343,7 +2343,7 @@ msgstr ""
#: bookwyrm/templates/ostatus/remote_follow.html:42
msgid "Follow!"
msgstr ""
msgstr "Folgen!"
#: bookwyrm/templates/ostatus/remote_follow_button.html:8
msgid "Follow on Fediverse"
@ -2371,7 +2371,7 @@ msgstr ""
#: bookwyrm/templates/ostatus/subscribe.html:18
msgid "Uh oh..."
msgstr ""
msgstr "Oh oh..."
#: bookwyrm/templates/ostatus/subscribe.html:20
msgid "Let's log in first..."

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-12-15 02:53+0000\n"
"POT-Creation-Date: 2021-12-27 20:43+0000\n"
"PO-Revision-Date: 2021-02-28 17:19-0800\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: English <LL@li.org>\n"
@ -73,16 +73,16 @@ msgstr ""
msgid "Descending"
msgstr ""
#: bookwyrm/importers/importer.py:141 bookwyrm/importers/importer.py:163
#: bookwyrm/importers/importer.py:145 bookwyrm/importers/importer.py:167
msgid "Error loading book"
msgstr ""
#: bookwyrm/importers/importer.py:150
#: bookwyrm/importers/importer.py:154
msgid "Could not find a match for book"
msgstr ""
#: bookwyrm/models/base_model.py:17
#: bookwyrm/templates/import/import_status.html:190
#: bookwyrm/templates/import/import_status.html:200
msgid "Pending"
msgstr ""
@ -1506,28 +1506,28 @@ msgstr ""
msgid "Data source:"
msgstr ""
#: bookwyrm/templates/import/import.html:37
#: bookwyrm/templates/import/import.html:40
msgid "Data file:"
msgstr ""
#: bookwyrm/templates/import/import.html:45
#: bookwyrm/templates/import/import.html:48
msgid "Include reviews"
msgstr ""
#: bookwyrm/templates/import/import.html:50
#: bookwyrm/templates/import/import.html:53
msgid "Privacy setting for imported reviews:"
msgstr ""
#: bookwyrm/templates/import/import.html:56
#: bookwyrm/templates/import/import.html:59
#: bookwyrm/templates/settings/federation/instance_blocklist.html:64
msgid "Import"
msgstr ""
#: bookwyrm/templates/import/import.html:61
#: bookwyrm/templates/import/import.html:64
msgid "Recent Imports"
msgstr ""
#: bookwyrm/templates/import/import.html:63
#: bookwyrm/templates/import/import.html:66
msgid "No recent imports"
msgstr ""
@ -1595,27 +1595,31 @@ msgstr ""
msgid "ISBN"
msgstr ""
#: bookwyrm/templates/import/import_status.html:109
#: bookwyrm/templates/import/import_status.html:110
msgid "Openlibrary key"
msgstr ""
#: bookwyrm/templates/import/import_status.html:114
#: bookwyrm/templates/shelf/shelf.html:145
#: bookwyrm/templates/shelf/shelf.html:169
msgid "Author"
msgstr ""
#: bookwyrm/templates/import/import_status.html:112
#: bookwyrm/templates/import/import_status.html:117
msgid "Shelf"
msgstr ""
#: bookwyrm/templates/import/import_status.html:115
#: bookwyrm/templates/import/import_status.html:120
#: bookwyrm/templates/import/manual_review.html:13
#: bookwyrm/templates/snippets/create_status.html:17
msgid "Review"
msgstr ""
#: bookwyrm/templates/import/import_status.html:119
#: bookwyrm/templates/import/import_status.html:124
msgid "Book"
msgstr ""
#: bookwyrm/templates/import/import_status.html:122
#: bookwyrm/templates/import/import_status.html:127
#: bookwyrm/templates/settings/announcements/announcements.html:38
#: bookwyrm/templates/settings/federation/instance_list.html:46
#: bookwyrm/templates/settings/invites/manage_invite_requests.html:44
@ -1625,31 +1629,31 @@ msgstr ""
msgid "Status"
msgstr ""
#: bookwyrm/templates/import/import_status.html:130
#: bookwyrm/templates/import/import_status.html:135
msgid "Import preview unavailable."
msgstr ""
#: bookwyrm/templates/import/import_status.html:162
#: bookwyrm/templates/import/import_status.html:172
msgid "View imported review"
msgstr ""
#: bookwyrm/templates/import/import_status.html:176
#: bookwyrm/templates/import/import_status.html:186
msgid "Imported"
msgstr ""
#: bookwyrm/templates/import/import_status.html:182
#: bookwyrm/templates/import/import_status.html:192
msgid "Needs manual review"
msgstr ""
#: bookwyrm/templates/import/import_status.html:195
#: bookwyrm/templates/import/import_status.html:205
msgid "Retry"
msgstr ""
#: bookwyrm/templates/import/import_status.html:213
#: bookwyrm/templates/import/import_status.html:223
msgid "This import is in an old format that is no longer supported. If you would like to troubleshoot missing items from this import, click the button below to update the import format."
msgstr ""
#: bookwyrm/templates/import/import_status.html:215
#: bookwyrm/templates/import/import_status.html:225
msgid "Update import"
msgstr ""
@ -4146,7 +4150,7 @@ msgstr ""
msgid "%(title)s: %(subtitle)s"
msgstr ""
#: bookwyrm/views/imports/import_data.py:64
#: bookwyrm/views/imports/import_data.py:67
msgid "Not a valid csv file"
msgstr ""

Binary file not shown.

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-12-08 15:40+0000\n"
"PO-Revision-Date: 2021-12-09 18:55\n"
"PO-Revision-Date: 2021-12-26 09:59\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: French\n"
"Language: fr\n"
@ -1131,7 +1131,7 @@ msgstr "Réinitialiser votre mot de passe sur %(site_name)s"
#: bookwyrm/templates/embed-layout.html:21 bookwyrm/templates/layout.html:37
#, python-format
msgid "%(site_name)s home page"
msgstr ""
msgstr "%(site_name)s page d'accueil"
#: bookwyrm/templates/embed-layout.html:34
#: bookwyrm/templates/landing/about.html:7 bookwyrm/templates/layout.html:230
@ -1145,7 +1145,7 @@ msgstr "Contacter ladministrateur du site"
#: bookwyrm/templates/embed-layout.html:46
msgid "Join Bookwyrm"
msgstr ""
msgstr "Rejoignez Bookwyrm"
#: bookwyrm/templates/feed/direct_messages.html:8
#, python-format
@ -1942,7 +1942,7 @@ msgstr "Modifier la liste"
#: bookwyrm/templates/lists/embed-list.html:7
#, python-format
msgid "%(list_name)s, a list by %(owner)s"
msgstr ""
msgstr "%(list_name)s, une liste de %(owner)s"
#: bookwyrm/templates/lists/embed-list.html:17
#, python-format
@ -2073,20 +2073,20 @@ msgstr "Suggérer"
#: bookwyrm/templates/lists/list.html:191
msgid "Embed this list on a website"
msgstr ""
msgstr "Intégrez cette liste sur un autre site internet"
#: bookwyrm/templates/lists/list.html:193
msgid "Copy embed code"
msgstr ""
msgstr "Copier le code d'intégration"
#: bookwyrm/templates/lists/list.html:193
msgid "Copied!"
msgstr ""
msgstr "Copié!"
#: bookwyrm/templates/lists/list.html:193
#, python-format
msgid "%(list_name)s, a list by %(owner)s on %(site_name)s"
msgstr ""
msgstr "%(list_name)s, une liste de %(owner)s sur %(site_name)s"
#: bookwyrm/templates/lists/list_items.html:15
msgid "Saved"

Binary file not shown.

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-12-08 15:40+0000\n"
"PO-Revision-Date: 2021-12-13 20:56\n"
"PO-Revision-Date: 2021-12-26 20:36\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Lithuanian\n"
"Language: lt\n"
@ -250,7 +250,7 @@ msgstr "Keisti autorių"
#: bookwyrm/templates/author/author.html:40
msgid "Author details"
msgstr ""
msgstr "Informacija apie autorių"
#: bookwyrm/templates/author/author.html:44
#: bookwyrm/templates/author/edit_author.html:42
@ -267,7 +267,7 @@ msgstr "Mirė:"
#: bookwyrm/templates/author/author.html:70
msgid "External links"
msgstr ""
msgstr "Išorinės nuorodos"
#: bookwyrm/templates/author/author.html:75
msgid "Wikipedia"
@ -282,7 +282,7 @@ msgstr "Peržiūrėti ISNI įrašą"
#: bookwyrm/templates/book/book.html:93
#: bookwyrm/templates/book/sync_modal.html:5
msgid "Load data"
msgstr ""
msgstr "Įkelti duomenis"
#: bookwyrm/templates/author/author.html:92
#: bookwyrm/templates/book/book.html:96
@ -420,7 +420,7 @@ msgstr "Atšaukti"
#: bookwyrm/templates/author/sync_modal.html:15
#, python-format
msgid "Loading data will connect to <strong>%(source_name)s</strong> and check for any metadata about this author which aren't present here. Existing metadata will not be overwritten."
msgstr ""
msgstr "Duomenų įkėlimas prisijungs prie <strong>%(source_name)s</strong> ir patikrins ar nėra naujos informacijos. Esantys metaduomenys nebus perrašomi."
#: bookwyrm/templates/author/sync_modal.html:23
#: bookwyrm/templates/book/edit/edit_book.html:108
@ -812,7 +812,7 @@ msgstr "Ištrinti šias skaitymo datas"
#: bookwyrm/templates/book/sync_modal.html:15
#, python-format
msgid "Loading data will connect to <strong>%(source_name)s</strong> and check for any metadata about this book which aren't present here. Existing metadata will not be overwritten."
msgstr ""
msgstr "Duomenų įkėlimas prisijungs prie <strong>%(source_name)s</strong> ir patikrins ar nėra naujos informacijos apie šią knygą. Esantys metaduomenys nebus perrašomi."
#: bookwyrm/templates/components/inline_form.html:8
#: bookwyrm/templates/components/modal.html:11
@ -2136,22 +2136,22 @@ msgstr "į sąrašą „<a href=\"%(list_path)s\">%(list_name)s</a>\" patariama
#: bookwyrm/templates/notifications/items/boost.html:19
#, python-format
msgid "boosted your <a href=\"%(related_path)s\">review of <em>%(book_title)s</em></a>"
msgstr "populiarėja <a href=\"%(related_path)s\">jūsų atsiliepimas apie <em>%(book_title)s</em></a>"
msgstr "populiarino <a href=\"%(related_path)s\">jūsų atsiliepimą apie <em>%(book_title)s</em></a>"
#: bookwyrm/templates/notifications/items/boost.html:25
#, python-format
msgid "boosted your <a href=\"%(related_path)s\">comment on<em>%(book_title)s</em></a>"
msgstr "populiarėja <a href=\"%(related_path)s\">jūsų komentaras apie <em>%(book_title)s</em></a>"
msgstr "populiarino <a href=\"%(related_path)s\">jūsų komentarą apie <em>%(book_title)s</em></a>"
#: bookwyrm/templates/notifications/items/boost.html:31
#, python-format
msgid "boosted your <a href=\"%(related_path)s\">quote from <em>%(book_title)s</em></a>"
msgstr "populiarėja <a href=\"%(related_path)s\">jūsų citata iš <em>%(book_title)s</em></a>"
msgstr "populiarino <a href=\"%(related_path)s\">jūsų citatą iš <em>%(book_title)s</em></a>"
#: bookwyrm/templates/notifications/items/boost.html:37
#, python-format
msgid "boosted your <a href=\"%(related_path)s\">status</a>"
msgstr "populiarėja jūsų <a href=\"%(related_path)s\">būsena</a>"
msgstr "populiarino jūsų <a href=\"%(related_path)s\">būseną</a>"
#: bookwyrm/templates/notifications/items/fav.html:19
#, python-format

Binary file not shown.

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-12-08 15:40+0000\n"
"PO-Revision-Date: 2021-12-11 15:41\n"
"PO-Revision-Date: 2021-12-18 17:19\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Portuguese, Brazilian\n"
"Language: pt\n"
@ -496,7 +496,7 @@ msgstr "Criar"
#: bookwyrm/templates/book/book.html:223
msgid "You don't have any reading activity for this book."
msgstr "Você ainda não registrou nenhuma atividade neste livro."
msgstr "Você ainda não registrou sua leitura."
#: bookwyrm/templates/book/book.html:249
msgid "Your reviews"
@ -3496,12 +3496,12 @@ msgstr "Andamento:"
#: bookwyrm/templates/snippets/create_status/comment.html:53
#: bookwyrm/templates/snippets/progress_field.html:18
msgid "pages"
msgstr "páginas"
msgstr "página"
#: bookwyrm/templates/snippets/create_status/comment.html:59
#: bookwyrm/templates/snippets/progress_field.html:23
msgid "percent"
msgstr "porcento"
msgstr "porcentagem"
#: bookwyrm/templates/snippets/create_status/comment.html:66
#, python-format

Binary file not shown.

Binary file not shown.