From 889810bb96f8ce3025370f01768111390b0c4d42 Mon Sep 17 00:00:00 2001
From: Coucouf
Date: Wed, 23 Sep 2020 22:25:11 +0000
Subject: [PATCH 01/46] Translated using Weblate (French)
Currently translated at 100.0% (1321 of 1321 strings)
---
plinth/locale/fr/LC_MESSAGES/django.po | 561 ++++++++++++-------------
1 file changed, 279 insertions(+), 282 deletions(-)
diff --git a/plinth/locale/fr/LC_MESSAGES/django.po b/plinth/locale/fr/LC_MESSAGES/django.po
index 10873041b..0d3483664 100644
--- a/plinth/locale/fr/LC_MESSAGES/django.po
+++ b/plinth/locale/fr/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: FreedomBox UI\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-09-15 16:55-0400\n"
-"PO-Revision-Date: 2020-09-23 11:08+0000\n"
+"PO-Revision-Date: 2020-09-24 22:41+0000\n"
"Last-Translator: Coucouf \n"
"Language-Team: French \n"
@@ -83,12 +83,12 @@ msgstr "Application installée."
#: plinth/middleware.py:63
#, python-brace-format
msgid "Error installing application: {string} {details}"
-msgstr "Erreur lors de l’installation de l'application : {string} {details}"
+msgstr "Erreur lors de l’installation de l’application : {string} {details}"
#: plinth/middleware.py:67
#, python-brace-format
msgid "Error installing application: {error}"
-msgstr "Erreur lors de l’installation de l'application : {error}"
+msgstr "Erreur lors de l’installation de l’application : {error}"
#: plinth/modules/apache/__init__.py:40
#: plinth/modules/monkeysphere/templates/monkeysphere.html:67
@@ -104,12 +104,12 @@ msgstr "Interface web de la {box_name} (Plinth)"
#: plinth/modules/apache/components.py:120
#, python-brace-format
msgid "Access URL {url} on tcp{kind}"
-msgstr "Accès à l'URL {url} via tcp{kind}"
+msgstr "Accès à l’URL {url} via tcp{kind}"
#: plinth/modules/apache/components.py:124
#, python-brace-format
msgid "Access URL {url}"
-msgstr "Accès à l'URL {url}"
+msgstr "Accès à l’URL {url}"
#: plinth/modules/avahi/__init__.py:34
#, python-brace-format
@@ -272,7 +272,7 @@ msgid ""
"Path of a new or existing repository. Example: user@host:~/path/to/repo/"
"i>"
msgstr ""
-"Chemin d'un dépôt, nouveau ou existant. Exemple : utilisateur@machine:~/"
+"Chemin d’un dépôt, nouveau ou existant. Exemple : utilisateur@machine:~/"
"chemin/vers/le/dépôt/"
#: plinth/modules/backups/forms.py:193
@@ -284,7 +284,7 @@ msgid ""
"Password of the SSH Server.
SSH key-based authentication is not yet "
"possible."
msgstr ""
-"Mot de passe du serveur SSH.
L’authentification par clé SSH n'est pas "
+"Mot de passe du serveur SSH.
L’authentification par clé SSH n’est pas "
"encore supportée."
#: plinth/modules/backups/forms.py:213
@@ -403,7 +403,7 @@ msgstr "Heure"
#: plinth/modules/backups/templates/backups_delete.html:36
#, python-format
msgid "Delete Archive %(name)s"
-msgstr "Supprimer l'archive %(name)s"
+msgstr "Supprimer l’archive %(name)s"
#: plinth/modules/backups/templates/backups_form.html:20
#: plinth/modules/bepasty/templates/bepasty_add.html:20
@@ -414,7 +414,7 @@ msgstr "Supprimer l'archive %(name)s"
#: plinth/modules/pagekite/templates/pagekite_custom_services.html:28
#: plinth/modules/sharing/templates/sharing_add_edit.html:20
msgid "Submit"
-msgstr "Soumettre"
+msgstr "Envoyer"
#: plinth/modules/backups/templates/backups_repository.html:19
msgid "This repository is encrypted"
@@ -422,16 +422,16 @@ msgstr "Ce dépôt est chiffré"
#: plinth/modules/backups/templates/backups_repository.html:34
msgid "Unmount Location"
-msgstr "Démonter l'emplacement"
+msgstr "Démonter l’emplacement"
#: plinth/modules/backups/templates/backups_repository.html:45
msgid "Mount Location"
-msgstr "Monter l'emplacement"
+msgstr "Monter l’emplacement"
#: plinth/modules/backups/templates/backups_repository.html:56
msgid "Remove Backup Location. This will not delete the remote backup."
msgstr ""
-"Supprimer l'emplacement de sauvegarde. Cette action ne supprimera pas la "
+"Supprimer l’emplacement de sauvegarde. Cette action ne supprimera pas la "
"sauvegarde distante."
#: plinth/modules/backups/templates/backups_repository.html:77
@@ -463,7 +463,7 @@ msgstr ""
#: plinth/modules/backups/templates/backups_repository_remove.html:31
msgid "Remove Location"
-msgstr "Enlever l'emplacement"
+msgstr "Enlever l’emplacement"
#: plinth/modules/backups/templates/backups_restore.html:15
msgid "Restore data from"
@@ -549,7 +549,7 @@ msgstr "Archive créée."
#: plinth/modules/backups/views.py:83
msgid "Delete Archive"
-msgstr "Supprimer l'archive"
+msgstr "Supprimer l’archive"
#: plinth/modules/backups/views.py:95
msgid "Archive deleted."
@@ -605,7 +605,7 @@ msgstr "La clé publique d’authenticité du serveur SSH n’a pu être vérifi
#: plinth/modules/backups/views.py:366
msgid "Authentication to remote server failed."
-msgstr "L'authentification sur le serveur distant a échoué."
+msgstr "L’authentification sur le serveur distant a échoué."
#: plinth/modules/backups/views.py:368
msgid "Error establishing connection to server: {}"
@@ -621,7 +621,7 @@ msgstr "Supprimer ce dépôt"
#: plinth/modules/backups/views.py:402
msgid "Repository removed. Backups were not deleted."
-msgstr "Dépôt supprimé. Les sauvegardes n'ont pas été supprimées."
+msgstr "Dépôt supprimé. Les sauvegardes n’ont pas été supprimées."
#: plinth/modules/backups/views.py:412
msgid "Unmounting failed!"
@@ -638,6 +638,11 @@ msgid ""
"audio, video and PDF documents can be previewed in the browser. Shared files "
"can be set to expire after a time period."
msgstr ""
+"bepasty est une application web permettant l’envoi et le partage de gros "
+"fichiers. Du texte et des extraits de code source peuvent également être "
+"collés et partagés. Les documents texte, images, audio, vidéo et PDF "
+"pourront être visualisés directement dans le navigateur. Il est possible de "
+"définir une date d’expiration pour les fichiers envoyés."
#: plinth/modules/bepasty/__init__.py:29
msgid ""
@@ -646,6 +651,11 @@ msgid ""
"password, you can share it with the users who should have the associated "
"permissions."
msgstr ""
+"bepasty n’utilise pas de noms d’utilisateurs comme identifiants mais "
+"seulement des mots de passe. Un jeu de permissions peut être défini pour "
+"chaque mot de passe. Une fois que vous avez créé un mot de passe et les "
+"permissions associées, vous pouvez le partager aux utilisateurs qui "
+"devraient y avoir accès."
#: plinth/modules/bepasty/__init__.py:33
msgid ""
@@ -654,56 +664,57 @@ msgid ""
"revoke access for a single person or group, by removing their password from "
"the list."
msgstr ""
+"Vous pouvez même créer plusieurs mots de passe avec le même jeu de "
+"permissions pour les distribuer à différentes personnes ou groupes de "
+"personnes. Cela vous permettra par la suite de révoquer les accès d’une "
+"seule personne ou d’un seul groupe en supprimant leur mot de passe de la "
+"liste."
#: plinth/modules/bepasty/__init__.py:42 plinth/modules/bepasty/__init__.py:51
msgid "Read a file, if a web link to the file is available"
-msgstr ""
+msgstr "Accès en lecture seule aux fichiers, à partir de leur lien"
#: plinth/modules/bepasty/__init__.py:43
-#, fuzzy
-#| msgid "Restore from uploaded file"
msgid "Create or upload files"
-msgstr "Restaurer du fichier téléversé"
+msgstr "Créer ou envoyer des fichiers"
#: plinth/modules/bepasty/__init__.py:44
msgid "List all files and their web links"
-msgstr ""
+msgstr "Lister l’ensemble des fichiers et récupérer leurs liens"
#: plinth/modules/bepasty/__init__.py:45
-#, fuzzy
-#| msgid "Delete User"
msgid "Delete files"
-msgstr "Supprimer Utilisateur"
+msgstr "Supprimer des fichiers"
#: plinth/modules/bepasty/__init__.py:46
msgid "Administer files: lock/unlock files"
-msgstr ""
+msgstr "Administrer des fichiers : verrouiller / déverouiller des fichiers"
#: plinth/modules/bepasty/__init__.py:50
msgid "None, password is always required"
-msgstr ""
+msgstr "Aucune, le mot de passe est toujours exigé"
#: plinth/modules/bepasty/__init__.py:52
msgid "List and read all files"
-msgstr ""
+msgstr "Listing et lecture de l’ensemble des fichiers"
#: plinth/modules/bepasty/__init__.py:65 plinth/modules/bepasty/manifest.py:9
msgid "bepasty"
-msgstr ""
+msgstr "bepasty"
#: plinth/modules/bepasty/__init__.py:67
-#, fuzzy
-#| msgid "File Sharing"
msgid "File & Snippet Sharing"
-msgstr "Partage de fichiers"
+msgstr "Partage de fichiers et de bribes"
#: plinth/modules/bepasty/forms.py:17
msgid "Public Access (default permissions)"
-msgstr ""
+msgstr "Accès public (permissions par défaut)"
#: plinth/modules/bepasty/forms.py:18
msgid "Permissions for anonymous users, who have not provided a password."
msgstr ""
+"Permissions pour les utilisateurs anonymes, n’ayant pas fourni de mot de "
+"passe."
#: plinth/modules/bepasty/forms.py:27
#: plinth/modules/bepasty/templates/bepasty.html:30
@@ -715,34 +726,31 @@ msgstr "Permissions"
msgid ""
"Users that log in with this password will have the selected permissions."
msgstr ""
+"Les utilisateurs se connectant avec ce mot de passe auront les permissions "
+"sélectionnées."
#: plinth/modules/bepasty/forms.py:33
#: plinth/modules/bepasty/templates/bepasty.html:31
msgid "Comment"
-msgstr ""
+msgstr "Commentaire"
#: plinth/modules/bepasty/forms.py:34
msgid "Any comment to help you remember the purpose of this password."
msgstr ""
+"Toute information utile pour vous rappeler de l’objet de ce mot de passe."
#: plinth/modules/bepasty/templates/bepasty.html:12
-#, fuzzy
-#| msgid "Change Password"
msgid "Manage Passwords"
-msgstr "Changer Mot de Passe"
+msgstr "Gestion des mots de passe"
#: plinth/modules/bepasty/templates/bepasty.html:16
#: plinth/modules/bepasty/templates/bepasty.html:18
-#, fuzzy
-#| msgid "Show password"
msgid "Add password"
-msgstr "Afficher le mot de passe"
+msgstr "Ajouter un mot de passe"
#: plinth/modules/bepasty/templates/bepasty.html:23
-#, fuzzy
-#| msgid "No shares currently configured."
msgid "No passwords currently configured."
-msgstr "Aucun partage actuellement configuré."
+msgstr "Aucun mot de passe actuellement configuré."
#: plinth/modules/bepasty/templates/bepasty.html:29
#: plinth/modules/dynamicdns/forms.py:106 plinth/modules/networks/forms.py:205
@@ -752,15 +760,15 @@ msgstr "Mot de passe"
#: plinth/modules/bepasty/views.py:42
msgid "Read"
-msgstr ""
+msgstr "Lecture"
#: plinth/modules/bepasty/views.py:43
msgid "Create"
-msgstr "Créer"
+msgstr "Création"
#: plinth/modules/bepasty/views.py:44
msgid "List"
-msgstr ""
+msgstr "Listing"
#: plinth/modules/bepasty/views.py:45
#: plinth/modules/letsencrypt/templates/letsencrypt.html:91
@@ -771,13 +779,11 @@ msgstr ""
#: plinth/modules/wireguard/templates/wireguard_show_client.html:73
#: plinth/modules/wireguard/templates/wireguard_show_server.html:74
msgid "Delete"
-msgstr "Supprimer"
+msgstr "Suppression"
#: plinth/modules/bepasty/views.py:46
-#, fuzzy
-#| msgid "admin"
msgid "Admin"
-msgstr "admin"
+msgstr "Admin"
#: plinth/modules/bepasty/views.py:83 plinth/modules/searx/views.py:38
#: plinth/modules/searx/views.py:49 plinth/modules/tor/views.py:130
@@ -792,29 +798,23 @@ msgid "An error occurred during configuration."
msgstr "Une erreur est survenue pendant la configuration."
#: plinth/modules/bepasty/views.py:97
-#, fuzzy
-#| msgid "Password updated"
msgid "Password added."
-msgstr "Mot de passe mis à jour"
+msgstr "Mot de passe ajouté."
#: plinth/modules/bepasty/views.py:102
-#, fuzzy
-#| msgid "Password"
msgid "Add Password"
-msgstr "Mot de passe"
+msgstr "Ajouter un mot de passe"
#: plinth/modules/bepasty/views.py:119
-#, fuzzy
-#| msgid "Password updated"
msgid "Password deleted."
-msgstr "Mot de passe mis à jour"
+msgstr "Mot de passe supprimé."
#: plinth/modules/bind/__init__.py:29
msgid ""
"BIND enables you to publish your Domain Name System (DNS) information on the "
"Internet, and to resolve DNS queries for your user devices on your network."
msgstr ""
-"BIND vous permet de publier sur l'internet vos informations DNS (Système de "
+"BIND vous permet de publier sur Internet vos informations DNS (Système de "
"Noms de Domaines) et de résoudre les requêtes DNS pour les appareils de "
"votre réseau local."
@@ -901,11 +901,11 @@ msgid ""
"advanced functions that are not usually required. A web based terminal for "
"console operations is also available."
msgstr ""
-"Cockpit est un gestionnaire de serveur qui simplifie l'administration de "
-"serveurs GNU/Linux à l'aide d'un navigateur web. Sur une {box_name}, des "
-"contrôles sont disponibles pour des fonctions avancées qui ne sont pas "
-"nécessaires habituellement. Un terminal web est également disponible pour "
-"des opérations depuis une console."
+"Cockpit est un gestionnaire de serveur qui simplifie l’administration de "
+"serveurs GNU/Linux à l’aide d'un navigateur web. Sur une {box_name}, des "
+"contrôles sont disponibles pour de nombreuses fonctions avancées qui ne sont "
+"pas nécessaires dans la plupart des cas. Un terminal web est également "
+"fourni pour les opérations d’administration réalisables via une console."
#: plinth/modules/cockpit/__init__.py:38
msgid ""
@@ -914,6 +914,12 @@ msgid ""
"firewall ports and advanced networking such as bonding, bridging and VLAN "
"management."
msgstr ""
+"Cockpit peut être utilisé pour des opérations avancées sur le stockage "
+"telles que le partitionnement de disques et la gestion de grappes de disques "
+"RAID. Il peut aussi être utilisé pour l’ouverture de ports personnalisés sur "
+"le pare-feu, ainsi que les opérations de réseau avancées telles que l’"
+"agrégation de liens (« bonding »), la création de ponts (« bridging ») et la "
+"gestion de réseaux virtuels VLAN."
#: plinth/modules/cockpit/__init__.py:43
#, python-brace-format
@@ -1002,8 +1008,8 @@ msgid ""
msgstr ""
"Le nom de machine est le nom local par lequel les autres appareils sur le "
"réseau local joindront votre {box_name}. Le nom doit commencer et se "
-"terminer par une lettre de l'alphabet ou un chiffre et contenir seulement "
-"des lettres de l'alphabet, des chiffres ou des traits d’union « - ». Sa "
+"terminer par une lettre de l’alphabet ou un chiffre et contenir seulement "
+"des lettres de l’alphabet, des chiffres ou des traits d’union « - ». Sa "
"longueur maximum est de 63 signes."
#: plinth/modules/config/forms.py:57
@@ -1023,8 +1029,8 @@ msgstr ""
"Le nom de domaine est le nom global par lequel les autres appareils sur "
"Internet peuvent joindre votre {box_name}. Le nom ne doit contenir que des "
"mots ou « labels » séparés par des points. Chaque label doit commencer et se "
-"terminer par une lettre de l'alphabet ou un chiffre et contenir uniquement "
-"des lettres de l'alphabet, des chiffres ou des traits d’union « - ». La "
+"terminer par une lettre de l’alphabet ou un chiffre et contenir uniquement "
+"des lettres de l’alphabet, des chiffres ou des traits d’union « - ». La "
"longueur maximum d'un label est de 63 signes. La longueur totale du nom de "
"domaine ne doit pas excéder 253 signes."
@@ -1110,7 +1116,7 @@ msgstr ""
"Coturn est un serveur facilitant les appels audios/vidéos ainsi que les "
"conférences, en fournissant une implémentation des protocoles TURN et STUN. "
"Les autres types de serveurs de communication, comme WebRTC et SIP, peuvent "
-"l'utiliser afin d'établir un appel entre plusieurs parties qui n'auraient "
+"l’utiliser afin d'établir un appel entre plusieurs parties qui n’auraient "
"pas pu se connecter entre elles autrement."
#: plinth/modules/coturn/__init__.py:35
@@ -1118,8 +1124,8 @@ msgid ""
"It is not meant to be used directly by users. Servers such as matrix-synapse "
"need to be configured with the details provided here."
msgstr ""
-"Ceci n'est pas sensé être utilisé directement par les utilisateurs. Les "
-"serveurs du type matrix-synapse nécessitent d'être configurés avec les "
+"Il n’est pas destiné à être utilisé directement par les utilisateurs finaux. "
+"Les serveurs du type matrix-synapse nécessitent d’être configurés avec les "
"détails fournis ici."
#: plinth/modules/coturn/__init__.py:52
@@ -1145,20 +1151,20 @@ msgstr ""
#: plinth/modules/coturn/templates/coturn.html:15
msgid "Use the following URLs to configure your communication server:"
msgstr ""
-"Veuillez utiliser l'URL suivante pour configurer votre serveur de "
+"Veuillez utiliser l’URL suivante pour configurer votre serveur de "
"communication :"
#: plinth/modules/coturn/templates/coturn.html:26
msgid "Use the following shared authentication secret:"
-msgstr "Veuillez utiliser les secrets d'authentification partagés suivants :"
+msgstr "Veuillez utiliser les secrets d’authentification partagés suivants :"
#: plinth/modules/datetime/__init__.py:25
msgid ""
"Network time server is a program that maintains the system time in "
"synchronization with servers on the Internet."
msgstr ""
-"Le serveur de temps réseau est un programme permettant de maintenir l'heure "
-"du système synchronisée avec les serveurs sur Internet."
+"Le serveur de temps réseau est un programme permettant de maintenir l’heure "
+"du système synchronisée avec les serveurs Internet."
#: plinth/modules/datetime/__init__.py:69
msgid "Date & Time"
@@ -1242,20 +1248,16 @@ msgid "Diagnostics"
msgstr "Diagnostiques"
#: plinth/modules/diagnostics/__init__.py:102
-#, fuzzy
-#| msgid "Quassel"
msgid "passed"
-msgstr "Quassel"
+msgstr "réussi"
#: plinth/modules/diagnostics/__init__.py:103
-#, fuzzy
-#| msgid "Setup failed."
msgid "failed"
-msgstr "Échec de l’installation."
+msgstr "échoué"
#: plinth/modules/diagnostics/__init__.py:104
msgid "error"
-msgstr ""
+msgstr "erreur"
#: plinth/modules/diagnostics/templates/diagnostics.html:17
#: plinth/modules/diagnostics/templates/diagnostics_button.html:13
@@ -1283,7 +1285,7 @@ msgstr "Résultats des diagnostics"
#: plinth/modules/diagnostics/templates/diagnostics_app.html:17
msgid "This app does not support diagnostics"
-msgstr "Cette application n'est pas compatible avec les tests de diagnostic."
+msgstr "Cette application n’est pas compatible avec les tests de diagnostic."
#: plinth/modules/diagnostics/templates/diagnostics_results.html:10
msgid "Test"
@@ -1316,7 +1318,7 @@ msgstr "Réseau social fédéré"
#: plinth/modules/diaspora/forms.py:13
msgid "Enable new user registrations"
-msgstr "Activer l'inscription de nouveaux utilisateurs"
+msgstr "Activer l’inscription de nouveaux utilisateurs"
#: plinth/modules/diaspora/manifest.py:11
msgid "dandelion*"
@@ -1372,7 +1374,7 @@ msgid ""
"24h), it may be hard for others to find you on the Internet. This will "
"prevent others from finding services which are provided by this {box_name}."
msgstr ""
-"Si votre fournisseur d'accès change régulièrement votre adresse IP (par ex. "
+"Si votre fournisseur d’accès change régulièrement votre adresse IP (par ex. "
"toutes les 24 h), il pourrait être difficile de vous retrouver sur Internet. "
"Et ceci empêchera d’autres utilisateurs de découvrir les services proposés "
"par cette {box_name}."
@@ -1387,13 +1389,13 @@ msgid ""
"Internet asks for your DNS name, they will get a response with your current "
"IP address."
msgstr ""
-"La solution consiste à assigner un nom DNS à votre adresse IP et "
-"d’actualiser le nom DNS à chaque fois que votre IP est modifiée par votre "
-"fournisseur d'accès. Le système de DNS dynamique vous permet de publier "
-"votre adresse IP courante vers un serveur GnuDIP. Le serveur se charge ensuite "
-"d’assigner votre nom DNS à la nouvelle IP, de sorte que si quelqu’un sur "
-"Internet demande votre nom DNS, il obtiendra bien votre adresse IP courante."
+"La solution consiste à assigner un nom DNS à votre adresse IP et d’"
+"actualiser le nom DNS à chaque fois que votre IP est modifiée par votre "
+"fournisseur d’accès. Le système de DNS dynamique vous permet de publier "
+"votre adresse IP courante vers un serveur GnuDIP. Le serveur se charge ensuite d’assigner "
+"votre nom DNS à la nouvelle IP, de sorte que si quelqu’un sur Internet "
+"demande votre nom DNS, il obtiendra bien votre adresse IP courante."
#: plinth/modules/dynamicdns/__init__.py:55
msgid "Dynamic DNS Client"
@@ -1450,7 +1452,7 @@ msgid ""
"If this option is selected, your username and password will be used for HTTP "
"basic authentication."
msgstr ""
-"Si ce choix est sélectionné, votre nom d'utilisateur et votre mot de passe "
+"Si ce choix est sélectionné, votre nom d’utilisateur et votre mot de passe "
"seront utilisés pour une authentification HTTP de base."
#: plinth/modules/dynamicdns/forms.py:51
@@ -1467,8 +1469,8 @@ msgid ""
msgstr ""
"Paramètre optionnel. Entrer l’URL d’un service qui détecte automatiquement "
"votre adresse IP publique réelle et la retourne en réponse (par exemple : "
-"http://myip.datasystems24.de). Cette URL sera utilisée si votre {box_name} "
-"n'est pas connectée directement à Internet (par exemple lorsqu’elle est "
+"http://myip.datasystems24.de). Cette URL sera utilisée si votre {box_name} n’"
+"est pas connectée directement à Internet (par exemple lorsqu’elle est "
"connectée à un routeur à translation d’adresse réseau « NAT »)."
#: plinth/modules/dynamicdns/forms.py:62
@@ -1478,17 +1480,15 @@ msgstr ""
#: plinth/modules/dynamicdns/forms.py:65
msgid "GnuDIP"
-msgstr ""
+msgstr "GnuDIP"
#: plinth/modules/dynamicdns/forms.py:68
-#, fuzzy
-#| msgid "Update URL"
msgid "other update URL"
-msgstr "URL d’actualisation"
+msgstr "autre URL d’actualisation"
#: plinth/modules/dynamicdns/forms.py:70
msgid "Enable Dynamic DNS"
-msgstr "Activer le DNS Dynamique"
+msgstr "Activer le DNS dynamique"
#: plinth/modules/dynamicdns/forms.py:73
msgid "Service Type"
@@ -1496,7 +1496,7 @@ msgstr "Type de service"
#: plinth/modules/dynamicdns/forms.py:78
msgid "GnuDIP Server Address"
-msgstr "Adresse Serveur GnuDIP"
+msgstr "Adresse du serveur GnuDIP"
#: plinth/modules/dynamicdns/forms.py:81
msgid "Invalid server name"
@@ -1553,10 +1553,10 @@ msgid ""
"freedns.afraid.org."
msgstr ""
"Si vous cherchez un compte DNS dynamique gratuit, vous pourrez sans doute "
-"trouver un service compatible GnuDIP sur gnudip.datasystems24.net ou un "
-"service à base d'URL d’actualisation sur freedns.afraid.org."
+"trouver un service compatible GnuDIP sur gnudip.datasystems24.net ou un "
+"service à base d’URL d’actualisation sur freedns.afraid.org."
#: plinth/modules/dynamicdns/templates/dynamicdns.html:23
#, python-format
@@ -1565,10 +1565,10 @@ msgid ""
"port forwarding for standard ports, including TCP port 80 (HTTP) and TCP "
"port 443 (HTTPS)."
msgstr ""
-"Si votre %(box_name)s est connectée derrière un routeur à translation "
-"d’adresse réseau (NAT), n'oubliez pas d'ajouter une redirection pour les "
-"ports 80 (serveur http) et 443 (serveur sécurisé https) vers l'IP de votre "
-"%(box_name)s. La redirection se configure sur l'interface web de votre "
+"Si votre %(box_name)s est connectée derrière un routeur à translation d’"
+"adresse réseau (NAT), n’oubliez pas d’ajouter une redirection pour les ports "
+"80 (serveur http) et 443 (serveur sécurisé https) vers l’IP de votre "
+"%(box_name)s. La redirection se configure sur l’interface web de votre "
"routeur."
#: plinth/modules/dynamicdns/templates/dynamicdns_configure.html:15
@@ -1577,7 +1577,7 @@ msgid ""
"functions may not work (but the main functionality should work)."
msgstr ""
"Vous avez désactivé Javascript. Le mode formulaire dynamique est désactivé, "
-"quelques fonctions d'aide pourraient ne pas marcher (mais la fonctionnalité "
+"quelques fonctions d’aide pourraient ne pas marcher (mais la fonctionnalité "
"principale devrait fonctionner)."
#: plinth/modules/dynamicdns/templates/dynamicdns_status.html:9
@@ -1605,12 +1605,12 @@ msgid ""
"for this, otherwise IP changes will not be detected). In case the WAN IP "
"changes, it may take up to %(timer)s minutes until your DNS entry is updated."
msgstr ""
-"Derrière un NAT. Cela signifie que votre fournisseur d'URL pour DNS "
-"dynamique sondera « l'URL pour rechercher l'IP publique » afin de détecter "
-"d'éventuels changements (c'est pour cela que « l'URL pour rechercher l'IP "
-"publique » est nécessaire, sinon les changements d'adresse IP ne seront pas "
+"Derrière un NAT. Cela signifie que votre fournisseur d’URL pour DNS "
+"dynamique sondera « l’URL pour rechercher l’IP publique » afin de détecter d’"
+"éventuels changements (c’est pour cela que « l’URL pour rechercher l’IP "
+"publique » est nécessaire, sinon les changements d’adresse IP ne seront pas "
"détectés). Cela peut prendre %(timer)s minutes avant la mise à jour de votre "
-"entrée DNS en cas de modification de l'IP publique."
+"entrée DNS en cas de modification de l’IP publique."
#: plinth/modules/dynamicdns/templates/dynamicdns_status.html:33
msgid "Last update"
@@ -1860,6 +1860,9 @@ msgid ""
"Advanced firewall operations such as opening custom ports are provided by "
"the Cockpit app."
msgstr ""
+"Les opérations de pare-feu avancées telles que l’ouverture de ports "
+"personnalisés sont accessibles dans l’appli Cockpit."
#: plinth/modules/first_boot/forms.py:14
#, python-brace-format
@@ -1930,8 +1933,8 @@ msgid ""
"To learn more on how to use Git visit Git tutorial."
msgstr ""
-"Pour en apprendre plus sur l'utilisation de Git, consultez ce tutoriel Git."
+"Pour en apprendre plus sur l’utilisation de Git, consultez ce tutoriel Git."
#: plinth/modules/gitweb/__init__.py:51
msgid "Read-write access to Git repositories"
@@ -1976,7 +1979,7 @@ msgstr "Dépôt privé"
#: plinth/modules/gitweb/forms.py:92
msgid "Allow only authorized users to access this repository."
-msgstr "Ne permet l'accès à ce dépôt qu'aux utilisateurs autorisés."
+msgstr "Ne permet l’accès à ce dépôt qu’aux utilisateurs autorisés."
#: plinth/modules/gitweb/forms.py:113 plinth/modules/gitweb/forms.py:155
msgid "A repository with this name already exists."
@@ -1991,14 +1994,12 @@ msgid "An alpha-numeric string that uniquely identifies a repository."
msgstr "Une chaîne alpha-numérique qui identifie de manière unique le dépôt."
#: plinth/modules/gitweb/forms.py:134
-#, fuzzy
-#| msgid "Default Skin"
msgid "Default branch"
-msgstr "Thème par défaut"
+msgstr "Branche par défaut"
#: plinth/modules/gitweb/forms.py:135
msgid "Gitweb displays this as a default branch."
-msgstr ""
+msgstr "Gitweb affiche cette branche comme branche par défaut."
#: plinth/modules/gitweb/manifest.py:21
msgid "Git"
@@ -2070,7 +2071,7 @@ msgstr "{name} supprimé."
#: plinth/modules/gitweb/views.py:142
#, python-brace-format
msgid "Could not delete {name}: {error}"
-msgstr "La suppression de {name} n'a pas abouti : {error}"
+msgstr "La suppression de {name} a échoué : {error}"
#: plinth/modules/help/__init__.py:33
msgid "Documentation"
@@ -2078,11 +2079,9 @@ msgstr "Documentation"
#: plinth/modules/help/__init__.py:37 plinth/templates/help-menu.html:20
#: plinth/templates/help-menu.html:21 plinth/templates/index.html:128
-#, fuzzy
-#| msgid "Manual"
msgctxt "User guide"
msgid "Manual"
-msgstr "Manuel"
+msgstr "Manuel utilisateur"
#: plinth/modules/help/__init__.py:41
#: plinth/modules/help/templates/help_support.html:9
@@ -2096,7 +2095,7 @@ msgstr "Obtenir de l’aide"
#: plinth/modules/help/views.py:36 plinth/templates/help-menu.html:33
#: plinth/templates/help-menu.html:34
msgid "Submit Feedback"
-msgstr "Faire un retour d'utilisation"
+msgstr "Partagez vos impressions"
#: plinth/modules/help/__init__.py:49
#: plinth/modules/help/templates/help_contribute.html:9
@@ -2153,7 +2152,7 @@ msgid ""
"package."
msgstr ""
"Il existe de nombreux projets dont le but est de développer une architecture "
-"distribuée de l'Internet. %(box_name)s vise à intégrer ces différents "
+"distribuée de l’Internet. %(box_name)s vise à intégrer ces différents "
"projets dans un système unique et facile à utiliser."
#: plinth/modules/help/templates/help_about.html:51
@@ -2162,7 +2161,7 @@ msgid ""
"For more information about the %(box_name)s project, see the %(box_name)s Wiki."
msgstr ""
-"Pour plus d'informations sur le projet %(box_name)s, visitez le Wiki %(box_name)s."
#: plinth/modules/help/templates/help_about.html:60
@@ -2249,9 +2248,9 @@ msgid ""
"improve them on our discussion forum."
msgstr ""
-"Faites-nous part de fonctionnalités qui vous manquent, de vos applications "
-"favorites et de vos idées pour les améliorer sur notre forum de discussions."
+"Faites-nous part des fonctionnalités qui vous manquent, de vos applications "
+"favorites et de vos idées pour les améliorer sur notre forum de discussions."
#: plinth/modules/help/templates/help_feedback.html:26
msgid ""
@@ -2263,9 +2262,9 @@ msgstr ""
"Si vous rencontrez des bogues ou des problèmes, veuillez utiliser notre outil de suivi de tickets (en anglais) pour que nos "
-"développeurs en soient informés. Avant cela, vérifiez que le problème n'a "
-"pas déjà été remonté auparavant, et si ce n’est pas le cas cliquez sur le "
-"bouton « Nouveau ticket »."
+"développeurs en soient informés. Vérifiez tout d’abord que votre problème n’"
+"a pas déjà été remonté, et si tel n’est pas le cas cliquez sur le bouton « "
+"Nouveau ticket »."
#: plinth/modules/help/templates/help_feedback.html:36
msgid "Thank you!"
@@ -2292,7 +2291,7 @@ msgid ""
"%(box_name)s project wiki contains further information."
msgstr ""
"Le wiki "
-"du projet %(box_name)scontient davantage d'informations."
+"du projet %(box_name)scontient davantage d’informations."
#: plinth/modules/help/templates/help_index.html:30
#, python-format
@@ -2302,7 +2301,7 @@ msgid ""
"\"> mailing list. The list archives also contain information about "
"problems faced by other users and possible solutions."
msgstr ""
-"Pour obtenir de l'aide de la communauté %(box_name)s, vous pouvez poser vos "
+"Pour obtenir de l’aide de la communauté %(box_name)s, vous pouvez poser vos "
"questions en anglais sur la liste de discussion du projet. "
"Les archives de la liste contiennent aussi des informations sur les "
@@ -2319,9 +2318,9 @@ msgid ""
msgstr ""
"De nombreux contributeurs et utilisateurs de %(box_name)s sont également "
"présents sur le réseau IRC irc.oftc.net. Rejoignez le canal (en anglais) #freedombox en utilisant "
-"l'interface web pour IRC et demandez de l'aide."
+"href=\"https://webchat.oftc.net/?randomnick=1&channels=freedombox&prompt=1\""
+"> #freedombox en utilisant l’interface web pour IRC et demandez de "
+"l’aide."
#: plinth/modules/help/templates/help_manual.html:25
msgid "Download as PDF"
@@ -2335,7 +2334,7 @@ msgid ""
"contributors."
msgstr ""
"Si vous avez besoin d’aide pour réaliser une action ou si vous faites face à "
-"un problème en utilisant la %(box_name)s, vous pouvez demander de l'aide à "
+"un problème en utilisant la %(box_name)s, vous pouvez demander de l’aide à "
"la communauté d’utilisateurs et de contributeurs."
#: plinth/modules/help/templates/help_support.html:20
@@ -2362,7 +2361,7 @@ msgstr ""
#: plinth/modules/help/templates/statuslog.html:10
msgid "Status Log"
-msgstr "Journal d'état"
+msgstr "Journal d’état"
#: plinth/modules/help/templates/statuslog.html:13
#, python-format
@@ -2430,7 +2429,7 @@ msgstr ""
#: plinth/modules/i2p/__init__.py:56
msgid "Manage I2P application"
-msgstr "Gérer l'application I2P"
+msgstr "Gérer l’application I2P"
#: plinth/modules/i2p/__init__.py:59 plinth/modules/i2p/manifest.py:16
msgid "I2P"
@@ -2769,8 +2768,8 @@ msgid ""
"Certificate successfully revoked for domain {domain}.This may take a few "
"moments to take effect."
msgstr ""
-"Certificat révoqué avec succès pour le domaine {domain}. Cette modification "
-"peut nécessiter quelques instants avant de prendre effet."
+"Le certificat du domaine {domain} a été révoqué. Cette modification peut "
+"nécessiter quelques instants avant de prendre effet."
#: plinth/modules/letsencrypt/views.py:47
#, python-brace-format
@@ -2782,7 +2781,7 @@ msgstr ""
#: plinth/modules/letsencrypt/views.py:77
#, python-brace-format
msgid "Certificate successfully obtained for domain {domain}"
-msgstr "Certificat obtenu avec succès pour le domaine {domain}"
+msgstr "Le certificat pour le domaine {domain} a été obtenu avec succès"
#: plinth/modules/letsencrypt/views.py:65
#: plinth/modules/letsencrypt/views.py:82
@@ -2793,7 +2792,7 @@ msgstr "Échec de l'obtention du certificat pour le domaine {domain} : {error}"
#: plinth/modules/letsencrypt/views.py:94
#, python-brace-format
msgid "Certificate successfully deleted for domain {domain}"
-msgstr "Certificat supprimé avec succès pour le domaine {domain}"
+msgstr "Le certificat pour le domaine {domain} a été supprimé"
#: plinth/modules/letsencrypt/views.py:99
#, python-brace-format
@@ -2820,19 +2819,14 @@ msgstr ""
"autres serveurs Matrix grâce la fédération."
#: plinth/modules/matrixsynapse/__init__.py:40
-#, fuzzy
-#| msgid ""
-#| "To communicate, you can use the available clients for mobile, desktop and the web. Riot client is recommended."
msgid ""
"To communicate, you can use the available clients for mobile, desktop and the web. Element client is recommended."
msgstr ""
-"Pour communiquer, vous pouvez utiliser les clients disponibles pour mobile, ordinateur de bureau et "
-"web. Le client Riot est recommandé."
+"Pour communiquer, vous pouvez utiliser un des clients disponibles pour mobile, ordinateur de bureau "
+"et web. Le client Element est recommandé."
#: plinth/modules/matrixsynapse/__init__.py:67
msgid "Matrix Synapse"
@@ -2854,7 +2848,7 @@ msgstr ""
#: plinth/modules/matrixsynapse/manifest.py:13
msgid "Element"
-msgstr ""
+msgstr "Element"
#: plinth/modules/matrixsynapse/templates/matrix-synapse-pre-setup.html:15
#: plinth/modules/snapshot/templates/snapshot.html:12
@@ -3171,7 +3165,7 @@ msgstr "Serveur de streaming de médias"
#: plinth/modules/minidlna/__init__.py:47
msgid "MiniDLNA"
-msgstr ""
+msgstr "MiniDLNA"
#: plinth/modules/minidlna/__init__.py:48
msgid "Simple Media Server"
@@ -3453,9 +3447,10 @@ msgid ""
"href=\"http://mumble.info\">Clients to connect to Mumble from your "
"desktop and Android devices are available."
msgstr ""
-"Vous pouvez vous connecter au serveur Mumble sur un port normal Mumble "
-"64738. Il existe des clients permettant "
-"de se connecter à Mumble depuis un ordinateur ou un appareil Android."
+"Vous pouvez vous connecter au serveur Mumble sur le port Mumble habituel "
+"64738. Utilisez l’un des clients "
+"permettant de se connecter à Mumble depuis votre ordinateur ou votre "
+"appareil Android."
#: plinth/modules/mumble/__init__.py:43 plinth/modules/mumble/manifest.py:12
msgid "Mumble"
@@ -3492,7 +3487,7 @@ msgstr "Mumla"
#: plinth/modules/mumble/views.py:27
msgid "SuperUser password successfully updated."
-msgstr "Mot de passe du super utilisateur mis à jour avec succès."
+msgstr "Le mot de passe du super utilisateur a été mis à jour."
#: plinth/modules/names/__init__.py:26
#, python-brace-format
@@ -3521,10 +3516,8 @@ msgid "All web apps"
msgstr "Toutes les applications web"
#: plinth/modules/names/templates/names.html:16
-#, fuzzy
-#| msgid "Service"
msgid "Services"
-msgstr "Service"
+msgstr "Services"
#: plinth/modules/networks/__init__.py:40
msgid ""
@@ -3615,11 +3608,9 @@ msgid "Shared"
msgstr "Partagée"
#: plinth/modules/networks/forms.py:48 plinth/modules/networks/forms.py:79
-#, fuzzy
-#| msgid "Manual"
msgctxt "Not automatically"
msgid "Manual"
-msgstr "Manuel"
+msgstr "Manuelle"
#: plinth/modules/networks/forms.py:54
msgid "Netmask"
@@ -3874,15 +3865,7 @@ msgstr ""
"choisir cette option.
"
#: plinth/modules/networks/forms.py:360
-#, fuzzy, python-brace-format
-#| msgid ""
-#| "I have a public IP address that does not change overtime (recommended)This means that devices on the Internet can reach "
-#| "you when you are connected to the Internet. Every time you connect to the "
-#| "Internet with your Internet Service Provider (ISP), you always get the "
-#| "same IP address. This is the most trouble-free setup for many {box_name} "
-#| "services but very few ISPs offer this. You may be able to get this "
-#| "service from your ISP by making an additional payment.
"
+#, python-brace-format
msgid ""
"I have a public IP address that does not change over time (recommended)This means that devices on the Internet can reach you "
@@ -3892,13 +3875,13 @@ msgid ""
"but very few ISPs offer this. You may be able to get this service from your "
"ISP by making an additional payment.
"
msgstr ""
-"J’ai une adresse IP publique ne changeant pas avec le temps (recommandé)Cela signifie que les appareils sur Internet peuvent "
-"vous joindre quand vous êtes connecté à Internet. À chaque connexion avec "
-"votre fournisseur d’accès à Internet (FAI), vous avez toujours la même "
-"adresse IP. Il s’agit de la situation la moins problématique pour beaucoup "
-"des services de la {box_name} mais très peu de FAI proposent cela. Il est "
-"possible que votre FAI propose cette option moyennant un surcoût.
"
+"J’ai toujours la même adresse IP publique statique (recommandé)Cela signifie que les appareils sur Internet peuvent vous "
+"joindre quand vous êtes connecté à Internet. À chaque connexion avec votre "
+"fournisseur d’accès à Internet (FAI), vous avez toujours la même adresse IP. "
+"Il s’agit de la situation la moins problématique pour beaucoup des services "
+"de la {box_name} mais très peu de FAI le proposent. Il se peut que votre FAI "
+"propose cette option moyennant un surcoût.
"
#: plinth/modules/networks/forms.py:373
#, python-brace-format
@@ -3978,10 +3961,10 @@ msgid ""
"have not configured or are unable to configure the router currently and wish "
"to be reminded later. Some of the other configuration steps may fail."
msgstr ""
-"Le routeur n’est pas configuré actuellement Choisissez cette option si vous n’avez pas encore pu configurer le "
-"routeur et souhaitez vous faire rappeler de le configurer plus tard. Il se "
-"peut que d’autres étapes de configuration échouent.
"
+"Le routeur n’est pas configuré actuellement Choisissez cette option si vous n’avez pas encore pu configurer le routeur "
+"et souhaitez vous faire rappeler de le configurer ultérieurement. Il se peut "
+"que d’autres étapes de configuration échouent.
"
#: plinth/modules/networks/templates/connection_show.html:28
msgid "Edit connection"
@@ -4132,7 +4115,7 @@ msgstr "Sécurité"
#: plinth/modules/networks/templates/connection_show.html:276
#: plinth/modules/networks/templates/connection_show.html:295
msgid "Firewall zone"
-msgstr "Zone pare-feu"
+msgstr "Zone de pare-feu"
#: plinth/modules/networks/templates/connection_show.html:265
msgid ""
@@ -4162,6 +4145,8 @@ msgid ""
"This interface is not maintained by %(box_name)s. For security, it is "
"automatically assigned to the external zone."
msgstr ""
+"Cette interface n’est pas gérée par la %(box_name)s. Pour des raisons de "
+"sécurité elle est assignée automatiquement à la zone externe."
#: plinth/modules/networks/templates/connections_create.html:19
msgid "Create Connection"
@@ -4314,7 +4299,7 @@ msgstr ""
#: plinth/modules/networks/templates/internet_connectivity_main.html:41
#: plinth/modules/networks/templates/network_topology_main.html:41
msgid "Update..."
-msgstr "Actualiser…"
+msgstr "Modifier…"
#: plinth/modules/networks/templates/network_topology_content.html:10
#, python-format
@@ -4365,7 +4350,7 @@ msgid ""
"connect to %(box_name)s for their Internet connectivity."
msgstr ""
"Votre %(box_name)s est directement connectée à Internet et tous vos "
-"appareils se connectent à la {box_name}s pour leur connectivité Internet."
+"appareils passent par la %(box_name)s pour leur connectivité Internet."
#: plinth/modules/networks/templates/network_topology_main.html:34
#, python-format
@@ -4381,6 +4366,10 @@ msgid ""
"Advanced networking operations such as bonding, bridging and VLAN management "
"are provided by the Cockpit app."
msgstr ""
+"Les opérations de réseau avancées telles que l’agrégation de liens (« "
+"bonding »), la création de ponts (« bridging ») et la gestion de réseaux "
+"virtuels VLAN sont accessibles dans l’appli Cockpit."
#: plinth/modules/networks/templates/router_configuration_content.html:10
#, python-format
@@ -4604,11 +4593,12 @@ msgid ""
"available for most platforms. Click \"Learn more...\" above for recommended "
"clients and instructions on how to configure them."
msgstr ""
-"Pour vous connecter au réseau VPN de la %(box_name)s, vous devez télécharger "
-"un profil et le fournir à un client OpenVPN sur votre mobile ou sur votre "
-"ordinateur. Les clients OpenVPN sont disponibles pour de nombreuses plate-"
-"formes. Cliquez sur « Application clientes » ci-dessus pour connaître les "
-"clients recommandés et leurs instructions de configuration."
+"Pour vous connecter au réseau privé virtuel VPN de la %(box_name)s, vous "
+"devez télécharger un profil et le fournir à un client OpenVPN sur votre "
+"mobile ou sur votre ordinateur. Les clients OpenVPN sont disponibles pour "
+"de nombreuses plate-formes. Cliquez sur « Application clientes » ci-dessus "
+"pour connaître les clients recommandés et leurs instructions de "
+"configuration."
#: plinth/modules/openvpn/templates/openvpn.html:74
#, python-format
@@ -4853,16 +4843,22 @@ msgid ""
"utilization of the hardware. This can give you basic insights into usage "
"patterns and whether the hardware is overloaded by users and services."
msgstr ""
+"L’appli Performance vous permet de collecter, stocker et visualiser les "
+"statistiques d’utilisation de la machine. Elle peut vous donner un aperçu "
+"des tendances d’utilisation et vérifier si la machine est surchargée par "
+"certains utilisateurs ou certains services."
#: plinth/modules/performance/__init__.py:29
msgid ""
"Performance metrics are collected by Performance Co-Pilot and can be viewed "
"using the Cockpit app."
msgstr ""
+"Les métriques de performance sont collectées par le Co-Pilote « Performance »"
+" et visualisables dans l’appli Cockpit."
#: plinth/modules/performance/__init__.py:46
msgid "System Monitoring"
-msgstr "Contrôle Système"
+msgstr "Surveillance du système"
#: plinth/modules/power/__init__.py:16
msgid "Restart or shut down the system."
@@ -5016,12 +5012,7 @@ msgid "Quasseldroid"
msgstr "Quasseldroid"
#: plinth/modules/radicale/__init__.py:29
-#, fuzzy, python-brace-format
-#| msgid ""
-#| "Radicale is a CalDAV and CardDAV server. It allows synchronization and "
-#| "sharing of scheduling and contact data. To use Radicale, a supported client application is "
-#| "needed. Radicale can be accessed by any user with a {box_name} login."
+#, python-brace-format
msgid ""
"Radicale is a CalDAV and CardDAV server. It allows synchronization and "
"sharing of scheduling and contact data. To use Radicale, a is needed. Radicale can be accessed by any user with a "
"{box_name} login."
msgstr ""
-"Radicale est un serveur pour les protocoles d’édition de calendrier en ligne "
-"CalDAV et de carnet d'adresses CardDAV. Il permet de synchroniser et de "
-"partager ses rendez-vous et ses contacts. Pour utiliser Radicale, une application client compatible est "
-"nécessaire. N’importe quel utilisateur disposant d’un compte sur la "
-"{box_name} peut accéder à Radicale."
+"Radicale est un serveur pour les protocoles de gestion de calendriers en "
+"ligne CalDAV et de carnets d’adresses CardDAV. Il permet de synchroniser ses "
+"rendez-vous et ses contacts entre plusieurs appareils et de les partager. "
+"Pour utiliser Radicale, vous devrez passer par une application cliente "
+"compatible. Tous les utilisateur disposant d’un compte sur la {box_name} "
+"ont accès à Radicale."
#: plinth/modules/radicale/__init__.py:35
msgid ""
@@ -5082,10 +5074,8 @@ msgstr ""
"visualiser et modifier n’importe quel agenda/carnet d’adresses."
#: plinth/modules/radicale/forms.py:30
-#, fuzzy
-#| msgid "Access Point"
msgid "Access rights"
-msgstr "Point d’accès"
+msgstr "Droits d’accès"
#: plinth/modules/radicale/manifest.py:10
msgid "DAVx5"
@@ -5251,7 +5241,7 @@ msgstr "Client Android Samba"
#: plinth/modules/samba/manifest.py:28
msgid "Ghost Commander - Samba plugin"
-msgstr ""
+msgstr "Module Samba pour Ghost Commander"
#: plinth/modules/samba/manifest.py:42
msgid "VLC media player"
@@ -5275,8 +5265,8 @@ msgid ""
"Note: Only specially created directories will be shared on selected disks, "
"not the whole disk."
msgstr ""
-"Note : seuls les répertoires créés spécialement pour seront partagés sur les "
-"disques sélectionnés, et non pas les disques entiers."
+"Remarque : seuls les répertoires créés spécialement pour seront partagés sur "
+"les disques sélectionnés, et non pas les disques entiers."
#: plinth/modules/samba/templates/samba.html:34
msgid "Disk Name"
@@ -5460,12 +5450,12 @@ msgstr "Afficher le rapport de sécurité"
#: plinth/modules/upgrades/templates/backports-firstboot.html:11
#: plinth/modules/upgrades/templates/upgrades_configure.html:11
msgid "Frequent Feature Updates"
-msgstr ""
+msgstr "Mise à jour régulière des fonctionnalités"
#: plinth/modules/security/templates/security.html:19
#: plinth/modules/upgrades/templates/upgrades_configure.html:19
msgid "Frequent feature updates are activated."
-msgstr ""
+msgstr "La mise à jour régulière des fonctionnalités est activée."
#: plinth/modules/security/templates/security.html:24
#: plinth/modules/upgrades/templates/backports-firstboot.html:14
@@ -5480,6 +5470,14 @@ msgid ""
"Instead, they are maintained by contributors to Debian and the %(box_name)s "
"community."
msgstr ""
+"En activant cette option, le système %(box_name)s et un nombre limité de "
+"logiciels recevront des mises à jours plus fréquentes (via le dépôt de "
+"rétroportage « backports ») pouvant inclure de nouvelles fonctionnalités. "
+"Ceci vous permet de bénéficier de nouvelles fonctionnalités au bout de "
+"quelques semaines plutôt que tous les 2 ans environ. Veuillez noter que les "
+"logiciels bénéficiant de ces mises à jours fréquentes ne sont pas supportés "
+"par l’équipe Debian en charge de la sécurité. Ils sont simplement maintenus "
+"par les contributeurs de Debian et la communauté %(box_name)s."
#: plinth/modules/security/templates/security_report.html:10
#: plinth/modules/security/views.py:74
@@ -5880,16 +5878,12 @@ msgid "Yearly Snapshots Limit"
msgstr "Instantanés annuels à conserver"
#: plinth/modules/snapshot/forms.py:49
-#, fuzzy
-#| msgid ""
-#| "Keep a maximum of this many yearly snapshots. The default value is 0 "
-#| "(disabled)."
msgid ""
"Keep a maximum of this many yearly snapshots. The default value is 0 (keep "
"no yearly snapshot)."
msgstr ""
"Conserver au maximum ce nombre d’instantanés pris tous les ans. La valeur "
-"par défaut est de 0 (désactivé)."
+"par défaut est de 0 (ne pas conserver d’instantané annuel)."
#: plinth/modules/snapshot/templates/snapshot_delete_selected.html:12
msgid "Delete the following snapshots permanently?"
@@ -6264,6 +6258,9 @@ msgid ""
"Advanced storage operations such as disk partitioning and RAID management "
"are provided by the Cockpit app."
msgstr ""
+"Les opérations avancées sur le stockage telles que le partitionnement de "
+"disques et la gestion de grappes de disques RAID sont accessible dans l’"
+"appli Cockpit."
#: plinth/modules/storage/templates/storage_expand.html:14
#, python-format
@@ -6283,7 +6280,7 @@ msgstr "Erreur lors de l’extension de la partition : {exception}"
#: plinth/modules/storage/views.py:73
msgid "Partition expanded successfully."
-msgstr "Partition étendue avec succès."
+msgstr "Extension de la partition réalisée avec succès."
#: plinth/modules/storage/views.py:91
#, python-brace-format
@@ -6694,11 +6691,12 @@ msgid ""
"to be unavailable briefly. If system reboot is deemed necessary, it is done "
"automatically at 02:00 causing all apps to be unavailable briefly."
msgstr ""
-"Les mises à jour sont exécutées à 6h00 tous les jours, selon votre heure "
-"locale. Veuillez chosir votre fuseau horaire dans l'application Date & Time. "
-"Les applications redémarrent après la mise à jour, causant brièvement leurs "
-"interruptions. Si le redémarrage du système est nécessaire, il sera effectué "
-"à 2h00, causant brièvement l'interruption de toutes les applications."
+"Les mises à jour sont exécutées à 6h00 heure locale tous les jours. Vous "
+"pouvez définir votre fuseau horaire dans l’application Date et heure. Les "
+"applications sont redémarrées après leur mise à jour ce qui les rend "
+"brièvement indisponibles. Lorsque qu’un redémarrage du système est "
+"nécessaire, il est effectué à 2h00, rendant indisponible l’ensemble des "
+"applications pour une courte période."
#: plinth/modules/upgrades/__init__.py:64 plinth/templates/setup.html:62
msgid "Update"
@@ -6721,19 +6719,24 @@ msgstr ""
#: plinth/modules/upgrades/forms.py:20
#: plinth/modules/upgrades/templates/upgrades_configure.html:56
msgid "Activate frequent feature updates (recommended)"
-msgstr ""
+msgstr "Activer la mise à jour régulière des fonctionnalités (recommandé)"
#: plinth/modules/upgrades/templates/backports-firstboot.html:26
msgid ""
"It is strongly recommended to activate frequent feature updates. If not "
"activated now, they can be activated later."
msgstr ""
+"Il est fortement recommandé d’activer la mise à jour régulière des "
+"fonctionnalités. Si vous choisissez de ne pas l’activez maintenant, vous "
+"pourrez toujours le faire par la suite."
#: plinth/modules/upgrades/templates/backports-firstboot.html:33
msgid ""
"Note: Once frequent feature updates are activated, they "
"cannot be deactivated."
msgstr ""
+"Remarque : Une fois activée, la mise à jour régulière des "
+"fonctionnalités ne peut plus être désactivée."
#: plinth/modules/upgrades/templates/upgrades-new-release.html:9
#, python-format
@@ -6758,12 +6761,16 @@ msgstr "Fermer"
msgid ""
"Frequent feature updates can be activated. Activating them is recommended."
msgstr ""
+"Vous pouvez activer la mise à jour régulière des fonctionnalités. Cette "
+"option est recommandée."
#: plinth/modules/upgrades/templates/upgrades_configure.html:23
msgid ""
"Frequent feature updates cannot be activated. They may not be necessary on "
"your distribution."
msgstr ""
+"La mise à jour régulière des fonctionnalités n’est pas disponibles. Elle n’"
+"est sans doute pas nécessaire pour votre distribution."
#: plinth/modules/upgrades/templates/upgrades_configure.html:45
#, python-format
@@ -6772,10 +6779,12 @@ msgid ""
"cannot be deactivated. You may wish to take a snapshot using Storage Snapshots before continuing."
msgstr ""
+"Attention ! Une fois activée, la mise à jour régulière des "
+"fonctionnalités ne peut plus être désactivée. Il est recommandé de prendre "
+"un instantané du système via la fonction d’Instantanés de disque avant de poursuivre."
#: plinth/modules/upgrades/templates/upgrades_configure.html:61
-#, fuzzy
-#| msgid "Manual update"
msgid "Manual Update"
msgstr "Mise à jour manuelle"
@@ -6785,29 +6794,22 @@ msgstr "Mise à jour en cours…"
#: plinth/modules/upgrades/templates/upgrades_configure.html:75
msgid "Update now"
-msgstr "Mettre à jour immédiatement"
+msgstr "Lancer la mise à jour"
#: plinth/modules/upgrades/templates/upgrades_configure.html:81
-#, fuzzy
-#| msgid ""
-#| "This may take a long time to complete. During an update, "
-#| "you cannot install apps. Also, this web interface may be temporarily "
-#| "unavailable and show an error. In that case, refresh the page to continue."
msgid ""
"This may take a long time to complete. During an update, "
"you cannot install apps. Also, this web interface may be temporarily "
"unavailable and show an error. In that case, refresh the page to continue."
msgstr ""
"Cette opération peut prendre du temps. Pendant la durée de "
-"la mise à jour, vous ne pouvez pas installer d’autre appli. Cette interface "
-"web pourrait aussi être temporairement indisponible ou afficher une erreur. "
-"Si cela se produit, rafraîchissez la page pour continuer."
+"la mise à jour, vous ne pourrez pas installer d’autre appli. Cette interface "
+"web pourrait aussi devenir temporairement indisponible ou afficher une "
+"erreur. Si cela se produit, rafraîchissez la page pour continuer."
#: plinth/modules/upgrades/templates/upgrades_configure.html:95
-#, fuzzy
-#| msgid "Toggle recent update logs"
msgid "Show recent update logs"
-msgstr "Basculer les journaux de modification récents"
+msgstr "Afficher les derniers journaux de mises à jour"
#: plinth/modules/upgrades/views.py:57
#, python-brace-format
@@ -6826,7 +6828,7 @@ msgstr "Mises à niveau automatiques désactivées"
#: plinth/modules/upgrades/views.py:79
msgid "Upgrade process started."
-msgstr "Mise à niveau initiée."
+msgstr "Mise à jour lancée."
#: plinth/modules/upgrades/views.py:81
msgid "Starting upgrade failed."
@@ -6834,7 +6836,7 @@ msgstr "Le lancement de la mise à niveau a échoué."
#: plinth/modules/upgrades/views.py:91
msgid "Frequent feature updates activated."
-msgstr ""
+msgstr "Mise à jour régulière des fonctionnalités activée."
#: plinth/modules/users/__init__.py:39
msgid ""
@@ -7074,7 +7076,7 @@ msgstr "Changer Mot de Passe"
#: plinth/modules/users/views.py:147
msgid "Password changed successfully."
-msgstr "Mot de passe changé avec succès."
+msgstr "Le mot de passe a été changé."
#: plinth/modules/wireguard/__init__.py:24
msgid "WireGuard is a fast, modern, secure VPN tunnel."
@@ -7086,9 +7088,9 @@ msgid ""
"It can be used to connect to a VPN provider which supports WireGuard, and to "
"route all outgoing traffic from {box_name} through the VPN."
msgstr ""
-"Il peut être utilisé pour se connecter à un fournisseur de VPN proposant un "
-"service WireGuard, pour router tout le trafic sortant de la {box_name} à "
-"travers ce VPN."
+"Il peut être utilisé pour se connecter à un fournisseur de réseau privé "
+"virtuel VPN proposant un service WireGuard, pour router tout le trafic "
+"sortant de la {box_name} à travers ce VPN."
#: plinth/modules/wireguard/__init__.py:30
#, python-brace-format
@@ -7160,9 +7162,9 @@ msgid ""
"endpoint. This value is usually provided by the server operator. Example: "
"192.168.0.10."
msgstr ""
-"Adresse IP attribuée à cette machine sur le VPN une fois connecté au serveur "
-"distant. Cette valeur est généralement fournie par le gestionnaire du "
-"serveur. Par exemple : 192.18.0.10."
+"Adresse IP attribuée à cette machine sur le réseau privé virtuel VPN une "
+"fois connecté au serveur distant. Cette valeur est généralement fournie par "
+"le gestionnaire du serveur. Par exemple : 192.18.0.10."
#: plinth/modules/wireguard/forms.py:89
msgid "Private key of this machine"
@@ -7203,8 +7205,8 @@ msgstr "Utiliser cette connexion pour y envoyer tout le trafic sortant"
#: plinth/modules/wireguard/forms.py:107
msgid "Typically checked for a VPN service though which all traffic is sent."
msgstr ""
-"Activé la plupart du temps pour un service VPN à travers lequel tout le "
-"trafic est envoyé."
+"À activer en général pour un service de réseau privé virtuel VPN à travers "
+"lequel tout le trafic est envoyé."
#: plinth/modules/wireguard/templates/wireguard.html:10
msgid "As a Server"
@@ -7554,7 +7556,7 @@ msgstr "S'identifier"
#: plinth/templates/clients-button.html:16
msgid "Launch web client"
-msgstr "Lancer le client web"
+msgstr "Ouvrir le client web"
#: plinth/templates/clients-button.html:25
msgid "Client Apps"
@@ -7641,10 +7643,10 @@ msgid ""
"server to deploy social applications on small machines. It provides online "
"communication tools respecting your privacy and data ownership."
msgstr ""
-"%(box_name)s, un pur produit de Debian, est un serveur web d’auto-"
-"hébergement 100%% à base de logiciel libre conçu pour déployer des "
-"applications sociales sur de petites machines. Les outils de communication "
-"proposés sont respectueux de votre vie privée et de vos données personnelles."
+"Pur produit de Debian, %(box_name)s est un serveur web d’auto-hébergement "
+"composé à 100%% de logiciels libres, conçu pour déployer des applications "
+"sociales sur de petites machines. Les outils de communication proposés sont "
+"respectueux de votre vie privée et de vos données personnelles."
#: plinth/templates/index.html:117
#, python-format
@@ -7653,7 +7655,7 @@ msgid ""
"free software, distributed under the GNU Affero General Public License, "
"Version 3 or later."
msgstr ""
-"Ce portail fait partie de l'interface web de la %(box_name)s. %(box_name)s "
+"Ce portail fait partie de l’interface web de la %(box_name)s. %(box_name)s "
"est un logiciel libre distribué sous licence GNU Affero General Public "
"License, Version 3 ou ultérieure."
@@ -7682,13 +7684,14 @@ msgid "Mailing list"
msgstr "Liste de discussion du projet"
#: plinth/templates/internal-zone.html:11
-#, fuzzy, python-format
-#| msgid "%(service_name)s is available only on internal networks."
+#, python-format
msgid ""
"%(service_name)s is available only on internal networks or when the "
"client is connected to %(box_name)s through VPN."
msgstr ""
-"%(service_name)s n'est disponible que sur les réseaux internes."
+"%(service_name)s n’est disponible que sur les réseaux internes où "
+"lorsque le client est connecté à la %(box_name)s via un réseau privé virtuel "
+"VPN."
#: plinth/templates/internal-zone.html:17
msgid "Currently there are no network interfaces configured as internal."
@@ -7713,16 +7716,13 @@ msgid "Port Forwarding"
msgstr "Redirection de port"
#: plinth/templates/port-forwarding-info.html:13
-#, fuzzy, python-format
-#| msgid ""
-#| "You may want to check the network setup "
-#| "and modify it if necessary."
+#, python-format
msgid ""
"Your FreedomBox is not behind a router. No "
"action is necessary."
msgstr ""
-"Vous pouvez vérifier la configuration du "
-"réseau et la modifier si nécessaire."
+"Votre FreedomBox n’est pas derrière un routeur"
+"a>. Aucune action n’est nécessaire."
#: plinth/templates/port-forwarding-info.html:19
#, python-format
@@ -7731,37 +7731,34 @@ msgid ""
"are using the DMZ feature to forward all ports. No further router "
"configuration is necessary."
msgstr ""
+"Votre FreedomBox est derrière un routeur "
+"et vous utilisez la fonction de zone démilitarisée DMZ pour rediriger tous "
+"les ports. Aucune configuration supplémentaire du routeur n’est nécessaire."
#: plinth/templates/port-forwarding-info.html:26
-#, fuzzy, python-format
-#| msgid ""
-#| "If your FreedomBox is behind a router, you will need to set up port "
-#| "forwarding on your router. You should forward the following ports for "
-#| "%(service_name)s:"
+#, python-format
msgid ""
"Your FreedomBox is behind a router and you "
"are not using the DMZ feature. You will need to set up port forwarding on "
"your router. You should forward the following ports for %(service_name)s:"
msgstr ""
-"Si votre FreedomBox se situe derrière un routeur, vous aurez besoin de "
-"mettre en place une redirection de port sur votre routeur. Vous devriez "
+"Votre FreedomBox est derrière un routeur et "
+"vous n’utilisez pas la fonction de zone démilitarisée DMZ. Vous allez devoir "
+"mettre en place une redirection de ports sur votre routeur. Vous devriez "
"rediriger les ports suivants pour %(service_name)s :"
#: plinth/templates/port-forwarding-info.html:36
-#, fuzzy
-#| msgid "protocol"
msgid "Protocol"
-msgstr "protocole"
+msgstr "Protocole"
#: plinth/templates/port-forwarding-info.html:37
msgid "From Router/WAN Ports"
-msgstr ""
+msgstr "Depuis les ports routeur/Internet (WAN)"
#: plinth/templates/port-forwarding-info.html:38
-#, fuzzy, python-format
-#| msgid "%(box_name)s Setup"
+#, python-format
msgid "To %(box_name)s Ports"
-msgstr "Configuration initiale de la %(box_name)s"
+msgstr "Vers les ports %(box_name)s"
#: plinth/templates/setup.html:24
msgid "Install this application?"
From f69d8289f64961c85a6dab2f8baf8f2c7d64bfd5 Mon Sep 17 00:00:00 2001
From: Joseph Nuthalapati
Date: Sat, 19 Sep 2020 15:38:41 +0530
Subject: [PATCH 02/46] bepasty: Change default permissions to 'read'
Allow read access by URL by default.
Tests:
- Installing bepasty fresh show the default permissions as read.
- Upgrading bepasty from older version when default permissions are none sets
the default permissions to read.
- Upgrading bepasty from older version when default permissions are not none
retrains the permissions.
Signed-off-by: Joseph Nuthalapati
[sunil: Don't relocate setup() method]
Signed-off-by: Sunil Mohan Adapa
Reviewed-by: Sunil Mohan Adapa
Tested-by: Sunil Mohan Adapa
---
actions/bepasty | 2 +-
plinth/modules/bepasty/__init__.py | 5 ++++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/actions/bepasty b/actions/bepasty
index f6bc5eb46..b8bf179a0 100755
--- a/actions/bepasty
+++ b/actions/bepasty
@@ -148,7 +148,7 @@ def subcommand_setup(arguments):
passwords[1]: 'editor',
passwords[2]: 'viewer',
},
- 'DEFAULT_PERMISSIONS': '',
+ 'DEFAULT_PERMISSIONS': 'read',
}
conf_file_write(conf)
CONF_FILE.chmod(0o640)
diff --git a/plinth/modules/bepasty/__init__.py b/plinth/modules/bepasty/__init__.py
index 597d87513..0fa082e8c 100644
--- a/plinth/modules/bepasty/__init__.py
+++ b/plinth/modules/bepasty/__init__.py
@@ -15,7 +15,7 @@ from plinth.modules.firewall.components import Firewall
from .manifest import backup, clients # noqa, pylint: disable=unused-import
-version = 1
+version = 2
managed_packages = ['bepasty', 'uwsgi', 'uwsgi-plugin-python3']
@@ -98,6 +98,9 @@ def setup(helper, old_version=None):
helper.call('post', actions.superuser_run, 'bepasty',
['setup', '--domain-name', 'freedombox.local'])
helper.call('post', app.enable)
+ if old_version == 1 and not get_configuration().get('DEFAULT_PERMISSIONS'):
+ # Upgrade to a better default only if user hasn't changed the value.
+ set_default_permissions('read')
def get_configuration():
From d588e7fa25c1575aeec78c78436edd9aa140c15c Mon Sep 17 00:00:00 2001
From: Fioddor Superconcentrado
Date: Mon, 14 Sep 2020 19:09:47 +0000
Subject: [PATCH 03/46] HACKING: Add extra development requirements
[sunil: Reword the extra requirements to explain when they are needed]
Signed-off-by: Sunil Mohan Adapa
Reviewed-by: Sunil Mohan Adapa
---
HACKING.md | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/HACKING.md b/HACKING.md
index 12431e4e4..d4c1a9152 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -4,13 +4,23 @@
FreedomBox is built as part of Debian GNU/Linux. However, you don't need to
install Debian to do development for FreedomBox. FreedomBox development is
-typically done on a container or a Virtual Machine.
+typically done with a container or a Virtual Machine.
-* For running a container, you need systemd containers, Git, Python and a
+* For running a container, you need systemd containers, Git, Python3 and a
sudo-enabled user. This approach is recommended.
* For running a VM, you can work on any operating system that can install latest
versions of Git, Vagrant and VirtualBox.
+In addition:
+
+- To run code quality checks you need flake8 and yapf. These may be installed
+ inside the container with Python3 and pip but installing them on host leads to
+ smoother development experience.
+- To update translation strings on the host machine, you need Django and gettext
+ to be installed on host machine. These can be installed with Python3 and pip.
+- You need Mumble voice chat software to participate in bi-weekly live
+ development discussions.
+
## Using Containers
The `./container` script shipped with FreedomBox source code can manage the
@@ -422,7 +432,7 @@ For more information on translations: https://wiki.debian.org/FreedomBox/Transla
## Application Icons
When adding a new App into FreedomBox, an icon is needed to represent the app in
-the application view and for shortcuts in the front page. The following the
+the application view and for shortcuts in the front page. Follow these
guidelines for creating an app icon:
- Use SVG format.
From b527b003a3517578e0cb8feaed7583b367929c1a Mon Sep 17 00:00:00 2001
From: Fioddor Superconcentrado
Date: Wed, 16 Sep 2020 13:59:09 +0000
Subject: [PATCH 04/46] CONTRIBUTING: Require flake8 compliance
Reviewed-by: Sunil Mohan Adapa
---
CONTRIBUTING.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bf0cb8465..9b74d4070 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -31,7 +31,7 @@ Naming conventions:
repository.
* Before committing your changes ensure that your code conforms to base code
hygiene by running tests (see HACKING.md) and the automatic code formatting
- tools `yapf` and `isort`.
+ tools `yapf` and `isort`. Please check also with `flake8`.
* Please include one single feature per merge request, to keep the review simple
and focused on one topic. (This might still mean hundreds of lines of code.)
Use a branch other than `master`, so you can create multiple merge requests
From 75a46962f054d4f5739f11ecc5751c2d1669f7bf Mon Sep 17 00:00:00 2001
From: Sunil Mohan Adapa
Date: Thu, 10 Sep 2020 16:57:08 -0700
Subject: [PATCH 05/46] module_loader, web_framework: Update console log
messages
- Drop message for initialization of each app. This was removed earlier but was
reintroduced during init() refactoring. There is not much use for these messages
and the noise they generate hide some important messages relating to domains
added.
- Print lists in a better way for humans.
- Add a log message after all the initialization is completed.
Signed-off-by: Sunil Mohan Adapa
Reviewed-by: James Valleroy
---
plinth/module_loader.py | 5 ++---
plinth/web_framework.py | 2 +-
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/plinth/module_loader.py b/plinth/module_loader.py
index a8c0621db..420c4a0e7 100644
--- a/plinth/module_loader.py
+++ b/plinth/module_loader.py
@@ -66,12 +66,13 @@ def load_modules():
except KeyError:
logger.error('Unsatified dependency for module - %s', module_name)
- logger.info('Module load order - %s', ordered_modules)
+ logger.info('Initializing apps - %s', ', '.join(ordered_modules))
for module_name in ordered_modules:
_initialize_module(module_name, modules[module_name])
loaded_modules[module_name] = modules[module_name]
+ logger.debug('App initialization completed.')
post_module_loading.send_robust(sender="module_loader")
@@ -134,8 +135,6 @@ def _initialize_module(module_name, module):
if module.setup_helper.get_state(
) != 'needs-setup' and module.app.is_enabled():
module.app.set_enabled(True)
-
- logger.debug("Initialized %s", module.__name__)
except Exception as exception:
logger.exception('Exception while running init for %s: %s', module,
exception)
diff --git a/plinth/web_framework.py b/plinth/web_framework.py
index 481e2bbf5..22689c963 100644
--- a/plinth/web_framework.py
+++ b/plinth/web_framework.py
@@ -51,7 +51,7 @@ def init(read_only=False):
django.setup(set_prefix=True)
logger.debug('Configured Django with applications - %s',
- settings.INSTALLED_APPS)
+ ', '.join(settings.INSTALLED_APPS))
def post_init():
From e9559b2d0dc10042e6ef1fda5d4f17cacb9b953b Mon Sep 17 00:00:00 2001
From: Sunil Mohan Adapa
Date: Thu, 10 Sep 2020 17:02:02 -0700
Subject: [PATCH 06/46] dynamicdns: Drop unnecessary code to set app as enabled
This is already handled by module loader.
Signed-off-by: Sunil Mohan Adapa
Reviewed-by: James Valleroy
---
plinth/modules/dynamicdns/__init__.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/plinth/modules/dynamicdns/__init__.py b/plinth/modules/dynamicdns/__init__.py
index bcf20774f..dc87de12c 100644
--- a/plinth/modules/dynamicdns/__init__.py
+++ b/plinth/modules/dynamicdns/__init__.py
@@ -9,9 +9,9 @@ from plinth import actions
from plinth import app as app_module
from plinth import cfg, menu
from plinth.modules.names.components import DomainType
+from plinth.modules.users.components import UsersAndGroups
from plinth.signals import domain_added
from plinth.utils import format_lazy
-from plinth.modules.users.components import UsersAndGroups
from .manifest import backup # noqa, pylint: disable=unused-import
@@ -76,7 +76,6 @@ class DynamicDNSApp(app_module.App):
domain_type='domain-type-dynamic',
name=current_status['dynamicdns_domain'],
services='__all__')
- self.set_enabled(True)
def is_enabled(self):
"""Return whether all the leader components are enabled.
From ec5f99406879baffca3e99844617c36d293c106a Mon Sep 17 00:00:00 2001
From: Sunil Mohan Adapa
Date: Thu, 10 Sep 2020 17:13:39 -0700
Subject: [PATCH 07/46] pagekite: Don't announce unconfigured kite as a valid
domain
Closes: #1891.
As soon as the app is installed, the default configuration has NAME.pagekite.me.
This is incorrectly announced as a valid domain to the system. Avoid this
behavior by never announcing this default configured kite name.
Tests:
- Install Pagekite. Observe that the default kite name is not announced as a
domain (confirmed in Name Services). With the patch, it will.
- Change the domain to anything other than default. It is announced. LE tries to
obtain cert. Name Services list the domain.
- Change the domain back to default kite name. It is not announced. Old domain
is removed.
- Start FreedomBox with Pagekite configured with default domain. It is not
announced.
- Start FreedomBox with Pagekite configured with non-default domain. It is
announced.
Signed-off-by: Sunil Mohan Adapa
Reviewed-by: James Valleroy
---
plinth/modules/pagekite/utils.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/plinth/modules/pagekite/utils.py b/plinth/modules/pagekite/utils.py
index d2a7d4572..7c3cf266d 100644
--- a/plinth/modules/pagekite/utils.py
+++ b/plinth/modules/pagekite/utils.py
@@ -20,6 +20,9 @@ KITE_SECRET = '@kitesecret'
# Augeas base path for Pagekite configuration files
CONF_PATH = '/files/etc/pagekite.d'
+# Kite name used by default in config. Should be treated as unconfigured.
+UNCONFIGURED_KITE = 'NAME.pagekite.me'
+
# Parameters that get stored in configuration service_on entries
SERVICE_PARAMS = [
'protocol', 'kitename', 'backend_host', 'backend_port', 'secret'
@@ -178,7 +181,7 @@ def update_names_module(is_enabled=None):
service for service, value in config['predefined_services'].items()
if value
]
- if config['kite_name']:
+ if config['kite_name'] and config['kite_name'] != UNCONFIGURED_KITE:
domain_added.send_robust(sender='pagekite',
domain_type='domain-type-pagekite',
name=config['kite_name'],
From 9b9238188a32b7c9350f1db0b7e89c0de043a36d Mon Sep 17 00:00:00 2001
From: Sunil Mohan Adapa
Date: Thu, 10 Sep 2020 17:29:46 -0700
Subject: [PATCH 08/46] pagekite: Don't update names module if not installed
- Due to a minor regression with init() method refactoring:
- Pagekite tries to check if it is enabled even when it is not installed. This
is an unnecessary check.
- Pagekite tries to remove domains from name services even when it is not
installed. This could have unnecessary consequences.
Fix this by checking if Pagekite is installed and perform name services updating
only if service is enabled.
Tests:
- Enable pagekite and configure it. When FreedomBox is restarted, Pagekite kite
is announced as domain and shown in Name Services.
Signed-off-by: Sunil Mohan Adapa
Reviewed-by: James Valleroy
---
plinth/modules/pagekite/__init__.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/plinth/modules/pagekite/__init__.py b/plinth/modules/pagekite/__init__.py
index eff0ab899..3a305ed76 100644
--- a/plinth/modules/pagekite/__init__.py
+++ b/plinth/modules/pagekite/__init__.py
@@ -81,7 +81,9 @@ class PagekiteApp(app_module.App):
self.add(daemon)
# Register kite name with Name Services module.
- utils.update_names_module(is_enabled=self.is_enabled())
+ setup_helper = globals()['setup_helper']
+ if setup_helper.get_state() != 'needs-setup' and self.is_enabled():
+ utils.update_names_module(is_enabled=True)
def enable(self):
"""Send domain signals after enabling the app."""
From 7b7d42fe331575b397346ff18875f64c3185ea4b Mon Sep 17 00:00:00 2001
From: Sunil Mohan Adapa
Date: Thu, 10 Sep 2020 17:45:02 -0700
Subject: [PATCH 09/46] tor: Don't check if enabled when not installed
Tests:
- Install Tor and enable onion service. Restart FreedomBox. During
initialization the onion service domain is added. Check in Name Services.
Signed-off-by: Sunil Mohan Adapa
Reviewed-by: James Valleroy
---
plinth/modules/tor/__init__.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/plinth/modules/tor/__init__.py b/plinth/modules/tor/__init__.py
index 7cd98bfc9..b8ae723f5 100644
--- a/plinth/modules/tor/__init__.py
+++ b/plinth/modules/tor/__init__.py
@@ -87,7 +87,9 @@ class TorApp(app_module.App):
self.add(users_and_groups)
# Register hidden service name with Name Services module.
- if self.is_enabled() and app_is_running(self):
+ setup_helper = globals()['setup_helper']
+ if setup_helper.get_state() != 'needs-setup' and \
+ self.is_enabled() and app_is_running(self):
status = utils.get_status(initialized=False)
hostname = status['hs_hostname']
services = [int(port['virtport']) for port in status['hs_ports']]
From 68475668ea1ed80b981b4658331285dd3eac9b2e Mon Sep 17 00:00:00 2001
From: Sunil Mohan Adapa
Date: Sun, 20 Sep 2020 20:33:15 -0700
Subject: [PATCH 10/46] tests: functional: Simplify calling the login helper
Signed-off-by: Sunil Mohan Adapa
Reviewed-by: Joseph Nuthalapati
---
plinth/tests/functional/__init__.py | 8 +++++++-
plinth/tests/functional/step_definitions.py | 4 +---
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py
index 9987eed8d..ec73a0be9 100644
--- a/plinth/tests/functional/__init__.py
+++ b/plinth/tests/functional/__init__.py
@@ -234,7 +234,13 @@ def _create_admin_account(browser, username, password):
submit(browser)
-def login(browser, url, username, password):
+def login(browser):
+ """Login to the interface."""
+ login_with_account(browser, base_url, config['DEFAULT']['username'],
+ config['DEFAULT']['password'])
+
+
+def login_with_account(browser, url, username, password):
# XXX: Find a way to remove the hardcoded jsxc URL
if '/plinth/' not in browser.url or '/jsxc/jsxc' in browser.url:
diff --git a/plinth/tests/functional/step_definitions.py b/plinth/tests/functional/step_definitions.py
index 034c002af..76fc56984 100644
--- a/plinth/tests/functional/step_definitions.py
+++ b/plinth/tests/functional/step_definitions.py
@@ -13,9 +13,7 @@ from plinth.tests import functional
@given("I'm a logged in user")
def logged_in_user(session_browser):
- functional.login(session_browser, functional.base_url,
- functional.config['DEFAULT']['username'],
- functional.config['DEFAULT']['password'])
+ functional.login(session_browser)
@given("I'm a logged out user")
From 6bfffeee138ea88ab3c9e84d0965ca193c2c4647 Mon Sep 17 00:00:00 2001
From: Joseph Nuthalapati
Date: Sun, 9 Dec 2018 21:34:18 +0530
Subject: [PATCH 11/46] calibre: Add new e-book library app
[joseph: initial code for the app]
Signed-off-by: Joseph Nuthalapati
[sunil: use the modified framework API]
[sunil: simplify setup logic, move to service file]
[sunil: strict security for service file, dynamic users]
[sunil: interface for managing libraries]
[sunil: implement backup/restore]
[sunil: add functional, action, and view tests]
[sunil: use svg icon]
[sunil: update description]
[sunil: fix apache configuration]
Signed-off-by: Sunil Mohan Adapa
Reviewed-by: Joseph Nuthalapati
---
actions/calibre | 76 ++
plinth/modules/calibre/__init__.py | 124 ++
.../conf-available/calibre-freedombox.conf | 10 +
.../data/etc/plinth/modules-enabled/calibre | 1 +
.../system/calibre-server-freedombox.service | 47 +
plinth/modules/calibre/forms.py | 29 +
plinth/modules/calibre/manifest.py | 21 +
.../templates/calibre-delete-library.html | 30 +
plinth/modules/calibre/templates/calibre.html | 59 +
plinth/modules/calibre/tests/__init__.py | 0
plinth/modules/calibre/tests/data/sample.txt | 1 +
plinth/modules/calibre/tests/test_actions.py | 84 ++
.../modules/calibre/tests/test_functional.py | 180 +++
plinth/modules/calibre/tests/test_views.py | 146 +++
plinth/modules/calibre/urls.py | 16 +
plinth/modules/calibre/views.py | 75 ++
plinth/templates/form.html | 22 +
pytest.ini | 1 +
static/themes/default/icons/calibre.png | Bin 0 -> 56951 bytes
static/themes/default/icons/calibre.svg | 1139 +++++++++++++++++
20 files changed, 2061 insertions(+)
create mode 100755 actions/calibre
create mode 100644 plinth/modules/calibre/__init__.py
create mode 100644 plinth/modules/calibre/data/etc/apache2/conf-available/calibre-freedombox.conf
create mode 100644 plinth/modules/calibre/data/etc/plinth/modules-enabled/calibre
create mode 100644 plinth/modules/calibre/data/lib/systemd/system/calibre-server-freedombox.service
create mode 100644 plinth/modules/calibre/forms.py
create mode 100644 plinth/modules/calibre/manifest.py
create mode 100644 plinth/modules/calibre/templates/calibre-delete-library.html
create mode 100644 plinth/modules/calibre/templates/calibre.html
create mode 100644 plinth/modules/calibre/tests/__init__.py
create mode 100644 plinth/modules/calibre/tests/data/sample.txt
create mode 100644 plinth/modules/calibre/tests/test_actions.py
create mode 100644 plinth/modules/calibre/tests/test_functional.py
create mode 100644 plinth/modules/calibre/tests/test_views.py
create mode 100644 plinth/modules/calibre/urls.py
create mode 100644 plinth/modules/calibre/views.py
create mode 100644 plinth/templates/form.html
create mode 100644 static/themes/default/icons/calibre.png
create mode 100644 static/themes/default/icons/calibre.svg
diff --git a/actions/calibre b/actions/calibre
new file mode 100755
index 000000000..bc8d51cd7
--- /dev/null
+++ b/actions/calibre
@@ -0,0 +1,76 @@
+#!/usr/bin/python3
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+Configuration helper for calibre.
+"""
+
+import argparse
+import json
+import pathlib
+import shutil
+import subprocess
+
+from plinth.modules import calibre
+
+LIBRARIES_PATH = pathlib.Path('/var/lib/calibre-server-freedombox/libraries')
+
+
+def parse_arguments():
+ """Return parsed command line arguments as dictionary."""
+ parser = argparse.ArgumentParser()
+ subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
+
+ subparsers.add_parser('list-libraries',
+ help='Return the list of libraries setup')
+ subparser = subparsers.add_parser('create-library',
+ help='Create an empty library')
+ subparser.add_argument('name', help='Name of the new library')
+ subparser = subparsers.add_parser('delete-library',
+ help='Delete a library and its contents')
+ subparser.add_argument('name', help='Name of the library to delete')
+
+ subparsers.required = True
+ return parser.parse_args()
+
+
+def subcommand_list_libraries(_):
+ """Return the list of libraries setup."""
+ libraries = []
+ for library in LIBRARIES_PATH.glob('*/metadata.db'):
+ libraries.append(str(library.parent.name))
+
+ print(json.dumps({'libraries': libraries}))
+
+
+def subcommand_create_library(arguments):
+ """Create an empty library."""
+ calibre.validate_library_name(arguments.name)
+ library = LIBRARIES_PATH / arguments.name
+ library.mkdir(mode=0o755) # Raise exception if already exists
+ subprocess.call(
+ ['calibredb', '--with-library', library, 'list_categories'],
+ stdout=subprocess.DEVNULL)
+
+ # Force systemd StateDirectory= logic to assign proper ownership to the
+ # DynamicUser=
+ shutil.chown(LIBRARIES_PATH.parent, 'root', 'root')
+
+
+def subcommand_delete_library(arguments):
+ """Delete a library and its contents."""
+ calibre.validate_library_name(arguments.name)
+ library = LIBRARIES_PATH / arguments.name
+ shutil.rmtree(library)
+
+
+def main():
+ """Parse arguments and perform all duties."""
+ arguments = parse_arguments()
+
+ subcommand = arguments.subcommand.replace('-', '_')
+ subcommand_method = globals()['subcommand_' + subcommand]
+ subcommand_method(arguments)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/plinth/modules/calibre/__init__.py b/plinth/modules/calibre/__init__.py
new file mode 100644
index 000000000..e3786812c
--- /dev/null
+++ b/plinth/modules/calibre/__init__.py
@@ -0,0 +1,124 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+FreedomBox app for calibre e-book library.
+"""
+
+import json
+import re
+
+from django.utils.translation import ugettext_lazy as _
+
+from plinth import actions
+from plinth import app as app_module
+from plinth import cfg, frontpage, menu
+from plinth.daemon import Daemon
+from plinth.modules.apache.components import Webserver
+from plinth.modules.firewall.components import Firewall
+from plinth.modules.users.components import UsersAndGroups
+from plinth.utils import format_lazy
+
+from .manifest import backup, clients # noqa, pylint: disable=unused-import
+
+version = 1
+
+managed_services = ['calibre-server-freedombox']
+
+managed_packages = ['calibre']
+
+_description = [
+ format_lazy(
+ _('calibre server provides online access to your e-book collection. '
+ 'You can store your e-books on your {box_name}, read them online or '
+ 'from any of your devices.'), box_name=_(cfg.box_name)),
+ _('You can organize your e-books, extract and edit their metadata, and '
+ 'perform advanced search. It can import, export, or convert across a '
+ 'wide range of formats to make e-books ready for reading on any '
+ 'device. You can read books on your browser with the builtin web '
+ 'reader. It remembers your last read location, bookmarks, and '
+ 'highlighted text.'),
+ _('Only users belonging to calibre group will be able to access '
+ 'the app. Content distribution with OPDS is currently not supported.')
+]
+
+app = None
+
+LIBRARY_NAME_PATTERN = r'[a-zA-Z0-9 _-]+'
+
+
+class CalibreApp(app_module.App):
+ """FreedomBox app for calibre."""
+
+ app_id = 'calibre'
+
+ def __init__(self):
+ """Create components for the app."""
+ super().__init__()
+
+ groups = {'calibre': _('Administer calibre application')}
+
+ info = app_module.Info(app_id=self.app_id, version=version,
+ name=_('calibre'), icon_filename='calibre',
+ short_description=_('E-book Library'),
+ description=_description, manual_page='calibre',
+ clients=clients)
+ self.add(info)
+
+ menu_item = menu.Menu('menu-calibre', info.name,
+ info.short_description, info.icon_filename,
+ 'calibre:index', parent_url_name='apps')
+ self.add(menu_item)
+
+ shortcut = frontpage.Shortcut('shortcut-calibre', info.name,
+ short_description=info.short_description,
+ icon=info.icon_filename, url='/calibre',
+ clients=info.clients,
+ login_required=True,
+ allowed_groups=list(groups))
+ self.add(shortcut)
+
+ firewall = Firewall('firewall-calibre', info.name,
+ ports=['http', 'https'], is_external=True)
+ self.add(firewall)
+
+ webserver = Webserver('webserver-calibre', 'calibre-freedombox',
+ urls=['https://{host}/calibre'])
+ self.add(webserver)
+
+ daemon = Daemon('daemon-calibre', managed_services[0],
+ listen_ports=[(8844, 'tcp4')])
+ self.add(daemon)
+
+ users_and_groups = UsersAndGroups('users-and-groups-calibre',
+ reserved_usernames=['calibre'],
+ groups=groups)
+ self.add(users_and_groups)
+
+
+def setup(helper, old_version=None):
+ """Install and configure the module."""
+ helper.install(managed_packages)
+ helper.call('post', app.enable)
+
+
+def validate_library_name(library_name):
+ """Raise exception if library name does not fit the accepted pattern."""
+ if not re.fullmatch(r'[A-Za-z0-9_.-]+', library_name):
+ raise Exception('Invalid library name')
+
+
+def list_libraries():
+ """Return a list of libraries."""
+ output = actions.superuser_run('calibre', ['list-libraries'])
+ return json.loads(output)['libraries']
+
+
+def create_library(name):
+ """Create an empty library."""
+ actions.superuser_run('calibre', ['create-library', name])
+ actions.superuser_run('service', ['try-restart', managed_services[0]])
+
+
+def delete_library(name):
+ """Delete a library and its contents."""
+ actions.superuser_run('calibre', ['delete-library', name])
+ actions.superuser_run('service', ['try-restart', managed_services[0]])
diff --git a/plinth/modules/calibre/data/etc/apache2/conf-available/calibre-freedombox.conf b/plinth/modules/calibre/data/etc/apache2/conf-available/calibre-freedombox.conf
new file mode 100644
index 000000000..e423f2134
--- /dev/null
+++ b/plinth/modules/calibre/data/etc/apache2/conf-available/calibre-freedombox.conf
@@ -0,0 +1,10 @@
+##
+## On all sites, provide calibre web interface on a path: /calibre
+##
+
+ ProxyPass http://localhost:8844/calibre
+ Include includes/freedombox-single-sign-on.conf
+
+ TKTAuthToken "admin" "calibre"
+
+
diff --git a/plinth/modules/calibre/data/etc/plinth/modules-enabled/calibre b/plinth/modules/calibre/data/etc/plinth/modules-enabled/calibre
new file mode 100644
index 000000000..bef75a6fd
--- /dev/null
+++ b/plinth/modules/calibre/data/etc/plinth/modules-enabled/calibre
@@ -0,0 +1 @@
+plinth.modules.calibre
diff --git a/plinth/modules/calibre/data/lib/systemd/system/calibre-server-freedombox.service b/plinth/modules/calibre/data/lib/systemd/system/calibre-server-freedombox.service
new file mode 100644
index 000000000..29941bc85
--- /dev/null
+++ b/plinth/modules/calibre/data/lib/systemd/system/calibre-server-freedombox.service
@@ -0,0 +1,47 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+[Unit]
+Description=calibre Content Server
+Documentation=man:calibre-server(1)
+After=network.target
+
+[Service]
+CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER CAP_NET_ADMIN CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE CAP_KILL CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_LINUX_IMMUTABLE CAP_IPC_LOCK CAP_SYS_CHROOT CAP_BLOCK_SUSPEND CAP_LEASE CAP_SYS_PACCT CAP_SYS_TTY_CONFIG CAP_SYS_BOOT CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYS_NICE CAP_SYS_RESOURCE
+DevicePolicy=closed
+Environment=HOME="/var/lib/calibre-server-freedombox"
+Environment=DEFAULT_LIBRARY="/var/lib/calibre-server-freedombox/libraries/Library"
+Environment=ARGS="--listen-on 127.0.0.1 --url-prefix /calibre --port 8844 --enable-local-write --disable-auth"
+ExecStartPre=sh -e -c "files=$$(ls ${HOME}/libraries/*/metadata.db 2>/dev/null || true); [ \"x$${files}\" = \"x\" ] && (mkdir -p \"${DEFAULT_LIBRARY}\" && calibredb --with-library=\"${DEFAULT_LIBRARY}\" list_categories > /dev/null) || true"
+ExecStart=sh -e -c "files=${HOME}/libraries/*/metadata.db; libraries=$$(dirname $${files}) ; exec /usr/bin/calibre-server $ARGS $${libraries}"
+Restart=on-failure
+ExecReload=/bin/kill -HUP $MAINPID
+DynamicUser=yes
+LockPersonality=yes
+NoNewPrivileges=yes
+PrivateDevices=yes
+PrivateMounts=yes
+PrivateTmp=yes
+PrivateUsers=yes
+ProtectControlGroups=yes
+ProtectClock=yes
+ProtectHome=yes
+ProtectHostname=yes
+ProtectKernelLogs=yes
+ProtectKernelModules=yes
+ProtectKernelTunables=yes
+ProtectSystem=strict
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
+RestrictNamespaces=yes
+RestrictRealtime=yes
+RestrictSUIDSGID=yes
+StateDirectory=calibre-server-freedombox
+SystemCallArchitectures=native
+SystemCallFilter=@system-service
+SystemCallFilter=~@resources
+SystemCallFilter=~@privileged
+SystemCallErrorNumber=EPERM
+Type=simple
+
+
+[Install]
+WantedBy=multi-user.target
diff --git a/plinth/modules/calibre/forms.py b/plinth/modules/calibre/forms.py
new file mode 100644
index 000000000..61fe67d61
--- /dev/null
+++ b/plinth/modules/calibre/forms.py
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+Django form for configuring calibre.
+"""
+
+from django import forms
+from django.core import validators
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
+
+from plinth.modules import calibre
+
+
+class CreateLibraryForm(forms.Form):
+ """Form to create an empty library."""
+
+ name = forms.CharField(
+ label=_('Name of the new library'), strip=True,
+ validators=[validators.RegexValidator(r'^[A-Za-z0-9_.-]+$')])
+
+ def clean_name(self):
+ """Check if the library name is valid."""
+ name = self.cleaned_data['name']
+
+ if name in calibre.list_libraries():
+ raise ValidationError(
+ _('A library with this name already exists.'))
+
+ return name
diff --git a/plinth/modules/calibre/manifest.py b/plinth/modules/calibre/manifest.py
new file mode 100644
index 000000000..2b6ce7317
--- /dev/null
+++ b/plinth/modules/calibre/manifest.py
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+from django.utils.translation import ugettext_lazy as _
+
+from plinth.clients import validate
+from plinth.modules.backups.api import validate as validate_backup
+
+clients = validate([{
+ 'name': _('calibre'),
+ 'platforms': [{
+ 'type': 'web',
+ 'url': '/calibre/'
+ }]
+}])
+
+backup = validate_backup({
+ 'data': {
+ 'directories': ['/var/lib/private/calibre-server-freedombox/']
+ },
+ 'services': ['calibre-server-freedombox']
+})
diff --git a/plinth/modules/calibre/templates/calibre-delete-library.html b/plinth/modules/calibre/templates/calibre-delete-library.html
new file mode 100644
index 000000000..93946e251
--- /dev/null
+++ b/plinth/modules/calibre/templates/calibre-delete-library.html
@@ -0,0 +1,30 @@
+{% extends "base.html" %}
+{% comment %}
+# SPDX-License-Identifier: AGPL-3.0-or-later
+{% endcomment %}
+
+{% load i18n %}
+
+{% block content %}
+
+
+ {% blocktrans trimmed %}
+ Delete calibre Library {{ name }}
+ {% endblocktrans %}
+
+
+
+ {% blocktrans trimmed %}
+ Delete this library permanently? All stored e-books and saved data will be
+ lost.
+ {% endblocktrans %}
+
+
+
+
+{% endblock %}
diff --git a/plinth/modules/calibre/templates/calibre.html b/plinth/modules/calibre/templates/calibre.html
new file mode 100644
index 000000000..9e4e854a8
--- /dev/null
+++ b/plinth/modules/calibre/templates/calibre.html
@@ -0,0 +1,59 @@
+{% extends "app.html" %}
+{% comment %}
+# SPDX-License-Identifier: AGPL-3.0-or-later
+{% endcomment %}
+
+{% load i18n %}
+
+{% block page_head %}
+
+{% endblock %}
+
+{% block configuration %}
+ {{ block.super }}
+
+ {% trans "Manage Libraries" %}
+
+
+
+
+
+ {% if not libraries %}
+
{% trans 'No libraries available.' %}
+ {% else %}
+
+ {% for library in libraries %}
+
+ {% endfor %}
+
+ {% endif %}
+
+
+
+{% endblock %}
diff --git a/plinth/modules/calibre/tests/__init__.py b/plinth/modules/calibre/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/plinth/modules/calibre/tests/data/sample.txt b/plinth/modules/calibre/tests/data/sample.txt
new file mode 100644
index 000000000..73709ba68
--- /dev/null
+++ b/plinth/modules/calibre/tests/data/sample.txt
@@ -0,0 +1 @@
+Testing
diff --git a/plinth/modules/calibre/tests/test_actions.py b/plinth/modules/calibre/tests/test_actions.py
new file mode 100644
index 000000000..ff61ece9c
--- /dev/null
+++ b/plinth/modules/calibre/tests/test_actions.py
@@ -0,0 +1,84 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+Test module for calibre actions.
+"""
+
+import imp
+import json
+import pathlib
+from unittest.mock import call, patch
+
+import pytest
+
+
+def _action_file():
+ """Return the path to the 'calibre' actions file."""
+ current_directory = pathlib.Path(__file__).parent
+ return str(current_directory / '..' / '..' / '..' / '..' / 'actions' /
+ 'calibre')
+
+
+actions = imp.load_source('calibre', _action_file())
+
+
+@pytest.fixture(name='call_action')
+def fixture_call_action(tmpdir, capsys):
+ """Run actions with custom root path."""
+
+ def _call_action(args, **kwargs):
+ actions.LIBRARIES_PATH = pathlib.Path(str(tmpdir))
+ with patch('argparse._sys.argv', ['calibre'] + args):
+ actions.main()
+ captured = capsys.readouterr()
+ return captured.out
+
+ return _call_action
+
+
+@pytest.fixture(autouse=True)
+def fixture_patch():
+ """Patch some underlying methods."""
+
+ def side_effect(*args, **_kwargs):
+ if args[0][0] != 'calibredb':
+ return
+
+ path = pathlib.Path(args[0][2]) / 'metadata.db'
+ path.touch()
+
+ with patch('subprocess.call') as subprocess_call, \
+ patch('shutil.chown'):
+ subprocess_call.side_effect = side_effect
+ yield
+
+
+def test_list_libraries(call_action):
+ """Test listing libraries."""
+ assert json.loads(call_action(['list-libraries'])) == {'libraries': []}
+ call_action(['create-library', 'TestLibrary'])
+ expected_output = {'libraries': ['TestLibrary']}
+ assert json.loads(call_action(['list-libraries'])) == expected_output
+
+
+@patch('shutil.chown')
+def test_create_library(chown, call_action):
+ """Test creating a library."""
+ call_action(['create-library', 'TestLibrary'])
+ library = actions.LIBRARIES_PATH / 'TestLibrary'
+ assert (library / 'metadata.db').exists()
+ assert library.stat().st_mode == 0o40755
+ expected_output = {'libraries': ['TestLibrary']}
+ assert json.loads(call_action(['list-libraries'])) == expected_output
+
+ chown.assert_has_calls([call(library.parent.parent, 'root', 'root')])
+
+
+def test_delete_library(call_action):
+ """Test deleting a library."""
+ call_action(['create-library', 'TestLibrary'])
+
+ call_action(['delete-library', 'TestLibrary'])
+ assert json.loads(call_action(['list-libraries'])) == {'libraries': []}
+
+ with pytest.raises(FileNotFoundError):
+ call_action(['delete-library', 'TestLibrary'])
diff --git a/plinth/modules/calibre/tests/test_functional.py b/plinth/modules/calibre/tests/test_functional.py
new file mode 100644
index 000000000..9f7ca97e6
--- /dev/null
+++ b/plinth/modules/calibre/tests/test_functional.py
@@ -0,0 +1,180 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+Functional, browser based tests for calibre app.
+"""
+
+import pathlib
+import time
+
+import pytest
+
+from plinth.tests import functional
+
+pytestmark = [pytest.mark.apps, pytest.mark.sso, pytest.mark.calibre]
+
+
+@pytest.fixture(scope='module', autouse=True)
+def fixture_background(session_browser):
+ """Login and install the app."""
+ functional.login(session_browser)
+ functional.install(session_browser, 'calibre')
+ yield
+ functional.app_disable(session_browser, 'calibre')
+
+
+def test_enable_disable(session_browser):
+ """Test enabling the app."""
+ functional.app_disable(session_browser, 'calibre')
+
+ functional.app_enable(session_browser, 'calibre')
+ assert functional.service_is_running(session_browser, 'calibre')
+ assert functional.is_available(session_browser, 'calibre')
+
+ functional.app_disable(session_browser, 'calibre')
+ assert not functional.service_is_running(session_browser, 'calibre')
+ assert not functional.is_available(session_browser, 'calibre')
+
+
+def test_add_delete_library(session_browser):
+ """Test adding/deleting a new library."""
+ functional.app_enable(session_browser, 'calibre')
+ _delete_library(session_browser, 'FunctionalTest', True)
+
+ _add_library(session_browser, 'FunctionalTest')
+ assert _is_library_available(session_browser, 'FunctionalTest')
+
+ _delete_library(session_browser, 'FunctionalTest')
+ assert not _is_library_available(session_browser, 'FunctionalTest')
+
+
+def test_add_delete_book(session_browser):
+ """Test adding/delete book in the library."""
+ functional.app_enable(session_browser, 'calibre')
+ _add_library(session_browser, 'FunctionalTest')
+ _delete_book(session_browser, 'FunctionalTest', 'sample.txt', True)
+
+ _add_book(session_browser, 'FunctionalTest', 'sample.txt')
+ assert _is_book_available(session_browser, 'FunctionalTest', 'sample.txt')
+
+ _delete_book(session_browser, 'FunctionalTest', 'sample.txt')
+ assert not _is_book_available(session_browser, 'FunctionalTest',
+ 'sample.txt')
+
+
+@pytest.mark.backups
+def test_backup(session_browser):
+ """Test backing up and restoring."""
+ functional.app_enable(session_browser, 'calibre')
+ _add_library(session_browser, 'FunctionalTest')
+ functional.backup_create(session_browser, 'calibre', 'test_calibre')
+ _delete_library(session_browser, 'FunctionalTest')
+ functional.backup_restore(session_browser, 'calibre', 'test_calibre')
+ assert _is_library_available(session_browser, 'FunctionalTest')
+
+
+def _add_library(browser, name):
+ """Add a new library."""
+ if _is_library_available(browser, name):
+ return
+
+ browser.find_link_by_href(
+ '/plinth/apps/calibre/library/create/').first.click()
+ browser.find_by_id('id_calibre-name').fill(name)
+ functional.submit(browser)
+
+
+def _delete_library(browser, name, ignore_missing=False):
+ """Delete a library."""
+ functional.nav_to_module(browser, 'calibre')
+ link = browser.find_link_by_href(
+ f'/plinth/apps/calibre/library/{name}/delete/')
+ if not link:
+ if ignore_missing:
+ return
+
+ raise ValueError('Library not found')
+
+ link.first.click()
+ functional.submit(browser)
+
+
+def _is_library_available(browser, name):
+ """Return whether a library is present in the list of libraries."""
+ functional.nav_to_module(browser, 'calibre')
+ link = browser.find_link_by_href(
+ f'/plinth/apps/calibre/library/{name}/delete/')
+ return bool(link)
+
+
+def _visit_library(browser, name):
+ """Open the page for the library."""
+ functional.visit(browser, '/calibre/')
+
+ # Calibre interface will be available a short time after restarting the
+ # service.
+ def _service_available():
+ unavailable_xpath = '//h1[contains(text(), "Service Unavailable")]'
+ available = not browser.find_by_xpath(unavailable_xpath)
+ if not available:
+ time.sleep(0.5)
+ functional.visit(browser, '/calibre/')
+
+ return available
+
+ functional.eventually(_service_available)
+
+ functional.eventually(browser.find_by_css,
+ args=[f'.calibre-push-button[data-lid="{name}"]'])
+ link = browser.find_by_css(f'.calibre-push-button[data-lid="{name}"]')
+ if not link:
+ raise ValueError('Library not found')
+
+ link.first.click()
+ functional.eventually(browser.find_by_css, [f'.book-list-cover-grid'])
+
+
+def _add_book(browser, library_name, book_name):
+ """Add a book to the library through Calibre interface."""
+ _visit_library(browser, library_name)
+ add_button = browser.find_by_css(f'a[data-button-icon="plus"]')
+ add_button.first.click()
+
+ functional.eventually(browser.find_by_xpath,
+ ['//span[contains(text(), "Add books")]'])
+ browser.execute_script(
+ '''document.querySelector('input[type="file"]').setAttribute(
+ 'name', 'test-book-upload');''')
+
+ file_path = pathlib.Path(__file__).parent / f'data/{book_name}'
+ browser.attach_file('test-book-upload', [str(file_path)])
+ functional.eventually(browser.find_by_xpath,
+ ['//span[contains(text(), "Added successfully")]'])
+
+
+def _delete_book(browser, library_name, book_name, ignore_missing=False):
+ """Delete a book from the library through Calibre interface."""
+ _visit_library(browser, library_name)
+ book_name = book_name.partition('.')[0]
+ book = browser.find_by_xpath(f'//a[contains(@title, "{book_name}")]')
+ if not book:
+ if ignore_missing:
+ return
+
+ raise Exception('Book not found')
+
+ book.first.click()
+ delete_button = browser.find_by_css(f'a[data-button-icon="trash"]')
+ delete_button.first.click()
+
+ dialog = browser.find_by_id('modal-container').first
+ functional.eventually(lambda: dialog.visible)
+ ok_button = browser.find_by_xpath('//span[contains(text(), "OK")]')
+ ok_button.first.click()
+
+
+def _is_book_available(browser, library_name, book_name):
+ """Return whether a book is present in Calibre interface."""
+ _visit_library(browser, library_name)
+ book_name = book_name.partition('.')[0]
+ book = browser.find_by_xpath(f'//a[contains(@title, "{book_name}")]')
+ return bool(book)
diff --git a/plinth/modules/calibre/tests/test_views.py b/plinth/modules/calibre/tests/test_views.py
new file mode 100644
index 000000000..24ede010d
--- /dev/null
+++ b/plinth/modules/calibre/tests/test_views.py
@@ -0,0 +1,146 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+Tests for calibre views.
+"""
+
+from unittest.mock import call, patch
+
+import pytest
+from django import urls
+from django.contrib.messages.storage.fallback import FallbackStorage
+from django.http.response import Http404
+
+from plinth import actions, module_loader
+from plinth.modules.calibre import views
+
+# For all tests, use plinth.urls instead of urls configured for testing
+pytestmark = pytest.mark.urls('plinth.urls')
+
+
+@pytest.fixture(autouse=True, scope='module')
+def fixture_calibre_urls():
+ """Make sure calibre app's URLs are part of plinth.urls."""
+ with patch('plinth.module_loader._modules_to_load', new=[]) as modules, \
+ patch('plinth.urls.urlpatterns', new=[]):
+ modules.append('plinth.modules.calibre')
+ module_loader.include_urls()
+ yield
+
+
+@pytest.fixture(autouse=True)
+def calibre_patch():
+ """Patch calibre methods."""
+ with patch('plinth.modules.calibre.list_libraries') as list_libraries:
+ list_libraries.return_value = ['TestExistingLibrary']
+
+ yield
+
+
+def make_request(request, view, **kwargs):
+ """Make request with a message storage."""
+ setattr(request, 'session', 'session')
+ messages = FallbackStorage(request)
+ setattr(request, '_messages', messages)
+ response = view(request, **kwargs)
+
+ return response, messages
+
+
+@patch('plinth.modules.calibre.create_library')
+def test_create_library(create_library, rf):
+ """Test that create library view works."""
+ form_data = {'calibre-name': 'TestLibrary'}
+ request = rf.post(urls.reverse('calibre:create-library'), data=form_data)
+ view = views.CreateLibraryView.as_view()
+ response, messages = make_request(request, view)
+
+ assert response.status_code == 302
+ assert response.url == urls.reverse('calibre:index')
+ assert list(messages)[0].message == 'Library created.'
+ create_library.assert_has_calls([call('TestLibrary')])
+
+
+@patch('plinth.modules.calibre.create_library')
+def test_create_library_failed(create_library, rf):
+ """Test that create library fails as expected."""
+ create_library.side_effect = actions.ActionError('calibre', 'TestOutput',
+ 'TestError')
+ form_data = {'calibre-name': 'TestLibrary'}
+ request = rf.post(urls.reverse('calibre:create-library'), data=form_data)
+ view = views.CreateLibraryView.as_view()
+ response, messages = make_request(request, view)
+
+ assert response.status_code == 302
+ assert response.url == urls.reverse('calibre:index')
+ assert list(messages)[0].message == \
+ 'An error occurred while creating the library. TestError'
+
+
+def test_create_library_existing_library(rf):
+ """Test that create library errors out for an existing library name."""
+ form_data = {'calibre-name': 'TestExistingLibrary'}
+ request = rf.post(urls.reverse('calibre:create-library'), data=form_data)
+ view = views.CreateLibraryView.as_view()
+ response, _ = make_request(request, view)
+
+ assert response.context_data['form'].errors['name'][
+ 0] == 'A library with this name already exists.'
+ assert response.status_code == 200
+
+
+def test_create_library_invalid_name(rf):
+ """Test that create library errors out for invalid name."""
+ form_data = {'calibre-name': 'Invalid Library'}
+ request = rf.post(urls.reverse('calibre:create-library'), data=form_data)
+ view = views.CreateLibraryView.as_view()
+ response, _ = make_request(request, view)
+
+ assert response.context_data['form'].errors['name'][
+ 0] == 'Enter a valid value.'
+ assert response.status_code == 200
+
+
+@patch('plinth.modules.calibre.app')
+def test_delete_library_confirmation_view(_app, rf):
+ """Test that deleting library confirmation shows correct name."""
+ response, _ = make_request(rf.get(''), views.delete_library,
+ name='TestExistingLibrary')
+ assert response.status_code == 200
+ assert response.context_data['name'] == 'TestExistingLibrary'
+
+
+@patch('plinth.modules.calibre.delete_library')
+@patch('plinth.modules.calibre.app')
+def test_delete_library(_app, delete_library, rf):
+ """Test that deleting a library works."""
+ response, messages = make_request(rf.post(''), views.delete_library,
+ name='TestExistingLibrary')
+ assert response.status_code == 302
+ assert response.url == urls.reverse('calibre:index')
+ assert list(messages)[0].message == 'TestExistingLibrary deleted.'
+ delete_library.assert_has_calls([call('TestExistingLibrary')])
+
+
+@patch('plinth.modules.calibre.delete_library')
+def test_delete_library_error(delete_library, rf):
+ """Test that deleting a library shows error when operation fails."""
+ delete_library.side_effect = actions.ActionError('calibre', 'TestInput',
+ 'TestError')
+ response, messages = make_request(rf.post(''), views.delete_library,
+ name='TestExistingLibrary')
+ assert response.status_code == 302
+ assert response.url == urls.reverse('calibre:index')
+ assert list(messages)[0].message == \
+ 'Could not delete TestExistingLibrary: '\
+ "('calibre', 'TestInput', 'TestError')"
+
+
+def test_delete_library_non_existing(rf):
+ """Test that deleting a library shows error when operation fails."""
+ with pytest.raises(Http404):
+ make_request(rf.post(''), views.delete_library,
+ name='TestNonExistingLibrary')
+
+ with pytest.raises(Http404):
+ make_request(rf.get(''), views.delete_library,
+ name='TestNonExistingLibrary')
diff --git a/plinth/modules/calibre/urls.py b/plinth/modules/calibre/urls.py
new file mode 100644
index 000000000..a4cb694e3
--- /dev/null
+++ b/plinth/modules/calibre/urls.py
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+URLs for the calibre module.
+"""
+
+from django.conf.urls import url
+
+from . import views
+
+urlpatterns = [
+ url(r'^apps/calibre/$', views.CalibreAppView.as_view(), name='index'),
+ url(r'^apps/calibre/library/create/$', views.CreateLibraryView.as_view(),
+ name='create-library'),
+ url(r'^apps/calibre/library/(?P[a-zA-Z0-9_.-]+)/delete/$',
+ views.delete_library, name='delete-library'),
+]
diff --git a/plinth/modules/calibre/views.py b/plinth/modules/calibre/views.py
new file mode 100644
index 000000000..f969de9d5
--- /dev/null
+++ b/plinth/modules/calibre/views.py
@@ -0,0 +1,75 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+Views for the calibre module.
+"""
+
+from django.contrib import messages
+from django.contrib.messages.views import SuccessMessageMixin
+from django.http import Http404
+from django.shortcuts import redirect
+from django.template.response import TemplateResponse
+from django.urls import reverse_lazy
+from django.utils.translation import ugettext as _
+from django.views.generic.edit import FormView
+
+from plinth import actions, views
+from plinth.modules import calibre
+
+from . import forms
+
+
+class CalibreAppView(views.AppView):
+ """Serve configuration form."""
+ app_id = 'calibre'
+ template_name = 'calibre.html'
+
+ def get_context_data(self, **kwargs):
+ """Return additional context for rendering the template."""
+ context = super().get_context_data(**kwargs)
+ context['libraries'] = calibre.list_libraries()
+ return context
+
+
+class CreateLibraryView(SuccessMessageMixin, FormView):
+ """View to create an empty library."""
+ form_class = forms.CreateLibraryForm
+ prefix = 'calibre'
+ template_name = 'form.html'
+ success_url = reverse_lazy('calibre:index')
+ success_message = _('Library created.')
+
+ def form_valid(self, form):
+ """Create the library on valid form submission."""
+ try:
+ calibre.create_library(form.cleaned_data['name'])
+ except actions.ActionError as error:
+ self.success_message = ''
+ error_text = error.args[2].split('\n')[0]
+ messages.error(
+ self.request, "{0} {1}".format(
+ _('An error occurred while creating the library.'),
+ error_text))
+
+ return super().form_valid(form)
+
+
+def delete_library(request, name):
+ """View to delete a library."""
+ if name not in calibre.list_libraries():
+ raise Http404
+
+ if request.method == 'POST':
+ try:
+ calibre.delete_library(name)
+ messages.success(request, _('{name} deleted.').format(name=name))
+ except actions.ActionError as error:
+ messages.error(
+ request,
+ _('Could not delete {name}: {error}').format(
+ name=name, error=error))
+ return redirect(reverse_lazy('calibre:index'))
+
+ return TemplateResponse(request, 'calibre-delete-library.html', {
+ 'title': calibre.app.info.name,
+ 'name': name
+ })
diff --git a/plinth/templates/form.html b/plinth/templates/form.html
new file mode 100644
index 000000000..ae21f928a
--- /dev/null
+++ b/plinth/templates/form.html
@@ -0,0 +1,22 @@
+{% extends "base.html" %}
+{% comment %}
+# SPDX-License-Identifier: AGPL-3.0-or-later
+{% endcomment %}
+
+{% load bootstrap %}
+{% load i18n %}
+
+{% block content %}
+
+ {{ title }}
+
+
+
+{% endblock %}
diff --git a/pytest.ini b/pytest.ini
index e95adc2ab..cad6c2306 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -4,6 +4,7 @@ markers = functional
apps
backups
bind
+ calibre
configuration
date_and_time
deluge
diff --git a/static/themes/default/icons/calibre.png b/static/themes/default/icons/calibre.png
new file mode 100644
index 0000000000000000000000000000000000000000..12e7f1067eecf35e6d66b9de0fab02d980e6ab93
GIT binary patch
literal 56951
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