From 5d3f00a5372906caf407d6b61c34914d24213420 Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Mon, 5 Apr 2021 12:46:34 +0200 Subject: [PATCH 001/288] [l10n] Add and update i18n strings for French: - Fix typo in English. - Add translations for missing strings. - Update translations for fuzzy strings. --- .../templates/snippets/table-sort-header.html | 2 +- locale/de_DE/LC_MESSAGES/django.po | 2 +- locale/en_US/LC_MESSAGES/django.po | 2 +- locale/es/LC_MESSAGES/django.po | 2 +- locale/fr_FR/LC_MESSAGES/django.mo | Bin 24395 -> 42813 bytes locale/fr_FR/LC_MESSAGES/django.po | 738 ++++++++---------- locale/zh_CN/LC_MESSAGES/django.mo | Bin 38419 -> 38418 bytes locale/zh_CN/LC_MESSAGES/django.po | 2 +- 8 files changed, 342 insertions(+), 406 deletions(-) diff --git a/bookwyrm/templates/snippets/table-sort-header.html b/bookwyrm/templates/snippets/table-sort-header.html index f016b4e27..2f20b9291 100644 --- a/bookwyrm/templates/snippets/table-sort-header.html +++ b/bookwyrm/templates/snippets/table-sort-header.html @@ -3,7 +3,7 @@ {{ text }} {% if sort == field %} - {% trans "Sorted asccending" %} + {% trans "Sorted ascending" %} {% elif sort == "-"|add:field %} diff --git a/locale/de_DE/LC_MESSAGES/django.po b/locale/de_DE/LC_MESSAGES/django.po index 87e77637a..962b97369 100644 --- a/locale/de_DE/LC_MESSAGES/django.po +++ b/locale/de_DE/LC_MESSAGES/django.po @@ -2565,7 +2565,7 @@ msgstr "Zu dieser Edition wechseln" #: bookwyrm/templates/snippets/table-sort-header.html:6 #, fuzzy #| msgid "Started reading" -msgid "Sorted asccending" +msgid "Sorted ascending" msgstr "Zu lesen angefangen" #: bookwyrm/templates/snippets/table-sort-header.html:10 diff --git a/locale/en_US/LC_MESSAGES/django.po b/locale/en_US/LC_MESSAGES/django.po index ab28a3942..a8720f17c 100644 --- a/locale/en_US/LC_MESSAGES/django.po +++ b/locale/en_US/LC_MESSAGES/django.po @@ -2408,7 +2408,7 @@ msgid "Switch to this edition" msgstr "" #: bookwyrm/templates/snippets/table-sort-header.html:6 -msgid "Sorted asccending" +msgid "Sorted ascending" msgstr "" #: bookwyrm/templates/snippets/table-sort-header.html:10 diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index c0fef6509..64921d5e5 100644 --- a/locale/es/LC_MESSAGES/django.po +++ b/locale/es/LC_MESSAGES/django.po @@ -2557,7 +2557,7 @@ msgstr "Cambiar a esta edición" #: bookwyrm/templates/snippets/table-sort-header.html:6 #, fuzzy #| msgid "Started reading" -msgid "Sorted asccending" +msgid "Sorted ascending" msgstr "Lectura se empezó" #: bookwyrm/templates/snippets/table-sort-header.html:10 diff --git a/locale/fr_FR/LC_MESSAGES/django.mo b/locale/fr_FR/LC_MESSAGES/django.mo index bae2cccfacb0d3e15078b395dd3b49b0c75cd72b..09e90f22c801bc681e5dfda3c171b81a4da45afc 100644 GIT binary patch literal 42813 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 XuVhG6$jb-OIgc7\n" "Language-Team: Mouse Reeve \n" "Language: fr_FR\n" @@ -20,74 +20,72 @@ msgstr "" #: bookwyrm/forms.py:226 msgid "A user with this email already exists." -msgstr "" +msgstr "Cet email est déjà associé à un compte." #: bookwyrm/forms.py:240 msgid "One Day" -msgstr "" +msgstr "Un jour" #: bookwyrm/forms.py:241 msgid "One Week" -msgstr "" +msgstr "Une semaine" #: bookwyrm/forms.py:242 msgid "One Month" -msgstr "" +msgstr "Un mois" #: bookwyrm/forms.py:243 msgid "Does Not Expire" -msgstr "" +msgstr "Sans expiration" #: bookwyrm/forms.py:248 #, python-format msgid "%(count)d uses" -msgstr "" +msgstr "%(count)d utilisations" #: bookwyrm/forms.py:251 -#, fuzzy #| msgid "Unlisted" msgid "Unlimited" -msgstr "Non listé" +msgstr "Sans limite" #: bookwyrm/models/fields.py:24 #, python-format msgid "%(value)s is not a valid remote_id" -msgstr "" +msgstr "%(value)s n’est pas une remote_id valide." #: bookwyrm/models/fields.py:33 bookwyrm/models/fields.py:42 #, python-format msgid "%(value)s is not a valid username" -msgstr "" +msgstr "%(value)s n’est pas un nom de compte valide." #: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:152 -#, fuzzy #| msgid "Username:" msgid "username" -msgstr "Nom d’utilisateur :" +msgstr "nom du compte :" #: bookwyrm/models/fields.py:170 msgid "A user with that username already exists." -msgstr "" +msgstr "Ce nom est déjà associé à un compte." #: bookwyrm/settings.py:150 msgid "English" -msgstr "" +msgstr "English" #: bookwyrm/settings.py:151 msgid "German" -msgstr "" +msgstr "Deutsch" #: bookwyrm/settings.py:152 msgid "Spanish" -msgstr "" +msgstr "Español" #: bookwyrm/settings.py:153 msgid "French" -msgstr "" +msgstr "Français" #: bookwyrm/settings.py:154 msgid "Simplified Chinese" -msgstr "" +msgstr "简化字" #: bookwyrm/templates/404.html:4 bookwyrm/templates/404.html:8 msgid "Not Found" @@ -126,7 +124,7 @@ msgstr "Livres par %(name)s" #: bookwyrm/templates/discover/large-book.html:12 #: bookwyrm/templates/discover/small-book.html:9 msgid "by" -msgstr "" +msgstr "par" #: bookwyrm/templates/book/book.html:29 bookwyrm/templates/book/book.html:30 msgid "Edit Book" @@ -138,22 +136,21 @@ msgid "Add cover" msgstr "Ajouter une couverture" #: bookwyrm/templates/book/book.html:53 -#, fuzzy #| msgid "Failed to load" msgid "Failed to load cover" -msgstr "Items non importés" +msgstr "La couverture n’a pu être chargée" #: bookwyrm/templates/book/book.html:62 msgid "ISBN:" msgstr "ISBN :" #: bookwyrm/templates/book/book.html:69 -#: bookwyrm/templates/book/edit_book.html:211 +#: bookwyrm/templates/book/edit_book.html:217 msgid "OCLC Number:" msgstr "Numéro OCLC :" #: bookwyrm/templates/book/book.html:76 -#: bookwyrm/templates/book/edit_book.html:215 +#: bookwyrm/templates/book/edit_book.html:221 msgid "ASIN:" msgstr "ASIN :" @@ -165,11 +162,10 @@ msgstr "Voir sur OpenLibrary" #, python-format msgid "(%(review_count)s review)" msgid_plural "(%(review_count)s reviews)" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "(%(review_count)s critique)" +msgstr[1] "(%(review_count)s critiques)" #: bookwyrm/templates/book/book.html:100 -#, fuzzy #| msgid "Description:" msgid "Add Description" msgstr "Ajouter une description" @@ -181,7 +177,7 @@ msgid "Description:" msgstr "Description :" #: bookwyrm/templates/book/book.html:111 -#: bookwyrm/templates/book/edit_book.html:225 +#: bookwyrm/templates/book/edit_book.html:231 #: 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 @@ -194,7 +190,7 @@ msgstr "Enregistrer" #: bookwyrm/templates/book/book.html:112 bookwyrm/templates/book/book.html:161 #: bookwyrm/templates/book/cover_modal.html:32 -#: bookwyrm/templates/book/edit_book.html:226 +#: bookwyrm/templates/book/edit_book.html:232 #: bookwyrm/templates/edit_author.html:79 #: bookwyrm/templates/moderation/report_modal.html:32 #: bookwyrm/templates/snippets/delete_readthrough_modal.html:17 @@ -208,29 +204,28 @@ msgid "Cancel" msgstr "Annuler" #: bookwyrm/templates/book/book.html:121 -#, fuzzy, python-format +#, python-format #| msgid "Editions of \"%(work_title)s\"" msgid "%(count)s editions" -msgstr "%(title)s par " +msgstr "%(count)s éditions" #: bookwyrm/templates/book/book.html:129 -#, fuzzy, python-format +#, python-format #| msgid "favorited your %(preview_name)s" msgid "This edition is on your %(shelf_name)s shelf." -msgstr "Messages directs avec %(username)s" +msgstr "Cette édition est sur votre étagère %(shelf_name)s." #: bookwyrm/templates/book/book.html:135 -#, fuzzy, python-format +#, python-format #| msgid "replied to your %(preview_name)s" msgid "A different edition of this book is on your %(shelf_name)s shelf." -msgstr " a ajouté %(book_title)s à votre liste « %(list_name)s »" +msgstr "Une édition différente de ce livre existe sur votre étagère %(shelf_name)s." #: bookwyrm/templates/book/book.html:144 msgid "Your reading activity" msgstr "Votre activité de lecture" #: bookwyrm/templates/book/book.html:146 -#, fuzzy #| msgid "Edit read dates" msgid "Add read dates" msgstr "Ajouter des dates de lecture" @@ -259,10 +254,9 @@ msgid "Lists" msgstr "Listes" #: bookwyrm/templates/book/book.html:213 -#, fuzzy #| msgid "Go to list" msgid "Add to list" -msgstr "Aller à la liste" +msgstr "Ajouter à la liste" #: bookwyrm/templates/book/book.html:223 #: bookwyrm/templates/book/cover_modal.html:31 @@ -275,30 +269,28 @@ msgid "rated it" msgstr "l’a noté" #: bookwyrm/templates/book/cover_modal.html:17 -#: bookwyrm/templates/book/edit_book.html:163 -#, fuzzy +#: bookwyrm/templates/book/edit_book.html:169 #| msgid "Add cover" msgid "Upload cover:" -msgstr "Ajouter une couverture" +msgstr "Charger une couverture :" #: bookwyrm/templates/book/cover_modal.html:23 -#: bookwyrm/templates/book/edit_book.html:169 +#: bookwyrm/templates/book/edit_book.html:175 msgid "Load cover from url:" -msgstr "" +msgstr "Charger la couverture depuis une URL :" #: bookwyrm/templates/book/edit_book.html:5 #: bookwyrm/templates/book/edit_book.html:11 -#, fuzzy, python-format +#, python-format #| msgid "Finish \"%(book_title)s\"" msgid "Edit \"%(book_title)s\"" -msgstr "Éditions de %(book_title)s" +msgstr "Modifier « %(book_title)s »" #: bookwyrm/templates/book/edit_book.html:5 #: bookwyrm/templates/book/edit_book.html:13 -#, fuzzy #| msgid "Add Books" msgid "Add Book" -msgstr "Ajouter des livres" +msgstr "Ajouter un livre" #: bookwyrm/templates/book/edit_book.html:18 #: bookwyrm/templates/edit_author.html:13 @@ -317,35 +309,35 @@ msgstr "Dernière modification par :" #: bookwyrm/templates/book/edit_book.html:40 msgid "Confirm Book Info" -msgstr "" +msgstr "Confirmer les informations de ce livre" #: bookwyrm/templates/book/edit_book.html:47 #, python-format msgid "Is \"%(name)s\" an existing author?" -msgstr "" +msgstr "Est‑ce que l’auteur ou l’autrice « %(name)s » existe déjà ?" #: bookwyrm/templates/book/edit_book.html:52 -#, fuzzy, python-format +#, python-format #| msgid "Start \"%(book_title)s\"" msgid "Author of %(book_title)s" msgstr "Commencer « %(book_title)s »" #: bookwyrm/templates/book/edit_book.html:55 msgid "This is a new author" -msgstr "" +msgstr "Il s’agit d’un nouvel auteur ou d’une nouvelle autrice." #: bookwyrm/templates/book/edit_book.html:61 #, python-format msgid "Creating a new author: %(name)s" -msgstr "" +msgstr "Création d’un nouvel auteur ou d’une nouvelle autrice : %(name)s" #: bookwyrm/templates/book/edit_book.html:67 msgid "Is this an edition of an existing work?" -msgstr "" +msgstr "Est‑ce l’édition d’un ouvrage existant ?" #: bookwyrm/templates/book/edit_book.html:71 msgid "This is a new work" -msgstr "" +msgstr "Il s’agit d’un nouvel ouvrage." #: bookwyrm/templates/book/edit_book.html:77 #: bookwyrm/templates/password_reset.html:30 @@ -379,82 +371,79 @@ msgid "Series number:" msgstr "Numéro dans la série :" #: bookwyrm/templates/book/edit_book.html:117 -#, fuzzy #| msgid "Published" msgid "Publisher:" -msgstr "Publié" +msgstr "Éditeur :" #: bookwyrm/templates/book/edit_book.html:119 msgid "Separate multiple publishers with commas." -msgstr "" +msgstr "Séparez plusieurs éditeurs par une virgule." -#: bookwyrm/templates/book/edit_book.html:125 +#: bookwyrm/templates/book/edit_book.html:126 msgid "First published date:" msgstr "Première date de publication :" -#: bookwyrm/templates/book/edit_book.html:130 +#: bookwyrm/templates/book/edit_book.html:134 msgid "Published date:" msgstr "Date de publication :" -#: bookwyrm/templates/book/edit_book.html:137 -#, fuzzy +#: bookwyrm/templates/book/edit_book.html:143 #| msgid "Author" msgid "Authors" -msgstr "Auteur ou autrice" - -#: bookwyrm/templates/book/edit_book.html:143 -#, fuzzy, python-format -#| msgid "favorited your %(preview_name)s" -msgid "Remove %(name)s" -msgstr "Messages directs avec %(username)s" - -#: bookwyrm/templates/book/edit_book.html:148 -#, fuzzy -#| msgid "Edit Author" -msgid "Add Authors:" -msgstr "Modifier l’auteur ou autrice" +msgstr "Auteurs ou autrices" #: bookwyrm/templates/book/edit_book.html:149 -msgid "John Doe, Jane Smith" -msgstr "" +#, python-format +#| msgid "favorited your %(preview_name)s" +msgid "Remove %(name)s" +msgstr "Supprimer %(name)s" + +#: bookwyrm/templates/book/edit_book.html:154 +#| msgid "Edit Author" +msgid "Add Authors:" +msgstr "Ajouter des auteurs ou autrices :" #: bookwyrm/templates/book/edit_book.html:155 +msgid "John Doe, Jane Smith" +msgstr "Claude Dupont, Dominique Durand" + +#: bookwyrm/templates/book/edit_book.html:161 #: bookwyrm/templates/user/shelf.html:75 msgid "Cover" msgstr "Couverture" -#: bookwyrm/templates/book/edit_book.html:182 +#: bookwyrm/templates/book/edit_book.html:188 msgid "Physical Properties" msgstr "Propriétés physiques" -#: bookwyrm/templates/book/edit_book.html:183 +#: bookwyrm/templates/book/edit_book.html:189 #: bookwyrm/templates/book/format_filter.html:5 msgid "Format:" msgstr "Format :" -#: bookwyrm/templates/book/edit_book.html:191 +#: bookwyrm/templates/book/edit_book.html:197 msgid "Pages:" msgstr "Pages :" -#: bookwyrm/templates/book/edit_book.html:198 +#: bookwyrm/templates/book/edit_book.html:204 msgid "Book Identifiers" msgstr "Identifiants du livre" -#: bookwyrm/templates/book/edit_book.html:199 +#: bookwyrm/templates/book/edit_book.html:205 msgid "ISBN 13:" msgstr "ISBN 13 :" -#: bookwyrm/templates/book/edit_book.html:203 +#: bookwyrm/templates/book/edit_book.html:209 msgid "ISBN 10:" msgstr "ISBN 10 :" -#: bookwyrm/templates/book/edit_book.html:207 +#: bookwyrm/templates/book/edit_book.html:213 #: bookwyrm/templates/edit_author.html:59 msgid "Openlibrary key:" msgstr "Clé Openlibrary :" #: bookwyrm/templates/book/editions.html:5 -#, fuzzy, python-format +#, python-format #| msgid "Finish \"%(book_title)s\"" msgid "Editions of %(book_title)s" msgstr "Éditions de %(book_title)s" @@ -467,89 +456,90 @@ msgstr "Éditions de « %(work_title)s »" #: bookwyrm/templates/book/format_filter.html:8 #: bookwyrm/templates/book/language_filter.html:8 msgid "Any" -msgstr "" +msgstr "Tou(te)s" #: bookwyrm/templates/book/language_filter.html:5 msgid "Language:" -msgstr "" +msgstr "Langue :" #: bookwyrm/templates/book/publisher_info.html:6 -#, fuzzy, python-format +#, python-format #| msgid "of %(book.pages)s pages" msgid "%(format)s, %(pages)s pages" -msgstr "sur %(book.pages)s pages" +msgstr "%(format)s, %(pages)s pages" #: bookwyrm/templates/book/publisher_info.html:8 -#, fuzzy, python-format +#, python-format #| msgid "of %(book.pages)s pages" msgid "%(pages)s pages" -msgstr "sur %(book.pages)s pages" +msgstr "%(pages)s pages" #: bookwyrm/templates/book/publisher_info.html:13 -#, fuzzy, python-format +#, python-format #| msgid "of %(book.pages)s pages" msgid "%(languages)s language" -msgstr "sur %(book.pages)s pages" +msgstr "%(languages)s langues" #: bookwyrm/templates/book/publisher_info.html:18 #, python-format msgid "Published %(date)s by %(publisher)s." -msgstr "" +msgstr "Publié %(date)s par %(publisher)s." #: bookwyrm/templates/book/publisher_info.html:20 -#, fuzzy, python-format +#, python-format #| msgid "Published date:" msgid "Published %(date)s" -msgstr "Date de publication :" +msgstr "Publié %(date)s" #: bookwyrm/templates/book/publisher_info.html:22 #, python-format msgid "Published by %(publisher)s." -msgstr "" +msgstr "Publié par %(publisher)s." #: bookwyrm/templates/components/inline_form.html:8 #: bookwyrm/templates/components/modal.html:11 -#: bookwyrm/templates/feed/feed_layout.html:57 +#: bookwyrm/templates/feed/feed_layout.html:70 #: bookwyrm/templates/get_started/layout.html:19 #: bookwyrm/templates/get_started/layout.html:52 -#, fuzzy #| msgid "Closed" msgid "Close" msgstr "Fermer" +#: bookwyrm/templates/compose.html:5 bookwyrm/templates/compose.html:8 +#| msgid "Boost status" +msgid "Compose status" +msgstr "Rédiger un statut" + #: bookwyrm/templates/directory/community_filter.html:5 -#, fuzzy #| msgid "Comment" msgid "Community" -msgstr "Commentaire" +msgstr "Communauté" #: bookwyrm/templates/directory/community_filter.html:8 -#, fuzzy #| msgid "Blocked users" msgid "Local users" -msgstr "Comptes bloqués" +msgstr "Comptes locaux" #: bookwyrm/templates/directory/community_filter.html:12 -#, fuzzy #| msgid "Federated" msgid "Federated community" -msgstr "Fédéré" +msgstr "Communauté fédérée" #: bookwyrm/templates/directory/directory.html:6 #: bookwyrm/templates/directory/directory.html:11 #: bookwyrm/templates/layout.html:92 msgid "Directory" -msgstr "" +msgstr "Répertoire" #: bookwyrm/templates/directory/directory.html:19 msgid "Make your profile discoverable to other BookWyrm users." -msgstr "" +msgstr "Autoriser d’autres utilisateurs ou utilisatrices de BookWyrm à découvrir votre profil." #: bookwyrm/templates/directory/directory.html:26 -#, fuzzy, python-format +#, python-format #| msgid "You can set or change your reading goal any time from your profile page" msgid "You can opt-out at any time in your profile settings." -msgstr "Vous pouvez définir ou changer vore défi lecture à n’importe quel moment depuis votre profil" +msgstr "Vous pouvez décider de ne plus y figurer à n’importe quel moment depuis vos paramètres de profil." #: bookwyrm/templates/directory/directory.html:31 #: bookwyrm/templates/snippets/goal_card.html:22 @@ -557,64 +547,59 @@ msgid "Dismiss message" msgstr "Rejeter le message" #: bookwyrm/templates/directory/directory.html:71 -#, fuzzy #| msgid "followed you" msgid "follower you follow" msgid_plural "followers you follow" -msgstr[0] "s’est abonné(e)" -msgstr[1] "s’est abonné(e)" +msgstr[0] "compte auquel vous êtes abonné(e)" +msgstr[1] "comptes auxquels vous êtes abonné(e)" #: bookwyrm/templates/directory/directory.html:78 -#, fuzzy #| msgid "Your shelves" msgid "book on your shelves" msgid_plural "books on your shelves" -msgstr[0] "Vos étagères" -msgstr[1] "Vos étagères" +msgstr[0] "livre sur vos étagères" +msgstr[1] "livres sur vos étagères" #: bookwyrm/templates/directory/directory.html:86 msgid "posts" -msgstr "" +msgstr "publications" #: bookwyrm/templates/directory/directory.html:92 msgid "last active" -msgstr "" +msgstr "dernière activité" #: bookwyrm/templates/directory/sort_filter.html:5 msgid "Order by" -msgstr "" +msgstr "Trier par" #: bookwyrm/templates/directory/sort_filter.html:8 -#, fuzzy #| msgid "Suggest" msgid "Suggested" -msgstr "Suggérer" +msgstr "Suggéré" #: bookwyrm/templates/directory/sort_filter.html:9 msgid "Recently active" -msgstr "" +msgstr "Actif récemment" #: bookwyrm/templates/directory/user_type_filter.html:5 -#, fuzzy #| msgid "User Activity" msgid "User type" -msgstr "Activité du compte" +msgstr "Type de compte" #: bookwyrm/templates/directory/user_type_filter.html:8 -#, fuzzy #| msgid "Blocked users" msgid "BookWyrm users" -msgstr "Comptes bloqués" +msgstr "Comptes BookWyrm" #: bookwyrm/templates/directory/user_type_filter.html:12 msgid "All known users" -msgstr "" +msgstr "Tous les comptes connus" #: bookwyrm/templates/discover/about.html:7 -#, fuzzy, python-format +#, python-format #| msgid "Join %(name)s" msgid "About %(site_name)s" -msgstr "À propos de %(name)s" +msgstr "À propos de %(site_name)s" #: bookwyrm/templates/discover/about.html:10 #: bookwyrm/templates/discover/about.html:20 @@ -659,11 +644,11 @@ msgstr "Cette instance est fermée" #: bookwyrm/templates/discover/landing_layout.html:57 msgid "Thank you! Your request has been received." -msgstr "" +msgstr "Merci ! Votre demande a bien été reçue." #: bookwyrm/templates/discover/landing_layout.html:60 msgid "Request an Invitation" -msgstr "" +msgstr "Demander une invitation" #: bookwyrm/templates/discover/landing_layout.html:64 #: bookwyrm/templates/password_reset_request.html:18 @@ -675,17 +660,16 @@ msgstr "Adresse email :" #: bookwyrm/templates/discover/landing_layout.html:70 #: bookwyrm/templates/moderation/report_modal.html:31 msgid "Submit" -msgstr "" +msgstr "Valider" #: bookwyrm/templates/discover/landing_layout.html:79 msgid "Your Account" msgstr "Votre compte" #: bookwyrm/templates/edit_author.html:5 -#, fuzzy #| msgid "Edit Author" msgid "Edit Author:" -msgstr "Modifier l’auteur ou autrice" +msgstr "Modifier l’auteur ou l’autrice :" #: bookwyrm/templates/edit_author.html:32 bookwyrm/templates/lists/form.html:8 #: bookwyrm/templates/user/create_shelf_form.html:13 @@ -724,49 +708,48 @@ msgstr "Clé Goodreads :" #: bookwyrm/templates/email/html_layout.html:15 #: bookwyrm/templates/email/text_layout.html:2 msgid "Hi there," -msgstr "" +msgstr "Bien le bonjour," #: bookwyrm/templates/email/html_layout.html:21 #, python-format msgid "BookWyrm hosted on %(site_name)s" -msgstr "" +msgstr "BookWyrm, hébergé par %(site_name)s" #: bookwyrm/templates/email/html_layout.html:23 msgid "Email preference" -msgstr "" +msgstr "Paramètres d’email" #: bookwyrm/templates/email/invite/html_content.html:6 #: bookwyrm/templates/email/invite/subject.html:2 -#, fuzzy, python-format +#, python-format #| msgid "Join %(name)s" msgid "You're invited to join %(site_name)s!" -msgstr "À propos de %(name)s" +msgstr "Vous avez reçu une invitation à rejoindre %(site_name)s !" #: bookwyrm/templates/email/invite/html_content.html:9 msgid "Join Now" -msgstr "" +msgstr "S’enregistrer maintenant" #: bookwyrm/templates/email/invite/html_content.html:15 #, python-format msgid "Learn more about this instance." -msgstr "" +msgstr "En savoir plus sur cette instance." #: bookwyrm/templates/email/invite/text_content.html:4 #, python-format msgid "You're invited to join %(site_name)s! Click the link below to create an account." -msgstr "" +msgstr "Vous avez reçu une invitation à rejoindre %(site_name)s ! Cliquez le lien suivant pour créer un compte." #: bookwyrm/templates/email/invite/text_content.html:8 -#, fuzzy #| msgid "More about this site" msgid "Learn more about this instance:" -msgstr "En savoir plus sur ce site" +msgstr "En savoir plus sur cete instance :" #: bookwyrm/templates/email/password_reset/html_content.html:6 #: bookwyrm/templates/email/password_reset/text_content.html:4 #, python-format msgid "You requested to reset your %(site_name)s password. Click the link below to set a new password and log in to your account." -msgstr "" +msgstr "Une demande de réinitialisation de votre mot de passe sur %(site_name)s a été initialisée. Cliquez le lien suivant pour définir un nouveau mot de passe et vous connecter à votre compte." #: bookwyrm/templates/email/password_reset/html_content.html:9 #: bookwyrm/templates/password_reset.html:4 @@ -779,13 +762,13 @@ msgstr "Changez le mot de passe" #: bookwyrm/templates/email/password_reset/html_content.html:13 #: bookwyrm/templates/email/password_reset/text_content.html:8 msgid "If you didn't request to reset your password, you can ignore this email." -msgstr "" +msgstr "Si vous n’avez pas demandé la réinitialisation de votre mot de passe, vous pouvez ignorer cet email." #: bookwyrm/templates/email/password_reset/subject.html:2 -#, fuzzy, python-format +#, python-format #| msgid "Join %(name)s" msgid "Reset your %(site_name)s password" -msgstr "À propos de %(name)s" +msgstr "Réinitialiser votre mot de passe sur %(site_name)s" #: bookwyrm/templates/feed/direct_messages.html:8 #, python-format @@ -807,19 +790,17 @@ msgstr "Vous n’avez aucun message pour l’instant." #: bookwyrm/templates/feed/feed.html:9 msgid "Home Timeline" -msgstr "" +msgstr "Mon fil d’actualité" #: bookwyrm/templates/feed/feed.html:11 -#, fuzzy #| msgid "%(tab_title)s Timeline" msgid "Local Timeline" -msgstr "%(tab_title)s — Fil d’actualité" +msgstr "Fil d’actualité local" #: bookwyrm/templates/feed/feed.html:13 -#, fuzzy #| msgid "Federated Servers" msgid "Federated Timeline" -msgstr "Serveurs fédérés" +msgstr "Fil d’actualité des instances fédérées" #: bookwyrm/templates/feed/feed.html:19 msgid "Home" @@ -836,7 +817,7 @@ msgstr "Fédéré" #: bookwyrm/templates/feed/feed.html:33 #, python-format msgid "load 0 unread status(es)" -msgstr "" +msgstr "charger le(s) 0 statut(s) non lu(s)" #: bookwyrm/templates/feed/feed.html:48 msgid "There aren't any activities right now! Try following a user to get started" @@ -845,10 +826,9 @@ msgstr "Aucune activité pour l’instant ! Abonnez‑vous à quelqu’un pour #: bookwyrm/templates/feed/feed.html:56 #: bookwyrm/templates/get_started/users.html:6 msgid "Who to follow" -msgstr "" +msgstr "À qui s’abonner" #: bookwyrm/templates/feed/feed_layout.html:5 -#, fuzzy #| msgid "Updated:" msgid "Updates" msgstr "Mises à jour" @@ -863,27 +843,25 @@ msgstr "Vos livres" msgid "There are no books here right now! Try searching for a book to get started" msgstr "Aucun livre ici pour l’instant ! Cherchez un livre pour commencer" -#: bookwyrm/templates/feed/feed_layout.html:23 -#: bookwyrm/templates/user/shelf.html:28 -#, fuzzy -#| msgid "Read" -msgid "To Read" -msgstr "Lu" - #: bookwyrm/templates/feed/feed_layout.html:24 #: bookwyrm/templates/user/shelf.html:28 -#, fuzzy -#| msgid "Started reading" -msgid "Currently Reading" -msgstr "Commencer la lecture" +#| msgid "Read" +msgid "To Read" +msgstr "À lire" #: bookwyrm/templates/feed/feed_layout.html:25 +#: bookwyrm/templates/user/shelf.html:28 +#| msgid "Started reading" +msgid "Currently Reading" +msgstr "En train de lire" + +#: bookwyrm/templates/feed/feed_layout.html:26 #: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:11 #: bookwyrm/templates/user/shelf.html:28 msgid "Read" msgstr "Lu" -#: bookwyrm/templates/feed/feed_layout.html:74 bookwyrm/templates/goal.html:26 +#: bookwyrm/templates/feed/feed_layout.html:88 bookwyrm/templates/goal.html:26 #: bookwyrm/templates/snippets/goal_card.html:6 #, python-format msgid "%(year)s Reading Goal" @@ -893,27 +871,26 @@ msgstr "Défi lecture pour %(year)s" #, python-format msgid "%(mutuals)s follower you follow" msgid_plural "%(mutuals)s followers you follow" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(mutuals)s abonnement auxquel vous êtes abonné(e)" +msgstr[1] "%(mutuals)s abonnements auxquels vous êtes abonné(e)" #: bookwyrm/templates/feed/suggested_users.html:19 #, python-format msgid "%(shared_books)s book on your shelves" msgid_plural "%(shared_books)s books on your shelves" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(shared_books)s livre sur vos étagères" +msgstr[1] "%(shared_books)s livres sur vos étagères" #: bookwyrm/templates/get_started/book_preview.html:6 -#, fuzzy, python-format +#, python-format #| msgid "Want to Read \"%(book_title)s\"" msgid "Have you read %(book_title)s?" -msgstr "A envie de lire « %(book_title)s »" +msgstr "Avez‑vous lu « %(book_title)s » ?" #: bookwyrm/templates/get_started/books.html:6 -#, fuzzy #| msgid "Started reading" msgid "What are you reading?" -msgstr "Lecture commencée le" +msgstr "Que lisez‑vous ?" #: bookwyrm/templates/get_started/books.html:9 #: bookwyrm/templates/lists/list.html:58 @@ -930,7 +907,7 @@ msgstr "Aucun livre trouvé pour « %(query)s »" #: bookwyrm/templates/get_started/books.html:11 #, python-format msgid "You can add books when you start using %(site_name)s." -msgstr "" +msgstr "Vous pourrez ajouter des livres lorsque vous commencerez à utiliser %(site_name)s." #: bookwyrm/templates/get_started/books.html:16 #: bookwyrm/templates/get_started/books.html:17 @@ -942,16 +919,15 @@ msgid "Search" msgstr "Chercher" #: bookwyrm/templates/get_started/books.html:26 -#, fuzzy #| msgid "Suggest Books" msgid "Suggested Books" msgstr "Suggérer des livres" #: bookwyrm/templates/get_started/books.html:41 -#, fuzzy, python-format +#, python-format #| msgid "Join %(name)s" msgid "Popular on %(site_name)s" -msgstr "À propos de %(name)s" +msgstr "Populaire sur %(site_name)s" #: bookwyrm/templates/get_started/books.html:51 #: bookwyrm/templates/lists/list.html:75 @@ -961,46 +937,42 @@ msgstr "Aucun livre trouvé" #: bookwyrm/templates/get_started/books.html:54 #: bookwyrm/templates/get_started/profile.html:54 msgid "Save & continue" -msgstr "" +msgstr "Enregistrer & continuer" #: bookwyrm/templates/get_started/layout.html:14 -#, fuzzy, python-format +#, python-format #| msgid "Join %(name)s" msgid "Welcome to %(site_name)s!" -msgstr "À propos de %(name)s" +msgstr "Bienvenu(e) sur %(site_name)s !" #: bookwyrm/templates/get_started/layout.html:16 msgid "These are some first steps to get you started." -msgstr "" +msgstr "Voici quelques étapes pour commencer votre profil." #: bookwyrm/templates/get_started/layout.html:30 #: bookwyrm/templates/get_started/profile.html:6 -#, fuzzy #| msgid "User Profile" msgid "Create your profile" -msgstr "Profil" +msgstr "Créez votre profil" #: bookwyrm/templates/get_started/layout.html:34 -#, fuzzy #| msgid "Add Books" msgid "Add books" -msgstr "Ajouter des livres" +msgstr "Ajoutez des livres" #: bookwyrm/templates/get_started/layout.html:38 -#, fuzzy #| msgid "Friendly" msgid "Find friends" -msgstr "Sympa" +msgstr "Établissez des contacts" #: bookwyrm/templates/get_started/layout.html:44 msgid "Skip this step" -msgstr "" +msgstr "Passer cette étape" #: bookwyrm/templates/get_started/layout.html:48 -#, fuzzy #| msgid "Finished" msgid "Finish" -msgstr "Terminé" +msgstr "Terminer" #: bookwyrm/templates/get_started/profile.html:15 #: bookwyrm/templates/preferences/edit_user.html:24 @@ -1014,7 +986,7 @@ msgstr "Résumé :" #: bookwyrm/templates/get_started/profile.html:23 msgid "A little bit about you" -msgstr "" +msgstr "Parlez‑nous de vous" #: bookwyrm/templates/get_started/profile.html:32 #: bookwyrm/templates/preferences/edit_user.html:17 @@ -1029,17 +1001,16 @@ msgstr "Autoriser les abonnements manuellement :" #: bookwyrm/templates/get_started/profile.html:48 #: bookwyrm/templates/preferences/edit_user.html:58 msgid "Show this account in suggested users:" -msgstr "" +msgstr "Afficher ce compte dans ceux suggérés :" #: bookwyrm/templates/get_started/profile.html:52 msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." -msgstr "" +msgstr "Votre compte sera listé dans le répertoire et pourra être recommandé à d’autres utilisateurs ou utilisatrices de BookWyrm." #: bookwyrm/templates/get_started/users.html:11 -#, fuzzy #| msgid "Search for a book or user" msgid "Search for a user" -msgstr "Chercher un livre ou un compte" +msgstr "Chercher un compte" #: bookwyrm/templates/get_started/users.html:13 #: bookwyrm/templates/search_results.html:76 @@ -1053,7 +1024,6 @@ msgid "%(year)s Reading Progress" msgstr "Progression de lecture pour %(year)s" #: bookwyrm/templates/goal.html:11 -#, fuzzy #| msgid "Edit Book" msgid "Edit Goal" msgstr "Modifier le défi" @@ -1070,13 +1040,13 @@ msgid "%(name)s hasn't set a reading goal for %(year)s." msgstr "%(name)s n’a aucun défi lecture pour %(year)s." #: bookwyrm/templates/goal.html:51 -#, fuzzy, python-format +#, python-format #| msgid "Your books" msgid "Your %(year)s Books" msgstr "Vos livres en %(year)s" #: bookwyrm/templates/goal.html:53 -#, fuzzy, python-format +#, python-format #| msgid "%(username)s has no followers" msgid "%(username)s's %(year)s Books" msgstr "Livres de %(username)s en %(year)s" @@ -1087,14 +1057,13 @@ msgid "Import Books" msgstr "Importer des livres" #: bookwyrm/templates/import.html:16 -#, fuzzy #| msgid "Data source" msgid "Data source:" -msgstr "Source de données" +msgstr "Source de données :" #: bookwyrm/templates/import.html:29 msgid "Data file:" -msgstr "" +msgstr "Fichier de données :" #: bookwyrm/templates/import.html:37 msgid "Include reviews" @@ -1224,10 +1193,9 @@ msgid "Feed" msgstr "Fil d’actualité" #: bookwyrm/templates/layout.html:102 -#, fuzzy #| msgid "Instance Settings" msgid "Settings" -msgstr "Paramètres de l’instance" +msgstr "Paramètres" #: bookwyrm/templates/layout.html:111 #: bookwyrm/templates/settings/admin_layout.html:24 @@ -1239,7 +1207,7 @@ msgstr "Invitations" #: bookwyrm/templates/layout.html:118 msgid "Admin" -msgstr "" +msgstr "Admin" #: bookwyrm/templates/layout.html:125 msgid "Log out" @@ -1255,13 +1223,12 @@ msgstr "Notifications" #: bookwyrm/templates/login.html:17 #: bookwyrm/templates/snippets/register_form.html:4 msgid "Username:" -msgstr "Nom d’utilisateur :" +msgstr "Nom du compte :" #: bookwyrm/templates/layout.html:156 -#, fuzzy #| msgid "Password:" msgid "password" -msgstr "Mot de passe :" +msgstr "Mot de passe" #: bookwyrm/templates/layout.html:157 bookwyrm/templates/login.html:36 msgid "Forgot your password?" @@ -1274,7 +1241,7 @@ msgstr "Se connecter" #: bookwyrm/templates/layout.html:168 msgid "Join" -msgstr "" +msgstr "Rejoindre" #: bookwyrm/templates/layout.html:191 msgid "About this server" @@ -1287,11 +1254,11 @@ msgstr "Contacter l’administrateur du site" #: bookwyrm/templates/layout.html:202 #, python-format msgid "Support %(site_name)s on %(support_title)s" -msgstr "" +msgstr "Soutenez %(site_name)s avec %(support_title)s" #: bookwyrm/templates/layout.html:206 msgid "BookWyrm is open source software. You can contribute or report issues on GitHub." -msgstr "Bookwyrm est un logiciel libre. Vous pouvez contribuer ou faire des rapports de bogues via GitHub." +msgstr "BookWyrm est un logiciel libre. Vous pouvez contribuer ou faire des rapports de bogues via GitHub." #: bookwyrm/templates/lists/create_form.html:5 #: bookwyrm/templates/lists/lists.html:19 @@ -1299,16 +1266,16 @@ msgid "Create List" msgstr "Créer une liste" #: bookwyrm/templates/lists/created_text.html:5 -#, fuzzy, python-format +#, python-format #| msgid "favorited your %(preview_name)s" msgid "Created and curated by %(username)s" -msgstr "Messages directs avec %(username)s" +msgstr "Créée et modérée par %(username)s" #: bookwyrm/templates/lists/created_text.html:7 -#, fuzzy, python-format +#, python-format #| msgid "favorited your %(preview_name)s" msgid "Created by %(username)s" -msgstr "Messages directs avec %(username)s" +msgstr "Créée par %(username)s" #: bookwyrm/templates/lists/curate.html:6 msgid "Pending Books" @@ -1349,7 +1316,7 @@ msgstr "Fermée" #: bookwyrm/templates/lists/form.html:22 msgid "Only you can add and remove books to this list" -msgstr "Vous seul(e) pouvez ajouter ou supprimer des livres dans cette liste" +msgstr "Vous seul(e) pouvez ajouter ou retirer des livres dans cette liste" #: bookwyrm/templates/lists/form.html:26 msgid "Curated" @@ -1373,10 +1340,10 @@ msgid "This list is currently empty" msgstr "Cette liste est vide actuellement" #: bookwyrm/templates/lists/list.html:35 -#, fuzzy, python-format +#, python-format #| msgid "favorited your %(preview_name)s" msgid "Added by %(username)s" -msgstr "Messages directs avec %(username)s" +msgstr "Ajoutée par %(username)s" #: bookwyrm/templates/lists/list.html:41 #: bookwyrm/templates/snippets/shelf_selector.html:26 @@ -1430,128 +1397,121 @@ msgstr "En savoir plus sur ce site" #: bookwyrm/templates/moderation/report_preview.html:6 #, python-format msgid "Report #%(report_id)s: %(username)s" -msgstr "" +msgstr "Signalement #%(report_id)s : %(username)s" #: bookwyrm/templates/moderation/report.html:10 msgid "Back to reports" -msgstr "" +msgstr "Retour aux signalements" #: bookwyrm/templates/moderation/report.html:18 -#, fuzzy #| msgid "Notifications" msgid "Actions" -msgstr "Notifications" +msgstr "Actions" #: bookwyrm/templates/moderation/report.html:19 -#, fuzzy #| msgid "User Profile" msgid "View user profile" -msgstr "Profil" +msgstr "Voir le profil" #: bookwyrm/templates/moderation/report.html:22 -#: bookwyrm/templates/snippets/status/status_options.html:25 +#: bookwyrm/templates/snippets/status/status_options.html:35 #: bookwyrm/templates/snippets/user_options.html:13 msgid "Send direct message" msgstr "Envoyer un message direct" #: bookwyrm/templates/moderation/report.html:27 msgid "Deactivate user" -msgstr "" +msgstr "Désactiver le compte" #: bookwyrm/templates/moderation/report.html:29 msgid "Reactivate user" -msgstr "" +msgstr "Réactiver le compte" #: bookwyrm/templates/moderation/report.html:36 msgid "Moderator Comments" -msgstr "" +msgstr "Commentaires de l’équipe de modération" #: bookwyrm/templates/moderation/report.html:54 -#: bookwyrm/templates/snippets/create_status.html:12 +#: bookwyrm/templates/snippets/create_status.html:28 #: bookwyrm/templates/snippets/create_status_form.html:44 msgid "Comment" msgstr "Commentaire" #: bookwyrm/templates/moderation/report.html:59 -#, fuzzy #| msgid "Delete status" msgid "Reported statuses" -msgstr "Supprimer le statut" +msgstr "Statuts signalés" #: bookwyrm/templates/moderation/report.html:61 msgid "No statuses reported" -msgstr "" +msgstr "Aucun statut signalé" #: bookwyrm/templates/moderation/report.html:67 msgid "Statuses has been deleted" -msgstr "" +msgstr "Les statuts ont été supprimés" #: bookwyrm/templates/moderation/report_modal.html:6 -#, fuzzy, python-format +#, python-format #| msgid "Join %(name)s" msgid "Report @%(username)s" -msgstr "Listes : %(username)s" +msgstr "Signaler @%(username)s" #: bookwyrm/templates/moderation/report_modal.html:21 #, python-format msgid "This report will be sent to %(site_name)s's moderators for review." -msgstr "" +msgstr "Ce signalement sera envoyé à l’équipe de modération de %(site_name)s pour traitement." #: bookwyrm/templates/moderation/report_modal.html:22 -#, fuzzy #| msgid "More about this site" msgid "More info about this report:" -msgstr "En savoir plus sur ce site" +msgstr "En savoir plus sur ce signalement :" #: bookwyrm/templates/moderation/report_preview.html:13 msgid "No notes provided" -msgstr "" +msgstr "Aucune note fournie" #: bookwyrm/templates/moderation/report_preview.html:20 -#, fuzzy, python-format +#, python-format #| msgid "favorited your %(preview_name)s" msgid "Reported by %(username)s" -msgstr "Messages directs avec %(username)s" +msgstr "Signalé par %(username)s" #: bookwyrm/templates/moderation/report_preview.html:30 msgid "Re-open" -msgstr "" +msgstr "Réouvrir" #: bookwyrm/templates/moderation/report_preview.html:32 msgid "Resolve" -msgstr "" +msgstr "Résoudre" #: bookwyrm/templates/moderation/reports.html:6 -#, fuzzy, python-format +#, python-format #| msgid "Join %(name)s" msgid "Reports: %(server_name)s" -msgstr "Listes : %(username)s" +msgstr "Signalements : %(server_name)s" #: bookwyrm/templates/moderation/reports.html:8 #: bookwyrm/templates/moderation/reports.html:16 #: bookwyrm/templates/settings/admin_layout.html:28 -#, fuzzy #| msgid "Recent Imports" msgid "Reports" -msgstr "Importations récentes" +msgstr "Signalements" #: bookwyrm/templates/moderation/reports.html:13 -#, fuzzy, python-format +#, python-format #| msgid "Join %(name)s" msgid "Reports: %(server_name)s" -msgstr "Listes : %(username)s" +msgstr "Signalements: %(server_name)s" #: bookwyrm/templates/moderation/reports.html:27 -#, fuzzy #| msgid "Shelved" msgid "Resolved" -msgstr "Ajouté à une étagère" +msgstr "Résolus" #: bookwyrm/templates/moderation/reports.html:34 -#, fuzzy #| msgid "No books found" msgid "No reports found." -msgstr "Aucun livre trouvé" +msgstr "Aucun signalement trouvé." #: bookwyrm/templates/notifications.html:14 msgid "Delete notifications" @@ -1663,7 +1623,7 @@ msgstr "Votre importation est terminée." #: bookwyrm/templates/notifications.html:113 #, python-format msgid "A new report needs moderation." -msgstr "" +msgstr "Un nouveau signalement a besoin d’être traité." #: bookwyrm/templates/notifications.html:139 msgid "You're all caught up!" @@ -1710,16 +1670,16 @@ msgstr "Modifier le profil" #: bookwyrm/templates/preferences/edit_user.html:46 msgid "Show set reading goal prompt in feed:" -msgstr "" +msgstr "Afficher le message pour définir un défi lecture dans le fil d’actualité :" #: bookwyrm/templates/preferences/edit_user.html:62 #, python-format msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." -msgstr "" +msgstr "Votre compte sera listé dans le répertoire et pourra être recommandé à d’autres utilisateurs ou utilisatrices de BookWyrm." #: bookwyrm/templates/preferences/edit_user.html:65 msgid "Preferred Timezone: " -msgstr "" +msgstr "Fuseau horaire préféré" #: bookwyrm/templates/preferences/preferences_layout.html:11 msgid "Account" @@ -1766,7 +1726,7 @@ msgstr "Gérer les comptes" #: bookwyrm/templates/settings/user_admin.html:3 #: bookwyrm/templates/settings/user_admin.html:10 msgid "Users" -msgstr "" +msgstr "Comptes" #: bookwyrm/templates/settings/admin_layout.html:32 #: bookwyrm/templates/settings/federation.html:3 @@ -1781,7 +1741,6 @@ msgstr "Paramètres de l’instance" #: bookwyrm/templates/settings/admin_layout.html:41 #: bookwyrm/templates/settings/site.html:4 #: bookwyrm/templates/settings/site.html:6 -#, fuzzy #| msgid "Instance Settings" msgid "Site Settings" msgstr "Paramètres du site" @@ -1808,29 +1767,26 @@ msgstr "Enregistrement" #: bookwyrm/templates/settings/federated_server.html:7 msgid "Back to server list" -msgstr "" +msgstr "Retour à la liste des serveurs" #: bookwyrm/templates/settings/federated_server.html:12 msgid "Details" -msgstr "" +msgstr "Détails" #: bookwyrm/templates/settings/federated_server.html:15 -#, fuzzy #| msgid "Software" msgid "Software:" -msgstr "Logiciel" +msgstr "Logiciel :" #: bookwyrm/templates/settings/federated_server.html:19 -#, fuzzy #| msgid "Description:" msgid "Version:" msgstr "Description :" #: bookwyrm/templates/settings/federated_server.html:23 -#, fuzzy #| msgid "Status" msgid "Status:" -msgstr "Statut" +msgstr "Statut :" #: bookwyrm/templates/settings/federated_server.html:30 #: bookwyrm/templates/user/user_layout.html:50 @@ -1838,56 +1794,50 @@ msgid "Activity" msgstr "Activité" #: bookwyrm/templates/settings/federated_server.html:33 -#, fuzzy #| msgid "Username:" msgid "Users:" -msgstr "Nom d’utilisateur :" +msgstr "Comptes :" #: bookwyrm/templates/settings/federated_server.html:36 #: bookwyrm/templates/settings/federated_server.html:43 msgid "View all" -msgstr "" +msgstr "Voir tous" #: bookwyrm/templates/settings/federated_server.html:40 -#, fuzzy #| msgid "Recent Imports" msgid "Reports:" -msgstr "Importations récentes" +msgstr "Signalements :" #: bookwyrm/templates/settings/federated_server.html:47 -#, fuzzy #| msgid "followed you" msgid "Followed by us:" -msgstr "s’est abonné(e)" +msgstr "Suivi par nous :" #: bookwyrm/templates/settings/federated_server.html:53 -#, fuzzy #| msgid "followed you" msgid "Followed by them:" -msgstr "s’est abonné(e)" +msgstr "Suivi par eux :" #: bookwyrm/templates/settings/federated_server.html:59 -#, fuzzy #| msgid "Blocked Users" msgid "Blocked by us:" -msgstr "Comptes bloqués" +msgstr "Bloqués par nous :" #: bookwyrm/templates/settings/federation.html:13 msgid "Server name" msgstr "Nom du serveur" #: bookwyrm/templates/settings/federation.html:17 -#, fuzzy #| msgid "Federated" msgid "Date federated" -msgstr "Fédéré" +msgstr "Date de fédération" #: bookwyrm/templates/settings/federation.html:21 msgid "Software" msgstr "Logiciel" #: bookwyrm/templates/settings/federation.html:24 -#: bookwyrm/templates/settings/manage_invite_requests.html:40 +#: bookwyrm/templates/settings/manage_invite_requests.html:44 #: bookwyrm/templates/settings/status_filter.html:5 #: bookwyrm/templates/settings/user_admin.html:32 msgid "Status" @@ -1897,75 +1847,77 @@ msgstr "Statut" #: bookwyrm/templates/settings/manage_invite_requests.html:11 #: bookwyrm/templates/settings/manage_invite_requests.html:25 #: bookwyrm/templates/settings/manage_invites.html:11 -#, fuzzy #| msgid "Invites" msgid "Invite Requests" msgstr "Invitations" #: bookwyrm/templates/settings/manage_invite_requests.html:23 msgid "Ignored Invite Requests" -msgstr "" +msgstr "Invitations ignorées" #: bookwyrm/templates/settings/manage_invite_requests.html:35 -msgid "Date" -msgstr "" +#| msgid "Federated" +msgid "Date requested" +msgstr "Date d’envoi" -#: bookwyrm/templates/settings/manage_invite_requests.html:38 +#: bookwyrm/templates/settings/manage_invite_requests.html:39 +#| msgid "Accept" +msgid "Date accepted" +msgstr "Date de validation" + +#: bookwyrm/templates/settings/manage_invite_requests.html:42 msgid "Email" -msgstr "" +msgstr "Email" -#: bookwyrm/templates/settings/manage_invite_requests.html:43 -#, fuzzy +#: bookwyrm/templates/settings/manage_invite_requests.html:47 #| msgid "Notifications" msgid "Action" -msgstr "Notifications" +msgstr "Action" -#: bookwyrm/templates/settings/manage_invite_requests.html:46 -#, fuzzy +#: bookwyrm/templates/settings/manage_invite_requests.html:50 #| msgid "Follow Requests" msgid "No requests" -msgstr "Demandes d’abonnement" +msgstr "Aucune demande" -#: bookwyrm/templates/settings/manage_invite_requests.html:54 +#: bookwyrm/templates/settings/manage_invite_requests.html:59 #: bookwyrm/templates/settings/status_filter.html:16 -#, fuzzy #| msgid "Accept" msgid "Accepted" -msgstr "Accepter" +msgstr "Accepté(e)s" -#: bookwyrm/templates/settings/manage_invite_requests.html:56 +#: bookwyrm/templates/settings/manage_invite_requests.html:61 #: bookwyrm/templates/settings/status_filter.html:12 msgid "Sent" -msgstr "" +msgstr "Envoyé(e)s" -#: bookwyrm/templates/settings/manage_invite_requests.html:58 +#: bookwyrm/templates/settings/manage_invite_requests.html:63 #: bookwyrm/templates/settings/status_filter.html:8 msgid "Requested" -msgstr "" +msgstr "Demandé(e)s" -#: bookwyrm/templates/settings/manage_invite_requests.html:68 +#: bookwyrm/templates/settings/manage_invite_requests.html:73 msgid "Send invite" -msgstr "" +msgstr "Envoyer l’invitation" -#: bookwyrm/templates/settings/manage_invite_requests.html:70 +#: bookwyrm/templates/settings/manage_invite_requests.html:75 msgid "Re-send invite" -msgstr "" +msgstr "Envoyer l’invitation de nouveau" -#: bookwyrm/templates/settings/manage_invite_requests.html:90 +#: bookwyrm/templates/settings/manage_invite_requests.html:95 msgid "Ignore" -msgstr "" +msgstr "Ignorer" -#: bookwyrm/templates/settings/manage_invite_requests.html:92 +#: bookwyrm/templates/settings/manage_invite_requests.html:97 msgid "Un-ignore" -msgstr "" +msgstr "Ne plus ignorer" -#: bookwyrm/templates/settings/manage_invite_requests.html:103 +#: bookwyrm/templates/settings/manage_invite_requests.html:108 msgid "Back to pending requests" -msgstr "" +msgstr "Retour aux demandes en attente" -#: bookwyrm/templates/settings/manage_invite_requests.html:105 +#: bookwyrm/templates/settings/manage_invite_requests.html:110 msgid "View ignored requests" -msgstr "" +msgstr "Voir les demandes ignorées" #: bookwyrm/templates/settings/manage_invites.html:21 msgid "Generate New Invite" @@ -2052,10 +2004,9 @@ msgid "Allow registration:" msgstr "Autoriser l’enregistrement :" #: bookwyrm/templates/settings/site.html:83 -#, fuzzy #| msgid "Follow Requests" msgid "Allow invite requests:" -msgstr "Demandes d’abonnement" +msgstr "Autoriser les demandes d’invitation :" #: bookwyrm/templates/settings/site.html:87 msgid "Registration closed text:" @@ -2064,43 +2015,39 @@ msgstr "Texte affiché lorsque les enregistrements sont clos :" #: bookwyrm/templates/settings/user_admin.html:7 #, python-format msgid "Users: %(server_name)s" -msgstr "" +msgstr "Comptes : %(server_name)s" #: bookwyrm/templates/settings/user_admin.html:20 -#, fuzzy #| msgid "Username:" msgid "Username" -msgstr "Nom d’utilisateur :" +msgstr "Nom du compte" #: bookwyrm/templates/settings/user_admin.html:24 -#, fuzzy #| msgid "added" msgid "Date Added" -msgstr "a ajouté" +msgstr "Date d’ajout" #: bookwyrm/templates/settings/user_admin.html:28 msgid "Last Active" -msgstr "" +msgstr "Dernière activité" #: bookwyrm/templates/settings/user_admin.html:36 -#, fuzzy #| msgid "Remove" msgid "Remote server" -msgstr "Supprimer" +msgstr "Serveur distant" #: bookwyrm/templates/settings/user_admin.html:45 -#, fuzzy #| msgid "Activity" msgid "Active" -msgstr "Activité" +msgstr "Actif" #: bookwyrm/templates/settings/user_admin.html:45 msgid "Inactive" -msgstr "" +msgstr "Inactif" #: bookwyrm/templates/settings/user_admin.html:50 msgid "Not set" -msgstr "" +msgstr "Non défini" #: bookwyrm/templates/snippets/block_button.html:5 msgid "Block" @@ -2117,8 +2064,8 @@ msgstr "%(title)s par " #: bookwyrm/templates/snippets/boost_button.html:8 #: bookwyrm/templates/snippets/boost_button.html:9 -#: bookwyrm/templates/snippets/status/status_body.html:51 #: bookwyrm/templates/snippets/status/status_body.html:52 +#: bookwyrm/templates/snippets/status/status_body.html:53 msgid "Boost status" msgstr "Partager le statut" @@ -2131,15 +2078,15 @@ msgstr "Annuler le partage du statut" msgid "Spoiler alert:" msgstr "Alerte Spoiler :" -#: bookwyrm/templates/snippets/content_warning_field.html:4 +#: bookwyrm/templates/snippets/content_warning_field.html:10 msgid "Spoilers ahead!" msgstr "Attention spoilers !" -#: bookwyrm/templates/snippets/create_status.html:9 +#: bookwyrm/templates/snippets/create_status.html:17 msgid "Review" msgstr "Critique" -#: bookwyrm/templates/snippets/create_status.html:15 +#: bookwyrm/templates/snippets/create_status.html:39 msgid "Quote" msgstr "Citation" @@ -2148,16 +2095,14 @@ msgid "Comment:" msgstr "Commentaire :" #: bookwyrm/templates/snippets/create_status_form.html:20 -#, fuzzy #| msgid "Quote" msgid "Quote:" -msgstr "Citation" +msgstr "Citation :" #: bookwyrm/templates/snippets/create_status_form.html:22 -#, fuzzy #| msgid "Review" msgid "Review:" -msgstr "Critique" +msgstr "Critique :" #: bookwyrm/templates/snippets/create_status_form.html:29 #: bookwyrm/templates/user/shelf.html:81 @@ -2191,14 +2136,14 @@ msgstr "sur %(pages)s pages" msgid "Include spoiler alert" msgstr "Afficher une alerte spoiler" -#: bookwyrm/templates/snippets/create_status_form.html:87 +#: bookwyrm/templates/snippets/create_status_form.html:88 #: 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:94 +#: bookwyrm/templates/snippets/create_status_form.html:99 msgid "Post" msgstr "Publier" @@ -2218,45 +2163,42 @@ msgstr "Supprimer" #: bookwyrm/templates/snippets/fav_button.html:7 #: bookwyrm/templates/snippets/fav_button.html:8 -#: bookwyrm/templates/snippets/status/status_body.html:55 #: bookwyrm/templates/snippets/status/status_body.html:56 +#: bookwyrm/templates/snippets/status/status_body.html:57 msgid "Like status" msgstr "Ajouter le statut aux favoris" #: bookwyrm/templates/snippets/fav_button.html:15 #: bookwyrm/templates/snippets/fav_button.html:16 msgid "Un-like status" -msgstr "Supprimer le statut des favoris" +msgstr "Retirer le statut des favoris" #: bookwyrm/templates/snippets/filters_panel/filters_panel.html:7 -#, fuzzy #| msgid "Show less" msgid "Show filters" -msgstr "Replier" +msgstr "Afficher les filtres" #: bookwyrm/templates/snippets/filters_panel/filters_panel.html:9 msgid "Hide filters" -msgstr "" +msgstr "Masquer les filtres" #: bookwyrm/templates/snippets/filters_panel/filters_panel.html:22 msgid "Apply filters" -msgstr "" +msgstr "Appliquer les filtres" #: bookwyrm/templates/snippets/filters_panel/filters_panel.html:26 -#, fuzzy #| msgid "Clear search" msgid "Clear filters" -msgstr "Vider la requête" +msgstr "Annuler les filtres" #: bookwyrm/templates/snippets/follow_button.html:12 msgid "Follow" msgstr "S’abonner" #: bookwyrm/templates/snippets/follow_button.html:18 -#, fuzzy #| msgid "Send follow request" msgid "Undo follow request" -msgstr "Envoyer une demande d’abonnement" +msgstr "Annuler la demande d’abonnement" #: bookwyrm/templates/snippets/follow_button.html:20 msgid "Unfollow" @@ -2287,29 +2229,29 @@ msgstr[0] "souhaite lire %(counter)s livre en %(year)s" msgstr[1] "souhaite lire %(counter)s livres en %(year)s" #: bookwyrm/templates/snippets/generated_status/rating.html:3 -#, fuzzy, python-format +#, python-format #| msgid "%(title)s by " msgid "Rated %(title)s: %(display_rating)s star" msgid_plural "Rated %(title)s: %(display_rating)s stars" -msgstr[0] "%(title)s par " -msgstr[1] "%(title)s par " +msgstr[0] "A noté %(title)s : %(display_rating)s star" +msgstr[1] "A noté %(title)s : %(display_rating)s stars" #: bookwyrm/templates/snippets/generated_status/review_pure_name.html:4 #, python-format msgid "Review of \"%(book_title)s\" (%(display_rating)s star): %(review_title)s" msgid_plural "Review of \"%(book_title)s\" (%(display_rating)s stars): %(review_title)s" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Critique de « %(book_title)s » (%(display_rating)s star): %(review_title)s" +msgstr[1] "Critique de « %(book_title)s » (%(display_rating)s stars) : %(review_title)s" #: bookwyrm/templates/snippets/generated_status/review_pure_name.html:8 #, python-format msgid "Review of \"%(book_title)s\": %(review_title)s" -msgstr "" +msgstr "Critique de « %(book_title)s » : %(review_title)s" #: bookwyrm/templates/snippets/goal_card.html:23 #, python-format msgid "You can set or change your reading goal any time from your profile page" -msgstr "Vous pouvez définir ou changer vore défi lecture à n’importe quel moment depuis votre profil" +msgstr "Vous pouvez définir ou changer votre défi lecture à n’importe quel moment depuis votre profil" #: bookwyrm/templates/snippets/goal_form.html:9 msgid "Reading goal:" @@ -2354,22 +2296,22 @@ msgid "%(username)s has read %(read_count)s of %(goal_count msgstr "%(username)s a lu %(read_count)s sur %(goal_count)s livres." #: bookwyrm/templates/snippets/page_text.html:4 -#, fuzzy, python-format +#, python-format #| msgid "of %(pages)s pages" msgid "page %(page)s of %(total_pages)s" -msgstr "sur %(pages)s pages" +msgstr "page %(page)s sur %(total_pages)s pages" #: bookwyrm/templates/snippets/page_text.html:6 -#, fuzzy, python-format +#, python-format #| msgid "of %(book.pages)s pages" msgid "page %(page)s" -msgstr "sur %(book.pages)s pages" +msgstr "page %(page)s" -#: bookwyrm/templates/snippets/pagination.html:5 +#: bookwyrm/templates/snippets/pagination.html:12 msgid "Previous" msgstr "Précédente" -#: bookwyrm/templates/snippets/pagination.html:9 +#: bookwyrm/templates/snippets/pagination.html:23 msgid "Next" msgstr "Suivante" @@ -2432,7 +2374,6 @@ msgid "Edit read dates" msgstr "Modifier les date de lecture" #: bookwyrm/templates/snippets/readthrough.html:61 -#, fuzzy #| msgid "Delete these read dates?" msgid "Delete these read dates" msgstr "Supprimer ces dates de lecture" @@ -2457,10 +2398,9 @@ msgid "Sign Up" msgstr "S’enregistrer" #: bookwyrm/templates/snippets/report_button.html:5 -#, fuzzy #| msgid "Import" msgid "Report" -msgstr "Importer" +msgstr "Signaler" #: bookwyrm/templates/snippets/rss_title.html:5 #: bookwyrm/templates/snippets/status/status_header.html:11 @@ -2483,10 +2423,9 @@ msgid "quoted" msgstr "a cité" #: bookwyrm/templates/snippets/search_result_text.html:10 -#, fuzzy #| msgid "Add cover" msgid "No cover" -msgstr "Ajouter une couverture" +msgstr "Aucune couverture" #: bookwyrm/templates/snippets/search_result_text.html:22 #, python-format @@ -2498,10 +2437,9 @@ msgid "Import book" msgstr "Importer le livre" #: bookwyrm/templates/snippets/shelf_selector.html:4 -#, fuzzy #| msgid "Your books" msgid "Move book" -msgstr "Vos livres" +msgstr "Déplacer le livre" #: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:5 #, python-format @@ -2510,23 +2448,20 @@ msgstr "Terminer « %(book_title)s »" #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:5 #: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:35 -#, fuzzy #| msgid "Updated:" msgid "Update progress" -msgstr "Mises à jour" +msgstr "Progression de la mise à jour" #: bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown.html:5 msgid "More shelves" msgstr "Plus d’étagères" #: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:8 -#, fuzzy #| msgid "Started reading" msgid "Start reading" msgstr "Commencer la lecture" #: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:13 -#, fuzzy #| msgid "Finished reading" msgid "Finish reading" msgstr "Terminer la lecture" @@ -2537,10 +2472,10 @@ msgid "Want to read" msgstr "Je veux le lire" #: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:48 -#, fuzzy, python-format +#, python-format #| msgid "Join %(name)s" msgid "Remove from %(name)s" -msgstr "Listes : %(username)s" +msgstr "Retirer de %(name)s" #: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:5 #, python-format @@ -2561,9 +2496,9 @@ msgstr "partagé" msgid "Delete status" msgstr "Supprimer le statut" -#: bookwyrm/templates/snippets/status/status_body.html:34 -#: bookwyrm/templates/snippets/status/status_body.html:47 +#: bookwyrm/templates/snippets/status/status_body.html:35 #: bookwyrm/templates/snippets/status/status_body.html:48 +#: bookwyrm/templates/snippets/status/status_body.html:49 msgid "Reply" msgstr "Répondre" @@ -2582,49 +2517,52 @@ msgid "Open image in new window" msgstr "Ouvrir l’image dans une nouvelle fenêtre" #: bookwyrm/templates/snippets/status/status_header.html:22 -#, fuzzy, python-format +#, python-format #| msgid "favorited your %(preview_name)s" msgid "replied to %(username)s's review" -msgstr "Messages directs avec %(username)s" +msgstr "a répondu à la critique de %(username)s" #: bookwyrm/templates/snippets/status/status_header.html:24 -#, fuzzy, python-format +#, python-format #| msgid "replied to your status" msgid "replied to %(username)s's comment" -msgstr "a répondu à votre statut" +msgstr "a répondu au commentaire de %(username)s" #: bookwyrm/templates/snippets/status/status_header.html:26 -#, fuzzy, python-format +#, python-format #| msgid "replied to your status" msgid "replied to %(username)s's quote" -msgstr "a répondu à votre statut" +msgstr "a répondu à la citation de %(username)s" #: bookwyrm/templates/snippets/status/status_header.html:28 -#, fuzzy, python-format +#, python-format #| msgid "replied to your status" msgid "replied to %(username)s's status" -msgstr "a répondu à votre statut" +msgstr "a répondu au statut de %(username)s" #: bookwyrm/templates/snippets/status/status_options.html:7 #: bookwyrm/templates/snippets/user_options.html:7 msgid "More options" msgstr "Plus d’options" +#: bookwyrm/templates/snippets/status/status_options.html:27 +#| msgid "Delete these read dates?" +msgid "Delete & re-draft" +msgstr "Supprimer & recommencer la rédaction" + #: bookwyrm/templates/snippets/switch_edition_button.html:5 msgid "Switch to this edition" msgstr "Changer vers cette édition" #: bookwyrm/templates/snippets/table-sort-header.html:6 -#, fuzzy #| msgid "Started reading" -msgid "Sorted asccending" -msgstr "Lecture commencée le" +msgid "Sorted ascending" +msgstr "Trié par ordre croissant" #: bookwyrm/templates/snippets/table-sort-header.html:10 -#, fuzzy #| msgid "Started reading" msgid "Sorted descending" -msgstr "Lecture commencée le" +msgstr "Trié par ordre décroissant" #: bookwyrm/templates/snippets/tag.html:14 msgid "Remove tag" @@ -2640,10 +2578,10 @@ msgid "Books tagged \"%(tag.name)s\"" msgstr "Livres tagués « %(tag.name)s »" #: bookwyrm/templates/user/books_header.html:5 -#, fuzzy, python-format +#, python-format #| msgid "%(username)s has no followers" msgid "%(username)s's books" -msgstr "Livres de %(username)s en %(year)s" +msgstr "Livres de %(username)s" #: bookwyrm/templates/user/create_shelf_form.html:5 #: bookwyrm/templates/user/create_shelf_form.html:22 @@ -2683,7 +2621,7 @@ msgid "Your Lists" msgstr "Vos listes" #: bookwyrm/templates/user/lists.html:11 -#, fuzzy, python-format +#, python-format #| msgid "Join %(name)s" msgid "Lists: %(username)s" msgstr "Listes : %(username)s" @@ -2693,10 +2631,9 @@ msgid "Create list" msgstr "Créer une liste" #: bookwyrm/templates/user/shelf.html:24 bookwyrm/views/shelf.py:56 -#, fuzzy #| msgid "books" msgid "All books" -msgstr "livres" +msgstr "Tous les livres" #: bookwyrm/templates/user/shelf.html:37 msgid "Create shelf" @@ -2731,14 +2668,14 @@ msgid "Edit profile" msgstr "Modifier le profil" #: bookwyrm/templates/user/user.html:34 -#, fuzzy, python-format +#, python-format #| msgid "See all %(size)s" msgid "View all %(size)s" msgstr "Voir les %(size)s" #: bookwyrm/templates/user/user.html:47 msgid "View all books" -msgstr "" +msgstr "Voir tous les livres" #: bookwyrm/templates/user/user.html:59 #, python-format @@ -2766,10 +2703,9 @@ msgid "Reading Goal" msgstr "Défi lecture" #: bookwyrm/templates/user/user_layout.html:68 -#, fuzzy #| msgid "Book" msgid "Books" -msgstr "Livre" +msgstr "Livres" #: bookwyrm/templates/user/user_preview.html:13 #, python-format @@ -2777,12 +2713,12 @@ msgid "Joined %(date)s" msgstr "Enregistré(e) %(date)s" #: bookwyrm/templates/user/user_preview.html:15 -#, fuzzy, python-format +#, python-format #| msgid "%(username)s has no followers" msgid "%(counter)s follower" msgid_plural "%(counter)s followers" -msgstr[0] "%(username)s n’a pas d’abonné(e)" -msgstr[1] "%(username)s n’a pas d’abonné(e)s" +msgstr[0] "%(counter)s abonnement" +msgstr[1] "%(counter)s abonnements" #: bookwyrm/templates/user/user_preview.html:16 #, python-format @@ -2791,12 +2727,12 @@ msgstr "%(counter)s abonnements" #: bookwyrm/views/password.py:32 msgid "No user with that email address was found." -msgstr "" +msgstr "Aucun compte avec cette adresse email n’a été trouvé." #: bookwyrm/views/password.py:41 #, python-format msgid "A password reset link sent to %s" -msgstr "" +msgstr "Un lien de réinitialisation a été envoyé à %s." #, fuzzy #~| msgid "Started" diff --git a/locale/zh_CN/LC_MESSAGES/django.mo b/locale/zh_CN/LC_MESSAGES/django.mo index 019000e2a74794ac764fb9313da0e56c4fa59479..07b8d63ca2e7a940d4feb0d0a94892b7a708e8b5 100644 GIT binary patch delta 5717 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 delta 5717 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 Date: Mon, 5 Apr 2021 12:53:20 +0200 Subject: [PATCH 002/288] [l10n] Add missing i18n string to template. --- bookwyrm/templates/lists/lists.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/lists/lists.html b/bookwyrm/templates/lists/lists.html index a92583054..27e56f11a 100644 --- a/bookwyrm/templates/lists/lists.html +++ b/bookwyrm/templates/lists/lists.html @@ -11,7 +11,7 @@

