From d38f787a42db0767e2217664018c4a2c2599cff0 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 4 Oct 2024 20:11:34 -0700 Subject: [PATCH 01/56] apache2: Allow popups to have different sandbox policy - Without this change when opening popups, Firefox throws the error 'Blocked Page' under certain conditions. - Complete a comment that was seemingly left unfinished. Tests: - With the changes installed with 'make build install', opening popups with works without 'Blocked page' error. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- data/etc/apache2/conf-available/freedombox.conf | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/etc/apache2/conf-available/freedombox.conf b/data/etc/apache2/conf-available/freedombox.conf index 95023151d..48fc827e3 100644 --- a/data/etc/apache2/conf-available/freedombox.conf +++ b/data/etc/apache2/conf-available/freedombox.conf @@ -72,15 +72,18 @@ RedirectMatch "^/$" "/plinth" ## Enable strict sandboxing enabled with some exceptions: ## - Allow running Javascript. ## - Allow popups as sometimes we use +## - Allow popups to have different sandbox requirements as we launch apps' web +## clients. ## - Allow forms to support configuration forms. -## - +## - Allow policies to treat same origin differently from other origins +## - Allow downloads such as backup tarballs. ## ## Disable browser guessing of MIME types. FreedoBox already sets good content ## types for all the common file types. ## Header set Referrer-Policy 'same-origin' - Header set Content-Security-Policy "font-src 'self'; frame-src 'none'; img-src 'self'; manifest-src 'none'; media-src 'none'; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self'; default-src 'self'; base-uri 'none'; sandbox allow-scripts allow-popups allow-forms allow-same-origin allow-downloads; form-action 'self'; frame-ancestors 'none'; block-all-mixed-content;" + Header set Content-Security-Policy "font-src 'self'; frame-src 'none'; img-src 'self'; manifest-src 'none'; media-src 'none'; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self'; default-src 'self'; base-uri 'none'; sandbox allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-same-origin allow-downloads; form-action 'self'; frame-ancestors 'none'; block-all-mixed-content;" Header set X-Content-Type-Options 'nosniff' From 17952c9cf39cc60a28538f067143f740855cacaf Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 4 Oct 2024 20:30:17 -0700 Subject: [PATCH 02/56] firstboot: Improve the setup complete page with more setups Fixes: #888. - Suggest all the steps that a typical user should likely take. - Custom styling to make the page look good. - Open the links in new windows as this page can't be reached again. - Add a button for software updates as this can be done easily and the most important step. Tests: - Trigger first setup by removing plinth.sqlite3. Notice the improved setup complete page. Text and icons are as expected. Links work and open in a new window. Clicking on 'Update now' button opens a page to software updates with manual upgrade triggered. - Mobile view looks good. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- .../templates/firstboot_complete.html | 90 +++++++++++++++---- static/themes/default/css/main.css | 23 +++++ 2 files changed, 94 insertions(+), 19 deletions(-) diff --git a/plinth/modules/first_boot/templates/firstboot_complete.html b/plinth/modules/first_boot/templates/firstboot_complete.html index d0323dd75..b931395dc 100644 --- a/plinth/modules/first_boot/templates/firstboot_complete.html +++ b/plinth/modules/first_boot/templates/firstboot_complete.html @@ -8,27 +8,79 @@ {% block content %} -

{% trans "Setup Complete!" %}

+

{% trans "Setup Complete! Next Steps:" %}

-

- {% blocktrans trimmed %} - Without any apps, your {{ box_name }} cannot do very much. - {% endblocktrans %} -

