From dd6ff3fa3d2a3a4a8a26a389d155d9b82eae4b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Tue, 16 Jun 2020 01:58:23 +0000 Subject: [PATCH 01/90] Translated using Weblate (Turkish) Currently translated at 60.1% (771 of 1281 strings) --- plinth/locale/tr/LC_MESSAGES/django.po | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/plinth/locale/tr/LC_MESSAGES/django.po b/plinth/locale/tr/LC_MESSAGES/django.po index 2b2da1829..62541ac38 100644 --- a/plinth/locale/tr/LC_MESSAGES/django.po +++ b/plinth/locale/tr/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-15 19:20-0400\n" -"PO-Revision-Date: 2019-08-08 00:22+0000\n" -"Last-Translator: Mesut Akcan \n" +"PO-Revision-Date: 2020-06-17 02:41+0000\n" +"Last-Translator: Oğuz Ersen \n" "Language-Team: Turkish \n" "Language: tr\n" @@ -16,7 +16,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 3.8-dev\n" +"X-Generator: Weblate 4.1.1-dev\n" #: doc/dev/_templates/layout.html:11 msgid "Page source" @@ -445,23 +445,12 @@ msgid "Are you sure that you want to remove this repository?" msgstr "Bu depoyu kaldırmak istediğinizden emin misiniz?" #: plinth/modules/backups/templates/backups_repository_remove.html:19 -#, fuzzy -#| msgid "" -#| "\n" -#| " The remote repository will not be deleted.\n" -#| " This just removes the repository from the listing on the backup " -#| "page, you\n" -#| " can add it again later on.\n" -#| " " msgid "" "The remote repository will not be deleted. This just removes the repository " "from the listing on the backup page, you can add it again later on." msgstr "" -"\n" -" Uzak depo silinmeyecek.\n" -" Bu sadece depoyu yedekleme sayfasındaki listeden kaldırır,\n" -" daha sonra tekrar ekleyebilirsiniz.\n" -" " +"Uzak depo silinmeyecek. Bu sadece depoyu yedekleme sayfasındaki listeden " +"kaldırır, daha sonra tekrar ekleyebilirsiniz." #: plinth/modules/backups/templates/backups_repository_remove.html:31 msgid "Remove Location" From f4d8a68f1d3911a8b90457eb0e1c78982407873c Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 18 Jun 2020 19:06:28 +0000 Subject: [PATCH 02/90] Translated using Weblate (Telugu) Currently translated at 64.0% (820 of 1281 strings) --- plinth/locale/te/LC_MESSAGES/django.po | 38 ++++++++++++++------------ 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/plinth/locale/te/LC_MESSAGES/django.po b/plinth/locale/te/LC_MESSAGES/django.po index a38666c1b..4beecc9e7 100644 --- a/plinth/locale/te/LC_MESSAGES/django.po +++ b/plinth/locale/te/LC_MESSAGES/django.po @@ -10,8 +10,8 @@ msgstr "" "Project-Id-Version: FreedomBox UI\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-15 19:20-0400\n" -"PO-Revision-Date: 2020-04-25 14:11+0000\n" -"Last-Translator: Joseph Nuthalapati \n" +"PO-Revision-Date: 2020-06-18 20:03+0000\n" +"Last-Translator: Sunil Mohan Adapa \n" "Language-Team: Telugu \n" "Language: te\n" @@ -19,7 +19,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.0.2-dev\n" +"X-Generator: Weblate 4.1.1-dev\n" #: doc/dev/_templates/layout.html:11 msgid "Page source" @@ -104,7 +104,7 @@ msgstr "{box_name}అంతర్జాల ముఖాంతరం (ఫ్ల #: plinth/modules/apache/components.py:120 #, python-brace-format msgid "Access URL {url} on tcp{kind}" -msgstr "tcp పై {kind} URL {url} యాక్సెస్ చేయండి" +msgstr "tcp{kind} పై URL {url} వాడుక" #: plinth/modules/apache/components.py:124 #, python-brace-format @@ -634,8 +634,9 @@ 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 ఇంటర్నెట్ లో మీ డొమైన్ నేమ్ సిస్టం (DNS) సమాచారాన్ని ప్రచురించడానికి, మరియు మీ నెట్వర్క్ లోని " -"వినియోగదారుల DNS విచారణలను పరిష్కరించడానికి అనుమతిస్తుంది." +"BIND ఇంటర్నెట్ లో మీ డొమైన్ నేమ్ సిస్టం (DNS) సమాచారాన్ని ప్రచురించడానికి, " +"మరియు మీ నెట్వర్క్ లోని వినియోగదారుల DNS విచారణలను పరిష్కరించడానికి " +"అనుమతిస్తుంది." #: plinth/modules/bind/__init__.py:33 #, python-brace-format @@ -644,9 +645,9 @@ msgid "" "machines on local network. It is also incompatible with sharing Internet " "connection from {box_name}." msgstr "" -"ప్రస్తుతం, ఈ {box_name} పైన. లోకల్ నెట్వర్క్ పరిధి లో ఉన్న మెషిన్ల డ్.ఎన్.ఎస్ శోధనలను " -"పరిష్కరించడానికి బైండ్ మాత్రమే ఉపయోగించబడుతుంది. ఇది ఈ {box_name} నుంచి అంతర్జాల భాగస్వామ్యానికి " -"అననుకూలమైనది." +"ప్రస్తుతం, ఈ {box_name} పైన. లోకల్ నెట్వర్క్ పరిధి లో ఉన్న మెషిన్ల డ్.ఎన్.ఎస్" +" శోధనలను పరిష్కరించడానికి బైండ్ మాత్రమే ఉపయోగించబడుతుంది. ఇది ఈ {box_name} " +"నుంచి అంతర్జాల భాగస్వామ్యానికి అననుకూలమైనది." #: plinth/modules/bind/__init__.py:82 msgid "BIND" @@ -1110,7 +1111,7 @@ msgstr "డైరెక్టరీని దిగుమతి చేయు" #: plinth/modules/deluge/manifest.py:10 msgid "Bittorrent client written in Python/PyGTK" -msgstr "పైథాన్ / పీవైజీటీకే లో బిట్ టోరెంట్ పై ఆధారపడిన వ్యక్తి వ్రాశారు" +msgstr "పైథాన్ / పీవైజీటీకే లో బిట్ టోరెంట్ పై ఆధారపడిన వ్యక్తి వ్రాశారు" #: plinth/modules/diagnostics/__init__.py:24 msgid "" @@ -1171,8 +1172,8 @@ msgid "" "diaspora* is a decentralized social network where you can store and control " "your own data." msgstr "" -"డయాస్పొరా అనే వికేంద్రీకృత సామాజిక నెట్వర్క్ లో మీరు మీ యొక్క సమాచారాన్ని నిల్వ ఉంచుకోవచ్చు మరియు నియంత్రణ " -"పరచుకోవచ్చు." +"డయాస్పొరా అనే వికేంద్రీకృత సామాజిక నెట్వర్క్ లో మీరు మీ యొక్క సమాచారాన్ని " +"నిల్వ ఉంచుకోవచ్చు మరియు నియంత్రణ పరచుకోవచ్చు." #: plinth/modules/diaspora/__init__.py:69 #: plinth/modules/diaspora/manifest.py:23 @@ -1207,10 +1208,11 @@ msgid "" "podname wouldn't be accessible.
You can access the diaspora* pod at diaspora.%(domain_name)s " msgstr "" -"డైస్పోరా * పాడ్ డొమైన్ %(domain_name)s గా అమర్చబడింది.వినియోగదారుడి ID లు username @ " -"diaspora.%(domain_name)s
వంటివి కనిపిస్తుంది. ఫ్రీడమ్బాక్స్ డొమైన్ పేరు మార్చబడితే, " -"మునుపటి podname తో నమోదైన వినియోగదారుల యొక్క మొత్తం డేటా అందుబాటులో
మీరు డయాస్పోరా.%(domain_name)s లు లో డయాస్పోరా *" +"డైస్పోరా* పాడ్ డొమైన్ %(domain_name)s గా అమర్చబడింది. వినియోగదారుడి " +"IDలు username@diaspora.%(domain_name)s
వంటివి కనిపిస్తుంది. " +"ఫ్రీడమ్బాక్స్ డొమైన్ పేరు మార్చబడితే, మునుపటి podname తో నమోదైన వినియోగదారుల " +"యొక్క మొత్తం డేటా అందుబాటులో
మీరు డయాస్పోరా.%(domain_name)s లు లో డయాస్పోరా*" #: plinth/modules/diaspora/templates/diaspora-pre-setup.html:43 #: plinth/modules/dynamicdns/templates/dynamicdns_configure.html:25 @@ -1296,7 +1298,9 @@ msgstr "" #: plinth/modules/dynamicdns/forms.py:40 #, python-brace-format msgid "The public domain name you want to use to reach your {box_name}." -msgstr "మీ {box_name} ను చేరుకోవడానికి మీరు ఉపయోగించాలని కోరుకుంటున్న మీ పబ్లిక్ డొమైన్ పేరు." +msgstr "" +"మీ {box_name} ను చేరుకోవడానికి మీరు ఉపయోగించాలని కోరుకుంటున్న మీ పబ్లిక్ " +"డొమైన్ పేరు." #: plinth/modules/dynamicdns/forms.py:43 msgid "Use this option if your provider uses self signed certificates." From 2f89bfa95b9472ef418efc5855bbe2c3824990f6 Mon Sep 17 00:00:00 2001 From: "ferhad.necef" Date: Thu, 18 Jun 2020 09:46:23 +0000 Subject: [PATCH 03/90] Translated using Weblate (Russian) Currently translated at 74.7% (957 of 1281 strings) --- plinth/locale/ru/LC_MESSAGES/django.po | 60 +++++++------------------- 1 file changed, 15 insertions(+), 45 deletions(-) diff --git a/plinth/locale/ru/LC_MESSAGES/django.po b/plinth/locale/ru/LC_MESSAGES/django.po index e87f5118f..c9c08c6b3 100644 --- a/plinth/locale/ru/LC_MESSAGES/django.po +++ b/plinth/locale/ru/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-15 19:20-0400\n" -"PO-Revision-Date: 2020-05-21 13:41+0000\n" -"Last-Translator: Artem \n" +"PO-Revision-Date: 2020-06-18 20:03+0000\n" +"Last-Translator: ferhad.necef \n" "Language-Team: Russian \n" "Language: ru\n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.1-dev\n" +"X-Generator: Weblate 4.1.1-dev\n" #: doc/dev/_templates/layout.html:11 msgid "Page source" @@ -160,10 +160,8 @@ msgid "Name" msgstr "Имя" #: plinth/modules/backups/forms.py:53 -#, fuzzy -#| msgid "Name for new backup archive." msgid "(Optional) Set a name for this backup archive" -msgstr "Имя для нового архива." +msgstr "(Необязательно) задайте имя для этого резервного архива" #: plinth/modules/backups/forms.py:56 msgid "Included apps" @@ -324,58 +322,40 @@ msgstr "Сохранение данных {box_name}" #: plinth/modules/backups/templates/backups.html:30 #: plinth/modules/backups/views.py:60 -#, fuzzy -#| msgid "Create Account" msgid "Create a new backup" -msgstr "Создать учетную запись" +msgstr "Создать новую резервную копию" #: plinth/modules/backups/templates/backups.html:34 -#, fuzzy -#| msgid "Create Account" msgid "Create Backup" msgstr "Создать учетную запись" #: plinth/modules/backups/templates/backups.html:37 -#, fuzzy -#| msgid "Exported backup archives" msgid "Upload and restore a backup archive" -msgstr "Экспортированные резервные копии" +msgstr "Выгрузить и резервировать архив бэкапа" #: plinth/modules/backups/templates/backups.html:41 -#, fuzzy -#| msgid "Exported backup archives" msgid "Upload and Restore" -msgstr "Экспортированные резервные копии" +msgstr "Выгрузить и восстановить" #: plinth/modules/backups/templates/backups.html:44 -#, fuzzy -#| msgid "Create remote backup repository" msgid "Add a backup location" -msgstr "Создать удаленный репозиторий резервного сохранения" +msgstr "Добавить местоположение для резервного копирования" #: plinth/modules/backups/templates/backups.html:48 -#, fuzzy -#| msgid "Create remote backup repository" msgid "Add Backup Location" -msgstr "Создать удаленный репозиторий резервного сохранения" +msgstr "Добавить местоположение для резервного копирования" #: plinth/modules/backups/templates/backups.html:51 -#, fuzzy -#| msgid "Create remote backup repository" msgid "Add a remote backup location" msgstr "Создать удаленный репозиторий резервного сохранения" #: plinth/modules/backups/templates/backups.html:55 -#, fuzzy -#| msgid "Create remote backup repository" msgid "Add Remote Backup Location" -msgstr "Создать удаленный репозиторий резервного сохранения" +msgstr "Добавить расположение удаленного резервного копирования" #: plinth/modules/backups/templates/backups.html:58 -#, fuzzy -#| msgid "Existing backup locations" msgid "Existing Backups" -msgstr "Существующие резервные хранилища" +msgstr "Существующие резервные копии" #: plinth/modules/backups/templates/backups_add_remote_repository.html:19 #, python-format @@ -389,10 +369,8 @@ msgstr "" "для входа SSH и, если выбрано, парольную фразу шифрования." #: plinth/modules/backups/templates/backups_add_remote_repository.html:28 -#, fuzzy -#| msgid "Create Connection" msgid "Create Location" -msgstr "Создание подключения" +msgstr "Создание расположения" #: plinth/modules/backups/templates/backups_add_repository.html:19 #: plinth/modules/gitweb/views.py:49 @@ -423,10 +401,8 @@ msgid "Submit" msgstr "Отправить" #: plinth/modules/backups/templates/backups_repository.html:19 -#, fuzzy -#| msgid "Repository removed." msgid "This repository is encrypted" -msgstr "Репозиторий удалён." +msgstr "Репозиторий зашифрован" #: plinth/modules/backups/templates/backups_repository.html:34 #, fuzzy @@ -435,8 +411,6 @@ msgid "Unmount Location" msgstr "Местоположение" #: plinth/modules/backups/templates/backups_repository.html:45 -#, fuzzy -#| msgid "Mount Point" msgid "Mount Location" msgstr "Точка монтирования" @@ -446,10 +420,8 @@ msgstr "" "Удалить место сохранения резервной копии. Резервная копия не будет удалена." #: plinth/modules/backups/templates/backups_repository.html:77 -#, fuzzy -#| msgid "downloading" msgid "Download" -msgstr "Загрузка" +msgstr "Скачивание" #: plinth/modules/backups/templates/backups_repository.html:81 #: plinth/modules/backups/templates/backups_restore.html:28 @@ -475,10 +447,8 @@ msgstr "" "сможете позже снова добавить его." #: plinth/modules/backups/templates/backups_repository_remove.html:31 -#, fuzzy -#| msgid "Location" msgid "Remove Location" -msgstr "Местоположение" +msgstr "Удаление местоположения" #: plinth/modules/backups/templates/backups_restore.html:15 msgid "Restore data from" From aaabb0d03092f29e6f2d26c65c62d5f104e316d8 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Thu, 18 Jun 2020 20:04:39 +0000 Subject: [PATCH 04/90] Translated using Weblate (French) Currently translated at 98.6% (1264 of 1281 strings) --- plinth/locale/fr/LC_MESSAGES/django.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plinth/locale/fr/LC_MESSAGES/django.po b/plinth/locale/fr/LC_MESSAGES/django.po index 3fe353065..54d9afcc7 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-06-15 19:20-0400\n" -"PO-Revision-Date: 2020-06-02 00:57+0000\n" +"PO-Revision-Date: 2020-06-20 05:41+0000\n" "Last-Translator: Thomas Vincent \n" "Language-Team: French \n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.1-dev\n" +"X-Generator: Weblate 4.1.1\n" #: doc/dev/_templates/layout.html:11 msgid "Page source" @@ -425,7 +425,7 @@ msgstr "" #: plinth/modules/backups/templates/backups_repository.html:77 msgid "Download" -msgstr "Télécharger" +msgstr "Téléchargement" #: plinth/modules/backups/templates/backups_repository.html:81 #: plinth/modules/backups/templates/backups_restore.html:28 From 2bcff5809d059a86bb9fd0af8945e8639409f2ad Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Fri, 19 Jun 2020 05:05:15 +0000 Subject: [PATCH 05/90] Translated using Weblate (Telugu) Currently translated at 64.0% (821 of 1281 strings) --- plinth/locale/te/LC_MESSAGES/django.po | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plinth/locale/te/LC_MESSAGES/django.po b/plinth/locale/te/LC_MESSAGES/django.po index 4beecc9e7..ee8aed00f 100644 --- a/plinth/locale/te/LC_MESSAGES/django.po +++ b/plinth/locale/te/LC_MESSAGES/django.po @@ -10,8 +10,8 @@ msgstr "" "Project-Id-Version: FreedomBox UI\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-15 19:20-0400\n" -"PO-Revision-Date: 2020-06-18 20:03+0000\n" -"Last-Translator: Sunil Mohan Adapa \n" +"PO-Revision-Date: 2020-06-20 05:41+0000\n" +"Last-Translator: Joseph Nuthalapati \n" "Language-Team: Telugu \n" "Language: te\n" @@ -19,7 +19,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.1.1-dev\n" +"X-Generator: Weblate 4.1.1\n" #: doc/dev/_templates/layout.html:11 msgid "Page source" @@ -434,10 +434,8 @@ msgid "Remove Backup Location. This will not delete the remote backup." msgstr "" #: plinth/modules/backups/templates/backups_repository.html:77 -#, fuzzy -#| msgid "downloading" msgid "Download" -msgstr "దిగుమతి అవుతోంది" +msgstr "దిగుమతి" #: plinth/modules/backups/templates/backups_repository.html:81 #: plinth/modules/backups/templates/backups_restore.html:28 From bc6b24247dba70334241f467619bc73193f7a5d0 Mon Sep 17 00:00:00 2001 From: wind Date: Thu, 18 Jun 2020 20:22:52 +0000 Subject: [PATCH 06/90] Translated using Weblate (Russian) Currently translated at 74.7% (957 of 1281 strings) --- plinth/locale/ru/LC_MESSAGES/django.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plinth/locale/ru/LC_MESSAGES/django.po b/plinth/locale/ru/LC_MESSAGES/django.po index c9c08c6b3..3b75d3b9f 100644 --- a/plinth/locale/ru/LC_MESSAGES/django.po +++ b/plinth/locale/ru/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-15 19:20-0400\n" -"PO-Revision-Date: 2020-06-18 20:03+0000\n" -"Last-Translator: ferhad.necef \n" +"PO-Revision-Date: 2020-06-20 05:41+0000\n" +"Last-Translator: wind \n" "Language-Team: Russian \n" "Language: ru\n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.1.1-dev\n" +"X-Generator: Weblate 4.1.1\n" #: doc/dev/_templates/layout.html:11 msgid "Page source" @@ -421,7 +421,7 @@ msgstr "" #: plinth/modules/backups/templates/backups_repository.html:77 msgid "Download" -msgstr "Скачивание" +msgstr "Загрузка" #: plinth/modules/backups/templates/backups_repository.html:81 #: plinth/modules/backups/templates/backups_restore.html:28 From f7688401653b13208ee75dbd2679848e96465512 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 28 May 2020 09:20:14 -0700 Subject: [PATCH 07/90] transmission: tests: functional: Fix to wait properly When the page is initially loaded, torrents don't appear. It is later loaded using an AJAX call. Wait until we find the torrents we need. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/transmission/tests/test_functional.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plinth/modules/transmission/tests/test_functional.py b/plinth/modules/transmission/tests/test_functional.py index ae7236b2e..68e8341e8 100644 --- a/plinth/modules/transmission/tests/test_functional.py +++ b/plinth/modules/transmission/tests/test_functional.py @@ -26,7 +26,9 @@ def transmission_upload_sample_torrent(session_browser): parsers.parse( 'there should be {torrents_number:d} torrents listed in transmission')) def transmission_assert_number_of_torrents(session_browser, torrents_number): - assert torrents_number == _get_number_of_torrents(session_browser) + functional.visit(session_browser, '/transmission') + assert functional.eventually( + lambda: torrents_number == _get_number_of_torrents(session_browser)) def _remove_all_torrents(browser): @@ -65,5 +67,4 @@ def _upload_sample_torrent(browser): def _get_number_of_torrents(browser): """Return the number torrents currently in transmission.""" - functional.visit(browser, '/transmission') return len(browser.find_by_css('#torrent_list .torrent')) From ae73b296de9fd2a3575dbaff2f1a3bfc37a8ef82 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 28 May 2020 09:22:18 -0700 Subject: [PATCH 08/90] ttrss: tests: functional: Fix to wait properly - When subscribe button is clicked in subscribe dialog, the dialog does not close immediately. Wait until it closes or error appears. - When a feed is added, the feed list refreshes and during that time, it is not possible to click on the feed expand button. Wait until it can be clicked. Extend the eventually() method to wait on exceptions and not just false values. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/ttrss/tests/test_functional.py | 13 +++++++++++-- plinth/tests/functional/__init__.py | 7 +++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/plinth/modules/ttrss/tests/test_functional.py b/plinth/modules/ttrss/tests/test_functional.py index 89009f9bc..c67d99f52 100644 --- a/plinth/modules/ttrss/tests/test_functional.py +++ b/plinth/modules/ttrss/tests/test_functional.py @@ -49,18 +49,27 @@ def _click_main_menu_item(browser, text): def _subscribe(browser): """Subscribe to a feed in TT-RSS.""" + + def _already_subscribed_message(): + return browser.is_text_present( + 'You are already subscribed to this feed.') + _ttrss_load_main_interface(browser) _click_main_menu_item(browser, 'Subscribe to feed...') browser.find_by_id('feedDlg_feedUrl').fill( 'https://planet.debian.org/atom.xml') browser.find_by_text('Subscribe').click() - if browser.is_text_present('You are already subscribed to this feed.'): + add_dialog = browser.find_by_css('#feedAddDlg') + functional.eventually( + lambda: not add_dialog.visible or _already_subscribed_message()) + if _already_subscribed_message(): browser.find_by_text('Cancel').click() + functional.eventually(lambda: not add_dialog.visible) expand = browser.find_by_css('span.dijitTreeExpandoClosed') if expand: - expand.first.click() + functional.eventually(expand.first.click) assert functional.eventually(_is_feed_shown, [browser]) diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py index 9ea5bc4bb..5a75812d9 100644 --- a/plinth/tests/functional/__init__.py +++ b/plinth/tests/functional/__init__.py @@ -64,8 +64,11 @@ def eventually(function, args=[], timeout=30): end_time = time.time() + timeout current_time = time.time() while current_time < end_time: - if function(*args): - return True + try: + if function(*args): + return True + except Exception: + pass time.sleep(0.1) current_time = time.time() From 399a132bac77b5d24c89f9799022a61c0536e045 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 28 May 2020 09:29:40 -0700 Subject: [PATCH 09/90] tor: tests: functional: Fix to wait properly on progress page - Fix the condition for checking if we are on progress page by ensuring that the page is loaded fully before checking if it is that progress loader. Avoid a race condition writing a single atomic JS script to check both conditions. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/tests/functional/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py index 5a75812d9..0f3cf136e 100644 --- a/plinth/tests/functional/__init__.py +++ b/plinth/tests/functional/__init__.py @@ -184,7 +184,16 @@ def change_checkbox_status(browser, app_name, checkbox_id, def wait_for_config_update(browser, app_name): - while browser.is_element_present_by_css('.running-status.loading'): + """Wait until the configuration update progress goes away. + + Perform an atomic check that page is fully loaded and that progress icon is + not present at the same time to avoid race conditions due to automatic page + reloads. + + """ + script = 'return (document.readyState == "complete") && ' \ + '(!Boolean(document.querySelector(".running-status.loading")));' + while not browser.execute_script(script): time.sleep(0.1) From 61d36e43fec9aeec2e93449b1e529dc060690374 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 28 May 2020 11:02:23 -0700 Subject: [PATCH 10/90] users: tests: functional: Leave no-language as final setting - Whenever functional tests for user app are run, the tester user is left with the last language that is tested. This is a minor inconvenience. Fix this by adding no-language option to test at the end. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/users/tests/test_functional.py | 2 ++ plinth/modules/users/tests/users.feature | 1 + 2 files changed, 3 insertions(+) diff --git a/plinth/modules/users/tests/test_functional.py b/plinth/modules/users/tests/test_functional.py index 02cfd78dc..fc642dbab 100644 --- a/plinth/modules/users/tests/test_functional.py +++ b/plinth/modules/users/tests/test_functional.py @@ -10,6 +10,7 @@ from plinth.tests import functional scenarios('users.feature') _language_codes = { + 'None': '', 'Deutsch': 'de', 'Nederlands': 'nl', 'Português': 'pt', @@ -26,6 +27,7 @@ _language_codes = { } _config_page_title_language_map = { + '': 'General Configuration', 'da': 'Generel Konfiguration', 'de': 'Allgemeine Konfiguration', 'es': 'Configuración general', diff --git a/plinth/modules/users/tests/users.feature b/plinth/modules/users/tests/users.feature index fcebebd10..ddb70ab14 100644 --- a/plinth/modules/users/tests/users.feature +++ b/plinth/modules/users/tests/users.feature @@ -51,3 +51,4 @@ Scenario Outline: Change language | తెలుగు | | Türkçe | | 简体中文 | + | None | From 1db2f4982aba180795402dff6805bf1b79fbedd4 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 28 May 2020 14:26:07 -0700 Subject: [PATCH 11/90] mldonkey: tests: functional: Wait for frame to load properly Before counting the number of downloads. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/mldonkey/tests/test_functional.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plinth/modules/mldonkey/tests/test_functional.py b/plinth/modules/mldonkey/tests/test_functional.py index 5a53cc855..6306e88ef 100644 --- a/plinth/modules/mldonkey/tests/test_functional.py +++ b/plinth/modules/mldonkey/tests/test_functional.py @@ -58,5 +58,6 @@ def _get_number_of_ed2k_files(browser): '//tr//td[contains(text(), "Transfers")]').click() with browser.get_iframe('output') as output_frame: + functional.eventually(output_frame.find_by_css, ['.downloaded']) return len(output_frame.find_by_css('.dl-1')) + len( output_frame.find_by_css('.dl-2')) From e36818e591c2e7127c1689e41db550d1a489dfd0 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 28 May 2020 14:27:20 -0700 Subject: [PATCH 12/90] snapshot: tests: functional: Delete all snapshots properly Don't depend on number of snapshots being 0 to conclude that all snapshots have been deleted instead use the disabled state of the 'Delete Selected' button. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/snapshot/tests/test_functional.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plinth/modules/snapshot/tests/test_functional.py b/plinth/modules/snapshot/tests/test_functional.py index 0a5fc1894..6016ad957 100644 --- a/plinth/modules/snapshot/tests/test_functional.py +++ b/plinth/modules/snapshot/tests/test_functional.py @@ -77,9 +77,11 @@ def snapshot_assert_configuration(session_browser, free_space, def _delete_all(browser): - if _get_count(browser): + functional.visit(browser, '/plinth/sys/snapshot/manage/') + delete_button = browser.find_by_name('delete_selected').first + if not delete_button['disabled']: browser.find_by_id('select-all').check() - functional.submit(browser, browser.find_by_name('delete_selected')) + functional.submit(browser, delete_button) confirm_button = browser.find_by_name('delete_confirm') if confirm_button: # Only if redirected to confirm page From ad5fae2180597f05871710513cddef09b36d71b3 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 28 May 2020 14:29:52 -0700 Subject: [PATCH 13/90] ejabberd: tests: functional: Fixes for no implicit waiting - When 'relogin' hit is seen after page load, reload the page instead of trying to login as it does not always succeed. - Wait after the page load until the connection with jabber server has been checked. - Wait until the roster appears after login. - Wait until contact appears after contact is added. - Wait for popups and dialogs when deleting the contact. - Wait for contact to appear when checking for contact. Don't reload on every check. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- .../modules/ejabberd/tests/test_functional.py | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/plinth/modules/ejabberd/tests/test_functional.py b/plinth/modules/ejabberd/tests/test_functional.py index 7a2755019..8fcae0d89 100644 --- a/plinth/modules/ejabberd/tests/test_functional.py +++ b/plinth/modules/ejabberd/tests/test_functional.py @@ -22,7 +22,7 @@ def ejabberd_delete_contact(session_browser): @then('I should have a contact on my roster') def ejabberd_should_have_contact(session_browser): - assert functional.eventually(_jsxc_has_contact, [session_browser]) + _jsxc_assert_has_contact(session_browser) @when(parsers.parse('I enable message archive management')) @@ -53,16 +53,17 @@ def _jsxc_login(browser): """Login to JSXC.""" username = functional.config['DEFAULT']['username'] password = functional.config['DEFAULT']['password'] - functional.access_url(browser, 'jsxc') + functional.visit(browser, '/plinth/apps/jsxc/jsxc/') + assert functional.eventually(browser.find_by_text, + ['BOSH Server reachable.']) + if browser.find_by_text('relogin'): + browser.reload() + browser.find_by_id('jsxc-username').fill(username) browser.find_by_id('jsxc-password').fill(password) browser.find_by_id('jsxc-submit').click() - relogin = browser.find_by_text('relogin') - if relogin: - relogin.first.click() - browser.find_by_id('jsxc_username').fill(username) - browser.find_by_id('jsxc_password').fill(password) - browser.find_by_text('Connect').first.click() + assert functional.eventually(browser.find_by_css, + ['#jsxc_roster.jsxc_state_shown']) def _jsxc_add_contact(browser): @@ -75,18 +76,23 @@ def _jsxc_add_contact(browser): new.first.click() browser.find_by_id('jsxc_username').fill('alice@localhost') browser.find_by_text('Add').first.click() + assert functional.eventually(browser.find_by_text, ['alice@localhost']) def _jsxc_delete_contact(browser): """Delete the contact from JSXC user's roster.""" _jsxc_login(browser) + + # noqa, pylint: disable=unnecessary-lambda + functional.eventually(browser.find_by_css, ['div.jsxc_more']) browser.find_by_css('div.jsxc_more').first.click() + functional.eventually(browser.find_by_text, ['delete contact']) browser.find_by_text('delete contact').first.click() + functional.eventually(browser.find_by_text, ['Remove']) browser.find_by_text('Remove').first.click() -def _jsxc_has_contact(browser): +def _jsxc_assert_has_contact(browser): """Check whether the contact is in JSXC user's roster.""" _jsxc_login(browser) - contact = browser.find_by_text('alice@localhost') - return bool(contact) + assert functional.eventually(browser.find_by_text, ['alice@localhost']) From 29f971a2727713152f88d340a834003227e870e6 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 28 May 2020 16:14:46 -0700 Subject: [PATCH 14/90] syncthing: tests: functional: Fix to wait properly Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/syncthing/tests/test_functional.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plinth/modules/syncthing/tests/test_functional.py b/plinth/modules/syncthing/tests/test_functional.py index 6709948d1..e927030bc 100644 --- a/plinth/modules/syncthing/tests/test_functional.py +++ b/plinth/modules/syncthing/tests/test_functional.py @@ -78,6 +78,7 @@ def _load_main_interface(browser): lambda: browser.evaluate_script('document.is_ui_online'), timeout=5) # Dismiss the Usage Reporting consent dialog + functional.eventually(browser.find_by_id, ['ur']) usage_reporting = browser.find_by_id('ur').first functional.eventually(lambda: usage_reporting.visible, timeout=2) if usage_reporting.visible: From 176dc69fc5a45dd1de7443d543389c172fb0254e Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 28 May 2020 09:12:52 -0700 Subject: [PATCH 15/90] tests: functional: Remove implicit and explicit wait times - Splinter/selenium have implicit and explicit waiting time. Implicit wait time will make every negative lookup wait for about 3 seconds before it actually fails. Because we ensure missing elements in quite a few places, this introduces many 3 seconds wait periods during testing. Remove it instead rely on explicit waiting whenever needed. - Explicit wait time is only used during explicitly requests waiting conditions. In a loop the API waits for a maximum of timeout period until a given condition is satisfied. Each time the condition is checked, it goes into sleep for explicit wait period amount of time. This is typically a second or so. Since we are impatient, make it 0.1 instead. - Also make sure that whenever a page is visit()ed, we automatically wait until the page is fully loaded by overriding the splinter wait condition. Otherwise, we will need to introduce waiting code in a lot of places. - Using document.readyState == complete is a better check to ensure that a page is fully loaded. If we proceed with the page 'loading' or 'interactive' state, we will have to change a lot of code to make it wait. - Handle Apache restarts when waiting for page load. The error page apparently is never reaches document.readyState == 'complete'. So, if an error page is encountered, always reload. - While waiting for installation, ensure that we atomically check that page has loaded fully and the installation progress is not being shown. Otherwise, there would be race condition due to installation page refreshing itself. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- conftest.py | 26 +++++++++++++++ plinth/templates/setup.html | 6 ++-- plinth/tests/functional/__init__.py | 51 +++++++++++++---------------- 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/conftest.py b/conftest.py index 873409358..d892c6120 100644 --- a/conftest.py +++ b/conftest.py @@ -85,3 +85,29 @@ def fixture_needs_sudo(): """Skip test if sudo command is not available.""" if not os.path.isfile('/usr/bin/sudo'): pytest.skip('Needs sudo command installed.') + + +@pytest.fixture(scope='session') +def splinter_selenium_implicit_wait(): + """Disable implicit waiting.""" + return 0 + + +@pytest.fixture(scope='session') +def splinter_wait_time(): + """Disable explicit waiting.""" + return 0.01 + + +@pytest.fixture(scope='session') +def splinter_browser_load_condition(): + """When a page it loaded, wait until is available.""" + + def _load_condition(browser): + if browser.url == 'about:blank': + return True + + ready_state = browser.execute_script('return document.readyState;') + return ready_state == 'complete' + + return _load_condition diff --git a/plinth/templates/setup.html b/plinth/templates/setup.html index 9ddbf5aa6..760373691 100644 --- a/plinth/templates/setup.html +++ b/plinth/templates/setup.html @@ -79,16 +79,16 @@ {% else %} {% if setup_current_operation.step == 'pre' %} -
+
{% trans "Performing pre-install operation" %}
{% elif setup_current_operation.step == 'post' %} -
+
{% trans "Performing post-install operation" %}
{% elif setup_current_operation.step == 'install' %} {% with transaction=setup_current_operation.transaction %} -
+
{% blocktrans trimmed with package_names=transaction.package_names|join:", " status=transaction.status_string %} Installing {{ package_names }}: {{ status }} {% endblocktrans %} diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py index 0f3cf136e..56138bb7c 100644 --- a/plinth/tests/functional/__init__.py +++ b/plinth/tests/functional/__init__.py @@ -94,11 +94,22 @@ class _PageLoaded(): try: self.element.has_class('whatever_class') except StaleElementReferenceException: - if self.expected_url is None: + # After a domain name change, Let's Encrypt will reload the web + # server and could cause a connection failure. + if driver.find_by_id('netErrorButtonContainer'): + driver.visit(driver.url) + return False + + is_fully_loaded = driver.execute_script( + 'return document.readyState;') == 'complete' + if not is_fully_loaded: + is_stale = False + elif self.expected_url is None: is_stale = True else: if driver.url.endswith(self.expected_url): is_stale = True + return is_stale @@ -273,33 +284,19 @@ def is_installed(browser, app_name): def install(browser, app_name): install_button = _find_install_button(browser, app_name) + if not install_button: + return - def install_in_progress(): - selectors = [ - '.install-state-' + state - for state in ['pre', 'post', 'installing'] - ] - return any( - browser.is_element_present_by_css(selector) - for selector in selectors) - - def is_server_restarting(): - return browser.is_element_present_by_css('.neterror') - - def wait_for_install(): - if install_in_progress(): - time.sleep(1) - elif is_server_restarting(): - time.sleep(1) + install_button.click() + while True: + script = 'return (document.readyState == "complete") && ' \ + '(!Boolean(document.querySelector(".installing")));' + if not browser.execute_script(script): + time.sleep(0.1) + elif browser.is_element_present_by_css('.neterror'): browser.visit(browser.url) else: - return - wait_for_install() - - if install_button: - install_button.click() - wait_for_install() - # sleep(2) # XXX This shouldn't be required. + break ################################ @@ -349,10 +346,6 @@ def set_domain_name(browser, domain_name): nav_to_module(browser, 'config') browser.find_by_id('id_domainname').fill(domain_name) submit(browser) - # After a domain name change, Let's Encrypt will reload the web server and - # could cause a connection failure. - if browser.find_by_id('netErrorButtonContainer'): - browser.visit(browser.url) ######################## From 28b5ad91911d0d671ef923c9b441f22cb7997bd3 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 28 May 2020 16:57:08 -0700 Subject: [PATCH 16/90] tests: functional: Allow parallel installation of apps - By waiting for one app to finish installing before trying to install. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/tests/functional/__init__.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py index 56138bb7c..0aded8d41 100644 --- a/plinth/tests/functional/__init__.py +++ b/plinth/tests/functional/__init__.py @@ -272,22 +272,14 @@ def app_select_domain_name(browser, app_name, domain_name): ######################### # App install utilities # ######################### -def _find_install_button(browser, app_name): - nav_to_module(browser, app_name) - return browser.find_by_css('.form-install input[type=submit]') - - def is_installed(browser, app_name): - install_button = _find_install_button(browser, app_name) - return not bool(install_button) + nav_to_module(browser, app_name) + return not bool(browser.find_by_css('.form-install input[type=submit]')) def install(browser, app_name): - install_button = _find_install_button(browser, app_name) - if not install_button: - return - - install_button.click() + nav_to_module(browser, app_name) + install_button_css = '.form-install input[type=submit]' while True: script = 'return (document.readyState == "complete") && ' \ '(!Boolean(document.querySelector(".installing")));' @@ -295,6 +287,18 @@ def install(browser, app_name): time.sleep(0.1) elif browser.is_element_present_by_css('.neterror'): browser.visit(browser.url) + elif browser.is_element_present_by_css(install_button_css): + install_button = browser.find_by_css(install_button_css).first + if install_button['disabled']: + if not browser.find_by_name('refresh-packages'): + # Package manager is busy, wait and refresh page + time.sleep(1) + browser.visit(browser.url) + else: + # This app is not available in this distribution + raise Exception('App not available in distribution') + else: + install_button.click() else: break From cf7ff2d1b6bb90a6b5aac43c06ec30c5eb7bd4b2 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Wed, 17 Jun 2020 11:34:49 -0400 Subject: [PATCH 17/90] upgrades: Combine into single page with manual update Manual update is placed in a new section under Configuration. Tests: - Ran manual update with packages to be upgraded. - Busy indicator is shown as expected. - Log display button appears when logs are available. - Logs can be expanded and collapsed. Closes: #1771. Signed-off-by: James Valleroy [sunil: Change the update now button into default priority button] Signed-off-by: Sunil Mohan Adapa Reviewed-by: Sunil Mohan Adapa --- .../modules/upgrades/templates/upgrades.html | 74 ------------------- .../templates/upgrades_configure.html | 63 ++++++++++++++-- plinth/modules/upgrades/views.py | 17 ++--- plinth/templates/app.html | 3 + 4 files changed, 69 insertions(+), 88 deletions(-) delete mode 100644 plinth/modules/upgrades/templates/upgrades.html diff --git a/plinth/modules/upgrades/templates/upgrades.html b/plinth/modules/upgrades/templates/upgrades.html deleted file mode 100644 index 8411e025b..000000000 --- a/plinth/modules/upgrades/templates/upgrades.html +++ /dev/null @@ -1,74 +0,0 @@ -{% extends 'base.html' %} -{% comment %} -# SPDX-License-Identifier: AGPL-3.0-or-later -{% endcomment %} - -{% load i18n %} -{% load static %} - -{% block page_head %} - - {% if is_busy %} - - {% endif %} - -{% endblock %} - - -{% block content %} - -