{% trans "Lists" %} {% if request.user.is_authenticated %} - Your lists + {% trans "Your Lists" %} {% endif %}

From 6971c9b133af3d4ebc6f32c62535f6e330457d34 Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Mon, 5 Apr 2021 16:16:05 +0200 Subject: [PATCH 003/288] [assets] Move toggleAllCheckboxes code to its own file. --- bookwyrm/static/js/check_all.js | 29 ++++++++++++++++++----------- bookwyrm/static/js/shared.js | 9 +-------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/bookwyrm/static/js/check_all.js b/bookwyrm/static/js/check_all.js index 07d30a686..72c83e3e9 100644 --- a/bookwyrm/static/js/check_all.js +++ b/bookwyrm/static/js/check_all.js @@ -1,17 +1,24 @@ -/* exported toggleAllCheckboxes */ - /** * Toggle all descendant checkboxes of a target. * - * Use `data-target="ID_OF_TARGET"` on the node being listened to. - * - * @param {Event} event - change Event - * @return {undefined} + * 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. */ -function toggleAllCheckboxes(event) { - const mainCheckbox = event.target; +(function() { + 'use strict'; + + function toggleAllCheckboxes(event) { + const mainCheckbox = event.target; + + document + .querySelectorAll(`#${mainCheckbox.dataset.target} [type="checkbox"]`) + .forEach(checkbox => {checkbox.checked = mainCheckbox.checked;}); + } document - .querySelectorAll(`#${mainCheckbox.dataset.target} [type="checkbox"]`) - .forEach(checkbox => {checkbox.checked = mainCheckbox.checked;}); -} + .querySelectorAll('[data-action="toggle-all"]') + .forEach(input => { + input.addEventListener('change', toggleAllCheckboxes); + }); +})(); diff --git a/bookwyrm/static/js/shared.js b/bookwyrm/static/js/shared.js index 7a198619c..5ca3d7d7a 100644 --- a/bookwyrm/static/js/shared.js +++ b/bookwyrm/static/js/shared.js @@ -1,4 +1,4 @@ -/* globals setDisplay TabGroup toggleAllCheckboxes updateDisplay */ +/* globals setDisplay TabGroup updateDisplay */ // set up javascript listeners window.onload = function() { @@ -36,13 +36,6 @@ window.onload = function() { // update localstorage Array.from(document.getElementsByClassName('set-display')) .forEach(t => t.onclick = updateDisplay); - - // Toggle all checkboxes. - document - .querySelectorAll('[data-action="toggle-all"]') - .forEach(input => { - input.addEventListener('change', toggleAllCheckboxes); - }); }; function back(e) { From 5aea7343b40f53aad7a512d6da80b7300d3498a0 Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Mon, 5 Apr 2021 16:16:48 +0200 Subject: [PATCH 004/288] [assets] Rename some files for consistency. --- bookwyrm/static/css/{format.css => bookwyrm.css} | 0 bookwyrm/static/js/{shared.js => bookwyrm.js} | 0 bookwyrm/templates/layout.html | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename bookwyrm/static/css/{format.css => bookwyrm.css} (100%) rename bookwyrm/static/js/{shared.js => bookwyrm.js} (100%) diff --git a/bookwyrm/static/css/format.css b/bookwyrm/static/css/bookwyrm.css similarity index 100% rename from bookwyrm/static/css/format.css rename to bookwyrm/static/css/bookwyrm.css diff --git a/bookwyrm/static/js/shared.js b/bookwyrm/static/js/bookwyrm.js similarity index 100% rename from bookwyrm/static/js/shared.js rename to bookwyrm/static/js/bookwyrm.js diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index 80eb386a5..ccc52a501 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -6,8 +6,8 @@ {% block title %}BookWyrm{% endblock %} | {{ site.name }} - + @@ -212,7 +212,7 @@ - + {% block scripts %}{% endblock %} From cbed5e331b02b95f09ee6aab7d703ce2b896d558 Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Mon, 5 Apr 2021 16:17:11 +0200 Subject: [PATCH 005/288] [assets] Move some frontend assets to _vendor_ directories: This simplifies linting of files we have no grasp on, and clarifies responsibilities. - Add .eslintignore. - Restrict linting to bookwyrm/static. --- .eslintignore | 1 + .github/workflows/lint-frontend.yaml | 6 ++++-- .stylelintignore | 3 +-- bookwyrm/static/css/{ => vendor}/bulma.css.map | 0 bookwyrm/static/css/{ => vendor}/bulma.min.css | 0 bookwyrm/static/css/{ => vendor}/icons.css | 0 bookwyrm/static/js/{ => vendor}/tabs.js | 0 bookwyrm/templates/book/book.html | 2 +- bookwyrm/templates/feed/feed_layout.html | 2 +- bookwyrm/templates/layout.html | 6 +++--- 10 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 .eslintignore rename bookwyrm/static/css/{ => vendor}/bulma.css.map (100%) rename bookwyrm/static/css/{ => vendor}/bulma.min.css (100%) rename bookwyrm/static/css/{ => vendor}/icons.css (100%) rename bookwyrm/static/js/{ => vendor}/tabs.js (100%) diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..b2cd33f89 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/vendor/** diff --git a/.github/workflows/lint-frontend.yaml b/.github/workflows/lint-frontend.yaml index 978bbbbe5..4c5e88234 100644 --- a/.github/workflows/lint-frontend.yaml +++ b/.github/workflows/lint-frontend.yaml @@ -22,8 +22,10 @@ jobs: - name: Install modules run: yarn + # See .stylelintignore for files that are not linted. - name: Run stylelint - run: yarn stylelint **/static/**/*.css --report-needless-disables --report-invalid-scope-disables + run: yarn stylelint bookwyrm/static/**/*.css --report-needless-disables --report-invalid-scope-disables + # See .eslintignore for files that are not linted. - name: Run ESLint - run: yarn eslint . --ext .js,.jsx,.ts,.tsx + run: yarn eslint bookwyrm/static --ext .js,.jsx,.ts,.tsx diff --git a/.stylelintignore b/.stylelintignore index f456cb226..b2cd33f89 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1,2 +1 @@ -bookwyrm/static/css/bulma.*.css* -bookwyrm/static/css/icons.css +**/vendor/** diff --git a/bookwyrm/static/css/bulma.css.map b/bookwyrm/static/css/vendor/bulma.css.map similarity index 100% rename from bookwyrm/static/css/bulma.css.map rename to bookwyrm/static/css/vendor/bulma.css.map diff --git a/bookwyrm/static/css/bulma.min.css b/bookwyrm/static/css/vendor/bulma.min.css similarity index 100% rename from bookwyrm/static/css/bulma.min.css rename to bookwyrm/static/css/vendor/bulma.min.css diff --git a/bookwyrm/static/css/icons.css b/bookwyrm/static/css/vendor/icons.css similarity index 100% rename from bookwyrm/static/css/icons.css rename to bookwyrm/static/css/vendor/icons.css diff --git a/bookwyrm/static/js/tabs.js b/bookwyrm/static/js/vendor/tabs.js similarity index 100% rename from bookwyrm/static/js/tabs.js rename to bookwyrm/static/js/vendor/tabs.js diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index b91cebbac..a7887c61e 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -267,5 +267,5 @@ {% endblock %} {% block scripts %} - + {% endblock %} diff --git a/bookwyrm/templates/feed/feed_layout.html b/bookwyrm/templates/feed/feed_layout.html index 9afd5e600..edbab016e 100644 --- a/bookwyrm/templates/feed/feed_layout.html +++ b/bookwyrm/templates/feed/feed_layout.html @@ -104,5 +104,5 @@ {% endblock %} {% block scripts %} - + {% endblock %} diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index ccc52a501..c1ff12762 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -5,9 +5,9 @@ {% block title %}BookWyrm{% endblock %} | {{ site.name }} - - - + + + From 27e47b0a35999972de72abff340a6778f2269649 Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Mon, 5 Apr 2021 16:35:09 +0200 Subject: [PATCH 006/288] [lint] Update context for linting frontend files: - Lint files when pushing on the _frontend_ branch. - Lint files when eslint or stylelint config files are updated. - Use _strict_ parsing of JS files by default. This should make the JS linting test to fail. --- .eslintrc.js | 6 +++++- .github/workflows/lint-frontend.yaml | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index d39859f19..b5f3c311f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,5 +6,9 @@ module.exports = { "es6": true }, - "extends": "eslint:recommended" + "extends": "eslint:recommended", + + "rules": { + "strict": "error" + } }; diff --git a/.github/workflows/lint-frontend.yaml b/.github/workflows/lint-frontend.yaml index 4c5e88234..f370d83bc 100644 --- a/.github/workflows/lint-frontend.yaml +++ b/.github/workflows/lint-frontend.yaml @@ -3,12 +3,14 @@ name: Lint Frontend on: push: - branches: [ main, ci ] + branches: [ main, ci, frontend ] paths: - '.github/workflows/**' - 'static/**' + - '.eslintrc' + - '.stylelintrc' pull_request: - branches: [ main, ci ] + branches: [ main, ci, frontend ] jobs: lint: From 964b47ea972e6484542603fb255049e04a45bbfe Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Mon, 5 Apr 2021 16:46:30 +0200 Subject: [PATCH 007/288] [assets] Fix path for icomoon font. --- bookwyrm/static/css/vendor/icons.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bookwyrm/static/css/vendor/icons.css b/bookwyrm/static/css/vendor/icons.css index 9915ecd18..c6dbad1d0 100644 --- a/bookwyrm/static/css/vendor/icons.css +++ b/bookwyrm/static/css/vendor/icons.css @@ -1,10 +1,10 @@ @font-face { font-family: 'icomoon'; - src: url('fonts/icomoon.eot?n5x55'); - src: url('fonts/icomoon.eot?n5x55#iefix') format('embedded-opentype'), - url('fonts/icomoon.ttf?n5x55') format('truetype'), - url('fonts/icomoon.woff?n5x55') format('woff'), - url('fonts/icomoon.svg?n5x55#icomoon') format('svg'); + src: url('../fonts/icomoon.eot?n5x55'); + src: url('../fonts/icomoon.eot?n5x55#iefix') format('embedded-opentype'), + url('../fonts/icomoon.ttf?n5x55') format('truetype'), + url('../fonts/icomoon.woff?n5x55') format('woff'), + url('../fonts/icomoon.svg?n5x55#icomoon') format('svg'); font-weight: normal; font-style: normal; font-display: block; From 21ba05b4a5fd8862bb17f6283f50f441efc87e14 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 5 Apr 2021 18:29:01 +0200 Subject: [PATCH 008/288] Added wyrms.de --- instances.md | 1 + 1 file changed, 1 insertion(+) diff --git a/instances.md b/instances.md index 1c8b282b4..570328b51 100644 --- a/instances.md +++ b/instances.md @@ -2,3 +2,4 @@ | name | url | admin contact | open registration | | :--- | :-- | :------------ | :---------------- | | bookwyrm.social | http://bookwyrm.social/ | mousereeve@riseup.net / @tripofmice@friend.camp | ❌ | +| wyrms.de | https://wyrms.de/ | wyrms@tofuwabo.hu / @tofuwabohu@subversive.zone | ❌ | From 04f459a2dff559d8e7edcd2cf96c684fff1eecd2 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Apr 2021 10:17:01 -0700 Subject: [PATCH 009/288] Fixes creting invites --- bookwyrm/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 1a114e05f..b159a89ef 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -233,7 +233,7 @@ class InviteRequestForm(CustomForm): class CreateInviteForm(CustomForm): class Meta: model = models.SiteInvite - exclude = ["code", "user", "times_used"] + exclude = ["code", "user", "times_used", "invitees"] widgets = { "expiry": ExpiryWidget( choices=[ From 56330d448b12d7a029b51019e5766050a7b476bb Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Apr 2021 14:08:24 -0700 Subject: [PATCH 010/288] Changes remove status redis mock --- bookwyrm/tests/models/test_status_model.py | 4 +++- bookwyrm/tests/test_templatetags.py | 4 +++- bookwyrm/tests/views/test_inbox.py | 6 +++--- bookwyrm/tests/views/test_interaction.py | 2 +- bookwyrm/tests/views/test_status.py | 16 +++++++++++----- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/bookwyrm/tests/models/test_status_model.py b/bookwyrm/tests/models/test_status_model.py index 208bf3ab4..4263c4572 100644 --- a/bookwyrm/tests/models/test_status_model.py +++ b/bookwyrm/tests/models/test_status_model.py @@ -116,7 +116,9 @@ class Status(TestCase): def test_status_to_activity_tombstone(self, *_): """ subclass of the base model version with a "pure" serializer """ - with patch("bookwyrm.activitystreams.ActivityStream.remove_status"): + with patch( + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" + ): status = models.Status.objects.create( content="test content", user=self.local_user, diff --git a/bookwyrm/tests/test_templatetags.py b/bookwyrm/tests/test_templatetags.py index 61136c2eb..b4dc517f1 100644 --- a/bookwyrm/tests/test_templatetags.py +++ b/bookwyrm/tests/test_templatetags.py @@ -85,7 +85,9 @@ class TemplateTags(TestCase): second_child = models.Status.objects.create( reply_parent=parent, user=self.user, content="hi" ) - with patch("bookwyrm.activitystreams.ActivityStream.remove_status"): + with patch( + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" + ): third_child = models.Status.objects.create( reply_parent=parent, user=self.user, diff --git a/bookwyrm/tests/views/test_inbox.py b/bookwyrm/tests/views/test_inbox.py index 37cf00ddc..935909e10 100644 --- a/bookwyrm/tests/views/test_inbox.py +++ b/bookwyrm/tests/views/test_inbox.py @@ -444,7 +444,7 @@ class Inbox(TestCase): "object": {"id": self.status.remote_id, "type": "Tombstone"}, } with patch( - "bookwyrm.activitystreams.ActivityStream.remove_status" + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" ) as redis_mock: views.inbox.activity_task(activity) self.assertTrue(redis_mock.called) @@ -477,7 +477,7 @@ class Inbox(TestCase): "object": {"id": self.status.remote_id, "type": "Tombstone"}, } with patch( - "bookwyrm.activitystreams.ActivityStream.remove_status" + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" ) as redis_mock: views.inbox.activity_task(activity) self.assertTrue(redis_mock.called) @@ -616,7 +616,7 @@ class Inbox(TestCase): }, } with patch( - "bookwyrm.activitystreams.ActivityStream.remove_status" + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" ) as redis_mock: views.inbox.activity_task(activity) self.assertTrue(redis_mock.called) diff --git a/bookwyrm/tests/views/test_interaction.py b/bookwyrm/tests/views/test_interaction.py index 297eeb73d..8d2c63ffc 100644 --- a/bookwyrm/tests/views/test_interaction.py +++ b/bookwyrm/tests/views/test_interaction.py @@ -164,7 +164,7 @@ class InteractionViews(TestCase): self.assertEqual(models.Boost.objects.count(), 1) self.assertEqual(models.Notification.objects.count(), 1) with patch( - "bookwyrm.activitystreams.ActivityStream.remove_status" + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" ) as redis_mock: view(request, status.id) self.assertTrue(redis_mock.called) diff --git a/bookwyrm/tests/views/test_status.py b/bookwyrm/tests/views/test_status.py index e7fc62d56..5eb13b6b2 100644 --- a/bookwyrm/tests/views/test_status.py +++ b/bookwyrm/tests/views/test_status.py @@ -177,7 +177,9 @@ class StatusViews(TestCase): content="hi", book=self.book, user=self.local_user ) - with patch("bookwyrm.activitystreams.ActivityStream.remove_status") as mock: + with patch( + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" + ) as mock: result = view(request, status.id) self.assertTrue(mock.called) result.render() @@ -196,7 +198,9 @@ class StatusViews(TestCase): book=self.book, rating=2.0, user=self.local_user ) - with patch("bookwyrm.activitystreams.ActivityStream.remove_status") as mock: + with patch( + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" + ) as mock: result = view(request, status.id) self.assertFalse(mock.called) self.assertEqual(result.status_code, 400) @@ -214,7 +218,9 @@ class StatusViews(TestCase): content="hi", user=self.local_user ) - with patch("bookwyrm.activitystreams.ActivityStream.remove_status") as mock: + with patch( + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" + ) as mock: result = view(request, status.id) self.assertFalse(mock.called) self.assertEqual(result.status_code, 400) @@ -316,7 +322,7 @@ class StatusViews(TestCase): request.user = self.local_user with patch( - "bookwyrm.activitystreams.ActivityStream.remove_status" + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" ) as redis_mock: view(request, status.id) self.assertTrue(redis_mock.called) @@ -351,7 +357,7 @@ class StatusViews(TestCase): request.user.is_superuser = True with patch( - "bookwyrm.activitystreams.ActivityStream.remove_status" + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" ) as redis_mock: view(request, status.id) self.assertTrue(redis_mock.called) From 6a3c01a67f3fc7fd932469bc1bf82377952043be Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Apr 2021 14:17:45 -0700 Subject: [PATCH 011/288] stream_users function has been renamed --- bookwyrm/tests/test_activitystreams.py | 58 +++++++++++++------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/bookwyrm/tests/test_activitystreams.py b/bookwyrm/tests/test_activitystreams.py index 88ca4693b..b4efeba3f 100644 --- a/bookwyrm/tests/test_activitystreams.py +++ b/bookwyrm/tests/test_activitystreams.py @@ -47,18 +47,18 @@ class Activitystreams(TestCase): "{}-test-unread".format(self.local_user.id), ) - def test_abstractstream_stream_users(self, *_): + def test_abstractstream_get_audience(self, *_): """ get a list of users that should see a status """ status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) - users = self.test_stream.stream_users(status) + users = self.test_stream.get_audience(status) # remote users don't have feeds self.assertFalse(self.remote_user in users) self.assertTrue(self.local_user in users) self.assertTrue(self.another_user in users) - def test_abstractstream_stream_users_direct(self, *_): + def test_abstractstream_get_audience_direct(self, *_): """ get a list of users that should see a status """ status = models.Status.objects.create( user=self.remote_user, @@ -66,7 +66,7 @@ class Activitystreams(TestCase): privacy="direct", ) status.mention_users.add(self.local_user) - users = self.test_stream.stream_users(status) + users = self.test_stream.get_audience(status) self.assertEqual(users, []) status = models.Comment.objects.create( @@ -76,22 +76,22 @@ class Activitystreams(TestCase): book=self.book, ) status.mention_users.add(self.local_user) - users = self.test_stream.stream_users(status) + users = self.test_stream.get_audience(status) self.assertTrue(self.local_user in users) self.assertFalse(self.another_user in users) self.assertFalse(self.remote_user in users) - def test_abstractstream_stream_users_followers_remote_user(self, *_): + def test_abstractstream_get_audience_followers_remote_user(self, *_): """ get a list of users that should see a status """ status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="followers", ) - users = self.test_stream.stream_users(status) + users = self.test_stream.get_audience(status) self.assertFalse(users.exists()) - def test_abstractstream_stream_users_followers_self(self, *_): + def test_abstractstream_get_audience_followers_self(self, *_): """ get a list of users that should see a status """ status = models.Comment.objects.create( user=self.local_user, @@ -99,12 +99,12 @@ class Activitystreams(TestCase): privacy="direct", book=self.book, ) - users = self.test_stream.stream_users(status) + users = self.test_stream.get_audience(status) self.assertTrue(self.local_user in users) self.assertFalse(self.another_user in users) self.assertFalse(self.remote_user in users) - def test_abstractstream_stream_users_followers_with_mention(self, *_): + def test_abstractstream_get_audience_followers_with_mention(self, *_): """ get a list of users that should see a status """ status = models.Comment.objects.create( user=self.remote_user, @@ -114,12 +114,12 @@ class Activitystreams(TestCase): ) status.mention_users.add(self.local_user) - users = self.test_stream.stream_users(status) + users = self.test_stream.get_audience(status) self.assertTrue(self.local_user in users) self.assertFalse(self.another_user in users) self.assertFalse(self.remote_user in users) - def test_abstractstream_stream_users_followers_with_relationship(self, *_): + def test_abstractstream_get_audience_followers_with_relationship(self, *_): """ get a list of users that should see a status """ self.remote_user.followers.add(self.local_user) status = models.Comment.objects.create( @@ -128,77 +128,77 @@ class Activitystreams(TestCase): privacy="direct", book=self.book, ) - users = self.test_stream.stream_users(status) + users = self.test_stream.get_audience(status) self.assertFalse(self.local_user in users) self.assertFalse(self.another_user in users) self.assertFalse(self.remote_user in users) - def test_homestream_stream_users(self, *_): + def test_homestream_get_audience(self, *_): """ get a list of users that should see a status """ status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) - users = activitystreams.HomeStream().stream_users(status) + users = activitystreams.HomeStream().get_audience(status) self.assertFalse(users.exists()) - def test_homestream_stream_users_with_mentions(self, *_): + def test_homestream_get_audience_with_mentions(self, *_): """ get a list of users that should see a status """ status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) status.mention_users.add(self.local_user) - users = activitystreams.HomeStream().stream_users(status) + users = activitystreams.HomeStream().get_audience(status) self.assertFalse(self.local_user in users) self.assertFalse(self.another_user in users) - def test_homestream_stream_users_with_relationship(self, *_): + def test_homestream_get_audience_with_relationship(self, *_): """ get a list of users that should see a status """ self.remote_user.followers.add(self.local_user) status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) - users = activitystreams.HomeStream().stream_users(status) + users = activitystreams.HomeStream().get_audience(status) self.assertTrue(self.local_user in users) self.assertFalse(self.another_user in users) - def test_localstream_stream_users_remote_status(self, *_): + def test_localstream_get_audience_remote_status(self, *_): """ get a list of users that should see a status """ status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) - users = activitystreams.LocalStream().stream_users(status) + users = activitystreams.LocalStream().get_audience(status) self.assertEqual(users, []) - def test_localstream_stream_users_local_status(self, *_): + def test_localstream_get_audience_local_status(self, *_): """ get a list of users that should see a status """ status = models.Status.objects.create( user=self.local_user, content="hi", privacy="public" ) - users = activitystreams.LocalStream().stream_users(status) + users = activitystreams.LocalStream().get_audience(status) self.assertTrue(self.local_user in users) self.assertTrue(self.another_user in users) - def test_localstream_stream_users_unlisted(self, *_): + def test_localstream_get_audience_unlisted(self, *_): """ get a list of users that should see a status """ status = models.Status.objects.create( user=self.local_user, content="hi", privacy="unlisted" ) - users = activitystreams.LocalStream().stream_users(status) + users = activitystreams.LocalStream().get_audience(status) self.assertEqual(users, []) - def test_federatedstream_stream_users(self, *_): + def test_federatedstream_get_audience(self, *_): """ get a list of users that should see a status """ status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) - users = activitystreams.FederatedStream().stream_users(status) + users = activitystreams.FederatedStream().get_audience(status) self.assertTrue(self.local_user in users) self.assertTrue(self.another_user in users) - def test_federatedstream_stream_users_unlisted(self, *_): + def test_federatedstream_get_audience_unlisted(self, *_): """ get a list of users that should see a status """ status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="unlisted" ) - users = activitystreams.FederatedStream().stream_users(status) + users = activitystreams.FederatedStream().get_audience(status) self.assertEqual(users, []) From 1901f7e6cb2f1111ba1a8754adcde0c8636c5c36 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Apr 2021 15:16:21 -0700 Subject: [PATCH 012/288] Check if incoming domains are blocked --- bookwyrm/models/federated_server.py | 19 ++++++++++--- bookwyrm/views/inbox.py | 44 +++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/bookwyrm/models/federated_server.py b/bookwyrm/models/federated_server.py index 8f7d903e4..d7dc5d960 100644 --- a/bookwyrm/models/federated_server.py +++ b/bookwyrm/models/federated_server.py @@ -2,16 +2,27 @@ from django.db import models from .base_model import BookWyrmModel +FederationStatus = models.TextChoices( + "Status", + [ + "federated", + "blocked", + ], +) + class FederatedServer(BookWyrmModel): """ store which servers we federate with """ server_name = models.CharField(max_length=255, unique=True) - # federated, blocked, whatever else - status = models.CharField(max_length=255, default="federated") + status = models.CharField( + max_length=255, default="federated", choices=FederationStatus.choices + ) # is it mastodon, bookwyrm, etc application_type = models.CharField(max_length=255, null=True) application_version = models.CharField(max_length=255, null=True) - -# TODO: blocked servers + def block(self): + """ block a server """ + self.status = "blocked" + self.save() diff --git a/bookwyrm/views/inbox.py b/bookwyrm/views/inbox.py index 34bd2e1cc..a4ae1e998 100644 --- a/bookwyrm/views/inbox.py +++ b/bookwyrm/views/inbox.py @@ -1,9 +1,10 @@ """ incoming activities """ import json -from urllib.parse import urldefrag +import re +from urllib.parse import urldefrag, urlparse -from django.http import HttpResponse -from django.http import HttpResponseBadRequest, HttpResponseNotFound +from django.http import HttpResponse, HttpResponseNotFound +from django.http import HttpResponseBadRequest, HttpResponseForbidden from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.csrf import csrf_exempt @@ -12,6 +13,7 @@ import requests from bookwyrm import activitypub, models from bookwyrm.tasks import app from bookwyrm.signatures import Signature +from bookwyrm.utils import regex @method_decorator(csrf_exempt, name="dispatch") @@ -21,6 +23,10 @@ class Inbox(View): def post(self, request, username=None): """ only works as POST request """ + # first check if this server is on our shitlist + if is_blocked_user_agent(request): + return HttpResponseForbidden() + # make sure the user's inbox even exists if username: try: @@ -34,6 +40,10 @@ class Inbox(View): except json.decoder.JSONDecodeError: return HttpResponseBadRequest() + # let's be extra sure we didn't block this domain + if is_blocked_activity(activity_json): + return HttpResponseForbidden() + if ( not "object" in activity_json or not "type" in activity_json @@ -54,6 +64,34 @@ class Inbox(View): return HttpResponse() +def is_blocked_user_agent(request): + """ check if a request is from a blocked server based on user agent """ + # check user agent + user_agent = request.headers.get("User-Agent") + domain = re.match(regex.domain, user_agent) + if not domain: + # idk, we'll try again later with the actor + return False + return is_blocked(domain) + + +def is_blocked_activity(activity_json): + """ get the sender out of activity json and check if it's blocked """ + actor = activity_json.get("actor") + if not actor: + # well I guess it's not even a valid activity so who knows + return False + url = urlparse(actor) + return is_blocked(url.netloc) + + +def is_blocked(domain): + """ is this domain blocked? """ + return models.FederatedServer.object.filter( + server_name=domain, status="blocked" + ).exists() + + @app.task def activity_task(activity_json): """ do something with this json we think is legit """ From e4fe47b53865fde8c404a78674fb8f2499b602d4 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Apr 2021 15:26:53 -0700 Subject: [PATCH 013/288] Default blocks in initdb --- bookwyrm/management/commands/initdb.py | 13 ++++++++++++- bookwyrm/templates/settings/federated_server.html | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/bookwyrm/management/commands/initdb.py b/bookwyrm/management/commands/initdb.py index d6101c877..a86a1652e 100644 --- a/bookwyrm/management/commands/initdb.py +++ b/bookwyrm/management/commands/initdb.py @@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand, CommandError from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType -from bookwyrm.models import Connector, SiteSettings, User +from bookwyrm.models import Connector, FederatedServer, SiteSettings, User from bookwyrm.settings import DOMAIN @@ -107,6 +107,16 @@ def init_connectors(): ) +def init_federated_servers(): + """ big no to nazis """ + built_in_blocks = ["gab.ai", "gab.com"] + for server in built_in_blocks: + FederatedServer.objects.create( + server_name=server, + status="blocked", + ) + + def init_settings(): SiteSettings.objects.create() @@ -118,4 +128,5 @@ class Command(BaseCommand): init_groups() init_permissions() init_connectors() + init_federated_servers() init_settings() diff --git a/bookwyrm/templates/settings/federated_server.html b/bookwyrm/templates/settings/federated_server.html index 13715bfb2..edab31397 100644 --- a/bookwyrm/templates/settings/federated_server.html +++ b/bookwyrm/templates/settings/federated_server.html @@ -21,7 +21,7 @@
{% trans "Status:" %}
-
Federated
+
{{ server.status }}
From ad543f46c1728a4ec573462eeab4646b88f7a34d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Apr 2021 15:38:32 -0700 Subject: [PATCH 014/288] Adds block and unblock functionality --- .../templates/settings/federated_server.html | 16 ++++++++++++++++ bookwyrm/views/federation.py | 9 ++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templates/settings/federated_server.html b/bookwyrm/templates/settings/federated_server.html index edab31397..147d6514e 100644 --- a/bookwyrm/templates/settings/federated_server.html +++ b/bookwyrm/templates/settings/federated_server.html @@ -4,6 +4,10 @@ {% block header %} {{ server.server_name }} + +{% if server.status == "blocked" %}{% trans "Blocked" %} +{% endif %} + {% trans "Back to server list" %} {% endblock %} @@ -64,5 +68,17 @@ +
+