+
    +
  1. +
    +
    + {% url 'upgrades:index' as upgrades_url %} + {% blocktrans trimmed %} + Automatic software + update runs daily by default. For the first time, manually run + it now. + {% endblocktrans %} +
    + {% csrf_token %} + +
    +
    +
  2. - +
  3. +
    +
    + {% url 'privacy:index' as privacy_url %} + {% blocktrans trimmed %} + Review privacy options. + {% endblocktrans %} +
    +
  4. -

    -

    - {% url 'networks:index' as networks_url %} - {% blocktrans trimmed %} - You may want to check the network - setup and modify it if necessary. - {% endblocktrans %} -

    -

    +
  5. +
    +
    + {% url 'networks:index' as networks_url %} + {% blocktrans trimmed %} + Review and setup network + connections. Change the default Wi-Fi password, if applicable. + {% endblocktrans %} +
    +
  6. + +
  7. +
    +
    + {% url 'names:index' as names_url %} + {% blocktrans trimmed %} + Configure a domain name. + {% endblocktrans %} +
    +
  8. + +
  9. +
    +
    + {% url 'backups:index' as backups_url %} + {% blocktrans trimmed %} + Configure and schedule remote + backups. + {% endblocktrans %} +
    +
  10. + +
  11. +
    +
    + {% url 'apps' as apps_url %} + {% blocktrans trimmed %} + Put {{ box_name }} to use by installing + apps. + {% endblocktrans %} +
    +
  12. + {% endblock %} diff --git a/static/themes/default/css/main.css b/static/themes/default/css/main.css index 5492689f8..b8aecfe08 100644 --- a/static/themes/default/css/main.css +++ b/static/themes/default/css/main.css @@ -636,6 +636,29 @@ a.menu_link_active { width: 50%; } +.next-steps { + list-style: none; + margin-top: 1.5rem; + padding: 0; +} + +.next-steps li { + display: flex; + align-items: center; + padding: 0.75rem 0; +} + +.next-steps .app-icon { + font-size: 3rem; + margin-right: 1rem; + width: 3rem; + text-align: center; +} + +.next-steps form { + display: inline; +} + /* * Toggle button */ From fb2d26a16c6887e96c717409fadb45834b3f7a00 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sat, 5 Oct 2024 14:22:43 -0700 Subject: [PATCH 03/56] firstboot: Hide navigation toggler in mobile layouts Tests: - Start first boot by removing /var/lib/plinth/plinth.sqlite3 and starting service. Switch to responsive design mode and select a phone layout. Notice that an inactive toggler appears during bootup/welcome/account first boot steps. - With the patch, the toggler button does not appear during those steps. After the account step, the toggler appears and is functional with help menu. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- .../first_boot/templates/base_firstboot.html | 9 +++++++++ plinth/templates/base.html | 20 +++++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/plinth/modules/first_boot/templates/base_firstboot.html b/plinth/modules/first_boot/templates/base_firstboot.html index ed22363e0..7efe77fd6 100644 --- a/plinth/modules/first_boot/templates/base_firstboot.html +++ b/plinth/modules/first_boot/templates/base_firstboot.html @@ -12,6 +12,15 @@ no-brand {% block mainmenu_left_collapse %} {% endblock %} +{% block notifications_dropdown %} +{% endblock %} + +{% block mainmenu_toggler %} + {% if user.is_authenticated %} + {{ block.super }} + {% endif %} +{% endblock %} + {% block mainmenu_right %} {% if user.is_authenticated %} {% include "firstboot_navbar.html" %} diff --git a/plinth/templates/base.html b/plinth/templates/base.html index 3bc8e83a5..eb91fc4b6 100644 --- a/plinth/templates/base.html +++ b/plinth/templates/base.html @@ -89,15 +89,19 @@ {% endblock %} - + {% block notifications_dropdown %} + + {% endblock %} - + {% block mainmenu_toggler %} + + {% endblock %} {% if is_first_setup_running %} - +

    + +

    {% endif %} {% endblock %} diff --git a/static/themes/default/css/main.css b/static/themes/default/css/main.css index b8aecfe08..a20a20398 100644 --- a/static/themes/default/css/main.css +++ b/static/themes/default/css/main.css @@ -636,6 +636,19 @@ a.menu_link_active { width: 50%; } + +/* + * First setup pages + */ +.firstboot-spinner { + text-align: center; +} + +.firstboot-spinner span.fa { + float: none; + margin: auto; +} + .next-steps { list-style: none; margin-top: 1.5rem; From 3e0b86eac8529b8b0968f4063d612a09f1b249d8 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 7 Oct 2024 04:53:48 -0700 Subject: [PATCH 06/56] css: Fix height of navbar in mobile layout during first boot Only item in the navbar present during first boot in mobile layout is the toggler. Set its height such that it maintains the height of the entire navbar. Tests: - After the account setup screen, switch to mobile layout. Notice that the navbar is no longer tiny but the usual height. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- static/themes/default/css/main.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/themes/default/css/main.css b/static/themes/default/css/main.css index a20a20398..78d4285b0 100644 --- a/static/themes/default/css/main.css +++ b/static/themes/default/css/main.css @@ -464,6 +464,8 @@ footer { .main-header .navbar-toggler { border: 1px solid #ddd; + /* In mobile layout, during first setup, maintain the height of the navbar */ + margin: 0.6875rem 0; } @media screen and (max-width: 767px) { From 07d0e839d33c0e3f4b761bd68ed0b66b39da8881 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 7 Oct 2024 05:20:23 -0700 Subject: [PATCH 07/56] css: Navbar styling fixes in mobile layout Fix the following: - In mobile layout, the logout button has incorrect text color and when hovering, wrong background color. - In mobile layout, when user menu is expanded, there is no spacing between the end of the menu and the border for the expanded menu items. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- static/themes/default/css/main.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/static/themes/default/css/main.css b/static/themes/default/css/main.css index 78d4285b0..f2ff58061 100644 --- a/static/themes/default/css/main.css +++ b/static/themes/default/css/main.css @@ -475,12 +475,19 @@ footer { .main-header .navbar-nav > li > a:focus, .main-header .navbar-nav > li > a:hover, + .main-header .navbar-nav li > form > input, + .main-header .navbar-nav li > form > input:focus, + .main-header .navbar-nav li > form > input:hover, .main-header .navbar-nav .show a, .main-header .navbar-nav .show a:hover, .main-header .navbar-nav .show a:focus { color: #FFF; background: transparent; } + + .main-header .navbar-nav .nav-item:last-of-type .dropdown-menu { + margin-bottom: 1.25rem; + } } /* Cards in Apps, System and Help pages */ From d158a22b5923c459e4b172f507285723a5585d7a Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 8 Oct 2024 11:32:20 +0000 Subject: [PATCH 08/56] Translated using Weblate (Spanish) Currently translated at 100.0% (1678 of 1678 strings) --- plinth/locale/es/LC_MESSAGES/django.po | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/plinth/locale/es/LC_MESSAGES/django.po b/plinth/locale/es/LC_MESSAGES/django.po index f0b3de12e..f0430d337 100644 --- a/plinth/locale/es/LC_MESSAGES/django.po +++ b/plinth/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-10-07 20:05-0400\n" -"PO-Revision-Date: 2024-09-25 06:15+0000\n" +"PO-Revision-Date: 2024-10-09 04:16+0000\n" "Last-Translator: gallegonovato \n" "Language-Team: Spanish \n" @@ -2380,10 +2380,9 @@ msgid "Failed to add wiki file." msgstr "Error al añadir el archivo wiki." #: modules/featherwiki/views.py:138 modules/tiddlywiki/views.py:139 -#, fuzzy, python-brace-format -#| msgid "Could not delete {name}: {error}" +#, python-brace-format msgid "Could not delete {name}" -msgstr "No se pudo eliminar {name}: {error}" +msgstr "No se pudo eliminar {name}" #: modules/firewall/__init__.py:25 #, python-brace-format @@ -3462,10 +3461,8 @@ msgid "Add a new content package" msgstr "Añadir un nuevo paquete de contenidos" #: modules/kiwix/views.py:76 -#, fuzzy -#| msgid "Content package added." msgid "Content package already exists." -msgstr "Se ha añadido un paquete de contenidos." +msgstr "El paquete de contenidos ya existe." #: modules/kiwix/views.py:79 msgid "Failed to add content package." @@ -4329,7 +4326,7 @@ msgstr "Servicios de nombres" #: modules/names/__init__.py:171 msgid "Package systemd-resolved is installed" -msgstr "" +msgstr "Paquete systemd-resolved está instalado" #: modules/names/__init__.py:195 #, python-brace-format @@ -4471,6 +4468,8 @@ msgid "" "systemd-resolved package is not installed. Install it for additional " "functionality." msgstr "" +"El paquete systemd-resolved no está instalado. Instálelo para obtener " +"funciones adicionales." #: modules/names/templates/names.html:121 templates/setup.html:66 msgid "Install" From 929c82d41e11aa4a03c57a0c7d16cbabfccf1ce8 Mon Sep 17 00:00:00 2001 From: Burak Yavuz Date: Tue, 8 Oct 2024 07:59:27 +0000 Subject: [PATCH 09/56] Translated using Weblate (Turkish) Currently translated at 100.0% (1678 of 1678 strings) --- plinth/locale/tr/LC_MESSAGES/django.po | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/plinth/locale/tr/LC_MESSAGES/django.po b/plinth/locale/tr/LC_MESSAGES/django.po index f1438b044..98b23c45f 100644 --- a/plinth/locale/tr/LC_MESSAGES/django.po +++ b/plinth/locale/tr/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-10-07 20:05-0400\n" -"PO-Revision-Date: 2024-09-25 06:15+0000\n" +"PO-Revision-Date: 2024-10-09 04:16+0000\n" "Last-Translator: Burak Yavuz \n" "Language-Team: Turkish \n" @@ -2360,10 +2360,9 @@ msgid "Failed to add wiki file." msgstr "Viki dosyası ekleme başarısız oldu." #: modules/featherwiki/views.py:138 modules/tiddlywiki/views.py:139 -#, fuzzy, python-brace-format -#| msgid "Could not delete {name}: {error}" +#, python-brace-format msgid "Could not delete {name}" -msgstr "{name} silinemedi: {error}" +msgstr "{name} silinemedi" #: modules/firewall/__init__.py:25 #, python-brace-format @@ -3442,10 +3441,8 @@ msgid "Add a new content package" msgstr "Yeni bir içerik paketi ekle" #: modules/kiwix/views.py:76 -#, fuzzy -#| msgid "Content package added." msgid "Content package already exists." -msgstr "İçerik paketi eklendi." +msgstr "İçerik paketi zaten var." #: modules/kiwix/views.py:79 msgid "Failed to add content package." @@ -4309,7 +4306,7 @@ msgstr "Ad Hizmetleri" #: modules/names/__init__.py:171 msgid "Package systemd-resolved is installed" -msgstr "" +msgstr "systemd-resolved paketi yüklendi" #: modules/names/__init__.py:195 #, python-brace-format @@ -4451,7 +4448,7 @@ msgstr "Yedek DNS Sunucuları" msgid "" "systemd-resolved package is not installed. Install it for additional " "functionality." -msgstr "" +msgstr "systemd-resolved paketi yüklü değil. Ek işlevsellik için yükleyin." #: modules/names/templates/names.html:121 templates/setup.html:66 msgid "Install" From 6462872ed7e59bbe60776e2aec054e9e13fb3045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Wed, 9 Oct 2024 00:51:19 +0000 Subject: [PATCH 10/56] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 65.0% (1091 of 1678 strings) --- plinth/locale/zh_Hans/LC_MESSAGES/django.po | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/plinth/locale/zh_Hans/LC_MESSAGES/django.po b/plinth/locale/zh_Hans/LC_MESSAGES/django.po index eb4a71b7d..6c6cb840f 100644 --- a/plinth/locale/zh_Hans/LC_MESSAGES/django.po +++ b/plinth/locale/zh_Hans/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: Plinth\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-10-07 20:05-0400\n" -"PO-Revision-Date: 2024-09-25 06:15+0000\n" +"PO-Revision-Date: 2024-10-09 04:16+0000\n" "Last-Translator: 大王叫我来巡山 \n" "Language-Team: Chinese (Simplified Han script) Date: Tue, 8 Oct 2024 04:17:23 +0000 Subject: [PATCH 11/56] Translated using Weblate (Bulgarian) Currently translated at 46.4% (780 of 1678 strings) --- plinth/locale/bg/LC_MESSAGES/django.po | 28 ++++++++++++++------------ 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/plinth/locale/bg/LC_MESSAGES/django.po b/plinth/locale/bg/LC_MESSAGES/django.po index 3f3a392d3..667e39798 100644 --- a/plinth/locale/bg/LC_MESSAGES/django.po +++ b/plinth/locale/bg/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-10-07 20:05-0400\n" -"PO-Revision-Date: 2024-09-25 06:15+0000\n" +"PO-Revision-Date: 2024-10-09 04:16+0000\n" "Last-Translator: 109247019824 \n" "Language-Team: Bulgarian \n" @@ -312,7 +312,7 @@ msgstr "Изберете приложенията, които да бъдат в #: modules/backups/forms.py:138 modules/kiwix/forms.py:21 msgid "Upload File" -msgstr "Качване на файл" +msgstr "Изпращане на файл" #: modules/backups/forms.py:140 msgid "Backup files have to be in .tar.gz format" @@ -627,7 +627,7 @@ msgstr "" #: modules/backups/templates/backups_upload.html:41 msgid "Upload file" -msgstr "Качване на файл" +msgstr "Изпращане на файл" #: modules/backups/templates/verify_ssh_hostkey.html:18 #, python-format @@ -1120,13 +1120,13 @@ msgstr "" #: modules/tiddlywiki/views.py:136 #, python-brace-format msgid "{name} deleted." -msgstr "" +msgstr "Пакетът {name} е премахнат." #: modules/calibre/views.py:67 modules/gitweb/views.py:143 #: modules/kiwix/views.py:101 #, python-brace-format msgid "Could not delete {name}: {error}" -msgstr "" +msgstr "Грешка при премахване на пакета {name}: {error}" #: modules/cockpit/__init__.py:23 #, python-brace-format @@ -3078,6 +3078,10 @@ msgid "" "Wikipedia available without using the internet, but it is potentially " "suitable for all HTML content. Kiwix packages are in the ZIM file format." msgstr "" +"Kiwix е четец за съдържание от интернет без достъп до мрежа. Това е софтуер, " +"предназначен да направи Уикипедия достъпна, без използване на интернет, но е " +"подходящ за всякакво съдържание във формат HTML. Пакетите на Kiwix са във " +"файлов формат ZIM." #: modules/kiwix/__init__.py:25 msgid "" @@ -3107,7 +3111,7 @@ msgstr "" #: modules/kiwix/__init__.py:56 modules/kiwix/manifest.py:8 msgid "Kiwix" -msgstr "" +msgstr "Kiwix" #: modules/kiwix/__init__.py:57 msgid "Offline Wikipedia" @@ -3128,7 +3132,7 @@ msgstr "" #: modules/kiwix/templates/kiwix-add-package.html:24 #, python-format msgid "You have %(max_filesize)s of free disk space available." -msgstr "" +msgstr "Налични са %(max_filesize)s свободно дисково пространство." #: modules/kiwix/templates/kiwix-add-package.html:36 msgid "Upload ZIM file" @@ -3151,7 +3155,7 @@ msgstr "" #: modules/kiwix/templates/kiwix.html:15 msgid "Add a content package" -msgstr "" +msgstr "Добавяне на пакети със съдържание" #: modules/kiwix/templates/kiwix.html:17 msgid "Add Package" @@ -3168,17 +3172,15 @@ msgstr "Премахване на пакета %(title)s" #: modules/kiwix/views.py:49 msgid "Content package added." -msgstr "" +msgstr "Пакетът със съдържание е добавен." #: modules/kiwix/views.py:54 msgid "Add a new content package" -msgstr "" +msgstr "Добавяне на пакет със съдържание" #: modules/kiwix/views.py:76 -#, fuzzy -#| msgid "Remote backup repository already exists." msgid "Content package already exists." -msgstr "Отдалеченото хранилище за резервни копия вече съществува." +msgstr "Пакетът със съдържание вече съществува." #: modules/kiwix/views.py:79 msgid "Failed to add content package." From e92279d593ceac64ef97ff54ab1c4488e5ffefd9 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 8 Oct 2024 07:23:28 +0000 Subject: [PATCH 12/56] Translated using Weblate (Albanian) Currently translated at 99.5% (1670 of 1678 strings) --- plinth/locale/sq/LC_MESSAGES/django.po | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plinth/locale/sq/LC_MESSAGES/django.po b/plinth/locale/sq/LC_MESSAGES/django.po index e4bb4303b..216d07764 100644 --- a/plinth/locale/sq/LC_MESSAGES/django.po +++ b/plinth/locale/sq/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-10-07 20:05-0400\n" -"PO-Revision-Date: 2024-10-04 12:15+0000\n" +"PO-Revision-Date: 2024-10-09 04:16+0000\n" "Last-Translator: Besnik Bleta \n" "Language-Team: Albanian \n" @@ -3464,10 +3464,8 @@ msgid "Add a new content package" msgstr "Shtoni paketë të re lënde" #: modules/kiwix/views.py:76 -#, fuzzy -#| msgid "Content package added." msgid "Content package already exists." -msgstr "U shtua paketë lënde." +msgstr "Paketa e lëndës ekziston tashmë." #: modules/kiwix/views.py:79 msgid "Failed to add content package." From 6cb3f980615657dce5b52a680a8c1fa2bd2daf9d Mon Sep 17 00:00:00 2001 From: 109247019824 Date: Wed, 9 Oct 2024 19:28:10 +0000 Subject: [PATCH 13/56] Translated using Weblate (Bulgarian) Currently translated at 47.0% (789 of 1678 strings) --- plinth/locale/bg/LC_MESSAGES/django.po | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/plinth/locale/bg/LC_MESSAGES/django.po b/plinth/locale/bg/LC_MESSAGES/django.po index 667e39798..c7762e62f 100644 --- a/plinth/locale/bg/LC_MESSAGES/django.po +++ b/plinth/locale/bg/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-10-07 20:05-0400\n" -"PO-Revision-Date: 2024-10-09 04:16+0000\n" +"PO-Revision-Date: 2024-10-10 05:16+0000\n" "Last-Translator: 109247019824 \n" "Language-Team: Bulgarian \n" @@ -2230,7 +2230,7 @@ msgstr "Грешка при добавяне на файла към енцикл #: modules/featherwiki/views.py:138 modules/tiddlywiki/views.py:139 #, python-brace-format msgid "Could not delete {name}" -msgstr "" +msgstr "„{name}“ не може да бъде премахнато" #: modules/firewall/__init__.py:25 #, python-brace-format @@ -3095,6 +3095,15 @@ msgid "" "
  13. Magazines: Low-tech Magazine
  14. \n" " " msgstr "" +"Kiwix може да съхранява различни видове съдържание:\n" +"
      \n" +"
    • Видео съдържание: Khan Academy, TED Talks, Crash Course
    • \n" +"
    • Изтеглени версии на страници: Проекти на Уикимедия, Stack Exchange
    • " +"\n" +"
    • Образователни материали: PHET, TED Ed, Vikidia
    • \n" +"
    • Електронни книги: Project Gutenberg
    • \n" +"
    • Списания: Low-tech Magazine
    • \n" +"
    " #: modules/kiwix/__init__.py:33 #: modules/kiwix/templates/kiwix-add-package.html:14 @@ -3104,10 +3113,14 @@ msgid "" "project or create your own." msgstr "" +"Можете да изтеглите пакети със съдържание от проекта Kiwix или да създадете свои собствени." #: modules/kiwix/__init__.py:53 msgid "Manage Kiwix content server" -msgstr "" +msgstr "Управление на сървъра за съдържание на Kiwix" #: modules/kiwix/__init__.py:56 modules/kiwix/manifest.py:8 msgid "Kiwix" @@ -3115,7 +3128,7 @@ msgstr "Kiwix" #: modules/kiwix/__init__.py:57 msgid "Offline Wikipedia" -msgstr "" +msgstr "Уикипедия извън мрежата" #: modules/kiwix/forms.py:23 msgid "Content packages have to be in .zim format" @@ -3128,6 +3141,9 @@ msgid "" "{box_name}. If Kiwix fails to add the file, it will be deleted immediately " "to save disk space." msgstr "" +"Качените ZIM файлове ще бъдат пазени в папката {kiwix_home}/content на " +"{box_name}. Ако Kiwix не успее да добави даден файл, той ще бъде незабавно " +"премахнат, за да бъде освободено мястото на диска." #: modules/kiwix/templates/kiwix-add-package.html:24 #, python-format @@ -3148,10 +3164,12 @@ msgid "" "Delete this package permanently? You may add it back later if you have a " "copy of the ZIM file." msgstr "" +"Безвъзвратно премахване на пакет? Можете да го добавите отново по-късно, ако " +"разполагате с копие на файла ZIM." #: modules/kiwix/templates/kiwix.html:11 msgid "Manage Content Packages" -msgstr "" +msgstr "Управление на пакети със съдържание" #: modules/kiwix/templates/kiwix.html:15 msgid "Add a content package" @@ -3163,7 +3181,7 @@ msgstr "Добавяне на пакет" #: modules/kiwix/templates/kiwix.html:24 msgid "No content packages available." -msgstr "" +msgstr "Няма пакети със съдържание." #: modules/kiwix/templates/kiwix.html:37 #, python-format From d0d53edce06898ff32298f056323a345763747e0 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 9 Oct 2024 08:55:04 +0000 Subject: [PATCH 14/56] Translated using Weblate (Albanian) Currently translated at 99.7% (1673 of 1678 strings) --- plinth/locale/sq/LC_MESSAGES/django.po | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plinth/locale/sq/LC_MESSAGES/django.po b/plinth/locale/sq/LC_MESSAGES/django.po index 216d07764..eda011d14 100644 --- a/plinth/locale/sq/LC_MESSAGES/django.po +++ b/plinth/locale/sq/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-10-07 20:05-0400\n" -"PO-Revision-Date: 2024-10-09 04:16+0000\n" +"PO-Revision-Date: 2024-10-10 05:16+0000\n" "Last-Translator: Besnik Bleta \n" "Language-Team: Albanian \n" @@ -2378,10 +2378,9 @@ msgid "Failed to add wiki file." msgstr "S’u arrit të shtohej kartelë wiki." #: modules/featherwiki/views.py:138 modules/tiddlywiki/views.py:139 -#, fuzzy, python-brace-format -#| msgid "Could not delete {name}: {error}" +#, python-brace-format msgid "Could not delete {name}" -msgstr "S’u fshi dot {name}: {error}" +msgstr "S’u fshi dot {name}" #: modules/firewall/__init__.py:25 #, python-brace-format @@ -4337,7 +4336,7 @@ msgstr "Shërbime Emrash" #: modules/names/__init__.py:171 msgid "Package systemd-resolved is installed" -msgstr "" +msgstr "Paketa systemd-resolved është instaluar" #: modules/names/__init__.py:195 #, python-brace-format @@ -4480,6 +4479,8 @@ msgid "" "systemd-resolved package is not installed. Install it for additional " "functionality." msgstr "" +"Paketa systemd-resolved s’është instaluar. Për më tepër funksione, " +"instalojeni." #: modules/names/templates/names.html:121 templates/setup.html:66 msgid "Install" From a998995f36b124091d35196cf0799867ec27e0be Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 7 Oct 2024 05:40:32 -0700 Subject: [PATCH 15/56] upgrades: Remove step upgrade during first setup - Remove the first setup wizard step to run security upgrades. At the time of its introduction, it was felt that this is very important. Some things have changed since then: - We have mechanism for queuing package operations. Users can now trigger software updates and start installing apps before that is completed. Or vice versa. Earlier if the software updates were running, app install used to fail with an error. - There were no notifications. Since then we have added 'first setup' notification for important topics such as Privacy. This step can be replaced with a notification. - Automatic diagnostics and a diagnostic to notify of updated packages also helps bring attention to software updates if they are missed during first setup. - A proposed change will re-introduce an advice to run updates in the 'Next steps' wizard step along with a button trigger it right there. - The new notification for software updates will bring more attention to running updates as part of first setup. - It would be nice not be stuck in the first setup wizard for a long period and make it look simple. It improves the fun factor of setting up FreedomBox. - It would present an opportunity to utilize the parallel installation of apps/updates to the full extent. Although this can also be done by skipping the progress step after updates are run. - First wizard steps tend to get less testing. Tests: - Run the first setup wizard by removing /var/lib/plinth/plinth.sqlite3 and running the service. Notice that the software update step is not shown and wizard completes successfully. - On stable container, backports step is shown as expected (if not already enabled). Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/upgrades/__init__.py | 5 --- plinth/modules/upgrades/forms.py | 6 --- .../templates/update-firstboot-progress.html | 42 ------------------ .../upgrades/templates/update-firstboot.html | 36 --------------- plinth/modules/upgrades/urls.py | 5 --- plinth/modules/upgrades/views.py | 44 +------------------ plinth/tests/functional/__init__.py | 4 -- 7 files changed, 1 insertion(+), 141 deletions(-) delete mode 100644 plinth/modules/upgrades/templates/update-firstboot-progress.html delete mode 100644 plinth/modules/upgrades/templates/update-firstboot.html diff --git a/plinth/modules/upgrades/__init__.py b/plinth/modules/upgrades/__init__.py index e0e9ba80a..34bb13dd0 100644 --- a/plinth/modules/upgrades/__init__.py +++ b/plinth/modules/upgrades/__init__.py @@ -27,11 +27,6 @@ first_boot_steps = [ 'url': 'upgrades:backports-firstboot', 'order': 5, }, - { - 'id': 'initial_update', - 'url': 'upgrades:update-firstboot', - 'order': 6, - }, ] _description = [ diff --git a/plinth/modules/upgrades/forms.py b/plinth/modules/upgrades/forms.py index c97688a43..390963512 100644 --- a/plinth/modules/upgrades/forms.py +++ b/plinth/modules/upgrades/forms.py @@ -33,9 +33,3 @@ class BackportsFirstbootForm(forms.Form): backports_enabled = forms.BooleanField( label=_('Activate frequent feature updates (recommended)'), required=False, initial=True) - - -class UpdateFirstbootForm(forms.Form): - """Form to run or skip initial update during first boot wizard.""" - update_now = forms.BooleanField(label=_('Update now (recommended)'), - required=False, initial=True) diff --git a/plinth/modules/upgrades/templates/update-firstboot-progress.html b/plinth/modules/upgrades/templates/update-firstboot-progress.html deleted file mode 100644 index 1082ebc46..000000000 --- a/plinth/modules/upgrades/templates/update-firstboot-progress.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "base_firstboot.html" %} -{% comment %} -# SPDX-License-Identifier: AGPL-3.0-or-later -{% endcomment %} - -{% load bootstrap %} -{% load i18n %} -{% load static %} - -{% block content %} -

    {% trans "Software Update" %}

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

    - {% trans "Updating, please wait..." %} -

    -

    - {% blocktrans trimmed %} - This may take a long time to complete. During - an update, this web interface may be temporarily unavailable and - show an error. In that case, refresh the page to continue. - {% endblocktrans %} -

    -
    - {% else %} -

    - {% blocktrans %} - {{ box_name }} is up to date. Press Next to continue. - {% endblocktrans %} -

    - - {% trans 'Next' %} - - {% endif %} - -{% endblock %} diff --git a/plinth/modules/upgrades/templates/update-firstboot.html b/plinth/modules/upgrades/templates/update-firstboot.html deleted file mode 100644 index 374c6bf95..000000000 --- a/plinth/modules/upgrades/templates/update-firstboot.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends "base_firstboot.html" %} -{% comment %} -# SPDX-License-Identifier: AGPL-3.0-or-later -{% endcomment %} - -{% load bootstrap %} -{% load i18n %} -{% load static %} - -{% block content %} -

    {% trans "Software Update" %}

    - -

    - {% blocktrans trimmed %} - Check for and apply the latest software and security updates. - {% endblocktrans %} -

    - -

    - {% blocktrans trimmed %} - This may take a long time to complete. During - an update, this web interface may be temporarily unavailable and - show an error. In that case, refresh the page to continue. - {% endblocktrans %} -

    - -
    - {% csrf_token %} - - {{ form|bootstrap }} - - -
    - -{% endblock %} diff --git a/plinth/modules/upgrades/urls.py b/plinth/modules/upgrades/urls.py index 807ebb328..15955bd91 100644 --- a/plinth/modules/upgrades/urls.py +++ b/plinth/modules/upgrades/urls.py @@ -15,11 +15,6 @@ urlpatterns = [ re_path(r'^sys/upgrades/firstboot/backports/$', views.BackportsFirstbootView.as_view(), name='backports-firstboot'), - re_path(r'^sys/upgrades/firstboot/update/$', - views.UpdateFirstbootView.as_view(), name='update-firstboot'), - re_path(r'^sys/upgrades/firstboot/update/progress/$', - views.UpdateFirstbootProgressView.as_view(), - name='update-firstboot-progress'), re_path(r'^sys/upgrades/upgrade/$', views.upgrade, name='upgrade'), re_path(r'^sys/upgrades/test-dist-upgrade/$', views.test_dist_upgrade, name='test-dist-upgrade'), diff --git a/plinth/modules/upgrades/views.py b/plinth/modules/upgrades/views.py index fc1d919c8..7488827b6 100644 --- a/plinth/modules/upgrades/views.py +++ b/plinth/modules/upgrades/views.py @@ -9,7 +9,6 @@ from django.http import HttpResponseRedirect from django.shortcuts import redirect from django.urls import reverse_lazy from django.utils.translation import gettext as _ -from django.views.generic import TemplateView from django.views.generic.edit import FormView from plinth import __version__ @@ -18,7 +17,7 @@ from plinth.privileged import packages as packages_privileged from plinth.views import AppView, messages_error from . import privileged -from .forms import BackportsFirstbootForm, ConfigureForm, UpdateFirstbootForm +from .forms import BackportsFirstbootForm, ConfigureForm class UpgradesConfigurationView(AppView): @@ -170,47 +169,6 @@ class BackportsFirstbootView(FormView): return super().form_valid(form) -class UpdateFirstbootView(FormView): - """View to run initial update during first boot wizard.""" - - template_name = 'update-firstboot.html' - form_class = UpdateFirstbootForm - - def __init__(self): - """Define instance attribute.""" - self.update = True - - def get_success_url(self): - """Return next firstboot step.""" - if self.update: - return reverse_lazy('upgrades:update-firstboot-progress') - - return reverse_lazy(first_boot.next_step()) - - def form_valid(self, form): - """Run update if selected, and mark step as done.""" - self.update = form.cleaned_data['update_now'] - if self.update: - privileged.run() - - first_boot.mark_step_done('initial_update') - return super().form_valid(form) - - -class UpdateFirstbootProgressView(TemplateView): - """View to show initial update progress.""" - - template_name = 'update-firstboot-progress.html' - - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) - context['is_busy'] = (_is_updating() - or packages_privileged.is_package_manager_busy()) - context['next_step'] = first_boot.next_step() - context['refresh_page_sec'] = 3 if context['is_busy'] else None - return context - - def test_dist_upgrade(request): """Test dist-upgrade from stable to testing.""" if request.method == 'POST': diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py index 0eee46ef2..57ed0a91e 100644 --- a/plinth/tests/functional/__init__.py +++ b/plinth/tests/functional/__init__.py @@ -354,10 +354,6 @@ def login_with_account(browser, url, username, password=None): if '/firstboot/backports' in browser.url: submit(browser, element=browser.find_by_name('next')[0]) - if '/firstboot/update' in browser.url: - browser.find_by_id('id_update_now').uncheck() - submit(browser, element=browser.find_by_name('next')[0]) - def logout(browser): """Log out of the FreedomBox interface.""" From ed3363105a2991a0545b21643ed121f830778798 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 7 Oct 2024 06:58:49 -0700 Subject: [PATCH 16/56] networks: Remove first boot steps for connectivity/topology - We have not yet implemented the main reason they exist. To guide users to establish reachability with Tor hidden services, Pagekite, Dynamic DNS, etc. - We now have a 'Next steps' page that talks about configuring network connections. The networks page linked from here has these steps prominently listed. - In the future we will implement a wizard for reachability and these steps will still be used. However, they don't have to part of first setup. They can add them as notification and as part of next steps page. - It is good to have a simplified first setup wizard. It is seldom tested properly. Tests: - Run the first setup wizard by removing /var/lib/plinth/plinth.sqlite3 and running the service. Notice that the software update step is not shown and wizard completes successfully. [vexch: Minor quote fix in functional tests] Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/networks/__init__.py | 18 ------ .../internet_connectivity_firstboot.html | 23 ------- .../templates/network_topology_firstboot.html | 24 ------- .../router_configuration_firstboot.html | 24 ------- plinth/modules/networks/urls.py | 9 --- plinth/modules/networks/views.py | 64 +------------------ plinth/tests/functional/__init__.py | 8 +-- 7 files changed, 2 insertions(+), 168 deletions(-) delete mode 100644 plinth/modules/networks/templates/internet_connectivity_firstboot.html delete mode 100644 plinth/modules/networks/templates/network_topology_firstboot.html delete mode 100644 plinth/modules/networks/templates/router_configuration_firstboot.html diff --git a/plinth/modules/networks/__init__.py b/plinth/modules/networks/__init__.py index f71dc5f72..3291e79a9 100644 --- a/plinth/modules/networks/__init__.py +++ b/plinth/modules/networks/__init__.py @@ -15,24 +15,6 @@ from plinth.package import Packages from . import privileged -first_boot_steps = [ - { - 'id': 'network_topology_wizard', - 'url': 'networks:network-topology-first-boot', - 'order': 2, - }, - { - 'id': 'router_setup_wizard', - 'url': 'networks:router-configuration-first-boot', - 'order': 3, - }, - { - 'id': 'internet_connectivity_type_wizard', - 'url': 'networks:internet-connection-type-first-boot', - 'order': 4, - }, -] - _description = [ _('Configure network devices. Connect to the Internet via Ethernet, Wi-Fi ' 'or PPPoE. Share that connection with other devices on the network.'), diff --git a/plinth/modules/networks/templates/internet_connectivity_firstboot.html b/plinth/modules/networks/templates/internet_connectivity_firstboot.html deleted file mode 100644 index 6bd240144..000000000 --- a/plinth/modules/networks/templates/internet_connectivity_firstboot.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "base_firstboot.html" %} -{% comment %} -# SPDX-License-Identifier: AGPL-3.0-or-later -{% endcomment %} - -{% load bootstrap %} -{% load i18n %} -{% load static %} - -{% block content %} - {% include "internet_connectivity_content.html" %} - -
    - {% csrf_token %} - - {{ form|bootstrap }} - - - -
    -{% endblock %} diff --git a/plinth/modules/networks/templates/network_topology_firstboot.html b/plinth/modules/networks/templates/network_topology_firstboot.html deleted file mode 100644 index 4c3eafbb0..000000000 --- a/plinth/modules/networks/templates/network_topology_firstboot.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "base_firstboot.html" %} -{% comment %} -# SPDX-License-Identifier: AGPL-3.0-or-later -{% endcomment %} - -{% load bootstrap %} -{% load i18n %} -{% load static %} - -{% block content %} - {% include "network_topology_content.html" %} - -
    - {% csrf_token %} - - {{ form|bootstrap }} - - - -
    - -{% endblock %} diff --git a/plinth/modules/networks/templates/router_configuration_firstboot.html b/plinth/modules/networks/templates/router_configuration_firstboot.html deleted file mode 100644 index 74877e993..000000000 --- a/plinth/modules/networks/templates/router_configuration_firstboot.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "base_firstboot.html" %} -{% comment %} -# SPDX-License-Identifier: AGPL-3.0-or-later -{% endcomment %} - -{% load bootstrap %} -{% load i18n %} -{% load static %} - -{% block content %} - {% include "router_configuration_content.html" %} - -
    - {% csrf_token %} - - {{ form|bootstrap }} - - - -
    - -{% endblock %} diff --git a/plinth/modules/networks/urls.py b/plinth/modules/networks/urls.py index b32f8f715..f9fc8f72b 100644 --- a/plinth/modules/networks/urls.py +++ b/plinth/modules/networks/urls.py @@ -32,18 +32,9 @@ urlpatterns = [ re_path(r'^sys/networks/router-configuration/$', views.RouterConfigurationView.as_view(), name='router-configuration'), - re_path(r'^sys/networks/firstboot/router-configuration/$', - views.RouterConfigurationFirstBootView.as_view(), - name='router-configuration-first-boot'), re_path(r'^sys/networks/internet-connection-type/$', views.InternetConnectionTypeView.as_view(), name='internet-connection-type'), - re_path(r'^sys/networks/firstboot/internet-connection-type/$', - views.InternetConnectionTypeFirstBootView.as_view(), - name='internet-connection-type-first-boot'), re_path(r'^sys/networks/network-topology/$', views.NetworkTopologyView.as_view(), name='network-topology'), - re_path(r'^sys/networks/firstboot/network-topology-first-boot/$', - views.NetworkTopologyFirstBootView.as_view(), - name='network-topology-first-boot'), ] diff --git a/plinth/modules/networks/views.py b/plinth/modules/networks/views.py index 6acc66ae8..5074b796f 100644 --- a/plinth/modules/networks/views.py +++ b/plinth/modules/networks/views.py @@ -3,7 +3,6 @@ import logging from django.contrib import messages -from django.http import HttpResponseRedirect from django.shortcuts import redirect from django.template.response import TemplateResponse from django.urls import reverse_lazy @@ -13,7 +12,7 @@ from django.views.decorators.http import require_POST from django.views.generic.edit import FormView from plinth import network -from plinth.modules import first_boot, names, networks +from plinth.modules import names, networks from plinth.views import AppView from .forms import (ConnectionTypeSelectForm, EthernetForm, GenericForm, @@ -561,24 +560,6 @@ class NetworkTopologyView(FormView): return super().form_valid(form) -class NetworkTopologyFirstBootView(NetworkTopologyView): - """View for network topology form during first wizard.""" - template_name = 'network_topology_firstboot.html' - - def get_success_url(self): - """Return next fistboot step.""" - return reverse_lazy(first_boot.next_step()) - - def form_valid(self, form): - """Mark the first wizard step as done, save value and redirect.""" - first_boot.mark_step_done('network_topology_wizard') - if 'skip' in form.data: - first_boot.mark_step_done('router_setup_wizard') - return FormView.form_valid(self, form) - - return super().form_valid(form) - - class RouterConfigurationView(FormView): """View for router configuration form.""" template_name = 'router_configuration_update.html' @@ -597,32 +578,6 @@ class RouterConfigurationView(FormView): return super().form_valid(form) -class RouterConfigurationFirstBootView(RouterConfigurationView): - """View for router configuration form during first wizard.""" - template_name = 'router_configuration_firstboot.html' - - def dispatch(self, request, *args, **kwargs): - """Don't show wizard step if FreedomBox is not behind a router.""" - network_topology = networks.get_network_topology_type() - if network_topology != 'to_router': - first_boot.mark_step_done('router_setup_wizard') - return HttpResponseRedirect(reverse_lazy(first_boot.next_step())) - - return super().dispatch(request, *args, *kwargs) - - def get_success_url(self): - """Return the next wizard step after this one.""" - return reverse_lazy(first_boot.next_step()) - - def form_valid(self, form): - """Mark the first wizard step as done, save value and redirect.""" - first_boot.mark_step_done('router_setup_wizard') - if 'skip' in form.data: - return FormView.form_valid(self, form) - - return super().form_valid(form) - - class InternetConnectionTypeView(FormView): """View for Internet connection type form.""" template_name = 'internet_connectivity_type.html' @@ -642,20 +597,3 @@ class InternetConnectionTypeView(FormView): logger.info('Updating internet connectivity type: %s', type_) networks.set_internet_connection_type(type_) return super().form_valid(form) - - -class InternetConnectionTypeFirstBootView(InternetConnectionTypeView): - """View to show Internet connection type form during first wizard.""" - template_name = 'internet_connectivity_firstboot.html' - - def get_success_url(self): - """Return the next wizard step after this one.""" - return reverse_lazy(first_boot.next_step()) - - def form_valid(self, form): - """Mark the first wizard step as done, save value and redirect.""" - first_boot.mark_step_done('internet_connectivity_type_wizard') - if 'skip' in form.data: - return FormView.form_valid(self, form) - - return super().form_valid(form) diff --git a/plinth/tests/functional/__init__.py b/plinth/tests/functional/__init__.py index 57ed0a91e..2d652646d 100644 --- a/plinth/tests/functional/__init__.py +++ b/plinth/tests/functional/__init__.py @@ -345,12 +345,6 @@ def login_with_account(browser, url, username, password=None): browser.visit(base_url + '/plinth/firstboot/welcome') submit(browser, form_class='form-start') # "Start Setup" button _create_admin_account(browser, username, password) - if '/network-topology-first-boot' in browser.url: - submit(browser, element=browser.find_by_name('skip')[0]) - - if '/internet-connection-type' in browser.url: - submit(browser, element=browser.find_by_name('skip')[0]) - if '/firstboot/backports' in browser.url: submit(browser, element=browser.find_by_name('next')[0]) @@ -614,7 +608,7 @@ def networks_set_firewall_zone(browser, zone): 'and contains(@class, "connection-status-label")]/following::a').first network_id = device['href'].split('/')[-3] device.click() - edit_url = "/plinth/sys/networks/{}/edit/".format(network_id) + edit_url = '/plinth/sys/networks/{}/edit/'.format(network_id) browser.links.find_by_href(edit_url).first.click() browser.select('zone', zone) submit(browser, form_class='form-connection-edit') From 7033b7cf1ec9fa462d6a1aa4b64101a4b99354a0 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 7 Oct 2024 11:09:30 -0700 Subject: [PATCH 17/56] upgrades: Show notification to remind user to run updates manually - This is needed as we don't have software updates step during first setup anymore. Tests: - Trigger first setup by removing /var/lib/plinth/plinth.sqlite3 and re-running the service. After completing the setup, a notification is shown with correct severity, title, app icon, message and options. Dismiss remove the notifications. 'Go to Software Updates' takes us to updates app. - After dismissing the notification, re-running the service does not show notification again. - Increasing the app version number also does not show notification again. - Re-running the app setup does not show notification again. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/upgrades/__init__.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/plinth/modules/upgrades/__init__.py b/plinth/modules/upgrades/__init__.py index 34bb13dd0..7b225e177 100644 --- a/plinth/modules/upgrades/__init__.py +++ b/plinth/modules/upgrades/__init__.py @@ -132,6 +132,31 @@ class UpgradesApp(app_module.App): group='admin') note.dismiss(should_dismiss=dismiss) + def _show_first_manual_update_notification(self): + """After first setup, show notification to manually run updates.""" + from plinth.notification import Notification + title = gettext_noop('Run software update manually') + message = gettext_noop( + 'Automatic software update runs daily by default. For the first ' + 'time, manually run it now.') + data = { + 'app_name': 'translate:' + gettext_noop('Software Update'), + 'app_icon': 'fa-refresh' + } + actions = [{ + 'type': 'link', + 'class': 'primary', + 'text': gettext_noop('Go to {app_name}'), + 'url': 'upgrades:index' + }, { + 'type': 'dismiss' + }] + Notification.update_or_create(id='upgrades-first-manual-update', + app_id='upgrades', severity='info', + title=title, message=message, + actions=actions, data=data, + group='admin', dismissed=False) + def setup(self, old_version): """Install and configure the app.""" super().setup(old_version) @@ -140,6 +165,10 @@ class UpgradesApp(app_module.App): if not old_version and not cfg.develop: privileged.enable_auto() + # Request user to run manual update as a one time activity + if not old_version: + self._show_first_manual_update_notification() + # Update apt preferences whenever on first install and on version # increment. privileged.setup() From 35312bd672fdc72761630fe3edd5e06b3f80d46d Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 7 Oct 2024 13:46:46 -0700 Subject: [PATCH 18/56] first_boot: Allow the next steps page to be revisited - Currently, after the user arrives the 'next steps' page after completing the first setup, trying to refresh the page takes us away from the page to the index page. - Since this page lists a lot of steps, user can't be expected to memorize the contents of the page and perform them one after the another. Opening the links in popups instead of navigating away from page helps but not full solve the problem. - If the page is a regular page and not part of the first step wizard, this page is a simple Django page. It can be refreshed. Back button can be used to view the page after navigating from it again. Tests: - On stable and testing containers, remove the sqlite3 file and start the service. This will trigger the first setup wizard. As a last step of the wizard, the 'setup complete! Next steps:' page is shown. - Refreshing the page works. - Navigating away from the page and using the back button to return to it works. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/first_boot/__init__.py | 10 ++-------- plinth/modules/first_boot/middleware.py | 15 ++++++++------- plinth/modules/first_boot/views.py | 12 +----------- 3 files changed, 11 insertions(+), 26 deletions(-) diff --git a/plinth/modules/first_boot/__init__.py b/plinth/modules/first_boot/__init__.py index bf3877710..771de746f 100644 --- a/plinth/modules/first_boot/__init__.py +++ b/plinth/modules/first_boot/__init__.py @@ -19,12 +19,6 @@ first_boot_steps = [ 'url': 'first_boot:welcome', 'order': 0 }, - { - # TODO: Rename this, or merge with 'firstboot_completed'. - 'id': 'firstboot_complete', - 'url': 'first_boot:complete', - 'order': 10 - } ] _all_first_boot_steps = None @@ -96,9 +90,9 @@ def _get_steps(): def next_step(): """Return the resolved next first boot step URL required to go to. - If there are no more step remaining, return index page. + If there are no more step remaining, return 'complete' page. """ - return next_step_or_none() or 'index' + return next_step_or_none() or 'first_boot:complete' def next_step_or_none(): diff --git a/plinth/modules/first_boot/middleware.py b/plinth/modules/first_boot/middleware.py index 2ba122b4a..fee5b2991 100644 --- a/plinth/modules/first_boot/middleware.py +++ b/plinth/modules/first_boot/middleware.py @@ -20,6 +20,7 @@ LOGGER = logging.getLogger(__name__) class FirstBootMiddleware(MiddlewareMixin): """Forward to firstboot page if firstboot isn't finished yet.""" + @staticmethod def process_request(request): """Handle a request as Django middleware request handler.""" @@ -46,11 +47,11 @@ class FirstBootMiddleware(MiddlewareMixin): # If user requests a step other than the welcome step, verify that they # indeed completed the secret verification by looking at the session. - if (user_requests_firstboot and - not request.path.startswith(reverse('first_boot:welcome')) and - first_boot.firstboot_wizard_secret_exists() and - not request.session.get('firstboot_secret_provided', False) and - not is_user_admin(request)): + if (user_requests_firstboot + and not request.path.startswith(reverse('first_boot:welcome')) + and first_boot.firstboot_wizard_secret_exists() + and not request.session.get('firstboot_secret_provided', False) + and not is_user_admin(request)): return HttpResponseRedirect(reverse('first_boot:welcome')) # Redirect to first boot if requesting normal page and first @@ -63,7 +64,7 @@ class FirstBootMiddleware(MiddlewareMixin): # No more steps in first boot first_boot.set_completed() - # Redirect to index page if request firstboot after it is + # Redirect to 'complete' page if user requested firstboot after it is # finished. if firstboot_completed and user_requests_firstboot: - return HttpResponseRedirect(reverse('index')) + return HttpResponseRedirect(reverse('first_boot:complete')) diff --git a/plinth/modules/first_boot/views.py b/plinth/modules/first_boot/views.py index 51ec66145..b66c0adac 100644 --- a/plinth/modules/first_boot/views.py +++ b/plinth/modules/first_boot/views.py @@ -31,22 +31,12 @@ class WelcomeView(FormView): class CompleteView(TemplateView): - """Show summary after all firstboot setup is done. - - After viewing this page the firstboot module can't be accessed anymore. - """ + """Show next steps after all firstboot wizard steps are done.""" template_name = 'firstboot_complete.html' - def get(self, request, *args, **kwargs): - """Mark as done as soon as page is served.""" - response = super().get(self, request, *args, **kwargs) - first_boot.mark_step_done('firstboot_complete') - return response - def get_context_data(self, **kwargs): """Add network connections to context list.""" context = super().get_context_data(**kwargs) context['title'] = _('Setup Complete') - context['firstboot_complete'] = True return context From 7671f4a74956c5a3b548584fa2d54a378f762acf Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 7 Oct 2024 14:20:17 -0700 Subject: [PATCH 19/56] first_boot: Add notification for next steps after first setup - Since there is no way to reach the next steps page from the interface, provide a notification for it. Until the notification is dismissed, the user can reach this page with the notification. Tests: - On testing and stable containers, remove the sqlite file start the service. Complete the first setup wizard. After reaching the 'setup complete' page, notice that there is a notification for next steps to take. Title, icon, message and button text and styling are as expected. - Clicking on 'See next steps' takes us to next steps page. - Clicking on dismiss removes the notification. - Restarting the service does not bring back the notification. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/first_boot/__init__.py | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/plinth/modules/first_boot/__init__.py b/plinth/modules/first_boot/__init__.py index 771de746f..ded007d11 100644 --- a/plinth/modules/first_boot/__init__.py +++ b/plinth/modules/first_boot/__init__.py @@ -8,6 +8,7 @@ import os import sys from django.urls import reverse +from django.utils.translation import gettext_noop from plinth import app as app_module from plinth import cfg @@ -48,8 +49,38 @@ class FirstBootApp(app_module.App): def setup(self, old_version): """Install and configure the app.""" super().setup(old_version) + + if not old_version: + self._show_next_steps_notification() + self.enable() + def _show_next_steps_notification(self): + """After first setup, show notification for next steps.""" + from plinth.notification import Notification + title = gettext_noop('Setup complete! Next steps:') + message = gettext_noop( + 'Initial setup has been completed. Perform the next steps to make ' + 'your {box_name} operational.') + data = { + 'app_name': 'translate:' + gettext_noop('Next steps'), + 'app_icon': 'fa-arrow-right', + 'box_name': 'translate:' + cfg.box_name + } + actions = [{ + 'type': 'link', + 'class': 'primary', + 'text': gettext_noop('See next steps'), + 'url': 'first_boot:complete' + }, { + 'type': 'dismiss' + }] + Notification.update_or_create(id='first-boot-complete', + app_id='first_boot', severity='info', + title=title, message=message, + actions=actions, data=data, + group='admin', dismissed=False) + def _clear_first_boot_steps(sender, module_name, **kwargs): """Flush the cache of first boot steps so it is recreated.""" From 56791df57e2e6226436005a4e2a9aa15211f612f Mon Sep 17 00:00:00 2001 From: Veiko Aasa Date: Thu, 10 Oct 2024 09:42:34 +0300 Subject: [PATCH 20/56] syncthing: Fix app setup in Debian testing Syncthing from Debian testing uses new config directory if the legacy configuration folder doesn't exist. Tests performed in stable and testing containers: - All syncthing tests pass when running twice. Signed-off-by: Veiko Aasa --- plinth/modules/syncthing/manifest.py | 4 ++- plinth/modules/syncthing/privileged.py | 28 +++++++++++++------ .../syncthing/tests/test_functional.py | 1 + 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/plinth/modules/syncthing/manifest.py b/plinth/modules/syncthing/manifest.py index 0c65840bd..82fab9d6c 100644 --- a/plinth/modules/syncthing/manifest.py +++ b/plinth/modules/syncthing/manifest.py @@ -48,7 +48,9 @@ clients = [{ backup = { 'secrets': { - 'directories': ['/var/lib/syncthing/.config'] + 'directories': [ + '/var/lib/syncthing/.config', '/var/lib/syncthing/.local' + ] }, 'services': ['syncthing@syncthing'] } diff --git a/plinth/modules/syncthing/privileged.py b/plinth/modules/syncthing/privileged.py index 8648af17e..f6b505c53 100644 --- a/plinth/modules/syncthing/privileged.py +++ b/plinth/modules/syncthing/privileged.py @@ -14,14 +14,18 @@ from plinth import action_utils from plinth.actions import privileged DATA_DIR = '/var/lib/syncthing' -CONF_FILE = DATA_DIR + '/.config/syncthing/config.xml' +# legacy configuration file +CONF_FILE_LEGACY = DATA_DIR + '/.config/syncthing/config.xml' +# configuration file since Debian Trixie if '.config/syncthing' directory +# doesn't exist +CONF_FILE = DATA_DIR + '/.local/state/syncthing/config.xml' -def augeas_load(): +def augeas_load(conf_file): """Initialize Augeas.""" aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD) - aug.add_transform('Xml.lns', CONF_FILE) + aug.add_transform('Xml.lns', conf_file) aug.load() return aug @@ -54,26 +58,30 @@ def setup(): def setup_config(): """Make configuration changes.""" # wait until the configuration file is created by the syncthing daemon + conf_file_in_use = CONF_FILE timeout = 300 while timeout > 0: - if os.path.exists(CONF_FILE): + if os.path.exists(CONF_FILE_LEGACY): + conf_file_in_use = CONF_FILE_LEGACY + break + elif os.path.exists(CONF_FILE): break timeout = timeout - 1 time.sleep(1) - aug = augeas_load() + aug = augeas_load(conf_file_in_use) # disable authentication missing notification as FreedomBox itself # provides authentication auth_conf = ('/configuration/options/unackedNotificationID' '[#text="authenticationUserAndPassword"]') - conf_changed = bool(aug.remove('/files' + CONF_FILE + auth_conf)) + conf_changed = bool(aug.remove('/files' + conf_file_in_use + auth_conf)) # disable usage reporting notification by declining reporting # if the user has not made a choice yet usage_conf = '/configuration/options/urAccepted/#text' - if aug.get('/files' + CONF_FILE + usage_conf) == '0': - aug.set('/files' + CONF_FILE + usage_conf, '-1') + if aug.get('/files' + conf_file_in_use + usage_conf) == '0': + aug.set('/files' + conf_file_in_use + usage_conf, '-1') conf_changed = True aug.save() @@ -84,5 +92,7 @@ def setup_config(): @privileged def uninstall(): - """Remove configuration file when app is uninstalled.""" + """Remove configuration directory when app is uninstalled.""" + # legacy location shutil.rmtree(DATA_DIR + '/.config', ignore_errors=True) + shutil.rmtree(DATA_DIR + '/.local', ignore_errors=True) diff --git a/plinth/modules/syncthing/tests/test_functional.py b/plinth/modules/syncthing/tests/test_functional.py index e4aa90ef7..8d1804089 100644 --- a/plinth/modules/syncthing/tests/test_functional.py +++ b/plinth/modules/syncthing/tests/test_functional.py @@ -48,6 +48,7 @@ class TestSyncthingApp(functional.BaseAppTests): 'test_syncthing') _remove_folder(session_browser, 'Test') + time.sleep(1) # Helps with browsing away in next step functional.backup_restore(session_browser, self.app_name, 'test_syncthing') From fc86f3e5079cd437bb3364e4e2eba36292a7fe15 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 10 Oct 2024 13:30:23 -0700 Subject: [PATCH 21/56] wordpress: tests: functional: Fix tests on Trixie - In versions of WordPress in Debian Trixie and up the editing widget is inside of an iframe instead of as a direct child of the main document. Elements inside these iframes can't be queried directly and one must be the 'context' of the iframe before querying elements inside. - Fix the failures by using the splinter API to query inside iframe. Tests: - Run functional tests on WordPress in stable and testing containers twice. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/wordpress/tests/test_functional.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plinth/modules/wordpress/tests/test_functional.py b/plinth/modules/wordpress/tests/test_functional.py index 0042ae427..040e7337a 100644 --- a/plinth/modules/wordpress/tests/test_functional.py +++ b/plinth/modules/wordpress/tests/test_functional.py @@ -143,7 +143,11 @@ def _write_post(browser, title): if browser.find_by_id('post-title-0'): browser.find_by_id('post-title-0').fill(title) else: - browser.find_by_css('.editor-post-title').first.type(title) + if browser.find_by_css('.editor-visual-editor.is-iframed'): + with browser.get_iframe('editor-canvas') as iframe: + iframe.find_by_css('.editor-post-title').first.type(title) + else: + browser.find_by_css('.editor-post-title').first.type(title) browser.find_by_css('.editor-post-publish-button__button')[0].click() functional.eventually(browser.find_by_css, ['.editor-post-publish-button']) From 4bc13f063f055dd54c0f39fe88eba827fc605021 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 10 Oct 2024 15:48:30 -0700 Subject: [PATCH 22/56] calibre: tests: functional: Fix occasional failure in add book test - When a library is added to using the FreedomBox interface and immediately Calibre interface is loaded, the library does not immediately get listed in the list of libraries. We will have to fresh the page to see the new library. Do this. Tests: - Run functional tests for calibre on Testing distribution multiple times without failures. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/calibre/tests/test_functional.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/plinth/modules/calibre/tests/test_functional.py b/plinth/modules/calibre/tests/test_functional.py index 6ddd7049f..d658ceecf 100644 --- a/plinth/modules/calibre/tests/test_functional.py +++ b/plinth/modules/calibre/tests/test_functional.py @@ -105,8 +105,17 @@ def _visit_library(browser, name): functional.eventually(_service_available) - functional.eventually(browser.find_by_css, - args=[f'.calibre-push-button[data-lid="{name}"]']) + def _library_available(): + """Refresh until the expected library is available.""" + available = browser.find_by_css( + f'.calibre-push-button[data-lid="{name}"]') + if not available: + time.sleep(0.5) + functional.visit(browser, '/calibre') + + return available + + functional.eventually(_library_available) link = browser.find_by_css(f'.calibre-push-button[data-lid="{name}"]') if not link: raise ValueError('Library not found') From 770ec09557c7aef4b7642d1e9ed26791a8157c46 Mon Sep 17 00:00:00 2001 From: Veiko Aasa Date: Sat, 12 Oct 2024 20:28:38 +0300 Subject: [PATCH 23/56] ssh: Start server after nslcd service Fixes an issue where SSH server is available but users can't login because LDAP user services are not yet started. Tests performed: - Installed new ssh systemd override conf, rebooted, ensured that the sshd service starts after the nslcd service. Relates to #2452. Signed-off-by: Veiko Aasa Reviewed-by: Sunil Mohan Adapa --- .../data/usr/lib/systemd/system/ssh.service.d/freedombox.conf | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 plinth/modules/ssh/data/usr/lib/systemd/system/ssh.service.d/freedombox.conf diff --git a/plinth/modules/ssh/data/usr/lib/systemd/system/ssh.service.d/freedombox.conf b/plinth/modules/ssh/data/usr/lib/systemd/system/ssh.service.d/freedombox.conf new file mode 100644 index 000000000..5a53f2118 --- /dev/null +++ b/plinth/modules/ssh/data/usr/lib/systemd/system/ssh.service.d/freedombox.conf @@ -0,0 +1,3 @@ +[Unit] +# The service depends on users and groups defined in LDAP +After=nslcd.service From 56a055639d4a3b4c104fc36f9a207b600f59a993 Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Wed, 9 Oct 2024 20:48:04 +0530 Subject: [PATCH 24/56] backups: Use new utility for handling file uploads - Use dedicated directory for uploads - Uploaded backup archives are owned by root and read-only (0o600) Signed-off-by: Joseph Nuthalapati [sunil: Fix checking the relativeness of file path before removing] [sunil: Create backups upload path recursively] Signed-off-by: Sunil Mohan Adapa Reviewed-by: Sunil Mohan Adapa --- plinth/modules/backups/decorators.py | 7 +++---- plinth/modules/backups/privileged.py | 20 ++++++++++++++++++++ plinth/modules/backups/views.py | 13 +++++++------ 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/plinth/modules/backups/decorators.py b/plinth/modules/backups/decorators.py index fd500457e..90a34920c 100644 --- a/plinth/modules/backups/decorators.py +++ b/plinth/modules/backups/decorators.py @@ -4,9 +4,8 @@ Decorators for the backup views. """ import functools -import os -from . import SESSION_PATH_VARIABLE +from . import privileged, SESSION_PATH_VARIABLE def delete_tmp_backup_file(function): @@ -15,12 +14,12 @@ def delete_tmp_backup_file(function): XXX: Implement a better way to delete uploaded files. """ + @functools.wraps(function) def wrapper(request, *args, **kwargs): path = request.session.get(SESSION_PATH_VARIABLE, None) if path: - if os.path.isfile(path): - os.remove(path) + privileged.remove_uploaded_archive(path) del request.session[SESSION_PATH_VARIABLE] return function(request, *args, **kwargs) diff --git a/plinth/modules/backups/privileged.py b/plinth/modules/backups/privileged.py index aa2f3298e..be0f2558a 100644 --- a/plinth/modules/backups/privileged.py +++ b/plinth/modules/backups/privileged.py @@ -8,11 +8,13 @@ import re import subprocess import tarfile +from plinth import action_utils from plinth.actions import privileged, secret_str from plinth.utils import Version TIMEOUT = 30 BACKUPS_DATA_PATH = pathlib.Path('/var/lib/plinth/backups-data/') +BACKUPS_UPLOAD_PATH = pathlib.Path('/var/lib/freedombox/backups-upload/') MANIFESTS_FOLDER = '/var/lib/plinth/backups-manifests/' @@ -143,6 +145,24 @@ def list_repo(path: str, return json.loads(process.stdout.decode()) +@privileged +def add_uploaded_archive(file_name: str, temporary_file_path: str): + """Store an archive uploaded by the user.""" + BACKUPS_UPLOAD_PATH.mkdir(parents=True, exist_ok=True) + action_utils.move_uploaded_file(temporary_file_path, BACKUPS_UPLOAD_PATH, + file_name, allow_overwrite=True, + permissions=0o600) + + +@privileged +def remove_uploaded_archive(file_path: str): + """Delete the archive uploaded by the user.""" + resolved_file_path = pathlib.Path(file_path).resolve() + if (resolved_file_path.is_relative_to(BACKUPS_UPLOAD_PATH) + and resolved_file_path.is_file()): + resolved_file_path.unlink() + + def _get_borg_version(): """Return the version of borgbackup.""" process = _run(['borg', '--version'], stdout=subprocess.PIPE) diff --git a/plinth/modules/backups/views.py b/plinth/modules/backups/views.py index 2dc0e7407..ba4780ffb 100644 --- a/plinth/modules/backups/views.py +++ b/plinth/modules/backups/views.py @@ -5,7 +5,6 @@ Views for the backups app. import logging import os -import tempfile from datetime import datetime from urllib.parse import unquote @@ -190,11 +189,13 @@ class UploadArchiveView(SuccessMessageMixin, FormView): return context def form_valid(self, form): - """store uploaded file.""" - with tempfile.NamedTemporaryFile(delete=False) as tmp_file: - self.request.session[SESSION_PATH_VARIABLE] = tmp_file.name - for chunk in self.request.FILES['backups-file'].chunks(): - tmp_file.write(chunk) + """Store uploaded file.""" + uploaded_file = self.request.FILES['backups-file'] + # Hold on to Django's uploaded file. It will be used by other views. + privileged.add_uploaded_archive(uploaded_file.name, + uploaded_file.temporary_file_path()) + self.request.session[SESSION_PATH_VARIABLE] = str( + privileged.BACKUPS_UPLOAD_PATH / uploaded_file.name) return super().form_valid(form) From 60c6fd4d271acaaa5aea3dc95076bb973fa6ee82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Podhoreck=C3=BD?= Date: Sun, 13 Oct 2024 21:13:41 +0000 Subject: [PATCH 25/56] Translated using Weblate (Czech) Currently translated at 100.0% (1678 of 1678 strings) --- plinth/locale/cs/LC_MESSAGES/django.po | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/plinth/locale/cs/LC_MESSAGES/django.po b/plinth/locale/cs/LC_MESSAGES/django.po index cc2ce0022..5e7bca0ba 100644 --- a/plinth/locale/cs/LC_MESSAGES/django.po +++ b/plinth/locale/cs/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-10-07 20:05-0400\n" -"PO-Revision-Date: 2024-10-01 10:18+0000\n" +"PO-Revision-Date: 2024-10-14 21:15+0000\n" "Last-Translator: Jiří Podhorecký \n" "Language-Team: Czech \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 5.8-dev\n" +"X-Generator: Weblate 5.8-rc\n" #: config.py:103 #, python-brace-format @@ -2353,10 +2353,9 @@ msgid "Failed to add wiki file." msgstr "Soubor wiki se nepodařilo přidat." #: modules/featherwiki/views.py:138 modules/tiddlywiki/views.py:139 -#, fuzzy, python-brace-format -#| msgid "Could not delete {name}: {error}" +#, python-brace-format msgid "Could not delete {name}" -msgstr "{name} se nepodařilo smazat: {error}" +msgstr "{name} se nepodařilo smazat" #: modules/firewall/__init__.py:25 #, python-brace-format @@ -3426,10 +3425,8 @@ msgid "Add a new content package" msgstr "Přidat nový balíček obsahu" #: modules/kiwix/views.py:76 -#, fuzzy -#| msgid "Content package added." msgid "Content package already exists." -msgstr "Přidán obsahový balíček." +msgstr "Obsahový balíček již existuje." #: modules/kiwix/views.py:79 msgid "Failed to add content package." @@ -4283,7 +4280,7 @@ msgstr "Jmenné služby" #: modules/names/__init__.py:171 msgid "Package systemd-resolved is installed" -msgstr "" +msgstr "Balíček systemd-resolved je nainstalován" #: modules/names/__init__.py:195 #, python-brace-format @@ -4425,6 +4422,8 @@ msgid "" "systemd-resolved package is not installed. Install it for additional " "functionality." msgstr "" +"balíček systemd-resolved není nainstalován. Nainstalujte jej, abyste získali " +"další funkce." #: modules/names/templates/names.html:121 templates/setup.html:66 msgid "Install" From 3742ab8f6b52d57685e9febc9ba792e02702f10c Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 11 Oct 2024 08:12:34 -0700 Subject: [PATCH 26/56] ci: Generalize script to update container, switch to podman Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- .ci/update-container-image.sh | 11 +++++++++++ .ci/update-gitlabci-docker-image.sh | 8 -------- 2 files changed, 11 insertions(+), 8 deletions(-) create mode 100755 .ci/update-container-image.sh delete mode 100755 .ci/update-gitlabci-docker-image.sh diff --git a/.ci/update-container-image.sh b/.ci/update-container-image.sh new file mode 100755 index 000000000..edcb44c69 --- /dev/null +++ b/.ci/update-container-image.sh @@ -0,0 +1,11 @@ +#! /bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later + +CONTAINER=$1 +CONTAINER="${CONTAINER:-gitlabci}" + +podman login registry.salsa.debian.org + +# Build and upload a new image to the container registry +podman build -t registry.salsa.debian.org/freedombox-team/freedombox:${CONTAINER} -f .ci/Dockerfile.${CONTAINER} . +podman push registry.salsa.debian.org/freedombox-team/freedombox:${CONTAINER} diff --git a/.ci/update-gitlabci-docker-image.sh b/.ci/update-gitlabci-docker-image.sh deleted file mode 100755 index 9ada7724d..000000000 --- a/.ci/update-gitlabci-docker-image.sh +++ /dev/null @@ -1,8 +0,0 @@ -#! /bin/bash -# SPDX-License-Identifier: AGPL-3.0-or-later - -docker login registry.salsa.debian.org - -# Build and upload a new image to the container registry -DOCKER_BUILDKIT=1 docker build -t registry.salsa.debian.org/freedombox-team/freedombox:gitlabci -f .ci/Dockerfile.gitlabci . -docker push registry.salsa.debian.org/freedombox-team/freedombox:gitlabci From ecc03e2d3bd77312a16cc440e9d8b77d33b94fa6 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 11 Oct 2024 11:34:28 -0700 Subject: [PATCH 27/56] ci: Dockerfile: Drop obsolete dependency on pytest-bdd Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- .ci/Dockerfile.gitlabci | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/Dockerfile.gitlabci b/.ci/Dockerfile.gitlabci index e89f81c2f..a4b4c6b9d 100644 --- a/.ci/Dockerfile.gitlabci +++ b/.ci/Dockerfile.gitlabci @@ -25,4 +25,4 @@ RUN apt-get install -y build-essential \ RUN apt-get install -y $(./run --list-dependencies) # Coverage should know that test_functional.py files are tests -RUN pip3 install --break-system-packages splinter pytest-bdd +RUN pip3 install --break-system-packages splinter From f5ff0a6cf52b4eabc41c2a7570b57f0c1a858a34 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 11 Oct 2024 15:57:39 -0700 Subject: [PATCH 28/56] ci: Rename Dockerfiles to Containerfiles Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- .ci/{Dockerfile.gitlabci => Containerfile.gitlabci} | 0 ...itlabci.dockerignore => Containerfile.gitlabci.dockerignore} | 0 .ci/update-container-image.sh | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename .ci/{Dockerfile.gitlabci => Containerfile.gitlabci} (100%) rename .ci/{Dockerfile.gitlabci.dockerignore => Containerfile.gitlabci.dockerignore} (100%) diff --git a/.ci/Dockerfile.gitlabci b/.ci/Containerfile.gitlabci similarity index 100% rename from .ci/Dockerfile.gitlabci rename to .ci/Containerfile.gitlabci diff --git a/.ci/Dockerfile.gitlabci.dockerignore b/.ci/Containerfile.gitlabci.dockerignore similarity index 100% rename from .ci/Dockerfile.gitlabci.dockerignore rename to .ci/Containerfile.gitlabci.dockerignore diff --git a/.ci/update-container-image.sh b/.ci/update-container-image.sh index edcb44c69..df541dff0 100755 --- a/.ci/update-container-image.sh +++ b/.ci/update-container-image.sh @@ -7,5 +7,5 @@ CONTAINER="${CONTAINER:-gitlabci}" podman login registry.salsa.debian.org # Build and upload a new image to the container registry -podman build -t registry.salsa.debian.org/freedombox-team/freedombox:${CONTAINER} -f .ci/Dockerfile.${CONTAINER} . +podman build -t registry.salsa.debian.org/freedombox-team/freedombox:${CONTAINER} -f .ci/Containerfile.${CONTAINER} . podman push registry.salsa.debian.org/freedombox-team/freedombox:${CONTAINER} From 82f4b70999c3df0abce7f834eebc6078b85e9119 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 11 Oct 2024 14:22:43 -0700 Subject: [PATCH 29/56] ci: Add docker container for functional-tests:stable Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- .ci/Containerfile.functional-tests-stable | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .ci/Containerfile.functional-tests-stable diff --git a/.ci/Containerfile.functional-tests-stable b/.ci/Containerfile.functional-tests-stable new file mode 100644 index 000000000..a3cf73997 --- /dev/null +++ b/.ci/Containerfile.functional-tests-stable @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later + +FROM debian:stable + +CMD /sbin/init +USER root:root + +ENV DEBIAN_FRONTEND=noninteractive + +RUN echo "deb http://deb.debian.org/debian bookworm-backports main" > /etc/apt/sources.list.d/backports.list; echo "deb-src http://deb.debian.org/debian bookworm-backports main" >> /etc/apt/sources.list.d/backports.list + +RUN apt-get update + +# Update all packages +RUN apt-get dist-upgrade -y + +# Install freedombox package so that plint:plinth user/group are created etc. +RUN apt-get install -y freedombox/bookworm-backports +RUN systemctl mask plinth.service + +# Don't ask for the secret in first wizard +RUN rm -f /var/lib/plinth/firstboot-wizard-secret + +# Dependencies of the freedombox Debian package +RUN apt-get build-dep -y freedombox/bookworm-backports + +# Build and test dependencies +RUN apt-get install -y \ + build-essential \ + parted \ + sshpass \ + wget + + +# Install functional test dependencies +ADD https://salsa.debian.org/freedombox-team/freedombox/-/raw/main/plinth/tests/functional/install.sh /usr/src/install.sh +RUN bash /usr/src/install.sh; rm -f /usr/src/install.sh + +# Allow daemons to start when container is started +RUN rm -f /usr/sbin/policy-rc.d From 2e25bcac4f45deac8e5d03b0aab1c788f1184f6b Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 14 Oct 2024 08:09:25 -0700 Subject: [PATCH 30/56] ci: Add gitlab runner configuration - Useful for setting up a new gitlab runner that helps in running functional tests. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- .ci/gitlab-runner/config.toml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .ci/gitlab-runner/config.toml diff --git a/.ci/gitlab-runner/config.toml b/.ci/gitlab-runner/config.toml new file mode 100644 index 000000000..35d887456 --- /dev/null +++ b/.ci/gitlab-runner/config.toml @@ -0,0 +1,21 @@ +concurrent = 1 +check_interval = 0 + +[[runners]] + name = "freedombox-functional" + url = "https://salsa.debian.org" + token = "" + executor = "custom" + builds_dir = "/freedombox" + cache_dir = "/cache" + [runners.custom] + prepare_exec = "/var/lib/fbx-functional/bin/prepare.sh" + prepare_exec_timeout = 1200 + + run_exec = "/var/lib/fbx-functional/bin/run.sh" + + cleanup_exec = "/var/lib/fbx-functional/bin/cleanup.sh" + cleanup_exec_timeout = 1200 + + graceful_kill_timeout = 200 + force_kill_timeout = 200 From 61fde67ba6b1ade35388a3673513dbf46a95c3ca Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 11 Oct 2024 19:40:21 -0700 Subject: [PATCH 31/56] ci: Add a custom driver for gitlab runner for podman Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- .ci/gitlab-runner/custom-driver/README | 7 ++++ .ci/gitlab-runner/custom-driver/base.sh | 4 ++ .ci/gitlab-runner/custom-driver/cleanup.sh | 10 +++++ .ci/gitlab-runner/custom-driver/prepare.sh | 48 ++++++++++++++++++++++ .ci/gitlab-runner/custom-driver/run.sh | 14 +++++++ 5 files changed, 83 insertions(+) create mode 100644 .ci/gitlab-runner/custom-driver/README create mode 100644 .ci/gitlab-runner/custom-driver/base.sh create mode 100755 .ci/gitlab-runner/custom-driver/cleanup.sh create mode 100755 .ci/gitlab-runner/custom-driver/prepare.sh create mode 100755 .ci/gitlab-runner/custom-driver/run.sh diff --git a/.ci/gitlab-runner/custom-driver/README b/.ci/gitlab-runner/custom-driver/README new file mode 100644 index 000000000..91c8823f7 --- /dev/null +++ b/.ci/gitlab-runner/custom-driver/README @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later + +This directory contains a custom driver for Gitlab-CI Runner. This is used to +run functional tests. + +Based on https://docs.gitlab.com/runner/executors/custom_examples/lxd.html under +Expat license. diff --git a/.ci/gitlab-runner/custom-driver/base.sh b/.ci/gitlab-runner/custom-driver/base.sh new file mode 100644 index 000000000..c4618898d --- /dev/null +++ b/.ci/gitlab-runner/custom-driver/base.sh @@ -0,0 +1,4 @@ +#!/usr/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later + +CONTAINER_ID="runner-$CUSTOM_ENV_CI_RUNNER_ID-project-$CUSTOM_ENV_CI_PROJECT_ID-concurrent-$CUSTOM_ENV_CI_CONCURRENT_PROJECT_ID-$CUSTOM_ENV_CI_JOB_ID" diff --git a/.ci/gitlab-runner/custom-driver/cleanup.sh b/.ci/gitlab-runner/custom-driver/cleanup.sh new file mode 100755 index 000000000..5094ef44a --- /dev/null +++ b/.ci/gitlab-runner/custom-driver/cleanup.sh @@ -0,0 +1,10 @@ +#!/usr/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later + +current_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +source ${current_dir}/base.sh # Get variables from base. + +echo "Deleting container $CONTAINER_ID" + +podman container stop "$CONTAINER_ID" +podman container rm -f "$CONTAINER_ID" diff --git a/.ci/gitlab-runner/custom-driver/prepare.sh b/.ci/gitlab-runner/custom-driver/prepare.sh new file mode 100755 index 000000000..e8fcbbeb3 --- /dev/null +++ b/.ci/gitlab-runner/custom-driver/prepare.sh @@ -0,0 +1,48 @@ +#!/usr/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later + +current_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +source ${current_dir}/base.sh # Get variables from base. + +set -eo pipefail + +# trap any error, and mark it as a system failure. +trap "exit $SYSTEM_FAILURE_EXIT_CODE" ERR + +start_container () { + if podman container exists "$CONTAINER_ID" ; then + echo 'Found old container, deleting' + podman container stop "$CONTAINER_ID" + podman container rm -f "$CONTAINER_ID" + fi + + podman pull registry.salsa.debian.org/freedombox-team/freedombox:functional-tests-stable + podman run --name "$CONTAINER_ID" --systemd=always \ + --privileged \ + --cap-add=SYS_ADMIN --cap-add=NET_ADMIN --cap-add=MKNOD \ + --detach registry.salsa.debian.org/freedombox-team/freedombox:functional-tests-stable /sbin/init + + if podman exec "$CONTAINER_ID" systemctl is-system-running --wait; then + echo 'Container started.' + else + echo 'Container started degraded.' + fi +} + +install_dependencies () { + podman exec "$CONTAINER_ID" /usr/bin/bash < /etc/apt/preferences.d/unstable +echo 'Pin: release a=unstable' >> /etc/apt/preferences.d/unstable +echo 'Pin-Priority: 400' >> /etc/apt/preferences.d/unstable +echo 'deb http://deb.debian.org/debian unstable main' > /etc/apt/sources.list.d/unstable.list +apt-get update +DEBIAN_FRONTEND=noninteractive apt-get install -y gitlab-runner git-lfs +EOF +} + +echo "Running in $CONTAINER_ID" + +start_container + +install_dependencies diff --git a/.ci/gitlab-runner/custom-driver/run.sh b/.ci/gitlab-runner/custom-driver/run.sh new file mode 100755 index 000000000..0950d4699 --- /dev/null +++ b/.ci/gitlab-runner/custom-driver/run.sh @@ -0,0 +1,14 @@ +#!/usr/bin/bash +# SPDX-License-Identifier: AGPL-3.0-or-later + +set -eo pipefail + +current_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +source ${current_dir}/base.sh # Get variables from base. + +podman exec --interactive "$CONTAINER_ID" /bin/bash < "${1}" +if [ $? -ne 0 ]; then + # Exit using the variable, to make the build as failure in GitLab + # CI. + exit $BUILD_FAILURE_EXIT_CODE +fi From 72ffaa62a0246eb5a0f6b16be8702234bac28fde Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 11 Oct 2024 08:06:53 -0700 Subject: [PATCH 32/56] .gitlab-ci.yml: Update for new infrastructure Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- .ci/functional-tests.yml | 63 ++++++++-------------------------------- .gitlab-ci.yml | 56 +---------------------------------- 2 files changed, 13 insertions(+), 106 deletions(-) diff --git a/.ci/functional-tests.yml b/.ci/functional-tests.yml index 2e839574a..c004a6926 100644 --- a/.ci/functional-tests.yml +++ b/.ci/functional-tests.yml @@ -1,66 +1,27 @@ --- # SPDX-License-Identifier: AGPL-3.0-or-later -.app-server: - stage: functional-tests - dependencies: [] - except: - - $GITLAB_USER_LOGIN == "weblate" - script: - - BUILD_JOB_ID=$(curl -s "https://salsa.debian.org/api/v4/projects/$CI_PROJECT_ID/pipelines/$CI_PIPELINE_ID/jobs?scope[]=success" | jq -r '.[] | select(.name==env.BUILD_JOB_NAME) | .id') - - export AWS_DEFAULT_REGION=us-east-1 - - | - aws lambda invoke --function-name launch_app_server --payload '{"launch_template_name": "'"$LAUNCH_TEMPLATE_NAME"'", "instance_name": "'"$INSTANCE_NAME"'", "ci_project_id": "'"$CI_PROJECT_ID"'", "build_job_id": "'"$BUILD_JOB_ID"'"}' response.json - - echo "APP_SERVER_IP_1=$(jq -r '.app_server_ip' response.json)" >> app-servers.env - - echo "INSTANCE_ID_1=$(jq -r '.instance_id' response.json)" >> app-servers.env - - | - aws lambda invoke --function-name launch_app_server --payload '{"launch_template_name": "'"$LAUNCH_TEMPLATE_NAME"'", "instance_name": "'"$INSTANCE_NAME"'", "ci_project_id": "'"$CI_PROJECT_ID"'", "build_job_id": "'"$BUILD_JOB_ID"'"}' response.json - - echo "APP_SERVER_IP_2=$(jq -r '.app_server_ip' response.json)" >> app-servers.env - - echo "INSTANCE_ID_2=$(jq -r '.instance_id' response.json)" >> app-servers.env - tags: - - functional-tests - artifacts: - reports: - dotenv: app-servers.env - .run-functional-tests: stage: functional-tests - timeout: 3h + needs: [] + dependencies: [] + tags: + - functional-tests + timeout: 6h # Need to find another way of running the cleanup step even on failure allow_failure: true - when: delayed - # Wait for the app-server to come up. Saves time for the CI runners. - start_in: 3 minutes + except: + - $GITLAB_USER_LOGIN == "weblate" before_script: - apt-get update - - apt-get install -y sudo curl wget - - ./plinth/tests/functional/install.sh - - adduser tester --gecos "First Last,RoomNumber,WorkPhone,HomePhone" --disabled-password && echo "tester:password" | chpasswd + - sudo -u plinth ./run --develop > plinth.log 2>&1 & + - 'echo -e "Cmnd_Alias FREEDOMBOX_ACTION_DEV = /usr/share/plinth/actions/actions, `pwd`/actions/actions\nDefaults!FREEDOMBOX_ACTION_DEV closefrom_override\nplinth ALL=(ALL:ALL) NOPASSWD:SETENV : FREEDOMBOX_ACTION_DEV\n" > /etc/sudoers.d/01-freedombox-development' + - while ! grep -q "Setup thread finished" plinth.log; do sleep 1; echo -n .; done script: - - cp -r . /home/tester/freedombox && chown -R tester:tester /home/tester/freedombox - - | - sudo APP_SERVER_URL_1="https://$APP_SERVER_IP_1" APP_SERVER_URL_2="https://$APP_SERVER_IP_2" -u tester bash -c \ - 'cd /home/tester/freedombox && py.test-3 -v --durations=10 --include-functional --splinter-headless -n 2 --dist=loadscope --template=html1/index.html --report=functional-tests.html' - after_script: - - echo "INSTANCE_ID_1=$INSTANCE_ID_1" >> app-servers.env - - echo "INSTANCE_ID_2=$INSTANCE_ID_2" >> app-servers.env - - cp /home/tester/freedombox/functional-tests.html . - - cp -r /home/tester/freedombox/screenshots/ . + - FREDOMBOX_URL=https://localhost FREEDOMBOX_SSH_PORT=22 FREEDOMBOX_SAMBA_PORT=445 pytest -v --durations=10 --include-functional --splinter-headless --template=html1/index.html --report=functional-tests.html artifacts: when: always - reports: - dotenv: app-servers.env paths: - functional-tests.html - screenshots/ - -# Does not run if the previous job times out or is cancelled -.terminate-app-server: - stage: functional-tests - script: - - export AWS_DEFAULT_REGION=us-east-1 - - | - aws lambda invoke --function-name terminate_app_server --payload '{"instance_id": "'"$INSTANCE_ID_1"'"}' response.json - aws lambda invoke --function-name terminate_app_server --payload '{"instance_id": "'"$INSTANCE_ID_2"'"}' response.json - tags: - - functional-tests + - plinth.log diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 14380bdfa..ea70cd905 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -47,63 +47,9 @@ doc-tests: script: - make check-doc -.app-server-stable: - variables: - LAUNCH_TEMPLATE_NAME: functional-tests-stable - INSTANCE_NAME: app-server-stable - BUILD_JOB_NAME: build-backports - extends: .app-server - -.run-functional-tests-stable: - needs: - - job: app-server-stable - artifacts: true +run-functional-tests-stable: extends: .run-functional-tests -.terminate-app-server-stable: - needs: - - job: run-functional-tests-stable - artifacts: true - extends: .terminate-app-server - -.app-server-testing: - variables: - LAUNCH_TEMPLATE_NAME: functional-tests-testing - INSTANCE_NAME: app-server-testing - BUILD_JOB_NAME: build - extends: .app-server - -.run-functional-tests-testing: - needs: - - job: app-server-testing - artifacts: true - extends: .run-functional-tests - -.terminate-app-server-testing: - needs: - - job: run-functional-tests-testing - artifacts: true - extends: .terminate-app-server - -.app-server-unstable: - variables: - LAUNCH_TEMPLATE_NAME: functional-tests-unstable - INSTANCE_NAME: app-server-unstable - BUILD_JOB_NAME: build - extends: .app-server - -.run-functional-tests-unstable: - needs: - - job: app-server-unstable - artifacts: true - extends: .run-functional-tests - -.terminate-app-server-unstable: - needs: - - job: run-functional-tests-unstable - artifacts: true - extends: .terminate-app-server - extract-source: extends: .provisioning-extract-source From e2ae29acb2f6c78ff4240e863c898046b06cbcaa Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 15 Oct 2024 21:49:34 -0700 Subject: [PATCH 33/56] ci: Update functional test timeout to 10h Signed-off-by: Sunil Mohan Adapa --- .ci/functional-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/functional-tests.yml b/.ci/functional-tests.yml index c004a6926..51f55f84c 100644 --- a/.ci/functional-tests.yml +++ b/.ci/functional-tests.yml @@ -7,7 +7,7 @@ dependencies: [] tags: - functional-tests - timeout: 6h + timeout: 10h # Need to find another way of running the cleanup step even on failure allow_failure: true except: From e5b7ed4fafffb56a7d43d0663977e5cd5d30ce0b Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Fri, 20 Sep 2024 15:49:45 +0530 Subject: [PATCH 34/56] *: Implements tags for apps - Add tags to Info component of apps. Use only English tags for all operations. Localized tags are used for presentation to the user only. Add tags to all the apps. Conventions (English): 1. Tags describing use cases should be in kebab case. 2. Protocols in tag names should be in their canonical format. 3. Tags needn't be 100% technically correct. This can get in the way of comparing apps using a tag. Words that describe use cases that users can easily understand should be preferred over being pedantic. 4. Tags should be short, ideally not more than 2 words. Avoid conjunctions like "and", "or" in tags. 5. Avoid redundant words like "server", or "web-clients". Most apps on FreedomBox are either servers or web clients. 6. Keep your nouns singular in tags. - Use query params to filter the Apps page by tags. When all tags are removed, redirect to /apps. - Add UI elements to add and remove tag filters in the Apps page. Make the UI similar to GitLab issue tags. Since there are 40 apps, there will be at least 40 tags. Selecting a tag from a dropdown will be difficult on mobile devices. A fuzzy search is useful to find tags to add to the filter. Allow user to find the best match for the search term and highlight it visually. The user can then press Enter to select the highlighted tag. Make tag search case-insensitive. Make the dropdown menu scrollable with a fixed size. User input is debounced by 300 ms during search. - tests: Add missing mock in test_module_loader.py - Add functional tests [sunil] - 'list' can be used instead of 'List' for typing in recent Python versions. - Reserve tripe-quoted strings for docstrings. - Undo some changes in module initialization, use module_name for logging errors. - isort and yapf changes. - Encode parameters before adding them to the URL. Tests: - Tested the functionality of filtering by tag with one tag and two tags. Signed-off-by: Joseph Nuthalapati Signed-off-by: Sunil Mohan Adapa Reviewed-by: Sunil Mohan Adapa --- plinth/app.py | 21 ++- plinth/module_loader.py | 7 +- plinth/modules/bepasty/__init__.py | 2 +- plinth/modules/bepasty/manifest.py | 2 + plinth/modules/calibre/__init__.py | 2 +- plinth/modules/calibre/manifest.py | 2 + plinth/modules/coturn/__init__.py | 3 +- plinth/modules/coturn/manifest.py | 4 + plinth/modules/deluge/__init__.py | 3 +- plinth/modules/deluge/manifest.py | 2 + plinth/modules/ejabberd/__init__.py | 12 +- plinth/modules/ejabberd/manifest.py | 9 + plinth/modules/email/__init__.py | 2 +- plinth/modules/email/manifest.py | 2 + plinth/modules/featherwiki/__init__.py | 2 +- plinth/modules/featherwiki/manifest.py | 2 + plinth/modules/gitweb/__init__.py | 2 +- plinth/modules/gitweb/manifest.py | 2 + plinth/modules/i2p/__init__.py | 2 +- plinth/modules/i2p/manifest.py | 2 + plinth/modules/ikiwiki/__init__.py | 2 +- plinth/modules/ikiwiki/manifest.py | 2 + plinth/modules/infinoted/__init__.py | 2 +- plinth/modules/infinoted/manifest.py | 2 + plinth/modules/janus/__init__.py | 2 +- plinth/modules/janus/manifest.py | 2 + plinth/modules/jsxc/__init__.py | 2 +- plinth/modules/jsxc/manifest.py | 2 + plinth/modules/kiwix/__init__.py | 2 +- plinth/modules/kiwix/manifest.py | 2 + plinth/modules/matrixsynapse/__init__.py | 3 +- plinth/modules/matrixsynapse/manifest.py | 10 ++ plinth/modules/mediawiki/__init__.py | 3 +- plinth/modules/mediawiki/manifest.py | 2 + plinth/modules/minetest/__init__.py | 2 +- plinth/modules/minetest/manifest.py | 2 + plinth/modules/minidlna/__init__.py | 3 +- plinth/modules/minidlna/manifest.py | 2 + plinth/modules/miniflux/__init__.py | 2 +- plinth/modules/miniflux/manifest.py | 2 + plinth/modules/mumble/__init__.py | 2 +- plinth/modules/mumble/manifest.py | 2 + plinth/modules/nextcloud/__init__.py | 2 +- plinth/modules/nextcloud/manifest.py | 2 + plinth/modules/openvpn/__init__.py | 2 +- plinth/modules/openvpn/manifest.py | 2 + plinth/modules/privoxy/__init__.py | 3 +- plinth/modules/privoxy/manifest.py | 4 + plinth/modules/quassel/__init__.py | 2 +- plinth/modules/quassel/manifest.py | 2 + plinth/modules/radicale/__init__.py | 3 +- plinth/modules/radicale/manifest.py | 8 + plinth/modules/roundcube/__init__.py | 3 +- plinth/modules/roundcube/manifest.py | 12 +- plinth/modules/rssbridge/__init__.py | 2 +- plinth/modules/rssbridge/manifest.py | 9 +- plinth/modules/samba/__init__.py | 2 +- plinth/modules/samba/manifest.py | 2 + plinth/modules/searx/__init__.py | 2 +- plinth/modules/searx/manifest.py | 2 + plinth/modules/shaarli/__init__.py | 2 +- plinth/modules/shaarli/manifest.py | 4 +- plinth/modules/shadowsocks/__init__.py | 3 +- plinth/modules/shadowsocks/manifest.py | 10 ++ plinth/modules/shadowsocksserver/__init__.py | 3 +- plinth/modules/shadowsocksserver/manifest.py | 4 + plinth/modules/sharing/__init__.py | 3 +- plinth/modules/sharing/manifest.py | 4 + plinth/modules/syncthing/__init__.py | 3 +- plinth/modules/syncthing/manifest.py | 2 + plinth/modules/tiddlywiki/__init__.py | 3 +- plinth/modules/tiddlywiki/manifest.py | 11 ++ plinth/modules/tor/__init__.py | 2 +- plinth/modules/tor/manifest.py | 7 + plinth/modules/torproxy/__init__.py | 2 +- plinth/modules/torproxy/manifest.py | 8 + plinth/modules/transmission/__init__.py | 3 +- plinth/modules/transmission/manifest.py | 2 + plinth/modules/ttrss/__init__.py | 2 +- plinth/modules/ttrss/manifest.py | 2 + plinth/modules/wireguard/__init__.py | 2 +- plinth/modules/wireguard/manifest.py | 2 + plinth/modules/wordpress/__init__.py | 2 +- plinth/modules/wordpress/manifest.py | 8 +- plinth/modules/zoph/manifest.py | 2 + plinth/templates/app-header.html | 11 ++ plinth/templates/apps.html | 37 ++++ plinth/templates/cards.html | 6 +- plinth/tests/tags/__init__.py | 0 plinth/tests/tags/test_functional.py | 60 +++++++ plinth/views.py | 56 +++++- static/tags.js | 170 +++++++++++++++++++ static/themes/default/css/main.css | 54 +++++- 93 files changed, 622 insertions(+), 72 deletions(-) create mode 100644 plinth/tests/tags/__init__.py create mode 100644 plinth/tests/tags/test_functional.py create mode 100644 static/tags.js diff --git a/plinth/app.py b/plinth/app.py index be936b674..b9aa568ef 100644 --- a/plinth/app.py +++ b/plinth/app.py @@ -7,7 +7,7 @@ import collections import enum import inspect import logging -from typing import ClassVar, TypeAlias +from typing import ClassVar, Dict, List, TypeAlias from plinth import cfg from plinth.diagnostic_check import DiagnosticCheck @@ -434,7 +434,7 @@ class Info(FollowerComponent): def __init__(self, app_id, version, is_essential=False, depends=None, name=None, icon=None, icon_filename=None, short_description=None, description=None, manual_page=None, - clients=None, donation_url=None): + clients=None, donation_url=None, tags=None): """Store the basic properties of an app as a component. Each app must contain at least one component of this type to provide @@ -504,6 +504,9 @@ class Info(FollowerComponent): 'donation_url' is a link to a webpage that describes how to donate to the upstream project. + 'tags' is a list of tags that describe the app. Tags help users to find + similar apps or alternatives and discover use cases. + """ self.component_id = app_id + '-info' self.app_id = app_id @@ -518,9 +521,19 @@ class Info(FollowerComponent): self.manual_page = manual_page self.clients = clients self.donation_url = donation_url + self.tags = tags or [] if clients: clients_module.validate(clients) + @classmethod + def list_tags(self) -> list: + """Return a list of untranslated tags.""" + tags = set() + for app in App.list(): + tags.update(app.info.tags) + + return list(tags) + class EnableState(LeaderComponent): """A component to hold the enable state of an app using a simple flag. @@ -626,8 +639,8 @@ def _initialize_module(module_name, module): for app_class in app_classes: app_class() except Exception as exception: - logger.exception('Exception while running init for %s: %s', module, - exception) + logger.exception('Exception while running init for module %s: %s', + module_name, exception) if cfg.develop: raise diff --git a/plinth/module_loader.py b/plinth/module_loader.py index 007fb396f..c45888be8 100644 --- a/plinth/module_loader.py +++ b/plinth/module_loader.py @@ -3,7 +3,6 @@ Discover, load and manage FreedomBox applications. """ -import collections import importlib import logging import pathlib @@ -16,7 +15,7 @@ from plinth.signals import pre_module_loading logger = logging.getLogger(__name__) -loaded_modules = collections.OrderedDict() +loaded_modules = dict() _modules_to_load = None @@ -36,8 +35,8 @@ def load_modules(): for module_import_path in get_modules_to_load(): module_name = module_import_path.split('.')[-1] try: - loaded_modules[module_name] = importlib.import_module( - module_import_path) + module = importlib.import_module(module_import_path) + loaded_modules[module_name] = module except Exception as exception: logger.exception('Could not import %s: %s', module_import_path, exception) diff --git a/plinth/modules/bepasty/__init__.py b/plinth/modules/bepasty/__init__.py index 09c199a3e..ff8208e2c 100644 --- a/plinth/modules/bepasty/__init__.py +++ b/plinth/modules/bepasty/__init__.py @@ -58,7 +58,7 @@ class BepastyApp(app_module.App): icon_filename='bepasty', short_description=_('File & Snippet Sharing'), description=_description, manual_page='bepasty', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-bepasty', info.name, diff --git a/plinth/modules/bepasty/manifest.py b/plinth/modules/bepasty/manifest.py index b29d271fa..6eb552dbb 100644 --- a/plinth/modules/bepasty/manifest.py +++ b/plinth/modules/bepasty/manifest.py @@ -19,3 +19,5 @@ backup = { }, 'services': ['uwsgi'], } + +tags = [_('File Sharing'), _('Pastebin')] diff --git a/plinth/modules/calibre/__init__.py b/plinth/modules/calibre/__init__.py index a04792a88..646e88525 100644 --- a/plinth/modules/calibre/__init__.py +++ b/plinth/modules/calibre/__init__.py @@ -56,7 +56,7 @@ class CalibreApp(app_module.App): name=_('calibre'), icon_filename='calibre', short_description=_('E-book Library'), description=_description, manual_page='Calibre', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://calibre-ebook.com/donate') self.add(info) diff --git a/plinth/modules/calibre/manifest.py b/plinth/modules/calibre/manifest.py index a862e90fd..f92b136ed 100644 --- a/plinth/modules/calibre/manifest.py +++ b/plinth/modules/calibre/manifest.py @@ -16,3 +16,5 @@ backup = { }, 'services': ['calibre-server-freedombox'] } + +tags = [_('Ebook'), _('Library'), _('Ebook Reader')] diff --git a/plinth/modules/coturn/__init__.py b/plinth/modules/coturn/__init__.py index 133511ac7..805de147d 100644 --- a/plinth/modules/coturn/__init__.py +++ b/plinth/modules/coturn/__init__.py @@ -51,7 +51,8 @@ class CoturnApp(app_module.App): info = app_module.Info(app_id=self.app_id, version=self._version, name=_('Coturn'), icon_filename='coturn', short_description=_('VoIP Helper'), - description=_description, manual_page='Coturn') + description=_description, manual_page='Coturn', + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-coturn', info.name, info.short_description, diff --git a/plinth/modules/coturn/manifest.py b/plinth/modules/coturn/manifest.py index 0e7047930..777f09a21 100644 --- a/plinth/modules/coturn/manifest.py +++ b/plinth/modules/coturn/manifest.py @@ -1,3 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later +from django.utils.translation import gettext_lazy as _ + backup = {'secrets': {'directories': ['/etc/coturn']}, 'services': ['coturn']} + +tags = [_('VoIP'), _('STUN'), _('TURN')] diff --git a/plinth/modules/deluge/__init__.py b/plinth/modules/deluge/__init__.py index deec8588e..9772fcf19 100644 --- a/plinth/modules/deluge/__init__.py +++ b/plinth/modules/deluge/__init__.py @@ -47,7 +47,8 @@ class DelugeApp(app_module.App): short_description=_('BitTorrent Web Client'), description=_description, manual_page='Deluge', clients=manifest.clients, - donation_url='https://www.patreon.com/deluge_cas') + donation_url='https://www.patreon.com/deluge_cas', + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-deluge', info.name, info.short_description, diff --git a/plinth/modules/deluge/manifest.py b/plinth/modules/deluge/manifest.py index 81f8fb8f9..7335ea1b5 100644 --- a/plinth/modules/deluge/manifest.py +++ b/plinth/modules/deluge/manifest.py @@ -17,3 +17,5 @@ backup = { }, 'services': ['deluged', 'deluge-web'] } + +tags = [_('File Sharing'), _('BitTorrent'), _('Client'), _('P2P')] diff --git a/plinth/modules/ejabberd/__init__.py b/plinth/modules/ejabberd/__init__.py index e8240e14f..20e2ee88d 100644 --- a/plinth/modules/ejabberd/__init__.py +++ b/plinth/modules/ejabberd/__init__.py @@ -56,11 +56,13 @@ class EjabberdApp(app_module.App): """Create components for the app.""" super().__init__() - info = app_module.Info( - app_id=self.app_id, version=self._version, depends=['coturn'], - name=_('ejabberd'), icon_filename='ejabberd', - short_description=_('Chat Server'), description=_description, - manual_page='ejabberd', clients=manifest.clients) + info = app_module.Info(app_id=self.app_id, version=self._version, + depends=['coturn'], name=_('ejabberd'), + icon_filename='ejabberd', + short_description=_('Chat Server'), + description=_description, + manual_page='ejabberd', + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-ejabberd', info.name, diff --git a/plinth/modules/ejabberd/manifest.py b/plinth/modules/ejabberd/manifest.py index 8c3c17b3a..62108c4ce 100644 --- a/plinth/modules/ejabberd/manifest.py +++ b/plinth/modules/ejabberd/manifest.py @@ -119,3 +119,12 @@ backup = { }, 'services': ['ejabberd'] } + +tags = [ + _('XMPP'), + _('VoIP'), + _('IM'), + _('Encrypted Messaging'), + _('Audio Chat'), + _('Video Chat') +] diff --git a/plinth/modules/email/__init__.py b/plinth/modules/email/__init__.py index 09ba154d4..08aea32d8 100644 --- a/plinth/modules/email/__init__.py +++ b/plinth/modules/email/__init__.py @@ -63,7 +63,7 @@ class EmailApp(plinth.app.App): icon_filename='email', short_description=_('Email Server'), description=_description, manual_page='Email', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://rspamd.com/support.html') self.add(info) diff --git a/plinth/modules/email/manifest.py b/plinth/modules/email/manifest.py index d46f8fbca..6097000e2 100644 --- a/plinth/modules/email/manifest.py +++ b/plinth/modules/email/manifest.py @@ -78,3 +78,5 @@ backup = { }, 'services': ['postfix', 'dovecot', 'rspamd'] } + +tags = [_('Email')] diff --git a/plinth/modules/featherwiki/__init__.py b/plinth/modules/featherwiki/__init__.py index eb6eeb9e9..3ac5dae01 100644 --- a/plinth/modules/featherwiki/__init__.py +++ b/plinth/modules/featherwiki/__init__.py @@ -61,7 +61,7 @@ class FeatherWikiApp(app_module.App): short_description=_('Personal Notebooks'), description=_description, manual_page='FeatherWiki', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-featherwiki', info.name, diff --git a/plinth/modules/featherwiki/manifest.py b/plinth/modules/featherwiki/manifest.py index 0469011c0..2855ee098 100644 --- a/plinth/modules/featherwiki/manifest.py +++ b/plinth/modules/featherwiki/manifest.py @@ -14,3 +14,5 @@ clients = [{ }] backup = {'data': {'directories': [str(wiki_dir)]}} + +tags = [_('Wiki'), _('Note Taking'), _('Website'), _('Quine'), _('non-Debian')] diff --git a/plinth/modules/gitweb/__init__.py b/plinth/modules/gitweb/__init__.py index 78c34ecf5..7e59fb0c5 100644 --- a/plinth/modules/gitweb/__init__.py +++ b/plinth/modules/gitweb/__init__.py @@ -48,7 +48,7 @@ class GitwebApp(app_module.App): name=_('Gitweb'), icon_filename='gitweb', short_description=_('Simple Git Hosting'), description=_description, manual_page='GitWeb', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-gitweb', info.name, info.short_description, diff --git a/plinth/modules/gitweb/manifest.py b/plinth/modules/gitweb/manifest.py index ad2f8e782..f48d69410 100644 --- a/plinth/modules/gitweb/manifest.py +++ b/plinth/modules/gitweb/manifest.py @@ -33,3 +33,5 @@ clients = [ ] backup = {'data': {'directories': [GIT_REPO_PATH]}} + +tags = [_('Git'), _('Version Control'), _('Dev Tool')] diff --git a/plinth/modules/i2p/__init__.py b/plinth/modules/i2p/__init__.py index acd89bcdf..4116357f3 100644 --- a/plinth/modules/i2p/__init__.py +++ b/plinth/modules/i2p/__init__.py @@ -52,7 +52,7 @@ class I2PApp(app_module.App): name=_('I2P'), icon_filename='i2p', short_description=_('Anonymity Network'), description=_description, manual_page='I2P', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-i2p', info.name, info.short_description, diff --git a/plinth/modules/i2p/manifest.py b/plinth/modules/i2p/manifest.py index 6c41320d3..2f1f64beb 100644 --- a/plinth/modules/i2p/manifest.py +++ b/plinth/modules/i2p/manifest.py @@ -39,3 +39,5 @@ backup = { }, 'services': ['i2p'] } + +tags = [_('Anonymity Network'), _('Censorship Resistance')] diff --git a/plinth/modules/ikiwiki/__init__.py b/plinth/modules/ikiwiki/__init__.py index 3d498fdea..7e07ef6ff 100644 --- a/plinth/modules/ikiwiki/__init__.py +++ b/plinth/modules/ikiwiki/__init__.py @@ -45,7 +45,7 @@ class IkiwikiApp(app_module.App): name=_('ikiwiki'), icon_filename='ikiwiki', short_description=_('Wiki and Blog'), description=_description, manual_page='Ikiwiki', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://ikiwiki.info/tipjar/') self.add(info) diff --git a/plinth/modules/ikiwiki/manifest.py b/plinth/modules/ikiwiki/manifest.py index 6d8f98bc2..c531de3c4 100644 --- a/plinth/modules/ikiwiki/manifest.py +++ b/plinth/modules/ikiwiki/manifest.py @@ -11,3 +11,5 @@ clients = [{ }] backup = {'data': {'directories': ['/var/lib/ikiwiki/', '/var/www/ikiwiki/']}} + +tags = [_('Wiki'), _('Blog'), _('Website')] diff --git a/plinth/modules/infinoted/__init__.py b/plinth/modules/infinoted/__init__.py index 00f98e69c..6a297ae6c 100644 --- a/plinth/modules/infinoted/__init__.py +++ b/plinth/modules/infinoted/__init__.py @@ -42,7 +42,7 @@ class InfinotedApp(app_module.App): short_description=_('Gobby Server'), description=_description, manual_page='Infinoted', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-infinoted', info.name, diff --git a/plinth/modules/infinoted/manifest.py b/plinth/modules/infinoted/manifest.py index d844d5c27..fad1f5786 100644 --- a/plinth/modules/infinoted/manifest.py +++ b/plinth/modules/infinoted/manifest.py @@ -42,3 +42,5 @@ backup = { }, 'services': ['infinoted'] } + +tags = [_('Note Taking'), _('Collaborative Editing'), _('Gobby')] diff --git a/plinth/modules/janus/__init__.py b/plinth/modules/janus/__init__.py index 3c4dafac2..f1e6ae8e1 100644 --- a/plinth/modules/janus/__init__.py +++ b/plinth/modules/janus/__init__.py @@ -43,7 +43,7 @@ class JanusApp(app_module.App): icon_filename='janus', short_description=_('Video Room'), description=_description, manual_page='Janus', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-janus', info.name, info.short_description, diff --git a/plinth/modules/janus/manifest.py b/plinth/modules/janus/manifest.py index 467c65002..780f75efb 100644 --- a/plinth/modules/janus/manifest.py +++ b/plinth/modules/janus/manifest.py @@ -12,3 +12,5 @@ clients = [{ }] backup: dict = {} + +tags = [_('Video Conferencing'), _('WebRTC')] diff --git a/plinth/modules/jsxc/__init__.py b/plinth/modules/jsxc/__init__.py index 2f66bdcd8..314f55d83 100644 --- a/plinth/modules/jsxc/__init__.py +++ b/plinth/modules/jsxc/__init__.py @@ -38,7 +38,7 @@ class JSXCApp(app_module.App): name=_('JSXC'), icon_filename='jsxc', short_description=_('Chat Client'), description=_description, manual_page='JSXC', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-jsxc', info.name, info.short_description, diff --git a/plinth/modules/jsxc/manifest.py b/plinth/modules/jsxc/manifest.py index 0eb350b7c..565924b3f 100644 --- a/plinth/modules/jsxc/manifest.py +++ b/plinth/modules/jsxc/manifest.py @@ -12,3 +12,5 @@ clients = [{ }] backup: dict = {} + +tags = [_('XMPP'), _('Client')] diff --git a/plinth/modules/kiwix/__init__.py b/plinth/modules/kiwix/__init__.py index eaa439a12..eded154d8 100644 --- a/plinth/modules/kiwix/__init__.py +++ b/plinth/modules/kiwix/__init__.py @@ -56,7 +56,7 @@ class KiwixApp(app_module.App): app_id=self.app_id, version=self._version, name=_('Kiwix'), icon_filename='kiwix', short_description=_('Offline Wikipedia'), description=_description, manual_page='Kiwix', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://www.kiwix.org/en/support-us/') self.add(info) diff --git a/plinth/modules/kiwix/manifest.py b/plinth/modules/kiwix/manifest.py index bf45ebe8d..f989f9cd8 100644 --- a/plinth/modules/kiwix/manifest.py +++ b/plinth/modules/kiwix/manifest.py @@ -18,3 +18,5 @@ backup = { }, 'services': ['kiwix-server-freedombox'] } + +tags = [_('Offline Reader'), _('Archival'), _('Censorship Resistance')] diff --git a/plinth/modules/matrixsynapse/__init__.py b/plinth/modules/matrixsynapse/__init__.py index dc1292081..403771907 100644 --- a/plinth/modules/matrixsynapse/__init__.py +++ b/plinth/modules/matrixsynapse/__init__.py @@ -54,7 +54,8 @@ class MatrixSynapseApp(app_module.App): app_id=self.app_id, version=self._version, depends=['coturn'], name=_('Matrix Synapse'), icon_filename='matrixsynapse', short_description=_('Chat Server'), description=_description, - manual_page='MatrixSynapse', clients=manifest.clients) + manual_page='MatrixSynapse', clients=manifest.clients, + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-matrixsynapse', info.name, diff --git a/plinth/modules/matrixsynapse/manifest.py b/plinth/modules/matrixsynapse/manifest.py index efeddb638..fc7ad7da0 100644 --- a/plinth/modules/matrixsynapse/manifest.py +++ b/plinth/modules/matrixsynapse/manifest.py @@ -96,3 +96,13 @@ backup = { }, 'services': ['matrix-synapse'] } + +tags = [ + _('Chat Room'), + _('Encrypted Messaging'), + _('IM'), + _('Audio Chat'), + _('Video Chat'), + _('Matrix'), + _('VoIP') +] diff --git a/plinth/modules/mediawiki/__init__.py b/plinth/modules/mediawiki/__init__.py index 413e8da85..31eb4edc9 100644 --- a/plinth/modules/mediawiki/__init__.py +++ b/plinth/modules/mediawiki/__init__.py @@ -52,7 +52,8 @@ class MediaWikiApp(app_module.App): short_description=_('Wiki'), description=_description, manual_page='MediaWiki', - clients=manifest.clients) + clients=manifest.clients, + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-mediawiki', info.name, diff --git a/plinth/modules/mediawiki/manifest.py b/plinth/modules/mediawiki/manifest.py index 3e4c72b75..e7f8713bd 100644 --- a/plinth/modules/mediawiki/manifest.py +++ b/plinth/modules/mediawiki/manifest.py @@ -21,3 +21,5 @@ backup = { }, 'services': ['mediawiki-jobrunner'] } + +tags = [_('Wiki'), _('Website')] diff --git a/plinth/modules/minetest/__init__.py b/plinth/modules/minetest/__init__.py index fe08ad228..cea175750 100644 --- a/plinth/modules/minetest/__init__.py +++ b/plinth/modules/minetest/__init__.py @@ -56,7 +56,7 @@ class MinetestApp(app_module.App): app_id=self.app_id, version=self._version, name=_('Minetest'), icon_filename='minetest', short_description=_('Block Sandbox'), description=_description, manual_page='Minetest', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://www.minetest.net/get-involved/#donate') self.add(info) diff --git a/plinth/modules/minetest/manifest.py b/plinth/modules/minetest/manifest.py index c23ff3f15..539a8c148 100644 --- a/plinth/modules/minetest/manifest.py +++ b/plinth/modules/minetest/manifest.py @@ -45,3 +45,5 @@ backup = { }, 'services': ['minetest-server'] } + +tags = [_('Game'), _('Block Sandbox')] diff --git a/plinth/modules/minidlna/__init__.py b/plinth/modules/minidlna/__init__.py index a301b9764..1dc6d7cd9 100644 --- a/plinth/modules/minidlna/__init__.py +++ b/plinth/modules/minidlna/__init__.py @@ -46,7 +46,8 @@ class MiniDLNAApp(app_module.App): short_description=_('Simple Media Server'), description=_description, manual_page='MiniDLNA', - clients=manifest.clients) + clients=manifest.clients, + tags=manifest.tags) self.add(info) menu_item = menu.Menu( diff --git a/plinth/modules/minidlna/manifest.py b/plinth/modules/minidlna/manifest.py index 5f62d84a1..007e8ce76 100644 --- a/plinth/modules/minidlna/manifest.py +++ b/plinth/modules/minidlna/manifest.py @@ -112,3 +112,5 @@ backup = { }, 'services': ['minidlna'] } + +tags = [_('Media Server'), _('Television'), _('UPnP'), _('DLNA')] diff --git a/plinth/modules/miniflux/__init__.py b/plinth/modules/miniflux/__init__.py index cb846b541..8ae1cccaa 100644 --- a/plinth/modules/miniflux/__init__.py +++ b/plinth/modules/miniflux/__init__.py @@ -44,7 +44,7 @@ class MinifluxApp(app_module.App): short_description=_('News Feed Reader'), description=_description, manual_page='miniflux', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://miniflux.app/#donations') self.add(info) diff --git a/plinth/modules/miniflux/manifest.py b/plinth/modules/miniflux/manifest.py index af244b701..f112c564b 100644 --- a/plinth/modules/miniflux/manifest.py +++ b/plinth/modules/miniflux/manifest.py @@ -134,3 +134,5 @@ backup = { }, 'services': ['miniflux'] } + +tags = [_('Feed Reader'), _('News'), _('RSS'), _('ATOM')] diff --git a/plinth/modules/mumble/__init__.py b/plinth/modules/mumble/__init__.py index e43f26be7..f03e81966 100644 --- a/plinth/modules/mumble/__init__.py +++ b/plinth/modules/mumble/__init__.py @@ -45,7 +45,7 @@ class MumbleApp(app_module.App): app_id=self.app_id, version=self._version, name=_('Mumble'), icon_filename='mumble', short_description=_('Voice Chat'), description=_description, manual_page='Mumble', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://wiki.mumble.info/wiki/Donate') self.add(info) diff --git a/plinth/modules/mumble/manifest.py b/plinth/modules/mumble/manifest.py index 95a4c8fb4..fa7843aed 100644 --- a/plinth/modules/mumble/manifest.py +++ b/plinth/modules/mumble/manifest.py @@ -63,3 +63,5 @@ backup = { }, 'services': ['mumble-server'] } + +tags = [_('Audio Chat'), _('VoIP')] diff --git a/plinth/modules/nextcloud/__init__.py b/plinth/modules/nextcloud/__init__.py index 3a3f8920f..cb2812790 100644 --- a/plinth/modules/nextcloud/__init__.py +++ b/plinth/modules/nextcloud/__init__.py @@ -59,7 +59,7 @@ class NextcloudApp(app_module.App): icon_filename='nextcloud', short_description=_('File Storage & Collaboration'), description=_description, manual_page='Nextcloud', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-nextcloud', info.name, diff --git a/plinth/modules/nextcloud/manifest.py b/plinth/modules/nextcloud/manifest.py index fa875a67a..cf30d69d2 100644 --- a/plinth/modules/nextcloud/manifest.py +++ b/plinth/modules/nextcloud/manifest.py @@ -52,3 +52,5 @@ backup = { 'files': ['/var/lib/plinth/backups-data/nextcloud-database.sql'] } } + +tags = [_('Cloud Storage'), _('File Sharing'), _('non-Debian')] diff --git a/plinth/modules/openvpn/__init__.py b/plinth/modules/openvpn/__init__.py index 5a41ea820..5ceb93601 100644 --- a/plinth/modules/openvpn/__init__.py +++ b/plinth/modules/openvpn/__init__.py @@ -46,7 +46,7 @@ class OpenVPNApp(app_module.App): name=_('OpenVPN'), icon_filename='openvpn', short_description=_('Virtual Private Network'), description=_description, manual_page='OpenVPN', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-openvpn', info.name, diff --git a/plinth/modules/openvpn/manifest.py b/plinth/modules/openvpn/manifest.py index 22f745926..cf7f2b613 100644 --- a/plinth/modules/openvpn/manifest.py +++ b/plinth/modules/openvpn/manifest.py @@ -56,3 +56,5 @@ clients = [{ 'url': 'https://tunnelblick.net/downloads.html' }] }] + +tags = [_('VPN'), _('Anonymity'), _('Remote Access')] diff --git a/plinth/modules/privoxy/__init__.py b/plinth/modules/privoxy/__init__.py index 0e4a01e28..f65a2b494 100644 --- a/plinth/modules/privoxy/__init__.py +++ b/plinth/modules/privoxy/__init__.py @@ -53,7 +53,8 @@ class PrivoxyApp(app_module.App): app_id=self.app_id, version=self._version, name=_('Privoxy'), icon_filename='privoxy', short_description=_('Web Proxy'), description=_description, manual_page='Privoxy', - donation_url='https://www.privoxy.org/faq/general.html#DONATE') + donation_url='https://www.privoxy.org/faq/general.html#DONATE', + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-privoxy', info.name, diff --git a/plinth/modules/privoxy/manifest.py b/plinth/modules/privoxy/manifest.py index 099231aa8..f346fe0a0 100644 --- a/plinth/modules/privoxy/manifest.py +++ b/plinth/modules/privoxy/manifest.py @@ -3,4 +3,8 @@ Application manifest for privoxy. """ +from django.utils.translation import gettext_lazy as _ + backup: dict = {} + +tags = [_('Ad Blocker'), _('Proxy'), _('Local Network')] diff --git a/plinth/modules/quassel/__init__.py b/plinth/modules/quassel/__init__.py index ed59be865..a0d90b66b 100644 --- a/plinth/modules/quassel/__init__.py +++ b/plinth/modules/quassel/__init__.py @@ -51,7 +51,7 @@ class QuasselApp(app_module.App): name=_('Quassel'), icon_filename='quassel', short_description=_('IRC Client'), description=_description, manual_page='Quassel', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-quassel', info.name, diff --git a/plinth/modules/quassel/manifest.py b/plinth/modules/quassel/manifest.py index 2deb7f047..63aa96256 100644 --- a/plinth/modules/quassel/manifest.py +++ b/plinth/modules/quassel/manifest.py @@ -50,3 +50,5 @@ backup = { }, 'services': ['quasselcore'], } + +tags = [_('Chat Room'), _('IRC'), _('Client')] diff --git a/plinth/modules/radicale/__init__.py b/plinth/modules/radicale/__init__.py index 70c7cdf2c..acc046597 100644 --- a/plinth/modules/radicale/__init__.py +++ b/plinth/modules/radicale/__init__.py @@ -54,7 +54,8 @@ class RadicaleApp(app_module.App): short_description=_('Calendar and Addressbook'), description=_description, manual_page='Radicale', - clients=manifest.clients) + clients=manifest.clients, + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-radicale', info.name, diff --git a/plinth/modules/radicale/manifest.py b/plinth/modules/radicale/manifest.py index 632ab1058..b27bc7523 100644 --- a/plinth/modules/radicale/manifest.py +++ b/plinth/modules/radicale/manifest.py @@ -87,3 +87,11 @@ backup = { }, 'services': ['uwsgi'] } + +tags = [ + _('Calendar'), + _('Contacts'), + _('Synchronization'), + _('CalDAV'), + _('CardDAV') +] diff --git a/plinth/modules/roundcube/__init__.py b/plinth/modules/roundcube/__init__.py index 0268208ff..25fe745e3 100644 --- a/plinth/modules/roundcube/__init__.py +++ b/plinth/modules/roundcube/__init__.py @@ -51,7 +51,8 @@ class RoundcubeApp(app_module.App): short_description=_('Email Client'), description=_description, manual_page='Roundcube', - clients=manifest.clients) + clients=manifest.clients, + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-roundcube', info.name, diff --git a/plinth/modules/roundcube/manifest.py b/plinth/modules/roundcube/manifest.py index 314d6740c..919f53bde 100644 --- a/plinth/modules/roundcube/manifest.py +++ b/plinth/modules/roundcube/manifest.py @@ -11,8 +11,12 @@ clients = [{ }] backup = { - 'data': { - 'files': ['/etc/roundcube/freedombox-config.php', - '/var/lib/dbconfig-common/sqlite3/roundcube/roundcube'] - } + 'data': { + 'files': [ + '/etc/roundcube/freedombox-config.php', + '/var/lib/dbconfig-common/sqlite3/roundcube/roundcube' + ] + } } + +tags = [_('Email'), _('Contacts'), _('Client')] diff --git a/plinth/modules/rssbridge/__init__.py b/plinth/modules/rssbridge/__init__.py index 7126b5ac3..13bf1cca7 100644 --- a/plinth/modules/rssbridge/__init__.py +++ b/plinth/modules/rssbridge/__init__.py @@ -50,7 +50,7 @@ class RSSBridgeApp(app_module.App): short_description=_('RSS Feed Generator'), description=_description, manual_page='RSSBridge', donation_url=None, - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-rssbridge', info.name, diff --git a/plinth/modules/rssbridge/manifest.py b/plinth/modules/rssbridge/manifest.py index 0048bcd43..cfe398c9f 100644 --- a/plinth/modules/rssbridge/manifest.py +++ b/plinth/modules/rssbridge/manifest.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later from django.utils.translation import gettext_lazy as _ - """ Application manifest for RSS-Bridge. """ @@ -14,8 +13,6 @@ clients = [{ }] }] -backup = { - 'data': { - 'files': ['/etc/rss-bridge/is_public'] - } -} +backup = {'data': {'files': ['/etc/rss-bridge/is_public']}} + +tags = [_('Feed Generator'), _('News'), _('RSS'), _('ATOM')] diff --git a/plinth/modules/samba/__init__.py b/plinth/modules/samba/__init__.py index a433db765..80747f700 100644 --- a/plinth/modules/samba/__init__.py +++ b/plinth/modules/samba/__init__.py @@ -53,7 +53,7 @@ class SambaApp(app_module.App): app_id=self.app_id, version=self._version, name=_('Samba'), icon_filename='samba', short_description=_('Network File Storage'), manual_page='Samba', description=_description, - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://www.samba.org/samba/donations.html') self.add(info) diff --git a/plinth/modules/samba/manifest.py b/plinth/modules/samba/manifest.py index 821a48ece..4195c3753 100644 --- a/plinth/modules/samba/manifest.py +++ b/plinth/modules/samba/manifest.py @@ -84,3 +84,5 @@ clients = [{ }] backup: dict = {} + +tags = [_('File Sharing'), _('Local Network')] diff --git a/plinth/modules/searx/__init__.py b/plinth/modules/searx/__init__.py index f81e3375f..8178c51b3 100644 --- a/plinth/modules/searx/__init__.py +++ b/plinth/modules/searx/__init__.py @@ -41,7 +41,7 @@ class SearxApp(app_module.App): app_id=self.app_id, version=self._version, name=_('Searx'), icon_filename='searx', short_description=_('Web Search'), description=_description, manual_page='Searx', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://searx.me/static/donate.html') self.add(info) diff --git a/plinth/modules/searx/manifest.py b/plinth/modules/searx/manifest.py index d8db2f2df..5643f9354 100644 --- a/plinth/modules/searx/manifest.py +++ b/plinth/modules/searx/manifest.py @@ -13,3 +13,5 @@ clients = [{ PUBLIC_ACCESS_SETTING_FILE = '/etc/searx/allow_public_access' backup = {'config': {'files': [PUBLIC_ACCESS_SETTING_FILE]}} + +tags = [_('Web Search'), _('Metasearch Engine')] diff --git a/plinth/modules/shaarli/__init__.py b/plinth/modules/shaarli/__init__.py index 4559f2613..14f322e94 100644 --- a/plinth/modules/shaarli/__init__.py +++ b/plinth/modules/shaarli/__init__.py @@ -36,7 +36,7 @@ class ShaarliApp(app_module.App): name=_('Shaarli'), icon_filename='shaarli', short_description=_('Bookmarks'), description=_description, manual_page='Shaarli', - clients=manifest.clients) + clients=manifest.clients, tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-shaarli', info.name, diff --git a/plinth/modules/shaarli/manifest.py b/plinth/modules/shaarli/manifest.py index 152413363..ae4a70823 100644 --- a/plinth/modules/shaarli/manifest.py +++ b/plinth/modules/shaarli/manifest.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """ -Application manifest for bind. +Application manifest for Shaarli. """ from django.utils.translation import gettext_lazy as _ @@ -30,3 +30,5 @@ clients = [{ }] backup = {'data': {'directories': ['/var/lib/shaarli/data']}} + +tags = [_('Bookmarks'), _('Link Blog'), _('Single User')] diff --git a/plinth/modules/shadowsocks/__init__.py b/plinth/modules/shadowsocks/__init__.py index 57f26d4cb..7b1fd4179 100644 --- a/plinth/modules/shadowsocks/__init__.py +++ b/plinth/modules/shadowsocks/__init__.py @@ -51,7 +51,8 @@ class ShadowsocksApp(app_module.App): icon_filename='shadowsocks', short_description=_('Bypass Censorship'), description=_description, - manual_page='Shadowsocks') + manual_page='Shadowsocks', + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-shadowsocks', info.name, diff --git a/plinth/modules/shadowsocks/manifest.py b/plinth/modules/shadowsocks/manifest.py index 52d8f00e6..5715b73c1 100644 --- a/plinth/modules/shadowsocks/manifest.py +++ b/plinth/modules/shadowsocks/manifest.py @@ -3,6 +3,8 @@ Application manifest for Shadowsocks Client. """ +from django.utils.translation import gettext_lazy as _ + backup = { 'secrets': { 'files': [ @@ -11,3 +13,11 @@ backup = { }, 'services': ['shadowsocks-libev-local@freedombox'] } + +tags = [ + _('Proxy'), + _('Client'), + _('SOCKS5'), + _('Censorship Resistance'), + _('Shadowsocks') +] diff --git a/plinth/modules/shadowsocksserver/__init__.py b/plinth/modules/shadowsocksserver/__init__.py index f574ca4e1..b78302d7d 100644 --- a/plinth/modules/shadowsocksserver/__init__.py +++ b/plinth/modules/shadowsocksserver/__init__.py @@ -47,7 +47,8 @@ class ShadowsocksServerApp(app_module.App): app_id=self.app_id, version=self._version, name=_('Shadowsocks Server'), icon_filename='shadowsocks', short_description=_('Help Others Bypass Censorship'), - description=_description, manual_page='Shadowsocks') + description=_description, manual_page='Shadowsocks', + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-shadowsocks-server', info.name, diff --git a/plinth/modules/shadowsocksserver/manifest.py b/plinth/modules/shadowsocksserver/manifest.py index c18b96fc4..0f34ea395 100644 --- a/plinth/modules/shadowsocksserver/manifest.py +++ b/plinth/modules/shadowsocksserver/manifest.py @@ -3,6 +3,8 @@ Application manifest for Shadowsocks Server. """ +from django.utils.translation import gettext_lazy as _ + backup = { 'secrets': { 'files': [ @@ -11,3 +13,5 @@ backup = { }, 'services': ['shadowsocks-libev-server@fbxserver'] } + +tags = [_('Proxy'), _('SOCKS5'), _('Censorship Resistance'), _('Shadowsocks')] diff --git a/plinth/modules/sharing/__init__.py b/plinth/modules/sharing/__init__.py index e6c1e4c5d..8650a899e 100644 --- a/plinth/modules/sharing/__init__.py +++ b/plinth/modules/sharing/__init__.py @@ -32,7 +32,8 @@ class SharingApp(app_module.App): super().__init__() info = app_module.Info(app_id=self.app_id, version=self._version, name=_('Sharing'), icon_filename='sharing', - manual_page='Sharing', description=_description) + manual_page='Sharing', description=_description, + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-sharing', info.name, None, diff --git a/plinth/modules/sharing/manifest.py b/plinth/modules/sharing/manifest.py index 2da78201d..f7b839a21 100644 --- a/plinth/modules/sharing/manifest.py +++ b/plinth/modules/sharing/manifest.py @@ -3,6 +3,8 @@ Application manifest for sharing. """ +from django.utils.translation import gettext_lazy as _ + backup = { 'config': { 'files': ['/etc/apache2/conf-available/sharing-freedombox.conf'] @@ -13,3 +15,5 @@ backup = { 'name': 'sharing-freedombox' }] } + +tags = [_('File Sharing')] diff --git a/plinth/modules/syncthing/__init__.py b/plinth/modules/syncthing/__init__.py index 4c11317a6..91db0559a 100644 --- a/plinth/modules/syncthing/__init__.py +++ b/plinth/modules/syncthing/__init__.py @@ -61,7 +61,8 @@ class SyncthingApp(app_module.App): description=_description, manual_page='Syncthing', clients=manifest.clients, - donation_url='https://syncthing.net/donations/') + donation_url='https://syncthing.net/donations/', + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-syncthing', info.name, diff --git a/plinth/modules/syncthing/manifest.py b/plinth/modules/syncthing/manifest.py index 82fab9d6c..2225a1a69 100644 --- a/plinth/modules/syncthing/manifest.py +++ b/plinth/modules/syncthing/manifest.py @@ -54,3 +54,5 @@ backup = { }, 'services': ['syncthing@syncthing'] } + +tags = [_('Synchronization'), _('File Sharing'), _('Cloud Storage')] diff --git a/plinth/modules/tiddlywiki/__init__.py b/plinth/modules/tiddlywiki/__init__.py index dd7d673ed..012064beb 100644 --- a/plinth/modules/tiddlywiki/__init__.py +++ b/plinth/modules/tiddlywiki/__init__.py @@ -66,7 +66,8 @@ class TiddlyWikiApp(app_module.App): short_description=_('Non-linear Notebooks'), description=_description, manual_page='TiddlyWiki', - clients=manifest.clients) + clients=manifest.clients, + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-tiddlywiki', info.name, diff --git a/plinth/modules/tiddlywiki/manifest.py b/plinth/modules/tiddlywiki/manifest.py index 9b9f4d29f..d7da627f0 100644 --- a/plinth/modules/tiddlywiki/manifest.py +++ b/plinth/modules/tiddlywiki/manifest.py @@ -14,3 +14,14 @@ clients = [{ }] backup = {'data': {'directories': [str(wiki_dir)]}} + +tags = [ + _('Wiki'), + _('Note Taking'), + _('Website'), + _('Journal'), + _('Digital Garden'), + _('Zettelkasten'), + _('Quine'), + _('non-Debian') +] diff --git a/plinth/modules/tor/__init__.py b/plinth/modules/tor/__init__.py index d603aab2b..1aaafa8c7 100644 --- a/plinth/modules/tor/__init__.py +++ b/plinth/modules/tor/__init__.py @@ -62,7 +62,7 @@ class TorApp(app_module.App): ], name=_('Tor'), icon_filename='tor', short_description=_('Anonymity Network'), description=_description, manual_page='Tor', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://donate.torproject.org/') self.add(info) diff --git a/plinth/modules/tor/manifest.py b/plinth/modules/tor/manifest.py index b2e0aa1bd..3d3eafe96 100644 --- a/plinth/modules/tor/manifest.py +++ b/plinth/modules/tor/manifest.py @@ -52,3 +52,10 @@ backup = { }, 'services': ['tor@plinth'] } + +tags = [ + _('Relay'), + _('Anonymity Network'), + _('Censorship Resistance'), + _('Tor') +] diff --git a/plinth/modules/torproxy/__init__.py b/plinth/modules/torproxy/__init__.py index cd38b8c17..191dd30de 100644 --- a/plinth/modules/torproxy/__init__.py +++ b/plinth/modules/torproxy/__init__.py @@ -57,7 +57,7 @@ class TorProxyApp(app_module.App): short_description=_('Anonymity Network'), description=_description, manual_page='TorProxy', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://donate.torproject.org/') self.add(info) diff --git a/plinth/modules/torproxy/manifest.py b/plinth/modules/torproxy/manifest.py index a5a2fb777..6378e8876 100644 --- a/plinth/modules/torproxy/manifest.py +++ b/plinth/modules/torproxy/manifest.py @@ -50,3 +50,11 @@ backup = { }, 'services': ['tor@fbxproxy'] } + +tags = [ + _('Proxy'), + _('SOCKS5'), + _('Anonymity Network'), + _('Censorship Resistance'), + _('Tor') +] diff --git a/plinth/modules/transmission/__init__.py b/plinth/modules/transmission/__init__.py index 4ad655571..830e4e758 100644 --- a/plinth/modules/transmission/__init__.py +++ b/plinth/modules/transmission/__init__.py @@ -76,7 +76,8 @@ class TransmissionApp(app_module.App): short_description=_('BitTorrent Web Client'), description=_description, manual_page='Transmission', clients=manifest.clients, - donation_url='https://transmissionbt.com/donate/') + donation_url='https://transmissionbt.com/donate/', + tags=manifest.tags) self.add(info) menu_item = menu.Menu('menu-transmission', info.name, diff --git a/plinth/modules/transmission/manifest.py b/plinth/modules/transmission/manifest.py index ed7023d47..b99e3c49c 100644 --- a/plinth/modules/transmission/manifest.py +++ b/plinth/modules/transmission/manifest.py @@ -35,3 +35,5 @@ backup = { }, 'services': ['transmission-daemon'] } + +tags = [_('File Sharing'), _('BitTorrent'), _('Client'), _('P2P')] diff --git a/plinth/modules/ttrss/__init__.py b/plinth/modules/ttrss/__init__.py index 945f2bb6b..acaddf54d 100644 --- a/plinth/modules/ttrss/__init__.py +++ b/plinth/modules/ttrss/__init__.py @@ -51,7 +51,7 @@ class TTRSSApp(app_module.App): short_description=_('News Feed Reader'), description=_description, manual_page='TinyTinyRSS', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://www.patreon.com/cthulhoo') self.add(info) diff --git a/plinth/modules/ttrss/manifest.py b/plinth/modules/ttrss/manifest.py index 46aa863a6..2510c0d3d 100644 --- a/plinth/modules/ttrss/manifest.py +++ b/plinth/modules/ttrss/manifest.py @@ -51,3 +51,5 @@ backup = { }, 'services': ['tt-rss'] } + +tags = [_('Feed Reader'), _('News'), _('RSS'), _('ATOM')] diff --git a/plinth/modules/wireguard/__init__.py b/plinth/modules/wireguard/__init__.py index 043db2aba..8b072a817 100644 --- a/plinth/modules/wireguard/__init__.py +++ b/plinth/modules/wireguard/__init__.py @@ -48,7 +48,7 @@ class WireguardApp(app_module.App): icon_filename='wireguard', short_description=_('Virtual Private Network'), description=_description, manual_page='WireGuard', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://www.wireguard.com/donations/') self.add(info) diff --git a/plinth/modules/wireguard/manifest.py b/plinth/modules/wireguard/manifest.py index 6c1be52b8..274783aa3 100644 --- a/plinth/modules/wireguard/manifest.py +++ b/plinth/modules/wireguard/manifest.py @@ -41,3 +41,5 @@ clients = [{ 'url': 'https://apps.apple.com/us/app/wireguard/id1441195209' }] }] + +tags = [_('VPN'), _('Anonymity'), _('Remote Access'), _('P2P')] diff --git a/plinth/modules/wordpress/__init__.py b/plinth/modules/wordpress/__init__.py index 91d24de24..35ed78b97 100644 --- a/plinth/modules/wordpress/__init__.py +++ b/plinth/modules/wordpress/__init__.py @@ -53,7 +53,7 @@ class WordPressApp(app_module.App): app_id=self.app_id, version=self._version, name=_('WordPress'), icon_filename='wordpress', short_description=_('Website and Blog'), description=_description, manual_page='WordPress', - clients=manifest.clients, + clients=manifest.clients, tags=manifest.tags, donation_url='https://wordpressfoundation.org/donate/') self.add(info) diff --git a/plinth/modules/wordpress/manifest.py b/plinth/modules/wordpress/manifest.py index 51199ce53..1928f26ad 100644 --- a/plinth/modules/wordpress/manifest.py +++ b/plinth/modules/wordpress/manifest.py @@ -12,11 +12,15 @@ clients = [{ backup = { 'data': { - 'files': ['/var/lib/plinth/backups-data/wordpress-database.sql', - '/etc/wordpress/is_public'], + 'files': [ + '/var/lib/plinth/backups-data/wordpress-database.sql', + '/etc/wordpress/is_public' + ], 'directories': ['/var/lib/wordpress/'] }, 'secrets': { 'directories': ['/etc/wordpress/'] }, } + +tags = [_('Website'), _('Blog')] diff --git a/plinth/modules/zoph/manifest.py b/plinth/modules/zoph/manifest.py index a9a0564f3..81680b453 100644 --- a/plinth/modules/zoph/manifest.py +++ b/plinth/modules/zoph/manifest.py @@ -22,3 +22,5 @@ backup = { ], } } + +tags = [_('Image Viewer'), _('Photo'), _('Library')] diff --git a/plinth/templates/app-header.html b/plinth/templates/app-header.html index 154a0681f..73acd4e6b 100644 --- a/plinth/templates/app-header.html +++ b/plinth/templates/app-header.html @@ -51,6 +51,17 @@ {% endfor %} {% endblock %} + {% if app_info.tags %} +
    + {% for tag in app_info.tags %} + + {% trans tag %} + + {% endfor %} +
    + {% endif %} + {% if app_info.manual_page %}

    diff --git a/plinth/templates/apps.html b/plinth/templates/apps.html index 99a69b797..1d2cabf73 100644 --- a/plinth/templates/apps.html +++ b/plinth/templates/apps.html @@ -7,3 +7,40 @@ {% load i18n %} {% block body_class %}apps-page{% endblock %} + +{% block tags %} + {% if tags %} +

    + {% endif %} +{% endblock %} + +{% block page_js %} + +{% endblock %} diff --git a/plinth/templates/cards.html b/plinth/templates/cards.html index f5ba47d82..64ac96354 100644 --- a/plinth/templates/cards.html +++ b/plinth/templates/cards.html @@ -15,10 +15,12 @@ + {% block tags %}{% endblock %} +
    - {% for item in submenu.sorted_items %} + {% for item in menu_items %} {% if not show_disabled or item.is_enabled %} {% if advanced_mode or not item.advanced %} {% include "card.html" %} @@ -34,7 +36,7 @@
    {% trans "Disabled" %}
    - {% for item in submenu.sorted_items %} + {% for item in menu_items %} {% if not item.is_enabled %} {% if advanced_mode or not item.advanced %} {% include "card.html" %} diff --git a/plinth/tests/tags/__init__.py b/plinth/tests/tags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plinth/tests/tags/test_functional.py b/plinth/tests/tags/test_functional.py new file mode 100644 index 000000000..85c8bd6f1 --- /dev/null +++ b/plinth/tests/tags/test_functional.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Functional, browser based tests for transmission app. +""" + +import pytest + +from selenium.webdriver.common.keys import Keys +from plinth.tests import functional + +pytestmark = [pytest.mark.tags] + + +def _is_app_listed(session_browser, app): + """Assert that the specified app is listed on the page.""" + app_links = session_browser.links.find_by_href(f'/plinth/apps/{app}/') + assert len(app_links) == 1 + + +@pytest.fixture(autouse=True) +def fixture_bittorrent_tag(session_browser): + """Click on the BitTorrent tag.""" + bittorrent_tag = '/plinth/apps/?tag=BitTorrent' + functional.login(session_browser) + functional.nav_to_module(session_browser, 'transmission') + with functional.wait_for_page_update(session_browser, timeout=10, + expected_url=bittorrent_tag): + session_browser.links.find_by_href(bittorrent_tag).click() + + +def test_bittorrent_tag(session_browser): + """Test that the BitTorrent tag lists Deluge and Transmission.""" + _is_app_listed(session_browser, 'deluge') + _is_app_listed(session_browser, 'transmission') + + +def test_search_for_tag(session_browser): + """Test that searching for a tag returns the expected apps.""" + search_input = session_browser.driver.find_element_by_id('add-tag-input') + with functional.wait_for_page_update( + session_browser, timeout=10, + expected_url='/plinth/apps/?tag=BitTorrent&tag=File%20Sharing'): + search_input.click() + search_input.send_keys('file') + search_input.send_keys(Keys.ENTER) + for app in ['deluge', 'nextcloud', 'sharing', 'syncthing', 'transmission']: + _is_app_listed(session_browser, app) + + +def test_click_on_tag(session_browser): + """Test that clicking on a tag lists the expected apps.""" + search_input = session_browser.driver.find_element_by_id('add-tag-input') + with functional.wait_for_page_update( + session_browser, timeout=10, + expected_url='/plinth/apps/?tag=BitTorrent&tag=Cloud%20Storage'): + search_input.click() + session_browser.find_by_css( + ".dropdown-item[data-value='Cloud Storage']").click() + for app in ['deluge', 'nextcloud', 'syncthing', 'transmission']: + _is_app_listed(session_browser, app) diff --git a/plinth/views.py b/plinth/views.py index be07617a4..ddd4eb312 100644 --- a/plinth/views.py +++ b/plinth/views.py @@ -23,6 +23,7 @@ from django.views.generic.edit import FormView from stronghold.decorators import public from plinth import app as app_module +from plinth import menu from plinth.daemon import app_is_running from plinth.modules.config import get_advanced_mode from plinth.modules.firewall.components import get_port_forwarding_info @@ -120,13 +121,61 @@ def index(request): class AppsIndexView(TemplateView): - """View for apps index""" + """View for apps index. + + This view supports filtering apps by one or more tags. If no tags are + provided, it will show all the apps. If one or more tags are provided, + it will select apps matching any of the provided tags. + """ template_name = 'apps.html' + @staticmethod + def _pick_menu_items(menu_items, selected_tags): + """Return a sorted list of menu items filtered by tags.""" + + def _mismatch_map(menu_item) -> list[bool]: + """Return a list of mismatches for selected tags. + + A mismatch is when a selected tag is *not* present in the list of + tags for menu item. + """ + menu_tags = set(menu_item.app.info.tags) + return [tag not in menu_tags for tag in selected_tags] + + def _sort_key(menu_item): + """Returns a comparable tuple to sort menu items. + + Sort items by tag match count first, then by the order of matched + tags in user specified order, then by the order set by menu item, + and then by the name of the menu item in current locale (by + configured collation order). + """ + return (_mismatch_map(menu_item).count(True), + _mismatch_map(menu_item), menu_item.order, + menu_item.name.lower()) + + # Filter out menu items that don't match any of the selected tags. If + # no tags are selected, return all menu items. Otherwise, return all + # menu items that have at least one matching tag. + filtered_menu_items = [ + menu_item for menu_item in menu_items + if (not selected_tags) or (not all(_mismatch_map(menu_item))) + ] + + return sorted(filtered_menu_items, key=_sort_key) + def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context['show_disabled'] = True context['advanced_mode'] = get_advanced_mode() + + tags = self.request.GET.getlist('tag', []) + menu_items = menu.main_menu.active_item(self.request).items + + context['tags'] = tags + context['all_tags'] = app_module.Info.list_tags() + context['menu_items'] = self._pick_menu_items(menu_items, tags) + return context @@ -364,8 +413,9 @@ class SetupView(TemplateView): context['setup_state'] = setup_state context['operations'] = operation.manager.filter(app.app_id) context['show_rerun_setup'] = False - context['show_uninstall'] = (not app.info.is_essential and setup_state - != app_module.App.SetupState.NEEDS_SETUP) + context['show_uninstall'] = ( + not app.info.is_essential + and setup_state != app_module.App.SetupState.NEEDS_SETUP) # Perform expensive operation only if needed. if not context['operations']: diff --git a/static/tags.js b/static/tags.js new file mode 100644 index 000000000..822ec9b06 --- /dev/null +++ b/static/tags.js @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +/** + * @licstart The following is the entire license notice for the JavaScript + * code in this page. + * + * This file is part of FreedomBox. + * + * 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. + */ + +/** + * Update the URL path and history based on the selected tags. + * + * If no tags are selected, redirects to the base apps path. Otherwise, + * constructs a new URL with query parameters for each tag and updates + * the browser history. + * + * @param {string[]} tags - An array of selected tag names. + */ +function updatePath(tags) { + const appsPath = window.location.pathname; + if (tags.length === 0) { + this.location.assign(appsPath); + } else { + let queryParams = tags.map(tag => `tag=${tag}`).join('&'); + let newPath = `${appsPath}?${queryParams}`; + this.history.pushState({ tags: tags }, '', newPath); + this.location.assign(newPath); + } +} + +/** + * Get a list of tags currently displayed, excluding a specific tag if provided. + * + * Iterates through the tag badges in the UI, extracts their text content, + * and returns an array of tag names. + * + * @param {string} [tagToRemove] - The name of the tag to exclude. + * @returns {string[]} An array of tag names currently displayed. + */ +function getTags(tagToRemove) { + const tagBadges = document.querySelectorAll('#selected-tags .tag-badge'); + return Array.from(tagBadges) + .map(tag => tag.dataset.tag) + .filter(tagName => tagName !== tagToRemove); +} + +/** + * Filter and highlight the best matching tag based on the search term. + * + * This function updates the visibility and highlighting of dropdown items + * to match the user's input in the search box. It determines the best + * matching item and marks it as active. + * + * @param {KeyboardEvent} event - The keyboard event that triggered the search. + */ +function findMatchingTag(addTagInput, dropdownItems) { + const searchTerm = addTagInput.value.toLowerCase().trim(); + + // Remove highlighting from all items + dropdownItems.forEach(item => item.classList.remove('active')); + + let bestMatch = null; + dropdownItems.forEach(item => { + const text = item.textContent.toLowerCase(); + if (text.includes(searchTerm)) { + item.style.display = 'block'; + function matchesEarly () { + return text.indexOf(searchTerm) < bestMatch.textContent.toLowerCase().indexOf(searchTerm); + }; + if (bestMatch === null || matchesEarly()) { + bestMatch = item; + } + } else { + item.style.display = 'none'; + } + }); + + // Highlight the best match + if (bestMatch) { + bestMatch.classList.add('active'); + } +} + +/** + * Manage tag-related UI interactions for filtering and displaying apps. + * + * This script manages the user interface for filtering apps based on + * selected tags. It provides functionality for adding and removing tags, + * updating the URL based on selected tags, and displaying a set of + * available tags in a searchable dropdown. + */ +document.addEventListener('DOMContentLoaded', function () { + + // Remove Tag handler. + document.querySelectorAll('.remove-tag').forEach(button => { + button.addEventListener('click', () => { + let tag = button.dataset.tag; + let tags = getTags(tag); + updatePath(tags); + }); + }); + + /** + * Searchable dropdown for selecting tags. + * + * As the user types in the input field, the dropdown list is filtered + * to show only matching items. The best matching item (first match if + * multiple match) is highlighted. Pressing Enter selects the + * highlighted item and adds it as a tag. + */ + const addTagInput = document.getElementById('add-tag-input'); + const dropdownItems = document.querySelectorAll('li.dropdown-item'); + + var timeoutId; + addTagInput.addEventListener('keyup', (event) => { + clearTimeout(timeoutId); + // Select the active tag if the user pressed Enter + if (event.key === 'Enter') { + dropdownItems.forEach(item => { + if (item.classList.contains('active')) { + item.click(); + } + }); + } + // Debounce the user input for search with 300ms delay. + timeoutId = setTimeout(findMatchingTag(addTagInput, dropdownItems), 300); + }); + + dropdownItems.forEach(item => { + item.addEventListener('click', () => { + const selectedTag = item.dataset.value; + + // Add the selected tag and update the path. + let tags = getTags(''); + tags.push(selectedTag); + updatePath(tags); + + // Reset the input field and dropdown. + addTagInput.value = ''; + dropdownItems.forEach(item => { + item.style.display = ''; + item.classList.remove('active'); + }); + }); + }); + + // Handle browser back/forward navigation. + window.addEventListener('popstate', function (event) { + if (event.state && event.state.tags) { + updatePath(event.state.tags); + } + }); +}); + diff --git a/static/themes/default/css/main.css b/static/themes/default/css/main.css index f2ff58061..bc636d527 100644 --- a/static/themes/default/css/main.css +++ b/static/themes/default/css/main.css @@ -309,7 +309,6 @@ html { } #wrapper { - min-height: 100%; position: relative; } @@ -321,6 +320,59 @@ html { margin-bottom: 1.25rem; } +/* Tag Input Container */ +.tag-input { + display: flex; + align-items: center; + border: 1px solid #ced4da; + border-radius: .25rem; + padding: .375rem .75rem; + width: 100%; + background-color: #fff; + margin-bottom: 3rem; +} + +/* Selected Tags */ +.tag-input #selected-tags { + display: flex; + flex-wrap: wrap; + margin-right: .5rem; +} + +.tag-input .tag { + background-color: #e9ecef; /* Light gray background */ + border-radius: .25rem; + padding: .25rem .5rem; + margin-right: .25rem; + margin-bottom: .25rem; + display: flex; + align-items: center; +} + +/* Remove tag button */ +.tag-badge .remove-tag { + background-color: #f8f9fa; /* Match the tag's background color */ + border: none; + padding: 0.25rem 0.5rem; + cursor: pointer; +} + +/* Adjust input field width */ +.tag-input input[type="search"] { + flex-grow: 1; + border: none; + outline: none; + box-shadow: none; + width: auto; + min-width: 3rem; +} + +/* dropdown-menu for tags is a scrollable list */ +.tag-input .dropdown-menu { + bottom: calc(-100% - 10px); + overflow-y: auto; +} + @media (min-width: 768px) { .content-container { padding: 1.5rem 3rem 3rem; From 5fa9bf292893188341214cb71e06661dec082d2f Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 15 Oct 2024 15:25:28 -0700 Subject: [PATCH 35/56] *: tags: Adjust tags and style - Don't use title casing, instead use simple capitalization. - Add some tags. - Drop outdated tags like 'VoIP', 'IM' while emphasizing 'Audio chat', 'Video chat', 'Encrypted messaging' instead. - Try to clarify server vs. web client with tags. Signed-off-by: Sunil Mohan Adapa --- plinth/modules/bepasty/manifest.py | 2 +- plinth/modules/calibre/manifest.py | 2 +- plinth/modules/coturn/manifest.py | 2 +- plinth/modules/deluge/manifest.py | 2 +- plinth/modules/ejabberd/manifest.py | 8 +++----- plinth/modules/email/manifest.py | 2 +- plinth/modules/featherwiki/manifest.py | 2 +- plinth/modules/gitweb/manifest.py | 2 +- plinth/modules/i2p/manifest.py | 2 +- plinth/modules/infinoted/manifest.py | 2 +- plinth/modules/janus/manifest.py | 2 +- plinth/modules/jsxc/manifest.py | 2 +- plinth/modules/kiwix/manifest.py | 7 ++++++- plinth/modules/matrixsynapse/manifest.py | 12 +++++------- plinth/modules/minetest/manifest.py | 2 +- plinth/modules/minidlna/manifest.py | 2 +- plinth/modules/miniflux/manifest.py | 2 +- plinth/modules/mumble/manifest.py | 2 +- plinth/modules/nextcloud/manifest.py | 2 +- plinth/modules/openvpn/manifest.py | 2 +- plinth/modules/privoxy/manifest.py | 2 +- plinth/modules/quassel/manifest.py | 2 +- plinth/modules/radicale/manifest.py | 8 +------- plinth/modules/roundcube/manifest.py | 3 ++- plinth/modules/rssbridge/manifest.py | 6 ++---- plinth/modules/samba/manifest.py | 8 +++++++- plinth/modules/searx/manifest.py | 2 +- plinth/modules/shaarli/manifest.py | 2 +- plinth/modules/shadowsocks/manifest.py | 10 +++++----- plinth/modules/shadowsocksserver/manifest.py | 7 ++++++- plinth/modules/sharing/manifest.py | 2 +- plinth/modules/syncthing/manifest.py | 2 +- plinth/modules/tiddlywiki/manifest.py | 6 +++--- plinth/modules/tor/manifest.py | 7 ++++--- plinth/modules/torproxy/manifest.py | 7 +++---- plinth/modules/transmission/manifest.py | 2 +- plinth/modules/ttrss/manifest.py | 2 +- plinth/modules/wireguard/manifest.py | 2 +- plinth/modules/wordpress/manifest.py | 2 +- plinth/modules/zoph/manifest.py | 2 +- plinth/tests/tags/test_functional.py | 10 +++++----- 41 files changed, 80 insertions(+), 75 deletions(-) diff --git a/plinth/modules/bepasty/manifest.py b/plinth/modules/bepasty/manifest.py index 6eb552dbb..754532584 100644 --- a/plinth/modules/bepasty/manifest.py +++ b/plinth/modules/bepasty/manifest.py @@ -20,4 +20,4 @@ backup = { 'services': ['uwsgi'], } -tags = [_('File Sharing'), _('Pastebin')] +tags = [_('File sharing'), _('Pastebin')] diff --git a/plinth/modules/calibre/manifest.py b/plinth/modules/calibre/manifest.py index f92b136ed..005fcc64a 100644 --- a/plinth/modules/calibre/manifest.py +++ b/plinth/modules/calibre/manifest.py @@ -17,4 +17,4 @@ backup = { 'services': ['calibre-server-freedombox'] } -tags = [_('Ebook'), _('Library'), _('Ebook Reader')] +tags = [_('Ebook'), _('Library'), _('Ebook reader')] diff --git a/plinth/modules/coturn/manifest.py b/plinth/modules/coturn/manifest.py index 777f09a21..779da3716 100644 --- a/plinth/modules/coturn/manifest.py +++ b/plinth/modules/coturn/manifest.py @@ -4,4 +4,4 @@ from django.utils.translation import gettext_lazy as _ backup = {'secrets': {'directories': ['/etc/coturn']}, 'services': ['coturn']} -tags = [_('VoIP'), _('STUN'), _('TURN')] +tags = [_('Video conference'), _('STUN'), _('TURN')] diff --git a/plinth/modules/deluge/manifest.py b/plinth/modules/deluge/manifest.py index 7335ea1b5..c3d4b0bbf 100644 --- a/plinth/modules/deluge/manifest.py +++ b/plinth/modules/deluge/manifest.py @@ -18,4 +18,4 @@ backup = { 'services': ['deluged', 'deluge-web'] } -tags = [_('File Sharing'), _('BitTorrent'), _('Client'), _('P2P')] +tags = [_('File sharing'), _('BitTorrent'), _('Web client'), _('P2P')] diff --git a/plinth/modules/ejabberd/manifest.py b/plinth/modules/ejabberd/manifest.py index 62108c4ce..825ac2e6c 100644 --- a/plinth/modules/ejabberd/manifest.py +++ b/plinth/modules/ejabberd/manifest.py @@ -121,10 +121,8 @@ backup = { } tags = [ + _('Encrypted messaging'), + _('Audio chat'), + _('Video chat'), _('XMPP'), - _('VoIP'), - _('IM'), - _('Encrypted Messaging'), - _('Audio Chat'), - _('Video Chat') ] diff --git a/plinth/modules/email/manifest.py b/plinth/modules/email/manifest.py index 6097000e2..8927516fb 100644 --- a/plinth/modules/email/manifest.py +++ b/plinth/modules/email/manifest.py @@ -79,4 +79,4 @@ backup = { 'services': ['postfix', 'dovecot', 'rspamd'] } -tags = [_('Email')] +tags = [_('Email server'), _('IMAP'), _('Spam control')] diff --git a/plinth/modules/featherwiki/manifest.py b/plinth/modules/featherwiki/manifest.py index 2855ee098..6e6d569fd 100644 --- a/plinth/modules/featherwiki/manifest.py +++ b/plinth/modules/featherwiki/manifest.py @@ -15,4 +15,4 @@ clients = [{ backup = {'data': {'directories': [str(wiki_dir)]}} -tags = [_('Wiki'), _('Note Taking'), _('Website'), _('Quine'), _('non-Debian')] +tags = [_('Wiki'), _('Note taking'), _('Website'), _('Quine'), _('Non-Debian')] diff --git a/plinth/modules/gitweb/manifest.py b/plinth/modules/gitweb/manifest.py index f48d69410..c5102c31e 100644 --- a/plinth/modules/gitweb/manifest.py +++ b/plinth/modules/gitweb/manifest.py @@ -34,4 +34,4 @@ clients = [ backup = {'data': {'directories': [GIT_REPO_PATH]}} -tags = [_('Git'), _('Version Control'), _('Dev Tool')] +tags = [_('Git hosting'), _('Version control'), _('Developer tool')] diff --git a/plinth/modules/i2p/manifest.py b/plinth/modules/i2p/manifest.py index 2f1f64beb..8e4cdda96 100644 --- a/plinth/modules/i2p/manifest.py +++ b/plinth/modules/i2p/manifest.py @@ -40,4 +40,4 @@ backup = { 'services': ['i2p'] } -tags = [_('Anonymity Network'), _('Censorship Resistance')] +tags = [_('Anonymity network'), _('Censorship resistance')] diff --git a/plinth/modules/infinoted/manifest.py b/plinth/modules/infinoted/manifest.py index fad1f5786..cc27ca983 100644 --- a/plinth/modules/infinoted/manifest.py +++ b/plinth/modules/infinoted/manifest.py @@ -43,4 +43,4 @@ backup = { 'services': ['infinoted'] } -tags = [_('Note Taking'), _('Collaborative Editing'), _('Gobby')] +tags = [_('Note taking'), _('Collaborative editing'), _('Gobby')] diff --git a/plinth/modules/janus/manifest.py b/plinth/modules/janus/manifest.py index 780f75efb..e8ece3120 100644 --- a/plinth/modules/janus/manifest.py +++ b/plinth/modules/janus/manifest.py @@ -13,4 +13,4 @@ clients = [{ backup: dict = {} -tags = [_('Video Conferencing'), _('WebRTC')] +tags = [_('Video conference'), _('WebRTC'), _('Web conference')] diff --git a/plinth/modules/jsxc/manifest.py b/plinth/modules/jsxc/manifest.py index 565924b3f..00dffa0c4 100644 --- a/plinth/modules/jsxc/manifest.py +++ b/plinth/modules/jsxc/manifest.py @@ -13,4 +13,4 @@ clients = [{ backup: dict = {} -tags = [_('XMPP'), _('Client')] +tags = [_('Web chat'), _('XMPP'), _('Client')] diff --git a/plinth/modules/kiwix/manifest.py b/plinth/modules/kiwix/manifest.py index f989f9cd8..536bb30a1 100644 --- a/plinth/modules/kiwix/manifest.py +++ b/plinth/modules/kiwix/manifest.py @@ -19,4 +19,9 @@ backup = { 'services': ['kiwix-server-freedombox'] } -tags = [_('Offline Reader'), _('Archival'), _('Censorship Resistance')] +tags = [ + _('Offline reader'), + _('Archival'), + _('Censorship resistance'), + _('Wikipedia') +] diff --git a/plinth/modules/matrixsynapse/manifest.py b/plinth/modules/matrixsynapse/manifest.py index fc7ad7da0..009529ea2 100644 --- a/plinth/modules/matrixsynapse/manifest.py +++ b/plinth/modules/matrixsynapse/manifest.py @@ -98,11 +98,9 @@ backup = { } tags = [ - _('Chat Room'), - _('Encrypted Messaging'), - _('IM'), - _('Audio Chat'), - _('Video Chat'), - _('Matrix'), - _('VoIP') + _('Chat room'), + _('Encrypted messaging'), + _('Audio chat'), + _('Video chat'), + _('Matrix server'), ] diff --git a/plinth/modules/minetest/manifest.py b/plinth/modules/minetest/manifest.py index 539a8c148..3963d7ce2 100644 --- a/plinth/modules/minetest/manifest.py +++ b/plinth/modules/minetest/manifest.py @@ -46,4 +46,4 @@ backup = { 'services': ['minetest-server'] } -tags = [_('Game'), _('Block Sandbox')] +tags = [_('Game server'), _('Block sandbox'), _('Platform')] diff --git a/plinth/modules/minidlna/manifest.py b/plinth/modules/minidlna/manifest.py index 007e8ce76..8f0f4d3e8 100644 --- a/plinth/modules/minidlna/manifest.py +++ b/plinth/modules/minidlna/manifest.py @@ -113,4 +113,4 @@ backup = { 'services': ['minidlna'] } -tags = [_('Media Server'), _('Television'), _('UPnP'), _('DLNA')] +tags = [_('Media server'), _('Television'), _('UPnP'), _('DLNA')] diff --git a/plinth/modules/miniflux/manifest.py b/plinth/modules/miniflux/manifest.py index f112c564b..7082fb720 100644 --- a/plinth/modules/miniflux/manifest.py +++ b/plinth/modules/miniflux/manifest.py @@ -135,4 +135,4 @@ backup = { 'services': ['miniflux'] } -tags = [_('Feed Reader'), _('News'), _('RSS'), _('ATOM')] +tags = [_('Feed reader'), _('News aggregation'), _('RSS'), _('ATOM')] diff --git a/plinth/modules/mumble/manifest.py b/plinth/modules/mumble/manifest.py index fa7843aed..b51c1d3e9 100644 --- a/plinth/modules/mumble/manifest.py +++ b/plinth/modules/mumble/manifest.py @@ -64,4 +64,4 @@ backup = { 'services': ['mumble-server'] } -tags = [_('Audio Chat'), _('VoIP')] +tags = [_('Audio chat'), _('Group conference'), _('Server')] diff --git a/plinth/modules/nextcloud/manifest.py b/plinth/modules/nextcloud/manifest.py index cf30d69d2..3b1e7172b 100644 --- a/plinth/modules/nextcloud/manifest.py +++ b/plinth/modules/nextcloud/manifest.py @@ -53,4 +53,4 @@ backup = { } } -tags = [_('Cloud Storage'), _('File Sharing'), _('non-Debian')] +tags = [_('File sync'), _('Sharing'), _('Groupware'), _('Non-Debian')] diff --git a/plinth/modules/openvpn/manifest.py b/plinth/modules/openvpn/manifest.py index cf7f2b613..68abee13d 100644 --- a/plinth/modules/openvpn/manifest.py +++ b/plinth/modules/openvpn/manifest.py @@ -57,4 +57,4 @@ clients = [{ }] }] -tags = [_('VPN'), _('Anonymity'), _('Remote Access')] +tags = [_('VPN server'), _('Privacy'), _('Remote access')] diff --git a/plinth/modules/privoxy/manifest.py b/plinth/modules/privoxy/manifest.py index f346fe0a0..1e236221c 100644 --- a/plinth/modules/privoxy/manifest.py +++ b/plinth/modules/privoxy/manifest.py @@ -7,4 +7,4 @@ from django.utils.translation import gettext_lazy as _ backup: dict = {} -tags = [_('Ad Blocker'), _('Proxy'), _('Local Network')] +tags = [_('Ad blocker'), _('Proxy server'), _('Local network')] diff --git a/plinth/modules/quassel/manifest.py b/plinth/modules/quassel/manifest.py index 63aa96256..df7c69112 100644 --- a/plinth/modules/quassel/manifest.py +++ b/plinth/modules/quassel/manifest.py @@ -51,4 +51,4 @@ backup = { 'services': ['quasselcore'], } -tags = [_('Chat Room'), _('IRC'), _('Client')] +tags = [_('Chat room'), _('IRC'), _('Client')] diff --git a/plinth/modules/radicale/manifest.py b/plinth/modules/radicale/manifest.py index b27bc7523..1a12e0e81 100644 --- a/plinth/modules/radicale/manifest.py +++ b/plinth/modules/radicale/manifest.py @@ -88,10 +88,4 @@ backup = { 'services': ['uwsgi'] } -tags = [ - _('Calendar'), - _('Contacts'), - _('Synchronization'), - _('CalDAV'), - _('CardDAV') -] +tags = [_('Calendar'), _('Contacts'), _('Server'), _('CalDAV'), _('CardDAV')] diff --git a/plinth/modules/roundcube/manifest.py b/plinth/modules/roundcube/manifest.py index 919f53bde..5ea6c3e97 100644 --- a/plinth/modules/roundcube/manifest.py +++ b/plinth/modules/roundcube/manifest.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: AGPL-3.0-or-later +"""Application manifest for roundcube.""" from django.utils.translation import gettext_lazy as _ @@ -19,4 +20,4 @@ backup = { } } -tags = [_('Email'), _('Contacts'), _('Client')] +tags = [_('Email'), _('Contacts'), _('Web client')] diff --git a/plinth/modules/rssbridge/manifest.py b/plinth/modules/rssbridge/manifest.py index cfe398c9f..cfc2675bc 100644 --- a/plinth/modules/rssbridge/manifest.py +++ b/plinth/modules/rssbridge/manifest.py @@ -1,9 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later +"""Application manifest for RSS-Bridge.""" from django.utils.translation import gettext_lazy as _ -""" -Application manifest for RSS-Bridge. -""" clients = [{ 'name': _('RSS-Bridge'), @@ -15,4 +13,4 @@ clients = [{ backup = {'data': {'files': ['/etc/rss-bridge/is_public']}} -tags = [_('Feed Generator'), _('News'), _('RSS'), _('ATOM')] +tags = [_('Feed generator'), _('News'), _('RSS'), _('ATOM')] diff --git a/plinth/modules/samba/manifest.py b/plinth/modules/samba/manifest.py index 4195c3753..6cdb6994b 100644 --- a/plinth/modules/samba/manifest.py +++ b/plinth/modules/samba/manifest.py @@ -85,4 +85,10 @@ clients = [{ backup: dict = {} -tags = [_('File Sharing'), _('Local Network')] +tags = [ + _('File sharing'), + _('Local network'), + _('Network drive'), + _('Media storage'), + _('Backup storage') +] diff --git a/plinth/modules/searx/manifest.py b/plinth/modules/searx/manifest.py index 5643f9354..66bd6f1af 100644 --- a/plinth/modules/searx/manifest.py +++ b/plinth/modules/searx/manifest.py @@ -14,4 +14,4 @@ PUBLIC_ACCESS_SETTING_FILE = '/etc/searx/allow_public_access' backup = {'config': {'files': [PUBLIC_ACCESS_SETTING_FILE]}} -tags = [_('Web Search'), _('Metasearch Engine')] +tags = [_('Web search'), _('Metasearch Engine')] diff --git a/plinth/modules/shaarli/manifest.py b/plinth/modules/shaarli/manifest.py index ae4a70823..6e34a337a 100644 --- a/plinth/modules/shaarli/manifest.py +++ b/plinth/modules/shaarli/manifest.py @@ -31,4 +31,4 @@ clients = [{ backup = {'data': {'directories': ['/var/lib/shaarli/data']}} -tags = [_('Bookmarks'), _('Link Blog'), _('Single User')] +tags = [_('Bookmarks'), _('Link blog'), _('Single user')] diff --git a/plinth/modules/shadowsocks/manifest.py b/plinth/modules/shadowsocks/manifest.py index 5715b73c1..337d5d270 100644 --- a/plinth/modules/shadowsocks/manifest.py +++ b/plinth/modules/shadowsocks/manifest.py @@ -15,9 +15,9 @@ backup = { } tags = [ - _('Proxy'), - _('Client'), - _('SOCKS5'), - _('Censorship Resistance'), - _('Shadowsocks') + _('Proxy server'), + _('Censorship resistance'), + _('Encrypted tunnel'), + _('Entry point'), + _('Shadowsocks'), ] diff --git a/plinth/modules/shadowsocksserver/manifest.py b/plinth/modules/shadowsocksserver/manifest.py index 0f34ea395..859e4c55d 100644 --- a/plinth/modules/shadowsocksserver/manifest.py +++ b/plinth/modules/shadowsocksserver/manifest.py @@ -14,4 +14,9 @@ backup = { 'services': ['shadowsocks-libev-server@fbxserver'] } -tags = [_('Proxy'), _('SOCKS5'), _('Censorship Resistance'), _('Shadowsocks')] +tags = [ + _('Censorship resistance'), + _('Encrypted tunnel'), + _('Exit point'), + _('Shadowsocks') +] diff --git a/plinth/modules/sharing/manifest.py b/plinth/modules/sharing/manifest.py index f7b839a21..f626db1a7 100644 --- a/plinth/modules/sharing/manifest.py +++ b/plinth/modules/sharing/manifest.py @@ -16,4 +16,4 @@ backup = { }] } -tags = [_('File Sharing')] +tags = [_('File sharing'), _('Web sharing')] diff --git a/plinth/modules/syncthing/manifest.py b/plinth/modules/syncthing/manifest.py index 2225a1a69..5b6c132fb 100644 --- a/plinth/modules/syncthing/manifest.py +++ b/plinth/modules/syncthing/manifest.py @@ -55,4 +55,4 @@ backup = { 'services': ['syncthing@syncthing'] } -tags = [_('Synchronization'), _('File Sharing'), _('Cloud Storage')] +tags = [_('File sync'), _('File sharing'), _('P2P')] diff --git a/plinth/modules/tiddlywiki/manifest.py b/plinth/modules/tiddlywiki/manifest.py index d7da627f0..c91dad349 100644 --- a/plinth/modules/tiddlywiki/manifest.py +++ b/plinth/modules/tiddlywiki/manifest.py @@ -17,11 +17,11 @@ backup = {'data': {'directories': [str(wiki_dir)]}} tags = [ _('Wiki'), - _('Note Taking'), + _('Note taking'), _('Website'), _('Journal'), - _('Digital Garden'), + _('Digital garden'), _('Zettelkasten'), _('Quine'), - _('non-Debian') + _('Non-Debian') ] diff --git a/plinth/modules/tor/manifest.py b/plinth/modules/tor/manifest.py index 3d3eafe96..93f70fc8b 100644 --- a/plinth/modules/tor/manifest.py +++ b/plinth/modules/tor/manifest.py @@ -54,8 +54,9 @@ backup = { } tags = [ + _('Onion services'), _('Relay'), - _('Anonymity Network'), - _('Censorship Resistance'), - _('Tor') + _('Anonymity network'), + _('Censorship resistance'), + _('Tor'), ] diff --git a/plinth/modules/torproxy/manifest.py b/plinth/modules/torproxy/manifest.py index 6378e8876..7d51fee96 100644 --- a/plinth/modules/torproxy/manifest.py +++ b/plinth/modules/torproxy/manifest.py @@ -52,9 +52,8 @@ backup = { } tags = [ - _('Proxy'), - _('SOCKS5'), - _('Anonymity Network'), - _('Censorship Resistance'), + _('Proxy server'), + _('Anonymity network'), + _('Censorship resistance'), _('Tor') ] diff --git a/plinth/modules/transmission/manifest.py b/plinth/modules/transmission/manifest.py index b99e3c49c..76e960db3 100644 --- a/plinth/modules/transmission/manifest.py +++ b/plinth/modules/transmission/manifest.py @@ -36,4 +36,4 @@ backup = { 'services': ['transmission-daemon'] } -tags = [_('File Sharing'), _('BitTorrent'), _('Client'), _('P2P')] +tags = [_('File sharing'), _('BitTorrent'), _('Web client'), _('P2P')] diff --git a/plinth/modules/ttrss/manifest.py b/plinth/modules/ttrss/manifest.py index 2510c0d3d..523b85870 100644 --- a/plinth/modules/ttrss/manifest.py +++ b/plinth/modules/ttrss/manifest.py @@ -52,4 +52,4 @@ backup = { 'services': ['tt-rss'] } -tags = [_('Feed Reader'), _('News'), _('RSS'), _('ATOM')] +tags = [_('Feed reader'), _('News aggregation'), _('RSS'), _('ATOM')] diff --git a/plinth/modules/wireguard/manifest.py b/plinth/modules/wireguard/manifest.py index 274783aa3..c7e9732e5 100644 --- a/plinth/modules/wireguard/manifest.py +++ b/plinth/modules/wireguard/manifest.py @@ -42,4 +42,4 @@ clients = [{ }] }] -tags = [_('VPN'), _('Anonymity'), _('Remote Access'), _('P2P')] +tags = [_('VPN client'), _('VPN server'), _('Privacy'), _('Remote access')] diff --git a/plinth/modules/wordpress/manifest.py b/plinth/modules/wordpress/manifest.py index 1928f26ad..ca90cf852 100644 --- a/plinth/modules/wordpress/manifest.py +++ b/plinth/modules/wordpress/manifest.py @@ -23,4 +23,4 @@ backup = { }, } -tags = [_('Website'), _('Blog')] +tags = [_('Website'), _('Blog'), _('Content management system')] diff --git a/plinth/modules/zoph/manifest.py b/plinth/modules/zoph/manifest.py index 81680b453..b973c0963 100644 --- a/plinth/modules/zoph/manifest.py +++ b/plinth/modules/zoph/manifest.py @@ -23,4 +23,4 @@ backup = { } } -tags = [_('Image Viewer'), _('Photo'), _('Library')] +tags = [_('Photo'), _('Organizer'), _('Web sharing')] diff --git a/plinth/tests/tags/test_functional.py b/plinth/tests/tags/test_functional.py index 85c8bd6f1..11ef3dd35 100644 --- a/plinth/tests/tags/test_functional.py +++ b/plinth/tests/tags/test_functional.py @@ -4,8 +4,8 @@ Functional, browser based tests for transmission app. """ import pytest - from selenium.webdriver.common.keys import Keys + from plinth.tests import functional pytestmark = [pytest.mark.tags] @@ -39,11 +39,11 @@ def test_search_for_tag(session_browser): search_input = session_browser.driver.find_element_by_id('add-tag-input') with functional.wait_for_page_update( session_browser, timeout=10, - expected_url='/plinth/apps/?tag=BitTorrent&tag=File%20Sharing'): + expected_url='/plinth/apps/?tag=BitTorrent&tag=File%20sharing'): search_input.click() search_input.send_keys('file') search_input.send_keys(Keys.ENTER) - for app in ['deluge', 'nextcloud', 'sharing', 'syncthing', 'transmission']: + for app in ['deluge', 'samba', 'sharing', 'syncthing', 'transmission']: _is_app_listed(session_browser, app) @@ -52,9 +52,9 @@ def test_click_on_tag(session_browser): search_input = session_browser.driver.find_element_by_id('add-tag-input') with functional.wait_for_page_update( session_browser, timeout=10, - expected_url='/plinth/apps/?tag=BitTorrent&tag=Cloud%20Storage'): + expected_url='/plinth/apps/?tag=BitTorrent&tag=File%20sync'): search_input.click() session_browser.find_by_css( - ".dropdown-item[data-value='Cloud Storage']").click() + ".dropdown-item[data-value='File sync']").click() for app in ['deluge', 'nextcloud', 'syncthing', 'transmission']: _is_app_listed(session_browser, app) From d605907bbec007484f53202caa999eb427183ee9 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 15 Oct 2024 15:45:16 -0700 Subject: [PATCH 36/56] context_processors: Use active menu urls to decide what to highlight - We are using submenu.url to check for specific URLs and then highlight a menu item. This is somewhat incorrect due to string search and not generic enough. We have another mechanism 'active_menu_urls' to perform this. Improve and use this instead. Signed-off-by: Sunil Mohan Adapa --- plinth/context_processors.py | 4 +++- plinth/templates/base.html | 8 ++++---- plinth/tests/test_context_processors.py | 12 ++++++------ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/plinth/context_processors.py b/plinth/context_processors.py index 01a4bbf8b..ddfc2af0f 100644 --- a/plinth/context_processors.py +++ b/plinth/context_processors.py @@ -27,7 +27,9 @@ def common(request): 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] + active_menu_urls = [ + request.path[:index + 1] for index in slash_indices[2:] + ] # Ignore the first two slashes '/plinth/apps/' return { 'cfg': cfg, 'submenu': menu.main_menu.active_item(request), diff --git a/plinth/templates/base.html b/plinth/templates/base.html index eb91fc4b6..680d93184 100644 --- a/plinth/templates/base.html +++ b/plinth/templates/base.html @@ -83,7 +83,7 @@ {% block mainmenu_left %} @@ -110,7 +110,7 @@
    - {% for section_item in submenu.sorted_items %} + {% for section_item in menu_items %}
    {{ section_item.name }}
    diff --git a/plinth/views.py b/plinth/views.py index ddd4eb312..5d555f6f2 100644 --- a/plinth/views.py +++ b/plinth/views.py @@ -181,8 +181,11 @@ class AppsIndexView(TemplateView): def system_index(request): """Serve the system index page.""" - return TemplateResponse(request, 'system.html', - {'advanced_mode': get_advanced_mode()}) + menu_items = menu.main_menu.active_item(request).sorted_items() + return TemplateResponse(request, 'system.html', { + 'advanced_mode': get_advanced_mode(), + 'menu_items': menu_items + }) class LanguageSelectionView(FormView): From 44aab658ed8519038dc54ee3e72ff91ba59759f3 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 15 Oct 2024 16:35:10 -0700 Subject: [PATCH 38/56] context_processors: Stop adding unused 'submenu' to context Signed-off-by: Sunil Mohan Adapa --- plinth/context_processors.py | 1 - plinth/tests/test_context_processors.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/plinth/context_processors.py b/plinth/context_processors.py index ddfc2af0f..8bef5d697 100644 --- a/plinth/context_processors.py +++ b/plinth/context_processors.py @@ -32,7 +32,6 @@ def common(request): ] # Ignore the first two slashes '/plinth/apps/' return { 'cfg': cfg, - 'submenu': menu.main_menu.active_item(request), 'active_menu_urls': active_menu_urls, 'box_name': _(cfg.box_name), 'user_is_admin': is_user_admin(request, True), diff --git a/plinth/tests/test_context_processors.py b/plinth/tests/test_context_processors.py index 50f7e9e13..94547d4ca 100644 --- a/plinth/tests/test_context_processors.py +++ b/plinth/tests/test_context_processors.py @@ -36,9 +36,6 @@ def test_common(Notification, load_cfg): assert response['box_name'] == 'FreedomBox' - submenu = response['submenu'] - assert submenu is None - urls = response['active_menu_urls'] assert urls is not None assert ['/plinth/aaa/', '/plinth/aaa/bbb/', '/plinth/aaa/bbb/ccc/'] == urls From 5ce7385f606edb38d59170f6386d61c7044b2975 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 16 Oct 2024 13:38:37 -0700 Subject: [PATCH 39/56] tags: css: Minor styling cleanups - Use CSS based styling instead of styling based on bootstrap classes. - Add a placeholder for the input box to easily locate it and convey what it does. - Drop dead code for '.tag-input .tag'. - Drop some repeated data- attributes by using DOM navigation. - Drop redundant styling. Signed-off-by: Sunil Mohan Adapa --- plinth/templates/apps.html | 31 +++++++++++++++--------------- static/tags.js | 2 +- static/themes/default/css/main.css | 27 ++++---------------------- 3 files changed, 21 insertions(+), 39 deletions(-) diff --git a/plinth/templates/apps.html b/plinth/templates/apps.html index 1d2cabf73..818867c29 100644 --- a/plinth/templates/apps.html +++ b/plinth/templates/apps.html @@ -11,29 +11,30 @@ {% block tags %} {% if tags %}
    -