{{ title }}

- - {% if not is_busy %} -

-

- {% csrf_token %} - - -
-

- {% endif %} - - {% if is_busy %} - - {% endif %} - -

- {% blocktrans trimmed %} - 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. - {% endblocktrans %} -

- - {% if log %} -

- - -

-
{{ log }}
-
-

- {% endif %} - -{% endblock %} - -{% block page_js %} - - {% if is_busy %} - - {% endif %} -{% endblock %} diff --git a/plinth/modules/upgrades/templates/upgrades_configure.html b/plinth/modules/upgrades/templates/upgrades_configure.html index 3533410e9..f2ef8ea33 100644 --- a/plinth/modules/upgrades/templates/upgrades_configure.html +++ b/plinth/modules/upgrades/templates/upgrades_configure.html @@ -5,10 +5,63 @@ {% load bootstrap %} {% load i18n %} +{% load static %} -{% block status %} - - {% trans 'Manual update' %} - +{% block page_head %} + {% if is_busy %} + + {% endif %} +{% endblock %} + +{% block extra_content %} +

{% trans "Manual update" %}

+ {% if is_busy %} +

+ +

+ {% else %} +

+

+ {% csrf_token %} + +
+

+ {% endif %} + +

+ {% blocktrans trimmed %} + 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. + {% endblocktrans %} +

+ + {% if log %} +

+ + +

+
{{ log }}
+
+