{% trans "Actions" %}

+
+ {% csrf_token %} + {% if server.status != 'blocked' %} + + {% else %} + + {% endif %} +
+
+ {% endblock %} diff --git a/bookwyrm/views/federation.py b/bookwyrm/views/federation.py index 464a207ca..0405ebf53 100644 --- a/bookwyrm/views/federation.py +++ b/bookwyrm/views/federation.py @@ -1,7 +1,7 @@ """ manage federated servers """ from django.contrib.auth.decorators import login_required, permission_required from django.core.paginator import Paginator -from django.shortcuts import get_object_or_404 +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 @@ -61,3 +61,10 @@ class FederatedServer(View): ), } return TemplateResponse(request, "settings/federated_server.html", data) + + def post(self, request, server): # pylint: disable=unused-argument + """ (un)block a server """ + server = get_object_or_404(models.FederatedServer, id=server) + server.status = "blocked" if server.status == "federated" else "federated" + server.save() + return redirect("settings-federated-server", server.id) From 194fcb8055b229843b2449f8dd8cc649e7ec45d7 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Apr 2021 15:54:20 -0700 Subject: [PATCH 015/288] Adds migration for federated server table statuses --- .../migrations/0062_auto_20210405_2249.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 bookwyrm/migrations/0062_auto_20210405_2249.py diff --git a/bookwyrm/migrations/0062_auto_20210405_2249.py b/bookwyrm/migrations/0062_auto_20210405_2249.py new file mode 100644 index 000000000..8229bf7ca --- /dev/null +++ b/bookwyrm/migrations/0062_auto_20210405_2249.py @@ -0,0 +1,22 @@ +# Generated by Django 3.1.6 on 2021-04-05 22:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0061_auto_20210402_1435"), + ] + + operations = [ + migrations.AlterField( + model_name="federatedserver", + name="status", + field=models.CharField( + choices=[("federated", "Federated"), ("blocked", "Blocked")], + default="federated", + max_length=255, + ), + ), + ] From 34b790a0864cf153100a8f6388cc5d9598bde4f2 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Apr 2021 15:54:33 -0700 Subject: [PATCH 016/288] Adds tests for blocked server checks --- bookwyrm/tests/views/test_inbox.py | 25 +++++++++++++++++++++++++ bookwyrm/views/inbox.py | 2 ++ 2 files changed, 27 insertions(+) diff --git a/bookwyrm/tests/views/test_inbox.py b/bookwyrm/tests/views/test_inbox.py index 37cf00ddc..889fdc329 100644 --- a/bookwyrm/tests/views/test_inbox.py +++ b/bookwyrm/tests/views/test_inbox.py @@ -6,6 +6,7 @@ from unittest.mock import patch from django.http import HttpResponseNotAllowed, HttpResponseNotFound from django.test import TestCase, Client +from django.test.client import RequestFactory import responses from bookwyrm import models, views @@ -18,6 +19,7 @@ class Inbox(TestCase): def setUp(self): """ basic user and book data """ self.client = Client() + self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@example.com", "mouse@mouse.com", @@ -936,3 +938,26 @@ class Inbox(TestCase): views.inbox.activity_task(activity) self.assertTrue(redis_mock.called) self.assertFalse(models.UserBlocks.objects.exists()) + + def test_is_blocked_user_agent(self): + """ check for blocked servers """ + request = self.factory.post( + "", + HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)", + ) + self.assertFalse(views.inbox.is_blocked_user_agent(request)) + + models.FederatedServer.objects.create( + server_name="mastodon.social", status="blocked" + ) + self.assertTrue(views.inbox.is_blocked_user_agent(request)) + + def test_is_blocked_activity(self): + """ check for blocked servers """ + activity = {"actor": "https://mastodon.social/user/whaatever/else"} + self.assertFalse(views.inbox.is_blocked_user_agent(activity)) + + models.FederatedServer.objects.create( + server_name="mastodon.social", status="blocked" + ) + self.assertTrue(views.inbox.is_blocked_user_agent(activity)) diff --git a/bookwyrm/views/inbox.py b/bookwyrm/views/inbox.py index a4ae1e998..9e5aa0195 100644 --- a/bookwyrm/views/inbox.py +++ b/bookwyrm/views/inbox.py @@ -68,6 +68,8 @@ def is_blocked_user_agent(request): """ check if a request is from a blocked server based on user agent """ # check user agent user_agent = request.headers.get("User-Agent") + if not user_agent: + return False domain = re.match(regex.domain, user_agent) if not domain: # idk, we'll try again later with the actor From a4b892dfadfe915c0dc2185682fe652e1a7661f2 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Apr 2021 16:47:48 -0700 Subject: [PATCH 017/288] Fixes domain block tests --- bookwyrm/tests/views/test_federation.py | 16 ++++++++++++++++ bookwyrm/tests/views/test_inbox.py | 4 ++-- bookwyrm/views/inbox.py | 5 +++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/bookwyrm/tests/views/test_federation.py b/bookwyrm/tests/views/test_federation.py index a60ea4327..874e277c9 100644 --- a/bookwyrm/tests/views/test_federation.py +++ b/bookwyrm/tests/views/test_federation.py @@ -44,3 +44,19 @@ class FederationViews(TestCase): self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) + + def test_server_page_post(self): + """ block and unblock a server """ + server = models.FederatedServer.objects.create(server_name="hi.there.com") + self.assertEqual(server.status, "federated") + + view = views.FederatedServer.as_view() + request = self.factory.post("") + request.user = self.local_user + request.user.is_superuser = True + + view(request, server.id) + self.assertEqual(server.status, "blocked") + + view(request, server.id) + self.assertEqual(server.status, "federated") diff --git a/bookwyrm/tests/views/test_inbox.py b/bookwyrm/tests/views/test_inbox.py index 889fdc329..f15b99f1a 100644 --- a/bookwyrm/tests/views/test_inbox.py +++ b/bookwyrm/tests/views/test_inbox.py @@ -955,9 +955,9 @@ class Inbox(TestCase): def test_is_blocked_activity(self): """ check for blocked servers """ activity = {"actor": "https://mastodon.social/user/whaatever/else"} - self.assertFalse(views.inbox.is_blocked_user_agent(activity)) + self.assertFalse(views.inbox.is_blocked_activity(activity)) models.FederatedServer.objects.create( server_name="mastodon.social", status="blocked" ) - self.assertTrue(views.inbox.is_blocked_user_agent(activity)) + self.assertTrue(views.inbox.is_blocked_activity(activity)) diff --git a/bookwyrm/views/inbox.py b/bookwyrm/views/inbox.py index 9e5aa0195..387efe9f0 100644 --- a/bookwyrm/views/inbox.py +++ b/bookwyrm/views/inbox.py @@ -70,7 +70,8 @@ def is_blocked_user_agent(request): user_agent = request.headers.get("User-Agent") if not user_agent: return False - domain = re.match(regex.domain, user_agent) + url = re.search(r"+https?://{:s}/?".format(regex.domain), user_agent) + domain = urlparse(url).netloc if not domain: # idk, we'll try again later with the actor return False @@ -89,7 +90,7 @@ def is_blocked_activity(activity_json): def is_blocked(domain): """ is this domain blocked? """ - return models.FederatedServer.object.filter( + return models.FederatedServer.objects.filter( server_name=domain, status="blocked" ).exists() From fb72db7507459829956b3ade89144912246980ec Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Apr 2021 16:50:36 -0700 Subject: [PATCH 018/288] Fixes federation view tests --- bookwyrm/tests/views/test_federation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bookwyrm/tests/views/test_federation.py b/bookwyrm/tests/views/test_federation.py index 874e277c9..3783417c1 100644 --- a/bookwyrm/tests/views/test_federation.py +++ b/bookwyrm/tests/views/test_federation.py @@ -56,7 +56,9 @@ class FederationViews(TestCase): request.user.is_superuser = True view(request, server.id) + server.refresh_from_db() self.assertEqual(server.status, "blocked") view(request, server.id) + server.refresh_from_db() self.assertEqual(server.status, "federated") From 2e245f84be846e967a1488aeb7f8b4ffe66408e0 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Apr 2021 18:02:24 -0700 Subject: [PATCH 019/288] Adds test for loading remote boosted statuses --- bookwyrm/tests/views/test_inbox.py | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/bookwyrm/tests/views/test_inbox.py b/bookwyrm/tests/views/test_inbox.py index 37cf00ddc..6973f9814 100644 --- a/bookwyrm/tests/views/test_inbox.py +++ b/bookwyrm/tests/views/test_inbox.py @@ -572,6 +572,56 @@ class Inbox(TestCase): self.assertEqual(notification.user, self.local_user) self.assertEqual(notification.related_status, self.status) + @responses.activate + @patch("bookwyrm.activitystreams.ActivityStream.add_status") + def test_handle_boost_remote_status(self, redis_mock): + """ boost a status """ + work = models.Work.objects.create(title="work title") + book = models.Edition.objects.create( + title="Test", + remote_id="https://bookwyrm.social/book/37292", + parent_work=work, + ) + self.assertEqual(models.Notification.objects.count(), 0) + activity = { + "type": "Announce", + "id": "%s/boost" % self.status.remote_id, + "actor": self.remote_user.remote_id, + "object": "https://remote.com/status/1", + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "published": "Mon, 25 May 2020 19:31:20 GMT", + } + responses.add( + responses.GET, + "https://remote.com/status/1", + json={ + "id": "https://remote.com/status/1", + "type": "Comment", + "published": "2021-04-05T18:04:59.735190+00:00", + "attributedTo": self.remote_user.remote_id, + "content": "

