diff --git a/HACKING.md b/HACKING.md
index a0cc9e4cd..1b69d3ab0 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -115,10 +115,9 @@ directory:
guest$ cd /freedombox
```
-Run the development version of FreedomBox Service (Plinth) from your source
-directory in the container using the following command. This command
-continuously deploys your code changes into the container providing a
-quick feedback cycle during development.
+Run the development version of FreedomBox Service in the container using the
+following command. This command continuously deploys your code changes into the
+container providing a quick feedback cycle during development.
```bash
guest$ freedombox-develop
diff --git a/actions/apache b/actions/apache
index 6b64c95c6..2fbfb2403 100755
--- a/actions/apache
+++ b/actions/apache
@@ -126,6 +126,13 @@ def subcommand_setup(arguments):
# Disable /server-status page to avoid leaking private info.
webserver.disable('status', kind='module')
+ # Enable HTTP/2 protocol
+ webserver.enable('http2', kind='module')
+
+ # Enable shared object cache needed for OSCP stapling. Needed by
+ # mod_ssl.
+ webserver.enable('socache_shmcb', kind='module')
+
# switch to mod_ssl from mod_gnutls
webserver.disable('gnutls', kind='module')
webserver.enable('ssl', kind='module')
diff --git a/container b/container
index c02e6b7b2..068de7962 100755
--- a/container
+++ b/container
@@ -130,7 +130,7 @@ from urllib.request import urlopen
URLS = {
'stable': 'https://ftp.freedombox.org/pub/freedombox/hardware/'
- 'amd64/stable/freedombox-stable-free_buster_all-amd64.img.xz',
+ 'amd64/bullseye/freedombox-bullseye-free_all-amd64.img.xz',
'testing': 'https://ftp.freedombox.org/pub/freedombox/hardware/'
'amd64/testing/freedombox-testing-free_dev_all-amd64.img.xz',
'unstable': 'https://ftp.freedombox.org/pub/freedombox/hardware/'
@@ -164,7 +164,7 @@ sudo apt-mark unhold freedombox
sudo DEBIAN_FRONTEND=noninteractive apt-get install --yes ncurses-term \
sshpass bash-completion
-echo 'alias freedombox-develop="sudo -u plinth /freedombox/run --develop"' \
+echo 'alias freedombox-develop="cd /freedombox; sudo -u plinth /freedombox/run --develop"' \
>> /home/fbx/.bashrc
# Make some pytest related files and directories writable to the fbx user
@@ -812,8 +812,16 @@ def _destroy(distribution):
connection_name = f'fbx-{distribution}-shared'
logger.info('Removing Network Manager connection %s', connection_name)
- subprocess.run(['sudo', 'nmcli', 'connection', 'delete', connection_name],
- stdout=subprocess.DEVNULL)
+ result = subprocess.run(
+ ['sudo', 'nmcli', 'connection', 'delete', connection_name],
+ capture_output=True)
+ if result.returncode not in (0, 10):
+ # nmcli failed and not due to 'Connection, device, or access point does
+ # not exist.' See
+ # https://developer-old.gnome.org/NetworkManager/stable/nmcli.html
+ logger.error('nmcli returned code %d', result.returncode)
+ logger.error('Error message:\n%s', result.stderr.decode())
+ logger.error('Output:\n%s', result.stdout.decode())
logger.info('Keeping downloaded image: %s',
_get_compressed_image_path(distribution))
@@ -920,9 +928,8 @@ def _get_latest_image_timestamp(distribution):
url = URLS[distribution]
response = urlopen(url[0:url.rindex('/')])
page_contents = response.read().decode()
- str_time = re.findall(r'\d{2}-[A-Z][a-z]{2}-\d{4} \d{2}:\d{2}',
- page_contents)[0]
- return datetime.datetime.strptime(str_time, '%d-%b-%Y %H:%M').timestamp()
+ str_time = re.findall(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}', page_contents)[0]
+ return datetime.datetime.strptime(str_time, '%Y-%m-%d %H:%M').timestamp()
def _is_update_required(distribution):
diff --git a/data/etc/apache2/conf-available/freedombox-tls-site-macro.conf b/data/etc/apache2/conf-available/freedombox-tls-site-macro.conf
index 8d5549b34..9f8389408 100644
--- a/data/etc/apache2/conf-available/freedombox-tls-site-macro.conf
+++ b/data/etc/apache2/conf-available/freedombox-tls-site-macro.conf
@@ -1,25 +1,6 @@
You will be suggested the most conservative actions. Вам буде запропоновано консервативніші дії.
Оберіть цей " +"пункт, якщо маршрутизатор наразі не налаштовано або неможливо налаштувати, і " +"нагадати про це пізніше. Деякі наступні кроки налаштування можуть бути " +"невдалими.
" #: plinth/modules/networks/templates/connection_show.html:24 #, python-format @@ -4410,28 +4407,32 @@ msgstr "" #: plinth/modules/networks/templates/internet_connectivity_main.html:23 msgid "My ISP provides a public IP address that does not change over time." msgstr "" +"Мій постачальник Інтернет-послуг надає публічну IP-адресу, яка не змінюється " +"протягом часу." #: plinth/modules/networks/templates/internet_connectivity_main.html:27 msgid "My ISP provides a public IP address that may change over time." msgstr "" +"Мій постачальник Інтернет-послуг надає публічну IP-адресу, яка може " +"змінюватися впродовж часу." #: plinth/modules/networks/templates/internet_connectivity_main.html:31 msgid "My ISP does not provide a public IP address." -msgstr "" +msgstr "Мій постачальник Інтернет-послуг не надає публічної IP-адерси." #: plinth/modules/networks/templates/internet_connectivity_main.html:35 msgid "I do not know the type of connection my ISP provides." -msgstr "" +msgstr "Я не знаю типу зʼєднання, яке надає мій постачальник Інтернет-послуг." #: plinth/modules/networks/templates/internet_connectivity_main.html:41 #: plinth/modules/networks/templates/network_topology_main.html:41 msgid "Update..." -msgstr "" +msgstr "Оновити..." #: plinth/modules/networks/templates/network_topology_content.html:10 #, python-format msgid "How is Your %(box_name)s Connected to the Internet?" -msgstr "Як ваш %(box_name)s підʼєднано до Інтернету?" +msgstr "Як Ваш %(box_name)s підʼєднано до Інтернету?" #: plinth/modules/networks/templates/network_topology_content.html:16 #, python-format @@ -4440,9 +4441,9 @@ msgid "" "your network. This information is used to guide you with further setup. It " "can be changed later." msgstr "" -"Виберіть пункт, що найкраще описує те, як ваш %(box_name)s підʼєднано до " +"Виберіть пункт, що найкраще описує те, як Ваш %(box_name)s підʼєднано до " "мережі. Ця інформація використовується лише для подальших вказівок " -"встановлення. Її можна змінити пізніше." +"установлення. Її можна змінити пізніше." #: plinth/modules/networks/templates/network_topology_main.html:9 #, python-format @@ -4477,6 +4478,8 @@ msgid "" "Your Internet connection is directly attached to your %(box_name)s and there " "are no other devices on the network." msgstr "" +"Ваше Інтернет-зʼєднання напряму підʼєднано до Вашого %(box_name)s і в мережі " +"нема інших пристроїв." #: plinth/modules/networks/templates/networks_configuration.html:24 msgid "" @@ -4517,13 +4520,13 @@ msgid "" "see options to overcome this limitation, choose 'no public address' option " "in Internet connection type selection." msgstr "" -"Якщо ви не контролюєте свій маршрутизатор, не налаштовуйте його. Щоб " -"побачити варіянти обходу цього обмеження, оберіть параметр 'немає публічної " +"Якщо Ви не контролюєте свій маршрутизатор, не налаштовуйте його. Щоб " +"побачити способи обходу цього обмеження, оберіть параметр 'немає публічної " "адреси' під час вибору типу зʼєднання з Інтернетом." #: plinth/modules/networks/templates/router_configuration_content.html:39 msgid "Choose How You Wish to Configure Your Router" -msgstr "Оберіть, як ви хочете сконфіґурувати свій роутер" +msgstr "Оберіть, як Ви бажаєте налаштувати свій маршрутизатор" #: plinth/modules/networks/templates/router_configuration_content.html:42 msgid "" @@ -4542,13 +4545,12 @@ msgid "" "model number and search online for the router's manual. This will provide " "full instructions on how to perform this task." msgstr "" -"Імʼя користувача й пароль конфіґурується під час першого налаштування " +"Імʼя користувача і пароль налаштовується під час першого налаштування " "маршрутизатора. Для багатьох маршрутизаторів ця інформація роздрукована на " -"задній частині маршрутизатора. Якщо ви не памʼятаєте параметри або IP-адресу " -"свого маршрутизатора, ви можливо захочете скинути налаштування та " -"налаштувати наново. Дізнайтеся номер моделі вашого маршрутизатора й " -"пошукайте в мережі посібник для нього. Це може надати повну інструкцію для " -"виконання цього завдання." +"задній частині маршрутизатора. Якщо Ви не памʼятаєте параметри або IP-адресу " +"свого маршрутизатора, можете скинути налаштування та налаштувати наново. " +"Дізнайтеся номер моделі свого маршрутизатора й пошукайте в мережі посібник " +"для нього. Це надасть повну інструкцію для виконання цього завдання." #: plinth/modules/networks/views.py:27 msgid "disabled" @@ -4806,7 +4808,7 @@ msgstr "" #: plinth/modules/openvpn/__init__.py:58 msgid "Connect to VPN services" -msgstr "Підʼєднатися до сервісів VPN" +msgstr "Підʼєднання до сервісів VPN" #: plinth/modules/openvpn/__init__.py:61 plinth/modules/openvpn/manifest.py:17 msgid "OpenVPN" @@ -4815,7 +4817,7 @@ msgstr "OpenVPN" #: plinth/modules/openvpn/__init__.py:62 #: plinth/modules/wireguard/__init__.py:51 msgid "Virtual Private Network" -msgstr "" +msgstr "Віртуальна приватна мережа" #: plinth/modules/openvpn/__init__.py:73 #, python-brace-format @@ -4900,7 +4902,7 @@ msgstr "" #, python-brace-format msgid "{box_name} is connected to a (wireless) router which you don't control." msgstr "" -"{box_name} підʼєднано до (бездротового) маршрутизатора, яким ви не можете " +"{box_name} підʼєднано до (бездротового) маршрутизатора, яким Ви не можете " "керувати." #: plinth/modules/pagekite/__init__.py:38 @@ -4908,16 +4910,20 @@ msgid "" "Your ISP does not provide you an external IP address and instead provides " "Internet connection through NAT." msgstr "" +"Ваш постачальник Інтернет-послуг не надає Вам зовнішньої IP-адреси, а надає " +"Інтернет-зʼєднання через NAT." #: plinth/modules/pagekite/__init__.py:40 msgid "" "Your ISP does not provide you a static IP address and your IP address " "changes every time you connect to Internet." msgstr "" +"Ваш постачальник Інтернет-послуг не надає Вам статичної IP-адреси і Ваша IP-" +"адреса змінюється кожного разу, коли ви підʼєднуєтеся до Інтернету." #: plinth/modules/pagekite/__init__.py:42 msgid "Your ISP limits incoming connections." -msgstr "" +msgstr "Ваш ISP обмежує вхідні зʼєднання." #: plinth/modules/pagekite/__init__.py:44 #, python-brace-format @@ -4934,7 +4940,7 @@ msgstr "PageKite" #: plinth/modules/pagekite/__init__.py:66 msgid "Public Visibility" -msgstr "Громадська видимість" +msgstr "Публічна видимість" #: plinth/modules/pagekite/__init__.py:76 msgid "PageKite Domain" @@ -4949,6 +4955,8 @@ msgid "" "Select your pagekite server. Set \"pagekite.net\" to use the default " "pagekite.net server." msgstr "" +"Виберіть Ваш сервер для pagekite. Вкажіть \"pagekite.net\", щоб " +"використовувати типовий сервер pagekite.net." #: plinth/modules/pagekite/forms.py:37 plinth/modules/shadowsocks/forms.py:39 msgid "Server port" @@ -5120,6 +5128,8 @@ msgid "" "Are you sure you want to restart? You will not be able to access this web " "interface for a few minutes until the system is restarted." msgstr "" +"Ви справді хочете перезавантажити систему? Ви не матимете доступ до " +"вебінтерфейсу протягом кількох хвилин, поки система не перезавантажиться." #: plinth/modules/power/templates/power_restart.html:34 msgid "" @@ -5137,6 +5147,8 @@ msgid "" "Are you sure you want to shut down? You will not be able to access this web " "interface after shut down." msgstr "" +"Ви справді хочете вимкнути систему? Ви не матимете доступу до вебінтерфейсу " +"після вимкнення." #: plinth/modules/power/templates/power_shutdown.html:33 msgid "" @@ -5172,7 +5184,7 @@ msgstr "Privoxy" #: plinth/modules/privoxy/__init__.py:57 msgid "Web Proxy" -msgstr "" +msgstr "Веб-проксі" #: plinth/modules/privoxy/__init__.py:115 #, python-brace-format @@ -5248,7 +5260,7 @@ msgstr "Radicale" #: plinth/modules/radicale/__init__.py:56 msgid "Calendar and Addressbook" -msgstr "Календар й Адресна книга" +msgstr "Календар і адресна книга" #: plinth/modules/radicale/forms.py:14 msgid "Only the owner of a calendar/addressbook can view or make changes." @@ -5326,9 +5338,10 @@ msgid "" "manipulation, message searching and spell checking." msgstr "" "Вебпошта Roundcube — це багатомовний клієнт IMAP на основі вебоглядача з " -"користувацьким інтерфейсом схожим на застосунок. Він надає весь функціонал, " -"який ви очікуєте від клієнта ел. пошти, враховуючи підтримку MIME, адресної " -"книжки, маніпулюванням теками, пошуком повідомлень та перевіркою правопису." +"користувацьким інтерфейсом, що виглядає як застосунок. Він надає весь " +"функціонал, який Ви очікуєте від клієнта ел. пошти, враховуючи підтримку " +"MIME, адресної книжки, маніпулювання теками, пошуку повідомлень та перевірки " +"правопису." #: plinth/modules/roundcube/__init__.py:28 msgid "" @@ -5338,6 +5351,11 @@ msgid "" "(recommended), fill the server field likeimaps://imap.example.com"
"code>."
msgstr ""
+"Можете використовувати його вказавши імʼя користувача і пароль обліківки "
+"електронної пошти, до якої Ви хочете отримати доступ за доменною назвою "
+"сервера IMAP від Вашого постачальника електронної пошти, наприклад "
+"imap.example.com. Для IMAP через SSL (рекомендується), поле "
+"сервера виглядає як imaps://imap.example.com."
#: plinth/modules/roundcube/__init__.py:33
msgid ""
@@ -5348,6 +5366,11 @@ msgid ""
"lesssecureapps\">https://www.google.com/settings/security/lesssecureapps"
"a>)."
msgstr ""
+"Для Gmail, імʼям користувача буде адреса Gmail, паролем – ваш пароль "
+"обліківки Google і сервером – imaps://imap.gmail.com. Зауважте, "
+"що потрібно дозволити \"Малозахищені додатки\" в налаштуваннях обліківки "
+"Google (https://www.google.com/settings/security/lesssecureapps)."
#: plinth/modules/roundcube/__init__.py:56
msgid "Email Client"
@@ -5386,7 +5409,7 @@ msgstr ""
#: plinth/modules/samba/__init__.py:59
msgid "Access to the private shares"
-msgstr ""
+msgstr "Доступ до приватних поширень"
#: plinth/modules/samba/__init__.py:62
msgid "Samba"
@@ -5505,20 +5528,20 @@ msgid ""
"Searx is a privacy-respecting Internet metasearch engine. It aggregrates and "
"displays results from multiple search engines."
msgstr ""
-"Searx — це метапошукова система в Інтернеті, що поважає приватність. Вона "
-"збирає та відображає результати з різних пошукових систем."
+"Searx — це система збірного пошуку в Інтернеті, яка поважає приватність. "
+"Вона збирає і відображає результати з різних пошукових систем."
#: plinth/modules/searx/__init__.py:27
msgid ""
"Searx can be used to avoid tracking and profiling by search engines. It "
"stores no cookies by default."
msgstr ""
-"Searx можна використовувати для обходу стеження та профілювання пошуковими "
+"Searx може використовуватися для обходу стеження та профілювання пошуковими "
"системами."
#: plinth/modules/searx/__init__.py:45
msgid "Search the web"
-msgstr "Пошук в інтернеті"
+msgstr "Пошук в Інтернеті"
#: plinth/modules/searx/__init__.py:48 plinth/modules/searx/manifest.py:6
msgid "Searx"
@@ -5553,11 +5576,11 @@ msgstr "Дозволити публічний доступ"
#: plinth/modules/searx/forms.py:19
msgid "Allow this application to be used by anyone who can reach it."
msgstr ""
-"Дозволити використовувати цей застосунок кожному, хто має до нього доступ."
+"Дозволити користуватися цим застосуноком кожному, хто має до нього доступ."
#: plinth/modules/security/forms.py:13
msgid "Restrict console logins (recommended)"
-msgstr ""
+msgstr "Обмежувати вхід до консолі (рекомендовано)"
#: plinth/modules/security/forms.py:14
msgid ""
@@ -5565,6 +5588,9 @@ msgid ""
"to log in to console or via SSH. Console users may be able to access some "
"services without further authorization."
msgstr ""
+"Коли опція ввімкнена — лише користувачі з групи \"admin\" можуть входити до "
+"консолі або через SSH. Користувачі консолі мають можливість доступу до "
+"деяких сервісів без додаткової авторизації."
#: plinth/modules/security/forms.py:19
msgid "Fail2Ban (recommended)"
@@ -5576,6 +5602,8 @@ msgid ""
"attempts to the SSH server and other enabled password protected internet-"
"services."
msgstr ""
+"Коли опція ввімкнена — Fail2Ban обмежуватиме спроби вторгнення за допомогою "
+"грубого перебору до сервера SSH та інших захищених паролем Інтернет-сервісів."
#: plinth/modules/security/templates/security.html:12
#: plinth/modules/security/templates/security.html:14
@@ -5684,7 +5712,7 @@ msgstr "Оновлено конфіґурацію безпеки"
#: plinth/modules/shaarli/__init__.py:20
msgid "Shaarli allows you to save and share bookmarks."
-msgstr "Shaarli дозволяє зберігати й ділитися закладками."
+msgstr "Shaarli дозволяє зберігати і ділитися закладками."
#: plinth/modules/shaarli/__init__.py:21
msgid ""
@@ -5788,7 +5816,7 @@ msgstr "Шлях до ділянки"
#: plinth/modules/sharing/forms.py:25
msgid "Disk path to a folder on this server that you intend to share."
-msgstr "Шлях до теки на сервері, якою ви маєте намір ділитися."
+msgstr "Шлях до теки на сервері, якою Ви маєте намір ділитися."
#: plinth/modules/sharing/forms.py:28
msgid "Public share"
@@ -5823,7 +5851,7 @@ msgstr "Додати ділянку"
#: plinth/modules/sharing/templates/sharing.html:27
msgid "No shares currently configured."
-msgstr "Поки що немає налаштованих спільних ділянок."
+msgstr "Поки що нема налаштованих спільних ділянок."
#: plinth/modules/sharing/templates/sharing.html:34
msgid "Disk Path"
@@ -5867,6 +5895,9 @@ msgid ""
"can be used to roll back the system to a previously known good state in case "
"of unwanted changes to the system."
msgstr ""
+"Зрізи дозволяють створювати і керувати зрізами файлової системи btrfs. Це "
+"можна використовувати для відкочування системи до попереднього хорошого "
+"стану в разі небажаних змін системи."
#: plinth/modules/snapshot/__init__.py:31
#, no-python-format
@@ -5875,6 +5906,9 @@ msgid ""
"and after a software installation. Older snapshots will be automatically "
"cleaned up according to the settings below."
msgstr ""
+"Зрізи робляться періодично (відомі як зрізи за часом) та після і до "
+"встановлення ПЗ. Старі зрізи автоматично видалятимуться відповідно до "
+"налаштувань нижче."
#: plinth/modules/snapshot/__init__.py:34
msgid ""
@@ -5882,14 +5916,17 @@ msgid ""
"partition only. Snapshots are not a replacement for backups since they can only be stored on the same partition. "
msgstr ""
+"Наразі зрізи працюють лише на файлових системах btrfs і розділі root. Зрізи "
+"не є заміною резервних копій, поки вони "
+"не зберігаються на одному розділі. "
#: plinth/modules/snapshot/__init__.py:56
msgid "Storage Snapshots"
-msgstr ""
+msgstr "Зрізи сховища"
#: plinth/modules/snapshot/forms.py:12
msgid "Free Disk Space to Maintain"
-msgstr ""
+msgstr "Вільний дисковий простір для утримання"
#: plinth/modules/snapshot/forms.py:13
msgid ""
@@ -5897,69 +5934,76 @@ msgid ""
"below this value, older snapshots are removed until this much free space is "
"regained. The default value is 30%."
msgstr ""
+"Зберігати цей відсоток вільного простору на диску. Якщо вільний простір "
+"падає нижче цього значення, старі зрізи вилучаються, поки не відновиться "
+"заданий обʼєм вільного простору. Типове значення — 30%."
#: plinth/modules/snapshot/forms.py:20
msgid "Timeline Snapshots"
-msgstr ""
+msgstr "Зрізи за часом"
#: plinth/modules/snapshot/forms.py:21
msgid ""
"Enable or disable timeline snapshots (hourly, daily, monthly and yearly)."
msgstr ""
+"Дозволити або заборонити зрізи за часом (щогодини, щоденно, щомісячно і "
+"щорічно)."
#: plinth/modules/snapshot/forms.py:26
msgid "Software Installation Snapshots"
-msgstr ""
+msgstr "Зрізи встановлення ПЗ"
#: plinth/modules/snapshot/forms.py:27
msgid "Enable or disable snapshots before and after software installation"
-msgstr ""
+msgstr "Дозволити або заборонити зрізи перед і після встановлення програм"
#: plinth/modules/snapshot/forms.py:32
msgid "Hourly Snapshots Limit"
-msgstr ""
+msgstr "Обмеження щогодинних зрізів"
#: plinth/modules/snapshot/forms.py:33
msgid "Keep a maximum of this many hourly snapshots."
-msgstr ""
+msgstr "Найбільша кількість щогодинних зрізів для зберігання."
#: plinth/modules/snapshot/forms.py:36
msgid "Daily Snapshots Limit"
-msgstr ""
+msgstr "Обмеження щоденних зрізів"
#: plinth/modules/snapshot/forms.py:37
msgid "Keep a maximum of this many daily snapshots."
-msgstr ""
+msgstr "Зберігати не більше стількох щоденних зрізів."
#: plinth/modules/snapshot/forms.py:40
msgid "Weekly Snapshots Limit"
-msgstr ""
+msgstr "Обмеження щотижневих зрізів"
#: plinth/modules/snapshot/forms.py:41
msgid "Keep a maximum of this many weekly snapshots."
-msgstr ""
+msgstr "Зберігати не більше стількох щотижневих зрізів."
#: plinth/modules/snapshot/forms.py:44
msgid "Monthly Snapshots Limit"
-msgstr ""
+msgstr "Обмеження щомісячних зрізів"
#: plinth/modules/snapshot/forms.py:45
msgid "Keep a maximum of this many monthly snapshots."
-msgstr ""
+msgstr "Зберігати не більше стількох щомісячних зрізів."
#: plinth/modules/snapshot/forms.py:48
msgid "Yearly Snapshots Limit"
-msgstr ""
+msgstr "Обмеження щорічних зрізів"
#: plinth/modules/snapshot/forms.py:49
msgid ""
"Keep a maximum of this many yearly snapshots. The default value is 0 (keep "
"no yearly snapshot)."
msgstr ""
+"Зберігати не більше стількох щорічних зрізів. Типове значення — 0 (не "
+"зберігати щорічних зрізів)."
#: plinth/modules/snapshot/templates/snapshot_delete_selected.html:12
msgid "Delete the following snapshots permanently?"
-msgstr ""
+msgstr "Видалити наступні зрізи назавжди?"
#: plinth/modules/snapshot/templates/snapshot_delete_selected.html:17
#: plinth/modules/snapshot/templates/snapshot_manage.html:27
@@ -5977,15 +6021,15 @@ msgstr "Дата"
#: plinth/modules/snapshot/templates/snapshot_manage.html:20
#: plinth/modules/snapshot/views.py:198
msgid "Delete Snapshots"
-msgstr ""
+msgstr "Видалити зріз"
#: plinth/modules/snapshot/templates/snapshot_manage.html:17
msgid "Create Snapshot"
-msgstr ""
+msgstr "Створити зріз"
#: plinth/modules/snapshot/templates/snapshot_manage.html:30
msgid "Rollback"
-msgstr ""
+msgstr "Відкат"
#: plinth/modules/snapshot/templates/snapshot_manage.html:40
msgid "will be used at next boot"
@@ -5998,7 +6042,7 @@ msgstr "використовується"
#: plinth/modules/snapshot/templates/snapshot_manage.html:54
#, python-format
msgid "Rollback to snapshot #%(number)s"
-msgstr ""
+msgstr "Відкотити до зрізу #%(number)s"
#: plinth/modules/snapshot/templates/snapshot_not_supported.html:11
#, python-format
@@ -6009,7 +6053,7 @@ msgstr ""
#: plinth/modules/snapshot/templates/snapshot_rollback.html:12
msgid "Roll back the system to this snapshot?"
-msgstr ""
+msgstr "Відкотити систему до цього зрізу?"
#: plinth/modules/snapshot/templates/snapshot_rollback.html:15
msgid ""
@@ -6029,7 +6073,7 @@ msgstr "створено вручну"
#: plinth/modules/snapshot/views.py:29
msgid "timeline"
-msgstr ""
+msgstr "за часом"
#: plinth/modules/snapshot/views.py:30
msgid "apt"
@@ -6037,37 +6081,37 @@ msgstr "apt"
#: plinth/modules/snapshot/views.py:41
msgid "Manage Snapshots"
-msgstr ""
+msgstr "Керування зрізами"
#: plinth/modules/snapshot/views.py:90
msgid "Created snapshot."
-msgstr ""
+msgstr "Створено зріз."
#: plinth/modules/snapshot/views.py:153
msgid "Storage snapshots configuration updated"
-msgstr ""
+msgstr "Налаштування зрізів сховища оновлено"
#: plinth/modules/snapshot/views.py:157 plinth/modules/tor/views.py:60
#, python-brace-format
msgid "Action error: {0} [{1}] [{2}]"
-msgstr ""
+msgstr "Помилка дії: {0} [{1}] [{2}]"
#: plinth/modules/snapshot/views.py:185
msgid "Deleted selected snapshots"
-msgstr ""
+msgstr "Видалити вибрані зрізи"
#: plinth/modules/snapshot/views.py:190
msgid "Snapshot is currently in use. Please try again later."
-msgstr ""
+msgstr "Зріз зараз використовується. Повторіть пізніше."
#: plinth/modules/snapshot/views.py:209
#, python-brace-format
msgid "Rolled back to snapshot #{number}."
-msgstr ""
+msgstr "Відкочено до зрізу #{number}."
#: plinth/modules/snapshot/views.py:212
msgid "The system must be restarted to complete the rollback."
-msgstr ""
+msgstr "Систему потрібно перезапустити, щоб завершити відкат."
#: plinth/modules/snapshot/views.py:224
msgid "Rollback to Snapshot"
@@ -6137,6 +6181,9 @@ msgid ""
"You can view the storage media currently in use, mount and unmount removable "
"media, expand the root partition etc."
msgstr ""
+"Цей модуль дозволяє Вам керувати медія-сховищем, яке привʼязане до "
+"{box_name}. Ви можете переглядати медія-сховище під час використання, "
+"монтувати і відмонтовувати знімні накопичувачі, розширювати розділ root тощо."
#: plinth/modules/storage/__init__.py:53 plinth/modules/storage/__init__.py:316
#: plinth/modules/storage/__init__.py:347
@@ -6282,11 +6329,11 @@ msgstr ""
#: plinth/modules/storage/templates/storage.html:17
msgid "The following storage devices are in use:"
-msgstr ""
+msgstr "Використовуються наступні пристрої сховища:"
#: plinth/modules/storage/templates/storage.html:24
msgid "Label"
-msgstr ""
+msgstr "Мітка"
#: plinth/modules/storage/templates/storage.html:25
msgid "Mount Point"
@@ -6294,7 +6341,7 @@ msgstr "Точка монтування"
#: plinth/modules/storage/templates/storage.html:27
msgid "Used"
-msgstr ""
+msgstr "Використано"
#: plinth/modules/storage/templates/storage.html:77
msgid "Partition Expansion"
@@ -6319,6 +6366,8 @@ msgid ""
"Advanced storage operations such as disk partitioning and RAID management "
"are provided by the Cockpit app."
msgstr ""
+"Розширені операції сховища, як розділення диска чи керування RAID, надаються "
+"застосунком Cockpit."
#: plinth/modules/storage/templates/storage_expand.html:14
#, python-format
@@ -6373,7 +6422,7 @@ msgstr ""
#: plinth/modules/syncthing/__init__.py:58
msgid "Administer Syncthing application"
-msgstr ""
+msgstr "Адміністрування програми Syncthing"
#: plinth/modules/syncthing/__init__.py:62
#: plinth/modules/syncthing/manifest.py:12
@@ -6406,7 +6455,7 @@ msgstr ""
#: plinth/modules/tahoe/__init__.py:64
msgid "Distributed File Storage"
-msgstr ""
+msgstr "Розподілене файлове сховище"
#: plinth/modules/tahoe/templates/tahoe-post-setup.html:17
#, python-format
@@ -6447,6 +6496,10 @@ msgid ""
"the Tor Browser."
msgstr ""
+"Tor — система анонімної комунікації. Дізнатися більше можете на вебсайті Tor Project. Для кращого захисту "
+"під час вебсерфінгу, проєкт Tor радить використовувати Tor Browser."
#: plinth/modules/tor/__init__.py:55
msgid "Tor"
@@ -6597,6 +6650,8 @@ msgid ""
"If your %(box_name)s is behind a router or firewall, you should make sure "
"the following ports are open, and port-forwarded, if necessary:"
msgstr ""
+"Якщо Ваш %(box_name)s за маршрутизатором або фаєрволом, Ви повинні "
+"переконатися, що наступні порти відкриті і перенаправлені, якщо необхідно:"
#: plinth/modules/tor/templates/tor.html:87
msgid "SOCKS"
@@ -6609,7 +6664,7 @@ msgstr ""
#: plinth/modules/tor/views.py:137 plinth/views.py:218
msgid "Setting unchanged"
-msgstr "Налаштування незмінено"
+msgstr "Налаштування не змінено"
#: plinth/modules/transmission/__init__.py:29
msgid "Transmission is a BitTorrent client with a web interface."
@@ -6724,7 +6779,7 @@ msgstr "Дозволити автооновлення"
#: plinth/modules/upgrades/forms.py:16
msgid "When enabled, FreedomBox automatically updates once a day."
-msgstr "Якщо ввімкнено, FreedomBox автоматично оновлятиметься раз у день."
+msgstr "Коли ввімкнено — FreedomBox автоматично оновлятиметься раз на день."
#: plinth/modules/upgrades/forms.py:19
msgid "Enable auto-update to next stable release"
@@ -6735,6 +6790,8 @@ msgid ""
"When enabled, FreedomBox will update to the next stable distribution release "
"when it is available."
msgstr ""
+"Коли ввімкнено — FreedomBox оновлятиметься до наступного стабільного випуску "
+"дистрибутиву, якщо він доступний."
#: plinth/modules/upgrades/forms.py:34
#: plinth/modules/upgrades/templates/upgrades_configure.html:89
@@ -6768,9 +6825,9 @@ msgid ""
"this web interface may be temporarily unavailable and show an error. In that "
"case, refresh the page to continue."
msgstr ""
-"Це може зайняти багато часу. Протягом оновлення "
-"вебінтерфейс може бути тимчасово недоступним і показувати помилку. В такому "
-"випадку оновіть сторінку і продовжіть."
+"Це може зайняти багато часу. Під час оновлення вебінтерфейс "
+"може бути тимчасово недоступним і показувати помилку. В такому випадку "
+"оновіть сторінку і продовжіть."
#: plinth/modules/upgrades/templates/update-firstboot-progress.html:31
#, python-format
@@ -6859,27 +6916,27 @@ msgstr ""
#: plinth/modules/upgrades/views.py:71
msgid "Automatic upgrades enabled"
-msgstr ""
+msgstr "Дозволено автоматичні оновлення"
#: plinth/modules/upgrades/views.py:74
msgid "Automatic upgrades disabled"
-msgstr ""
+msgstr "Вимкнено автоматичні оновлення"
#: plinth/modules/upgrades/views.py:82
msgid "Distribution upgrade enabled"
-msgstr ""
+msgstr "Дозволено оновлення дистрибутиву"
#: plinth/modules/upgrades/views.py:85
msgid "Distribution upgrade disabled"
-msgstr ""
+msgstr "Вимкнено оновлення дистрибутиву"
#: plinth/modules/upgrades/views.py:126
msgid "Upgrade process started."
-msgstr ""
+msgstr "Процес оновлення розпочато."
#: plinth/modules/upgrades/views.py:128
msgid "Starting upgrade failed."
-msgstr ""
+msgstr "Не вдалося розпочати оновлення."
#: plinth/modules/upgrades/views.py:138
msgid "Frequent feature updates activated."
@@ -6891,6 +6948,10 @@ msgid ""
"authentication mechanism for most apps. Some apps further require a user "
"account to be part of a group to authorize the user to access the app."
msgstr ""
+"Створення і керування обліковими записами користувачів. Ці обліківки "
+"обробляються як централізований механізм автентифікації для більшості "
+"застосунків. Деякі застосунки також вимагають, щоб обліківка була частиною "
+"групи, щоб отримати авторизований доступ до застосунку."
#: plinth/modules/users/__init__.py:43
#, python-brace-format
@@ -6906,7 +6967,7 @@ msgstr ""
#: plinth/modules/users/__init__.py:64
msgid "Users and Groups"
-msgstr "Користувачі й групи"
+msgstr "Користувачі і групи"
#: plinth/modules/users/__init__.py:77
msgid "Access to all services and system settings"
@@ -6937,7 +6998,7 @@ msgstr "Пароль для авторизації"
#: plinth/modules/users/forms.py:80
msgid "Enter your current password to authorize account modifications."
-msgstr ""
+msgstr "Уведіть свій поточний пароль для авторизування змін обліківки."
#: plinth/modules/users/forms.py:88
msgid "Invalid password."
@@ -6964,7 +7025,7 @@ msgstr "Не вдалося додати нового користувача д
#: plinth/modules/users/forms.py:177
msgid "Authorized SSH Keys"
-msgstr "Авторизовані ключі SSH"
+msgstr "Ключі SSH для авторизації"
#: plinth/modules/users/forms.py:179
msgid ""
@@ -6972,6 +7033,9 @@ msgid ""
"system without using a password. You may enter multiple keys, one on each "
"line. Blank lines and lines starting with # will be ignored."
msgstr ""
+"Налаштування публічного ключа SSH дозволяє користувачеві безпечно заходити в "
+"систему без використання пароля. Ви можете вказати декілька ключів, один на "
+"кожен рядок. Порожні рядки і рядки, що починаються на # іґноруються."
#: plinth/modules/users/forms.py:266
msgid "Renaming LDAP user failed."
@@ -7013,7 +7077,7 @@ msgstr "Не вдалося обмежити доступ до консолі: {
#: plinth/modules/users/forms.py:437
msgid "User account created, you are now logged in"
-msgstr "Обліківку користувача створено, ви ввійшли в систему"
+msgstr "Обліківку користувача створено, Ви ввійшли в систему"
#: plinth/modules/users/templates/users_change_password.html:11
#, python-format
@@ -7101,7 +7165,7 @@ msgstr "Видалити користувача %(username)s"
#: plinth/modules/users/templates/users_update.html:11
#, python-format
msgid "Edit User %(username)s"
-msgstr "Змінити користувача %(username)s"
+msgstr "Зміни користувача %(username)s"
#: plinth/modules/users/templates/users_update.html:19
#, python-format
@@ -7109,6 +7173,8 @@ msgid ""
"Use the change password form to "
"change the password."
msgstr ""
+"Щоб змінити пароль використовуйте форму "
+"зміни пароля."
#: plinth/modules/users/templates/users_update.html:31
#: plinth/templates/language-selection.html:17
@@ -7127,7 +7193,7 @@ msgstr "Користувача %(username)s оновлено."
#: plinth/modules/users/views.py:77
msgid "Edit User"
-msgstr "Змінити користувача"
+msgstr "Зміни користувача"
#: plinth/modules/users/views.py:132
#, python-brace-format
@@ -7156,6 +7222,8 @@ msgid ""
"It can be used to connect to a VPN provider which supports WireGuard, and to "
"route all outgoing traffic from {box_name} through the VPN."
msgstr ""
+"Може використовуватися для зʼєднання з постачальником VPN, що підтримує "
+"WireGuard, і перенаправлення всього вихідного трафіку {box_name} через VPN."
#: plinth/modules/wireguard/__init__.py:29
#, python-brace-format
@@ -7164,6 +7232,9 @@ msgid ""
"travelling. While connected to a public Wi-Fi network, all traffic can be "
"securely relayed through {box_name}."
msgstr ""
+"Інше використання — підʼєднати мобільний пристрій до {box_name} під час "
+"подорожі. Коли підʼєднано до публічної мережі Wi-Fi, увесь трафік може "
+"безпечно передаватися через {box_name}."
#: plinth/modules/wireguard/forms.py:32
msgid "Invalid key."
@@ -7317,7 +7388,7 @@ msgstr "Додати клієнта"
#: plinth/modules/wireguard/templates/wireguard_delete_client.html:14
msgid "Are you sure that you want to delete this client?"
-msgstr "Ви дійсно бажаєте видалити цей клієнт?"
+msgstr "Ви дійсно бажаєте видалити даний клієнт?"
#: plinth/modules/wireguard/templates/wireguard_delete_server.html:14
msgid "Are you sure that you want to delete this server?"
@@ -7485,22 +7556,16 @@ msgstr ""
#: plinth/modules/wordpress/__init__.py:69
#: plinth/modules/wordpress/manifest.py:6
-#, fuzzy
-#| msgid "Address"
msgid "WordPress"
-msgstr "Адреса"
+msgstr "WordPress"
#: plinth/modules/wordpress/__init__.py:70
-#, fuzzy
-#| msgid "Wiki and Blog"
msgid "Website and Blog"
-msgstr "Вікі та блоґ"
+msgstr "Вебсайт і блоґ"
#: plinth/modules/wordpress/forms.py:14
-#, fuzzy
-#| msgid "public access"
msgid "Public access"
-msgstr "публічний доступ"
+msgstr "Публічний доступ"
#: plinth/modules/wordpress/forms.py:15
msgid ""
@@ -7569,11 +7634,11 @@ msgstr ""
#: plinth/package.py:136
msgid "Error during installation"
-msgstr "Помилка під час встановлення"
+msgstr "Помилка під час установлення"
#: plinth/package.py:158
msgid "installing"
-msgstr "встановлення"
+msgstr "установлення"
#: plinth/package.py:160
msgid "downloading"
@@ -7628,7 +7693,7 @@ msgstr ""
#: plinth/templates/app-header.html:22
msgid "Installation"
-msgstr "Встановлення"
+msgstr "Установлення"
#: plinth/templates/app.html:29
#, python-format
@@ -7766,8 +7831,8 @@ msgid ""
msgstr ""
"%(box_name)s є спеціальною збіркою Debian та особистим вебсервером із на "
"100%% вільним програмним забезпеченням для розгортання соціальних "
-"застосунків на малих машинах. Він надає засоби для мережевого спілкування, "
-"що поважають вашу приватність й особисті дані."
+"застосунків на малих машинах. Він пропонує засоби для мережевого "
+"спілкування, які поважають вашу приватність й особисті дані."
#: plinth/templates/index.html:117
#, python-format
@@ -7810,10 +7875,12 @@ msgid ""
"%(service_name)s is available only on internal networks or when the "
"client is connected to %(box_name)s through VPN."
msgstr ""
+"%(service_name)s доступне лише у внутрішніх мережах або коли клієнт "
+"підключено до %(box_name)s через VPN."
#: plinth/templates/internal-zone.html:17
msgid "Currently there are no network interfaces configured as internal."
-msgstr ""
+msgstr "Зараз тут нема мережевих інтерфейсів, що налаштовані як внутрішні."
#: plinth/templates/internal-zone.html:19
#, python-format
@@ -7821,6 +7888,8 @@ msgid ""
"Currently the following network interfaces are configured as internal: "
"%(interface_list)s"
msgstr ""
+"Наразі наступні мережеві інтерфейси налаштовано як внутрішні: "
+"%(interface_list)s"
#: plinth/templates/messages.html:11
msgid "Close"
@@ -7850,8 +7919,8 @@ msgid ""
"are using the DMZ feature to forward all ports. No further router "
"configuration is necessary."
msgstr ""
-"Ваш FreedomBox поза маршрутизатором і ви "
-"використовуєте функцію DMZ для перенаправлення усіх портів. Подальша "
+"Ваш FreedomBox поза маршрутизатором і Ви "
+"використовуєте функцію DMZ для перенаправлення всіх портів. Подальша "
"конфіґурація маршрутизатора непотрібна."
#: plinth/templates/port-forwarding-info.html:26
@@ -7861,7 +7930,7 @@ msgid ""
"are not using the DMZ feature. You will need to set up port forwarding on "
"your router. You should forward the following ports for %(service_name)s:"
msgstr ""
-"Ваш FreedomBox поза маршрутизатором і ви не "
+"Ваш FreedomBox поза маршрутизатором і Ви не "
"використовуєте функцію DMZ. Вам потрібно налаштувати перенаправлення портів "
"на маршрутизаторі. Вам слід перенаправити наступні порти для "
"%(service_name)s:"
@@ -7893,7 +7962,7 @@ msgid ""
"moments before trying again."
msgstr ""
"Інше встановлення або оновлення вже запущено. Будь ласка, зачекайте кілька "
-"хвилин і спробуйте знову."
+"хвилин і спробуйте ще раз."
#: plinth/templates/setup.html:46
msgid "This application is currently not available in your distribution."
@@ -7905,7 +7974,7 @@ msgstr "Перевірити знову"
#: plinth/templates/setup.html:60
msgid "Install"
-msgstr "Встановити"
+msgstr "Установити"
#: plinth/templates/setup.html:72
msgid "Performing pre-install operation"
@@ -7918,14 +7987,14 @@ msgstr "Виконання післяінсталяційних операцій
#: plinth/templates/setup.html:83
#, python-format
msgid "Installing %(package_names)s: %(status)s"
-msgstr "Встановлення %(package_names)s: %(status)s"
+msgstr "Установлюється %(package_names)s: %(status)s"
#: plinth/templates/setup.html:93
#, python-format
msgid "%(percentage)s%% complete"
msgstr "%(percentage)s%% завершено"
-#: plinth/web_framework.py:113
+#: plinth/web_framework.py:117
msgid "Gujarati"
msgstr "Gujarati"
diff --git a/plinth/locale/vi/LC_MESSAGES/django.po b/plinth/locale/vi/LC_MESSAGES/django.po
index 6604035c7..6001bc06c 100644
--- a/plinth/locale/vi/LC_MESSAGES/django.po
+++ b/plinth/locale/vi/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-08-30 19:29-0400\n"
+"POT-Creation-Date: 2021-09-18 09:34-0400\n"
"PO-Revision-Date: 2021-07-28 08:34+0000\n"
"Last-Translator: bruh \n"
"Language-Team: Vietnamese \n"
"Language-Team: Chinese (Simplified) \n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
-"X-Generator: Weblate 4.7-dev\n"
+"X-Generator: Weblate 4.9-dev\n"
#: doc/dev/_templates/layout.html:11
msgid "Page source"
@@ -544,15 +544,7 @@ msgid "Restore data from"
msgstr "从 恢复数据"
#: plinth/modules/backups/templates/backups_upload.html:17
-#, fuzzy, python-format
-#| msgid ""
-#| "\n"
-#| " Upload a backup file downloaded from another %(box_name)s to "
-#| "restore is\n"
-#| " contents. You can choose the apps you wish to restore after "
-#| "uploading a\n"
-#| " backup file.\n"
-#| " "
+#, python-format
msgid ""
"\n"
" Upload a backup file downloaded from another %(box_name)s to restore "
@@ -563,8 +555,8 @@ msgid ""
" "
msgstr ""
"\n"
-" 上传一个从其它 %(box_name)s 下载的备份文件来恢复内容。\n"
-" 备份上传完成后,你可以选择你想要恢复的应用。\n"
+" 上传从另一个%(box_name)s下载的备份文件,以恢复其内容。\n"
+"\t您可以选择您希望在上传备份文件后恢复的应用程序。\n"
" "
#: plinth/modules/backups/templates/backups_upload.html:27
@@ -716,7 +708,6 @@ msgid "Mounting failed"
msgstr "安装失败"
#: plinth/modules/bepasty/__init__.py:24
-#, fuzzy
msgid ""
"bepasty is a web application that allows large files to be uploaded and "
"shared. Text and code snippets can also be pasted and shared. Text, image, "
@@ -728,7 +719,6 @@ msgstr ""
"一段时间后过期。"
#: plinth/modules/bepasty/__init__.py:28
-#, fuzzy
msgid ""
"bepasty does not use usernames for login. It only uses passwords. For each "
"password, a set of permissions can be selected. Once you have created a "
@@ -739,7 +729,6 @@ msgstr ""
"一旦你创建了一个密码,你就可以与应该拥有相关权限的用户分享它。"
#: plinth/modules/bepasty/__init__.py:32
-#, fuzzy
msgid ""
"You can also create multiple passwords with the same set of privileges, and "
"distribute them to different people or groups. This will allow you to later "
@@ -1316,15 +1305,15 @@ msgstr ""
#: plinth/modules/coturn/__init__.py:62
msgid "Coturn"
-msgstr ""
+msgstr "Coturn"
#: plinth/modules/coturn/__init__.py:63
msgid "VoIP Helper"
-msgstr ""
+msgstr "网络电话助手"
#: plinth/modules/coturn/forms.py:22
msgid "Invalid list of STUN/TURN Server URIs"
-msgstr ""
+msgstr "无效的STUN/TURN服务器URI列表"
#: plinth/modules/coturn/forms.py:30 plinth/modules/mumble/forms.py:21
#: plinth/modules/quassel/forms.py:22
@@ -1338,11 +1327,11 @@ msgstr "子域"
msgid ""
"Select a domain to use TLS with. If the list is empty, please configure at "
"least one domain with certificates."
-msgstr ""
+msgstr "选择一个要使用TLS的域。如果列表中是空的,请至少配置一个带有证书的域。"
#: plinth/modules/coturn/templates/coturn.html:15
msgid "Use the following URLs to configure your communication server:"
-msgstr ""
+msgstr "使用以下URL来配置你的通信服务器。"
#: plinth/modules/coturn/templates/coturn.html:24
#, fuzzy
@@ -1362,7 +1351,7 @@ msgstr "日期与时间"
#: plinth/modules/datetime/__init__.py:116
msgid "Time synchronized to NTP server"
-msgstr ""
+msgstr "时间同步到NTP服务器"
#: plinth/modules/datetime/forms.py:18
msgid "Time Zone"
@@ -1408,7 +1397,7 @@ msgstr ""
#: plinth/modules/deluge/__init__.py:47
#: plinth/modules/transmission/__init__.py:50
msgid "Download files using BitTorrent applications"
-msgstr ""
+msgstr "使用BitTorrent应用程序下载文件"
#: plinth/modules/deluge/__init__.py:51 plinth/modules/deluge/manifest.py:6
msgid "Deluge"
@@ -1427,7 +1416,7 @@ msgstr "下载目录"
#: plinth/modules/deluge/manifest.py:7
msgid "Bittorrent client written in Python/PyGTK"
-msgstr ""
+msgstr "用Python/PyGTK编写的Bittorrent客户端"
#: plinth/modules/diagnostics/__init__.py:28
msgid ""
@@ -1444,7 +1433,7 @@ msgstr "诊断程序"
#: plinth/modules/diagnostics/__init__.py:98
#: plinth/modules/email_server/templates/email_server.html:29
msgid "passed"
-msgstr ""
+msgstr "通过了"
#: plinth/modules/diagnostics/__init__.py:99
#: plinth/modules/email_server/templates/email_server.html:27
@@ -1457,31 +1446,31 @@ msgstr "安装失败。"
#: plinth/modules/diagnostics/__init__.py:100
#: plinth/modules/email_server/templates/email_server.html:25
msgid "error"
-msgstr ""
+msgstr "错误"
#: plinth/modules/diagnostics/__init__.py:101
msgid "warning"
-msgstr ""
+msgstr "警告"
#. Translators: This is the unit of computer storage Mebibyte similar to
#. Megabyte.
#: plinth/modules/diagnostics/__init__.py:204
msgid "MiB"
-msgstr ""
+msgstr "MiB"
#. Translators: This is the unit of computer storage Gibibyte similar to
#. Gigabyte.
#: plinth/modules/diagnostics/__init__.py:209
msgid "GiB"
-msgstr ""
+msgstr "GiB"
#: plinth/modules/diagnostics/__init__.py:216
msgid "You should disable some apps to reduce memory usage."
-msgstr ""
+msgstr "你应该禁用一些应用程序以减少内存的使用。"
#: plinth/modules/diagnostics/__init__.py:221
msgid "You should not install any new apps on this system."
-msgstr ""
+msgstr "你不应该在这个系统上安装任何新的应用程序。"
#: plinth/modules/diagnostics/__init__.py:233
#, no-python-format, python-brace-format
@@ -1489,10 +1478,12 @@ msgid ""
"System is low on memory: {percent_used}% used, {memory_available} "
"{memory_available_unit} free. {advice_message}"
msgstr ""
+"系统内存不足。{percent_used}%已用,{memory_available} {memory_available_unit}"
+"可用。{advice_message}。"
#: plinth/modules/diagnostics/__init__.py:235
msgid "Low Memory"
-msgstr ""
+msgstr "低内存"
#: plinth/modules/diagnostics/templates/diagnostics.html:17
#: plinth/modules/diagnostics/templates/diagnostics_button.html:11
@@ -1514,6 +1505,9 @@ msgid ""
" App: %(app_name)s\n"
" "
msgstr ""
+"\n"
+" 应用程序。%(app_name)s\n"
+" "
#: plinth/modules/diagnostics/templates/diagnostics_app.html:10
msgid "Diagnostic Results"
@@ -1522,7 +1516,7 @@ msgstr "诊断结果"
#: plinth/modules/diagnostics/templates/diagnostics_app.html:12
#, python-format
msgid "App: %(app_name)s"
-msgstr ""
+msgstr "应用程序。%(app_name)s"
#: plinth/modules/diagnostics/templates/diagnostics_app.html:21
#, fuzzy
@@ -1546,16 +1540,16 @@ msgstr "诊断测试"
msgid ""
"diaspora* is a decentralized social network where you can store and control "
"your own data."
-msgstr ""
+msgstr "diaspora*是一个去中心化的社交网络,你可以存储和控制自己的数据。"
#: plinth/modules/diaspora/__init__.py:68
#: plinth/modules/diaspora/manifest.py:23
msgid "diaspora*"
-msgstr ""
+msgstr "diaspora*"
#: plinth/modules/diaspora/__init__.py:69
msgid "Federated Social Network"
-msgstr ""
+msgstr "联合社交网络"
#: plinth/modules/diaspora/forms.py:13
msgid "Enable new user registrations"
@@ -1563,13 +1557,14 @@ msgstr "实现新用户注册"
#: plinth/modules/diaspora/manifest.py:11
msgid "dandelion*"
-msgstr ""
+msgstr "dandelion*"
#: plinth/modules/diaspora/manifest.py:13
msgid ""
"It is an unofficial webview based client for the community-run, distributed "
"social network diaspora*"
msgstr ""
+"它是一个非官方的基于webview的客户端,用于社区运营的分布式社交网络diaspora*。"
#: plinth/modules/diaspora/templates/diaspora-post-setup.html:16
#, python-format
@@ -1580,6 +1575,11 @@ msgid ""
"podname wouldn't be accessible.
You can access the diaspora* pod at diaspora.%(domain_name)s "
msgstr ""
+"diaspora*的pod域名被设置为%(domain_name)s。用户ID将看起来像"
+"username@diaspora.%(domain_name)s
如果FreedomBox域名被改变,所有用"
+"以前podname注册的用户的数据将无法访问。
你可以在 diaspora.%(domain_name)s 访问diaspora*荚。"
+"i>"
#: plinth/modules/diaspora/templates/diaspora-pre-setup.html:36
#: plinth/modules/matrixsynapse/templates/matrix-synapse-pre-setup.html:15
@@ -1709,7 +1709,7 @@ msgstr "账户创建时使用的用户名"
#: plinth/modules/dynamicdns/forms.py:65
msgid "GnuDIP"
-msgstr ""
+msgstr "GnuDIP"
#: plinth/modules/dynamicdns/forms.py:68
#, fuzzy
@@ -1760,7 +1760,7 @@ msgstr "查寻公开 IP 的 URL"
#: plinth/modules/dynamicdns/forms.py:119
msgid "Use IPv6 instead of IPv4"
-msgstr ""
+msgstr "使用IPv6而不是IPv4"
#: plinth/modules/dynamicdns/forms.py:142
msgid "Please provide an update URL or a GnuDIP server address"
@@ -1895,10 +1895,12 @@ msgid ""
"ejabberd needs a STUN/TURN server for audio/video calls. Install the Coturn app or configure an external server."
msgstr ""
+"ejabberd需要一个STUN/TURN服务器用于音频/视频呼叫。安装Coturn应用程序或配置一个外部服务器。"
#: plinth/modules/ejabberd/__init__.py:70
msgid "ejabberd"
-msgstr ""
+msgstr "ejabberd"
#: plinth/modules/ejabberd/__init__.py:71
#: plinth/modules/matrixsynapse/__init__.py:77
@@ -1909,7 +1911,7 @@ msgstr "Web 服务器"
#: plinth/modules/ejabberd/forms.py:18
msgid "Enable Message Archive Management"
-msgstr ""
+msgstr "启用消息存档管理"
#: plinth/modules/ejabberd/forms.py:20
#, python-brace-format
@@ -1919,10 +1921,13 @@ msgid ""
"history of a multi-user chat room. It depends on the client settings whether "
"the histories are stored as plain text or encrypted."
msgstr ""
+"如果启用,您的{box_name}将存储聊天信息历史。这允许在多个客户端之间同步对话,"
+"并读取多用户聊天室的历史。聊天记录是以纯文本还是加密形式存储,这取决于客户端"
+"的设置。"
#: plinth/modules/ejabberd/forms.py:27 plinth/modules/matrixsynapse/forms.py:22
msgid "Automatically manage audio/video call setup"
-msgstr ""
+msgstr "自动管理音频/视频通话设置"
#: plinth/modules/ejabberd/forms.py:29
#, python-brace-format
@@ -1931,14 +1936,16 @@ msgid ""
"server for ejabberd. Disable this if you want to use a different STUN/TURN "
"server."
msgstr ""
+"将本地coturn应用程序配置为ejabberd的STUN/TURN服"
+"务器。如果你想使用一个不同的STUN/TURN服务器,请禁用此功能。"
#: plinth/modules/ejabberd/forms.py:36 plinth/modules/matrixsynapse/forms.py:31
msgid "STUN/TURN Server URIs"
-msgstr ""
+msgstr "STUN/TURN服务器URI"
#: plinth/modules/ejabberd/forms.py:38 plinth/modules/matrixsynapse/forms.py:33
msgid "List of public URIs of the STUN/TURN server, one on each line."
-msgstr ""
+msgstr "STUN/TURN服务器的公共URI列表,每行一个。"
#: plinth/modules/ejabberd/forms.py:42 plinth/modules/matrixsynapse/forms.py:37
#, fuzzy
@@ -2034,6 +2041,12 @@ msgstr ""
msgid "Cannot be a number"
msgstr ""
+#: plinth/modules/email_server/audit/domain.py:35
+#, fuzzy
+#| msgid "Error setting domain name: {exception}"
+msgid "Postfix domain name config"
+msgstr "设置域名错误:{exception}"
+
#: plinth/modules/email_server/audit/home.py:23
#: plinth/modules/email_server/audit/home.py:32
msgid "User does not exist"
@@ -8700,7 +8713,7 @@ msgstr "正在安装 %(package_names)s:%(status)s"
msgid "%(percentage)s%% complete"
msgstr "已完成 %(percentage)s%%"
-#: plinth/web_framework.py:113
+#: plinth/web_framework.py:117
msgid "Gujarati"
msgstr "古吉拉特语"
diff --git a/plinth/locale/zh_Hant/LC_MESSAGES/django.po b/plinth/locale/zh_Hant/LC_MESSAGES/django.po
index 55f1f3ffc..09711c2d4 100644
--- a/plinth/locale/zh_Hant/LC_MESSAGES/django.po
+++ b/plinth/locale/zh_Hant/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-08-30 19:29-0400\n"
+"POT-Creation-Date: 2021-09-18 09:34-0400\n"
"PO-Revision-Date: 2021-04-27 13:32+0000\n"
"Last-Translator: James Pan \n"
"Language-Team: Chinese (Traditional) 0:
+ corrected_value = ', '.join(sorted(dest_set))
+ diagnosis.error('Update $mydestination')
+ diagnosis.flag('mydestination', corrected_value)
+
+ return diagnosis
+
+
+def _amend_mailname(domain):
+ with open('/etc/mailname', 'r') as fd:
+ mailname = fd.readline().strip()
+
+ # If mailname is not localhost, refresh it
+ if mailname != 'localhost':
+ temp = _change_to_domain_name(mailname, domain, False)
+ if temp != mailname:
+ return temp
+
+ return None
+
+
+def _amend_mydomain(conf_value, domain):
+ temp = _change_to_domain_name(conf_value, domain, False)
+ if temp != conf_value:
+ return temp
+
+ return None
+
+
+def _amend_myhostname(conf_value, mydomain):
+ if conf_value != mydomain:
+ if not conf_value.endswith('.' + mydomain):
+ return mydomain
+
+ return None
+
+
+def _amend_mydestination(dest_set, mydomain, myhostname, error):
+ addition_set = set()
+ if mydomain not in dest_set:
+ error('Value of $mydomain is not in $mydestination')
+ addition_set.add('$mydomain')
+ addition_set.add('$myhostname')
+ if myhostname not in dest_set:
+ error('Value of $myhostname is not in $mydestination')
+ addition_set.add('$mydomain')
+ addition_set.add('$myhostname')
+ if 'localhost' not in dest_set:
+ error('localhost is not in $mydestination')
+ addition_set.add('localhost')
+
+ if addition_set:
+ addition_set.update(dest_set)
+ return ', '.join(sorted(addition_set))
+
+ return None
+
+
+def _change_to_domain_name(value, domain, allow_old_fqdn):
+ # Detect invalid values
+ if not value or '.' not in value:
+ return domain
+
+ if not allow_old_fqdn and value != domain:
+ return domain
+ else:
+ return value
+
+
+def fix_postfix_domains(diagnosis):
+ diagnosis.apply_changes(_apply_domain_changes)
+
+
+def _apply_domain_changes(conf_dict):
+ for key, value in conf_dict.items():
+ if key.startswith('_'):
+ update = globals()['su_set' + key]
+ update(value)
+
+ post = {k: v for (k, v) in conf_dict.items() if not k.startswith('_')}
+ postconf.set_many_unsafe(post)
def get_domain_config():
diff --git a/plinth/modules/email_server/audit/models.py b/plinth/modules/email_server/audit/models.py
index de4b3e77d..173e77630 100644
--- a/plinth/modules/email_server/audit/models.py
+++ b/plinth/modules/email_server/audit/models.py
@@ -19,7 +19,7 @@ class Diagnosis:
"""Class constructor"""
self.title = title
self.action = action
- self.critical = []
+ self.critical_errors = []
self.errors = []
def to_json(self):
@@ -29,7 +29,7 @@ class Diagnosis:
'title': self.title,
'action': self.action,
'errors': self.errors,
- 'critical': self.critical
+ 'critical_errors': self.critical_errors
}
@classmethod
@@ -47,15 +47,15 @@ class Diagnosis:
title = translate(title) or title
result = cls(title, action=valid_dict['action'])
result.errors.extend(valid_dict['errors'])
- result.critical.extend(valid_dict['critical'])
+ result.critical_errors.extend(valid_dict['critical_errors'])
return result
def critical(self, message_fmt, *args):
"""Append a message to the critical errors list"""
if args:
- self.critical.append(message_fmt % args)
+ self.critical_errors.append(message_fmt % args)
else:
- self.critical.append(message_fmt)
+ self.critical_errors.append(message_fmt)
def error(self, message_fmt, *args):
"""Append a message to the errors list"""
@@ -69,7 +69,7 @@ class Diagnosis:
if log:
self.write_logs()
- if self.critical:
+ if self.critical_errors:
return [self.title, 'error']
elif self.errors:
return [self.title, 'failed']
@@ -79,19 +79,19 @@ class Diagnosis:
@property
def has_failed(self):
"""True if the diagnosis has failed or contains an error"""
- return (self.critical or self.errors)
+ return (self.critical_errors or self.errors)
def write_logs(self):
"""Log errors and failures"""
logger.debug('Ran audit: %s', self.title)
- for message in self.critical:
+ for message in self.critical_errors:
logger.critical(message)
for message in self.errors:
logger.error(message)
def sorting_key(self):
"""The key function for list.sort"""
- return (-len(self.critical), -len(self.errors), self.title)
+ return (-len(self.critical_errors), -len(self.errors), self.title)
class MainCfDiagnosis(Diagnosis):
diff --git a/plinth/modules/email_server/postconf.py b/plinth/modules/email_server/postconf.py
index cba3a007b..31826ddf5 100644
--- a/plinth/modules/email_server/postconf.py
+++ b/plinth/modules/email_server/postconf.py
@@ -61,10 +61,26 @@ def get_many(key_list):
return get_many_unsafe(key_list)
-def get_many_unsafe(key_iterator):
+def get_many_unsafe(key_iterator, flag=''):
result = {}
+ args = ['/sbin/postconf']
+ if flag:
+ args.append(flag)
+
+ number_of_keys = 0
for key in key_iterator:
- result[key] = get_unsafe(key)
+ args.append(key)
+ number_of_keys += 1
+
+ stdout = _run(args)
+ for line in filter(None, stdout.split('\n')):
+ key, sep, value = line.partition('=')
+ if not sep:
+ raise ValueError('Invalid output detected')
+ result[key.strip()] = value.strip()
+
+ if len(result) != number_of_keys:
+ raise ValueError('Some keys were missing from the output')
return result
diff --git a/plinth/modules/email_server/templates/email_server.html b/plinth/modules/email_server/templates/email_server.html
index 78c1fd575..d4eab825f 100644
--- a/plinth/modules/email_server/templates/email_server.html
+++ b/plinth/modules/email_server/templates/email_server.html
@@ -21,7 +21,7 @@
{{ model.title }}
- {% if model.critical %}
+ {% if model.critical_errors %}
{% trans "error" %}
{% elif model.errors %}
{% trans "failed" %}
@@ -41,7 +41,7 @@
{% endif %}
- {% for message in model.critical %}
+ {% for message in model.critical_errors %}
- {{ message }}
{% endfor %}
{% for message in model.errors %}
diff --git a/plinth/modules/email_server/views.py b/plinth/modules/email_server/views.py
index 57492845a..81ff70dd5 100644
--- a/plinth/modules/email_server/views.py
+++ b/plinth/modules/email_server/views.py
@@ -85,7 +85,7 @@ class EmailServerView(TabMixin, AppView):
"""Server configuration page"""
app_id = 'email_server'
template_name = 'email_server.html'
- audit_modules = ('tls', 'rcube')
+ audit_modules = ('domain', 'tls', 'rcube')
def get_context_data(self, *args, **kwargs):
dlist = []
diff --git a/plinth/modules/gitweb/tests/gitweb.feature b/plinth/modules/gitweb/tests/gitweb.feature
deleted file mode 100644
index 15685d5d4..000000000
--- a/plinth/modules/gitweb/tests/gitweb.feature
+++ /dev/null
@@ -1,113 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-or-later
-
-@apps @gitweb @sso
-Feature: gitweb Simple Git Hosting
- Git web interface.
-
-Background:
- Given I'm a logged in user
- And the gitweb application is installed
-
-Scenario: Enable gitweb application
- Given the gitweb application is disabled
- When I enable the gitweb application
- Then the gitweb site should be available
-
-Scenario: Create public repository
- Given the gitweb application is enabled
- And a public repository that doesn't exist
- When I create the repository
- Then the repository should be listed as a public
- And the repository should be listed on gitweb
-
-Scenario: Create private repository
- Given the gitweb application is enabled
- And a private repository that doesn't exist
- When I create the repository
- Then the repository should be listed as a private
- And the repository should be listed on gitweb
-
-@backups
-Scenario: Backup and restore gitweb
- Given the gitweb application is enabled
- And a repository
- When I create a backup of the gitweb app data with name test_gitweb
- And I delete the repository
- And I restore the gitweb app data backup with name test_gitweb
- Then the repository should be restored
- And the gitweb site should be available
-
-Scenario: Public gitweb site shows only public repositories
- Given the gitweb application is enabled
- And both public and private repositories exist
- When I log out
- Then the public repository should be listed on gitweb
- And the private repository should not be listed on gitweb
-
-Scenario: Gitweb is not public if there are only private repositories
- Given the gitweb application is enabled
- And at least one repository exists
- And all repositories are private
- When I log out
- And I access gitweb application
- Then I should be prompted for login
- And gitweb app should not be visible on the front page
-
-Scenario: Edit repository metadata
- Given the gitweb application is enabled
- And a public repository that doesn't exist
- And a repository metadata:
- description: Test Description
- owner: Test Owner
- access: private
- When I create the repository
- And I set the metadata of the repository
- Then the metadata of the repository should be as set
-
-Scenario: Edit default branch of the repository
- Given the gitweb application is enabled
- And a repository with the branch branch1
- When I set branch1 as a default branch
- Then the gitweb site should show branch1 as a default repo branch
-
-Scenario: Access public repository with git client
- Given the gitweb application is enabled
- And a public repository
- When using a git client
- Then the repository should be publicly readable
- And the repository should not be publicly writable
- And the repository should be privately writable
-
-Scenario: Access private repository with git client
- Given the gitweb application is enabled
- And a private repository
- When using a git client
- Then the repository should not be publicly readable
- And the repository should not be publicly writable
- And the repository should be privately readable
- And the repository should be privately writable
-
-Scenario: User of git-access group can access gitweb site
- Given the gitweb application is enabled
- And all repositories are private
- And the user gituser in group git-access exists
- When I'm logged in as the user gituser
- Then the gitweb site should be available
-
-Scenario: User not of git-access group can't access gitweb site
- Given the gitweb application is enabled
- And all repositories are private
- And the user nogroupuser exists
- When I'm logged in as the user nogroupuser
- Then the gitweb site should not be available
-
-Scenario: Delete repository
- Given the gitweb application is enabled
- And a repository
- When I delete the repository
- Then the repository should not be listed
-
-Scenario: Disable gitweb application
- Given the gitweb application is enabled
- When I disable the gitweb application
- Then the gitweb site should not be available
diff --git a/plinth/modules/gitweb/tests/test_actions.py b/plinth/modules/gitweb/tests/test_actions.py
index 2f1bc062c..26ccad990 100644
--- a/plinth/modules/gitweb/tests/test_actions.py
+++ b/plinth/modules/gitweb/tests/test_actions.py
@@ -17,7 +17,6 @@ REPO_DATA = {
'description': '',
'owner': '',
'access': 'private',
- 'default_branch': 'master',
}
@@ -65,9 +64,11 @@ def test_create_repo(call_action):
'create-repo', '--name', REPO_NAME, '--description', '', '--owner', '',
'--is-private', '--keep-ownership'
])
+ repo = json.loads(call_action(['repo-info', '--name', REPO_NAME]))
+ default_branch = repo.pop('default_branch')
- assert json.loads(call_action(['repo-info', '--name',
- REPO_NAME])) == REPO_DATA
+ assert repo == REPO_DATA
+ assert len(default_branch) > 0
def test_change_repo_medatada(call_action, existing_repo):
@@ -77,7 +78,6 @@ def test_change_repo_medatada(call_action, existing_repo):
'description': 'description2',
'owner': 'owner2',
'access': 'public',
- 'default_branch': 'master',
}
call_action([
@@ -89,9 +89,10 @@ def test_change_repo_medatada(call_action, existing_repo):
call_action([
'set-repo-access', '--name', REPO_NAME, '--access', new_data['access']
])
+ repo = json.loads(call_action(['repo-info', '--name', REPO_NAME]))
+ del repo['default_branch']
- assert json.loads(call_action(['repo-info', '--name',
- REPO_NAME])) == new_data
+ assert repo == new_data
def test_rename_repository(call_action, existing_repo):
@@ -101,21 +102,17 @@ def test_rename_repository(call_action, existing_repo):
call_action(['rename-repo', '--oldname', REPO_NAME, '--newname', new_name])
with pytest.raises(RuntimeError, match='Repository not found'):
call_action(['repo-info', '--name', REPO_NAME])
+ repo = json.loads(call_action(['repo-info', '--name', new_name]))
- assert json.loads(call_action(['repo-info', '--name', new_name])) == {
- **REPO_DATA,
- **{
- 'name': new_name
- }
- }
+ assert repo['name'] == new_name
def test_get_branches(call_action, existing_repo):
"""Test getting all the branches of the repository."""
- assert json.loads(call_action(['get-branches', '--name', REPO_NAME])) == {
- "default_branch": "master",
- "branches": []
- }
+ result = json.loads(call_action(['get-branches', '--name', REPO_NAME]))
+
+ assert 'default_branch' in result
+ assert result['branches'] == []
def test_delete_repository(call_action, existing_repo):
diff --git a/plinth/modules/gitweb/tests/test_functional.py b/plinth/modules/gitweb/tests/test_functional.py
index 80bb1e76d..16fdfe43d 100644
--- a/plinth/modules/gitweb/tests/test_functional.py
+++ b/plinth/modules/gitweb/tests/test_functional.py
@@ -2,185 +2,136 @@
"""
Functional, browser based tests for gitweb app.
"""
-
import contextlib
import os
import shutil
import subprocess
import tempfile
-from pytest_bdd import given, parsers, scenarios, then, when
+import pytest
from plinth.tests import functional
-scenarios('gitweb.feature')
+pytestmark = [pytest.mark.apps, pytest.mark.gitweb]
_default_url = functional.config['DEFAULT']['url']
-@given('a public repository')
-@given('a repository')
-@given('at least one repository exists')
-def gitweb_repo(session_browser):
- _create_repo(session_browser, 'Test-repo', 'public', True)
+@pytest.fixture(scope='module', autouse=True)
+def fixture_background(session_browser):
+ """Login and install the app."""
+ functional.login(session_browser)
+ functional.install(session_browser, 'gitweb')
+ functional.app_enable(session_browser, 'gitweb')
+ yield
+ functional.login(session_browser)
+ functional.app_disable(session_browser, 'gitweb')
-@given('a private repository')
-def gitweb_private_repo(session_browser):
- _create_repo(session_browser, 'Test-repo', 'private', True)
+@pytest.fixture(autouse=True)
+def fixture_login(session_browser):
+ """Login fixture."""
+ functional.login(session_browser)
+ functional.app_enable(session_browser, 'gitweb')
+ yield
-@given(parsers.parse('a repository with the branch {branch:w}'))
-def _create_repo_with_branch(session_browser, branch):
- _delete_repo(session_browser, 'Test-repo', ignore_missing=True)
- _create_repo(session_browser, 'Test-repo', 'public')
- _create_branch('Test-repo', branch)
-
-
-@given('both public and private repositories exist')
-def gitweb_public_and_private_repo(session_browser):
- _create_repo(session_browser, 'Test-repo', 'public', True)
- _create_repo(session_browser, 'Test-repo2', 'private', True)
-
-
-@given(parsers.parse("a {access:w} repository that doesn't exist"))
-def gitweb_nonexistent_repo(session_browser, access):
- _delete_repo(session_browser, 'Test-repo', ignore_missing=True)
- return dict(access=access)
-
-
-@given('all repositories are private')
-def gitweb_all_repositories_private(session_browser):
+def test_all_repos_private(session_browser):
+ """Test repo accessability when all repos are private."""
+ _create_repo(session_browser, 'Test-repo', 'private', ok_if_exists=True)
_set_all_repos_private(session_browser)
+ if not functional.user_exists(session_browser, 'gitweb_user'):
+ functional.create_user(session_browser, 'gitweb_user',
+ groups=['git-access'])
+ if not functional.user_exists(session_browser, 'nogroupuser'):
+ functional.create_user(session_browser, 'nogroupuser', groups=[])
+
+ functional.login_with_account(session_browser, functional.base_url,
+ 'gitweb_user')
+ assert functional.is_available(session_browser, 'gitweb')
+ assert len(functional.find_on_front_page(session_browser, 'gitweb')) == 1
+
+ functional.login_with_account(session_browser, functional.base_url,
+ 'nogroupuser')
+ assert not functional.is_available(session_browser, 'gitweb')
+ assert len(functional.find_on_front_page(session_browser, 'gitweb')) == 0
+
+ functional.logout(session_browser)
+ functional.access_url(session_browser, 'gitweb')
+ assert functional.is_login_prompt(session_browser)
+ assert len(functional.find_on_front_page(session_browser, 'gitweb')) == 0
-@given(parsers.parse('a repository metadata:\n{metadata}'),
- target_fixture='gitweb_repo_metadata')
-def gitweb_repo_metadata(session_browser, metadata):
- metadata_dict = {}
- for item in metadata.split('\n'):
- item = item.split(': ')
- metadata_dict[item[0]] = item[1]
- return metadata_dict
-
-
-@when('I create the repository')
-def gitweb_create_repo(session_browser, access):
- _create_repo(session_browser, 'Test-repo', access)
-
-
-@when('I delete the repository')
-def gitweb_delete_repo(session_browser):
+@pytest.mark.backups
+def test_backup(session_browser):
+ """Test backing up and restoring."""
+ _create_repo(session_browser, 'Test-repo', ok_if_exists=True)
+ functional.backup_create(session_browser, 'gitweb', 'test_gitweb')
_delete_repo(session_browser, 'Test-repo')
+ functional.backup_restore(session_browser, 'gitweb', 'test_gitweb')
+ assert _repo_exists(session_browser, 'Test-repo')
+ assert functional.is_available(session_browser, 'gitweb')
-@when(parsers.parse('I set {branch:w} as a default branch'))
-def gitweb_set_default_branch(session_browser, branch):
- _set_default_branch(session_browser, 'Test-repo', branch)
+@pytest.mark.parametrize('access', ['public', 'private'])
+@pytest.mark.parametrize('repo_name', ['Test-repo', 'Test-repo.git'])
+def test_create_delete_repo(session_browser, access, repo_name):
+ """Test creating and deleting a repo and accessing with a git client."""
+ _delete_repo(session_browser, repo_name, ignore_missing=True)
+ _create_repo(session_browser, repo_name, access)
+
+ assert _repo_exists(session_browser, repo_name, access)
+ assert _site_repo_exists(session_browser, repo_name)
+
+ if access == "public":
+ assert _repo_is_readable(repo_name)
+ else:
+ assert not _repo_is_readable(repo_name)
+
+ assert not _repo_is_writable(repo_name)
+ assert _repo_is_readable(repo_name, with_auth=True)
+ assert _repo_is_writable(repo_name, with_auth=True)
+
+ _delete_repo(session_browser, repo_name)
+ assert not _repo_exists(session_browser, repo_name)
-@when('I set the metadata of the repository')
-def gitweb_edit_repo_metadata(session_browser, gitweb_repo_metadata):
- _edit_repo_metadata(session_browser, 'Test-repo', gitweb_repo_metadata)
+def test_both_private_and_public_repo_exist(session_browser):
+ """Tests when both private and public repo exist."""
+ _create_repo(session_browser, 'Test-repo', 'public', True)
+ _create_repo(session_browser, 'Test-repo-private', 'private', True)
-
-@when('using a git client')
-def gitweb_using_git_client():
- pass
-
-
-@then(
- parsers.parse(
- 'the gitweb site should show {branch:w} as a default repo branch'))
-def gitweb_site_check_default_repo_branch(session_browser, branch):
- assert _get_gitweb_site_default_repo_branch(session_browser,
- 'Test-repo') == branch
-
-
-@then('the repository should be restored')
-@then('the repository should be listed as a public')
-def gitweb_repo_should_exists(session_browser):
- assert _repo_exists(session_browser, 'Test-repo', access='public')
-
-
-@then('the repository should be listed as a private')
-def gitweb_private_repo_should_exists(session_browser):
- assert _repo_exists(session_browser, 'Test-repo', 'private')
-
-
-@then('the repository should not be listed')
-def gitweb_repo_should_not_exist(session_browser):
- assert not _repo_exists(session_browser, 'Test-repo')
-
-
-@then('the public repository should be listed on gitweb')
-@then('the repository should be listed on gitweb')
-def gitweb_repo_should_exist_on_gitweb(session_browser):
+ functional.logout(session_browser)
assert _site_repo_exists(session_browser, 'Test-repo')
+ assert not _site_repo_exists(session_browser, 'Test-repo-private')
-@then('the private repository should not be listed on gitweb')
-def gitweb_private_repo_should_exists_on_gitweb(session_browser):
- assert not _site_repo_exists(session_browser, 'Test-repo2')
+def test_edit_repo_metadata(session_browser):
+ """Test edit repo metadata."""
+ _create_repo(session_browser, 'Test-repo2', 'public', ok_if_exists=True)
+ _delete_repo(session_browser, 'Test-repo', ignore_missing=True)
+ repo_metadata = {
+ 'name': 'Test-repo',
+ 'description': 'Test Description',
+ 'owner': 'Test Owner',
+ 'access': 'private',
+ }
+ _edit_repo_metadata(session_browser, 'Test-repo2', repo_metadata)
+ assert _get_repo_metadata(session_browser, "Test-repo") == repo_metadata
+
+ _create_branch('Test-repo', 'branch1')
+ _set_default_branch(session_browser, 'Test-repo', 'branch1')
+ assert _get_gitweb_site_default_repo_branch(session_browser,
+ 'Test-repo') == 'branch1'
-@then('the metadata of the repository should be as set')
-def gitweb_repo_metadata_should_match(session_browser, gitweb_repo_metadata):
- actual_metadata = _get_repo_metadata(session_browser, 'Test-repo')
- assert all(item in actual_metadata.items()
- for item in gitweb_repo_metadata.items())
+def test_enable_disable(session_browser):
+ """Test enabling and disabling the app."""
+ functional.app_disable(session_browser, 'gitweb')
+ assert not functional.is_available(session_browser, 'gitweb')
-
-@then('the repository should be publicly readable')
-def gitweb_repo_publicly_readable():
- assert _repo_is_readable('Test-repo')
- assert _repo_is_readable('Test-repo', url_git_extension=True)
-
-
-@then('the repository should not be publicly readable')
-def gitweb_repo_not_publicly_readable():
- assert not _repo_is_readable('Test-repo')
- assert not _repo_is_readable('Test-repo', url_git_extension=True)
-
-
-@then('the repository should not be publicly writable')
-def gitweb_repo_not_publicly_writable():
- assert not _repo_is_writable('Test-repo')
- assert not _repo_is_writable('Test-repo', url_git_extension=True)
-
-
-@then('the repository should be privately readable')
-def gitweb_repo_privately_readable():
- assert _repo_is_readable('Test-repo', with_auth=True)
- assert _repo_is_readable('Test-repo', with_auth=True,
- url_git_extension=True)
-
-
-@then('the repository should be privately writable')
-def gitweb_repo_privately_writable():
- assert _repo_is_writable('Test-repo', with_auth=True)
- assert _repo_is_writable('Test-repo', with_auth=True,
- url_git_extension=True)
-
-
-def _create_branch(repo, branch):
- """Create a branch on the remote repository."""
- repo_url = _get_repo_url(repo, with_auth=True)
-
- with _gitweb_temp_directory() as temp_directory:
- repo_path = os.path.join(temp_directory, repo)
-
- _create_local_repo(repo_path)
-
- add_branch_commands = [['git', 'checkout', '-q', '-b', branch],
- [
- 'git', '-c', 'user.name=Tester', '-c',
- 'user.email=tester', 'commit', '-q',
- '--allow-empty', '-m', 'test_branch1'
- ],
- ['git', 'push', '-q', '-f', repo_url, branch]]
- for command in add_branch_commands:
- subprocess.check_call(command, cwd=repo_path)
+ functional.app_enable(session_browser, 'gitweb')
+ assert functional.is_available(session_browser, 'gitweb')
def _create_local_repo(path):
@@ -200,7 +151,7 @@ def _create_repo(browser, repo, access=None, ok_if_exists=False):
"""Create repository."""
if not _repo_exists(browser, repo, access):
_delete_repo(browser, repo, ignore_missing=True)
- browser.find_link_by_href('/plinth/apps/gitweb/create/').first.click()
+ browser.links.find_by_href('/plinth/apps/gitweb/create/').first.click()
browser.find_by_id('id_gitweb-name').fill(repo)
if access == 'private':
browser.find_by_id('id_gitweb-is_private').check()
@@ -211,10 +162,32 @@ def _create_repo(browser, repo, access=None, ok_if_exists=False):
assert False, 'Repo already exists.'
+def _create_branch(repo, branch):
+ """Add a branch to the repo."""
+ repo_url = _get_repo_url(repo, with_auth=True)
+
+ with _gitweb_temp_directory() as temp_directory:
+ repo_path = os.path.join(temp_directory, repo)
+
+ _create_local_repo(repo_path)
+
+ add_branch_commands = [['git', 'checkout', '-q', '-b', branch],
+ [
+ 'git', '-c', 'user.name=Tester', '-c',
+ 'user.email=tester', 'commit', '-q',
+ '--allow-empty', '-m', 'test_branch1'
+ ],
+ ['git', 'push', '-q', '-f', repo_url, branch]]
+ for command in add_branch_commands:
+ subprocess.check_call(command, cwd=repo_path)
+
+
def _delete_repo(browser, repo, ignore_missing=False):
"""Delete repository."""
functional.nav_to_module(browser, 'gitweb')
- delete_link = browser.find_link_by_href(
+ if repo.endswith('.git'):
+ repo = repo[:-4]
+ delete_link = browser.links.find_by_href(
'/plinth/apps/gitweb/{}/delete/'.format(repo))
if delete_link or not ignore_missing:
delete_link.first.click()
@@ -224,20 +197,15 @@ def _delete_repo(browser, repo, ignore_missing=False):
def _edit_repo_metadata(browser, repo, metadata):
"""Set repository metadata."""
functional.nav_to_module(browser, 'gitweb')
- browser.find_link_by_href(
+ browser.links.find_by_href(
'/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click()
- if 'name' in metadata:
- browser.find_by_id('id_gitweb-name').fill(metadata['name'])
- if 'description' in metadata:
- browser.find_by_id('id_gitweb-description').fill(
- metadata['description'])
- if 'owner' in metadata:
- browser.find_by_id('id_gitweb-owner').fill(metadata['owner'])
- if 'access' in metadata:
- if metadata['access'] == 'private':
- browser.find_by_id('id_gitweb-is_private').check()
- else:
- browser.find_by_id('id_gitweb-is_private').uncheck()
+ browser.find_by_id('id_gitweb-name').fill(metadata['name'])
+ browser.find_by_id('id_gitweb-description').fill(metadata['description'])
+ browser.find_by_id('id_gitweb-owner').fill(metadata['owner'])
+ if metadata['access'] == 'private':
+ browser.find_by_id('id_gitweb-is_private').check()
+ else:
+ browser.find_by_id('id_gitweb-is_private').uncheck()
functional.submit(browser)
@@ -251,7 +219,7 @@ def _get_gitweb_site_default_repo_branch(browser, repo):
def _get_repo_metadata(browser, repo):
"""Get repository metadata."""
functional.nav_to_module(browser, 'gitweb')
- browser.find_link_by_href(
+ browser.links.find_by_href(
'/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click()
metadata = {}
for item in ['name', 'description', 'owner']:
@@ -278,6 +246,22 @@ def _get_repo_url(repo, with_auth):
scheme, functional.config['DEFAULT']['username'], password, url, repo)
+def _gitweb_git_command_is_successful(command, cwd):
+ """Check if a command runs successfully or gives authentication error"""
+ # Tell OS not to translate command return messages
+ env = os.environ.copy()
+ env['LC_ALL'] = 'C'
+
+ process = subprocess.run(command, capture_output=True, cwd=cwd, env=env,
+ check=False)
+ if process.returncode != 0:
+ if 'Authentication failed' in process.stderr.decode():
+ return False
+ print(process.stdout.decode())
+ process.check_returncode() # Raise exception
+ return True
+
+
@contextlib.contextmanager
def _gitweb_temp_directory():
"""Create temporary directory"""
@@ -286,26 +270,12 @@ def _gitweb_temp_directory():
shutil.rmtree(name)
-def _gitweb_git_command_is_successful(command, cwd):
- """Check if a command runs successfully or gives authentication error"""
- # Tell OS not to translate command return messages
- env = os.environ.copy()
- env['LC_ALL'] = 'C'
-
- process = subprocess.run(command, capture_output=True, cwd=cwd, env=env)
- if process.returncode != 0:
- if 'Authentication failed' in process.stderr.decode():
- return False
- print(process.stdout.decode())
- # raise exception
- process.check_returncode()
- return True
-
-
def _repo_exists(browser, repo, access=None):
"""Check whether the repository exists."""
functional.nav_to_module(browser, 'gitweb')
- links_found = browser.find_link_by_href('/gitweb/{}.git'.format(repo))
+ if not repo.endswith('.git'):
+ repo = repo + ".git"
+ links_found = browser.links.find_by_href('/gitweb/{}'.format(repo))
access_matches = True
if links_found and access:
parent = links_found.first.find_by_xpath('..').first
@@ -317,53 +287,27 @@ def _repo_exists(browser, repo, access=None):
return bool(links_found) and access_matches
-def _repo_is_readable(repo, with_auth=False, url_git_extension=False):
+def _repo_is_readable(repo, with_auth=False):
"""Check if a git repo is readable with git client."""
url = _get_repo_url(repo, with_auth)
- if url_git_extension:
- url = url + '.git'
git_command = ['git', 'clone', '-c', 'http.sslverify=false', url]
with _gitweb_temp_directory() as cwd:
return _gitweb_git_command_is_successful(git_command, cwd)
-def _repo_is_writable(repo, with_auth=False, url_git_extension=False):
+def _repo_is_writable(repo, with_auth=False):
"""Check if a git repo is writable with git client."""
url = _get_repo_url(repo, with_auth)
- if url_git_extension:
- url = url + '.git'
-
with _gitweb_temp_directory() as temp_directory:
repo_directory = os.path.join(temp_directory, 'test-project')
_create_local_repo(repo_directory)
-
- git_push_command = ['git', 'push', '-qf', url, 'master']
-
+ git_push_command = [
+ 'git', '-c', 'push.default=current', 'push', '-qf', url
+ ]
return _gitweb_git_command_is_successful(git_push_command,
repo_directory)
-def _set_default_branch(browser, repo, branch):
- """Set default branch of the repository."""
- functional.nav_to_module(browser, 'gitweb')
- browser.find_link_by_href(
- '/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click()
- browser.find_by_id('id_gitweb-default_branch').select(branch)
- functional.submit(browser)
-
-
-def _set_repo_access(browser, repo, access):
- """Set repository as public or private."""
- functional.nav_to_module(browser, 'gitweb')
- browser.find_link_by_href(
- '/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click()
- if access == 'private':
- browser.find_by_id('id_gitweb-is_private').check()
- else:
- browser.find_by_id('id_gitweb-is_private').uncheck()
- functional.submit(browser)
-
-
def _set_all_repos_private(browser):
"""Set all repositories private"""
functional.nav_to_module(browser, 'gitweb')
@@ -376,7 +320,30 @@ def _set_all_repos_private(browser):
_set_repo_access(browser, repo, 'private')
+def _set_default_branch(browser, repo, branch):
+ """Set default branch of the repository."""
+ functional.nav_to_module(browser, 'gitweb')
+ browser.links.find_by_href(
+ '/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click()
+ browser.find_by_id('id_gitweb-default_branch').select(branch)
+ functional.submit(browser)
+
+
+def _set_repo_access(browser, repo, access):
+ """Set repository as public or private."""
+ functional.nav_to_module(browser, 'gitweb')
+ browser.links.find_by_href(
+ '/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click()
+ if access == 'private':
+ browser.find_by_id('id_gitweb-is_private').check()
+ else:
+ browser.find_by_id('id_gitweb-is_private').uncheck()
+ functional.submit(browser)
+
+
def _site_repo_exists(browser, repo):
"""Check whether the repository exists on Gitweb site."""
browser.visit('{}/gitweb'.format(_default_url))
- return browser.find_by_css('a[href="/gitweb/{0}.git"]'.format(repo))
+ if not repo.endswith('.git'):
+ repo = repo + ".git"
+ return bool(browser.find_by_css('a[href="/gitweb/{0}"]'.format(repo)))
diff --git a/plinth/modules/i2p/tests/i2p.feature b/plinth/modules/i2p/tests/i2p.feature
deleted file mode 100644
index 8d53106cc..000000000
--- a/plinth/modules/i2p/tests/i2p.feature
+++ /dev/null
@@ -1,19 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-or-later
-
-@apps @i2p
-Feature: I2P Anonymity Network
- Manage I2P configuration.
-
-Background:
- Given I'm a logged in user
- Given the i2p application is installed
-
-Scenario: Enable i2p application
- Given the i2p application is disabled
- When I enable the i2p application
- Then the i2p service should be running
-
-Scenario: Disable i2p application
- Given the i2p application is enabled
- When I disable the i2p application
- Then the i2p service should not be running
diff --git a/plinth/modules/i2p/tests/test_functional.py b/plinth/modules/i2p/tests/test_functional.py
index 20926e817..cde14aa8d 100644
--- a/plinth/modules/i2p/tests/test_functional.py
+++ b/plinth/modules/i2p/tests/test_functional.py
@@ -3,6 +3,29 @@
Functional, browser based tests for i2p app.
"""
-from pytest_bdd import scenarios
+import pytest
+from plinth.tests import functional
-scenarios('i2p.feature')
+pytestmark = [pytest.mark.apps, pytest.mark.i2p]
+
+
+@pytest.fixture(scope='module', autouse=True)
+def fixture_background(session_browser):
+ """Login and install the app."""
+ functional.login(session_browser)
+ functional.install(session_browser, 'i2p')
+ yield
+ functional.app_disable(session_browser, 'i2p')
+
+
+def test_enable_disable(session_browser):
+ """Test enabling the app."""
+ functional.app_disable(session_browser, 'i2p')
+
+ functional.app_enable(session_browser, 'i2p')
+ assert functional.service_is_running(session_browser, 'i2p')
+ assert functional.is_available(session_browser, 'i2p')
+
+ functional.app_disable(session_browser, 'i2p')
+ assert functional.service_is_not_running(session_browser, 'i2p')
+ assert not functional.is_available(session_browser, 'i2p')
diff --git a/plinth/modules/infinoted/tests/infinoted.feature b/plinth/modules/infinoted/tests/infinoted.feature
deleted file mode 100644
index 9687a6b9c..000000000
--- a/plinth/modules/infinoted/tests/infinoted.feature
+++ /dev/null
@@ -1,19 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-or-later
-
-@apps @infinoted
-Feature: Infinoted Collaborative Text Editor
- Run Gobby Server - Infinoted
-
-Background:
- Given I'm a logged in user
- Given the infinoted application is installed
-
-Scenario: Enable infinoted application
- Given the infinoted application is disabled
- When I enable the infinoted application
- Then the infinoted service should be running
-
-Scenario: Disable infinoted application
- Given the infinoted application is enabled
- When I disable the infinoted application
- Then the infinoted service should not be running
diff --git a/plinth/modules/infinoted/tests/test_functional.py b/plinth/modules/infinoted/tests/test_functional.py
index be32e536e..eab534960 100644
--- a/plinth/modules/infinoted/tests/test_functional.py
+++ b/plinth/modules/infinoted/tests/test_functional.py
@@ -3,6 +3,27 @@
Functional, browser based tests for infinoted app.
"""
-from pytest_bdd import scenarios
+import pytest
+from plinth.tests import functional
-scenarios('infinoted.feature')
+pytestmark = [pytest.mark.apps, pytest.mark.infinoted]
+
+
+@pytest.fixture(scope='module', autouse=True)
+def fixture_background(session_browser):
+ """Login and install the app."""
+ functional.login(session_browser)
+ functional.install(session_browser, 'infinoted')
+ yield
+ functional.app_disable(session_browser, 'infinoted')
+
+
+def test_enable_disable(session_browser):
+ """Test enabling the app."""
+ functional.app_disable(session_browser, 'infinoted')
+
+ functional.app_enable(session_browser, 'infinoted')
+ assert functional.service_is_running(session_browser, 'infinoted')
+
+ functional.app_disable(session_browser, 'infinoted')
+ assert functional.service_is_not_running(session_browser, 'infinoted')
diff --git a/plinth/modules/jsxc/tests/jsxc.feature b/plinth/modules/jsxc/tests/jsxc.feature
deleted file mode 100644
index f39577bd9..000000000
--- a/plinth/modules/jsxc/tests/jsxc.feature
+++ /dev/null
@@ -1,19 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-or-later
-
-@apps @jsxc
-Feature: JSXC XMPP Client
- Run the JSXC XMPP client.
-
-Background:
- Given I'm a logged in user
-
-Scenario: Install jsxc application
- Given the jsxc application is installed
- Then the jsxc site should be available
-
-@backups
-Scenario: Backup and restore jsxc
- Given the jsxc application is installed
- When I create a backup of the jsxc app data with name test_jsxc
- And I restore the jsxc app data backup with name test_jsxc
- Then the jsxc site should be available
diff --git a/plinth/modules/jsxc/tests/test_functional.py b/plinth/modules/jsxc/tests/test_functional.py
index 47f30123e..f482a82c7 100644
--- a/plinth/modules/jsxc/tests/test_functional.py
+++ b/plinth/modules/jsxc/tests/test_functional.py
@@ -3,6 +3,27 @@
Functional, browser based tests for jsxc app.
"""
-from pytest_bdd import scenarios
+import pytest
+from plinth.tests import functional
-scenarios('jsxc.feature')
+pytestmark = [pytest.mark.apps, pytest.mark.jsxc]
+
+
+@pytest.fixture(scope='module', autouse=True)
+def fixture_background(session_browser):
+ """Login."""
+ functional.login(session_browser)
+
+
+def test_install(session_browser):
+ """Test installing the app."""
+ functional.install(session_browser, 'jsxc')
+ assert functional.is_available(session_browser, 'jsxc')
+
+
+@pytest.mark.backups
+def test_backup(session_browser):
+ """Test backing up and restoring."""
+ functional.backup_create(session_browser, 'jsxc', 'test_jsxc')
+ functional.backup_restore(session_browser, 'jsxc', 'test_jsxc')
+ assert functional.is_available(session_browser, 'jsxc')
diff --git a/plinth/modules/matrixsynapse/tests/matrixsynapse.feature b/plinth/modules/matrixsynapse/tests/matrixsynapse.feature
deleted file mode 100644
index aefa9fc72..000000000
--- a/plinth/modules/matrixsynapse/tests/matrixsynapse.feature
+++ /dev/null
@@ -1,21 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-or-later
-
-@apps @matrixsynapse
-Feature: Matrix Synapse VoIP and Chat Server
- Run Matrix Synapse server
-
-Background:
- Given I'm a logged in user
- Given the domain name is set to mydomain.example
- Given the matrixsynapse application is installed
- Given the domain name for matrixsynapse is set to mydomain.example
-
-Scenario: Enable matrixsynapse application
- Given the matrixsynapse application is disabled
- When I enable the matrixsynapse application
- Then the matrixsynapse service should be running
-
-Scenario: Disable matrixsynapse application
- Given the matrixsynapse application is enabled
- When I disable the matrixsynapse application
- Then the matrixsynapse service should not be running
diff --git a/plinth/modules/matrixsynapse/tests/test_functional.py b/plinth/modules/matrixsynapse/tests/test_functional.py
index 46d251545..3d13bc553 100644
--- a/plinth/modules/matrixsynapse/tests/test_functional.py
+++ b/plinth/modules/matrixsynapse/tests/test_functional.py
@@ -3,6 +3,30 @@
Functional, browser based tests for matrixsynapse app.
"""
-from pytest_bdd import scenarios
+import pytest
+from plinth.tests import functional
-scenarios('matrixsynapse.feature')
+pytestmark = [pytest.mark.apps, pytest.mark.matrixsynapse]
+
+
+@pytest.fixture(scope='module', autouse=True)
+def fixture_background(session_browser):
+ """Login and install the app."""
+ functional.login(session_browser)
+ functional.set_domain_name(session_browser, 'mydomain.example')
+ functional.install(session_browser, 'matrixsynapse')
+ functional.app_select_domain_name(session_browser, 'matrixsynapse',
+ 'mydomain.example')
+ yield
+ functional.app_disable(session_browser, 'matrixsynapse')
+
+
+def test_enable_disable(session_browser):
+ """Test enabling the app."""
+ functional.app_disable(session_browser, 'matrixsynapse')
+
+ functional.app_enable(session_browser, 'matrixsynapse')
+ assert functional.service_is_running(session_browser, 'matrixsynapse')
+
+ functional.app_disable(session_browser, 'matrixsynapse')
+ assert functional.service_is_not_running(session_browser, 'matrixsynapse')
diff --git a/plinth/modules/mediawiki/__init__.py b/plinth/modules/mediawiki/__init__.py
index 99befe518..4c1762a32 100644
--- a/plinth/modules/mediawiki/__init__.py
+++ b/plinth/modules/mediawiki/__init__.py
@@ -18,7 +18,7 @@ from plinth.modules.firewall.components import Firewall
from . import manifest
-version = 9
+version = 10
managed_packages = ['mediawiki', 'imagemagick', 'php-sqlite3']
diff --git a/plinth/modules/mediawiki/data/etc/mediawiki/FreedomBoxStaticSettings.php b/plinth/modules/mediawiki/data/etc/mediawiki/FreedomBoxStaticSettings.php
index 5bda4c474..6026cfd6d 100644
--- a/plinth/modules/mediawiki/data/etc/mediawiki/FreedomBoxStaticSettings.php
+++ b/plinth/modules/mediawiki/data/etc/mediawiki/FreedomBoxStaticSettings.php
@@ -44,3 +44,11 @@ $wgDefaultSkin = "timeless";
# Domain Name
$wgServer = "https://freedombox.local";
+
+# Enable default extensions
+wfLoadExtension( 'Cite' );
+wfLoadExtension( 'Interwiki' );
+wfLoadExtension( 'MultimediaViewer' );
+wfLoadExtension( 'Renameuser' );
+wfLoadExtension( 'VisualEditor' );
+wfLoadExtension( 'WikiEditor' );
diff --git a/plinth/modules/mediawiki/manifest.py b/plinth/modules/mediawiki/manifest.py
index 1612300d4..ff8e5142f 100644
--- a/plinth/modules/mediawiki/manifest.py
+++ b/plinth/modules/mediawiki/manifest.py
@@ -15,7 +15,9 @@ backup = {
'files': ['/etc/mediawiki/FreedomBoxSettings.php']
},
'data': {
- 'directories': ['/var/lib/mediawiki-db/']
+ 'directories': [
+ '/var/lib/mediawiki-db/', '/var/lib/mediawiki/images/'
+ ]
},
'services': ['mediawiki-jobrunner']
}
diff --git a/plinth/modules/mediawiki/tests/mediawiki.feature b/plinth/modules/mediawiki/tests/mediawiki.feature
index fb2bc6f71..bf35af7d8 100644
--- a/plinth/modules/mediawiki/tests/mediawiki.feature
+++ b/plinth/modules/mediawiki/tests/mediawiki.feature
@@ -67,12 +67,15 @@ Scenario: Upload SVG image
@backups
Scenario: Backup and restore mediawiki
Given the mediawiki application is enabled
+ And I ensure that there is Noise.png image with credentials admin and whatever123
When I create a backup of the mediawiki app data with name test_mediawiki
- When I enable mediawiki public registrations
- And I delete the mediawiki main page
+ And I enable mediawiki public registrations
+ And I delete Noise.png image with credentials admin and whatever123
+ And I delete the mediawiki main page with credentials admin and whatever123
And I restore the mediawiki app data backup with name test_mediawiki
Then the mediawiki main page should be restored
- Then the mediawiki site should allow creating accounts
+ And there should be Noise.png image
+ And the mediawiki site should allow creating accounts
Scenario: Disable mediawiki application
Given the mediawiki application is enabled
diff --git a/plinth/modules/mediawiki/tests/test_functional.py b/plinth/modules/mediawiki/tests/test_functional.py
index 48654a0ac..3c9c50577 100644
--- a/plinth/modules/mediawiki/tests/test_functional.py
+++ b/plinth/modules/mediawiki/tests/test_functional.py
@@ -6,10 +6,10 @@ Functional, browser based tests for mediawiki app.
import pathlib
from urllib.parse import urlparse
-from pytest_bdd import given, parsers, scenarios, then, when
-
+import requests
from plinth.tests import functional
from plinth.tests.functional import config
+from pytest_bdd import given, parsers, scenarios, then, when
scenarios('mediawiki.feature')
@@ -76,9 +76,18 @@ def login_to_mediawiki_with_credentials(session_browser, username, password):
_login_with_credentials(session_browser, username, password)
-@when('I delete the mediawiki main page')
-def mediawiki_delete_main_page(session_browser):
- _delete_main_page(session_browser)
+@when(
+ parsers.parse('I delete the mediawiki main page with credentials '
+ '{username:w} and {password:w}'))
+def mediawiki_delete_main_page(session_browser, username, password):
+ _delete_main_page(session_browser, username, password)
+
+
+@when(
+ parsers.parse('I delete {image:S} image with credentials '
+ '{username:w} and {password:w}'))
+def delete_image(session_browser, username, password, image):
+ _delete_image(session_browser, username, password, image)
@then('the mediawiki main page should be restored')
@@ -94,10 +103,17 @@ def upload_image(session_browser, username, password, image):
_upload_image(session_browser, username, password, image)
+@given(
+ parsers.parse('I ensure that there is {image:S} image with credentials '
+ '{username:w} and {password:w}'))
+def ensure_image_exists(session_browser, username, password, image):
+ if not _image_exists(session_browser, image):
+ _upload_image(session_browser, username, password, image)
+
+
@then(parsers.parse('there should be {image:S} image'))
def uploaded_image_should_be_available(session_browser, image):
- uploaded_image = _get_uploaded_image(session_browser, image)
- assert image.lower() == uploaded_image.lower()
+ assert _image_exists(session_browser, image)
def _enable_public_registrations(browser):
@@ -137,30 +153,35 @@ def _set_admin_password(browser, password):
functional.submit(browser, form_class='form-configuration')
-def _verify_create_account_link(browser):
+def _is_create_account_available(browser):
+ """Load the create account page and return whether creating is allowed."""
functional.visit(browser, '/mediawiki/index.php/Special:CreateAccount')
- assert functional.eventually(browser.is_element_present_by_id,
- args=['wpCreateaccount'])
+ return browser.is_element_present_by_id('wpCreateaccount')
+
+
+def _verify_create_account_link(browser):
+ assert functional.eventually(_is_create_account_available, args=[browser])
def _verify_no_create_account_link(browser):
- functional.visit(browser, '/mediawiki/index.php/Special:CreateAccount')
- assert functional.eventually(browser.is_element_not_present_by_id,
- args=['wpCreateaccount'])
+ assert functional.eventually(
+ lambda: not _is_create_account_available(browser))
+
+
+def _is_anonymouse_read_allowed(browser):
+ """Load the main page and check if anonymous reading is allowed."""
+ functional.visit(browser, '/mediawiki')
+ return browser.is_element_present_by_id('ca-nstab-main')
def _verify_anonymous_reads_edits_link(browser):
- functional.visit(browser, '/mediawiki')
- assert functional.eventually(browser.is_element_present_by_id,
- args=['ca-nstab-main'])
+ assert functional.eventually(_is_anonymouse_read_allowed, args=[browser])
def _verify_no_anonymous_reads_edits_link(browser):
- functional.visit(browser, '/mediawiki')
- assert functional.eventually(browser.is_element_not_present_by_id,
- args=['ca-nstab-main'])
- assert functional.eventually(browser.is_element_present_by_id,
- args=['ca-nstab-special'])
+ assert functional.eventually(
+ lambda: not _is_anonymouse_read_allowed(browser))
+ assert browser.is_element_present_by_id('ca-nstab-special')
def _login(browser, username, password):
@@ -179,7 +200,7 @@ def _login_with_credentials(browser, username, password):
args=['t-upload'])
-def _upload_image(browser, username, password, image):
+def _upload_image(browser, username, password, image, ignore_warnings=True):
"""Upload an image to MediaWiki. Idempotent."""
functional.visit(browser, '/mediawiki')
_login(browser, username, password)
@@ -187,25 +208,43 @@ def _upload_image(browser, username, password, image):
# Upload file
functional.visit(browser, '/mediawiki/Special:Upload')
file_path = pathlib.Path(__file__).parent
- file_path /= '../../../../static/themes/default/img/' + image
+ file_path /= '../../../../static/themes/default/img/' + image.lower()
browser.attach_file('wpUploadFile', str(file_path.resolve()))
+ if ignore_warnings: # allow uploading file with the same name
+ browser.find_by_name('wpIgnoreWarning').first.click()
functional.submit(browser, element=browser.find_by_name('wpUpload')[0])
+def _delete_image(browser, username, password, image):
+ """Delete an image from MediaWiki."""
+ _login(browser, username, password)
+ path = f'/mediawiki/index.php?title=File:{image}&action=delete'
+ functional.visit(browser, path)
+ delete_button = browser.find_by_id('mw-filedelete-submit')
+ functional.submit(browser, element=delete_button)
+
+
def _get_number_of_uploaded_images(browser):
functional.visit(browser, '/mediawiki/Special:ListFiles')
return len(browser.find_by_css('.TablePager_col_img_timestamp'))
-def _get_uploaded_image(browser, image):
+def _image_exists(browser, image):
+ """Check whether the given image exists."""
functional.visit(browser, '/mediawiki/Special:ListFiles')
elements = browser.find_link_by_partial_href(image)
- return elements[0].value
+ if not elements: # Necessary but insufficient check.
+ # Special:ListFiles also shows deleted images.
+ return False
+
+ # The second hyperlink is a direct link to the image.
+ response = requests.get(elements[1]['href'], verify=False)
+ return response.status_code != 404
-def _delete_main_page(browser):
+def _delete_main_page(browser, username, password):
"""Delete the mediawiki main page."""
- _login(browser, 'admin', 'whatever123')
+ _login(browser, username, password)
functional.visit(browser,
'/mediawiki/index.php?title=Main_Page&action=delete')
with functional.wait_for_page_update(browser):
diff --git a/plinth/modules/minetest/tests/minetest.feature b/plinth/modules/minetest/tests/minetest.feature
deleted file mode 100644
index 58bd863ce..000000000
--- a/plinth/modules/minetest/tests/minetest.feature
+++ /dev/null
@@ -1,19 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-or-later
-
-@apps @minetest
-Feature: Minetest Block Sandbox
- Run the Minetest server
-
-Background:
- Given I'm a logged in user
- Given the minetest application is installed
-
-Scenario: Enable minetest application
- Given the minetest application is disabled
- When I enable the minetest application
- Then the minetest service should be running
-
-Scenario: Disable minetest application
- Given the minetest application is enabled
- When I disable the minetest application
- Then the minetest service should not be running
diff --git a/plinth/modules/minetest/tests/test_functional.py b/plinth/modules/minetest/tests/test_functional.py
index 741bc2b22..8bad3e900 100644
--- a/plinth/modules/minetest/tests/test_functional.py
+++ b/plinth/modules/minetest/tests/test_functional.py
@@ -3,6 +3,27 @@
Functional, browser based tests for minetest app.
"""
-from pytest_bdd import scenarios
+import pytest
+from plinth.tests import functional
-scenarios('minetest.feature')
+pytestmark = [pytest.mark.apps, pytest.mark.minetest]
+
+
+@pytest.fixture(scope='module', autouse=True)
+def fixture_background(session_browser):
+ """Login and install the app."""
+ functional.login(session_browser)
+ functional.install(session_browser, 'minetest')
+ yield
+ functional.app_disable(session_browser, 'minetest')
+
+
+def test_enable_disable(session_browser):
+ """Test enabling the app."""
+ functional.app_disable(session_browser, 'minetest')
+
+ functional.app_enable(session_browser, 'minetest')
+ assert functional.service_is_running(session_browser, 'minetest')
+
+ functional.app_disable(session_browser, 'minetest')
+ assert functional.service_is_not_running(session_browser, 'minetest')
diff --git a/plinth/modules/minidlna/tests/minidlna.feature b/plinth/modules/minidlna/tests/minidlna.feature
deleted file mode 100644
index 22aa22308..000000000
--- a/plinth/modules/minidlna/tests/minidlna.feature
+++ /dev/null
@@ -1,19 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-or-later
-
-@apps @minidlna
-Feature: minidlna Simple Media Server
- Run miniDLNA media server
-
-Background:
- Given I'm a logged in user
- And the minidlna application is installed
-
-Scenario: Enable minidlna application
- Given the minidlna application is disabled
- When I enable the minidlna application
- Then the minidlna service should be running
-
-Scenario: Disable minidlna application
- Given the minidlna application is enabled
- When I disable the minidlna application
- Then the minidlna service should not be running
diff --git a/plinth/modules/minidlna/tests/test_functional.py b/plinth/modules/minidlna/tests/test_functional.py
index 2b44dbc18..85d106ce9 100644
--- a/plinth/modules/minidlna/tests/test_functional.py
+++ b/plinth/modules/minidlna/tests/test_functional.py
@@ -3,6 +3,27 @@
Functional, browser based tests for minidlna app.
"""
-from pytest_bdd import scenarios
+import pytest
+from plinth.tests import functional
-scenarios('minidlna.feature')
+pytestmark = [pytest.mark.apps, pytest.mark.minidlna]
+
+
+@pytest.fixture(scope='module', autouse=True)
+def fixture_background(session_browser):
+ """Login and install the app."""
+ functional.login(session_browser)
+ functional.install(session_browser, 'minidlna')
+ yield
+ functional.app_disable(session_browser, 'minidlna')
+
+
+def test_enable_disable(session_browser):
+ """Test enabling the app."""
+ functional.app_disable(session_browser, 'minidlna')
+
+ functional.app_enable(session_browser, 'minidlna')
+ assert functional.service_is_running(session_browser, 'minidlna')
+
+ functional.app_disable(session_browser, 'minidlna')
+ assert functional.service_is_not_running(session_browser, 'minidlna')
diff --git a/plinth/modules/performance/tests/performance.feature b/plinth/modules/performance/tests/performance.feature
deleted file mode 100644
index cbae2c492..000000000
--- a/plinth/modules/performance/tests/performance.feature
+++ /dev/null
@@ -1,20 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-or-later
-
-@system @performance
-Feature: Performance - system monitoring
- Run the Performance Co-Pilot app.
-
-Background:
- Given I'm a logged in user
- And advanced mode is on
- And the performance application is installed
-
-Scenario: Enable performance application
- Given the performance application is disabled
- When I enable the performance application
- Then the performance service should be running
-
-Scenario: Disable performance application
- Given the performance application is enabled
- When I disable the performance application
- Then the performance service should not be running
diff --git a/plinth/modules/performance/tests/test_functional.py b/plinth/modules/performance/tests/test_functional.py
index eed641b09..3f7e7a70e 100644
--- a/plinth/modules/performance/tests/test_functional.py
+++ b/plinth/modules/performance/tests/test_functional.py
@@ -3,6 +3,27 @@
Functional, browser based tests for performance app.
"""
-from pytest_bdd import scenarios
+import pytest
+from plinth.tests import functional
-scenarios('performance.feature')
+pytestmark = [pytest.mark.system, pytest.mark.performance]
+
+
+@pytest.fixture(scope='module', autouse=True)
+def fixture_background(session_browser):
+ """Login and install the app."""
+ functional.login(session_browser)
+ functional.install(session_browser, 'performance')
+ yield
+ functional.app_disable(session_browser, 'performance')
+
+
+def test_enable_disable(session_browser):
+ """Test enabling the app."""
+ functional.app_disable(session_browser, 'performance')
+
+ functional.app_enable(session_browser, 'performance')
+ assert functional.service_is_running(session_browser, 'performance')
+
+ functional.app_disable(session_browser, 'performance')
+ assert functional.service_is_not_running(session_browser, 'performance')
diff --git a/plinth/tests/data/django_test_settings.py b/plinth/tests/data/django_test_settings.py
index 1d31afca4..1cc2f15a3 100644
--- a/plinth/tests/data/django_test_settings.py
+++ b/plinth/tests/data/django_test_settings.py
@@ -5,6 +5,12 @@ Django settings for test modules.
import os
+# Workaround for django-simple-captcha 0.5.6 not being compatible with
+# Django 3.2. 0.5.14 is almost there in Debian. Workaround only until then.
+import django.utils.encoding
+
+django.utils.encoding.python_2_unicode_compatible = lambda x: x
+
TEST_DATA_DIR = os.path.dirname(os.path.abspath(__file__))
AXES_ENABLED = False
diff --git a/plinth/web_framework.py b/plinth/web_framework.py
index 22689c963..678012688 100644
--- a/plinth/web_framework.py
+++ b/plinth/web_framework.py
@@ -22,6 +22,10 @@ logger = logging.getLogger(__name__)
def init(read_only=False):
"""Setup Django configuration in the absence of .settings file"""
+ # Workaround for django-simple-captcha 0.5.6 not being compatible with
+ # Django 3.2. 0.5.14 is almost there in Debian. Workaround only until then.
+ django.utils.encoding.python_2_unicode_compatible = lambda x: x
+
if cfg.secure_proxy_ssl_header:
settings.SECURE_PROXY_SSL_HEADER = (cfg.secure_proxy_ssl_header,
'https')
diff --git a/pytest.ini b/pytest.ini
index 4bb087a37..4f3ac0700 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -7,6 +7,7 @@ markers = functional
bepasty
bind
calibre
+ cockpit
config
coturn
datetime
@@ -52,3 +53,4 @@ markers = functional
ttrss
upgrades
users
+ zoph
diff --git a/static/themes/default/icons/mediawiki.png b/static/themes/default/icons/mediawiki.png
index f172eddfe..79b1b8fed 100644
Binary files a/static/themes/default/icons/mediawiki.png and b/static/themes/default/icons/mediawiki.png differ
diff --git a/static/themes/default/icons/mediawiki.svg b/static/themes/default/icons/mediawiki.svg
index 44cd4b399..8bc9feee4 100644
--- a/static/themes/default/icons/mediawiki.svg
+++ b/static/themes/default/icons/mediawiki.svg
@@ -1,49 +1,25 @@
-
-