+ {% endif %} +{% endblock %} + +{% block page_js %} + {% if is_busy %} + + {% endif %} {% endblock %} diff --git a/plinth/modules/upgrades/views.py b/plinth/modules/upgrades/views.py index 4e0549f2c..8413df8aa 100644 --- a/plinth/modules/upgrades/views.py +++ b/plinth/modules/upgrades/views.py @@ -4,7 +4,7 @@ FreedomBox app for upgrades. """ from django.contrib import messages -from django.template.response import TemplateResponse +from django.shortcuts import redirect from django.urls import reverse_lazy from django.utils.translation import ugettext as _ @@ -26,6 +26,12 @@ class UpgradesConfigurationView(AppView): def get_initial(self): return {'auto_upgrades_enabled': upgrades.is_enabled()} + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context['is_busy'] = package.is_package_manager_busy() + context['log'] = get_log() + return context + def form_valid(self, form): """Apply the form changes.""" old_status = form.initial @@ -62,18 +68,11 @@ def get_log(): def upgrade(request): """Serve the upgrade page.""" - is_busy = package.is_package_manager_busy() - if request.method == 'POST': try: actions.superuser_run('upgrades', ['run']) messages.success(request, _('Upgrade process started.')) - is_busy = True except ActionError: messages.error(request, _('Starting upgrade failed.')) - return TemplateResponse(request, 'upgrades.html', { - 'title': _('Manual update'), - 'is_busy': is_busy, - 'log': get_log() - }) + return redirect(reverse_lazy('upgrades:index')) diff --git a/plinth/templates/app.html b/plinth/templates/app.html index 21eb4d50d..b24f817e6 100644 --- a/plinth/templates/app.html +++ b/plinth/templates/app.html @@ -56,4 +56,7 @@ {% endif %} {% endblock %} + {% block extra_content %} + {% endblock %} + {% endblock %} From fea795652781a0c7a3f177b951522deb7c03c684 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Wed, 17 Jun 2020 11:01:32 -0400 Subject: [PATCH 18/90] upgrades: Skip enable-auto in develop mode Signed-off-by: James Valleroy Reviewed-by: Sunil Mohan Adapa --- plinth/modules/upgrades/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plinth/modules/upgrades/__init__.py b/plinth/modules/upgrades/__init__.py index 6b6d91331..7d83ca633 100644 --- a/plinth/modules/upgrades/__init__.py +++ b/plinth/modules/upgrades/__init__.py @@ -100,7 +100,7 @@ def setup(helper, old_version=None): helper.install(managed_packages) # Enable automatic upgrades but only on first install - if not old_version: + if not old_version and not cfg.develop: helper.call('post', actions.superuser_run, 'upgrades', ['enable-auto']) # Update apt preferences whenever on first install and on version From 28e19abf463447070a3d98beb3a0d9ad605e01a1 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 17 Jun 2020 21:00:27 -0700 Subject: [PATCH 19/90] d/control: Add python3-systemd as a dependency Module systemd.journal is used for writing proper structured messages to systemd-journald. This was earlier only a recommends. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- debian/control | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/debian/control b/debian/control index d14cd7ded..7851dc283 100644 --- a/debian/control +++ b/debian/control @@ -104,6 +104,7 @@ Depends: python3-psutil, python3-requests, python3-ruamel.yaml, + python3-systemd, python3-yaml, sudo, wget, @@ -155,8 +156,6 @@ Recommends: powermgmt-base, # fuser, pstree and other utilities psmisc, -# Optional FreedomBox dependency - python3-systemd, # Manage /etc/resolv.conf resolvconf, # Tool to kill WLAN, Bluetooth and moble broadband From f07a81ef391e112d9de460eb6e027e9991ede849 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 17 Jun 2020 23:12:16 -0700 Subject: [PATCH 20/90] apache: Add ssl-cert package as dependency The make-ssl-cert command is used during initial setup so it is a hard dependency. Earlier it was being included as a Recommends: via apache2 package. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/apache/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plinth/modules/apache/__init__.py b/plinth/modules/apache/__init__.py index 7028e955f..c598a6930 100644 --- a/plinth/modules/apache/__init__.py +++ b/plinth/modules/apache/__init__.py @@ -19,7 +19,7 @@ is_essential = True managed_services = ['apache2'] -managed_packages = ['apache2', 'php-fpm'] +managed_packages = ['apache2', 'php-fpm', 'ssl-cert'] app = None From aac511d534ca2ef2700bdd10a63aaf25b836f48f Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Mon, 22 Jun 2020 10:48:39 -0400 Subject: [PATCH 21/90] debian: Add nscd >= 2 as dependency This is a slightly hacky way to ensure that nscd package is installed rather than unscd (which provides nscd as a virtual package). This will work as long as unscd does not jump to a version 2. It is currently 0.53-1 and has little recent activity, so this condition seems likely to hold in the near future. Tests: - In vagrant box, installed unscd. Installed modified freedombox deb. Saw that unscd was removed and nscd was installed. - In DigitalOcean droplet, reproduced issue from #1877. Installed modified freedombox deb. Saw that unscd was removed (no other packages were removed) and nscd was installed. FreedomBox interface was available again. - Built a freedom-maker image with modified freedombox deb. Checked build log that nscd was installed and unscd was not installed. Closes: #1877. Signed-off-by: James Valleroy Reviewed-by: Sunil Mohan Adapa --- debian/control | 2 ++ 1 file changed, 2 insertions(+) diff --git a/debian/control b/debian/control index 7851dc283..daf96a30e 100644 --- a/debian/control +++ b/debian/control @@ -85,6 +85,8 @@ Depends: lsof, netcat-openbsd, network-manager, +# Ensure that nscd is installed rather than unscd. + nscd (>= 2), ppp, pppoe, python3-apt, From f97902615b60ee027dc5fa897d8e23cdba8f7bbc Mon Sep 17 00:00:00 2001 From: Veiko Aasa Date: Mon, 22 Jun 2020 21:59:08 +0300 Subject: [PATCH 22/90] functional-tests: Handle connection error when web server restarts Catch exeptions if web server restarts on form submit. Signed-off-by: Veiko Aasa Reviewed-by: Sunil Mohan Adapa --- plinth/tests/functional/__init__.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py index 0aded8d41..52379847e 100644 --- a/plinth/tests/functional/__init__.py +++ b/plinth/tests/functional/__init__.py @@ -12,7 +12,8 @@ import time from contextlib import contextmanager import requests -from selenium.common.exceptions import StaleElementReferenceException +from selenium.common.exceptions import (WebDriverException, + StaleElementReferenceException) from selenium.webdriver.support.ui import WebDriverWait config = configparser.ConfigParser() @@ -94,10 +95,13 @@ class _PageLoaded(): try: self.element.has_class('whatever_class') except StaleElementReferenceException: - # After a domain name change, Let's Encrypt will reload the web + # After a domain name change, Let's Encrypt will restart the web # server and could cause a connection failure. if driver.find_by_id('netErrorButtonContainer'): - driver.visit(driver.url) + try: + driver.visit(driver.url) + except WebDriverException: + pass return False is_fully_loaded = driver.execute_script( @@ -116,7 +120,12 @@ class _PageLoaded(): @contextmanager def wait_for_page_update(browser, timeout=300, expected_url=None): page_body = browser.find_by_tag('body').first - yield + try: + yield + except WebDriverException: + # ignore a connection failure which may happen after web server restart + pass + WebDriverWait(browser, timeout).until(_PageLoaded(page_body, expected_url)) From 0c59dbb0e47739a27794d29be2cf505c099b6741 Mon Sep 17 00:00:00 2001 From: Veiko Aasa Date: Mon, 22 Jun 2020 23:18:20 +0300 Subject: [PATCH 23/90] functional-tests: Skip tests if app is not available in distribution Signed-off-by: Veiko Aasa Reviewed-by: Sunil Mohan Adapa --- plinth/tests/functional/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py index 52379847e..133b09db8 100644 --- a/plinth/tests/functional/__init__.py +++ b/plinth/tests/functional/__init__.py @@ -11,6 +11,7 @@ import tempfile import time from contextlib import contextmanager +import pytest import requests from selenium.common.exceptions import (WebDriverException, StaleElementReferenceException) @@ -305,7 +306,7 @@ def install(browser, app_name): browser.visit(browser.url) else: # This app is not available in this distribution - raise Exception('App not available in distribution') + pytest.skip('App not available in distribution') else: install_button.click() else: From 34a28e35c991eed24cad5cc9e64c58164e9838e3 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Tue, 23 Jun 2020 20:14:38 -0400 Subject: [PATCH 24/90] upgrades: Append unattended-upgrades-dpkg.log for more detail Signed-off-by: James Valleroy Reviewed-by: Sunil Mohan Adapa --- actions/upgrades | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/actions/upgrades b/actions/upgrades index d37297498..86c91f10a 100755 --- a/actions/upgrades +++ b/actions/upgrades @@ -15,6 +15,7 @@ from plinth.modules.apache.components import check_url AUTO_CONF_FILE = '/etc/apt/apt.conf.d/20auto-upgrades' LOG_FILE = '/var/log/unattended-upgrades/unattended-upgrades.log' +DPKG_LOG_FILE = '/var/log/unattended-upgrades/unattended-upgrades-dpkg.log' BUSTER_BACKPORTS_RELEASE_FILE_URL = \ 'https://deb.debian.org/debian/dists/buster-backports/Release' @@ -132,11 +133,19 @@ def subcommand_disable_auto(_): def subcommand_get_log(_): """Print the automatic upgrades log.""" try: + print('==> ' + os.path.basename(LOG_FILE)) with open(LOG_FILE, 'r') as file_handle: print(file_handle.read()) except IOError: pass + try: + print('==> ' + os.path.basename(DPKG_LOG_FILE)) + with open(DPKG_LOG_FILE, 'r') as file_handle: + print(file_handle.read()) + except IOError: + pass + def _get_protocol(): """Return the protocol to use for newly added repository sources.""" From 225d86e344d51ef8856321d9b063d4ddddfac194 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 29 May 2020 16:09:07 -0700 Subject: [PATCH 25/90] storage: Use DBus directly for listing disks Bring us closer to avoiding the use of two different methods to access UDisks DBus API: UDisks2 client library and direct DBus access with GDBus. Perform formatting of the bytes outside of udisks2 module to avoid depending on Django. Tests performed: - Visit the storage page. Disks are listed properly. - Sizes are formatted to be human readable. - Filesystem type is show properly: ext4, btrfs - Labels for disks are shown as set by tune2fs etc. - Device paths are shown properly. - Mount point is shown properly. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/storage/__init__.py | 41 ++++-------------------------- plinth/modules/storage/udisks2.py | 38 ++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/plinth/modules/storage/__init__.py b/plinth/modules/storage/__init__.py index f7c2ba935..313089e89 100644 --- a/plinth/modules/storage/__init__.py +++ b/plinth/modules/storage/__init__.py @@ -13,7 +13,7 @@ from django.utils.translation import ugettext_noop from plinth import actions from plinth import app as app_module -from plinth import cfg, glib, menu, utils +from plinth import cfg, glib, menu from plinth.errors import ActionError, PlinthError from plinth.utils import format_lazy, import_from_gi @@ -77,50 +77,19 @@ def init(): def get_disks(): """Returns list of disks by combining information from df and udisks.""" disks = _get_disks_from_df() - disks_from_udisks = _get_disks_from_udisks() + disks_from_udisks = udisks2.get_disks() + for disk in disks_from_udisks: + disk['size'] = format_bytes(disk['size']) # Add info from udisks to the disks from df based on mount point. for disk_from_df in disks: for disk_from_udisks in disks_from_udisks: - if disk_from_udisks['mount_point'] == disk_from_df['mount_point']: + if disk_from_df['mount_point'] in disk_from_udisks['mount_points']: disk_from_df.update(disk_from_udisks) return sorted(disks, key=lambda disk: disk['device']) -def _get_disks_from_udisks(): - """List devices that can be ejected.""" - udisks = utils.import_from_gi('UDisks', '2.0') - client = udisks.Client.new_sync() - object_manager = client.get_object_manager() - devices = [] - - for obj in object_manager.get_objects(): - - if not obj.get_block(): - continue - - block = obj.get_block() - - if block.props.id_usage != 'filesystem': - continue - - device = { - 'device': block.props.device, - 'label': block.props.id_label, - 'size': format_bytes(block.props.size), - 'filesystem_type': block.props.id_type, - 'is_removable': not block.props.hint_system - } - try: - device['mount_point'] = obj.get_filesystem().props.mount_points[0] - except Exception: - continue - devices.append(device) - - return devices - - def _get_disks_from_df(): """Return the list of disks and free space available using 'df'.""" try: diff --git a/plinth/modules/storage/udisks2.py b/plinth/modules/storage/udisks2.py index e662ea75e..a91c2aded 100644 --- a/plinth/modules/storage/udisks2.py +++ b/plinth/modules/storage/udisks2.py @@ -79,7 +79,7 @@ class Proxy: if signature == 'aay': return [bytes(value_item).decode()[:-1] for value_item in value] - if signature in ('s', 'b', 'o', 'u'): + if signature in ('s', 'b', 'o', 'u', 't'): return glib.Variant.unpack(value) raise ValueError('Unhandled type') @@ -100,7 +100,11 @@ class BlockDevice(Proxy): 'hint_ignore': ('b', 'HintIgnore'), 'hint_system': ('b', 'HintSystem'), 'id': ('s', 'Id'), + 'id_label': ('s', 'IdLabel'), + 'id_type': ('s', 'IdType'), + 'id_usage': ('s', 'IdUsage'), 'preferred_device': ('ay', 'PreferredDevice'), + 'size': ('t', 'Size'), 'symlinks': ('aay', 'Symlinks'), } @@ -119,6 +123,38 @@ class Filesystem(Proxy): properties = {'mount_points': ('aay', 'MountPoints')} +def get_disks(): + """List devices that can be ejected.""" + devices = [] + + manager = _get_dbus_proxy(_OBJECTS['UDisks2'], + _INTERFACES['ObjectManager']) + objects = manager.GetManagedObjects() + for object_, interface_and_properties in objects.items(): + if _INTERFACES['Block'] not in interface_and_properties: + continue + + block = BlockDevice(object_) + if block.id_usage != 'filesystem': + continue + + device = { + 'device': block.device, + 'label': block.id_label, + 'size': block.size, + 'filesystem_type': block.id_type, + 'is_removable': not block.hint_system + } + try: + file_system = Filesystem(object_) + device['mount_points'] = file_system.mount_points + except Exception: + continue + devices.append(device) + + return devices + + def _mount(object_path): """Start the mount operation on an block device. From 955dfea866550cfdf70576aefdff33026d499f30 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 29 May 2020 16:58:30 -0700 Subject: [PATCH 26/90] storage: Fix regression with showing error messages In ed09028fcd2c850c3b87b65de66187b214190150, when eject was made to run as superuser inside storage action, parsing of the error messages was not handled properly. Fix it to show simple error messages about why the eject was not successful. Tests performed: - In a terminal, switch to the directory where a disk is mounted to keep the mount point busy. Attempt to eject the disk. A large stack trace is shown without the patch and a clean error message is shown with it. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- actions/storage | 8 ++- plinth/modules/storage/__init__.py | 79 +++++++++++++++--------------- plinth/modules/storage/views.py | 10 ++-- 3 files changed, 50 insertions(+), 47 deletions(-) diff --git a/actions/storage b/actions/storage index 6d5b97602..44da86c53 100755 --- a/actions/storage +++ b/actions/storage @@ -259,8 +259,12 @@ def subcommand_mount(arguments): def subcommand_eject(arguments): """Eject a device by its path.""" device_path = arguments.device - drive = eject_drive_of_device(device_path) - print(json.dumps(drive)) + try: + drive = eject_drive_of_device(device_path) + print(json.dumps(drive)) + except Exception as exception: + print(exception, file=sys.stderr) + sys.exit(1) def _get_options(): diff --git a/plinth/modules/storage/__init__.py b/plinth/modules/storage/__init__.py index 313089e89..b2d0e1949 100644 --- a/plinth/modules/storage/__init__.py +++ b/plinth/modules/storage/__init__.py @@ -15,7 +15,7 @@ from plinth import actions from plinth import app as app_module from plinth import cfg, glib, menu from plinth.errors import ActionError, PlinthError -from plinth.utils import format_lazy, import_from_gi +from plinth.utils import format_lazy from . import udisks2 from .manifest import backup # noqa, pylint: disable=unused-import @@ -198,45 +198,46 @@ def format_bytes(size): def get_error_message(error): """Return an error message given an exception.""" - udisks = import_from_gi('UDisks', '2.0') - if error.matches(udisks.Error.quark(), udisks.Error.FAILED): - message = _('The operation failed.') - elif error.matches(udisks.Error.quark(), udisks.Error.CANCELLED): - message = _('The operation was cancelled.') - elif error.matches(udisks.Error.quark(), udisks.Error.ALREADY_UNMOUNTING): - message = _('The device is already unmounting.') - elif error.matches(udisks.Error.quark(), udisks.Error.NOT_SUPPORTED): - message = _('The operation is not supported due to ' - 'missing driver/tool support.') - elif error.matches(udisks.Error.quark(), udisks.Error.TIMED_OUT): - message = _('The operation timed out.') - elif error.matches(udisks.Error.quark(), udisks.Error.WOULD_WAKEUP): - message = _('The operation would wake up a disk that is ' - 'in a deep-sleep state.') - elif error.matches(udisks.Error.quark(), udisks.Error.DEVICE_BUSY): - message = _('Attempting to unmount a device that is busy.') - elif error.matches(udisks.Error.quark(), udisks.Error.ALREADY_CANCELLED): - message = _('The operation has already been cancelled.') - elif error.matches(udisks.Error.quark(), udisks.Error.NOT_AUTHORIZED) or \ - error.matches(udisks.Error.quark(), - udisks.Error.NOT_AUTHORIZED_CAN_OBTAIN) or \ - error.matches(udisks.Error.quark(), - udisks.Error.NOT_AUTHORIZED_DISMISSED): - message = _('Not authorized to perform the requested operation.') - elif error.matches(udisks.Error.quark(), udisks.Error.ALREADY_MOUNTED): - message = _('The device is already mounted.') - elif error.matches(udisks.Error.quark(), udisks.Error.NOT_MOUNTED): - message = _('The device is not mounted.') - elif error.matches(udisks.Error.quark(), - udisks.Error.OPTION_NOT_PERMITTED): - message = _('Not permitted to use the requested option.') - elif error.matches(udisks.Error.quark(), - udisks.Error.MOUNTED_BY_OTHER_USER): - message = _('The device is mounted by another user.') - else: - message = error.message + error_parts = error.split(':') + if error_parts[0] != 'udisks-error-quark': + return error - return message + short_error = error_parts[2].strip().split('.')[-1] + message_map = { + 'Failed': + _('The operation failed.'), + 'Cancelled': + _('The operation was cancelled.'), + 'AlreadyUnmounting': + _('The device is already unmounting.'), + 'NotSupported': + _('The operation is not supported due to missing driver/tool ' + 'support.'), + 'TimedOut': + _('The operation timed out.'), + 'WouldWakeup': + _('The operation would wake up a disk that is in a deep-sleep ' + 'state.'), + 'DeviceBusy': + _('Attempting to unmount a device that is busy.'), + 'AlreadyCancelled': + _('The operation has already been cancelled.'), + 'NotAuthorized': + _('Not authorized to perform the requested operation.'), + 'NotAuthorizedCanObtain': + _('Not authorized to perform the requested operation.'), + 'NotAuthorizedDismissed': + _('Not authorized to perform the requested operation.'), + 'AlreadyMounted': + _('The device is already mounted.'), + 'NotMounted': + _('The device is not mounted.'), + 'OptionNotPermitted': + _('Not permitted to use the requested option.'), + 'MountedByOtherUser': + _('The device is mounted by another user.') + } + return message_map.get(short_error, error) def setup(helper, old_version=None): diff --git a/plinth/modules/storage/views.py b/plinth/modules/storage/views.py index 7a239372f..d1294ca78 100644 --- a/plinth/modules/storage/views.py +++ b/plinth/modules/storage/views.py @@ -15,6 +15,7 @@ from django.utils.translation import ugettext as _ from django.views.decorators.http import require_POST from plinth import actions, views +from plinth.errors import ActionError from plinth.modules import storage from . import get_error_message @@ -92,13 +93,10 @@ def eject(request, device_path): drive_model=drive['model'])) else: messages.success(request, _('Device can be safely unplugged.')) - except Exception as exception: - try: - message = get_error_message(exception) - except AttributeError: - message = str(exception) + except ActionError as exception: + message = get_error_message(exception.args[2]) - logger.exception('Error ejecting device - %s', message) + logger.error('Error ejecting device - %s', message) messages.error( request, _('Error ejecting device: {error_message}').format( From 10b46f1968845df1999d0bd9cf468cb93325cd80 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 29 May 2020 17:38:31 -0700 Subject: [PATCH 27/90] storage: Use UDisks information as primary source Rename get_disks() to get_mounts() and use it in for backups and samba shares. Create a new get_disks() similar to get_mounts() but use df information only for showing free space. This inverts the importance of 'df' and UDisks. Use UDisks as primary source of information for showing list of disks and then use df to fill in the free space information. - Retrieve all the mount points of a device and return them as part of get_disks() in an extra 'mount_points' property. - For storage listing, this fixes showing up of /.snapshots as separate disk and showing of vboxsf, network mounts etc. Only shows mounts that are related to block devices. - Update various uses of get_disks() within storage module to use 'mounts_points' instead of 'mount_point' to be accurate in cases where there are multiple mounts for a given device. Use get_mounts() where appropriate instead. - Display all the mount points against a devices in multiple lines. - Also show devices that are not currently mounted. Tests performed: - Filling up a disk shows a disk space warning properly. Warning contains the free disk space correctly. - Calling get_root_device(get_disks()) return the correct root device. - In Deluge, the download directory contains a list of all samba current shares. If a disk with samba share is unmouted, it does not show up in the list. - In the Samba app page, all disks are shown properly. Root disk is shown as 'disk'. All other mount points such as .snapshots and /vagrant also show up. - In the Samba app page, unavailable shares list shows up when a disk with a share is unmounted. - Upload a backup, warning on the form shows available disk space properly. - When adding a backup location. The list includes all mount points. Duplicated mount points are not shown. Root disk is not shown in the list. When all the disks are used up for backup location, a warning that no additional disks are available is shown. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/backups/forms.py | 4 +-- plinth/modules/samba/tests/test_views.py | 10 +++--- plinth/modules/samba/views.py | 2 +- plinth/modules/storage/__init__.py | 36 +++++++++++++++++-- plinth/modules/storage/forms.py | 2 +- plinth/modules/storage/templates/storage.html | 2 +- plinth/modules/storage/udisks2.py | 4 ++- 7 files changed, 46 insertions(+), 14 deletions(-) diff --git a/plinth/modules/backups/forms.py b/plinth/modules/backups/forms.py index 79aabd12f..60178e54f 100644 --- a/plinth/modules/backups/forms.py +++ b/plinth/modules/backups/forms.py @@ -15,7 +15,7 @@ from django.core.validators import (FileExtensionValidator, from django.utils.translation import ugettext from django.utils.translation import ugettext_lazy as _ -from plinth.modules.storage import get_disks +from plinth.modules.storage import get_mounts from plinth.utils import format_lazy from . import api, split_path @@ -158,7 +158,7 @@ def get_disk_choices(): if repository.storage_type == 'disk' ] choices = [] - for device in get_disks(): + for device in get_mounts(): if device['mount_point'] == '/': continue diff --git a/plinth/modules/samba/tests/test_views.py b/plinth/modules/samba/tests/test_views.py index 6a181ded7..2bf5ae490 100644 --- a/plinth/modules/samba/tests/test_views.py +++ b/plinth/modules/samba/tests/test_views.py @@ -88,11 +88,11 @@ def make_request(request, view, **kwargs): def test_samba_shares_view(rf): """Test that a share list has correct view data.""" - with patch('plinth.views.AppView.get_context_data', - return_value={'is_enabled': True}), patch( - 'plinth.modules.samba.get_users', - return_value=USERS), patch( - 'plinth.modules.storage.get_disks', return_value=DISKS): + with patch('plinth.views.AppView.get_context_data', return_value={ + 'is_enabled': True + }), patch('plinth.modules.samba.get_users', + return_value=USERS), patch('plinth.modules.storage.get_mounts', + return_value=DISKS): view = views.SambaAppView.as_view() response, _ = make_request(rf.get(''), view) diff --git a/plinth/modules/samba/views.py b/plinth/modules/samba/views.py index 942770364..3de708c44 100644 --- a/plinth/modules/samba/views.py +++ b/plinth/modules/samba/views.py @@ -28,7 +28,7 @@ class SambaAppView(views.AppView): def get_context_data(self, *args, **kwargs): """Return template context data.""" context = super().get_context_data(*args, **kwargs) - disks = storage.get_disks() + disks = storage.get_mounts() shares = samba.get_shares() for disk in disks: diff --git a/plinth/modules/storage/__init__.py b/plinth/modules/storage/__init__.py index b2d0e1949..b6d199539 100644 --- a/plinth/modules/storage/__init__.py +++ b/plinth/modules/storage/__init__.py @@ -75,7 +75,36 @@ def init(): def get_disks(): - """Returns list of disks by combining information from df and udisks.""" + """Returns list of disks and their free space. + + The primary source of information is UDisks' list of block devices. + Information from df is used for free space available. + + """ + disks_from_df = _get_disks_from_df() + disks = udisks2.get_disks() + for disk in disks: + disk['size'] = format_bytes(disk['size']) + + # Add info from df to the disks from udisks based on mount point. + for disk in disks: + for disk_from_df in disks_from_df: + if disk_from_df['mount_point'] in disk['mount_points']: + disk['mount_point'] = disk_from_df['mount_point'] + for key in ('percent_used', 'size', 'used', 'free', 'size_str', + 'used_str', 'free_str'): + disk[key] = disk_from_df[key] + + return sorted(disks, key=lambda disk: disk['device']) + + +def get_mounts(): + """Return list of mounts by combining information from df and UDisks. + + The primary source of information is the df command. Information from + UDisks is used for labels. + + """ disks = _get_disks_from_df() disks_from_udisks = udisks2.get_disks() for disk in disks_from_udisks: @@ -130,7 +159,7 @@ def get_filesystem_type(mount_point='/'): def get_disk_info(mount_point): """Get information about the free space of a drive""" disks = get_disks() - list_root = [disk for disk in disks if disk['mount_point'] == mount_point] + list_root = [disk for disk in disks if mount_point in disk['mount_points']] if not list_root: raise PlinthError('Mount point {} not found.'.format(mount_point)) @@ -147,8 +176,9 @@ def get_disk_info(mount_point): def get_root_device(disks): """Return the root partition's device from list of partitions.""" for disk in disks: - if disk['mount_point'] == '/': + if '/' in disk['mount_points']: return disk['device'] + return None diff --git a/plinth/modules/storage/forms.py b/plinth/modules/storage/forms.py index b825bc4b1..a5870a377 100644 --- a/plinth/modules/storage/forms.py +++ b/plinth/modules/storage/forms.py @@ -21,7 +21,7 @@ def get_available_samba_shares(): samba_shares = json.loads( actions.superuser_run('samba', ['get-shares'])) if samba_shares: - disks = storage.get_disks() + disks = storage.get_mounts() for share in samba_shares: for disk in disks: if share['mount_point'] == disk['mount_point']: diff --git a/plinth/modules/storage/templates/storage.html b/plinth/modules/storage/templates/storage.html index 91d693b77..e8776878a 100644 --- a/plinth/modules/storage/templates/storage.html +++ b/plinth/modules/storage/templates/storage.html @@ -35,7 +35,7 @@ {{ disk.device }} {{ disk.label|default_if_none:"" }} - {{ disk.mount_point }} + {{ disk.mount_points|join:"
" }} {{ disk.filesystem_type }}
diff --git a/plinth/modules/storage/udisks2.py b/plinth/modules/storage/udisks2.py index a91c2aded..70fd73eb4 100644 --- a/plinth/modules/storage/udisks2.py +++ b/plinth/modules/storage/udisks2.py @@ -143,13 +143,15 @@ def get_disks(): 'label': block.id_label, 'size': block.size, 'filesystem_type': block.id_type, - 'is_removable': not block.hint_system + 'is_removable': not block.hint_system, + 'mount_points': [], } try: file_system = Filesystem(object_) device['mount_points'] = file_system.mount_points except Exception: continue + devices.append(device) return devices From d4ce843e229d840bc86fc6b29c8bb33832121f8a Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 29 May 2020 18:10:59 -0700 Subject: [PATCH 28/90] storage: Don't show empty progress bar for disks not mounted In future, we may consider showing disks that are not mounted and allow mounting them manually or unlocking encrypted volumes manually. Prepare for that scenario. Tests performed: - Edit get_disks() code to show not mounted disks. The progress bar or free space information should not be shown. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/storage/templates/storage.html | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/plinth/modules/storage/templates/storage.html b/plinth/modules/storage/templates/storage.html index e8776878a..e71493f04 100644 --- a/plinth/modules/storage/templates/storage.html +++ b/plinth/modules/storage/templates/storage.html @@ -38,21 +38,24 @@ {{ disk.mount_points|join:"
" }} {{ disk.filesystem_type }} -
- {% if disk.percent_used < 75 %} -
- {{ disk.percent_used }}% -
-
-
{{ disk.used_str }} / {{ disk.size_str }}
+ {% if 'percent_used' in disk %} +
+
+ {{ disk.percent_used }}% +
+
+
{{ disk.used_str }} / {{ disk.size_str }}
+ {% endif %} {% if disk.is_removable %} From 8c05fb072297583ea37154673e4473cffc2e7bd8 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 2 Jun 2020 11:13:05 -0700 Subject: [PATCH 29/90] storage: Remove rule to not automount system disks with no paritions This rule imported originally from udiskie's defaults seems somewhat arbitrary. We attach a SATA drive with partitions, it mounts them. However, if the entire drive is a file system, it does not auto-mount. Tests performed: - In VirtualBox, attach a SATA drive. Create two partitions and filesystems in them. Both filesystems are auto-mounted. - In VirtualBox, attach a SATA drive. Format the entire drive as a filesystem. The filesystem is auto-mounted. - Same behavior is observed with loop devices. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/storage/udisks2.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/plinth/modules/storage/udisks2.py b/plinth/modules/storage/udisks2.py index 70fd73eb4..a9448431d 100644 --- a/plinth/modules/storage/udisks2.py +++ b/plinth/modules/storage/udisks2.py @@ -254,16 +254,6 @@ def _consider_for_mounting(object_path): block_device.id, block_device.preferred_device) return - # Ignore non-external devices that don't have partition table (top-level - # filesystem). If the device is backed by a crypto device, still handle it. - # XXX: This rule is from udiskie. Should we keep it? - partition = Partition(object_path) - if block_device.hint_system and not partition.number and \ - block_device.crypto_backing_device == '/': - logger.info('Ignoring auto-mount of top-level internal device: %s %s', - block_device.id, block_device.preferred_device) - return - _mount(object_path) From 426cef4c2cda2fadbe0ba103ed324fe612088f14 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 2 Jun 2020 11:15:40 -0700 Subject: [PATCH 30/90] storage: Don't auto-mount loopback devices except in develop mode In the event containers are being used on the server with images, attempting to auto-mounting loop devices could interfere with their operation. We currently don't have a use case where a user would want to auto-mount loop devices. Initially suggested in https://salsa.debian.org/freedombox-team/freedombox/-/issues/1854 Tests performed: - Add a loopback device as follows and observe that is automatically mounted. dd if=/dev/zero of=/tmp/test_disk bs=1M count=100 mkfs.ext4 /tmp/test_disk losetup loop0 /tmp/test_disk umount /dev/loop0 losetup -d /dev/loop0 - Add a loopback device as follows and observe that both partitions are mounted. dd if=/dev/zero of=/tmp/test_disk bs=1M count=100 parted /tmp/test_disk mklabel gpt mkpart Part1 ext4 0% 50% mkpart Part2 ext4 50% 100% kpartx -avs /tmp/test_disk mkfs.ext4 /dev/mapper/loop0p1 mkfs.ext4 /dev/mapper/loop0p2 umount /dev/mapper/loop0p1 umount /dev/mapper/loop0p2 kpartx -dvs /tmp/test_disk - When --develop is removed or when code is modified to negate the not condition, the partitions are not auto-mounted in the above cases. Reported-by: James Valleroy Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/storage/udisks2.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/plinth/modules/storage/udisks2.py b/plinth/modules/storage/udisks2.py index a9448431d..794ca2464 100644 --- a/plinth/modules/storage/udisks2.py +++ b/plinth/modules/storage/udisks2.py @@ -6,7 +6,7 @@ Handle disk operations using UDisk2 DBus API. import logging import threading -from plinth import actions +from plinth import actions, cfg from plinth.errors import ActionError from plinth.utils import import_from_gi @@ -21,6 +21,7 @@ _INTERFACES = { 'Drive': 'org.freedesktop.UDisks2.Drive', 'Filesystem': 'org.freedesktop.UDisks2.Filesystem', 'Job': 'org.freedesktop.UDisks2.Job', + 'Loop': 'org.freedesktop.UDisks2.Loop', 'Manager': 'org.freedesktop.UDisks2.Manager', 'ObjectManager': 'org.freedesktop.DBus.ObjectManager', 'Partition': 'org.freedesktop.UDisks2.Partition', @@ -114,6 +115,7 @@ class Partition(Proxy): interface = _INTERFACES['Partition'] properties = { 'number': ('u', 'Number'), + 'table': ('o', 'Table'), } @@ -123,6 +125,12 @@ class Filesystem(Proxy): properties = {'mount_points': ('aay', 'MountPoints')} +class Loop(Proxy): + """Abstraction for UDisks2 Loop.""" + interface = _INTERFACES['Loop'] + properties = {'backing_file': ('ay', 'BackingFile')} + + def get_disks(): """List devices that can be ejected.""" devices = [] @@ -226,6 +234,19 @@ def _on_filesystem_added(object_path, _interfaces): threading.Thread(target=_consider_for_mounting, args=[object_path]).start() +def _is_loop_device(object_path): + """Return if the block device is a loop device backed by a file.""" + loop = Loop(object_path) + if loop.backing_file: + return True + + partition_table = Partition(object_path).table + if partition_table: + return _is_loop_device(partition_table) + + return False + + def _consider_for_mounting(object_path): """Check if the block device needs mounting and mount it.""" block_device = BlockDevice(object_path) @@ -254,6 +275,12 @@ def _consider_for_mounting(object_path): block_device.id, block_device.preferred_device) return + # Ignore loopback devices except in development mode. + if _is_loop_device(object_path) and not cfg.develop: + logger.info('Ignoring loop device in production mode: %s %s', + block_device.id, block_device.preferred_device) + return + _mount(object_path) From d3b5143ed6659ad826f17ba5aae7c80c4a4650e7 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 2 Jun 2020 17:15:45 -0700 Subject: [PATCH 31/90] storage: Allow ejecting any device not in fstab or crypttab Allow any disk including SATA disks to be ejected. Helps: #1597. Tests performed: - Attach SATA disk to VirtualBox. Create two partitions and filesystem in it. FreedomBox will auto-mount the filesystems. Eject button is shown on both the partitions. Without the patch, this is not shown. - Eject button is not shown against '/' as it is part of fstab. - Add a device into /etc/fstab. Eject button will not be shown against the device. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/storage/udisks2.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/plinth/modules/storage/udisks2.py b/plinth/modules/storage/udisks2.py index 794ca2464..466bce0f4 100644 --- a/plinth/modules/storage/udisks2.py +++ b/plinth/modules/storage/udisks2.py @@ -74,6 +74,9 @@ class Proxy: if value is None: return value + if signature.startswith('a('): + return value + if signature == 'ay': return bytes(value)[:-1].decode() @@ -96,6 +99,7 @@ class BlockDevice(Proxy): """Abstraction for UDisks2 Block device.""" interface = _INTERFACES['Block'] properties = { + 'configuration': ('a(sa{sv})', 'Configuration'), 'crypto_backing_device': ('o', 'CryptoBackingDevice'), 'device': ('ay', 'Device'), 'hint_ignore': ('b', 'HintIgnore'), @@ -131,6 +135,16 @@ class Loop(Proxy): properties = {'backing_file': ('ay', 'BackingFile')} +def _is_removable(object_path): + """Return True if the device is not part of fstab or crypttab.""" + block = BlockDevice(object_path) + for type_, _details in block.configuration: + if type_ in ('fstab', 'crypttab'): + return False + + return True + + def get_disks(): """List devices that can be ejected.""" devices = [] @@ -151,7 +165,7 @@ def get_disks(): 'label': block.id_label, 'size': block.size, 'filesystem_type': block.id_type, - 'is_removable': not block.hint_system, + 'is_removable': _is_removable(object_), 'mount_points': [], } try: From e59f9ac3fc2c806685f31cb0fc337a49de28b250 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 2 Jun 2020 17:42:33 -0700 Subject: [PATCH 32/90] storage: Ignore eject failures if filesystems unmounted properly Not all disks can be ejected. For example, SATA disks can't be ejected. However, they can be removed as long as all filesystems are unmounted properly. Ignore errors during ejecting of a disk. Closes: #1597. Tests performed: - In VirtualBox, attach a SATA disk, format it with two partitions. See them auto-mounted by FreedomBox. Eject one of the partitions, both partitions are unmounted but operation does not fail despite SATA disks not being eject-able. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- actions/storage | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/actions/storage b/actions/storage index 44da86c53..bf818d3de 100755 --- a/actions/storage +++ b/actions/storage @@ -281,6 +281,7 @@ def eject_drive_of_device(device_path): Return the details (model, vendor) of drives ejected. """ udisks = utils.import_from_gi('UDisks', '2.0') + glib = utils.import_from_gi('GLib', '2.0') client = udisks.Client.new_sync() object_manager = client.get_object_manager() @@ -307,7 +308,13 @@ def eject_drive_of_device(device_path): # Eject the drive drive = client.get_drive_for_block(block_device) if drive: - drive.call_eject_sync(_get_options(), None) + try: + drive.call_eject_sync(_get_options(), None) + except glib.Error: + # Ignore error during ejection as along as all the filesystems are + # unmounted, the disk can be removed. + pass + return { 'vendor': drive.props.vendor, 'model': drive.props.model, From 3fd0921e0f981bb91b202127e0690f67436a533e Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 2 Jun 2020 22:34:32 -0700 Subject: [PATCH 33/90] backups: Remove an unnecessary print() statement Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/backups/repository.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plinth/modules/backups/repository.py b/plinth/modules/backups/repository.py index 01bd6b712..94aafe0e1 100644 --- a/plinth/modules/backups/repository.py +++ b/plinth/modules/backups/repository.py @@ -130,7 +130,6 @@ class BaseBorgRepository(abc.ABC): """Return Borg information about a repository.""" output = self.run(['info', '--path', self.borg_path]) output = json.loads(output) - print(output, self._get_encryption_data()) if output['encryption']['mode'] == 'none' and \ self._get_encryption_data(): raise errors.BorgUnencryptedRepository( From 2c1a8685678bffd8d54ca36725c5a7f850980c1a Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Wed, 24 Jun 2020 07:20:55 -0400 Subject: [PATCH 34/90] storage: Handle multi-line text in functional test Signed-off-by: James Valleroy --- plinth/modules/storage/tests/test_functional.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plinth/modules/storage/tests/test_functional.py b/plinth/modules/storage/tests/test_functional.py index aa1cce7e1..157f792c4 100644 --- a/plinth/modules/storage/tests/test_functional.py +++ b/plinth/modules/storage/tests/test_functional.py @@ -22,4 +22,4 @@ def go_to_module(session_browser, name): def _is_root_disk_shown(browser): table_cells = browser.find_by_tag('td') - return any(cell.text == '/' for cell in table_cells) + return any(cell.text.split('\n')[0] == '/' for cell in table_cells) From b80bd00d5d005814ef0456d7b2420c14669a72bb Mon Sep 17 00:00:00 2001 From: Michael Breidenbach Date: Sun, 21 Jun 2020 06:15:33 +0000 Subject: [PATCH 35/90] Translated using Weblate (German) Currently translated at 100.0% (1281 of 1281 strings) --- plinth/locale/de/LC_MESSAGES/django.po | 112 ++++++++++--------------- 1 file changed, 46 insertions(+), 66 deletions(-) diff --git a/plinth/locale/de/LC_MESSAGES/django.po b/plinth/locale/de/LC_MESSAGES/django.po index 615c67b4b..df8708062 100644 --- a/plinth/locale/de/LC_MESSAGES/django.po +++ b/plinth/locale/de/LC_MESSAGES/django.po @@ -10,8 +10,8 @@ msgstr "" "Project-Id-Version: FreedomBox UI\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-15 19:20-0400\n" -"PO-Revision-Date: 2020-06-06 00:41+0000\n" -"Last-Translator: Ralf Barkow \n" +"PO-Revision-Date: 2020-06-24 11:41+0000\n" +"Last-Translator: Michael Breidenbach \n" "Language-Team: German \n" "Language: de\n" @@ -19,7 +19,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.1-dev\n" +"X-Generator: Weblate 4.2-dev\n" #: doc/dev/_templates/layout.html:11 msgid "Page source" @@ -4225,6 +4225,8 @@ msgid "" "Advanced networking operations such as bonding, bridging and VLAN management " "are provided by the Cockpit app." msgstr "" +"Erweiterte Netzwerkoperationen wie Bonding, Bridging und VLAN-Management " +"werden von der CockpitApp bereitgestellt." #: plinth/modules/networks/templates/router_configuration_content.html:10 #, python-format @@ -4685,13 +4687,11 @@ msgstr "" #: plinth/modules/performance/__init__.py:16 #: plinth/modules/performance/__init__.py:45 msgid "Performance" -msgstr "" +msgstr "Leistung" #: plinth/modules/performance/__init__.py:46 -#, fuzzy -#| msgid "System Configuration" msgid "System Monitoring" -msgstr "System-Konfiguration" +msgstr "Systemüberwachung" #: plinth/modules/power/__init__.py:16 msgid "Restart or shut down the system." @@ -4842,12 +4842,7 @@ msgid "Quasseldroid" msgstr "Quasseldroid" #: plinth/modules/radicale/__init__.py:33 -#, 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 ist ein CalDav- und CarDAV-Server. Er ermöglicht das " +"Radicale ist ein CalDav- und CardDAV-Server. Er ermöglicht das " "Synchronisieren und Teilen von Zeitplänen und Kontaktdaten. Um Radicale zu " -"nutzen, ist eine unterstützte " -"Client Software notwendig. Radicale kann von jedem Benutzer mit einem " -"{box_name}-Konto verwendet werden." +"nutzen, ist eine unterstützte Client Software notwendig. Radicale " +"kann von jedem Benutzer mit einem {box_name}-Konto verwendet werden." #: plinth/modules/radicale/__init__.py:39 msgid "" @@ -4969,14 +4964,6 @@ msgstr "" "Nachrichten und Rechtschreibprüfung." #: plinth/modules/roundcube/__init__.py:26 -#, fuzzy -#| msgid "" -#| "You can access Roundcube from /roundcube. Provide the username and password of the email " -#| "account you wish to access followed by the domain name of the IMAP server " -#| "for your email provider, like imap.example.com. For IMAP " -#| "over SSL (recommended), fill the server field like imaps://imap." -#| "example.com." msgid "" "You can use it by providing the username and password of the email account " "you wish to access followed by the domain name of the IMAP server for your " @@ -4984,11 +4971,11 @@ msgid "" "(recommended), fill the server field like imaps://imap.example.com." msgstr "" -"Sie können auf Roundcube von /roundcube zugreifen. Geben Sie Benutzernamen und Passwort des E-Mail-" -"Kontos ein, gefolgt von dem Domainnamen des IMAP-Servers Ihres Anbieters wie " -"z. B. imap.example.com. Für IMAP über SSL (empfohlen) geben Sie " -"im Feld Server etwas ein wie imaps://imap.example.com." +"Sie können es verwenden, indem Sie den Benutzernamen und das Passwort des E" +"-Mail-Kontos angeben, auf das Sie zugreifen möchten, gefolgt vom Domainnamen " +"des IMAP-Servers für Ihren E-Mail-Provider, wie imap.example.com. Bei IMAP über SSL (empfohlen) füllen Sie das Server-Feld aus, z. B. " +"imaps://imap.beispiel.com." #: plinth/modules/roundcube/__init__.py:31 msgid "" @@ -5067,34 +5054,28 @@ msgid "Samba" msgstr "Samba" #: plinth/modules/samba/__init__.py:63 -#, fuzzy -#| msgid "Network Time Server" msgid "Network File Storage" -msgstr "Netzwerk Zeit-Server" +msgstr "Netzwerkdateispeicherung" #: plinth/modules/samba/manifest.py:15 -#, fuzzy -#| msgid "Add Client" msgid "Android Samba Client" -msgstr "Client hinzufügen" +msgstr "Android-Samba-Klient" #: plinth/modules/samba/manifest.py:28 msgid "Ghost Commander - Samba plugin" -msgstr "" +msgstr "Ghost Commander - Samba-Plugin" #: plinth/modules/samba/manifest.py:42 msgid "VLC media player" -msgstr "" +msgstr "VLC-Mediaplayer" #: plinth/modules/samba/manifest.py:56 -#, fuzzy -#| msgid "GNOME Calendar" msgid "GNOME Files" -msgstr "GNOME Calendar" +msgstr "GNOME-Dateinen" #: plinth/modules/samba/manifest.py:68 msgid "Dolphin" -msgstr "" +msgstr "Dolphin" #: plinth/modules/samba/templates/samba.html:24 #: plinth/modules/samba/templates/samba.html:35 @@ -5384,20 +5365,12 @@ msgid "Shaarli allows you to save and share bookmarks." msgstr "Shaarli ermöglicht das Speichern und Teilen von Lesezeichen." #: plinth/modules/shaarli/__init__.py:21 -#, fuzzy -#| msgid "" -#| "When enabled, Shaarli will be available from /shaarli path on the web server. Note that " -#| "Shaarli only supports a single user account, which you will need to setup " -#| "on the initial visit." msgid "" "Note that Shaarli only supports a single user account, which you will need " "to setup on the initial visit." msgstr "" -"Falls aktiviert, wird Shaarli auf dem Webserver unter /shaarli erreichar sein. Shaarli unterstützt " -"nur ein Benutzerkonto; dieses muss beim ersten Besuch der Seite eingerichtet " -"werden." +"Beachten Sie, dass Shaarli nur ein einziges Benutzerkonto unterstützt, das " +"Sie bei Ihrem ersten Besuch einrichten müssen." #: plinth/modules/shaarli/__init__.py:37 plinth/modules/shaarli/manifest.py:8 msgid "Shaarli" @@ -5732,11 +5705,11 @@ msgstr "Zurücksetzen" #: plinth/modules/snapshot/templates/snapshot_manage.html:42 msgid "will be used at next boot" -msgstr "" +msgstr "wird beim nächsten Boot verwendet" #: plinth/modules/snapshot/templates/snapshot_manage.html:47 msgid "in use" -msgstr "" +msgstr "in Benutzung" #: plinth/modules/snapshot/templates/snapshot_manage.html:56 #, python-format @@ -5989,7 +5962,7 @@ msgstr "Wenig Plattenspeicherplatz" #: plinth/modules/storage/__init__.py:344 msgid "Disk failure imminent" -msgstr "" +msgstr "Festplattenfehler unmittelbar bevorstehend" #: plinth/modules/storage/__init__.py:346 #, python-brace-format @@ -5997,6 +5970,9 @@ msgid "" "Disk {id} is reporting that it is likely to fail in the near future. Copy " "any data while you still can and replace the drive." msgstr "" +"Disk {id} berichtet, dass sie wahrscheinlich in naher Zukunft versagen wird. " +"Kopieren Sie alle Daten, solange Sie noch können, und ersetzen Sie das " +"Laufwerk." #: plinth/modules/storage/forms.py:63 msgid "Invalid directory name." @@ -6073,6 +6049,9 @@ msgid "" "Advanced storage operations such as disk partitioning and RAID management " "are provided by the Cockpit app." msgstr "" +"Erweiterte Speicheroperationen wie Festplattenpartitionierung und RAID-" +"Verwaltung werden von der Cockpit-" +"Anwendung bereitgestellt." #: plinth/modules/storage/templates/storage_expand.html:14 #, python-format @@ -6463,18 +6442,13 @@ msgstr "" "\"{users_url}\">Benutzer mit einem {box_name} Login aufgerufen werden." #: plinth/modules/ttrss/__init__.py:37 -#, fuzzy -#| msgid "" -#| "When using a mobile or desktop application for Tiny Tiny RSS, use the URL " -#| "/tt-rss-app for " -#| "connecting." msgid "" "When using a mobile or desktop application for Tiny Tiny RSS, use the URL /tt-rss-app for connecting." msgstr "" -"Um Tiny Tiny RSS mit einer Anwendung auf ihrem Handy oder Computer zu " -"nutzen, tragen Sie die URL /tt-rss-app ein." +"Wenn Sie eine Mobil- oder Desktop-Anwendung für Tiny Tiny RSS verwenden, " +"verwenden Sie die URL /tt-rss-app für die " +"Verbindung." #: plinth/modules/ttrss/__init__.py:53 msgid "Read and subscribe to news feeds" @@ -6505,6 +6479,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 "" +"Aktualisierungen werden täglich um 06:00 Uhr entsprechend der lokalen " +"Zeitzone ausgeführt. Stellen Sie Ihre Zeitzone in der App Datum & Zeit ein. " +"Die Apps werden nach der Aktualisierung neu gestartet, wodurch sie " +"kurzzeitig nicht verfügbar sind. Wenn ein Neustart des Systems für notwendig " +"erachtet wird, erfolgt dieser automatisch um 02:00 Uhr, so dass alle " +"Anwendungen kurzzeitig nicht verfügbar sind." #: plinth/modules/upgrades/__init__.py:45 plinth/templates/setup.html:74 msgid "Update" @@ -7447,13 +7427,13 @@ msgid "Mailing list" msgstr "Mailingliste" #: 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 "" -"Dienst %(service_name)s ist nur im internen Netzwerk erreichbar." +"Dienst %(service_name)s ist nur im internen Netzwerk erreichbar " +"oder wenn der Client über VPN mit %(box_name)s verbunden ist." #: plinth/templates/internal-zone.html:17 msgid "Currently there are no network interfaces configured as internal." From 7d0647319a0cb93ce5f310b385877e5ef1318991 Mon Sep 17 00:00:00 2001 From: Fioddor Superconcentrado Date: Sat, 20 Jun 2020 21:18:18 +0000 Subject: [PATCH 36/90] Translated using Weblate (Spanish) Currently translated at 100.0% (1281 of 1281 strings) --- plinth/locale/es/LC_MESSAGES/django.po | 57 ++++++++------------------ 1 file changed, 17 insertions(+), 40 deletions(-) diff --git a/plinth/locale/es/LC_MESSAGES/django.po b/plinth/locale/es/LC_MESSAGES/django.po index fd618a111..1fb3cf6f0 100644 --- a/plinth/locale/es/LC_MESSAGES/django.po +++ b/plinth/locale/es/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-15 19:20-0400\n" -"PO-Revision-Date: 2020-06-14 16:41+0000\n" -"Last-Translator: Luis A. Arizmendi \n" +"PO-Revision-Date: 2020-06-24 11:41+0000\n" +"Last-Translator: Fioddor Superconcentrado \n" "Language-Team: Spanish \n" "Language: es\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.1-dev\n" +"X-Generator: Weblate 4.2-dev\n" #: doc/dev/_templates/layout.html:11 msgid "Page source" @@ -421,7 +421,7 @@ msgstr "Quitar ubicación de respaldo. Esto no borra la copia remota." #: plinth/modules/backups/templates/backups_repository.html:77 msgid "Download" -msgstr "Descargar" +msgstr "Descargas" #: plinth/modules/backups/templates/backups_repository.html:81 #: plinth/modules/backups/templates/backups_restore.html:28 @@ -1919,7 +1919,7 @@ msgstr "Manual" #: plinth/modules/help/views.py:43 plinth/templates/help-menu.html:27 #: plinth/templates/help-menu.html:28 msgid "Get Support" -msgstr "Obtener Soporte" +msgstr "Obtener soporte" #: plinth/modules/help/__init__.py:43 #: plinth/modules/help/templates/help_feedback.html:9 @@ -1933,7 +1933,7 @@ msgstr "Enviar Comentarios" #: plinth/modules/help/views.py:31 plinth/templates/help-menu.html:39 #: plinth/templates/help-menu.html:40 msgid "Contribute" -msgstr "Contribuir" +msgstr "Contribuír" #: plinth/modules/help/templates/help_about.html:17 #, python-format @@ -2283,7 +2283,7 @@ msgstr "I2P Proxys y Túneles" #: plinth/modules/i2p/templates/i2p.html:21 #: plinth/modules/i2p/templates/i2p.html:34 plinth/templates/clients.html:28 msgid "Launch" -msgstr "Ejecutar" +msgstr "Lanzar" #: plinth/modules/i2p/templates/i2p.html:25 msgid "Anonymous Torrents" @@ -4891,14 +4891,6 @@ msgstr "" "organización de carpetas, búsqueda de mensajes y corrección ortográfica." #: plinth/modules/roundcube/__init__.py:26 -#, fuzzy -#| msgid "" -#| "You can access Roundcube from /roundcube. Provide the username and password of the email " -#| "account you wish to access followed by the domain name of the IMAP server " -#| "for your email provider, like imap.example.com. For IMAP " -#| "over SSL (recommended), fill the server field like imaps://imap." -#| "example.com." msgid "" "You can use it by providing the username and password of the email account " "you wish to access followed by the domain name of the IMAP server for your " @@ -4906,12 +4898,11 @@ msgid "" "(recommended), fill the server field like imaps://imap.example.com." msgstr "" -"Puede acceder a Roundcube en roundcube. Debe facilitar el nombre de usuaria/o y la clave de la " -"cuenta de correo a la que desea acceder, además del nombre de dominio del " -"servidor IMAP de su proveedor, por ejemplo imap.ejemplo.com. " -"Para IMAP sobre SSL (recomendado) rellene el campo del servidor como " -"imaps://imap.ejemplo.com." +"Puede usarlo facilitando el nombre de usuaria/o y la clave de la cuenta de " +"correo a la que desea acceder, además del nombre de dominio del servidor " +"IMAP de su proveedor, por ejemplo imap.ejemplo.com. Para IMAP " +"sobre SSL (recomendado) rellene el campo del servidor como imaps://imap" +".ejemplo.com." #: plinth/modules/roundcube/__init__.py:31 msgid "" @@ -5301,20 +5292,12 @@ msgid "Shaarli allows you to save and share bookmarks." msgstr "Shaarli le permite guardar y compartir marcadores." #: plinth/modules/shaarli/__init__.py:21 -#, fuzzy -#| msgid "" -#| "When enabled, Shaarli will be available from /shaarli path on the web server. Note that " -#| "Shaarli only supports a single user account, which you will need to setup " -#| "on the initial visit." msgid "" "Note that Shaarli only supports a single user account, which you will need " "to setup on the initial visit." msgstr "" -"Cuando se activa Shaarli está disponible en la dirección /shaarli de su servidor. Note que Shaarli " -"solo soporta una cuenta de usuaria/o, que debe configurar en el primer " -"acceso." +"Note que Shaarli solo soporta una cuenta de usuaria/o, que debe configurar " +"en el primer acceso." #: plinth/modules/shaarli/__init__.py:37 plinth/modules/shaarli/manifest.py:8 msgid "Shaarli" @@ -6373,18 +6356,12 @@ msgstr "" "\"{users_url}\">persona con una cuenta de acceso en {box_name}." #: plinth/modules/ttrss/__init__.py:37 -#, fuzzy -#| msgid "" -#| "When using a mobile or desktop application for Tiny Tiny RSS, use the URL " -#| "/tt-rss-app for " -#| "connecting." msgid "" "When using a mobile or desktop application for Tiny Tiny RSS, use the URL /tt-rss-app for connecting." msgstr "" "Cuando emplee una aplicación de móvil o de escritorio para Tiny Tiny RSS, " -"use la URL /tt-rss-app para conectar." +"use la URL /tt-rss-app para conectar." #: plinth/modules/ttrss/__init__.py:53 msgid "Read and subscribe to news feeds" @@ -7181,7 +7158,7 @@ msgstr "Cambiar modo de navegación" #: plinth/templates/base.html:101 plinth/templates/base.html:104 msgid "Home" -msgstr "Inicio" +msgstr "Principal" #: plinth/templates/base.html:109 plinth/templates/base.html:113 msgid "Apps" @@ -7327,7 +7304,7 @@ msgstr "Página inicio" #: plinth/templates/index.html:140 msgid "Source Code" -msgstr "Código fuente" +msgstr "Código Fuente" #: plinth/templates/index.html:143 msgid "Donate" From cf9f086e6f26f18716058ec2a0d0e3a09e63f92b Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 22 Jun 2020 04:47:43 +0000 Subject: [PATCH 37/90] Translated using Weblate (Telugu) Currently translated at 64.2% (823 of 1281 strings) --- plinth/locale/te/LC_MESSAGES/django.po | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/plinth/locale/te/LC_MESSAGES/django.po b/plinth/locale/te/LC_MESSAGES/django.po index ee8aed00f..a36a46fe3 100644 --- a/plinth/locale/te/LC_MESSAGES/django.po +++ b/plinth/locale/te/LC_MESSAGES/django.po @@ -10,8 +10,8 @@ msgstr "" "Project-Id-Version: FreedomBox UI\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-15 19:20-0400\n" -"PO-Revision-Date: 2020-06-20 05:41+0000\n" -"Last-Translator: Joseph Nuthalapati \n" +"PO-Revision-Date: 2020-06-24 11:41+0000\n" +"Last-Translator: Sunil Mohan Adapa \n" "Language-Team: Telugu \n" "Language: te\n" @@ -19,7 +19,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.1.1\n" +"X-Generator: Weblate 4.2-dev\n" #: doc/dev/_templates/layout.html:11 msgid "Page source" @@ -156,10 +156,8 @@ msgid "Name" msgstr "పేరు" #: plinth/modules/backups/forms.py:53 -#, fuzzy -#| msgid "Name for new backup archive." msgid "(Optional) Set a name for this backup archive" -msgstr "కొత్త బ్యాకప్ ఆర్కైవుకి పేరు." +msgstr "(ఐచ్ఛిక) బ్యాకప్ ఆర్కైవుకి పేరు పెట్తండి" #: plinth/modules/backups/forms.py:56 msgid "Included apps" @@ -330,13 +328,11 @@ msgstr "ఖాతా సృష్టించు" #, fuzzy #| msgid "Name for new backup archive." msgid "Upload and restore a backup archive" -msgstr "కొత్త బ్యాకప్ ఆర్కైవుకి పేరు." +msgstr "బ్యాకప్‌ను అప్‌లోడ్ చేసి పునరుద్ధరించండి" #: plinth/modules/backups/templates/backups.html:41 -#, fuzzy -#| msgid "Name for new backup archive." msgid "Upload and Restore" -msgstr "కొత్త బ్యాకప్ ఆర్కైవుకి పేరు." +msgstr "అప్‌లోడ్ చేసి పునరుద్ధరించండి" #: plinth/modules/backups/templates/backups.html:44 #, fuzzy From 88920292688cd1166949550af3a4e4ef1ea6350d Mon Sep 17 00:00:00 2001 From: Michael Breidenbach Date: Sun, 21 Jun 2020 06:38:18 +0000 Subject: [PATCH 38/90] Translated using Weblate (Swedish) Currently translated at 100.0% (1281 of 1281 strings) --- plinth/locale/sv/LC_MESSAGES/django.po | 43 ++++++-------------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/plinth/locale/sv/LC_MESSAGES/django.po b/plinth/locale/sv/LC_MESSAGES/django.po index 8d03ce647..5579053c3 100644 --- a/plinth/locale/sv/LC_MESSAGES/django.po +++ b/plinth/locale/sv/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-15 19:20-0400\n" -"PO-Revision-Date: 2020-06-03 02:52+0000\n" +"PO-Revision-Date: 2020-06-24 11:41+0000\n" "Last-Translator: Michael Breidenbach \n" "Language-Team: Swedish \n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.1-dev\n" +"X-Generator: Weblate 4.2-dev\n" #: doc/dev/_templates/layout.html:11 msgid "Page source" @@ -4878,14 +4878,6 @@ msgstr "" "mappmanipulering, meddelande sökning och stavningskontroll." #: plinth/modules/roundcube/__init__.py:26 -#, fuzzy -#| msgid "" -#| "You can access Roundcube from /roundcube. Provide the username and password of the email " -#| "account you wish to access followed by the domain name of the IMAP server " -#| "for your email provider, like imap.example.com. For IMAP " -#| "over SSL (recommended), fill the server field like imaps://imap." -#| "example.com." msgid "" "You can use it by providing the username and password of the email account " "you wish to access followed by the domain name of the IMAP server for your " @@ -4893,12 +4885,11 @@ msgid "" "(recommended), fill the server field like imaps://imap.example.com." msgstr "" -"Du kan komma åt roundcube från /roundcube. Ange användarnamn och lösenord för det e-postkonto du " -"vill komma åt följt av domännamnet för IMAP-servern för din e-" -"postleverantör, som imap.example.com. För IMAP över SSL " -"(rekommenderas), Fyll i fältet Server som imaps://imap.example.com." +"Du kan använda det genom att ge användarnamn och lösenord till den e-" +"postkonto som du vill nå, följt av domän namn av IMAP-server för din e-" +"postleverantör, som imap.example.com. För IMAP över SSL-" +"kryptering (rekommenderas), fyll server fält som imaps://imap.exempel." +"kom." #: plinth/modules/roundcube/__init__.py:31 msgid "" @@ -5281,20 +5272,12 @@ msgid "Shaarli allows you to save and share bookmarks." msgstr "Shaarli kan du spara och dela bokmärken." #: plinth/modules/shaarli/__init__.py:21 -#, fuzzy -#| msgid "" -#| "When enabled, Shaarli will be available from /shaarli path on the web server. Note that " -#| "Shaarli only supports a single user account, which you will need to setup " -#| "on the initial visit." msgid "" "Note that Shaarli only supports a single user account, which you will need " "to setup on the initial visit." msgstr "" -"När den är aktiverad kommer Shaarli att vara tillgänglig från /shaarli Path på webbservern. " -"Observera att Shaarli bara stöder ett enda användarkonto, som du kommer att " -"behöva ställa in på det första besöket." +"Observera att Shaarli endast stöd för ett enskilt användarkonto, som du " +"behöver för att ställa den första besök." #: plinth/modules/shaarli/__init__.py:37 plinth/modules/shaarli/manifest.py:8 msgid "Shaarli" @@ -6341,18 +6324,12 @@ msgstr "" "\"{users_url}\">användare med en {box_name} login." #: plinth/modules/ttrss/__init__.py:37 -#, fuzzy -#| msgid "" -#| "When using a mobile or desktop application for Tiny Tiny RSS, use the URL " -#| "/tt-rss-app for " -#| "connecting." msgid "" "When using a mobile or desktop application for Tiny Tiny RSS, use the URL /tt-rss-app for connecting." msgstr "" "När du använder en mobil eller stationär applikation för Tiny Tiny RSS, " -"Använd URL: en /tt-RSS-" -"app för att ansluta." +"Använd URL för att ansluta." #: plinth/modules/ttrss/__init__.py:53 msgid "Read and subscribe to news feeds" From a988828f73924dba50aa5f04b50a4825dcde80cf Mon Sep 17 00:00:00 2001 From: Pavel Borecki Date: Mon, 22 Jun 2020 07:14:08 +0000 Subject: [PATCH 39/90] Translated using Weblate (Czech) Currently translated at 79.0% (1012 of 1281 strings) --- plinth/locale/cs/LC_MESSAGES/django.po | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plinth/locale/cs/LC_MESSAGES/django.po b/plinth/locale/cs/LC_MESSAGES/django.po index 2b9fcff47..87523b3b2 100644 --- a/plinth/locale/cs/LC_MESSAGES/django.po +++ b/plinth/locale/cs/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-15 19:20-0400\n" -"PO-Revision-Date: 2020-05-29 18:41+0000\n" -"Last-Translator: Allan Nordhøy \n" +"PO-Revision-Date: 2020-06-24 11:41+0000\n" +"Last-Translator: Pavel Borecki \n" "Language-Team: Czech \n" "Language: cs\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" -"X-Generator: Weblate 4.1-dev\n" +"X-Generator: Weblate 4.2-dev\n" #: doc/dev/_templates/layout.html:11 msgid "Page source" @@ -419,7 +419,7 @@ msgstr "" #: plinth/modules/backups/templates/backups_repository.html:77 msgid "Download" -msgstr "Stáhnout" +msgstr "Stažení si" #: plinth/modules/backups/templates/backups_repository.html:81 #: plinth/modules/backups/templates/backups_restore.html:28 @@ -1477,7 +1477,7 @@ msgstr "Minulá aktualizace" #: plinth/modules/dynamicdns/views.py:26 plinth/modules/help/__init__.py:51 #: plinth/templates/help-menu.html:46 plinth/templates/help-menu.html:47 msgid "About" -msgstr "O systému" +msgstr "O projektu" #: plinth/modules/dynamicdns/views.py:32 #: plinth/modules/firewall/templates/firewall.html:10 @@ -1947,7 +1947,7 @@ msgstr "Příručka" #: plinth/modules/help/views.py:43 plinth/templates/help-menu.html:27 #: plinth/templates/help-menu.html:28 msgid "Get Support" -msgstr "Získat podporu" +msgstr "Získejte podporu" #: plinth/modules/help/__init__.py:43 #: plinth/modules/help/templates/help_feedback.html:9 @@ -7382,7 +7382,7 @@ msgstr "Zdrojové kódy" #: plinth/templates/index.html:143 msgid "Donate" -msgstr "Podpořit darem" +msgstr "Podpořit vývoj darem" #: plinth/templates/index.html:147 msgid "FreedomBox Foundation" From 7f7f4cfb521fe5579337ab619e0b6cb547a0d576 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 22 Jun 2020 22:34:40 -0700 Subject: [PATCH 40/90] container: Remove sqlite3 file early enough During --list-dependencies if an old sqlite3 file is present with gitweb enabled, then a2enconf -c gitweb-freedombox-auth get executed. In this case, setting up apache2 module fails because authpubtkt tokens are not yet generated but they are being referred to in the configuration files. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- container | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/container b/container index 095c81620..5dab9a619 100755 --- a/container +++ b/container @@ -138,6 +138,9 @@ KEY_SERVER = 'keys.gnupg.net' PROVISION_SCRIPT = ''' set -x +# Remove FreedomBox database lingering in source directory to start fresh +sudo rm -f /freedombox/data/var/lib/plinth/plinth.sqlite3 + cd /freedombox/ sudo apt-get update @@ -158,9 +161,6 @@ sudo apt-mark unhold freedombox # Install ncurses-term sudo DEBIAN_FRONTEND=noninteractive apt-get install --yes ncurses-term -# Remove FreedomBox database lingering in source directory to start fresh -sudo rm -f /freedombox/data/var/lib/plinth/plinth.sqlite3 - echo 'alias freedombox-develop="sudo -u plinth /freedombox/run --develop"' \ >> /home/fbx/.bashrc ''' From bf53fd8b179e45da1aa5e9fadcc6d432bdd30601 Mon Sep 17 00:00:00 2001 From: Veiko Aasa Date: Thu, 25 Jun 2020 00:47:07 +0300 Subject: [PATCH 41/90] functional-tests: Fix page not fully loaded errors when taking backups - Use a submit() function to wait for a update when visiting app pages. This prevents failures on pages which have custom javascript, including the backup app. Those errors are more common if the server hardware is slower. - Remove unnecessary wait_for_page_update() as submit() already waits for a page update. Signed-off-by: Veiko Aasa Reviewed-by: Sunil Mohan Adapa --- plinth/tests/functional/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py index 133b09db8..f2b8c3b63 100644 --- a/plinth/tests/functional/__init__.py +++ b/plinth/tests/functional/__init__.py @@ -269,7 +269,8 @@ def nav_to_module(browser, module): sys_or_apps = 'sys' if module in _sys_modules else 'apps' required_url = base_url + f'/plinth/{sys_or_apps}/{module}/' if browser.url != required_url: - browser.visit(required_url) + with wait_for_page_update(browser, expected_url=required_url): + browser.visit(required_url) def app_select_domain_name(browser, app_name, domain_name): @@ -404,10 +405,8 @@ def set_advanced_mode(browser, mode): def _click_button_and_confirm(browser, href): buttons = browser.find_link_by_href(href) if buttons: - buttons.first.click() - with wait_for_page_update(browser, - expected_url='/plinth/sys/backups/'): - submit(browser) + submit(browser, buttons.first) + submit(browser, expected_url='/plinth/sys/backups/') def _backup_delete_archive_by_name(browser, archive_name): @@ -421,7 +420,8 @@ def backup_create(browser, app_name, archive_name=None): if archive_name: _backup_delete_archive_by_name(browser, archive_name) - browser.find_link_by_href('/plinth/sys/backups/create/').first.click() + buttons = browser.find_link_by_href('/plinth/sys/backups/create/') + submit(browser, buttons.first) browser.find_by_id('select-all').uncheck() if archive_name: browser.find_by_id('id_backups-name').fill(archive_name) From 5424e1e23faffe162b7f8a97760e9ab861635670 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Wed, 24 Jun 2020 19:50:47 -0400 Subject: [PATCH 42/90] apt: Run `apt-get -f install` before other commands Run `apt-get --fix-broken install` before installing package or manual update. This will attempt to correct broken dependencies. Tests: - Install a package without its dependencies using `dpkg -i`. - Both app install and manual update successfully recover from this situation. Signed-off-by: James Valleroy Reviewed-by: Sunil Mohan Adapa --- actions/packages | 28 +++++----------------------- actions/upgrades | 2 ++ plinth/action_utils.py | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/actions/packages b/actions/packages index 6f853c0de..fbce81755 100755 --- a/actions/packages +++ b/actions/packages @@ -19,6 +19,7 @@ import apt.cache import apt_inst import apt_pkg from plinth import cfg +from plinth.action_utils import run_apt_command LOCK_FILE = '/var/lib/dpkg/lock' @@ -68,29 +69,9 @@ def _apt_hold(): subprocess.run(['apt-mark', 'unhold', 'freedombox'], check=True) -def _run_apt_command(arguments): - """Run apt-get with provided arguments.""" - # Ask apt-get to output its progress to file descriptor 3. - command = [ - 'apt-get', '--assume-yes', '--quiet=2', '--option', 'APT::Status-Fd=3' - ] + arguments - - # Duplicate stdout to file descriptor 3 for this process. - os.dup2(1, 3) - - # Pass on file descriptor 3 instead of closing it. Close stdout - # so that regular output is ignored. - env = os.environ.copy() - env['DEBIAN_FRONTEND'] = 'noninteractive' - process = subprocess.run(command, stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, close_fds=False, - env=env) - return process.returncode - - def subcommand_update(arguments): """Update apt package lists.""" - sys.exit(_run_apt_command(['update'])) + sys.exit(run_apt_command(['update'])) def subcommand_install(arguments): @@ -114,8 +95,9 @@ def subcommand_install(arguments): extra_arguments += ['-o', 'Dpkg::Options::=--force-confnew'] with _apt_hold(): - returncode = _run_apt_command(['install'] + extra_arguments + - arguments.packages) + run_apt_command(['--fix-broken', 'install']) + returncode = run_apt_command(['install'] + extra_arguments + + arguments.packages) sys.exit(returncode) diff --git a/actions/upgrades b/actions/upgrades index 86c91f10a..82098a225 100755 --- a/actions/upgrades +++ b/actions/upgrades @@ -11,6 +11,7 @@ import re import subprocess import sys +from plinth.action_utils import run_apt_command from plinth.modules.apache.components import check_url AUTO_CONF_FILE = '/etc/apt/apt.conf.d/20auto-upgrades' @@ -83,6 +84,7 @@ def parse_arguments(): def subcommand_run(_): """Run unattended-upgrades""" + run_apt_command(['--fix-broken', 'install']) try: subprocess.Popen(['systemctl', 'start', 'freedombox-manual-upgrade'], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, diff --git a/plinth/action_utils.py b/plinth/action_utils.py index afe7b6715..55411a7b5 100644 --- a/plinth/action_utils.py +++ b/plinth/action_utils.py @@ -197,6 +197,7 @@ def webserver_disable(name, kind='config', apply_changes=True): class WebserverChange(object): """Context to restart/reload Apache after configuration changes.""" + def __init__(self): """Initialize the context object state.""" self.actions_required = set() @@ -388,3 +389,23 @@ def is_disk_image(): - Installing packages on a Debian machine using apt. """ return os.path.exists('/var/lib/freedombox/is-freedombox-disk-image') + + +def run_apt_command(arguments): + """Run apt-get with provided arguments.""" + # Ask apt-get to output its progress to file descriptor 3. + command = [ + 'apt-get', '--assume-yes', '--quiet=2', '--option', 'APT::Status-Fd=3' + ] + arguments + + # Duplicate stdout to file descriptor 3 for this process. + os.dup2(1, 3) + + # Pass on file descriptor 3 instead of closing it. Close stdout + # so that regular output is ignored. + env = os.environ.copy() + env['DEBIAN_FRONTEND'] = 'noninteractive' + process = subprocess.run(command, stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, close_fds=False, + env=env) + return process.returncode From 0f54fab067985ece09ee16be8ff5d5501acbc746 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Wed, 24 Jun 2020 20:36:13 -0400 Subject: [PATCH 43/90] apt: Run `dpkg --configure -a` before other actions Signed-off-by: James Valleroy Reviewed-by: Sunil Mohan Adapa --- actions/packages | 1 + actions/upgrades | 1 + 2 files changed, 2 insertions(+) diff --git a/actions/packages b/actions/packages index fbce81755..d7112413c 100755 --- a/actions/packages +++ b/actions/packages @@ -94,6 +94,7 @@ def subcommand_install(arguments): elif arguments.force_configuration == 'new': extra_arguments += ['-o', 'Dpkg::Options::=--force-confnew'] + subprocess.run(['dpkg', '--configure', '-a']) with _apt_hold(): run_apt_command(['--fix-broken', 'install']) returncode = run_apt_command(['install'] + extra_arguments + diff --git a/actions/upgrades b/actions/upgrades index 82098a225..3db0e7ac1 100755 --- a/actions/upgrades +++ b/actions/upgrades @@ -84,6 +84,7 @@ def parse_arguments(): def subcommand_run(_): """Run unattended-upgrades""" + subprocess.run(['dpkg', '--configure', '-a']) run_apt_command(['--fix-broken', 'install']) try: subprocess.Popen(['systemctl', 'start', 'freedombox-manual-upgrade'], From 4c742d690eba78c343740f213fd891542ebcc232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89frit?= Date: Wed, 24 Jun 2020 18:49:21 +0000 Subject: [PATCH 44/90] Translated using Weblate (French) Currently translated at 98.7% (1265 of 1281 strings) --- plinth/locale/fr/LC_MESSAGES/django.po | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plinth/locale/fr/LC_MESSAGES/django.po b/plinth/locale/fr/LC_MESSAGES/django.po index 54d9afcc7..40069ef5f 100644 --- a/plinth/locale/fr/LC_MESSAGES/django.po +++ b/plinth/locale/fr/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgstr "" "Project-Id-Version: FreedomBox UI\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-15 19:20-0400\n" -"PO-Revision-Date: 2020-06-20 05:41+0000\n" -"Last-Translator: Thomas Vincent \n" +"PO-Revision-Date: 2020-06-25 19:41+0000\n" +"Last-Translator: Éfrit \n" "Language-Team: French \n" "Language: fr\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.1.1\n" +"X-Generator: Weblate 4.2-dev\n" #: doc/dev/_templates/layout.html:11 msgid "Page source" @@ -159,10 +159,8 @@ msgid "Name" msgstr "Nom" #: plinth/modules/backups/forms.py:53 -#, fuzzy -#| msgid "Name for new backup archive." msgid "(Optional) Set a name for this backup archive" -msgstr "Nom de la nouvelle archive de sauvegarde." +msgstr "(Optionnel) Nommez cette archive de sauvegarde" #: plinth/modules/backups/forms.py:56 msgid "Included apps" From 29813696c65c927dd5f24dcf6d0ecf345d54c33f Mon Sep 17 00:00:00 2001 From: Veiko Aasa Date: Fri, 26 Jun 2020 11:01:42 +0300 Subject: [PATCH 45/90] functional-tests: Remove unnecessary wait when navigating to module The fixture splinter_browser_load_condition already waits until a page is loaded. Signed-off-by: Veiko Aasa Reviewed-by: Sunil Mohan Adapa --- plinth/tests/functional/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py index f2b8c3b63..c0ecaa4ea 100644 --- a/plinth/tests/functional/__init__.py +++ b/plinth/tests/functional/__init__.py @@ -269,8 +269,7 @@ def nav_to_module(browser, module): sys_or_apps = 'sys' if module in _sys_modules else 'apps' required_url = base_url + f'/plinth/{sys_or_apps}/{module}/' if browser.url != required_url: - with wait_for_page_update(browser, expected_url=required_url): - browser.visit(required_url) + browser.visit(required_url) def app_select_domain_name(browser, app_name, domain_name): From 64ff37e83f804fbe27bae87c92860c73bb8d3ccf Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sun, 21 Jun 2020 20:00:25 -0700 Subject: [PATCH 46/90] storage: Don't log exception of disk space check fails When running inside a container, it is not possible to retrieve information about the disk that is mounted on '/'. Ignore errors in such cases. Tests performed: - Inside a container, start the service using freedombox-devel. Every 3 minutes a check for disk space will be done in developer mode. If UDisks is being used as source of information, the free space check will fail with an error logged. - With the changes, the error is not shown. Signed-off-by: Sunil Mohan Adapa Tested-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/storage/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plinth/modules/storage/__init__.py b/plinth/modules/storage/__init__.py index b6d199539..ac1c21fbc 100644 --- a/plinth/modules/storage/__init__.py +++ b/plinth/modules/storage/__init__.py @@ -290,9 +290,7 @@ def warn_about_low_disk_space(request): try: root_info = get_disk_info('/') - except PlinthError as exception: - logger.exception('Error getting information about root partition: %s', - exception) + except PlinthError: return show = False From c3eac2c02e296cf393fd3be02a7936225ac2b76e Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sun, 21 Jun 2020 20:28:20 -0700 Subject: [PATCH 47/90] storage: Use mount info instead of disk info for free space warning Tests: - In a container, fill up space. Start FreedomBox in develop mode wait 3 minutes for storage warning to show. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/backups/views.py | 7 +++++-- plinth/modules/storage/__init__.py | 12 +++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/plinth/modules/backups/views.py b/plinth/modules/backups/views.py index 4dc6d3bf9..65b9aafc0 100644 --- a/plinth/modules/backups/views.py +++ b/plinth/modules/backups/views.py @@ -107,7 +107,7 @@ class UploadArchiveView(SuccessMessageMixin, FormView): context = super().get_context_data(**kwargs) context['title'] = _('Upload and restore a backup') try: - disk_info = storage.get_disk_info('/') + mount_info = storage.get_mount_info('/') except PlinthError as exception: logger.exception( 'Error getting information about root partition: %s', @@ -121,7 +121,7 @@ class UploadArchiveView(SuccessMessageMixin, FormView): # - For restoring it's highly advisable to have at least as much # free disk space as the file size. context['max_filesize'] = storage.format_bytes( - disk_info['free_bytes'] / 2) + mount_info['free_bytes'] / 2) return context @@ -164,6 +164,7 @@ class BaseRestoreView(SuccessMessageMixin, FormView): class RestoreFromUploadView(BaseRestoreView): """View to restore files from an (uploaded) exported archive.""" + def get(self, *args, **kwargs): path = self.request.session.get(SESSION_PATH_VARIABLE) if not os.path.isfile(path): @@ -193,6 +194,7 @@ class RestoreFromUploadView(BaseRestoreView): class RestoreArchiveView(BaseRestoreView): """View to restore files from an archive.""" + def _get_included_apps(self): """Save some data used to instantiate the form.""" name = unquote(self.kwargs['name']) @@ -210,6 +212,7 @@ class RestoreArchiveView(BaseRestoreView): class DownloadArchiveView(View): """View to export and download an archive as stream.""" + def get(self, request, uuid, name): repository = get_instance(uuid) filename = f'{name}.tar.gz' diff --git a/plinth/modules/storage/__init__.py b/plinth/modules/storage/__init__.py index ac1c21fbc..facb5d0b9 100644 --- a/plinth/modules/storage/__init__.py +++ b/plinth/modules/storage/__init__.py @@ -156,10 +156,12 @@ def get_filesystem_type(mount_point='/'): raise ValueError('No such mount point') -def get_disk_info(mount_point): - """Get information about the free space of a drive""" - disks = get_disks() - list_root = [disk for disk in disks if mount_point in disk['mount_points']] +def get_mount_info(mount_point): + """Get information about the free space of a mount point.""" + mounts = get_mounts() + list_root = [ + mount for mount in mounts if mount_point == mount['mount_point'] + ] if not list_root: raise PlinthError('Mount point {} not found.'.format(mount_point)) @@ -289,7 +291,7 @@ def warn_about_low_disk_space(request): from plinth.notification import Notification try: - root_info = get_disk_info('/') + root_info = get_mount_info('/') except PlinthError: return From 5a126e62a8d629c12abd70c5421ed5d603814456 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 25 Jun 2020 15:47:14 -0700 Subject: [PATCH 48/90] notifications: Fix issue with redirection on dismiss The request variable is not available when a custom template is used to render a notification. Fix this by passing the template rendering context additional request variable. Closes: #1887. Tests: - Reduce the version number in data for 'upgrades-new-release' notification in the plinth_storednotification table in the DB. Start FreedomBox. New release message will appear. Go to page other than home page. The dismiss button has next= parameter filled properly with current URL. Dismiss the notification and notice that page URL stays the same. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/context_processors.py | 3 ++- plinth/notification.py | 9 +++++---- .../tests/data/templates/test-notification.html | 2 +- plinth/tests/test_notification.py | 17 +++++++++++------ 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/plinth/context_processors.py b/plinth/context_processors.py index a8ff94709..7793d9a64 100644 --- a/plinth/context_processors.py +++ b/plinth/context_processors.py @@ -23,7 +23,8 @@ def common(request): ugettext_noop('FreedomBox') from plinth.notification import Notification - notifications_context = Notification.get_display_context(user=request.user) + notifications_context = Notification.get_display_context( + request, user=request.user) slash_indices = [match.start() for match in re.finditer('/', request.path)] active_menu_urls = [request.path[:index + 1] for index in slash_indices] diff --git a/plinth/notification.py b/plinth/notification.py index 26c9c5858..77004ada0 100644 --- a/plinth/notification.py +++ b/plinth/notification.py @@ -316,12 +316,12 @@ class Notification(models.StoredNotification): return new_dict @staticmethod - def _render(template, data): + def _render(request, template, data): """Use the template name and render it.""" if not template: return None - context = dict(data, box_name=ugettext(cfg.box_name)) + context = dict(data, box_name=ugettext(cfg.box_name), request=request) try: return SimpleTemplateResponse(template, context).render() except TemplateDoesNotExist: @@ -329,7 +329,7 @@ class Notification(models.StoredNotification): return {'content': f'Template {template} does not exist.'.encode()} @staticmethod - def get_display_context(user): + def get_display_context(request, user): """Return a list of notifications meant for display to a user.""" notifications = Notification.list(user=user) max_severity = max(notifications, default=None, @@ -345,13 +345,14 @@ class Notification(models.StoredNotification): action['text'] = Notification._translate( action['text'], data) + body = Notification._render(request, note.body_template, data) notes.append({ 'id': note.id, 'app_id': note.app_id, 'severity': note.severity, 'title': Notification._translate(note.title, data), 'message': Notification._translate(note.message, data), - 'body': Notification._render(note.body_template, data), + 'body': body, 'actions': actions, 'data': data, 'created_time': note.created_time, diff --git a/plinth/tests/data/templates/test-notification.html b/plinth/tests/data/templates/test-notification.html index d966c62df..11e974ec7 100644 --- a/plinth/tests/data/templates/test-notification.html +++ b/plinth/tests/data/templates/test-notification.html @@ -1 +1 @@ -Test notification body +Test notification body {{request.path}} diff --git a/plinth/tests/test_notification.py b/plinth/tests/test_notification.py index b83d6d3b5..e7ba76582 100644 --- a/plinth/tests/test_notification.py +++ b/plinth/tests/test_notification.py @@ -335,8 +335,10 @@ def test_list_filter_user_and_group(note, user): @patch('plinth.notification.ugettext') -def test_display_context(ugettext, note, user): +def test_display_context(ugettext, note, user, rf): """Test display context for a notification.""" + request = rf.get('/plinth/help/about/') + data = { 'test-key1': 'test-value1', 'test-key2': 'translate:test-value2', @@ -370,7 +372,7 @@ def test_display_context(ugettext, note, user): note.data = data note.save() - context = Notification.get_display_context(user) + context = Notification.get_display_context(request, user) assert len(context['notifications']) == 1 assert context['max_severity'] == 'error' @@ -392,12 +394,14 @@ def test_display_context(ugettext, note, user): assert not context_note['dismissed'] -def test_display_context_body_template(note, user, load_cfg): +def test_display_context_body_template(note, user, load_cfg, rf): """Test display context for a notification with body template.""" + request = rf.get('/plinth/help/about/') + note.body_template = 'invalid-template.html' note.save() - context = Notification.get_display_context(user) + context = Notification.get_display_context(request, user) assert context['notifications'][0]['body'] == { 'content': b'Template invalid-template.html does not exist.' } @@ -405,6 +409,7 @@ def test_display_context_body_template(note, user, load_cfg): note.body_template = 'test-notification.html' note.save() - context = Notification.get_display_context(user) + context = Notification.get_display_context(request, user) context_note = context['notifications'][0] - assert context_note['body'].content == b'Test notification body\n' + assert context_note['body'].content == \ + b'Test notification body /plinth/help/about/\n' From 6f30e12cd7f0f7f270f6461d4f17b10177530b50 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 25 Jun 2020 15:52:07 -0700 Subject: [PATCH 49/90] views: Drop use of private Django utility We are currently using django.utils.http.is_safe_url which is a private method and may break API anytime. Replace it with similar but limited implementation. Tests: - Unit tests. - Dismiss a notification and the redirect to the same page happens properly. - Logout, goto to home page or login page. Change the language and it will redirect back to home page or login page appropriately. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/tests/test_views.py | 31 +++++++++++++++++++++++++++++++ plinth/views.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 plinth/tests/test_views.py diff --git a/plinth/tests/test_views.py b/plinth/tests/test_views.py new file mode 100644 index 000000000..f4971a36b --- /dev/null +++ b/plinth/tests/test_views.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Tests for common FreedomBox views. +""" + +import pytest + +from plinth.views import is_safe_url + + +@pytest.mark.parametrize('url', [ + '/plinth/login/', + '/', + 'safe', +]) +def test_is_safe_url_valid_url(url): + """Test valid URLs for safe URL checks.""" + assert is_safe_url(url) + + +@pytest.mark.parametrize('url', [ + '', + None, + '\\plinth', + '///plinth', + 'https://example.com/plinth/login/', + 'https:///plinth/login', +]) +def test_is_safe_url_invalid_url(url): + """Test invalid URLs for safe URL checks.""" + assert not is_safe_url(url) diff --git a/plinth/views.py b/plinth/views.py index 8e4fa5b70..1117cb83c 100644 --- a/plinth/views.py +++ b/plinth/views.py @@ -4,13 +4,13 @@ Main FreedomBox views. """ import time +import urllib.parse from django.contrib import messages from django.core.exceptions import ImproperlyConfigured from django.http import Http404, HttpResponseBadRequest, HttpResponseRedirect from django.template.response import TemplateResponse from django.urls import reverse -from django.utils.http import is_safe_url from django.utils.translation import ugettext as _ from django.views.generic import TemplateView from django.views.generic.edit import FormView @@ -26,11 +26,35 @@ from . import forms, frontpage REDIRECT_FIELD_NAME = 'next' +def is_safe_url(url): + """Check if the URL is safe to redirect to. + + Based on Django internal utility. + + """ + if url is not None: + url = url.strip() + + if not url: + return False + + if '\\' in url or url.startswith('///'): + return False + + result = urllib.parse.urlparse(url) + + # Only accept URLs to the same site and scheme. + if result.scheme or result.netloc: + return False + + return True + + def _get_redirect_url_from_param(request): """Return the redirect URL from 'next' GET/POST param.""" redirect_to = request.GET.get(REDIRECT_FIELD_NAME, '') redirect_to = request.POST.get(REDIRECT_FIELD_NAME, redirect_to) - if is_safe_url(url=redirect_to, allowed_hosts={request.get_host()}): + if is_safe_url(redirect_to): return redirect_to return reverse('index') From 6230f8e6ce06904a1ba0ebabf893b58ac480706d Mon Sep 17 00:00:00 2001 From: Jens Molgaard Date: Sun, 28 Jun 2020 13:20:22 +0000 Subject: [PATCH 50/90] Translated using Weblate (Danish) Currently translated at 42.7% (547 of 1281 strings) --- plinth/locale/da/LC_MESSAGES/django.po | 372 +++++++++++-------------- 1 file changed, 164 insertions(+), 208 deletions(-) diff --git a/plinth/locale/da/LC_MESSAGES/django.po b/plinth/locale/da/LC_MESSAGES/django.po index b1dda59f3..3697c3f31 100644 --- a/plinth/locale/da/LC_MESSAGES/django.po +++ b/plinth/locale/da/LC_MESSAGES/django.po @@ -10,8 +10,8 @@ msgstr "" "Project-Id-Version: FreedomBox UI\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-15 19:20-0400\n" -"PO-Revision-Date: 2016-07-03 21:44+0000\n" -"Last-Translator: Mikkel Kirkgaard Nielsen \n" +"PO-Revision-Date: 2020-06-28 19:30+0000\n" +"Last-Translator: Jens Molgaard \n" "Language-Team: Danish \n" "Language: da\n" @@ -19,21 +19,20 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 2.7-dev\n" +"X-Generator: Weblate 4.2-dev\n" #: doc/dev/_templates/layout.html:11 msgid "Page source" -msgstr "" +msgstr "Sidens kildekode" #: plinth/context_processors.py:23 plinth/views.py:53 msgid "FreedomBox" msgstr "FreedomBox" #: plinth/daemon.py:85 -#, fuzzy, python-brace-format -#| msgid "Service discovery server is running" +#, python-brace-format msgid "Service {service_name} is running" -msgstr "Tjenestesøgningstjenesten er aktiv" +msgstr "Tjenesten {service_name} er aktiv" #: plinth/daemon.py:111 #, python-brace-format @@ -56,30 +55,28 @@ msgid "Cannot connect to {host}:{port}" msgstr "Kan ikke forbinde til {host}:{port}" #: plinth/forms.py:38 -#, fuzzy -#| msgid "Invalid domain name" msgid "Select a domain name to be used with this application" -msgstr "Ugyldigt domænenavn" +msgstr "Vælg et domænenavn til brug i denne applikation" #: plinth/forms.py:40 msgid "" "Warning! The application may not work properly if domain name is changed " "later." msgstr "" +"Advarsel! Applikationen holder muligvis op med at fungere korrekt hvis " +"domænenavnet bliver ændret senere." #: plinth/forms.py:48 msgid "Language" msgstr "Sprog" #: plinth/forms.py:49 -#, fuzzy -#| msgid "Language for this web administration interface" msgid "Language to use for presenting this web interface" -msgstr "Sprog i dette administrationsværktøj" +msgstr "Sprog denne web-brugergrænseflade skal vises i" #: plinth/forms.py:56 msgid "Use the language preference set in the browser" -msgstr "" +msgstr "Benyt browserens sprogindstilling" #: plinth/middleware.py:57 plinth/templates/setup.html:30 msgid "Application installed." @@ -139,20 +136,20 @@ msgstr "Tjenestesøgning" #: plinth/modules/avahi/__init__.py:68 msgid "Local Network Domain" -msgstr "" +msgstr "Lokalt netværksdomæne" #: plinth/modules/backups/__init__.py:28 msgid "Backups allows creating and managing backup archives." -msgstr "" +msgstr "Sikkerhedskopiering lader dig oprette og administrere sikkerhedskopier." #: plinth/modules/backups/__init__.py:47 msgid "Backups" -msgstr "" +msgstr "Sikkerhedskopiering" #: plinth/modules/backups/forms.py:33 #, python-brace-format msgid "{app} (No data to backup)" -msgstr "" +msgstr "{app} (Ingen data at sikkerhedskopiere)" #: plinth/modules/backups/forms.py:52 #: plinth/modules/backups/templates/backups_delete.html:18 @@ -164,225 +161,201 @@ msgstr "Navn" #: plinth/modules/backups/forms.py:53 msgid "(Optional) Set a name for this backup archive" -msgstr "" +msgstr "(Valgfrit) Navngiv denne sikkerhedskopi" #: plinth/modules/backups/forms.py:56 msgid "Included apps" -msgstr "" +msgstr "Inkluderede apps" #: plinth/modules/backups/forms.py:56 msgid "Apps to include in the backup" -msgstr "" +msgstr "Applikationer som skal inkluderes i sikkerhedskopien" #: plinth/modules/backups/forms.py:70 -#, fuzzy -#| msgid "The subdomain you want to register" msgid "Select the apps you want to restore" -msgstr "Subdomænet du vil registrere" +msgstr "Vælg de apps du vil genskabe" #: plinth/modules/backups/forms.py:83 msgid "Upload File" -msgstr "" +msgstr "Upload fil" #: plinth/modules/backups/forms.py:85 msgid "Backup files have to be in .tar.gz format" -msgstr "" +msgstr "Sikkerhedskopier skal være gemt i filformatet .tar.gz" #: plinth/modules/backups/forms.py:86 msgid "Select the backup file you want to upload" -msgstr "" +msgstr "Vælg den sikkerhedskopi du vil uploade" #: plinth/modules/backups/forms.py:92 -#, fuzzy -#| msgid "packages not found" msgid "Repository path format incorrect." -msgstr "pakker ikke fundet" +msgstr "Sti til fjernlager er fejlagtigt angivet." #: plinth/modules/backups/forms.py:99 -#, fuzzy, python-brace-format -#| msgid "Invalid server name" +#, python-brace-format msgid "Invalid username: {username}" -msgstr "Ugyldigt servernavn" +msgstr "Ugyldigt brugernavn: {username}" #: plinth/modules/backups/forms.py:109 -#, fuzzy, python-brace-format -#| msgid "Invalid hostname" +#, python-brace-format msgid "Invalid hostname: {hostname}" -msgstr "Ugyldigt værtsnavn" +msgstr "Ugyldigt værtsnavn: {hostname}" #: plinth/modules/backups/forms.py:113 #, python-brace-format msgid "Invalid directory path: {dir_path}" -msgstr "" +msgstr "Ugyldig sti til filkatalog: {dir_path}" #: plinth/modules/backups/forms.py:119 -#, fuzzy -#| msgid "Description" msgid "Encryption" -msgstr "Beskrivelse" +msgstr "Kryptering" #: plinth/modules/backups/forms.py:120 msgid "" "\"Key in Repository\" means that a password-protected key is stored with the " "backup." msgstr "" +"\"Nøgle i arkiv\" betyder at en nøglefil beskyttet med kodeord gemmes sammen " +"med sikkerhedskopien." #: plinth/modules/backups/forms.py:124 plinth/modules/networks/forms.py:262 msgid "Passphrase" -msgstr "Kodesætning" +msgstr "Adgangsfrase" #: plinth/modules/backups/forms.py:125 msgid "Passphrase; Only needed when using encryption." -msgstr "" +msgstr "Adgangsfrase; Kun påkrævet ved brug af kryptering." #: plinth/modules/backups/forms.py:128 -#, fuzzy -#| msgid "Passphrase" msgid "Confirm Passphrase" -msgstr "Kodesætning" +msgstr "Bekræft adgangsfrase" #: plinth/modules/backups/forms.py:128 msgid "Repeat the passphrase." -msgstr "" +msgstr "Gentag adgangsfrasen." #: plinth/modules/backups/forms.py:139 msgid "The entered encryption passphrases do not match" -msgstr "" +msgstr "De angivne adgangsfraser til kryptering er ikke ens" #: plinth/modules/backups/forms.py:143 msgid "Passphrase is needed for encryption." -msgstr "" +msgstr "Adgangsfrase er påkrævet ved brug af kryptering." #: plinth/modules/backups/forms.py:178 msgid "Select Disk or Partition" -msgstr "" +msgstr "Vælg disk eller partition" #: plinth/modules/backups/forms.py:179 msgid "Backups will be stored in the directory FreedomBoxBackups" -msgstr "" +msgstr "Sikkerhedskopier vil blive gemt i mappen FreedomBoxBackups" #: plinth/modules/backups/forms.py:188 msgid "SSH Repository Path" -msgstr "" +msgstr "Sti til SSH-fjernlager" #: plinth/modules/backups/forms.py:189 msgid "" "Path of a new or existing repository. Example: user@host:~/path/to/repo/" msgstr "" +"Sti til nyt eller eksisterende fjernlager. Eksempelvis: bruger@vært:~/sti/" +"til/arkiv/" #: plinth/modules/backups/forms.py:193 -#, fuzzy -#| msgid "Save Password" msgid "SSH server password" -msgstr "Gem Kodeord" +msgstr "SSH-serverens adgangskode" #: plinth/modules/backups/forms.py:194 msgid "" "Password of the SSH Server.
SSH key-based authentication is not yet " "possible." msgstr "" +"SSH-serverens adgangskode.
Nøglebaseret SSH-autentifikation er endnu " +"ikke muligt." #: plinth/modules/backups/forms.py:213 msgid "Remote backup repository already exists." -msgstr "" +msgstr "Fjernlager for sikkerhedskopier eksisterer i forvejen." #: plinth/modules/backups/forms.py:219 msgid "Select verified SSH public key" -msgstr "" +msgstr "Vælg en verificeret offentlig SSH-nøgle" #: plinth/modules/backups/repository.py:33 msgid "" "Connection refused - make sure you provided correct credentials and the " "server is running." msgstr "" +"Forbindelse afvist – sørg for at du har angivet de korrekte loginoplysninger " +"og at serveren kører." #: plinth/modules/backups/repository.py:40 -#, fuzzy -#| msgid "Connection Type" msgid "Connection refused" -msgstr "Forbindelsestype" +msgstr "Forbindelse afvist" #: plinth/modules/backups/repository.py:47 -#, fuzzy -#| msgid "packages not found" msgid "Repository not found" -msgstr "pakker ikke fundet" +msgstr "Arkiv ikke fundet" #: plinth/modules/backups/repository.py:52 msgid "Incorrect encryption passphrase" -msgstr "" +msgstr "Fejlagtig adgangsfrase til kryptering" #: plinth/modules/backups/repository.py:57 msgid "SSH access denied" -msgstr "" +msgstr "SSH-adgang afvist" #: plinth/modules/backups/repository.py:63 msgid "Repository path is neither empty nor is an existing backups repository." msgstr "" +"Sti til lager er hverken tom eller et eksisterende lager af sikkerhedskopier." #: plinth/modules/backups/repository.py:137 msgid "Existing repository is not encrypted." -msgstr "" +msgstr "Eksisterende lager er ikke krypteret." #: plinth/modules/backups/repository.py:322 -#, fuzzy, python-brace-format -#| msgid "{box_name} Manual" +#, python-brace-format msgid "{box_name} storage" -msgstr "{box_name} Brugervejledning" +msgstr "Lagring af {box_name}" #: plinth/modules/backups/templates/backups.html:30 #: plinth/modules/backups/views.py:60 -#, fuzzy -#| msgid "PageKite Account" msgid "Create a new backup" -msgstr "PageKite-konto" +msgstr "Opret en ny sikkerhedskopi" #: plinth/modules/backups/templates/backups.html:34 -#, fuzzy -#| msgid "PageKite Account" msgid "Create Backup" -msgstr "PageKite-konto" +msgstr "Opret sikkerhedskopi" #: plinth/modules/backups/templates/backups.html:37 msgid "Upload and restore a backup archive" -msgstr "" +msgstr "Upload og genopret en sikkerhedskopi" #: plinth/modules/backups/templates/backups.html:41 -#, fuzzy -#| msgid "Save Password" msgid "Upload and Restore" -msgstr "Gem Kodeord" +msgstr "Upload og genopret" #: plinth/modules/backups/templates/backups.html:44 -#, fuzzy -#| msgid "Existing custom services" msgid "Add a backup location" -msgstr "Eksisterende brugerdefinerede tjenester" +msgstr "Tilføj sikkerhedskopieringslager" #: plinth/modules/backups/templates/backups.html:48 -#, fuzzy -#| msgid "Existing custom services" msgid "Add Backup Location" -msgstr "Eksisterende brugerdefinerede tjenester" +msgstr "Tilføj sikkerhedskopieringslager" #: plinth/modules/backups/templates/backups.html:51 -#, fuzzy -#| msgid "Existing custom services" msgid "Add a remote backup location" -msgstr "Eksisterende brugerdefinerede tjenester" +msgstr "Tilføj fjernlager for sikkerhedskopier" #: plinth/modules/backups/templates/backups.html:55 -#, fuzzy -#| msgid "Existing custom services" msgid "Add Remote Backup Location" -msgstr "Eksisterende brugerdefinerede tjenester" +msgstr "Tilføj fjernlager for sikkerhedskopier" #: plinth/modules/backups/templates/backups.html:58 -#, fuzzy -#| msgid "Existing custom services" msgid "Existing Backups" -msgstr "Eksisterende brugerdefinerede tjenester" +msgstr "Eksisterende sikkerhedskopier" #: plinth/modules/backups/templates/backups_add_remote_repository.html:19 #, python-format @@ -391,37 +364,31 @@ msgid "" "To restore a backup on a new %(box_name)s you need the ssh credentials and, " "if chosen, the encryption passphrase." msgstr "" +"Adgangsoplysningerne til dette arkiv gemmes på din %(box_name)s.
For " +"at genoprette en sikkerhedskopi på en ny %(box_name)s har du brug for SSH-" +"loginoplysningerne samt adgangsfrasen til kryptering hvis denne er slået til." #: plinth/modules/backups/templates/backups_add_remote_repository.html:28 -#, fuzzy -#| msgid "Create Connection" msgid "Create Location" -msgstr "Opret Forbindelse" +msgstr "Opret sted" #: plinth/modules/backups/templates/backups_add_repository.html:19 #: plinth/modules/gitweb/views.py:49 -#, fuzzy -#| msgid "Create User" msgid "Create Repository" -msgstr "Opret Bruger" +msgstr "Opret lager" #: plinth/modules/backups/templates/backups_delete.html:12 -#, fuzzy -#| msgid "Delete user permanently?" msgid "Delete this archive permanently?" -msgstr "Slet bruger permanent?" +msgstr "Slet dette arkiv permanent?" #: plinth/modules/backups/templates/backups_delete.html:19 -#, fuzzy -#| msgid "Time Zone" msgid "Time" -msgstr "Tidszone" +msgstr "Tid" #: plinth/modules/backups/templates/backups_delete.html:36 -#, fuzzy, python-format -#| msgid "Delete %(name)s" +#, python-format msgid "Delete Archive %(name)s" -msgstr "Slet %(name)s" +msgstr "Slet arkivet %(name)s" #: plinth/modules/backups/templates/backups_form.html:20 #: plinth/modules/gitweb/templates/gitweb_create_edit.html:20 @@ -431,75 +398,63 @@ msgstr "Slet %(name)s" #: plinth/modules/pagekite/templates/pagekite_custom_services.html:28 #: plinth/modules/sharing/templates/sharing_add_edit.html:20 msgid "Submit" -msgstr "Indsend" +msgstr "Send" #: plinth/modules/backups/templates/backups_repository.html:19 -#, fuzzy -#| msgid "packages not found" msgid "This repository is encrypted" -msgstr "pakker ikke fundet" +msgstr "Dette arkiv er krypteret" #: plinth/modules/backups/templates/backups_repository.html:34 -#, fuzzy -#| msgid "Documentation" msgid "Unmount Location" -msgstr "Dokumentation" +msgstr "Afmonter lager" #: plinth/modules/backups/templates/backups_repository.html:45 -#, fuzzy -#| msgid "Mount Point" msgid "Mount Location" msgstr "Monteringspunkt" #: plinth/modules/backups/templates/backups_repository.html:56 msgid "Remove Backup Location. This will not delete the remote backup." msgstr "" +"Fjern sikkerhedskopilageret. Dette vil ikke slette sikkerhedskopien på " +"fjernlageret." #: plinth/modules/backups/templates/backups_repository.html:77 -#, fuzzy -#| msgid "downloading" msgid "Download" -msgstr "downloader" +msgstr "Downloader" #: plinth/modules/backups/templates/backups_repository.html:81 #: plinth/modules/backups/templates/backups_restore.html:28 #: plinth/modules/backups/views.py:155 -#, fuzzy -#| msgid "reStore" msgid "Restore" -msgstr "reStore" +msgstr "Genopret" #: plinth/modules/backups/templates/backups_repository.html:103 msgid "No archives currently exist." -msgstr "" +msgstr "Ingen arkiver er oprettet endnu." #: plinth/modules/backups/templates/backups_repository_remove.html:13 msgid "Are you sure that you want to remove this repository?" -msgstr "" +msgstr "Er du sikker på at du vil fjerne dette lager?" #: plinth/modules/backups/templates/backups_repository_remove.html:19 msgid "" "The remote repository will not be deleted. This just removes the repository " "from the listing on the backup page, you can add it again later on." msgstr "" +"Fjernlageret vil ikke blive slettet. Dette fjerner bare lageret fra listen " +"på sikkerhedskopieringssiden – du kan tilføje det igen senere." #: plinth/modules/backups/templates/backups_repository_remove.html:31 -#, fuzzy -#| msgid "Documentation" msgid "Remove Location" -msgstr "Dokumentation" +msgstr "Fjern sted" #: plinth/modules/backups/templates/backups_restore.html:15 -#, fuzzy -#| msgid "Delete %(name)s" msgid "Restore data from" -msgstr "Slet %(name)s" +msgstr "Genopret data fra" #: plinth/modules/backups/templates/backups_restore.html:32 -#, fuzzy -#| msgid "reStore" msgid "Restoring" -msgstr "reStore" +msgstr "Genopretter" #: plinth/modules/backups/templates/backups_upload.html:17 #, python-format @@ -512,11 +467,17 @@ msgid "" " backup file.\n" " " msgstr "" +"\n" +" Upload en sikkerhedskopi fra en anden %(box_name)s for at genskabe " +"dens\n" +" indhold. Når du har uploadet en sikkerhedskopi kan du vælge hvilke\n" +" applikationer du vil genoprette.\n" +" " #: plinth/modules/backups/templates/backups_upload.html:27 #: plinth/modules/help/templates/statuslog.html:23 msgid "Caution:" -msgstr "" +msgstr "Advarsel:" #: plinth/modules/backups/templates/backups_upload.html:28 #, python-format @@ -524,12 +485,12 @@ msgid "" "You have %(max_filesize)s available to restore a backup. Exceeding this " "limit can leave your %(box_name)s unusable." msgstr "" +"Du har %(max_filesize)s tilgængelig til at genoprette en backup. Hvis du " +"overskrider denne grænse kan det efterlade din %(box_name)s ubrugelig." #: plinth/modules/backups/templates/backups_upload.html:41 -#, fuzzy -#| msgid "Download my profile" msgid "Upload file" -msgstr "Hent min profil" +msgstr "Upload fil" #: plinth/modules/backups/templates/verify_ssh_hostkey.html:18 #, python-format @@ -537,6 +498,8 @@ msgid "" "Could not reach SSH host %(hostname)s. Please verify that the host is up and " "accepting connections." msgstr "" +"SSH-værten %(hostname)s kunne ikke nås. Sørg venligst for at værten kører og " +"accepterer forbindelser." #: plinth/modules/backups/templates/verify_ssh_hostkey.html:28 #, python-format @@ -544,10 +507,12 @@ msgid "" "The authenticity of SSH host %(hostname)s could not be established. The host " "advertises the following SSH public keys. Please verify any one of them." msgstr "" +"Autenticiteten af SSH-værten %(hostname)s kunne ikke bekræftes. Værten " +"angiver de følgende offentlige SSH-nøgler. Verificér venligst en af dem." #: plinth/modules/backups/templates/verify_ssh_hostkey.html:40 msgid "How to verify?" -msgstr "" +msgstr "Hvordan verificerer man?" #: plinth/modules/backups/templates/verify_ssh_hostkey.html:45 msgid "" @@ -555,116 +520,109 @@ msgid "" "one of the provided options. You can also use dsa, ecdsa, ed25519 etc. " "instead of rsa, by choosing the corresponding file." msgstr "" +"Kør følgende kommando på SSH-værtscomputeren. Output skal svare til en af de " +"angivne indstillinger. I stedet for rsa kan du også bruge dsa, ecdsa, " +"ed25519 osv. ved at vælge den tilsvarende fil." #: plinth/modules/backups/templates/verify_ssh_hostkey.html:60 msgid "Verify Host" -msgstr "" +msgstr "Verificér vært" #: plinth/modules/backups/views.py:55 msgid "Archive created." -msgstr "" +msgstr "Arkiv oprettet." #: plinth/modules/backups/views.py:83 -#, fuzzy -#| msgid "Delete" msgid "Delete Archive" -msgstr "Slet" +msgstr "Slet arkiv" #: plinth/modules/backups/views.py:95 -#, fuzzy -#| msgid "{name} deleted." msgid "Archive deleted." -msgstr "{name} slettet." +msgstr "Arkiv slettet." #: plinth/modules/backups/views.py:108 msgid "Upload and restore a backup" -msgstr "" +msgstr "Upload og genopret en sikkerhedskopi" #: plinth/modules/backups/views.py:143 msgid "Restored files from backup." -msgstr "" +msgstr "Filer genoprettet fra sikkerhedskopi." #: plinth/modules/backups/views.py:170 msgid "No backup file found." -msgstr "" +msgstr "Ingen sikkerhedskopi fundet." #: plinth/modules/backups/views.py:178 msgid "Restore from uploaded file" -msgstr "" +msgstr "Genopret fra overført fil" #: plinth/modules/backups/views.py:235 msgid "No additional disks available to add a repository." -msgstr "" +msgstr "Ingen yderligere diske er tilgængelige til oprettelse af et lager." #: plinth/modules/backups/views.py:243 -#, fuzzy -#| msgid "Create User" msgid "Create backup repository" -msgstr "Opret Bruger" +msgstr "Opret lager til sikkerhedskopier" #: plinth/modules/backups/views.py:270 msgid "Create remote backup repository" -msgstr "" +msgstr "Opret fjernlager til sikkerhedskopier" #: plinth/modules/backups/views.py:289 -#, fuzzy -#| msgid "Create User" msgid "Added new remote SSH repository." -msgstr "Opret Bruger" +msgstr "Tilføjede et nyt SSH-fjernlager." #: plinth/modules/backups/views.py:311 msgid "Verify SSH hostkey" -msgstr "" +msgstr "Verificér SSH-værtsnøgle" #: plinth/modules/backups/views.py:337 msgid "SSH host already verified." -msgstr "" +msgstr "SSH-vært er allerede verificeret." #: plinth/modules/backups/views.py:347 msgid "SSH host verified." -msgstr "" +msgstr "SSH-vært verificeret." #: plinth/modules/backups/views.py:361 msgid "SSH host public key could not be verified." -msgstr "" +msgstr "SSH-værtens offentlige nøgle kunne ikke verificeres." #: plinth/modules/backups/views.py:363 msgid "Authentication to remote server failed." -msgstr "" +msgstr "Godkendelse til serveren mislykkedes." #: plinth/modules/backups/views.py:365 -#, fuzzy -#| msgid "Error installing application: {error}" msgid "Error establishing connection to server: {}" -msgstr "Kunne ikke installere applikation: {error}" +msgstr "Fejl ved oprettelse af forbindelse til serveren: {}" #: plinth/modules/backups/views.py:376 -#, fuzzy -#| msgid "packages not found" msgid "Repository removed." -msgstr "pakker ikke fundet" +msgstr "Lager fjernet." #: plinth/modules/backups/views.py:390 msgid "Remove Repository" -msgstr "" +msgstr "Fjern lager" #: plinth/modules/backups/views.py:399 msgid "Repository removed. Backups were not deleted." -msgstr "" +msgstr "Lager fjernet. Sikkerhedskopier slettedes ikke." #: plinth/modules/backups/views.py:409 msgid "Unmounting failed!" -msgstr "" +msgstr "Afmontering mislykkedes!" #: plinth/modules/backups/views.py:424 plinth/modules/backups/views.py:428 msgid "Mounting failed" -msgstr "" +msgstr "Montering mislykkedes" #: 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 "" +"Med BIND kan du offentliggøre dine DNS-oplysninger (Domain Name System) på " +"internettet, og løse DNS-forespørgsler fra brugerenhederne på dit netværk." #: plinth/modules/bind/__init__.py:33 #, python-brace-format @@ -673,41 +631,40 @@ msgid "" "machines on local network. It is also incompatible with sharing Internet " "connection from {box_name}." msgstr "" +"I øjeblikket bruges BIND på {box_name} kun til at løse DNS-forespørgsler for " +"andre maskiner på det lokale netværk. Det er også uforeneligt med deling af " +"internetforbindelse fra {box_name}." #: plinth/modules/bind/__init__.py:82 msgid "BIND" -msgstr "" +msgstr "BIND" #: plinth/modules/bind/__init__.py:83 -#, fuzzy -#| msgid "Domain Name" msgid "Domain Name Server" -msgstr "Domænenavn" +msgstr "DNS-server" #: plinth/modules/bind/forms.py:20 msgid "Forwarders" -msgstr "" +msgstr "Viderestillere" #: plinth/modules/bind/forms.py:21 msgid "" "A list DNS servers, separated by space, to which requests will be forwarded" msgstr "" +"En liste over DNS-servere, adskilt med mellemrum, som forespørgsler vil " +"blive viderestillet til" #: plinth/modules/bind/forms.py:25 -#, fuzzy -#| msgid "Enable Dynamic DNS" msgid "Enable DNSSEC" -msgstr "Aktiver Dynamisk DNS" +msgstr "Aktivér DNSSEC" #: plinth/modules/bind/forms.py:26 msgid "Enable Domain Name System Security Extensions" -msgstr "" +msgstr "Akiver sikkerhedsudvidelser til DNS (DNSSEC)" #: plinth/modules/bind/templates/bind.html:11 -#, fuzzy -#| msgid "Server domain" msgid "Serving Domains" -msgstr "Serverdomæne" +msgstr "Betjener domæner" #: plinth/modules/bind/templates/bind.html:16 #: plinth/modules/ikiwiki/forms.py:12 @@ -717,27 +674,21 @@ msgid "Type" msgstr "Type" #: plinth/modules/bind/templates/bind.html:17 -#, fuzzy -#| msgid "Domain Name" msgid "Domain Names" -msgstr "Domænenavn" +msgstr "Domænenavne" #: plinth/modules/bind/templates/bind.html:18 -#, fuzzy -#| msgid "Service" msgid "Serving" -msgstr "Tjeneste" +msgstr "Betjener" #: plinth/modules/bind/templates/bind.html:19 -#, fuzzy -#| msgid "IP address" msgid "IP addresses" -msgstr "IP-adresse" +msgstr "IP-adresser" #: plinth/modules/bind/templates/bind.html:35 #: plinth/modules/bind/templates/bind.html:37 msgid "Refresh IP address and domains" -msgstr "" +msgstr "Opfrisk IP-adresse og domæner" #: plinth/modules/bind/views.py:72 plinth/modules/coturn/views.py:40 #: plinth/modules/deluge/views.py:42 plinth/modules/dynamicdns/views.py:150 @@ -755,6 +706,10 @@ msgid "" "advanced functions that are not usually required. A web based terminal for " "console operations is also available." msgstr "" +"Cockpit er et serverhåndteringsværktøj til administration af GNU/Linux-" +"servere via en webbrowser. På en {box_name} er der indstillingsmuligheder " +"for mange avancerede funktioner som typisk ikke påkræves. En web-baseret " +"terminal til kørsel af konsolkommandoer er ligeledes tilgængelig." #: plinth/modules/cockpit/__init__.py:38 msgid "" @@ -763,43 +718,44 @@ msgid "" "firewall ports and advanced networking such as bonding, bridging and VLAN " "management." msgstr "" +"Cockpit kan bruges til avanceret håndtering af lagermedier såsom " +"diskpartitionering og RAID-styring. Det kan også bruges til at åbne " +"brugerdefinerede porte i systemets firewall, samt avanceret " +"netværkshåndtering såsom bonding, bridging og håndtering af VLAN." #: plinth/modules/cockpit/__init__.py:43 -#, fuzzy, python-brace-format -#| msgid "" -#| "When enabled, Tiny Tiny RSS will be available from
/" -#| "tt-rss path on the web server." +#, python-brace-format msgid "" "It can be accessed by any user on {box_name} " "belonging to the admin group." msgstr "" -"Når aktiveret, vil Tiny Tiny RSS være tilgængelige på stien /tt-rss på webserveren." +"Det kan tilgås af enhver bruger på {box_name} " +"som tilhører administratorgruppen." #: plinth/modules/cockpit/__init__.py:47 msgid "" "Cockpit requires that you access it through a domain name. It will not work " "when accessed using an IP address as part of the URL." msgstr "" +"Cockpit skal tilgås via et domænenavn. Det fungerer ikke hvis det tilgås med " +"en IP-adresse som en del af webadressen." #: plinth/modules/cockpit/__init__.py:64 plinth/modules/cockpit/manifest.py:12 #: plinth/modules/performance/manifest.py:11 msgid "Cockpit" -msgstr "" +msgstr "Cockpit" #: plinth/modules/cockpit/__init__.py:66 -#, fuzzy -#| msgid "Server domain" msgid "Server Administration" -msgstr "Serverdomæne" +msgstr "Serveradministration" #: plinth/modules/cockpit/templates/cockpit.html:11 msgid "Access" -msgstr "" +msgstr "Adgang" #: plinth/modules/cockpit/templates/cockpit.html:14 msgid "Cockpit will only work when accessed using the following URLs." -msgstr "" +msgstr "Cockpit vil kun fungere når det tilgås gennem de følgende webadresser." #: plinth/modules/config/__init__.py:23 msgid "" From 64b1c21fe0ecf39df6c0185e056206cb062f3496 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Jun 2020 17:33:14 -0700 Subject: [PATCH 51/90] cfg: Don't fallback to develop config if main is not found This was needed when our behavior for reading production vs. development configuration depended on the presence of configuration files in expected locations. The current behavior is based on whether --develop option is given or not. This behavior is safer and more predictable. So, remove the option to fallback to develop configuration if the production configuration is not found. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/cfg.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/plinth/cfg.py b/plinth/cfg.py index f3ebd3576..c270d2b1b 100644 --- a/plinth/cfg.py +++ b/plinth/cfg.py @@ -39,13 +39,8 @@ def get_fallback_config_paths(): def get_config_paths(): - """Get config paths. - Return the fallback plinth config if the default one does not exist""" - root_directory = DEFAULT_ROOT - config_path = DEFAULT_CONFIG_FILE - if not os.path.isfile(config_path): - config_path, root_directory = get_fallback_config_paths() - return config_path, root_directory + """Get default config paths.""" + return '/etc/plinth/plinth.config', '/' def read(config_path=None, root_directory=None): From 4263f9e2c892c187c96658892417a84809cc9752 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Jun 2020 17:36:41 -0700 Subject: [PATCH 52/90] cfg: Drop the default configuration file - The configuration module defaults to values in the production configuration file. - If the file is found, it is read and the read values overwrite the defaults. If the file is not found, no error is raised. This allows us to not ship the configuration file. User may create the configuration if they want to change the defaults. This eases upgrades when configuration is edited. This also make FreedomBox robust to deployments where /etc/ is not populated by default such as OSTree. It is also a good practice for daemons as followed by the likes of systemd. - If the file partly populated only the values read override the defaults and the remaining values don't change. This allows the user to write simpler configuration file. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- conftest.py | 2 +- data/etc/plinth/plinth.config | 36 ----------- debian/freedombox.maintscript | 1 + plinth/cfg.py | 64 ++++++++++++------- .../data/plinth.config.with_missing_options | 2 +- .../data/plinth.config.with_missing_sections | 2 +- plinth/tests/test_cfg.py | 63 ++++++------------ 7 files changed, 67 insertions(+), 103 deletions(-) delete mode 100644 data/etc/plinth/plinth.config diff --git a/conftest.py b/conftest.py index d892c6120..c250213c8 100644 --- a/conftest.py +++ b/conftest.py @@ -55,7 +55,7 @@ def fixture_load_cfg(): cfg_file = test_data_dir / 'etc' / 'plinth' / 'plinth.config' cfg.read(str(cfg_file), str(root_dir)) yield cfg - cfg.read() + cfg.read(str(cfg_file), str(root_dir)) @pytest.fixture(name='develop_mode') diff --git a/data/etc/plinth/plinth.config b/data/etc/plinth/plinth.config deleted file mode 100644 index f1bd618fe..000000000 --- a/data/etc/plinth/plinth.config +++ /dev/null @@ -1,36 +0,0 @@ -[Path] -# directory locations -file_root = /usr/share/plinth -config_dir = /etc/plinth -data_dir = /var/lib/plinth -server_dir = /plinth -actions_dir = /usr/share/plinth/actions -doc_dir = /usr/share/freedombox -custom_static_dir = /var/www/plinth/custom/static - -# file locations -store_file = %(data_dir)s/plinth.sqlite3 - -[Network] -host = 127.0.0.1 -port = 8000 - -# Enable the following only if Plinth is behind a proxy server. The -# proxy server should properly clean and the following HTTP headers: -# X-Forwarded-For -# X-Forwarded-Host -# X-Forwarded-Proto -# If you enable these unnecessarily, this will lead to serious security -# problems. For more information, see -# https://docs.djangoproject.com/en/1.7/ref/settings/ -# -# These are enabled by default in Plinth because the default -# configuration allows only connections from localhost -# -# Leave the values blank to disable -use_x_forwarded_for = True -use_x_forwarded_host = True -secure_proxy_ssl_header = HTTP_X_FORWARDED_PROTO - -[Misc] -box_name = FreedomBox diff --git a/debian/freedombox.maintscript b/debian/freedombox.maintscript index 9349c7399..2ccd25060 100644 --- a/debian/freedombox.maintscript +++ b/debian/freedombox.maintscript @@ -10,3 +10,4 @@ rm_conffile /etc/plinth/modules-enabled/udiskie 0.39.0~ rm_conffile /etc/plinth/modules-enabled/restore 20.1~ rm_conffile /etc/plinth/modules-enabled/repro 20.1~ rm_conffile /etc/apt/preferences.d/50freedombox3.pref 20.5~ +rm_conffile /etc/plinth/plinth.config 20.12~ diff --git a/plinth/cfg.py b/plinth/cfg.py index c270d2b1b..0494cd810 100644 --- a/plinth/cfg.py +++ b/plinth/cfg.py @@ -1,4 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later +""" +Configuration parser and default values for configuration options. +""" import configparser import logging @@ -6,28 +9,46 @@ import os logger = logging.getLogger(__name__) -box_name = None +# [Path] section root = None -file_root = None -config_dir = None -data_dir = None -custom_static_dir = None -store_file = None -actions_dir = None -doc_dir = None -host = None -port = None -use_x_forwarded_for = False -use_x_forwarded_host = False -secure_proxy_ssl_header = None +file_root = '/usr/share/plinth' +config_dir = '/etc/plinth' +data_dir = '/var/lib/plinth' +custom_static_dir = '/var/www/plinth/custom/static' +store_file = data_dir + '/plinth.sqlite3' +actions_dir = '/usr/share/plinth/actions' +doc_dir = '/usr/share/freedombox' +server_dir = '/plinth' + +# [Network] section +host = '127.0.0.1' +port = 8000 + +# Enable the following only if Plinth is behind a proxy server. The +# proxy server should properly clean and the following HTTP headers: +# X-Forwarded-For +# X-Forwarded-Host +# X-Forwarded-Proto +# If you enable these unnecessarily, this will lead to serious security +# problems. For more information, see +# https://docs.djangoproject.com/en/1.7/ref/settings/ +# +# These are enabled by default in FreedomBox because the default +# configuration allows only connections from localhost +# +# Leave the values blank to disable +use_x_forwarded_for = True +use_x_forwarded_host = True +secure_proxy_ssl_header = 'HTTP_X_FORWARDED_PROTO' + +# [Misc] section +box_name = 'FreedomBox' + +# Other globals develop = False -server_dir = '/' config_file = None -DEFAULT_CONFIG_FILE = '/etc/plinth/plinth.config' -DEFAULT_ROOT = '/' - def get_fallback_config_paths(): """Get config paths of the current source code folder""" @@ -54,8 +75,8 @@ def read(config_path=None, root_directory=None): config_path, root_directory = get_config_paths() if not os.path.isfile(config_path): - msg = 'No plinth.config file could be found on path: %s' % config_path - raise FileNotFoundError(msg) + # Ignore missing configuration files + return global config_file # pylint: disable-msg=invalid-name,global-statement config_file = config_path @@ -87,9 +108,8 @@ def read(config_path=None, root_directory=None): try: value = parser.get(section, name) except (configparser.NoSectionError, configparser.NoOptionError): - logger.error('Configuration does not contain option: %s.%s', - section, name) - raise + # Use default values for any missing keys in configuration + continue else: if datatype == 'int': value = int(value) diff --git a/plinth/tests/data/plinth.config.with_missing_options b/plinth/tests/data/plinth.config.with_missing_options index 684b415d1..f66c8f4d3 100644 --- a/plinth/tests/data/plinth.config.with_missing_options +++ b/plinth/tests/data/plinth.config.with_missing_options @@ -1,5 +1,5 @@ [Misc] -box_name = FreedomBox +box_name = FreedomBoxTestMissingOptions [Path] diff --git a/plinth/tests/data/plinth.config.with_missing_sections b/plinth/tests/data/plinth.config.with_missing_sections index fbb473246..1d76ef7a8 100644 --- a/plinth/tests/data/plinth.config.with_missing_sections +++ b/plinth/tests/data/plinth.config.with_missing_sections @@ -1,2 +1,2 @@ [Misc] -box_name = FreedomBox +box_name = FreedomBoxTestMissingSections diff --git a/plinth/tests/test_cfg.py b/plinth/tests/test_cfg.py index f33c2c586..0fab9bb89 100644 --- a/plinth/tests/test_cfg.py +++ b/plinth/tests/test_cfg.py @@ -6,6 +6,7 @@ Test module for configuration module. import configparser import logging import os +from unittest.mock import patch import pytest @@ -23,54 +24,33 @@ logging.disable(logging.CRITICAL) pytestmark = pytest.mark.usefixtures('load_cfg') -@pytest.fixture(name='test_config_file') -def fixture_test_config_file(load_cfg): - """Test fixture to return the configuration file path""" - return cfg.get_config_paths()[0] - - -@pytest.fixture(name='test_config_dir') -def fixture_test_config_dir(load_cfg): - """Test fixture to return the configuration file directory.""" - return cfg.get_config_paths()[1] - - -def test_read_default_config_file(test_config_dir, test_config_file): +def test_read_default_config_file(): """Verify that the default config file can be read correctly.""" + config_file, config_dir = cfg.get_fallback_config_paths() + # Read the plinth.config file directly - parser = configparser.ConfigParser(defaults={'root': test_config_dir}) - parser.read(test_config_file) + parser = configparser.ConfigParser(defaults={'root': config_dir}) + parser.read(config_file) # Read the plinth.config file via the cfg module - cfg.read(test_config_file, test_config_dir) + cfg.read(config_file, config_dir) # Compare the two results compare_configurations(parser) -def test_read_primary_config_file(): +@patch('plinth.cfg.get_config_paths') +def test_read_primary_config_file(get_config_paths): """Verify that the primary config file is used by default.""" - original_config_path = cfg.DEFAULT_CONFIG_FILE - original_root_directory = cfg.DEFAULT_ROOT - expected_config_path = CONFIG_FILE_WITH_MISSING_OPTIONS root_directory = 'x-default-root' expected_root_directory = os.path.realpath(root_directory) + get_config_paths.return_value = (expected_config_path, root_directory) - try: - cfg.DEFAULT_CONFIG_FILE = expected_config_path - cfg.DEFAULT_ROOT = root_directory - # reading the config file will fail, but still cfg.root and - # cfg.config_file will be set for parsing the config file - try: - cfg.read() - except configparser.NoOptionError: - pass - assert cfg.config_file == expected_config_path - assert cfg.root == expected_root_directory - finally: - cfg.DEFAULT_CONFIG_FILE = original_config_path - cfg.DEFAULT_ROOT = original_root_directory + cfg.read() + + assert cfg.config_file == expected_config_path + assert cfg.root == expected_root_directory def test_read_fallback_config_file(): @@ -86,20 +66,19 @@ def test_read_fallback_config_file(): def test_read_missing_config_file(): """Verify that an exception is raised when there's no config file.""" - with pytest.raises(FileNotFoundError): - cfg.read('x-non-existant-file', 'x-root-directory') + cfg.read('x-non-existant-file', 'x-root-directory') -def test_read_config_file_with_missing_sections(test_config_dir): +def test_read_config_file_with_missing_sections(): """Verify that missing configuration sections can be detected.""" - with pytest.raises(configparser.NoSectionError): - cfg.read(CONFIG_FILE_WITH_MISSING_SECTIONS, test_config_dir) + cfg.read(CONFIG_FILE_WITH_MISSING_SECTIONS, TEST_CONFIG_DIR) + assert cfg.box_name == 'FreedomBoxTestMissingSections' -def test_read_config_file_with_missing_options(test_config_dir): +def test_read_config_file_with_missing_options(): """Verify that missing configuration options can be detected.""" - with pytest.raises(configparser.NoOptionError): - cfg.read(CONFIG_FILE_WITH_MISSING_OPTIONS, test_config_dir) + cfg.read(CONFIG_FILE_WITH_MISSING_OPTIONS, TEST_CONFIG_DIR) + assert cfg.box_name == 'FreedomBoxTestMissingOptions' def compare_configurations(parser): From 5b579ff06d46a401cf9b1cf510aff8a1763c25a8 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Jun 2020 19:30:11 -0700 Subject: [PATCH 53/90] frontpage: Read custom shortcuts from multiple locations Read from /etc/plinth, /usr/share/plinth and /var/lib/plinth. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/frontpage.py | 29 ++++++++++++++++++----------- plinth/modules/api/views.py | 4 +--- plinth/tests/conftest.py | 2 +- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/plinth/frontpage.py b/plinth/frontpage.py index 171fb021d..5e63bff1c 100644 --- a/plinth/frontpage.py +++ b/plinth/frontpage.py @@ -5,7 +5,7 @@ Manage application shortcuts on front page. import json import logging -import os +import pathlib from plinth import app, cfg @@ -129,8 +129,6 @@ class Shortcut(app.FollowerComponent): def add_custom_shortcuts(): custom_shortcuts = get_custom_shortcuts() - if not custom_shortcuts: - return for shortcut in custom_shortcuts['shortcuts']: web_app_url = _extract_web_app_url(shortcut) @@ -161,11 +159,20 @@ def _extract_web_app_url(custom_shortcut): def get_custom_shortcuts(): - cfg_dir = os.path.dirname(cfg.config_file) - shortcuts_file = os.path.join(cfg_dir, 'custom-shortcuts.json') - if os.path.isfile(shortcuts_file) and os.stat(shortcuts_file).st_size: - logger.info('Loading custom shortcuts from %s', shortcuts_file) - with open(shortcuts_file) as shortcuts: - custom_shortcuts = json.load(shortcuts) - return custom_shortcuts - return None + """Return a merged dictionary of all custom shortcuts.""" + shortcuts_dirs = [cfg.config_dir, cfg.data_dir, cfg.file_root] + + shortcuts = {'shortcuts': []} + for shortcuts_dir in shortcuts_dirs: + file_path = pathlib.Path(shortcuts_dir) / 'custom-shortcuts.json' + if not file_path.is_file() or not file_path.stat().st_size: + continue + + logger.info('Loading custom shortcuts from %s', file_path) + with file_path.open() as file_handle: + try: + shortcuts['shortcuts'] += json.load(file_handle)['shortcuts'] + except (KeyError, json.JSONDecodeError): + logger.info('Error loading shortcuts from %s', file_path) + + return shortcuts diff --git a/plinth/modules/api/views.py b/plinth/modules/api/views.py index 9fb5e82b9..d5a375409 100644 --- a/plinth/modules/api/views.py +++ b/plinth/modules/api/views.py @@ -40,9 +40,7 @@ def get_shortcuts_as_json(username=None): for shortcut in frontpage.Shortcut.list(username) if shortcut.component_id and shortcut.is_enabled() ] - custom_shortcuts = frontpage.get_custom_shortcuts() - if custom_shortcuts: - shortcuts += custom_shortcuts['shortcuts'] + shortcuts += frontpage.get_custom_shortcuts()['shortcuts'] return {'shortcuts': shortcuts} diff --git a/plinth/tests/conftest.py b/plinth/tests/conftest.py index 122dc1c11..2afad9b4a 100644 --- a/plinth/tests/conftest.py +++ b/plinth/tests/conftest.py @@ -31,7 +31,7 @@ NEXTCLOUD_SHORTCUT = { @pytest.fixture(name='custom_shortcuts_file') def fixture_custom_shortcuts_file(load_cfg, tmp_path): """Fixture to set path for a custom shortcuts file.""" - load_cfg.config_file = str(tmp_path / 'plinth.conf') + load_cfg.config_dir = str(tmp_path) return tmp_path / 'custom-shortcuts.json' From 9def9750c05ad85020d7320431cbd31021648075 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Jun 2020 19:49:46 -0700 Subject: [PATCH 54/90] frontpage: Drop empty custom shortcut files Installing an empty file in /etc/ that is meant to be modified is an unnecessary invitation to upgrade issues. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- custom-shortcuts.json | 0 data/etc/plinth/custom-shortcuts.json | 0 debian/freedombox.maintscript | 1 + 3 files changed, 1 insertion(+) delete mode 100644 custom-shortcuts.json delete mode 100644 data/etc/plinth/custom-shortcuts.json diff --git a/custom-shortcuts.json b/custom-shortcuts.json deleted file mode 100644 index e69de29bb..000000000 diff --git a/data/etc/plinth/custom-shortcuts.json b/data/etc/plinth/custom-shortcuts.json deleted file mode 100644 index e69de29bb..000000000 diff --git a/debian/freedombox.maintscript b/debian/freedombox.maintscript index 2ccd25060..aea3d40da 100644 --- a/debian/freedombox.maintscript +++ b/debian/freedombox.maintscript @@ -11,3 +11,4 @@ rm_conffile /etc/plinth/modules-enabled/restore 20.1~ rm_conffile /etc/plinth/modules-enabled/repro 20.1~ rm_conffile /etc/apt/preferences.d/50freedombox3.pref 20.5~ rm_conffile /etc/plinth/plinth.config 20.12~ +rm_conffile /etc/plinth/custom-shortcuts.json 20.12~ From 2a38e60d1ce8ac531bff3f9f48c74c3a85d7d438 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Jun 2020 19:51:23 -0700 Subject: [PATCH 55/90] cfg: Allow loading multiple configuration files Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/__main__.py | 3 ++- plinth/cfg.py | 8 ++++---- plinth/tests/test_cfg.py | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/plinth/__main__.py b/plinth/__main__.py index e594cc0fb..6d3abd820 100644 --- a/plinth/__main__.py +++ b/plinth/__main__.py @@ -136,7 +136,8 @@ def main(): web_framework.init() logger.info('FreedomBox Service (Plinth) version - %s', __version__) - logger.info('Configuration loaded from file - %s', cfg.config_file) + for config_file in cfg.config_files: + logger.info('Configuration loaded from file - %s', config_file) logger.info('Script prefix - %s', cfg.server_dir) module_loader.include_urls() diff --git a/plinth/cfg.py b/plinth/cfg.py index 0494cd810..c09afb951 100644 --- a/plinth/cfg.py +++ b/plinth/cfg.py @@ -47,7 +47,7 @@ box_name = 'FreedomBox' # Other globals develop = False -config_file = None +config_files = [] def get_fallback_config_paths(): @@ -78,13 +78,13 @@ def read(config_path=None, root_directory=None): # Ignore missing configuration files return - global config_file # pylint: disable-msg=invalid-name,global-statement - config_file = config_path + # Keep a note of configuration files read. + config_files.append(config_path) parser = configparser.ConfigParser(defaults={ 'root': os.path.realpath(root_directory), }) - parser.read(config_file) + parser.read(config_path) config_items = ( ('Path', 'root', 'string'), diff --git a/plinth/tests/test_cfg.py b/plinth/tests/test_cfg.py index 0fab9bb89..0f709cc27 100644 --- a/plinth/tests/test_cfg.py +++ b/plinth/tests/test_cfg.py @@ -49,7 +49,7 @@ def test_read_primary_config_file(get_config_paths): cfg.read() - assert cfg.config_file == expected_config_path + assert cfg.config_files[-1] == expected_config_path assert cfg.root == expected_root_directory @@ -60,7 +60,7 @@ def test_read_fallback_config_file(): fallback_config_file = os.path.join(fallback_root, 'plinth.config') config_path, root_directory = cfg.get_fallback_config_paths() cfg.read(config_path, root_directory) - assert cfg.config_file == fallback_config_file + assert cfg.config_files[-1] == fallback_config_file assert cfg.root == fallback_root From 8d2c33bf711b2120a0d202f7cfeca44ea51e1b15 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Jun 2020 20:44:59 -0700 Subject: [PATCH 56/90] cfg: For develop mode, overlay on top of regular configuration Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- conftest.py | 4 ++-- plinth/__main__.py | 7 +++---- plinth/cfg.py | 19 +++++++++++-------- plinth/tests/test_cfg.py | 14 +++++++------- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/conftest.py b/conftest.py index c250213c8..3e6e47364 100644 --- a/conftest.py +++ b/conftest.py @@ -53,9 +53,9 @@ def fixture_load_cfg(): root_dir = pathlib.Path(__file__).resolve().parent test_data_dir = root_dir / 'plinth' / 'tests' / 'data' cfg_file = test_data_dir / 'etc' / 'plinth' / 'plinth.config' - cfg.read(str(cfg_file), str(root_dir)) + cfg.read_file(str(cfg_file), str(root_dir)) yield cfg - cfg.read(str(cfg_file), str(root_dir)) + cfg.read_file(str(cfg_file), str(root_dir)) @pytest.fixture(name='develop_mode') diff --git a/plinth/__main__.py b/plinth/__main__.py index 6d3abd820..3fc6e1adc 100644 --- a/plinth/__main__.py +++ b/plinth/__main__.py @@ -122,12 +122,11 @@ def main(): """Initialize and start the application""" arguments = parse_arguments() + cfg.read() if arguments.develop: # use the root and plinth.config of the current working directory - config_path, root_directory = cfg.get_fallback_config_paths() - cfg.read(config_path, root_directory) - else: - cfg.read() + config_path, root_directory = cfg.get_develop_config_paths() + cfg.read_file(config_path, root_directory) adapt_config(arguments) diff --git a/plinth/cfg.py b/plinth/cfg.py index c09afb951..70a861e6d 100644 --- a/plinth/cfg.py +++ b/plinth/cfg.py @@ -50,8 +50,8 @@ develop = False config_files = [] -def get_fallback_config_paths(): - """Get config paths of the current source code folder""" +def get_develop_config_paths(): + """Return config paths of current source folder for development mode.""" root_directory = os.path.dirname(os.path.realpath(__file__)) root_directory = os.path.join(root_directory, '..') root_directory = os.path.realpath(root_directory) @@ -64,16 +64,19 @@ def get_config_paths(): return '/etc/plinth/plinth.config', '/' -def read(config_path=None, root_directory=None): - """ - Read configuration. +def read(): + """Read all configuration files.""" + config_path, root_directory = get_config_paths() + read_file(config_path, root_directory) + + +def read_file(config_path, root_directory): + """Read and merge into defaults a single configuration file. - config_path: path of plinth.config file - root_directory: path of plinth root folder - """ - if not config_path and not root_directory: - config_path, root_directory = get_config_paths() + """ if not os.path.isfile(config_path): # Ignore missing configuration files return diff --git a/plinth/tests/test_cfg.py b/plinth/tests/test_cfg.py index 0f709cc27..bb710dc69 100644 --- a/plinth/tests/test_cfg.py +++ b/plinth/tests/test_cfg.py @@ -26,14 +26,14 @@ pytestmark = pytest.mark.usefixtures('load_cfg') def test_read_default_config_file(): """Verify that the default config file can be read correctly.""" - config_file, config_dir = cfg.get_fallback_config_paths() + config_file, config_dir = cfg.get_develop_config_paths() # Read the plinth.config file directly parser = configparser.ConfigParser(defaults={'root': config_dir}) parser.read(config_file) # Read the plinth.config file via the cfg module - cfg.read(config_file, config_dir) + cfg.read_file(config_file, config_dir) # Compare the two results compare_configurations(parser) @@ -58,26 +58,26 @@ def test_read_fallback_config_file(): test_dir = os.path.dirname(os.path.realpath(__file__)) fallback_root = os.path.realpath(os.path.join(test_dir, '..', '..')) fallback_config_file = os.path.join(fallback_root, 'plinth.config') - config_path, root_directory = cfg.get_fallback_config_paths() - cfg.read(config_path, root_directory) + config_path, root_directory = cfg.get_develop_config_paths() + cfg.read_file(config_path, root_directory) assert cfg.config_files[-1] == fallback_config_file assert cfg.root == fallback_root def test_read_missing_config_file(): """Verify that an exception is raised when there's no config file.""" - cfg.read('x-non-existant-file', 'x-root-directory') + cfg.read_file('x-non-existant-file', 'x-root-directory') def test_read_config_file_with_missing_sections(): """Verify that missing configuration sections can be detected.""" - cfg.read(CONFIG_FILE_WITH_MISSING_SECTIONS, TEST_CONFIG_DIR) + cfg.read_file(CONFIG_FILE_WITH_MISSING_SECTIONS, TEST_CONFIG_DIR) assert cfg.box_name == 'FreedomBoxTestMissingSections' def test_read_config_file_with_missing_options(): """Verify that missing configuration options can be detected.""" - cfg.read(CONFIG_FILE_WITH_MISSING_OPTIONS, TEST_CONFIG_DIR) + cfg.read_file(CONFIG_FILE_WITH_MISSING_OPTIONS, TEST_CONFIG_DIR) assert cfg.box_name == 'FreedomBoxTestMissingOptions' From 40663b7b5a8eeb92e2c14df72c74308fdf7e11a1 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Jun 2020 20:45:55 -0700 Subject: [PATCH 57/90] context_processor: tests: Use already available config fixture Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/tests/test_context_processors.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plinth/tests/test_context_processors.py b/plinth/tests/test_context_processors.py index 867c30000..168811274 100644 --- a/plinth/tests/test_context_processors.py +++ b/plinth/tests/test_context_processors.py @@ -8,7 +8,6 @@ from unittest.mock import MagicMock, Mock, patch import pytest from django.http import HttpRequest -from plinth import cfg from plinth import context_processors as cp from plinth import menu as menu_module @@ -20,9 +19,8 @@ def fixture_menu(): @patch('plinth.notification.Notification') -def test_common(Notification): +def test_common(Notification, load_cfg): """Verify that the common() function returns the correct values.""" - cfg.read() # initialize config settings request = HttpRequest() request.path = '/aaa/bbb/ccc/' From 62fc33e12c72cd3887c78043d9a72155f93efa99 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Jun 2020 22:07:26 -0700 Subject: [PATCH 58/90] cfg: Eliminate the need for 'root' directory in configuration Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- conftest.py | 4 +- plinth.config | 2 +- plinth/__main__.py | 5 +-- plinth/actions.py | 4 +- plinth/cfg.py | 28 ++++++------- plinth/tests/data/etc/plinth/plinth.config | 2 +- plinth/tests/test_actions.py | 2 +- plinth/tests/test_cfg.py | 46 ++++++++++------------ 8 files changed, 41 insertions(+), 52 deletions(-) diff --git a/conftest.py b/conftest.py index 3e6e47364..89e0e455d 100644 --- a/conftest.py +++ b/conftest.py @@ -53,9 +53,9 @@ def fixture_load_cfg(): root_dir = pathlib.Path(__file__).resolve().parent test_data_dir = root_dir / 'plinth' / 'tests' / 'data' cfg_file = test_data_dir / 'etc' / 'plinth' / 'plinth.config' - cfg.read_file(str(cfg_file), str(root_dir)) + cfg.read_file(str(cfg_file)) yield cfg - cfg.read_file(str(cfg_file), str(root_dir)) + cfg.read_file(str(cfg_file)) @pytest.fixture(name='develop_mode') diff --git a/plinth.config b/plinth.config index 738da7ed9..8b21b06d6 100644 --- a/plinth.config +++ b/plinth.config @@ -1,6 +1,6 @@ [Path] # directory locations -file_root = %(root)s +file_root = %(parent_dir)s config_dir = %(file_root)s/data/etc/plinth data_dir = %(file_root)s/data/var/lib/plinth server_dir = /plinth diff --git a/plinth/__main__.py b/plinth/__main__.py index 3fc6e1adc..a6f2292cb 100644 --- a/plinth/__main__.py +++ b/plinth/__main__.py @@ -124,9 +124,8 @@ def main(): cfg.read() if arguments.develop: - # use the root and plinth.config of the current working directory - config_path, root_directory = cfg.get_develop_config_paths() - cfg.read_file(config_path, root_directory) + # Use the config in the current working directory + cfg.read_file(cfg.get_develop_config_path()) adapt_config(arguments) diff --git a/plinth/actions.py b/plinth/actions.py index ccbf89426..1827b04a5 100644 --- a/plinth/actions.py +++ b/plinth/actions.py @@ -169,7 +169,7 @@ def _run(action, options=None, input=None, run_in_background=False, if cfg.develop and sudo_call: # Passing 'env' does not work with sudo, so append the PYTHONPATH # as part of the command - sudo_call += ['PYTHONPATH=%s' % cfg.root] + sudo_call += ['PYTHONPATH=%s' % cfg.file_root] if sudo_call: cmd = sudo_call + cmd @@ -186,7 +186,7 @@ def _run(action, options=None, input=None, run_in_background=False, } if cfg.develop: # In development mode pass on local pythonpath to access Plinth - kwargs['env'] = {'PYTHONPATH': cfg.root} + kwargs['env'] = {'PYTHONPATH': cfg.file_root} proc = subprocess.Popen(cmd, **kwargs) diff --git a/plinth/cfg.py b/plinth/cfg.py index 70a861e6d..cc2538189 100644 --- a/plinth/cfg.py +++ b/plinth/cfg.py @@ -6,11 +6,11 @@ Configuration parser and default values for configuration options. import configparser import logging import os +import pathlib logger = logging.getLogger(__name__) # [Path] section -root = None file_root = '/usr/share/plinth' config_dir = '/etc/plinth' data_dir = '/var/lib/plinth' @@ -50,33 +50,28 @@ develop = False config_files = [] -def get_develop_config_paths(): - """Return config paths of current source folder for development mode.""" +def get_develop_config_path(): + """Return config path of current source folder for development mode.""" root_directory = os.path.dirname(os.path.realpath(__file__)) root_directory = os.path.join(root_directory, '..') root_directory = os.path.realpath(root_directory) config_path = os.path.join(root_directory, 'plinth.config') - return config_path, root_directory + return config_path -def get_config_paths(): +def get_config_path(): """Get default config paths.""" - return '/etc/plinth/plinth.config', '/' + return '/etc/plinth/plinth.config' def read(): """Read all configuration files.""" - config_path, root_directory = get_config_paths() - read_file(config_path, root_directory) + config_path = get_config_path() + read_file(config_path) -def read_file(config_path, root_directory): - """Read and merge into defaults a single configuration file. - - - config_path: path of plinth.config file - - root_directory: path of plinth root folder - - """ +def read_file(config_path): + """Read and merge into defaults a single configuration file.""" if not os.path.isfile(config_path): # Ignore missing configuration files return @@ -85,12 +80,11 @@ def read_file(config_path, root_directory): config_files.append(config_path) parser = configparser.ConfigParser(defaults={ - 'root': os.path.realpath(root_directory), + 'parent_dir': pathlib.Path(config_path).parent.resolve(), }) parser.read(config_path) config_items = ( - ('Path', 'root', 'string'), ('Path', 'file_root', 'string'), ('Path', 'config_dir', 'string'), ('Path', 'data_dir', 'string'), diff --git a/plinth/tests/data/etc/plinth/plinth.config b/plinth/tests/data/etc/plinth/plinth.config index 738da7ed9..8b21b06d6 100644 --- a/plinth/tests/data/etc/plinth/plinth.config +++ b/plinth/tests/data/etc/plinth/plinth.config @@ -1,6 +1,6 @@ [Path] # directory locations -file_root = %(root)s +file_root = %(parent_dir)s config_dir = %(file_root)s/data/etc/plinth data_dir = %(file_root)s/data/var/lib/plinth server_dir = /plinth diff --git a/plinth/tests/test_actions.py b/plinth/tests/test_actions.py index e922ed237..f3eb9bf9a 100644 --- a/plinth/tests/test_actions.py +++ b/plinth/tests/test_actions.py @@ -167,7 +167,7 @@ def test_action_path(monkeypatch): monkeypatch.setitem(os.environ, 'PYTHONPATH', '') plinth_path = run('test_path').strip() su_plinth_path = superuser_run('test_path').strip() - assert plinth_path.startswith(cfg.root) + assert plinth_path.startswith(cfg.file_root) assert plinth_path == su_plinth_path diff --git a/plinth/tests/test_cfg.py b/plinth/tests/test_cfg.py index bb710dc69..d4ea490dd 100644 --- a/plinth/tests/test_cfg.py +++ b/plinth/tests/test_cfg.py @@ -6,6 +6,7 @@ Test module for configuration module. import configparser import logging import os +import pathlib from unittest.mock import patch import pytest @@ -26,58 +27,54 @@ pytestmark = pytest.mark.usefixtures('load_cfg') def test_read_default_config_file(): """Verify that the default config file can be read correctly.""" - config_file, config_dir = cfg.get_develop_config_paths() + config_file = cfg.get_develop_config_path() # Read the plinth.config file directly - parser = configparser.ConfigParser(defaults={'root': config_dir}) + parser = configparser.ConfigParser( + defaults={'parent_dir': pathlib.Path(config_file).parent}) parser.read(config_file) # Read the plinth.config file via the cfg module - cfg.read_file(config_file, config_dir) + cfg.read_file(config_file) # Compare the two results compare_configurations(parser) -@patch('plinth.cfg.get_config_paths') -def test_read_primary_config_file(get_config_paths): +@patch('plinth.cfg.get_config_path') +def test_read_primary_config_file(get_config_path): """Verify that the primary config file is used by default.""" - expected_config_path = CONFIG_FILE_WITH_MISSING_OPTIONS - root_directory = 'x-default-root' - expected_root_directory = os.path.realpath(root_directory) - get_config_paths.return_value = (expected_config_path, root_directory) - + config_path = CONFIG_FILE_WITH_MISSING_OPTIONS + get_config_path.return_value = config_path cfg.read() - - assert cfg.config_files[-1] == expected_config_path - assert cfg.root == expected_root_directory + assert cfg.config_files[-1] == config_path -def test_read_fallback_config_file(): - """Verify that the correct fallback config file is used""" +def test_read_develop_config_file(): + """Verify that the correct develop config file is used.""" test_dir = os.path.dirname(os.path.realpath(__file__)) - fallback_root = os.path.realpath(os.path.join(test_dir, '..', '..')) - fallback_config_file = os.path.join(fallback_root, 'plinth.config') - config_path, root_directory = cfg.get_develop_config_paths() - cfg.read_file(config_path, root_directory) - assert cfg.config_files[-1] == fallback_config_file - assert cfg.root == fallback_root + develop_root = os.path.realpath(os.path.join(test_dir, '..', '..')) + develop_config_file = os.path.join(develop_root, 'plinth.config') + config_path = cfg.get_develop_config_path() + cfg.read_file(config_path) + assert cfg.config_files[-1] == develop_config_file + assert cfg.file_root == develop_root def test_read_missing_config_file(): """Verify that an exception is raised when there's no config file.""" - cfg.read_file('x-non-existant-file', 'x-root-directory') + cfg.read_file('x-non-existant-file') def test_read_config_file_with_missing_sections(): """Verify that missing configuration sections can be detected.""" - cfg.read_file(CONFIG_FILE_WITH_MISSING_SECTIONS, TEST_CONFIG_DIR) + cfg.read_file(CONFIG_FILE_WITH_MISSING_SECTIONS) assert cfg.box_name == 'FreedomBoxTestMissingSections' def test_read_config_file_with_missing_options(): """Verify that missing configuration options can be detected.""" - cfg.read_file(CONFIG_FILE_WITH_MISSING_OPTIONS, TEST_CONFIG_DIR) + cfg.read_file(CONFIG_FILE_WITH_MISSING_OPTIONS) assert cfg.box_name == 'FreedomBoxTestMissingOptions' @@ -86,7 +83,6 @@ def compare_configurations(parser): # Note that the count of items within each section includes the number # of default items (1, for 'root'). assert len(parser.items('Path')) == 9 - assert parser.get('Path', 'root') == cfg.root assert parser.get('Path', 'file_root') == cfg.file_root assert parser.get('Path', 'config_dir') == cfg.config_dir assert parser.get('Path', 'custom_static_dir') == cfg.custom_static_dir From 50ef5861d0558edbc881ad7a9d0071d8869d6749 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Jun 2020 22:21:22 -0700 Subject: [PATCH 59/90] cfg: Move /plinth.config to plinth/develop.config - Avoid a top level source code file. - Makes it clear that the configuration file is only meant for development purposes. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/cfg.py | 13 ++++++++----- plinth.config => plinth/develop.config | 2 +- plinth/tests/test_cfg.py | 3 ++- 3 files changed, 11 insertions(+), 7 deletions(-) rename plinth.config => plinth/develop.config (96%) diff --git a/plinth/cfg.py b/plinth/cfg.py index cc2538189..b2ad72b42 100644 --- a/plinth/cfg.py +++ b/plinth/cfg.py @@ -53,9 +53,8 @@ config_files = [] def get_develop_config_path(): """Return config path of current source folder for development mode.""" root_directory = os.path.dirname(os.path.realpath(__file__)) - root_directory = os.path.join(root_directory, '..') root_directory = os.path.realpath(root_directory) - config_path = os.path.join(root_directory, 'plinth.config') + config_path = os.path.join(root_directory, 'develop.config') return config_path @@ -79,9 +78,13 @@ def read_file(config_path): # Keep a note of configuration files read. config_files.append(config_path) - parser = configparser.ConfigParser(defaults={ - 'parent_dir': pathlib.Path(config_path).parent.resolve(), - }) + parser = configparser.ConfigParser( + defaults={ + 'parent_dir': + pathlib.Path(config_path).parent.resolve(), + 'parent_parent_dir': + pathlib.Path(config_path).parent.parent.resolve(), + }) parser.read(config_path) config_items = ( diff --git a/plinth.config b/plinth/develop.config similarity index 96% rename from plinth.config rename to plinth/develop.config index 8b21b06d6..2c0791032 100644 --- a/plinth.config +++ b/plinth/develop.config @@ -1,6 +1,6 @@ [Path] # directory locations -file_root = %(parent_dir)s +file_root = %(parent_parent_dir)s config_dir = %(file_root)s/data/etc/plinth data_dir = %(file_root)s/data/var/lib/plinth server_dir = /plinth diff --git a/plinth/tests/test_cfg.py b/plinth/tests/test_cfg.py index d4ea490dd..922a22354 100644 --- a/plinth/tests/test_cfg.py +++ b/plinth/tests/test_cfg.py @@ -54,7 +54,8 @@ def test_read_develop_config_file(): """Verify that the correct develop config file is used.""" test_dir = os.path.dirname(os.path.realpath(__file__)) develop_root = os.path.realpath(os.path.join(test_dir, '..', '..')) - develop_config_file = os.path.join(develop_root, 'plinth.config') + develop_config_file = os.path.join(develop_root, 'plinth', + 'develop.config') config_path = cfg.get_develop_config_path() cfg.read_file(config_path) assert cfg.config_files[-1] == develop_config_file From 6b1622bcecce4d290b901abc522d257d22c4c04e Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Jun 2020 22:43:51 -0700 Subject: [PATCH 60/90] cfg: Rename configuration file to freedombox.config Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- conftest.py | 2 +- plinth/__main__.py | 2 +- plinth/cfg.py | 2 +- .../freedombox.config} | 0 .../freedombox.config.with_missing_options} | 0 .../freedombox.config.with_missing_sections} | 0 plinth/tests/test_cfg.py | 22 +++++++++---------- 7 files changed, 14 insertions(+), 14 deletions(-) rename plinth/tests/data/{etc/plinth/plinth.config => configs/freedombox.config} (100%) rename plinth/tests/data/{plinth.config.with_missing_options => configs/freedombox.config.with_missing_options} (100%) rename plinth/tests/data/{plinth.config.with_missing_sections => configs/freedombox.config.with_missing_sections} (100%) diff --git a/conftest.py b/conftest.py index 89e0e455d..3f1eaf5ac 100644 --- a/conftest.py +++ b/conftest.py @@ -52,7 +52,7 @@ def fixture_load_cfg(): root_dir = pathlib.Path(__file__).resolve().parent test_data_dir = root_dir / 'plinth' / 'tests' / 'data' - cfg_file = test_data_dir / 'etc' / 'plinth' / 'plinth.config' + cfg_file = test_data_dir / 'configs' / 'freedombox.config' cfg.read_file(str(cfg_file)) yield cfg cfg.read_file(str(cfg_file)) diff --git a/plinth/__main__.py b/plinth/__main__.py index a6f2292cb..70f9e7580 100644 --- a/plinth/__main__.py +++ b/plinth/__main__.py @@ -105,7 +105,7 @@ def run_diagnostics_and_exit(): def adapt_config(arguments): - """Give commandline arguments precedence over plinth.config entries""" + """Give commandline arguments precedence over config entries""" for argument_name in precedence_commandline_arguments: argument_value = getattr(arguments, argument_name) if argument_value is not None: diff --git a/plinth/cfg.py b/plinth/cfg.py index b2ad72b42..75f8a7bc6 100644 --- a/plinth/cfg.py +++ b/plinth/cfg.py @@ -60,7 +60,7 @@ def get_develop_config_path(): def get_config_path(): """Get default config paths.""" - return '/etc/plinth/plinth.config' + return '/etc/freedombox/freedombox.config' def read(): diff --git a/plinth/tests/data/etc/plinth/plinth.config b/plinth/tests/data/configs/freedombox.config similarity index 100% rename from plinth/tests/data/etc/plinth/plinth.config rename to plinth/tests/data/configs/freedombox.config diff --git a/plinth/tests/data/plinth.config.with_missing_options b/plinth/tests/data/configs/freedombox.config.with_missing_options similarity index 100% rename from plinth/tests/data/plinth.config.with_missing_options rename to plinth/tests/data/configs/freedombox.config.with_missing_options diff --git a/plinth/tests/data/plinth.config.with_missing_sections b/plinth/tests/data/configs/freedombox.config.with_missing_sections similarity index 100% rename from plinth/tests/data/plinth.config.with_missing_sections rename to plinth/tests/data/configs/freedombox.config.with_missing_sections diff --git a/plinth/tests/test_cfg.py b/plinth/tests/test_cfg.py index 922a22354..f512df28a 100644 --- a/plinth/tests/test_cfg.py +++ b/plinth/tests/test_cfg.py @@ -14,11 +14,12 @@ import pytest from plinth import cfg TEST_CONFIG_DIR = \ - os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data') + os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'data', 'configs') CONFIG_FILE_WITH_MISSING_OPTIONS = \ - os.path.join(TEST_CONFIG_DIR, 'plinth.config.with_missing_options') + os.path.join(TEST_CONFIG_DIR, 'freedombox.config.with_missing_options') CONFIG_FILE_WITH_MISSING_SECTIONS = \ - os.path.join(TEST_CONFIG_DIR, 'plinth.config.with_missing_sections') + os.path.join(TEST_CONFIG_DIR, 'freedombox.config.with_missing_sections') logging.disable(logging.CRITICAL) @@ -29,12 +30,15 @@ def test_read_default_config_file(): """Verify that the default config file can be read correctly.""" config_file = cfg.get_develop_config_path() - # Read the plinth.config file directly + # Read the freedombox.config file directly parser = configparser.ConfigParser( - defaults={'parent_dir': pathlib.Path(config_file).parent}) + defaults={ + 'parent_dir': pathlib.Path(config_file).parent, + 'parent_parent_dir': pathlib.Path(config_file).parent.parent + }) parser.read(config_file) - # Read the plinth.config file via the cfg module + # Read the freedombox.config file via the cfg module cfg.read_file(config_file) # Compare the two results @@ -81,9 +85,6 @@ def test_read_config_file_with_missing_options(): def compare_configurations(parser): """Compare two sets of configuration values.""" - # Note that the count of items within each section includes the number - # of default items (1, for 'root'). - assert len(parser.items('Path')) == 9 assert parser.get('Path', 'file_root') == cfg.file_root assert parser.get('Path', 'config_dir') == cfg.config_dir assert parser.get('Path', 'custom_static_dir') == cfg.custom_static_dir @@ -92,7 +93,6 @@ def compare_configurations(parser): assert parser.get('Path', 'actions_dir') == cfg.actions_dir assert parser.get('Path', 'doc_dir') == cfg.doc_dir - assert len(parser.items('Network')) == 6 assert parser.get('Network', 'host') == cfg.host assert int(parser.get('Network', 'port')) == cfg.port assert parser.get('Network', 'secure_proxy_ssl_header') == \ @@ -103,5 +103,5 @@ def compare_configurations(parser): assert isinstance(cfg.use_x_forwarded_host, bool) assert parser.get('Network', 'use_x_forwarded_host') == \ str(cfg.use_x_forwarded_host) - assert len(parser.items('Misc')) == 2 + assert parser.get('Misc', 'box_name') == cfg.box_name From 823735729b19d8e08923ed04da84fa5d50a9682e Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Jun 2020 22:45:41 -0700 Subject: [PATCH 61/90] d/tests/control: Rename Plinth to FreedomBox in a comment Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- debian/tests/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/tests/control b/debian/tests/control index 4ff94d8f9..837bb7779 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -3,7 +3,7 @@ # checks that the following aspects are working okay: # - Python runtime # - Python library dependencies -# - Plinth configuration +# - FreedomBox configuration # - Ability to create and initialize database # - Module inititailzation for essential modules # From aaa306aef58b749c8dad76d7c061f11c361cff07 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Jun 2020 23:31:29 -0700 Subject: [PATCH 62/90] cfg: Read configuration from .d files and multiple locations - Read configuration files from three different locations. /usr/share/freedombox/freedombox.config, /etc/plinth/plinth.config and /etc/freedombox/freedombox.conf. Later listed has higher priority. - Provide backward compatibility for /etc/plinth/plinth.config files. With lower priority than /etc/freedombox but higher priority than /usr/share/. - Read sorted files from config.d directories with the same suffix as original configuration file. Parse them by priority. This allows administrator/programs to drop in configuration bits without worry about editing files. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/cfg.py | 26 ++++++++++++++++--- .../freedombox.config.d/01_first.config | 2 ++ .../freedombox.config.d/02_second.config | 2 ++ .../configs/freedombox.config.d/03_third.foo | 2 ++ plinth/tests/test_cfg.py | 21 ++++++++++++--- 5 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 plinth/tests/data/configs/freedombox.config.d/01_first.config create mode 100644 plinth/tests/data/configs/freedombox.config.d/02_second.config create mode 100644 plinth/tests/data/configs/freedombox.config.d/03_third.foo diff --git a/plinth/cfg.py b/plinth/cfg.py index 75f8a7bc6..d03d2cadd 100644 --- a/plinth/cfg.py +++ b/plinth/cfg.py @@ -50,6 +50,19 @@ develop = False config_files = [] +def expand_to_dot_d_paths(file_paths): + """Expand a list of file paths to include file.d/* also.""" + final_list = [] + for file_path in file_paths: + final_list.append(str(file_path)) + path = pathlib.Path(file_path) + path_d = path.with_suffix(path.suffix + '.d') + for dot_d_file in sorted(path_d.glob('*' + path.suffix)): + final_list.append(str(dot_d_file)) + + return final_list + + def get_develop_config_path(): """Return config path of current source folder for development mode.""" root_directory = os.path.dirname(os.path.realpath(__file__)) @@ -58,15 +71,20 @@ def get_develop_config_path(): return config_path -def get_config_path(): +def get_config_paths(): """Get default config paths.""" - return '/etc/freedombox/freedombox.config' + return [ + '/usr/share/freedombox/freedombox.config', + '/etc/plinth/plinth.config', + '/etc/freedombox/freedombox.config', + ] def read(): """Read all configuration files.""" - config_path = get_config_path() - read_file(config_path) + config_paths = get_config_paths() + for config_path in expand_to_dot_d_paths(config_paths): + read_file(config_path) def read_file(config_path): diff --git a/plinth/tests/data/configs/freedombox.config.d/01_first.config b/plinth/tests/data/configs/freedombox.config.d/01_first.config new file mode 100644 index 000000000..df319c2a9 --- /dev/null +++ b/plinth/tests/data/configs/freedombox.config.d/01_first.config @@ -0,0 +1,2 @@ +[Misc] +box_name = FreedomBox01 diff --git a/plinth/tests/data/configs/freedombox.config.d/02_second.config b/plinth/tests/data/configs/freedombox.config.d/02_second.config new file mode 100644 index 000000000..af3a01772 --- /dev/null +++ b/plinth/tests/data/configs/freedombox.config.d/02_second.config @@ -0,0 +1,2 @@ +[Misc] +box_name = FreedomBox02 diff --git a/plinth/tests/data/configs/freedombox.config.d/03_third.foo b/plinth/tests/data/configs/freedombox.config.d/03_third.foo new file mode 100644 index 000000000..ff7355ae2 --- /dev/null +++ b/plinth/tests/data/configs/freedombox.config.d/03_third.foo @@ -0,0 +1,2 @@ +[Misc] +box_name = FreedomBox03 diff --git a/plinth/tests/test_cfg.py b/plinth/tests/test_cfg.py index f512df28a..3c087c704 100644 --- a/plinth/tests/test_cfg.py +++ b/plinth/tests/test_cfg.py @@ -45,15 +45,30 @@ def test_read_default_config_file(): compare_configurations(parser) -@patch('plinth.cfg.get_config_path') -def test_read_primary_config_file(get_config_path): +@patch('plinth.cfg.get_config_paths') +def test_read_primary_config_file(get_config_paths): """Verify that the primary config file is used by default.""" config_path = CONFIG_FILE_WITH_MISSING_OPTIONS - get_config_path.return_value = config_path + get_config_paths.return_value = [config_path] cfg.read() assert cfg.config_files[-1] == config_path +@patch('plinth.cfg.get_config_paths') +def test_read_dot_d_config_files(get_config_paths): + """Verify that the configuration is read from .d directories.""" + root_dir = pathlib.Path(__file__).resolve().parent + config_path = root_dir / 'data' / 'configs' / 'freedombox.config' + config_path_d = config_path.with_suffix(config_path.suffix + '.d') + + get_config_paths.return_value = [str(config_path)] + cfg.read() + assert cfg.config_files[-3] == str(config_path) + assert cfg.config_files[-2] == str(config_path_d / '01_first.config') + assert cfg.config_files[-1] == str(config_path_d / '02_second.config') + assert cfg.box_name == 'FreedomBox02' + + def test_read_develop_config_file(): """Verify that the correct develop config file is used.""" test_dir = os.path.dirname(os.path.realpath(__file__)) From 4ea2e755dbc31e9d8f60bef0bf4877b9b319849c Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Jun 2020 23:42:00 -0700 Subject: [PATCH 63/90] frontpage: Load shortcuts from .d directories too Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/frontpage.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plinth/frontpage.py b/plinth/frontpage.py index 5e63bff1c..ae1f73f46 100644 --- a/plinth/frontpage.py +++ b/plinth/frontpage.py @@ -160,11 +160,15 @@ def _extract_web_app_url(custom_shortcut): def get_custom_shortcuts(): """Return a merged dictionary of all custom shortcuts.""" - shortcuts_dirs = [cfg.config_dir, cfg.data_dir, cfg.file_root] + dirs = [cfg.config_dir, cfg.data_dir, cfg.file_root] + file_paths = [ + pathlib.Path(dir_) / 'custom-shortcuts.json' for dir_ in dirs + ] + file_paths = cfg.expand_to_dot_d_paths(file_paths) shortcuts = {'shortcuts': []} - for shortcuts_dir in shortcuts_dirs: - file_path = pathlib.Path(shortcuts_dir) / 'custom-shortcuts.json' + for file_path in file_paths: + file_path = pathlib.Path(file_path) if not file_path.is_file() or not file_path.stat().st_size: continue From 3dfceda78587b3fd9bb0d11991c9e22f4f76895b Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 24 Jun 2020 00:30:40 -0700 Subject: [PATCH 64/90] frontpage: Read from .d files too Read from following paths: /etc/freedombox/custom-shortcuts.json /etc/freedombox/custom-shortcuts.json.d/*.json /etc/plinth/custom-shortcuts.json /etc/plinth/custom-shortcuts.json.d/*.json /var/lib/freedombox/custom-shortcuts.json /var/lib/freedombox/custom-shortcuts.json.d/*.json /usr/share/freedombox/custom-shortcuts.json /usr/share/freedombox/custom-shortcuts.json.d/*.json Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/frontpage.py | 19 ++++-- plinth/tests/conftest.py | 41 ++++-------- plinth/tests/data/shortcuts/blank.json | 0 plinth/tests/data/shortcuts/dotd.json | 13 ++++ .../data/shortcuts/dotd.json.d/01_extra.json | 13 ++++ plinth/tests/data/shortcuts/empty.json | 1 + plinth/tests/data/shortcuts/nextcloud.json | 13 ++++ plinth/tests/test_custom_shortcuts.py | 65 +++++++++---------- plinth/tests/test_frontpage.py | 4 +- 9 files changed, 96 insertions(+), 73 deletions(-) create mode 100644 plinth/tests/data/shortcuts/blank.json create mode 100644 plinth/tests/data/shortcuts/dotd.json create mode 100644 plinth/tests/data/shortcuts/dotd.json.d/01_extra.json create mode 100644 plinth/tests/data/shortcuts/empty.json create mode 100644 plinth/tests/data/shortcuts/nextcloud.json diff --git a/plinth/frontpage.py b/plinth/frontpage.py index ae1f73f46..49d1b02dd 100644 --- a/plinth/frontpage.py +++ b/plinth/frontpage.py @@ -158,16 +158,21 @@ def _extract_web_app_url(custom_shortcut): return None +def get_custom_shortcuts_paths(): + """Return the list of custom shortcut file paths.""" + file_paths = [ + '/etc/freedombox/custom-shortcuts.json', + '/etc/plinth/custom-shortcuts.json', + '/var/lib/freedombox/custom-shortcuts.json', + '/usr/share/freedombox/custom-shortcuts.json', + ] + return cfg.expand_to_dot_d_paths(file_paths) + + def get_custom_shortcuts(): """Return a merged dictionary of all custom shortcuts.""" - dirs = [cfg.config_dir, cfg.data_dir, cfg.file_root] - file_paths = [ - pathlib.Path(dir_) / 'custom-shortcuts.json' for dir_ in dirs - ] - file_paths = cfg.expand_to_dot_d_paths(file_paths) - shortcuts = {'shortcuts': []} - for file_path in file_paths: + for file_path in get_custom_shortcuts_paths(): file_path = pathlib.Path(file_path) if not file_path.is_file() or not file_path.stat().st_size: continue diff --git a/plinth/tests/conftest.py b/plinth/tests/conftest.py index 2afad9b4a..ee2bfb5e5 100644 --- a/plinth/tests/conftest.py +++ b/plinth/tests/conftest.py @@ -3,40 +3,21 @@ pytest configuration for all tests in the plinth/tests/ directory. """ -import json +import pathlib +from unittest.mock import patch import pytest -NEXTCLOUD_SHORTCUT = { - 'name': - 'NextCloud', - 'short_description': - 'File Hosting Service', - 'description': [ - 'Nextcloud is a suite of client-server software for creating ' - 'and using file hosting services.' - ], - 'icon_url': - '/plinth/custom/static/themes/default/icons/nextcloud.png', - 'clients': [{ - 'name': 'nextcloud', - 'platforms': [{ - 'type': 'web', - 'url': '/nextcloud' - }] - }] -} +from plinth import cfg -@pytest.fixture(name='custom_shortcuts_file') -def fixture_custom_shortcuts_file(load_cfg, tmp_path): - """Fixture to set path for a custom shortcuts file.""" - load_cfg.config_dir = str(tmp_path) - return tmp_path / 'custom-shortcuts.json' +@pytest.fixture(name='shortcuts_file') +def fixture_shortcuts_file(): + with patch('plinth.frontpage.get_custom_shortcuts_paths') as func: + def setter(file_name): + path = pathlib.Path(__file__).parent / 'data' / 'shortcuts' + path /= file_name + func.return_value = cfg.expand_to_dot_d_paths([path]) -@pytest.fixture(name='nextcloud_shortcut') -def fixture_nextcloud_shortcut(custom_shortcuts_file): - """Create a custom_shortcuts file with NextCloud shortcut.""" - shortcuts = {'shortcuts': [NEXTCLOUD_SHORTCUT]} - custom_shortcuts_file.write_text(json.dumps(shortcuts)) + yield setter diff --git a/plinth/tests/data/shortcuts/blank.json b/plinth/tests/data/shortcuts/blank.json new file mode 100644 index 000000000..e69de29bb diff --git a/plinth/tests/data/shortcuts/dotd.json b/plinth/tests/data/shortcuts/dotd.json new file mode 100644 index 000000000..8bbe19a17 --- /dev/null +++ b/plinth/tests/data/shortcuts/dotd.json @@ -0,0 +1,13 @@ +{"shortcuts": [{ + "name": "NextCloud", + "short_description": "File Hosting Service", + "description": [ "Nextcloud is a suite of client-server software for creating and using file hosting services." ], + "icon_url": "/plinth/custom/static/themes/default/icons/nextcloud.png", + "clients": [{ + "name": "nextcloud", + "platforms": [{ + "type": "web", + "url": "/nextcloud" + }] + }] +}]} diff --git a/plinth/tests/data/shortcuts/dotd.json.d/01_extra.json b/plinth/tests/data/shortcuts/dotd.json.d/01_extra.json new file mode 100644 index 000000000..a18eb4686 --- /dev/null +++ b/plinth/tests/data/shortcuts/dotd.json.d/01_extra.json @@ -0,0 +1,13 @@ +{"shortcuts": [{ + "name": "NextCloud2", + "short_description": "File Hosting Service", + "description": [ "Nextcloud is a suite of client-server software for creating and using file hosting services." ], + "icon_url": "/plinth/custom/static/themes/default/icons/nextcloud.png", + "clients": [{ + "name": "nextcloud", + "platforms": [{ + "type": "web", + "url": "/nextcloud" + }] + }] +}]} diff --git a/plinth/tests/data/shortcuts/empty.json b/plinth/tests/data/shortcuts/empty.json new file mode 100644 index 000000000..9f4656aed --- /dev/null +++ b/plinth/tests/data/shortcuts/empty.json @@ -0,0 +1 @@ +{'shortcuts': []} diff --git a/plinth/tests/data/shortcuts/nextcloud.json b/plinth/tests/data/shortcuts/nextcloud.json new file mode 100644 index 000000000..8bbe19a17 --- /dev/null +++ b/plinth/tests/data/shortcuts/nextcloud.json @@ -0,0 +1,13 @@ +{"shortcuts": [{ + "name": "NextCloud", + "short_description": "File Hosting Service", + "description": [ "Nextcloud is a suite of client-server software for creating and using file hosting services." ], + "icon_url": "/plinth/custom/static/themes/default/icons/nextcloud.png", + "clients": [{ + "name": "nextcloud", + "platforms": [{ + "type": "web", + "url": "/nextcloud" + }] + }] +}]} diff --git a/plinth/tests/test_custom_shortcuts.py b/plinth/tests/test_custom_shortcuts.py index 57a1b7505..cac2dc42c 100644 --- a/plinth/tests/test_custom_shortcuts.py +++ b/plinth/tests/test_custom_shortcuts.py @@ -4,62 +4,59 @@ Test module for custom shortcuts. """ import json - -import pytest +import pathlib from plinth.modules.api.views import get_shortcuts_as_json -from .conftest import NEXTCLOUD_SHORTCUT - -@pytest.fixture(name='no_custom_shortcuts_file') -def fixture_no_custom_shortcuts_file(custom_shortcuts_file): - """Delete the custom_shortcuts file.""" - if custom_shortcuts_file.exists(): - custom_shortcuts_file.unlink() - - -@pytest.fixture(name='blank_custom_shortcuts_file') -def fixture_blank_custom_shortcuts_file(custom_shortcuts_file): - """Create a blank shortcuts file.""" - custom_shortcuts_file.write_text('') - - -@pytest.fixture(name='empty_custom_shortcuts') -def fixture_empty_custom_shortcuts(custom_shortcuts_file): - """Create a custom_shortcuts file with an empty list of shortcuts.""" - custom_shortcuts_file.write_text(json.dumps({'shortcuts': []})) - - -@pytest.mark.usefixtures('no_custom_shortcuts_file') -def test_shortcuts_api_with_no_custom_shortcuts_file(): +def test_non_existent_custom_shortcuts_file(shortcuts_file): + """Test loading a non-existent shortcuts file.""" + shortcuts_file('x-non-existant.json') get_shortcuts_as_json() -@pytest.mark.usefixtures('blank_custom_shortcuts_file') -def test_shortcuts_api_with_blank_custom_shortcuts_file(): +def test_blank_custom_shortcuts_file(shortcuts_file): + """Test loading a shortcuts file that is blank.""" + shortcuts_file('blank.json') get_shortcuts_as_json() -@pytest.mark.usefixtures('empty_custom_shortcuts') -def test_shortcuts_api_with_empty_custom_shortcuts_list(): +def test_empty_custom_shortcuts_list(shortcuts_file): + """Test loading a shortcuts file that has zero shortcuts.""" + shortcuts_file('empty.json') get_shortcuts_as_json() -@pytest.mark.usefixtures('nextcloud_shortcut') -def test_shortcuts_api_with_custom_nextcloud_shortcut(): +def test_dotd_shortcuts_files(shortcuts_file): + """Test loading a shortcuts file that has more files in .d directory.""" + shortcuts_file('dotd.json') + shortcuts = get_shortcuts_as_json() + assert len(shortcuts['shortcuts']) >= 2 + assert any(shortcut['name'] == 'NextCloud' + for shortcut in shortcuts['shortcuts']) + assert any(shortcut['name'] == 'NextCloud2' + for shortcut in shortcuts['shortcuts']) + + +def test_custom_nextcloud_shortcut(shortcuts_file): + """Test loading a shortcuts file that has nextcloud shortcut.""" + shortcuts_file('nextcloud.json') shortcuts = get_shortcuts_as_json() assert len(shortcuts['shortcuts']) >= 1 assert any(shortcut['name'] == 'NextCloud' for shortcut in shortcuts['shortcuts']) -@pytest.mark.usefixtures('nextcloud_shortcut') -def test_retrieved_custom_shortcut_from_api_is_correct(): +def test_retrieved_custom_shortcut(shortcuts_file): + """Test the value of loaded nextcloud shortcut.""" + shortcuts_file('nextcloud.json') shortcuts = get_shortcuts_as_json() shortcut = [ current_shortcut for current_shortcut in shortcuts['shortcuts'] if current_shortcut['name'] == 'NextCloud' ] assert shortcut - assert shortcut[0] == NEXTCLOUD_SHORTCUT + + path = pathlib.Path(__file__).parent / 'data' / 'shortcuts' + path /= 'nextcloud.json' + assert shortcut[0] == json.loads(path.read_bytes())['shortcuts'][0] diff --git a/plinth/tests/test_frontpage.py b/plinth/tests/test_frontpage.py index 86cad2072..95f16135d 100644 --- a/plinth/tests/test_frontpage.py +++ b/plinth/tests/test_frontpage.py @@ -138,7 +138,7 @@ def test_shortcut_list_with_username(superuser_run, common_shortcuts): assert return_list == [cuts[0], cuts[3], cut] -@pytest.mark.usefixtures('nextcloud_shortcut') -def test_add_custom_shortcuts(): +def test_add_custom_shortcuts(shortcuts_file): """Test that adding custom shortcuts succeeds.""" + shortcuts_file('nextcloud.json') add_custom_shortcuts() From a33160d6a47d666f9868c25d884063ba4410c17e Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 24 Jun 2020 00:50:55 -0700 Subject: [PATCH 65/90] cfg: Remove redundant data in develop.config Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/develop.config | 28 ---------------------------- plinth/tests/test_cfg.py | 7 ++++--- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/plinth/develop.config b/plinth/develop.config index 2c0791032..05157b757 100644 --- a/plinth/develop.config +++ b/plinth/develop.config @@ -1,36 +1,8 @@ [Path] -# directory locations file_root = %(parent_parent_dir)s config_dir = %(file_root)s/data/etc/plinth data_dir = %(file_root)s/data/var/lib/plinth -server_dir = /plinth actions_dir = %(file_root)s/actions doc_dir = %(file_root)s/doc custom_static_dir = %(file_root)s/data/var/www/plinth/custom/static - -# file locations store_file = %(data_dir)s/plinth.sqlite3 - -[Network] -host = 127.0.0.1 -port = 8000 - -# Enable the following only if Plinth is behind a proxy server. The -# proxy server should properly clean and the following HTTP headers: -# X-Forwarded-For -# X-Forwarded-Host -# X-Forwarded-Proto -# If you enable these unnecessarily, this will lead to serious security -# problems. For more information, see -# https://docs.djangoproject.com/en/1.7/ref/settings/ -# -# These are enabled by default in Plinth because the default -# configuration allows only connections from localhost -# -# Leave the values blank to disable -use_x_forwarded_for = True -use_x_forwarded_host = True -secure_proxy_ssl_header = HTTP_X_FORWARDED_PROTO - -[Misc] -box_name = FreedomBox diff --git a/plinth/tests/test_cfg.py b/plinth/tests/test_cfg.py index 3c087c704..6c5d81223 100644 --- a/plinth/tests/test_cfg.py +++ b/plinth/tests/test_cfg.py @@ -28,13 +28,14 @@ pytestmark = pytest.mark.usefixtures('load_cfg') def test_read_default_config_file(): """Verify that the default config file can be read correctly.""" - config_file = cfg.get_develop_config_path() + path = pathlib.Path(__file__).resolve().parent + config_file = path / 'data' / 'configs' / 'freedombox.config' # Read the freedombox.config file directly parser = configparser.ConfigParser( defaults={ - 'parent_dir': pathlib.Path(config_file).parent, - 'parent_parent_dir': pathlib.Path(config_file).parent.parent + 'parent_dir': config_file.parent, + 'parent_parent_dir': config_file.parent.parent }) parser.read(config_file) From a2281aaf07ff85fbf509108234377d8a2e86cb46 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 24 Jun 2020 00:52:32 -0700 Subject: [PATCH 66/90] cfg: Remove comments in test data The file is not meant for human reading. The comments are already part of the code. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/tests/data/configs/freedombox.config | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/plinth/tests/data/configs/freedombox.config b/plinth/tests/data/configs/freedombox.config index 8b21b06d6..db6fc00ff 100644 --- a/plinth/tests/data/configs/freedombox.config +++ b/plinth/tests/data/configs/freedombox.config @@ -1,5 +1,4 @@ [Path] -# directory locations file_root = %(parent_dir)s config_dir = %(file_root)s/data/etc/plinth data_dir = %(file_root)s/data/var/lib/plinth @@ -7,27 +6,11 @@ server_dir = /plinth actions_dir = %(file_root)s/actions doc_dir = %(file_root)s/doc custom_static_dir = %(file_root)s/data/var/www/plinth/custom/static - -# file locations store_file = %(data_dir)s/plinth.sqlite3 [Network] host = 127.0.0.1 port = 8000 - -# Enable the following only if Plinth is behind a proxy server. The -# proxy server should properly clean and the following HTTP headers: -# X-Forwarded-For -# X-Forwarded-Host -# X-Forwarded-Proto -# If you enable these unnecessarily, this will lead to serious security -# problems. For more information, see -# https://docs.djangoproject.com/en/1.7/ref/settings/ -# -# These are enabled by default in Plinth because the default -# configuration allows only connections from localhost -# -# Leave the values blank to disable use_x_forwarded_for = True use_x_forwarded_host = True secure_proxy_ssl_header = HTTP_X_FORWARDED_PROTO From 91c4d6742ea8bfd75a767ede95c1f0bb76cc8753 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 24 Jun 2020 00:54:17 -0700 Subject: [PATCH 67/90] cfg: In develop mode, use /var/lib for DB and sessions - Only effects develop mode. - To primarily avoid writing to the source code directory. Multiple containers or VMs using the source folder won't fight with the database file (the overlay file system plan is not working out well for containers #1873). - In the earlier days, we used to allow running from source code directory without even doing ./setup.py install. Currently it is not possible anyway. We pretty much install freedombox package before running from source directory. - If the build process itself learns not to write to source directory, then containers/VMs won't have to write to source directory at all. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- container | 22 +++++++++------------- plinth/develop.config | 3 --- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/container b/container index 5dab9a619..2829b819d 100755 --- a/container +++ b/container @@ -47,16 +47,15 @@ Shared folder: Using systemd-nspawn, the project directory is mounted as /freedombox inside the container. The project directory is determined as directory in which this script resides. The project folder from the container point of view will be read-only. Container should be able to write various -files such as build files, FreedomBox sqlite3 database and session files into -the /freedombox folder. To enable writing, an additional read-write folder is -overlayed onto /freedombox folder in the container. This directory can't be -created under the project folder and is created instead in -$XDG_DATA_HOME/freedombox-container/overlay/$DISTRIBUTION. If XDG_DATA_HOME is -not set, it is assumed to be $HOME/.local/shared/. Whenever data is written -into /freedombox directory inside the container, this directory on the host -receives the changes. See documentation for Overlay filesystem for further -details. When container is destroyed, this overlay folder is destroyed to -ensure clean state after bringing up the container again. +files such as build files into the /freedombox folder. To enable writing, an +additional read-write folder is overlayed onto /freedombox folder in the +container. This directory can't be created under the project folder and is +created instead in $XDG_DATA_HOME/freedombox-container/overlay/$DISTRIBUTION. +If XDG_DATA_HOME is not set, it is assumed to be $HOME/.local/shared/. Whenever +data is written into /freedombox directory inside the container, this directory +on the host receives the changes. See documentation for Overlay filesystem for +further details. When container is destroyed, this overlay folder is destroyed +to ensure clean state after bringing up the container again. Users: PrivateUsers configuration flag for systemd-nspawn is currently off. This means that each user's UID on the host is also the same UID in the @@ -138,9 +137,6 @@ KEY_SERVER = 'keys.gnupg.net' PROVISION_SCRIPT = ''' set -x -# Remove FreedomBox database lingering in source directory to start fresh -sudo rm -f /freedombox/data/var/lib/plinth/plinth.sqlite3 - cd /freedombox/ sudo apt-get update diff --git a/plinth/develop.config b/plinth/develop.config index 05157b757..e2a61f89a 100644 --- a/plinth/develop.config +++ b/plinth/develop.config @@ -1,8 +1,5 @@ [Path] file_root = %(parent_parent_dir)s config_dir = %(file_root)s/data/etc/plinth -data_dir = %(file_root)s/data/var/lib/plinth actions_dir = %(file_root)s/actions doc_dir = %(file_root)s/doc -custom_static_dir = %(file_root)s/data/var/www/plinth/custom/static -store_file = %(data_dir)s/plinth.sqlite3 From 28fe8c8c3e6f8b155187d05f9ebdbc31b93bf372 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 24 Jun 2020 14:35:45 -0700 Subject: [PATCH 68/90] web_framework: Split initialization into two parts A simple Django configuration does not need to create the database whereas DB migration requires creating the database. In some operations such as listing dependencies, we can skip running the second part and so writing to database will no longer be necessary during such operations. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/__main__.py | 1 + plinth/web_framework.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/plinth/__main__.py b/plinth/__main__.py index 70f9e7580..84978bf0f 100644 --- a/plinth/__main__.py +++ b/plinth/__main__.py @@ -132,6 +132,7 @@ def main(): log.init() web_framework.init() + web_framework.post_init() logger.info('FreedomBox Service (Plinth) version - %s', __version__) for config_file in cfg.config_files: diff --git a/plinth/web_framework.py b/plinth/web_framework.py index b5f68e80d..6aa25cd28 100644 --- a/plinth/web_framework.py +++ b/plinth/web_framework.py @@ -53,6 +53,9 @@ def init(): logger.debug('Configured Django with applications - %s', settings.INSTALLED_APPS) + +def post_init(): + """Perform operations after completing init of other modules.""" logger.debug('Creating or adding new tables to data file') django.core.management.call_command('migrate', '--fake-initial', interactive=False, verbosity=0) From e5177289dc839e1b857032f6e832e1629473d43d Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 24 Jun 2020 18:08:07 -0700 Subject: [PATCH 69/90] web_framework: Don't create Django secret key when listing depends This allows --list-dependencies to run without having to write to disk. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/web_framework.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plinth/web_framework.py b/plinth/web_framework.py index 6aa25cd28..481e2bbf5 100644 --- a/plinth/web_framework.py +++ b/plinth/web_framework.py @@ -20,7 +20,7 @@ from . import cfg, glib, log, module_loader, settings logger = logging.getLogger(__name__) -def init(): +def init(read_only=False): """Setup Django configuration in the absence of .settings file""" if cfg.secure_proxy_ssl_header: settings.SECURE_PROXY_SSL_HEADER = (cfg.secure_proxy_ssl_header, @@ -36,7 +36,7 @@ def init(): settings.LANGUAGES = get_languages() settings.LOGGING = log.get_configuration() settings.MESSAGE_TAGS = {message_constants.ERROR: 'danger'} - settings.SECRET_KEY = _get_secret_key() + settings.SECRET_KEY = _get_secret_key(read_only) settings.SESSION_FILE_PATH = os.path.join(cfg.data_dir, 'sessions') settings.STATIC_URL = '/'.join([cfg.server_dir, 'static/']).replace('//', '/') @@ -65,7 +65,7 @@ def post_init(): glib.schedule(24 * 3600, _cleanup_expired_sessions, in_thread=True) -def _get_secret_key(): +def _get_secret_key(read_only=False): """Retrieve or create a new Django secret key.""" secret_key_file = pathlib.Path(cfg.data_dir) / 'django-secret.key' if secret_key_file.exists(): @@ -73,6 +73,9 @@ def _get_secret_key(): if len(secret_key) >= 128: return secret_key + if read_only: + return '' + secret_key = _generate_secret_key() # File should be created with permission 0o700 old_umask = os.umask(0o077) From a145742ebc83b1f236aad22c5cb269d8e59a5ffd Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 24 Jun 2020 14:39:25 -0700 Subject: [PATCH 70/90] log: Allow setting the default log level before log configuration Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/log.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plinth/log.py b/plinth/log.py index f6392168a..0d0a316c4 100644 --- a/plinth/log.py +++ b/plinth/log.py @@ -11,6 +11,8 @@ import cherrypy from . import cfg +default_level = None + class ColoredFormatter(logging.Formatter): """Print parts of log message in color.""" @@ -105,7 +107,7 @@ def get_configuration(): }, 'root': { 'handlers': ['console'], - 'level': 'DEBUG' if cfg.develop else 'INFO' + 'level': default_level or ('DEBUG' if cfg.develop else 'INFO') }, 'loggers': { 'django.db.backends': { From 5d3c010b2ed29a30769d2ffa51a27dd446d9cfc9 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 24 Jun 2020 14:41:24 -0700 Subject: [PATCH 71/90] main: List dependencies without writing to disk - Don't run the second phase of web framework initialization. This avoids writing to the DB file. - Set log level to ERROR so that no messages get printed even to stderr while listing dependencies. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/__main__.py | 8 +++++--- plinth/setup.py | 5 ++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/plinth/__main__.py b/plinth/__main__.py index 84978bf0f..c8fdbce75 100644 --- a/plinth/__main__.py +++ b/plinth/__main__.py @@ -129,6 +129,11 @@ def main(): adapt_config(arguments) + if arguments.list_dependencies is not False: + log.default_level = 'ERROR' + web_framework.init(read_only=True) + list_dependencies(arguments.list_dependencies) + log.init() web_framework.init() @@ -152,9 +157,6 @@ def main(): if arguments.setup_no_install is not False: run_setup_and_exit(arguments.setup_no_install, allow_install=False) - if arguments.list_dependencies is not False: - list_dependencies(arguments.list_dependencies) - if arguments.list_modules is not False: list_modules(arguments.list_modules) diff --git a/plinth/setup.py b/plinth/setup.py index e96486f52..7809215c0 100644 --- a/plinth/setup.py +++ b/plinth/setup.py @@ -3,6 +3,7 @@ Utilities for performing application setup operations. """ +import importlib import logging import os import threading @@ -207,7 +208,9 @@ def setup_modules(module_list=None, essential=False, allow_install=True): def list_dependencies(module_list=None, essential=False): """Print list of packages required by selected or essential modules.""" - for module_name, module in plinth.module_loader.loaded_modules.items(): + for module_import_path in plinth.module_loader.get_modules_to_load(): + module_name = module_import_path.split('.')[-1] + module = importlib.import_module(module_import_path) if essential and not _is_module_essential(module): continue From bf7a9b8c7a49f9d17e37ba9e5ce889a94d1a70c1 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 24 Jun 2020 14:51:36 -0700 Subject: [PATCH 72/90] d/rules: vagrant: INSTALL.md: Fix installing dependencies - Using ./run --develop ensures that the last list of dependencies are picked up from current source directory instead of list of dependencies from system configuration. - Using sudo -u plinth ensures that even if any temporary files are created, they belong to the plinth user instead of root user. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- INSTALL.md | 2 +- Vagrantfile | 2 +- debian/rules | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index aa2b89fb7..771354100 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -16,7 +16,7 @@ FreedomBox [Manual](https://wiki.debian.org/FreedomBox/Manual/)'s ``` ``` - $ sudo apt install -y $(./run --list-dependencies) + $ sudo apt install -y $(./run --develop --list-dependencies) ``` Install additional dependencies by picking the list from debian/control file diff --git a/Vagrantfile b/Vagrantfile index bda895ea4..a6b41488d 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -32,7 +32,7 @@ Vagrant.configure(2) do |config| apt-get update # In case new dependencies conflict with old dependencies apt-mark hold freedombox - DEBIAN_FRONTEND=noninteractive apt-get install --no-upgrade -y $(plinth --list-dependencies) + DEBIAN_FRONTEND=noninteractive apt-get install --no-upgrade -y $(sudo -u plinth ./run --develop --list-dependencies) apt-mark unhold freedombox # Install ncurses-term DEBIAN_FRONTEND=noninteractive apt-get install -y ncurses-term diff --git a/debian/rules b/debian/rules index b0a3a6f76..924d3b366 100755 --- a/debian/rules +++ b/debian/rules @@ -8,7 +8,7 @@ export PYBUILD_DESTDIR=debian/tmp override_dh_auto_install-indep: dh_auto_install - ./run --list-dependencies 2> /dev/null | sort | tr '\n' ', ' | \ + ./run --develop --list-dependencies | sort | tr '\n' ', ' | \ sed -e 's/^/freedombox:Depends=/' >> debian/freedombox.substvars override_dh_auto_test: From 384c34bb3fb52a0157bb6277f9f59740df4a8cd0 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 24 Jun 2020 17:50:56 -0700 Subject: [PATCH 73/90] *: Drop files paths in data/var data/var/log and data/var/run were not being used for a while. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- .gitignore | 6 ------ data/var/lib/plinth/.gitkeep | 0 data/var/lib/plinth/sessions/.gitkeep | 0 3 files changed, 6 deletions(-) delete mode 100644 data/var/lib/plinth/.gitkeep delete mode 100644 data/var/lib/plinth/sessions/.gitkeep diff --git a/.gitignore b/.gitignore index f2e8380a2..8ed9a9b69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,6 @@ *.pyc *.py.bak *.tiny.css -data/var/log/plinth/*.log -data/var/lib/plinth/django-secret.key -data/var/lib/plinth/*.sqlite3 -data/var/lib/plinth/sessions/* -data/var/lib/plinth/.ssh/ -data/var/run/*.pid doc/manual/*/*.pdf doc/manual/*/*.html doc/manual/*/*.xml diff --git a/data/var/lib/plinth/.gitkeep b/data/var/lib/plinth/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/data/var/lib/plinth/sessions/.gitkeep b/data/var/lib/plinth/sessions/.gitkeep deleted file mode 100644 index e69de29bb..000000000 From 60bbdfabf7d78c99d416513891105facc99b367d Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Jun 2020 22:45:22 -0700 Subject: [PATCH 74/90] doc: Update manual page with configuration file changes Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- doc/plinth.xml | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/doc/plinth.xml b/doc/plinth.xml index 191ffaa6c..ee22c7757 100644 --- a/doc/plinth.xml +++ b/doc/plinth.xml @@ -71,17 +71,10 @@ - This the URL fragment under which Plinth will provide its - services. By default the value from - plinth.config is used. Plinth is - shipped with a value of /plinth in - /etc/plinth/plinth.config. This - means that Plinth will be available as - http://localhost:8000/plinth by default. - When /etc/plinth/plinth.config is not - available, plinth.config from the current - working directory is used. - + This the URL fragment under which Plinth will provide its services. + Plinth is shipped with a default value of + /plinth. This means that Plinth will be + available as http://localhost:8000/plinth by default. @@ -89,9 +82,9 @@ - Enable development mode. Use plinth.config and the actions_dir - of the current working directory. Enables extra debug messages, - enable Django debug mode for detailed error pages and and turn off + Enable development mode. Use develop.config and action scripts + from the current working directory. Enables extra debug messages, + enable Django debug mode for detailed error pages and turn off Django security features. Monitor source files for changes and restart Plinth on modifications. Die if there is an error during module initialization. @@ -165,12 +158,15 @@ Configuration - Plinth reads various configuration options from the file - /etc/plinth/plinth.config. If this file is - not present, then it reads configuration file - ./plinth.config from the current directory. - This is mainly meant to make Plinth work with configuration from - source code directory for debugging purposes. + Plinth reads various configuration options from the files + /usr/share/freedombox/freedombox.config, + /usr/share/freedombox/freedombox.config.d/*.config, + /etc/plinth/plinth.config, + /etc/plinth/plinth.config.d/*.config, + /etc/freedombox/freedombox.config and + /etc/freedombox/freedombox.config.d/*.config in that + order. Options in a file read later override options specified earlier. + /etc/plinth/ locations are deprecated. From 86829a29c1306f31e4319f937f0ee47fc99f6627 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sun, 28 Jun 2020 17:20:25 -0700 Subject: [PATCH 75/90] network: test: Fix race condition when deleting connections When deleting connections after editing, sometimes the connection is not found. Wait until the connection settles down to avoid connection not found errors during cleanup. Seems to work for now but still not the best way to handle this. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/tests/test_network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plinth/tests/test_network.py b/plinth/tests/test_network.py index a2a40c234..3bed5153f 100644 --- a/plinth/tests/test_network.py +++ b/plinth/tests/test_network.py @@ -109,6 +109,7 @@ def _connection(network, settings): uuid = network.add_connection(settings) time.sleep(0.1) yield uuid + time.sleep(0.1) network.delete_connection(uuid) From 618501f8e671c449db842688282fb9a148c8eeb7 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sun, 28 Jun 2020 17:22:21 -0700 Subject: [PATCH 76/90] storage: tests: Ignore cases needing loop devices when not available - In containers, loopback devices may not be available. Skip tests in this case by looking at the output of losetup setup utility. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/storage/tests/test_storage.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plinth/modules/storage/tests/test_storage.py b/plinth/modules/storage/tests/test_storage.py index 4b0716cdf..3a3f84b49 100644 --- a/plinth/modules/storage/tests/test_storage.py +++ b/plinth/modules/storage/tests/test_storage.py @@ -48,7 +48,13 @@ class Disk(): command = 'losetup --show --find {file}'.format( file=self.disk_file.name) process = subprocess.run(command.split(), stdout=subprocess.PIPE, - check=True) + stderr=subprocess.PIPE) + if process.returncode: + if b'cannot find an unused loop device' in process.stderr: + pytest.skip('Loopback devices not available') + else: + raise Exception(process.stderr) + device = process.stdout.decode().strip() subprocess.run(['partprobe', device], check=True) From 41fc24d29695adadad8580e1ffe6dd1e4e809cca Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sun, 28 Jun 2020 17:24:04 -0700 Subject: [PATCH 77/90] actions: tests: Fix test failures due order of fixtures Ensuring that load_cfg fixture is ordered first will ensure that configuration is properly restored after test and that changes in other fixtures take effect. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/tests/test_actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plinth/tests/test_actions.py b/plinth/tests/test_actions.py index f3eb9bf9a..27633a97d 100644 --- a/plinth/tests/test_actions.py +++ b/plinth/tests/test_actions.py @@ -22,8 +22,8 @@ from plinth.actions import run, superuser_run from plinth.errors import ActionError -@pytest.fixture(scope='module', autouse=True) -def actions_test_setup(): +@pytest.fixture(autouse=True) +def actions_test_setup(load_cfg): """Setup a temporary directory for testing actions. Copy system commands ``echo`` and ``id`` into actions directory during From 0c7c4b12fb987338a80c6c21de0602fbc8856e10 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sun, 28 Jun 2020 17:25:47 -0700 Subject: [PATCH 78/90] tests: Use develop configuration for most tests - Use the test configuration only when needed. This simplifies having to load test configuration properly for action tests. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- conftest.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/conftest.py b/conftest.py index 3f1eaf5ac..6b8cb0ce7 100644 --- a/conftest.py +++ b/conftest.py @@ -50,12 +50,21 @@ def fixture_load_cfg(): """Load test configuration.""" from plinth import cfg + keys = ('file_root', 'config_dir', 'data_dir', 'custom_static_dir', + 'store_file', 'actions_dir', 'doc_dir', 'server_dir', 'host', + 'port', 'use_x_forwarded_for', 'use_x_forwarded_host', + 'secure_proxy_ssl_header', 'box_name', 'develop') + saved_state = {} + for key in keys: + saved_state[key] = getattr(cfg, key) + root_dir = pathlib.Path(__file__).resolve().parent - test_data_dir = root_dir / 'plinth' / 'tests' / 'data' - cfg_file = test_data_dir / 'configs' / 'freedombox.config' + cfg_file = root_dir / 'plinth' / 'develop.config' cfg.read_file(str(cfg_file)) yield cfg - cfg.read_file(str(cfg_file)) + + for key in keys: + setattr(cfg, key, saved_state[key]) @pytest.fixture(name='develop_mode') From ba023c3ef8c540c8c0845d93e7aaa759ba5a9e9b Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Thu, 25 Jun 2020 20:09:49 -0400 Subject: [PATCH 79/90] upgrades: Skip enabling backports on testing and unstable Uses lsb-release which is a dependency of unattended-upgrades. Closes: #1844. Signed-off-by: James Valleroy [sunil: Minor change to the printed message] Signed-off-by: Sunil Mohan Adapa Reviewed-by: Sunil Mohan Adapa --- actions/upgrades | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/actions/upgrades b/actions/upgrades index 3db0e7ac1..f96ba270c 100755 --- a/actions/upgrades +++ b/actions/upgrades @@ -211,6 +211,12 @@ def _check_and_backports_sources(): 'backports.') return + release = subprocess.check_output(['lsb_release', '--release', + '--short']).decode().strip() + if release in ['testing', 'unstable']: + print(f'System release is {release}. Skip enabling backports.') + return + protocol = _get_protocol() if protocol == 'tor+http': print('Package download over Tor is enabled.') From 994a7a1d4bba5e57d485a8e9b0478d0ffc68132e Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Sat, 27 Jun 2020 08:15:05 -0400 Subject: [PATCH 80/90] networks: Remove firewall zone warning Mention that interface is automatically assigned to external zone. Test: - Re-assigned host0 interface to public zone. Disabled firewalld to still access interface. Firewall zone is shown as "external" with the note about automatic assignment. Closes: #1858. Signed-off-by: James Valleroy Reviewed-by: Sunil Mohan Adapa --- .../networks/templates/connection_show.html | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/plinth/modules/networks/templates/connection_show.html b/plinth/modules/networks/templates/connection_show.html index aca1bdba5..e0a35a81f 100644 --- a/plinth/modules/networks/templates/connection_show.html +++ b/plinth/modules/networks/templates/connection_show.html @@ -294,19 +294,22 @@
{% trans "Firewall zone" %}
- {{ connection.zone }} + external
-
- +
+ {% blocktrans trimmed %} - This interface is not maintained by {{ box_name }}. Its - security status is unknown to {{ box_name }}. Many {{ box_name }} - services may not be available on this interface. It is - recommended that you deactivate or delete this connection and - re-configure it. + This interface is not maintained by {{ box_name }}. For security, + it is automatically assigned to the external zone. + {% endblocktrans %} + {% blocktrans trimmed %} + This interface should receive your Internet connection. + If you connect it to a local network/machine, many + services meant to available only internally will not be + available. {% endblocktrans %}
{% endif %} From ff84d3f97ead124cfdc564c33ad2e6088af2fc27 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 26 Jun 2020 14:53:37 -0700 Subject: [PATCH 81/90] templates: Disable button and show spinner on submit for all forms Tests performed: - Submit a form and notice that button has a spinner soon after click. - Select a from like Gitweb repository creation form and submit it. After submit go back to previous form using back button. Notice that button has been restored to proper state. - Without filling valid information the form, press submit. Notice that the button does not change to a spinner. - Check installing an app, snapshots management, network forms, wireguard forms, etc. - Test on Firefox and Chromium. - Test with LibreJS that the script is accepted as valid free software license. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/templates/base.html | 3 ++ static/themes/default/css/main.css | 20 +++++++ static/themes/default/js/main.js | 83 ++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 static/themes/default/js/main.js diff --git a/plinth/templates/base.html b/plinth/templates/base.html index 174f33bc6..72ad95c74 100644 --- a/plinth/templates/base.html +++ b/plinth/templates/base.html @@ -62,6 +62,9 @@ + + + {% block app_head %}{% endblock %} {% block page_head %}{% endblock %} diff --git a/static/themes/default/css/main.css b/static/themes/default/css/main.css index edc66372e..be8db5614 100644 --- a/static/themes/default/css/main.css +++ b/static/themes/default/css/main.css @@ -522,6 +522,26 @@ a.menu_link_active { transform: translateY(-50%) translateX(-100%) } +/* + * Form button with loading progress. + */ +.running-status-button-before { + display: inline-block; + border: 4px solid #f3f3f3; /* Light grey */ + border-top: 4px solid #3498db; /* Blue */ + border-radius: 50%; + width: 16px; + height: 16px; + animation: spin 1s linear infinite; + margin-left: 10px; + margin-bottom: -4px; + margin-right: -26px; +} + +input[type='submit'].running-status-button { + padding-left: 32px; +} + /* * Button toolbar */ diff --git a/static/themes/default/js/main.js b/static/themes/default/js/main.js new file mode 100644 index 000000000..5fea98824 --- /dev/null +++ b/static/themes/default/js/main.js @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +/* + This file is part of FreedomBox. + + @licstart The following is the entire license notice for the + JavaScript code in this page. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + + @licend The above is the entire license notice + for the JavaScript code in this page. +*/ + +/* + * Disable submit button on click. + */ +function onSubmitAddProgress(event) { + // Using activeElement is not reliable. If the user presses Enter on a text + // field, activeElement with be that text field. However, we do safety + // checks and fallback to not disabling/animating the submit button, which + // is okay. + button = document.activeElement; + if (!button.classList.contains('btn') || + button.classList.contains('btn-link') || + button.classList.contains('no-running-status') || + button.hasAttribute('disabled')) { + return; + } + + // Don't disable the submit button immediately as that will prevent the + // button from being sent in the HTTP request. Instead schedule disabling + // for the next event loop run which will happen after current event is + // processed. + window.setTimeout(() => { + const beforeElement = document.createElement('div'); + beforeElement.classList.add('running-status-button-before'); + button.parentNode.insertBefore(beforeElement, button); + + button.classList.add('running-status-button'); + button.setAttribute('disabled', 'disabled'); + }, 0); +} + +document.addEventListener('DOMContentLoaded', function(event) { + const submitButtons = document.querySelectorAll("input[type='submit']"); + for (const button of submitButtons) { + if (button.form) { + // Don't listen for 'click' event on buttons as they are triggered + // even when the form is invalid. + button.form.addEventListener('submit', onSubmitAddProgress); + } + } +}); + +// When using back/forward browser's bfcache is used and pages won't receive +// 'load' events. Instead a 'pageshow' event is available. When a user does +// back/forward we want them to be able to submit the forms again. So clear all +// the button disabling. +window.addEventListener('pageshow', function(event) { + const selector = "input[type='submit'].running-status-button"; + const submitButtons = document.querySelectorAll(selector); + for (const button of submitButtons) { + button.classList.remove('running-status-button'); + button.removeAttribute('disabled'); + } + + const beforeSelector = ".running-status-button-before"; + const beforeElements = document.querySelectorAll(beforeSelector); + for (const element of beforeElements) { + element.remove(); + } +}); From 1b5a10a62847ccc1d603edcd2fe6e5e9b43b8971 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 26 Jun 2020 15:07:44 -0700 Subject: [PATCH 82/90] backups: Remove custom handling of progress on the restore button Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- .../modules/backups/static/loading_button.js | 31 ------------------- .../backups/templates/backups_restore.html | 9 ------ 2 files changed, 40 deletions(-) delete mode 100644 plinth/modules/backups/static/loading_button.js diff --git a/plinth/modules/backups/static/loading_button.js b/plinth/modules/backups/static/loading_button.js deleted file mode 100644 index 8d6057c4d..000000000 --- a/plinth/modules/backups/static/loading_button.js +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -/* - This file is part of FreedomBox. - - @licstart The following is the entire license notice for the - JavaScript code in this page. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - - @licend The above is the entire license notice - for the JavaScript code in this page. -*/ - - -// XXX: This is misuse of sr-only class. This is problematic for people using -// screen readers. -function swapWithLoadingButton() { - $("#restore_btn").addClass("sr-only"); - $("#loading_btn").removeClass("sr-only"); -} diff --git a/plinth/modules/backups/templates/backups_restore.html b/plinth/modules/backups/templates/backups_restore.html index 8ce2c153a..cf523553e 100644 --- a/plinth/modules/backups/templates/backups_restore.html +++ b/plinth/modules/backups/templates/backups_restore.html @@ -24,21 +24,12 @@ - - -

{% endblock %} {% block page_js %} - {% endblock %} From 3efff2fa42c41cb6929742a1086aee201a6405a2 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sat, 27 Jun 2020 00:30:48 -0700 Subject: [PATCH 83/90] js: Simplify auto-refresh page logic - Implement ability to refresh page at the framework level so that every page does not need to handle it. - Refresh after number of seconds specified in context of the view. Tests performed: - Trigger the following functions and ensure that page reload after 3 seconds during the running operation while it does refresh before and after the operation. - Diagnostics tests from the module. - Gitweb repository cloning. - Monkeysphere publish key to server. - OpenVPN setup. - Tor configuration update. - Manual software update. - App installation. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- .../diagnostics/templates/diagnostics.html | 19 ---------- plinth/modules/diagnostics/views.py | 6 ++-- .../gitweb/templates/gitweb_configure.html | 9 ----- plinth/modules/gitweb/views.py | 1 + .../monkeysphere/templates/monkeysphere.html | 13 ------- plinth/modules/monkeysphere/views.py | 3 +- plinth/modules/openvpn/templates/openvpn.html | 17 --------- plinth/modules/openvpn/views.py | 2 ++ plinth/modules/tor/templates/tor.html | 14 -------- plinth/modules/tor/views.py | 1 + .../templates/upgrades_configure.html | 14 -------- plinth/modules/upgrades/views.py | 1 + plinth/templates/base.html | 11 +++++- plinth/templates/setup.html | 25 ------------- plinth/views.py | 6 ++++ static/themes/default/js/main.js | 18 ++++++++++ static/themes/default/js/refresh.js | 36 ------------------- .../themes/default/js/refresh_immediately.js | 25 ------------- 18 files changed, 45 insertions(+), 176 deletions(-) delete mode 100644 static/themes/default/js/refresh.js delete mode 100644 static/themes/default/js/refresh_immediately.js diff --git a/plinth/modules/diagnostics/templates/diagnostics.html b/plinth/modules/diagnostics/templates/diagnostics.html index f2212c096..e87525db6 100644 --- a/plinth/modules/diagnostics/templates/diagnostics.html +++ b/plinth/modules/diagnostics/templates/diagnostics.html @@ -6,17 +6,6 @@ {% load i18n %} {% load static %} -{% block page_head %} - - {% if is_running %} - - {% endif %} - -{% endblock %} - - {% block configuration %} {% if not is_running %} @@ -61,11 +50,3 @@ {% endif %} {% endblock %} - - -{% block page_js %} - {% if is_running %} - - {% endif %} -{% endblock %} - diff --git a/plinth/modules/diagnostics/views.py b/plinth/modules/diagnostics/views.py index 9b441032e..0c81f7440 100644 --- a/plinth/modules/diagnostics/views.py +++ b/plinth/modules/diagnostics/views.py @@ -17,11 +17,13 @@ def index(request): if request.method == 'POST' and not diagnostics.running_task: diagnostics.start_task() + is_running = diagnostics.running_task is not None return TemplateResponse( request, 'diagnostics.html', { 'app_info': diagnostics.app.info, - 'is_running': diagnostics.running_task is not None, - 'results': diagnostics.current_results + 'is_running': is_running, + 'results': diagnostics.current_results, + 'refresh_page_sec': 3 if is_running else None }) diff --git a/plinth/modules/gitweb/templates/gitweb_configure.html b/plinth/modules/gitweb/templates/gitweb_configure.html index 888241a64..93813c452 100644 --- a/plinth/modules/gitweb/templates/gitweb_configure.html +++ b/plinth/modules/gitweb/templates/gitweb_configure.html @@ -82,12 +82,3 @@
{% endblock %} - - -{% block page_js %} - - {% if cloning %} - - {% endif %} - -{% endblock %} diff --git a/plinth/modules/gitweb/views.py b/plinth/modules/gitweb/views.py index 9affd6baf..26027579f 100644 --- a/plinth/modules/gitweb/views.py +++ b/plinth/modules/gitweb/views.py @@ -31,6 +31,7 @@ class GitwebAppView(views.AppView): repos = gitweb.app.get_repo_list() context['repos'] = repos context['cloning'] = any('clone_progress' in repo for repo in repos) + context['refresh_page_sec'] = 3 if context['cloning'] else None return context diff --git a/plinth/modules/monkeysphere/templates/monkeysphere.html b/plinth/modules/monkeysphere/templates/monkeysphere.html index 16647a098..76c170181 100644 --- a/plinth/modules/monkeysphere/templates/monkeysphere.html +++ b/plinth/modules/monkeysphere/templates/monkeysphere.html @@ -9,12 +9,6 @@ {% block page_head %} - {% if running %} - - {% endif %} -