a comment

", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://b875df3d118b.ngrok.io/user/mouse/followers"], + "inReplyTo": "", + "inReplyToBook": book.remote_id, + "summary": "", + "tag": [], + "sensitive": False, + "@context": "https://www.w3.org/ns/activitystreams" + } + ) + + with patch("bookwyrm.models.status.Status.ignore_activity") as discarder: + discarder.return_value = False + views.inbox.activity_task(activity) + self.assertTrue(redis_mock.called) + + boost = models.Boost.objects.get() + self.assertEqual(boost.boosted_status.remote_id, "https://remote.com/status/1") + self.assertEqual(boost.boosted_status.comment.status_type, "Comment") + self.assertEqual(boost.boosted_status.comment.book, book) + @responses.activate def test_handle_discarded_boost(self): """ test a boost of a mastodon status that will be discarded """ From a39cd670eff52d27a4145cb134a63d9726d7405d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Apr 2021 18:05:06 -0700 Subject: [PATCH 020/288] Fixes boosted remote statuses coming in as Notes --- bookwyrm/activitypub/base_activity.py | 3 ++- bookwyrm/tests/views/test_inbox.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 768eb2084..452f61e03 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -265,7 +265,8 @@ def resolve_remote_id( "Could not connect to host for remote_id in: %s" % (remote_id) ) # determine the model implicitly, if not provided - if not model: + # or if it's a model with subclasses like Status, check again + if not model or hasattr(model.objects, "select_subclasses"): model = get_model_from_type(data.get("type")) # check for existing items with shared unique identifiers diff --git a/bookwyrm/tests/views/test_inbox.py b/bookwyrm/tests/views/test_inbox.py index 6973f9814..f44a79c69 100644 --- a/bookwyrm/tests/views/test_inbox.py +++ b/bookwyrm/tests/views/test_inbox.py @@ -608,8 +608,8 @@ class Inbox(TestCase): "summary": "", "tag": [], "sensitive": False, - "@context": "https://www.w3.org/ns/activitystreams" - } + "@context": "https://www.w3.org/ns/activitystreams", + }, ) with patch("bookwyrm.models.status.Status.ignore_activity") as discarder: From 6b0a3ce4b1833d82b6529563567be6be7a6d18cd Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Tue, 6 Apr 2021 09:06:12 +0200 Subject: [PATCH 021/288] [assets] Move localStorage chunks of code to their own file: This should prevent a sync issue with updateDisplay not always being loaded on time. --- bookwyrm/static/js/bookwyrm.js | 10 +--------- bookwyrm/static/js/localstorage.js | 8 ++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 5ca3d7d7a..a4149c6f4 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -1,4 +1,4 @@ -/* globals setDisplay TabGroup updateDisplay */ +/* globals TabGroup */ // set up javascript listeners window.onload = function() { @@ -28,14 +28,6 @@ window.onload = function() { Array.from(document.getElementsByClassName('tab-group')) .forEach(t => new TabGroup(t)); - - // display based on localstorage vars - document.querySelectorAll('[data-hide]') - .forEach(t => setDisplay(t)); - - // update localstorage - Array.from(document.getElementsByClassName('set-display')) - .forEach(t => t.onclick = updateDisplay); }; function back(e) { diff --git a/bookwyrm/static/js/localstorage.js b/bookwyrm/static/js/localstorage.js index aa79ee303..5104b44af 100644 --- a/bookwyrm/static/js/localstorage.js +++ b/bookwyrm/static/js/localstorage.js @@ -18,3 +18,11 @@ function setDisplay(el) { var value = window.localStorage.getItem(key); addRemoveClass(el, 'hidden', value); } + +// display based on localstorage vars +document.querySelectorAll('[data-hide]') + .forEach(t => setDisplay(t)); + +// update localstorage +Array.from(document.getElementsByClassName('set-display')) + .forEach(t => t.onclick = updateDisplay); From f6c3e581b95c2f1b3acbf62b9595fc9b9def1618 Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Tue, 6 Apr 2021 09:11:27 +0200 Subject: [PATCH 022/288] =?UTF-8?q?[assets]=20Replace=20`Array.from(docume?= =?UTF-8?q?nt.getElementsByClassName(=E2=80=A6))`=20by=20`querySelectorAll?= =?UTF-8?q?(=E2=80=A6)`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bookwyrm/static/js/bookwyrm.js | 10 ++++++---- bookwyrm/static/js/localstorage.js | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index a4149c6f4..7d542747c 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -7,11 +7,11 @@ window.onload = function() { .forEach(t => t.onclick = toggleAction); // javascript interactions (boost/fav) - Array.from(document.getElementsByClassName('interaction')) + document.querySelectorAll('.interaction') .forEach(t => t.onsubmit = interact); // handle aria settings on menus - Array.from(document.getElementsByClassName('pulldown-menu')) + document.querySelectorAll('.pulldown-menu') .forEach(t => t.onclick = toggleMenu); // hidden submit button in a form @@ -26,7 +26,7 @@ window.onload = function() { document.querySelectorAll('[data-back]') .forEach(t => t.onclick = back); - Array.from(document.getElementsByClassName('tab-group')) + document.querySelectorAll('.tab-group') .forEach(t => new TabGroup(t)); }; @@ -103,7 +103,9 @@ function interact(e) { e.preventDefault(); ajaxPost(e.target); var identifier = e.target.getAttribute('data-id'); - Array.from(document.getElementsByClassName(identifier)) + // @todo This should be `querySelector`, unless there are duplicated IDs, + // which is a problem in itself. + document.querySelectorAll(`#${identifier}`) .forEach(t => addRemoveClass(t, 'hidden', t.className.indexOf('hidden') == -1)); } diff --git a/bookwyrm/static/js/localstorage.js b/bookwyrm/static/js/localstorage.js index 5104b44af..514d8748e 100644 --- a/bookwyrm/static/js/localstorage.js +++ b/bookwyrm/static/js/localstorage.js @@ -24,5 +24,5 @@ document.querySelectorAll('[data-hide]') .forEach(t => setDisplay(t)); // update localstorage -Array.from(document.getElementsByClassName('set-display')) +document.querySelectorAll('.set-display') .forEach(t => t.onclick = updateDisplay); From f430363be0c55a8275d3e91e098c75b3f2cb94d9 Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Tue, 6 Apr 2021 09:11:56 +0200 Subject: [PATCH 023/288] [assets] Simplify a function expression. --- bookwyrm/static/js/check_all.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/static/js/check_all.js b/bookwyrm/static/js/check_all.js index 72c83e3e9..de067f780 100644 --- a/bookwyrm/static/js/check_all.js +++ b/bookwyrm/static/js/check_all.js @@ -13,7 +13,7 @@ document .querySelectorAll(`#${mainCheckbox.dataset.target} [type="checkbox"]`) - .forEach(checkbox => {checkbox.checked = mainCheckbox.checked;}); + .forEach(checkbox => checkbox.checked = mainCheckbox.checked); } document From 2f2f7db0865972401ea6a092bb82c3032007d19f Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Tue, 6 Apr 2021 09:44:59 +0200 Subject: [PATCH 024/288] [assets] Use classes for JS files: - Classes strictly parse code implicitly. - Fix previously updated code. --- bookwyrm/static/js/bookwyrm.js | 304 +++++++++++++++-------------- bookwyrm/static/js/localstorage.js | 54 ++--- 2 files changed, 187 insertions(+), 171 deletions(-) diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 7d542747c..0ac33e0f1 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -1,156 +1,168 @@ +/* exported BookWyrm */ /* globals TabGroup */ -// set up javascript listeners -window.onload = function() { - // buttons that display or hide content - document.querySelectorAll('[data-controls]') - .forEach(t => t.onclick = toggleAction); - - // javascript interactions (boost/fav) - document.querySelectorAll('.interaction') - .forEach(t => t.onsubmit = interact); - - // handle aria settings on menus - document.querySelectorAll('.pulldown-menu') - .forEach(t => t.onclick = toggleMenu); - - // hidden submit button in a form - document.querySelectorAll('.hidden-form input') - .forEach(t => t.onchange = revealForm); - - // polling - document.querySelectorAll('[data-poll]') - .forEach(el => polling(el)); - - // browser back behavior - document.querySelectorAll('[data-back]') - .forEach(t => t.onclick = back); - - document.querySelectorAll('.tab-group') - .forEach(t => new TabGroup(t)); -}; - -function back(e) { - e.preventDefault(); - history.back(); -} - -function polling(el, delay) { - delay = delay || 10000; - delay += (Math.random() * 1000); - setTimeout(function() { - fetch('/api/updates/' + el.getAttribute('data-poll')) - .then(response => response.json()) - .then(data => updateCountElement(el, data)); - polling(el, delay * 1.25); - }, delay, el); -} - -function updateCountElement(el, data) { - const currentCount = el.innerText; - const count = data.count; - if (count != currentCount) { - addRemoveClass(el.closest('[data-poll-wrapper]'), 'hidden', count < 1); - el.innerText = count; - } -} - - -function revealForm(e) { - var hidden = e.currentTarget.closest('.hidden-form').getElementsByClassName('hidden')[0]; - if (hidden) { - removeClass(hidden, 'hidden'); - } -} - - -function toggleAction(e) { - var el = e.currentTarget; - var pressed = el.getAttribute('aria-pressed') == 'false'; - - var targetId = el.getAttribute('data-controls'); - document.querySelectorAll('[data-controls="' + targetId + '"]') - .forEach(t => t.setAttribute('aria-pressed', (t.getAttribute('aria-pressed') == 'false'))); - - if (targetId) { - var target = document.getElementById(targetId); - addRemoveClass(target, 'hidden', !pressed); - addRemoveClass(target, 'is-active', pressed); +let BookWyrm = new class { + constructor() { + this.initOnLoad(); + this.initEventListeners(); } - // show/hide container - var container = document.getElementById('hide-' + targetId); - if (container) { - addRemoveClass(container, 'hidden', pressed); + initEventListeners() { + // buttons that display or hide content + document.querySelectorAll('[data-controls]') + .forEach(t => t.onclick = this.toggleAction.bind(this)); + + // javascript interactions (boost/fav) + document.querySelectorAll('.interaction') + .forEach(t => t.onsubmit = this.interact.bind(this)); + + // handle aria settings on menus + document.querySelectorAll('.pulldown-menu') + .forEach(t => t.onclick = this.toggleMenu.bind(this)); + + // hidden submit button in a form + document.querySelectorAll('.hidden-form input') + .forEach(t => t.onchange = this.revealForm.bind(this)); + + // polling + document.querySelectorAll('[data-poll]') + .forEach(el => this.polling(el)); + + // browser back behavior + document.querySelectorAll('[data-back]') + .forEach(t => t.onclick = this.back); } - // set checkbox, if appropriate - var checkbox = el.getAttribute('data-controls-checkbox'); - if (checkbox) { - document.getElementById(checkbox).checked = !!pressed; + initOnLoad(){ + // set up javascript listeners + window.onload = function() { + document.querySelectorAll('.tab-group') + .forEach(t => new TabGroup(t)); + }; } - // set focus, if appropriate - var focus = el.getAttribute('data-focus-target'); - if (focus) { - var focusEl = document.getElementById(focus); - focusEl.focus(); - setTimeout(function(){ focusEl.selectionStart = focusEl.selectionEnd = 10000; }, 0); + back(e) { + e.preventDefault(); + history.back(); + } + + polling(el, delay) { + let poller = this; + delay = delay || 10000; + delay += (Math.random() * 1000); + setTimeout(function() { + fetch('/api/updates/' + el.getAttribute('data-poll')) + .then(response => response.json()) + .then(data => poller.updateCountElement(el, data)); + poller.polling(el, delay * 1.25); + }, delay, el); + } + + updateCountElement(el, data) { + const currentCount = el.innerText; + const count = data.count; + if (count != currentCount) { + this.addRemoveClass(el.closest('[data-poll-wrapper]'), 'hidden', count < 1); + el.innerText = count; + } + } + + revealForm(e) { + var hidden = e.currentTarget.closest('.hidden-form').getElementsByClassName('hidden')[0]; + if (hidden) { + this.removeClass(hidden, 'hidden'); + } + } + + toggleAction(e) { + var el = e.currentTarget; + var pressed = el.getAttribute('aria-pressed') == 'false'; + + var targetId = el.getAttribute('data-controls'); + document.querySelectorAll('[data-controls="' + targetId + '"]') + .forEach(t => t.setAttribute('aria-pressed', (t.getAttribute('aria-pressed') == 'false'))); + + if (targetId) { + var target = document.getElementById(targetId); + this.addRemoveClass(target, 'hidden', !pressed); + this.addRemoveClass(target, 'is-active', pressed); + } + + // show/hide container + var container = document.getElementById('hide-' + targetId); + if (container) { + this.addRemoveClass(container, 'hidden', pressed); + } + + // set checkbox, if appropriate + var checkbox = el.getAttribute('data-controls-checkbox'); + if (checkbox) { + document.getElementById(checkbox).checked = !!pressed; + } + + // set focus, if appropriate + var focus = el.getAttribute('data-focus-target'); + if (focus) { + var focusEl = document.getElementById(focus); + focusEl.focus(); + setTimeout(function(){ focusEl.selectionStart = focusEl.selectionEnd = 10000; }, 0); + } + } + + // @todo Only update status if the promise is successful. + interact(e) { + e.preventDefault(); + this.ajaxPost(e.target); + var identifier = e.target.getAttribute('data-id'); + + // @todo This probably should be done with IDs. + document.querySelectorAll(`.${identifier}`) + .forEach(t => this.addRemoveClass(t, 'hidden', t.className.indexOf('hidden') == -1)); + } + + toggleMenu(e) { + var el = e.currentTarget; + var expanded = el.getAttribute('aria-expanded') == 'false'; + el.setAttribute('aria-expanded', expanded); + var targetId = el.getAttribute('data-controls'); + if (targetId) { + var target = document.getElementById(targetId); + this.addRemoveClass(target, 'is-active', expanded); + } + } + + ajaxPost(form) { + fetch(form.action, { + method : "POST", + body: new FormData(form) + }); + } + + addRemoveClass(el, classname, bool) { + if (bool) { + this.addClass(el, classname); + } else { + this.removeClass(el, classname); + } + } + + addClass(el, classname) { + var classes = el.className.split(' '); + if (classes.indexOf(classname) > -1) { + return; + } + el.className = classes.concat(classname).join(' '); + } + + removeClass(el, className) { + var classes = []; + if (el.className) { + classes = el.className.split(' '); + } + const idx = classes.indexOf(className); + if (idx > -1) { + classes.splice(idx, 1); + } + el.className = classes.join(' '); } } - -function interact(e) { - e.preventDefault(); - ajaxPost(e.target); - var identifier = e.target.getAttribute('data-id'); - // @todo This should be `querySelector`, unless there are duplicated IDs, - // which is a problem in itself. - document.querySelectorAll(`#${identifier}`) - .forEach(t => addRemoveClass(t, 'hidden', t.className.indexOf('hidden') == -1)); -} - -function toggleMenu(e) { - var el = e.currentTarget; - var expanded = el.getAttribute('aria-expanded') == 'false'; - el.setAttribute('aria-expanded', expanded); - var targetId = el.getAttribute('data-controls'); - if (targetId) { - var target = document.getElementById(targetId); - addRemoveClass(target, 'is-active', expanded); - } -} - -function ajaxPost(form) { - fetch(form.action, { - method : "POST", - body: new FormData(form) - }); -} - -function addRemoveClass(el, classname, bool) { - if (bool) { - addClass(el, classname); - } else { - removeClass(el, classname); - } -} - -function addClass(el, classname) { - var classes = el.className.split(' '); - if (classes.indexOf(classname) > -1) { - return; - } - el.className = classes.concat(classname).join(' '); -} - -function removeClass(el, className) { - var classes = []; - if (el.className) { - classes = el.className.split(' '); - } - const idx = classes.indexOf(className); - if (idx > -1) { - classes.splice(idx, 1); - } - el.className = classes.join(' '); -} diff --git a/bookwyrm/static/js/localstorage.js b/bookwyrm/static/js/localstorage.js index 514d8748e..b8ef02cd8 100644 --- a/bookwyrm/static/js/localstorage.js +++ b/bookwyrm/static/js/localstorage.js @@ -1,28 +1,32 @@ -/* exported updateDisplay */ -/* globals addRemoveClass */ +/* exported LocalStorageTools */ +/* globals BookWyrm */ -// set javascript listeners -function updateDisplay(e) { - // used in set reading goal - var key = e.target.getAttribute('data-id'); - var value = e.target.getAttribute('data-value'); - window.localStorage.setItem(key, value); +let LocalStorageTools = new class { + constructor() { + // display based on localstorage vars + document.querySelectorAll('[data-hide]') + .forEach(t => this.setDisplay(t)); - document.querySelectorAll('[data-hide="' + key + '"]') - .forEach(t => setDisplay(t)); + // update localstorage + document.querySelectorAll('.set-display') + .forEach(t => t.onclick = this.updateDisplay.bind(this)); + } + + // set javascript listeners + updateDisplay(e) { + // used in set reading goal + var key = e.target.getAttribute('data-id'); + var value = e.target.getAttribute('data-value'); + window.localStorage.setItem(key, value); + + document.querySelectorAll('[data-hide="' + key + '"]') + .forEach(t => this.setDisplay(t)); + } + + setDisplay(el) { + // used in set reading goal + var key = el.getAttribute('data-hide'); + var value = window.localStorage.getItem(key); + BookWyrm.addRemoveClass(el, 'hidden', value); + } } - -function setDisplay(el) { - // used in set reading goal - var key = el.getAttribute('data-hide'); - var value = window.localStorage.getItem(key); - addRemoveClass(el, 'hidden', value); -} - -// display based on localstorage vars -document.querySelectorAll('[data-hide]') - .forEach(t => setDisplay(t)); - -// update localstorage -document.querySelectorAll('.set-display') - .forEach(t => t.onclick = updateDisplay); From 991d897ac75ddbd0122d61d175ba0f34380cf53a Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Tue, 6 Apr 2021 09:57:52 +0200 Subject: [PATCH 025/288] [assets] Listen to events as soon as possible. --- bookwyrm/static/js/bookwyrm.js | 38 ++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 0ac33e0f1..ceab51767 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -3,42 +3,50 @@ let BookWyrm = new class { constructor() { - this.initOnLoad(); + this.initOnDOMLoaded(); + this.initReccuringTasks(); this.initEventListeners(); } initEventListeners() { // buttons that display or hide content document.querySelectorAll('[data-controls]') - .forEach(t => t.onclick = this.toggleAction.bind(this)); + .forEach(button => button.onclick = this.toggleAction.bind(this)); // javascript interactions (boost/fav) document.querySelectorAll('.interaction') - .forEach(t => t.onsubmit = this.interact.bind(this)); + .forEach(button => button.onsubmit = this.interact.bind(this)); // handle aria settings on menus document.querySelectorAll('.pulldown-menu') - .forEach(t => t.onclick = this.toggleMenu.bind(this)); + .forEach(button => button.onclick = this.toggleMenu.bind(this)); // hidden submit button in a form document.querySelectorAll('.hidden-form input') - .forEach(t => t.onchange = this.revealForm.bind(this)); - - // polling - document.querySelectorAll('[data-poll]') - .forEach(el => this.polling(el)); + .forEach(button => button.onchange = this.revealForm.bind(this)); // browser back behavior document.querySelectorAll('[data-back]') - .forEach(t => t.onclick = this.back); + .forEach(button => button.onclick = this.back); } - initOnLoad(){ - // set up javascript listeners - window.onload = function() { + /** + * Execute code once the DOM is loaded. + */ + initOnDOMLoaded() { + window.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('.tab-group') - .forEach(t => new TabGroup(t)); - }; + .forEach(tabs => new TabGroup(tabs)); + }); + } + + /** + * Execute recurring tasks. + */ + initReccuringTasks() { + // Polling + document.querySelectorAll('[data-poll]') + .forEach(liveArea => this.polling(liveArea)); } back(e) { From 70c652d5657dffbb971c3c13cfe301eeab7303a8 Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Tue, 6 Apr 2021 10:42:52 +0200 Subject: [PATCH 026/288] [assets] Add rules to ESLint: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix long line. - Enforce a few stylistic habits: - Avoid some potential dangerous constructs. - `arrow-spacing`: Use at least one space around arrows. - `keyword-spacing`: Use at least one space around keywords (if, else, for…). - `no-multiple-empty-lines`: Only use one empty line between code. - `no-var`: Use `let` or `const` instead of `var`: - `padded-blocks`: Do not pad blocks. - `padding-line-between-statements`: Use empty lines between some statements. - `space-before-blocks`: Use at least one space before the opening brace of a block. --- .eslintrc.js | 60 +++++++++++++++++++++++++++++- bookwyrm/static/js/bookwyrm.js | 58 ++++++++++++++++++++--------- bookwyrm/static/js/localstorage.js | 10 +++-- 3 files changed, 105 insertions(+), 23 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index b5f3c311f..5a247a43f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,6 +9,64 @@ module.exports = { "extends": "eslint:recommended", "rules": { - "strict": "error" + // Possible Errors + "no-async-promise-executor": "error", + "no-await-in-loop": "error", + "no-class-assign": "error", + "no-confusing-arrow": "error", + "no-const-assign": "error", + "no-dupe-class-members": "error", + "no-duplicate-imports": "error", + "no-template-curly-in-string": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "no-useless-rename": "error", + "require-atomic-updates": "error", + + // Best practices + "strict": "error", + "no-var": "error", + + // Stylistic Issues + "arrow-spacing": "error", + "keyword-spacing": "error", + "no-multiple-empty-lines": [ + "error", + { + "max": 1, + }, + ], + "padded-blocks": [ + "error", + "never", + ], + "padding-line-between-statements": [ + "error", + { + // always before return + "blankLine": "always", + "prev": "*", + "next": "return", + }, + { + // always before block-like expressions + "blankLine": "always", + "prev": "*", + "next": "block-like", + }, + { + // always after variable declaration + "blankLine": "always", + "prev": [ "const", "let", "var" ], + "next": "*", + }, + { + // not necessary between variable declaration + "blankLine": "any", + "prev": [ "const", "let", "var" ], + "next": [ "const", "let", "var" ], + }, + ], + "space-before-blocks": "error", } }; diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index ceab51767..3df49ae5e 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -56,8 +56,10 @@ let BookWyrm = new class { polling(el, delay) { let poller = this; + delay = delay || 10000; delay += (Math.random() * 1000); + setTimeout(function() { fetch('/api/updates/' + el.getAttribute('data-poll')) .then(response => response.json()) @@ -69,6 +71,7 @@ let BookWyrm = new class { updateCountElement(el, data) { const currentCount = el.innerText; const count = data.count; + if (count != currentCount) { this.addRemoveClass(el.closest('[data-poll-wrapper]'), 'hidden', count < 1); el.innerText = count; @@ -76,52 +79,62 @@ let BookWyrm = new class { } revealForm(e) { - var hidden = e.currentTarget.closest('.hidden-form').getElementsByClassName('hidden')[0]; + let hidden = e.currentTarget.closest('.hidden-form').getElementsByClassName('hidden')[0]; + if (hidden) { this.removeClass(hidden, 'hidden'); } } toggleAction(e) { - var el = e.currentTarget; - var pressed = el.getAttribute('aria-pressed') == 'false'; + let el = e.currentTarget; + let pressed = el.getAttribute('aria-pressed') == 'false'; + let targetId = el.getAttribute('data-controls'); - var targetId = el.getAttribute('data-controls'); document.querySelectorAll('[data-controls="' + targetId + '"]') - .forEach(t => t.setAttribute('aria-pressed', (t.getAttribute('aria-pressed') == 'false'))); + .forEach(t => { + t.setAttribute('aria-pressed', (t.getAttribute('aria-pressed') == 'false')) + }); if (targetId) { - var target = document.getElementById(targetId); + let target = document.getElementById(targetId); + this.addRemoveClass(target, 'hidden', !pressed); this.addRemoveClass(target, 'is-active', pressed); } // show/hide container - var container = document.getElementById('hide-' + targetId); + let container = document.getElementById('hide-' + targetId); + if (container) { this.addRemoveClass(container, 'hidden', pressed); } // set checkbox, if appropriate - var checkbox = el.getAttribute('data-controls-checkbox'); + let checkbox = el.getAttribute('data-controls-checkbox'); + if (checkbox) { document.getElementById(checkbox).checked = !!pressed; } // set focus, if appropriate - var focus = el.getAttribute('data-focus-target'); + let focus = el.getAttribute('data-focus-target'); + if (focus) { - var focusEl = document.getElementById(focus); + let focusEl = document.getElementById(focus); + focusEl.focus(); - setTimeout(function(){ focusEl.selectionStart = focusEl.selectionEnd = 10000; }, 0); + setTimeout(function() { focusEl.selectionStart = focusEl.selectionEnd = 10000; }, 0); } } // @todo Only update status if the promise is successful. interact(e) { e.preventDefault(); + + let identifier = e.target.getAttribute('data-id'); + this.ajaxPost(e.target); - var identifier = e.target.getAttribute('data-id'); // @todo This probably should be done with IDs. document.querySelectorAll(`.${identifier}`) @@ -129,12 +142,15 @@ let BookWyrm = new class { } toggleMenu(e) { - var el = e.currentTarget; - var expanded = el.getAttribute('aria-expanded') == 'false'; + let el = e.currentTarget; + let expanded = el.getAttribute('aria-expanded') == 'false'; + let targetId = el.getAttribute('data-controls'); + el.setAttribute('aria-expanded', expanded); - var targetId = el.getAttribute('data-controls'); + if (targetId) { - var target = document.getElementById(targetId); + let target = document.getElementById(targetId); + this.addRemoveClass(target, 'is-active', expanded); } } @@ -155,22 +171,28 @@ let BookWyrm = new class { } addClass(el, classname) { - var classes = el.className.split(' '); + let classes = el.className.split(' '); + if (classes.indexOf(classname) > -1) { return; } + el.className = classes.concat(classname).join(' '); } removeClass(el, className) { - var classes = []; + let classes = []; + if (el.className) { classes = el.className.split(' '); } + const idx = classes.indexOf(className); + if (idx > -1) { classes.splice(idx, 1); } + el.className = classes.join(' '); } } diff --git a/bookwyrm/static/js/localstorage.js b/bookwyrm/static/js/localstorage.js index b8ef02cd8..08b831624 100644 --- a/bookwyrm/static/js/localstorage.js +++ b/bookwyrm/static/js/localstorage.js @@ -15,8 +15,9 @@ let LocalStorageTools = new class { // set javascript listeners updateDisplay(e) { // used in set reading goal - var key = e.target.getAttribute('data-id'); - var value = e.target.getAttribute('data-value'); + let key = e.target.getAttribute('data-id'); + let value = e.target.getAttribute('data-value'); + window.localStorage.setItem(key, value); document.querySelectorAll('[data-hide="' + key + '"]') @@ -25,8 +26,9 @@ let LocalStorageTools = new class { setDisplay(el) { // used in set reading goal - var key = el.getAttribute('data-hide'); - var value = window.localStorage.getItem(key); + let key = el.getAttribute('data-hide'); + let value = window.localStorage.getItem(key); + BookWyrm.addRemoveClass(el, 'hidden', value); } } From 7e49b3cb26bf55d2114567cfd043c84410782fbe Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Tue, 6 Apr 2021 11:19:44 +0200 Subject: [PATCH 027/288] [assets] Simplify addRemoveClass function. --- bookwyrm/static/js/bookwyrm.js | 46 ++++++++++------------------------ 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 3df49ae5e..0ba4b7c2b 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -81,9 +81,7 @@ let BookWyrm = new class { revealForm(e) { let hidden = e.currentTarget.closest('.hidden-form').getElementsByClassName('hidden')[0]; - if (hidden) { - this.removeClass(hidden, 'hidden'); - } + this.addRemoveClass(hidden, 'hidden', !hidden); } toggleAction(e) { @@ -162,37 +160,19 @@ let BookWyrm = new class { }); } - addRemoveClass(el, classname, bool) { - if (bool) { - this.addClass(el, classname); + /** + * Add or remove a class based on a boolean condition. + * + * @param {object} node - DOM node to change class on + * @param {string} classname - Name of the class + * @param {boolean} add - Add? + * @return {undefined} + */ + addRemoveClass(node, classname, add) { + if (add) { + node.classList.add(classname); } else { - this.removeClass(el, classname); + node.classList.remove(classname); } } - - addClass(el, classname) { - let classes = el.className.split(' '); - - if (classes.indexOf(classname) > -1) { - return; - } - - el.className = classes.concat(classname).join(' '); - } - - removeClass(el, className) { - let classes = []; - - if (el.className) { - classes = el.className.split(' '); - } - - const idx = classes.indexOf(className); - - if (idx > -1) { - classes.splice(idx, 1); - } - - el.className = classes.join(' '); - } } From 62fe2ef600e340747e5bbec65cabcc24bc20596d Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Tue, 6 Apr 2021 11:37:23 +0200 Subject: [PATCH 028/288] [assets] Replace inline events with `addEventListener`; this breaks sliding menu. --- bookwyrm/static/js/bookwyrm.js | 10 +++++----- bookwyrm/static/js/localstorage.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 0ba4b7c2b..6be1d4fb0 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -11,23 +11,23 @@ let BookWyrm = new class { initEventListeners() { // buttons that display or hide content document.querySelectorAll('[data-controls]') - .forEach(button => button.onclick = this.toggleAction.bind(this)); + .forEach(button => button.addEventListener('click', this.toggleAction.bind(this))); // javascript interactions (boost/fav) document.querySelectorAll('.interaction') - .forEach(button => button.onsubmit = this.interact.bind(this)); + .forEach(button => button.addEventListener('submit', this.interact.bind(this))); // handle aria settings on menus document.querySelectorAll('.pulldown-menu') - .forEach(button => button.onclick = this.toggleMenu.bind(this)); + .forEach(button => button.addEventListener('click', this.toggleMenu.bind(this))); // hidden submit button in a form document.querySelectorAll('.hidden-form input') - .forEach(button => button.onchange = this.revealForm.bind(this)); + .forEach(button => button.addEventListener('change', this.revealForm.bind(this))); // browser back behavior document.querySelectorAll('[data-back]') - .forEach(button => button.onclick = this.back); + .forEach(button => button.addEventListener('click', this.back)); } /** diff --git a/bookwyrm/static/js/localstorage.js b/bookwyrm/static/js/localstorage.js index 08b831624..17adf2bf3 100644 --- a/bookwyrm/static/js/localstorage.js +++ b/bookwyrm/static/js/localstorage.js @@ -9,7 +9,7 @@ let LocalStorageTools = new class { // update localstorage document.querySelectorAll('.set-display') - .forEach(t => t.onclick = this.updateDisplay.bind(this)); + .forEach(t => t.addEventListener('click', this.updateDisplay.bind(this))); } // set javascript listeners From d6ee136c109dc3ed013c6e736673553428cf097a Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Tue, 6 Apr 2021 11:54:45 +0200 Subject: [PATCH 029/288] [assets] Allow to run `./bw-dev collectstatic` every time a change occurs in bookwyrm/static. --- README.md | 2 ++ package.json | 6 +++++- yarn.lock | 22 +++++++++++++++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e798fedf5..91225a563 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,8 @@ If you edit the CSS or JavaScript, you will need to run Django's `collectstatic` ./bw-dev collectstatic ``` +If you have [installed yarn](https://yarnpkg.com/getting-started/install), you can run `yarn watch:static` to automatically run the previous script every time a change occurs in _bookwyrm/static_ directory. + ### Working with translations and locale files Text in the html files are wrapped in translation tags (`{% trans %}` and `{% blocktrans %}`), and Django generates locale files for all the strings in which you can add translations for the text. You can find existing translations in the `locale/` directory. diff --git a/package.json b/package.json index 8059255d9..b7abe4342 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,12 @@ { + "scripts": { + "watch:static": "yarn watch \"./bw-dev collectstatic\" bookwyrm/static/**" + }, "devDependencies": { "eslint": "^7.23.0", "stylelint": "^13.12.0", "stylelint-config-standard": "^21.0.0", - "stylelint-order": "^4.1.0" + "stylelint-order": "^4.1.0", + "watch": "^1.0.2" } } diff --git a/yarn.lock b/yarn.lock index de4e0107e..c1a1c1812 100644 --- a/yarn.lock +++ b/yarn.lock @@ -768,6 +768,13 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +exec-sh@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" + integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw== + dependencies: + merge "^1.2.0" + execall@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/execall/-/execall-2.0.0.tgz#16a06b5fe5099df7d00be5d9c06eecded1663b45" @@ -1368,6 +1375,11 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +merge@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" + integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== + micromark@~2.11.0: version "2.11.4" resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.11.4.tgz#d13436138eea826383e822449c9a5c50ee44665a" @@ -1405,7 +1417,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.2.5: +minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -2183,6 +2195,14 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" +watch@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c" + integrity sha1-NApxe952Vyb6CqB9ch4BR6VR3ww= + dependencies: + exec-sh "^0.2.0" + minimist "^1.2.0" + which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" From 5d3d00f6946dd6c88c8e0f086fcdd1125b98abfe Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Tue, 6 Apr 2021 15:36:34 +0200 Subject: [PATCH 030/288] [assets] Use `dataset` + use expressive names for some variables. --- bookwyrm/static/js/bookwyrm.js | 34 ++++++++++++++++-------------- bookwyrm/static/js/localstorage.js | 6 +++--- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 6be1d4fb0..a434d36a3 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -54,18 +54,18 @@ let BookWyrm = new class { history.back(); } - polling(el, delay) { + polling(counter, delay) { let poller = this; delay = delay || 10000; delay += (Math.random() * 1000); setTimeout(function() { - fetch('/api/updates/' + el.getAttribute('data-poll')) + fetch('/api/updates/' + counter.dataset.poll) .then(response => response.json()) - .then(data => poller.updateCountElement(el, data)); - poller.polling(el, delay * 1.25); - }, delay, el); + .then(data => poller.updateCountElement(counter, data)); + poller.polling(counter, delay * 1.25); + }, delay, counter); } updateCountElement(el, data) { @@ -84,15 +84,17 @@ let BookWyrm = new class { this.addRemoveClass(hidden, 'hidden', !hidden); } - toggleAction(e) { - let el = e.currentTarget; - let pressed = el.getAttribute('aria-pressed') == 'false'; - let targetId = el.getAttribute('data-controls'); + toggleAction(event) { + let trigger = event.currentTarget; + let pressed = trigger.getAttribute('aria-pressed') == 'false'; + let targetId = trigger.dataset.controls; + // Un‑press all triggers controlling the same target. document.querySelectorAll('[data-controls="' + targetId + '"]') - .forEach(t => { - t.setAttribute('aria-pressed', (t.getAttribute('aria-pressed') == 'false')) - }); + .forEach(triggers => triggers.setAttribute( + 'aria-pressed', + (triggers.getAttribute('aria-pressed') == 'false')) + ); if (targetId) { let target = document.getElementById(targetId); @@ -109,14 +111,14 @@ let BookWyrm = new class { } // set checkbox, if appropriate - let checkbox = el.getAttribute('data-controls-checkbox'); + let checkbox = trigger.dataset['controls-checkbox']; if (checkbox) { document.getElementById(checkbox).checked = !!pressed; } // set focus, if appropriate - let focus = el.getAttribute('data-focus-target'); + let focus = trigger.dataset['focus-target']; if (focus) { let focusEl = document.getElementById(focus); @@ -130,7 +132,7 @@ let BookWyrm = new class { interact(e) { e.preventDefault(); - let identifier = e.target.getAttribute('data-id'); + let identifier = e.target.dataset.id; this.ajaxPost(e.target); @@ -142,7 +144,7 @@ let BookWyrm = new class { toggleMenu(e) { let el = e.currentTarget; let expanded = el.getAttribute('aria-expanded') == 'false'; - let targetId = el.getAttribute('data-controls'); + let targetId = el.dataset.controls; el.setAttribute('aria-expanded', expanded); diff --git a/bookwyrm/static/js/localstorage.js b/bookwyrm/static/js/localstorage.js index 17adf2bf3..5d978c2f9 100644 --- a/bookwyrm/static/js/localstorage.js +++ b/bookwyrm/static/js/localstorage.js @@ -15,8 +15,8 @@ let LocalStorageTools = new class { // set javascript listeners updateDisplay(e) { // used in set reading goal - let key = e.target.getAttribute('data-id'); - let value = e.target.getAttribute('data-value'); + let key = e.target.dataset.id; + let value = e.target.dataset.value; window.localStorage.setItem(key, value); @@ -26,7 +26,7 @@ let LocalStorageTools = new class { setDisplay(el) { // used in set reading goal - let key = el.getAttribute('data-hide'); + let key = el.dataset.hide; let value = window.localStorage.getItem(key); BookWyrm.addRemoveClass(el, 'hidden', value); From 52d2f0e3318c7fb4071e99e270e40982333b1a8d Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Tue, 6 Apr 2021 16:17:20 +0200 Subject: [PATCH 031/288] [assets] Document functions and variables: - Use expressive names for variables. - Add docblocks for each function. - Add ESLint rules for comments. --- .eslintrc.js | 18 +++++ bookwyrm/static/js/bookwyrm.js | 120 ++++++++++++++++++++++------- bookwyrm/static/js/localstorage.js | 31 +++++--- 3 files changed, 132 insertions(+), 37 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 5a247a43f..b65fe9885 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,7 +29,25 @@ module.exports = { // Stylistic Issues "arrow-spacing": "error", + "capitalized-comments": [ + "warn", + "always", + { + "ignoreConsecutiveComments": true + }, + ], "keyword-spacing": "error", + "lines-around-comment": [ + "error", + { + "beforeBlockComment": true, + "beforeLineComment": true, + "allowBlockStart": true, + "allowClassStart": true, + "allowObjectStart": true, + "allowArrayStart": true, + }, + ], "no-multiple-empty-lines": [ "error", { diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index a434d36a3..63947aaa4 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -49,13 +49,28 @@ let BookWyrm = new class { .forEach(liveArea => this.polling(liveArea)); } - back(e) { - e.preventDefault(); + /** + * Go back in browser history. + * + * @param {Event} event + * + * @return {undefined} + */ + back(event) { + event.preventDefault(); history.back(); } + /** + * Update a counter with recurring requests to the API + * + * @param {Object} counter - DOM node + * @param {int} delay - frequency for polling in ms + * + * @return {undefined} + */ polling(counter, delay) { - let poller = this; + const bookwyrm = this; delay = delay || 10000; delay += (Math.random() * 1000); @@ -63,27 +78,51 @@ let BookWyrm = new class { setTimeout(function() { fetch('/api/updates/' + counter.dataset.poll) .then(response => response.json()) - .then(data => poller.updateCountElement(counter, data)); - poller.polling(counter, delay * 1.25); + .then(data => bookwyrm.updateCountElement(counter, data)); + + bookwyrm.polling(counter, delay * 1.25); }, delay, counter); } - updateCountElement(el, data) { - const currentCount = el.innerText; + /** + * Update a counter. + * + * @param {object} counter - DOM node + * @param {object} data - json formatted response from a fetch + * + * @return {undefined} + */ + updateCountElement(counter, data) { + const currentCount = counter.innerText; const count = data.count; if (count != currentCount) { - this.addRemoveClass(el.closest('[data-poll-wrapper]'), 'hidden', count < 1); - el.innerText = count; + this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'hidden', count < 1); + counter.innerText = count; } } - revealForm(e) { - let hidden = e.currentTarget.closest('.hidden-form').getElementsByClassName('hidden')[0]; + /** + * Toggle form. + * + * @param {Event} event + * + * @return {undefined} + */ + revealForm(event) { + let trigger = event.currentTarget; + let hidden = trigger.closest('.hidden-form').querySelectorAll('.hidden')[0]; this.addRemoveClass(hidden, 'hidden', !hidden); } + /** + * Execute actions on targets based on triggers. + * + * @param {Event} event + * + * @return {undefined} + */ toggleAction(event) { let trigger = event.currentTarget; let pressed = trigger.getAttribute('aria-pressed') == 'false'; @@ -103,21 +142,21 @@ let BookWyrm = new class { this.addRemoveClass(target, 'is-active', pressed); } - // show/hide container + // Show/hide container. let container = document.getElementById('hide-' + targetId); if (container) { this.addRemoveClass(container, 'hidden', pressed); } - // set checkbox, if appropriate + // Check checkbox, if appropriate. let checkbox = trigger.dataset['controls-checkbox']; if (checkbox) { document.getElementById(checkbox).checked = !!pressed; } - // set focus, if appropriate + // Set focus, if appropriate. let focus = trigger.dataset['focus-target']; if (focus) { @@ -128,25 +167,45 @@ let BookWyrm = new class { } } - // @todo Only update status if the promise is successful. - interact(e) { - e.preventDefault(); + /** + * Make a request and update the UI accordingly. + * This function is used for boosts and favourites. + * + * @todo Only update status if the promise is successful. + * + * @param {Event} event + * + * @return {undefined} + */ + interact(event) { + event.preventDefault(); - let identifier = e.target.dataset.id; - - this.ajaxPost(e.target); + this.ajaxPost(event.target); // @todo This probably should be done with IDs. - document.querySelectorAll(`.${identifier}`) - .forEach(t => this.addRemoveClass(t, 'hidden', t.className.indexOf('hidden') == -1)); + document.querySelectorAll(`.${event.target.dataset.id}`) + .forEach(node => this.addRemoveClass( + node, + 'hidden', + node.className.indexOf('hidden') == -1 + )); } - toggleMenu(e) { - let el = e.currentTarget; - let expanded = el.getAttribute('aria-expanded') == 'false'; - let targetId = el.dataset.controls; + /** + * Handle ARIA states on toggled menus. + * + * @note This function seems to be redundant and conflicts with toggleAction. + * + * @param {Event} event + * + * @return {undefined} + */ + toggleMenu(event) { + let trigger = event.currentTarget; + let expanded = trigger.getAttribute('aria-expanded') == 'false'; + let targetId = trigger.dataset.controls; - el.setAttribute('aria-expanded', expanded); + trigger.setAttribute('aria-expanded', expanded); if (targetId) { let target = document.getElementById(targetId); @@ -155,6 +214,13 @@ let BookWyrm = new class { } } + /** + * Submit a form using POST. + * + * @param {object} form - Form to be submitted + * + * @return {undefined} + */ ajaxPost(form) { fetch(form.action, { method : "POST", diff --git a/bookwyrm/static/js/localstorage.js b/bookwyrm/static/js/localstorage.js index 5d978c2f9..eccac3471 100644 --- a/bookwyrm/static/js/localstorage.js +++ b/bookwyrm/static/js/localstorage.js @@ -3,32 +3,43 @@ let LocalStorageTools = new class { constructor() { - // display based on localstorage vars document.querySelectorAll('[data-hide]') .forEach(t => this.setDisplay(t)); - // update localstorage document.querySelectorAll('.set-display') .forEach(t => t.addEventListener('click', this.updateDisplay.bind(this))); } - // set javascript listeners - updateDisplay(e) { + /** + * Update localStorage, then display content based on keys in localStorage. + * + * @param {Event} event + * + * @return {undefined} + */ + updateDisplay(event) { // used in set reading goal - let key = e.target.dataset.id; - let value = e.target.dataset.value; + let key = event.target.dataset.id; + let value = event.target.dataset.value; window.localStorage.setItem(key, value); document.querySelectorAll('[data-hide="' + key + '"]') - .forEach(t => this.setDisplay(t)); + .forEach(node => this.setDisplay(node)); } - setDisplay(el) { + /** + * Toggle display of a DOM node based on its value in the localStorage. + * + * @param {object} node - DOM node to toggle. + * + * @return {undefined} + */ + setDisplay(node) { // used in set reading goal - let key = el.dataset.hide; + let key = node.dataset.hide; let value = window.localStorage.getItem(key); - BookWyrm.addRemoveClass(el, 'hidden', value); + BookWyrm.addRemoveClass(node, 'hidden', value); } } From a21f954fb1e90a99a915273de994ce7a5936f79b Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Tue, 6 Apr 2021 16:29:58 +0200 Subject: [PATCH 032/288] Remove redundant `class` attribute and format template. --- bookwyrm/templates/components/dropdown.html | 27 ++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/components/dropdown.html b/bookwyrm/templates/components/dropdown.html index 72582ddc3..734d62764 100644 --- a/bookwyrm/templates/components/dropdown.html +++ b/bookwyrm/templates/components/dropdown.html @@ -1,13 +1,34 @@ +{% spaceless %} {% load bookwyrm_tags %} + {% with 0|uuid as uuid %} - {% endwith %} +{% endspaceless %} From fd66ff1861fbaaef572122c5d2636cb1e9ba067f Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 6 Apr 2021 07:53:34 -0700 Subject: [PATCH 033/288] Small tweaks to commends and super() calls --- bookwyrm/activitystreams.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bookwyrm/activitystreams.py b/bookwyrm/activitystreams.py index 2db6cc20a..949ae9dad 100644 --- a/bookwyrm/activitystreams.py +++ b/bookwyrm/activitystreams.py @@ -19,12 +19,12 @@ class ActivityStream(RedisStore): return "{}-unread".format(self.stream_id(user)) def get_rank(self, obj): # pylint: disable=no-self-use - """ the sort rank of a status, which is published date """ + """ statuses are sorted by date published """ return obj.published_date.timestamp() def add_status(self, status): """ add a status to users' feeds """ - # the pipeline contains all the addp-to-stream activities + # the pipeline contains all the add-to-stream activities pipeline = self.add_object_to_related_stores(status, execute=False) for user in self.get_audience(status): @@ -36,11 +36,13 @@ class ActivityStream(RedisStore): def add_user_statuses(self, viewer, user): """ add a user's statuses to another user's feed """ + # only add the statuses that the viewer should be able to see (ie, not dms) statuses = privacy_filter(viewer, user.status_set.all()) self.bulk_add_objects_to_store(statuses, self.stream_id(viewer)) def remove_user_statuses(self, viewer, user): """ remove a user's status from another user's feed """ + # remove all so that followers only statuses are removed statuses = user.status_set.all() self.bulk_remove_objects_from_store(statuses, self.stream_id(viewer)) @@ -49,7 +51,7 @@ class ActivityStream(RedisStore): # clear unreads for this feed r.set(self.unread_id(user), 0) - statuses = super().get_store(self.stream_id(user)) + statuses = self.get_store(self.stream_id(user)) return ( models.Status.objects.select_subclasses() .filter(id__in=statuses) @@ -62,7 +64,7 @@ class ActivityStream(RedisStore): def populate_streams(self, user): """ go from zero to a timeline """ - super().populate_store(self.stream_id(user)) + self.populate_store(self.stream_id(user)) def get_audience(self, status): # pylint: disable=no-self-use """ given a status, what users should see it """ From 9d95f54aa23ebc684255ba301cc8391333b61860 Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Tue, 6 Apr 2021 17:48:56 +0200 Subject: [PATCH 034/288] [assets] Refactor `toggleAction` and `toggleMenu` a bit. --- bookwyrm/static/js/bookwyrm.js | 102 ++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 28 deletions(-) diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 63947aaa4..0ec14336f 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -9,25 +9,29 @@ let BookWyrm = new class { } initEventListeners() { - // buttons that display or hide content document.querySelectorAll('[data-controls]') - .forEach(button => button.addEventListener('click', this.toggleAction.bind(this))); + .forEach(button => button.addEventListener( + 'click', + this.toggleAction.bind(this)) + ); - // javascript interactions (boost/fav) document.querySelectorAll('.interaction') - .forEach(button => button.addEventListener('submit', this.interact.bind(this))); + .forEach(button => button.addEventListener( + 'submit', + this.interact.bind(this)) + ); - // handle aria settings on menus - document.querySelectorAll('.pulldown-menu') - .forEach(button => button.addEventListener('click', this.toggleMenu.bind(this))); - - // hidden submit button in a form document.querySelectorAll('.hidden-form input') - .forEach(button => button.addEventListener('change', this.revealForm.bind(this))); + .forEach(button => button.addEventListener( + 'change', + this.revealForm.bind(this)) + ); - // browser back behavior document.querySelectorAll('[data-back]') - .forEach(button => button.addEventListener('click', this.back)); + .forEach(button => button.addEventListener( + 'click', + this.back) + ); } /** @@ -125,45 +129,48 @@ let BookWyrm = new class { */ toggleAction(event) { let trigger = event.currentTarget; - let pressed = trigger.getAttribute('aria-pressed') == 'false'; + let pressed = trigger.getAttribute('aria-pressed') === 'false'; let targetId = trigger.dataset.controls; - // Un‑press all triggers controlling the same target. + // Toggle pressed status on all triggers controlling the same target. document.querySelectorAll('[data-controls="' + targetId + '"]') - .forEach(triggers => triggers.setAttribute( + .forEach(otherTrigger => otherTrigger.setAttribute( 'aria-pressed', - (triggers.getAttribute('aria-pressed') == 'false')) - ); + otherTrigger.getAttribute('aria-pressed') === 'false' + )); - if (targetId) { + // @todo Find a better way to handle the exception. + if (targetId && ! trigger.classList.contains('pulldown-menu')) { let target = document.getElementById(targetId); this.addRemoveClass(target, 'hidden', !pressed); this.addRemoveClass(target, 'is-active', pressed); } + // Show/hide pulldown-menus. + if (trigger.classList.contains('pulldown-menu')) { + this.toggleMenu(trigger, targetId); + } + // Show/hide container. let container = document.getElementById('hide-' + targetId); if (container) { - this.addRemoveClass(container, 'hidden', pressed); + this.toggleContainer(container, pressed); } // Check checkbox, if appropriate. let checkbox = trigger.dataset['controls-checkbox']; if (checkbox) { - document.getElementById(checkbox).checked = !!pressed; + this.toggleCheckbox(checkbox, pressed); } // Set focus, if appropriate. let focus = trigger.dataset['focus-target']; if (focus) { - let focusEl = document.getElementById(focus); - - focusEl.focus(); - setTimeout(function() { focusEl.selectionStart = focusEl.selectionEnd = 10000; }, 0); + this.toggleFocus(focus); } } @@ -192,7 +199,7 @@ let BookWyrm = new class { } /** - * Handle ARIA states on toggled menus. + * Show or hide menus. * * @note This function seems to be redundant and conflicts with toggleAction. * @@ -200,10 +207,8 @@ let BookWyrm = new class { * * @return {undefined} */ - toggleMenu(event) { - let trigger = event.currentTarget; + toggleMenu(trigger, targetId) { let expanded = trigger.getAttribute('aria-expanded') == 'false'; - let targetId = trigger.dataset.controls; trigger.setAttribute('aria-expanded', expanded); @@ -214,6 +219,47 @@ let BookWyrm = new class { } } + /** + * Show or hide generic containers. + * + * @param {object} container - DOM node + * @param {boolean} pressed - Is the trigger pressed? + * + * @return {undefined} + */ + toggleContainer(container, pressed) { + this.addRemoveClass(container, 'hidden', pressed); + } + + /** + * Check or uncheck a checbox. + * + * @param {object} checkbox - DOM node + * @param {boolean} pressed - Is the trigger pressed? + * + * @return {undefined} + */ + toggleCheckbox(checkbox, pressed) { + document.getElementById(checkbox).checked = !!pressed; + } + + /** + * Give the focus to an element. + * + * @param {string} nodeId - ID of the DOM node to focus (button, link…) + * + * @return {undefined} + */ + toggleFocus(nodeId) { + let node = document.getElementById(nodeId); + + node.focus(); + + setTimeout(function() { + node.selectionStart = node.selectionEnd = 10000; + }, 0); + } + /** * Submit a form using POST. * From 44040201f9d28ea5ea78dfd847d2c72dafcef038 Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Tue, 6 Apr 2021 17:57:39 +0200 Subject: [PATCH 035/288] [assets] Move interact function. --- bookwyrm/static/js/bookwyrm.js | 48 +++++++++++++++++----------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 0ec14336f..b3959e30e 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -174,30 +174,6 @@ let BookWyrm = new class { } } - /** - * Make a request and update the UI accordingly. - * This function is used for boosts and favourites. - * - * @todo Only update status if the promise is successful. - * - * @param {Event} event - * - * @return {undefined} - */ - interact(event) { - event.preventDefault(); - - this.ajaxPost(event.target); - - // @todo This probably should be done with IDs. - document.querySelectorAll(`.${event.target.dataset.id}`) - .forEach(node => this.addRemoveClass( - node, - 'hidden', - node.className.indexOf('hidden') == -1 - )); - } - /** * Show or hide menus. * @@ -260,6 +236,30 @@ let BookWyrm = new class { }, 0); } + /** + * Make a request and update the UI accordingly. + * This function is used for boosts and favourites. + * + * @todo Only update status if the promise is successful. + * + * @param {Event} event + * + * @return {undefined} + */ + interact(event) { + event.preventDefault(); + + this.ajaxPost(event.target); + + // @todo This probably should be done with IDs. + document.querySelectorAll(`.${event.target.dataset.id}`) + .forEach(node => this.addRemoveClass( + node, + 'hidden', + node.className.indexOf('hidden') == -1 + )); + } + /** * Submit a form using POST. * From 1f99710dcdc17b5f7fb42ecfa8676da3d9e2c3e7 Mon Sep 17 00:00:00 2001 From: tofuwabohu Date: Tue, 6 Apr 2021 22:36:24 +0200 Subject: [PATCH 036/288] Links to own user in menu --- bookwyrm/templates/layout.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index 80eb386a5..0f100f2cd 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -70,7 +70,7 @@ {% if request.user.is_authenticated %}