plinth Debian release 20.17

-----BEGIN PGP SIGNATURE-----
 
 iQJKBAABCgA0FiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAl+gtzYWHGp2YWxsZXJv
 eUBtYWlsYm94Lm9yZwAKCRB3wMdee2UICNLkD/4n7nspeHzxwTE/+RTPEvp4/dCV
 B45EOb051Of7TeHGIsysGIigNvYqmjNDi+y6pPTLAxyCDw9EsHDM/rD0FrQ9L/fS
 8BEPjRVqMyQFldNVGUDwqpMRzTmsfoOE4HJAqIEOar2dClxbT9IXfXe58/hcKETT
 PTQJ2N0xDVMcAhh6BfILfE2OxlkpeAeCehwQkhfH/DypEX0uxJ1I7+itmr6JT3Wg
 Y8kkuav1PTIHgF8BICaNdgTCUUSf2RMQBo2Bsm6EvvXAqBYGw9IVC1X/Vlq2QZWi
 djwA864zPdjnxHJfSHc2fFfvoP46jdKy71DnKOKHOJWRkiXX+q5v2BfH+zomnke+
 2TI4QaY+v7aZRMfNnnlp520gHpRqhBsdNnhuYlYpxt2GeUmKwbkIKLFMoC8i0DVa
 zhj3pBhkjLZr6W0wUMKbTtmgRu/OJJMrCAKJFBNOM+1hu1Yq7KytyyWcpGtVMu0x
 ugX/9arHzjivj73LcfUBNLvouk0VRzRws6NwCo1OXU+ITPN23SGsIltpr+LOrCq+
 +U862mivzBJ3s0FOAJrW3oGiyK6Q+a6YV7iCqsAvx1wEk87SXbcqNJXrKLwo8HG2
 xtv/w7d12bS5qLI5ifemLyNk+dDivr8CWotFAA5G7A17jZZFasSAql9AeuzLFxGa
 AvKA5O3X3lmUH6JT9A==
 =kZks
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQJKBAABCgA0FiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAl+j2DwWHGp2YWxsZXJv
 eUBtYWlsYm94Lm9yZwAKCRB3wMdee2UICJdnD/9O8gdf24djnIKuobRrpyitD8zJ
 Bz9cW0uxh/T1L4IZr0bRK8UDDZ6hBYpKp+yfPQEAiZkVHAlu4gaW4Udmcu8B3z1N
 V2W7jZM5CNHOrDbusOtpdm1GNPafAmwoQTiSnPO7rKQIUMIjefKMUwiKK/iAp7zo
 BsW5wToXX6kN31JzIpCsNuYEF8gTTTemoGNQlhMzTcNvLh0hzMJMrdKdARq+N2TT
 hZVvB3LoBbbelbbAkIStK6iKUBsTjQ+4VVpUu731+Wn/I4i4GcxXfi2bH4T1QQ2l
 466CW/oCy6rcMoGwUZSLUH0dp3BwrcSTFI2jDJvBLMu53hoMGIBLG+ag2sDJ9tj1
 kQMlBgneS0pa9vebcvYfO1BkVGqtbZvJ9b4LrkkyxNxxxWUV6MlBDIHRLDlqxX/a
 fXcmOHtQUYl8Keuyvg7VrAKVWgfDl4nS9Tr8t/DetdwD53ignEPpvU9hkwxACMY3
 Hav9ZoaUg9lH2tDh6qCB0vbPEDpsHlrqhrTUxzpTTEj2iEbW8Mt3BEm/mPe97vS8
 TCbPzouqNo3kRDjxZJr+FpFc4Sx0GL3DHE7tOmvlHiG+xVP8Y7NT6ko6YqtCTTp0
 gamd3ebWKnLvnUwt10dL6xEKhLCS3DDzQKNRxkWI26sX65FUKmIRiILXsujgL5ID
 4ErfJFzJtpXgYrqjxw==
 =ohLr
 -----END PGP SIGNATURE-----

Merge tag 'v20.17' into debian/buster-backports

plinth Debian release 20.17

Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
James Valleroy 2020-11-05 05:47:18 -05:00
commit 51704497f4
75 changed files with 23048 additions and 7140 deletions

View File

@ -11,6 +11,11 @@ stages:
- test
- package
run-code-quality-tests:
stage: test
script:
- python3 -m flake8 --exclude actions/domainname-change,actions/dynamicdns,actions/hostname-change,actions/networks plinth actions/*
run-unit-tests:
stage: test
script:
@ -18,15 +23,18 @@ run-unit-tests:
- echo "tester:password" | chpasswd
- cp -r . /home/tester/plinth
- chown -R tester:tester /home/tester/plinth
- su -c "cd ~/plinth; python3 -m flake8 --exclude actions/domainname-change,actions/dynamicdns,actions/hostname-change,actions/networks plinth actions/*" tester
- su -c "cd ~/plinth;PYTHONPATH='.' py.test-3 --cov=plinth --cov-report=html:/home/tester/plinth/htmlcov --cov-report=term" tester
- cp -r /home/tester/plinth/htmlcov test-coverage-report
coverage: '/^TOTAL\s+.*\s+(\d+\.\d+%)$/'
artifacts:
paths:
- test-coverage-report/*
run-doc-tests:
stage: test
script:
- doc/scripts/wikiparser.py
build-debian-package:
stage: package
script:

View File

@ -48,6 +48,10 @@ def parse_arguments():
help='Set the default skin')
default_skin.add_argument('skin', help='name of the skin')
server_url = subparsers.add_parser(
'set-server-url', help='Set the value of $wgServer for this server')
server_url.add_argument('server_url', help='value of $wgServer')
subparsers.required = True
return parser.parse_args()
@ -86,6 +90,7 @@ def subcommand_setup(_):
password = generate_password()
with tempfile.NamedTemporaryFile() as password_file_handle:
password_file_handle.write(password.encode())
password_file_handle.flush()
subprocess.check_call([
_get_php_command(), install_script,
'--confpath=/etc/mediawiki', '--dbtype=sqlite',
@ -217,28 +222,37 @@ def subcommand_private_mode(arguments):
conf_value + '\n')
def subcommand_set_default_skin(arguments):
"""Set a default skin"""
skin = arguments.skin
skin_setting = f'$wgDefaultSkin = "{skin}";\n'
def _update_setting(setting_name, setting_line):
"""Update the value of one setting in the config file."""
with open(CONF_FILE, 'r') as conf_file:
lines = conf_file.readlines()
inserted = False
for i, line in enumerate(lines):
if line.strip().startswith('$wgDefaultSkin'):
lines[i] = skin_setting
if line.strip().startswith(setting_name):
lines[i] = setting_line
inserted = True
break
if not inserted:
lines.append(skin_setting)
lines.append(setting_line)
with open(CONF_FILE, 'w') as conf_file:
conf_file.writelines(lines)
def subcommand_set_default_skin(arguments):
"""Set a default skin."""
skin = arguments.skin
_update_setting('$wgDefaultSkin ', f'$wgDefaultSkin = "{skin}";\n')
def subcommand_set_server_url(arguments):
"""Set the value of $wgServer for this MediaWiki server."""
# This is a required setting from MediaWiki 1.34
_update_setting('$wgServer', f'$wgServer = "{arguments.server_url}";\n')
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()

View File

@ -92,15 +92,20 @@ def parse_arguments():
subparsers.add_parser('get-log', help='Print the automatic upgrades log')
subparsers.add_parser('setup', help='Setup apt preferences')
setup_repositories = subparsers.add_parser(
'setup-repositories',
help='Setup software repositories for FreedomBox')
setup_repositories.add_argument('--develop', required=False, default=False,
activate_backports = subparsers.add_parser(
'activate-backports', help='Activate backports if possible')
activate_backports.add_argument('--develop', required=False, default=False,
action='store_true',
help='Development mode')
setup_repositories.add_argument(
'--test-upgrade', required=False, default=False, action='store_true',
help='Test dist-upgrade from stable to testing')
dist_upgrade = subparsers.add_parser(
'dist-upgrade', help='Perform dist upgrade if possible')
dist_upgrade.add_argument('--develop', required=False, default=False,
action='store_true', help='Development mode')
dist_upgrade.add_argument('--test', required=False, default=False,
action='store_true',
help='Test dist-upgrade from stable to testing')
subparsers.required = True
return parser.parse_args()
@ -407,17 +412,24 @@ def subcommand_setup(_):
_add_apt_preferences()
def subcommand_setup_repositories(arguments):
def subcommand_activate_backports(arguments):
"""Setup software repositories needed for FreedomBox.
Repositories list for now only contains the backports. If the file exists,
assume that it contains backports.
Check if a new stable release is available, and perform
dist-upgrade if updates are enabled.
"""
_check_and_backports_sources(arguments.develop)
_check_and_dist_upgrade(arguments.develop, arguments.test_upgrade)
def subcommand_dist_upgrade(arguments):
"""Perform major distribution upgrade.
Check if a new stable release is available, and perform dist-upgrade if
updates are enabled.
"""
_check_and_dist_upgrade(arguments.develop, arguments.test)
def main():

67
debian/changelog vendored
View File

@ -1,3 +1,70 @@
plinth (20.17) unstable; urgency=medium
[ Fioddor Superconcentrado ]
* package: i18n: Mark progress status strings for translation
* networks: i18n: Mark string for translation on delete page
* networks: i18n: Mark various strings for translation
* notifications: i18n: Mark app names and extra data for translation
* networks: css: Make button wider in network list
* Translated using Weblate (Spanish)
[ Sunil Mohan Adapa ]
* backups: i18n: Mark form success messages for translation
* doc: wikiparser: Fix issue with running parser outside doc/ dir
* upgrades: Disable the option when not able to dist upgrade
* ci: Split testing stages into smaller stages
[ Coucouf ]
* Translated using Weblate (French)
* Translated using Weblate (French)
[ Burak Yavuz ]
* Translated using Weblate (Turkish)
* Translated using Weblate (Turkish)
[ Nikita Epifanov ]
* Translated using Weblate (Russian)
[ Jens Molgaard ]
* Translated using Weblate (Danish)
[ Petter Reinholdtsen ]
* Translated using Weblate (Norwegian Bokmål)
[ Praveen Illa ]
* Translated using Weblate (Telugu)
[ James Valleroy ]
* Translated using Weblate (Danish)
* ci: Run wikiparser doctests
* wikiparser: Exit with return value 1 on test failure
* upgrades: Add a setting to enable dist upgrade
* locale: Update translation strings
* doc: Fetch latest manual
[ Michael Breidenbach ]
* Translated using Weblate (German)
* Translated using Weblate (Swedish)
[ marklin0913 ]
* Added translation using Weblate (Chinese (Traditional))
[ Joseph Nuthalapati ]
* mediawiki: Ensure password file is not empty
* mediawiki: Add action to set domain name
[ Dietmar ]
* Translated using Weblate (German)
* Translated using Weblate (Italian)
[ Radek Pasiok ]
* Translated using Weblate (Polish)
[ Onurb ]
* apache: setup uwsgi by default
-- James Valleroy <jvalleroy@mailbox.org> Mon, 02 Nov 2020 19:45:57 -0500
plinth (20.16~bpo10+1) buster-backports; urgency=medium
* Rebuild for buster-backports.

View File

@ -38,7 +38,21 @@ If your !FreedomBox is behind a router, you will need to set up port forwarding
{{{
client
remote mybox.sds-ip.de 1194
remote mybox.freedombox.rocks 1194
proto udp
}}}
=== Troubleshooting ===
If your network doesn't support IPv6, you might have to remove the following line from your OpenVPN client configuration. This is especially in cases where your server supports IPv6 but client does not thus confusing the OpenVPN client on which protocol to use.
{{{
proto udp6
}}}
To connect via IPv4, ensure that the following line is present.
{{{
proto udp
}}}

View File

@ -10,6 +10,27 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f
The following are the release notes for each !FreedomBox version.
== FreedomBox 20.17 (2020-11-02) ==
=== Highlights ===
* locale: Add Chinese (Traditional) translation
* mediawiki: Add action to set domain name
* upgrades: Add a setting to enable dist upgrade
=== Other Changes ===
* apache: setup uwsgi by default
* backups: i18n: Mark form success messages for translation
* locale: Update translations for Danish, French, German, Italian, Norwegian Bokmål, Polish, Russian, Spanish, Swedish, Telugu, Turkish
* mediawiki: Ensure password file is not empty
* networks: css: Make button wider in network list
* networks: i18n: Mark string for translation on delete page
* networks: i18n: Mark various strings for translation
* notifications: i18n: Mark app names and extra data for translation
* package: i18n: Mark progress status strings for translation
* upgrades: Disable the option when not able to dist upgrade
== FreedomBox 20.16 (2020-10-19) ==
=== Highlights ===

View File

@ -65,6 +65,11 @@ This will generate a QR code that is readable by the mobile client.
The advantage of this approach is that there is no need to transfer sensitive information via data channels that can potentially be compromised and there is no need for any additional software.
=== External Links ===
* Website: https://www.wireguard.com
## END_INCLUDE
Back to [[FreedomBox/Features|Features introduction]] or [[FreedomBox/Manual|manual]] pages.

View File

@ -42,6 +42,19 @@ remote tu.freedombox.org 1194
proto udp
}}}
=== Resolución de problemas ===
Si tu red no soporta IPv6 quizá tengas que eliminar la siguiente línea de la configuración del cliente OpenVPN. Especialmente en casos en los que tu servidor soporta IPv6 pero tu cliente no, lo que causa confusión en el cliente respecto a qué protocolo emplear.
{{{
proto udp6
}}}
Para conectar por IPv4, asegúrate de que la siguiente línea consta.
{{{
proto udp
}}}
=== Navegar por Internet tras conectar a una VPN ===
Tras conectar a la VPN el dispositivo cliente podrá navegar por Internet sin más configuración adicional. No obstante una pre-condición para que esto funcione es que necesitas tener al menos 1 interfaz (tarjeta) de red conectado a Internet en la zona ''Externa'' del cortafuegos. Usa la página de configuración de redes para editar la zona del cortafuegos con los interfaces (tarjetas) de red del dispositivo.

View File

@ -10,6 +10,27 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f
The following are the release notes for each !FreedomBox version.
== FreedomBox 20.17 (2020-11-02) ==
=== Highlights ===
* locale: Add Chinese (Traditional) translation
* mediawiki: Add action to set domain name
* upgrades: Add a setting to enable dist upgrade
=== Other Changes ===
* apache: setup uwsgi by default
* backups: i18n: Mark form success messages for translation
* locale: Update translations for Danish, French, German, Italian, Norwegian Bokmål, Polish, Russian, Spanish, Swedish, Telugu, Turkish
* mediawiki: Ensure password file is not empty
* networks: css: Make button wider in network list
* networks: i18n: Mark string for translation on delete page
* networks: i18n: Mark various strings for translation
* notifications: i18n: Mark app names and extra data for translation
* package: i18n: Mark progress status strings for translation
* upgrades: Disable the option when not able to dist upgrade
== FreedomBox 20.16 (2020-10-19) ==
=== Highlights ===

View File

@ -65,6 +65,11 @@ Esto generará un código QR legible desde el cliente móvil.
La ventaja de este enfoque es que no hay necesidad de software adicional ni de transferir información sensible a través de canales de datos que podrían estar comprometidos.
=== Enlaces externos ===
* Sitio web: https://www.wireguard.com
## END_INCLUDE
Volver a la [[es/FreedomBox/Features|descripción de Funcionalidades]] o a las páginas del [[es/FreedomBox/Manual|manual]].

View File

@ -6,6 +6,7 @@ MoinMoin wiki parser
import logging
import re
import sys
import urllib
from enum import Enum
from pathlib import Path
@ -693,7 +694,8 @@ def resolve_url(url, context):
link_language = DEFAULT_LANGUAGE
# Check for local file and use local path
file_ = Path(f'manual/{link_language}') / (link_page + '.raw.wiki')
file_ = Path(__file__).parent.parent
file_ = file_ / f'manual/{link_language}' / (link_page + '.raw.wiki')
if file_.exists():
help_base = LOCAL_BASE.format(lang=link_language)
url = f'{help_base}{link_page}'
@ -2060,7 +2062,9 @@ if __name__ == '__main__':
if not arguments.skip_tests:
# Make tests verbose if no input files given
verbose = not arguments.input
doctest.testmod(verbose=verbose)
num_failed = doctest.testmod(verbose=verbose)[0]
if num_failed > 0:
sys.exit(1)
for in_file in arguments.input:
with in_file.open() as wiki_file:

View File

@ -3,4 +3,4 @@
Package init file.
"""
__version__ = '20.16'
__version__ = '20.17'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -17,9 +17,10 @@ version = 8
is_essential = True
managed_services = ['apache2']
managed_services = ['apache2', 'uwsgi']
managed_packages = ['apache2', 'php-fpm', 'ssl-cert']
managed_packages = ['apache2', 'php-fpm', 'ssl-cert', 'uwsgi',
'uwsgi-plugin-python3']
app = None

View File

@ -17,6 +17,7 @@ from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from django.views.generic import FormView, TemplateView, View
from plinth.errors import PlinthError
@ -52,7 +53,7 @@ class CreateArchiveView(SuccessMessageMixin, FormView):
prefix = 'backups'
template_name = 'backups_form.html'
success_url = reverse_lazy('backups:index')
success_message = _('Archive created.')
success_message = ugettext_lazy('Archive created.')
def get_context_data(self, **kwargs):
"""Return additional context for rendering the template."""
@ -140,7 +141,7 @@ class BaseRestoreView(SuccessMessageMixin, FormView):
prefix = 'backups'
template_name = 'backups_restore.html'
success_url = reverse_lazy('backups:index')
success_message = _('Restored files from backup.')
success_message = ugettext_lazy('Restored files from backup.')
def get_form_kwargs(self):
"""Pass additional keyword args for instantiating the form."""

View File

@ -17,9 +17,7 @@ from .manifest import backup, clients # noqa, pylint: disable=unused-import
version = 2
managed_packages = ['bepasty', 'uwsgi', 'uwsgi-plugin-python3']
managed_services = ['uwsgi']
managed_packages = ['bepasty']
_description = [
_('bepasty is a web application that allows large files to be uploaded '

View File

@ -223,7 +223,7 @@ def _warn_about_low_ram_space(request):
title = ugettext_noop('Low Memory')
data = {
'app_icon': 'fa-heartbeat',
'app_name': ugettext_noop('Diagnostics'),
'app_name': 'translate:' + ugettext_noop('Diagnostics'),
'percent_used': f'{memory_info["percent_used"]:.1f}',
'memory_available': f'{memory_available:.1f}',
'memory_available_unit': 'translate:' + memory_available_unit,

View File

@ -4,6 +4,7 @@ FreedomBox app to configure MediaWiki.
"""
import re
from urllib.parse import urlparse
from django.utils.translation import ugettext_lazy as _
@ -16,7 +17,7 @@ from plinth.modules.firewall.components import Firewall
from .manifest import backup, clients # noqa, pylint: disable=unused-import
version = 8
version = 9
managed_packages = ['mediawiki', 'imagemagick', 'php-sqlite3']
@ -39,6 +40,9 @@ _description = [
app = None
STATIC_CONFIG_FILE = '/etc/mediawiki/FreedomBoxStaticSettings.php'
USER_CONFIG_FILE = '/etc/mediawiki/FreedomBoxSettings.php'
class MediaWikiApp(app_module.App):
"""FreedomBox app for MediaWiki."""
@ -109,24 +113,44 @@ def is_public_registration_enabled():
def is_private_mode_enabled():
""" Return whether private mode is enabled or disabled"""
"""Return whether private mode is enabled or disabled."""
output = actions.superuser_run('mediawiki', ['private-mode', 'status'])
return output.strip() == 'enabled'
def get_default_skin():
"""Return the value of the default skin"""
def _find_skin(config_file):
with open(config_file, 'r') as config:
for line in config:
if line.startswith('$wgDefaultSkin'):
return re.findall(r'["\'][^"\']*["\']',
line)[0].strip('"\'')
def _get_config_value_in_file(setting_name, config_file):
"""Return the value of a setting from a config file."""
with open(config_file, 'r') as config:
for line in config:
if line.startswith(setting_name):
return re.findall(r'["\'][^"\']*["\']', line)[0].strip('"\'')
return None
user_config = '/etc/mediawiki/FreedomBoxSettings.php'
static_config = '/etc/mediawiki/FreedomBoxStaticSettings.php'
return _find_skin(user_config) or _find_skin(static_config)
def _get_config_value(setting_name):
"""Return a configuration value from multiple configuration files."""
return _get_config_value_in_file(setting_name, USER_CONFIG_FILE) or \
_get_config_value_in_file(setting_name, STATIC_CONFIG_FILE)
def get_default_skin():
"""Return the value of the default skin."""
return _get_config_value('$wgDefaultSkin')
def set_default_skin(skin):
"""Set the value of the default skin."""
actions.superuser_run('mediawiki', ['set-default-skin', skin])
def get_server_url():
"""Return the value of the server URL."""
server_url = _get_config_value('$wgServer')
return urlparse(server_url).netloc
def set_server_url(server_url):
"""Set the value of $wgServer."""
actions.superuser_run('mediawiki',
['set-server-url', f'https://{server_url}'])

View File

@ -38,3 +38,6 @@ $wgSessionCacheType = CACHE_DB;
# Use the mobile-friendly skin Timeless by default
$wgDefaultSkin = "timeless";
# Domain Name
$wgServer = "https://freedombox.local";

View File

@ -6,6 +6,8 @@ FreedomBox app for configuring MediaWiki.
import pathlib
from django import forms
from django.forms import Widget
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
@ -19,6 +21,29 @@ def get_skins():
if skin.is_dir()]
class PrependWidget(Widget):
"""Widget to create input-groups with prepended text."""
def __init__(self, base_widget, data, *args, **kwargs):
"""Initialize widget and get base instance"""
super(PrependWidget, self).__init__(*args, **kwargs)
self.base_widget = base_widget(*args, **kwargs)
self.data = data
def render(self, name, value, attrs=None, renderer=None):
"""Render base widget and add bootstrap spans."""
attrs['class'] = 'form-control'
field = self.base_widget.render(name, value, attrs, renderer)
widget_html = '''
<div class="input-group">
<span class="input-group-addon">
%(data)s
</span>
%(field)s
</div>'''
return mark_safe((widget_html) % {'field': field, 'data': self.data})
class MediaWikiForm(forms.Form): # pylint: disable=W0232
"""MediaWiki configuration form."""
password = forms.CharField(
@ -27,6 +52,12 @@ class MediaWikiForm(forms.Form): # pylint: disable=W0232
'(admin). Leave this field blank to keep the current password.'),
required=False, widget=forms.PasswordInput)
server_url = forms.CharField(
label=_('Server URL'), required=False, help_text=_(
'Used by MediaWiki to generate URLs that point to the wiki '
'such as in footer, feeds and emails.'),
widget=PrependWidget(base_widget=forms.TextInput, data='https://'))
enable_public_registrations = forms.BooleanField(
label=_('Enable public registrations'), required=False,
help_text=_('If enabled, anyone on the internet will be able to '

View File

@ -0,0 +1,53 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Common test fixtures for MediaWiki.
"""
import shutil
import importlib
import pathlib
import types
from unittest.mock import patch
import pytest
current_directory = pathlib.Path(__file__).parent
def _load_actions_module():
actions_file_path = str(current_directory / '..' / '..' / '..' / '..' /
'actions' / 'mediawiki')
loader = importlib.machinery.SourceFileLoader('mediawiki',
actions_file_path)
module = types.ModuleType(loader.name)
loader.exec_module(module)
return module
actions = _load_actions_module()
@pytest.fixture(name='call_action')
def fixture_call_action(capsys, conf_file):
"""Run actions with custom root path."""
def _call_action(module_name, args, **kwargs):
actions.CONF_FILE = conf_file
with patch('argparse._sys.argv', [module_name] + args):
actions.main()
captured = capsys.readouterr()
return captured.out
return _call_action
@pytest.fixture(name='conf_file')
def fixture_conf_file(tmp_path):
"""Uses a dummy configuration file."""
settings_file_name = 'FreedomBoxSettings.php'
conf_file = tmp_path / settings_file_name
conf_file.touch()
shutil.copyfile(
str(current_directory / '..' / 'data' / 'etc' / 'mediawiki' /
settings_file_name), str(conf_file))
return str(conf_file)

View File

@ -7,6 +7,7 @@ Feature: MediaWiki Wiki Engine
Background:
Given I'm a logged in user
Given the mediawiki application is installed
Given the server url is set to test config url
Scenario: Enable mediawiki application
Given the mediawiki application is disabled

View File

@ -4,14 +4,21 @@ Functional, browser based tests for mediawiki app.
"""
import pathlib
from urllib.parse import urlparse
from pytest_bdd import parsers, scenarios, then, when
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
from plinth.tests.functional import config
scenarios('mediawiki.feature')
@given(parsers.parse('the server url is set to test config url'))
def set_server_url(session_browser):
_set_server_url(session_browser)
@when(parsers.parse('I enable mediawiki public registrations'))
def enable_mediawiki_public_registrations(session_browser):
_enable_public_registrations(session_browser)
@ -57,8 +64,7 @@ def mediawiki_allows_anonymous_reads_edits(session_browser):
@then(
parsers.parse(
'the mediawiki site should not allow anonymous reads and writes'))
def mediawiki_does_not_allow__account_creation_anonymous_reads_edits(
session_browser):
def mediawiki_does_not_allow_anonymous_reads_edits(session_browser):
_verify_no_anonymous_reads_edits_link(session_browser)
@ -216,3 +222,11 @@ def __has_main_page(browser):
functional.visit(browser, '/mediawiki/Main_Page')
content = browser.find_by_id('mw-content-text').first
return 'This page has been deleted.' not in content.text
def _set_server_url(browser):
"""Set the value of server url to the value in the given env_var."""
functional.nav_to_module(browser, 'mediawiki')
server_url = urlparse(config['DEFAULT']['url']).netloc
browser.find_by_id('id_server_url').fill(server_url)
functional.submit(browser, form_class='form-configuration')

View File

@ -0,0 +1,46 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Test module for MediaWiki utility functions.
"""
import pathlib
from unittest.mock import patch
import pytest
from plinth.modules import mediawiki
@pytest.fixture(name='test_configuration', autouse=True)
def fixture_test_configuration(call_action, conf_file):
"""Use a separate MediaWiki configuration for tests.
Uses local FreedomBoxStaticSettings.php, a temp version of
FreedomBoxSettings.php and patches actions.superuser_run with the fixture
call_action
"""
data_directory = pathlib.Path(__file__).parent.parent / 'data'
static_config_file = str(data_directory / 'etc' / 'mediawiki' /
mediawiki.STATIC_CONFIG_FILE.split('/')[-1])
with patch('plinth.modules.mediawiki.STATIC_CONFIG_FILE',
static_config_file), \
patch('plinth.modules.mediawiki.USER_CONFIG_FILE', conf_file), \
patch('plinth.actions.superuser_run', call_action):
yield
def test_default_skin():
"""Test getting and setting the default skin."""
assert mediawiki.get_default_skin() == 'timeless'
new_skin = 'vector'
mediawiki.set_default_skin(new_skin)
assert mediawiki.get_default_skin() == new_skin
def test_server_url():
"""Test getting and setting $wgServer."""
assert mediawiki.get_server_url() == 'freedombox.local'
new_server_url = 'mydomain.freedombox.rocks'
mediawiki.set_server_url(new_server_url)
assert mediawiki.get_server_url() == new_server_url

View File

@ -11,7 +11,7 @@ from django.utils.translation import ugettext as _
from plinth import actions, views
from plinth.modules import mediawiki
from . import (get_default_skin, is_private_mode_enabled,
from . import (get_default_skin, get_server_url, is_private_mode_enabled,
is_public_registration_enabled)
from .forms import MediaWikiForm
@ -30,7 +30,8 @@ class MediaWikiAppView(views.AppView):
initial.update({
'enable_public_registrations': is_public_registration_enabled(),
'enable_private_mode': is_private_mode_enabled(),
'default_skin': get_default_skin()
'default_skin': get_default_skin(),
'server_url': get_server_url()
})
return initial
@ -39,15 +40,15 @@ class MediaWikiAppView(views.AppView):
old_config = self.get_initial()
new_config = form.cleaned_data
def is_unchanged(key):
return old_config[key] == new_config[key]
def is_changed(key):
return old_config.get(key) != new_config.get(key)
if new_config['password']:
actions.superuser_run('mediawiki', ['change-password'],
input=new_config['password'].encode())
messages.success(self.request, _('Password updated'))
if not is_unchanged('enable_public_registrations'):
if is_changed('enable_public_registrations'):
# note action public-registration restarts, if running now
if new_config['enable_public_registrations']:
if not new_config['enable_private_mode']:
@ -65,7 +66,7 @@ class MediaWikiAppView(views.AppView):
messages.success(self.request,
_('Public registrations disabled'))
if not is_unchanged('enable_private_mode'):
if is_changed('enable_private_mode'):
if new_config['enable_private_mode']:
actions.superuser_run('mediawiki', ['private-mode', 'enable'])
messages.success(self.request, _('Private mode enabled'))
@ -80,9 +81,12 @@ class MediaWikiAppView(views.AppView):
shortcut = mediawiki.app.get_component('shortcut-mediawiki')
shortcut.login_required = new_config['enable_private_mode']
if not is_unchanged('default_skin'):
actions.superuser_run(
'mediawiki', ['set-default-skin', new_config['default_skin']])
if is_changed('default_skin'):
mediawiki.set_default_skin(new_config['default_skin'])
messages.success(self.request, _('Default skin changed'))
if is_changed('server_url'):
mediawiki.set_server_url(new_config['server_url'])
messages.success(self.request, _('Server URL updated'))
return super().form_valid(form)

View File

@ -35,8 +35,7 @@ class ConnectionForm(forms.Form):
label=_('Firewall Zone'),
help_text=_('The firewall zone will control which services are '
'available over this interfaces. Select Internal only '
'for trusted networks.'),
choices=[('external', _('External')), ('internal', _('Internal'))])
'for trusted networks.'), choices=network.ZONES)
ipv4_method = forms.ChoiceField(
label=_('IPv4 Addressing Method'), help_text=format_lazy(
_('"Automatic" method will make {box_name} acquire '

View File

@ -71,17 +71,17 @@
<div class="list-group">
<div class="list-group-item">
{% trans "State" %}
<span class="pull-right">{{ device.state }}</span>
<span class="pull-right">{{ device.state_string }}</span>
</div>
{% if device.state_reason != 'none' %}
<div class="list-group-item">
{% trans "State reason" %}
<span class="pull-right">{{ device.state_reason }}</span>
<span class="pull-right">{{ device.state_reason_string }}</span>
</div>
{% endif %}
<div class="list-group-item">
{% trans "Type" %}
<span class="pull-right">{{ device.type }}</span>
<span class="pull-right">{{ device.type_string }}</span>
</div>
<div class="list-group-item">
{% trans "MAC address" %}
@ -140,7 +140,7 @@
</div>
<div class="list-group-item">
{% trans "Mode" %}
<span class="pull-right">{{ device.wireless.mode }}</span>
<span class="pull-right">{{ device.wireless.mode_string }}</span>
</div>
{% endif %}
{% if access_point.channel %}
@ -169,7 +169,7 @@
{% if connection.ipv4.method %}
<div class="list-group-item">
{% trans "Method" %}
<span class="pull-right">{{ connection.ipv4.method }}</span>
<span class="pull-right">{{ connection.ipv4.method_string }}</span>
</div>
{% endif %}
@ -210,7 +210,7 @@
{% if connection.ipv6.method %}
<div class="list-group-item">
{% trans "Method" %}
<span class="pull-right">{{ connection.ipv6.method }}</span>
<span class="pull-right">{{ connection.ipv6.method_string }}</span>
</div>
{% endif %}
@ -255,7 +255,7 @@
<div class="list-group-item">
{% trans "Firewall zone" %}
<div class="pull-right">
<span class="label label-success">{{ connection.zone }}</span>
<span class="label label-success">{{ connection.zone_string }}</span>
</div>
</div>
</div>
@ -275,7 +275,7 @@
<div class="list-group-item">
{% trans "Firewall zone" %}
<div class="pull-right">
<span class="label label-warning">{{ connection.zone }}</span>
<span class="label label-warning">{{ connection.zone_string }}</span>
</div>
</div>
</div>
@ -294,7 +294,7 @@
<div class="list-group-item">
{% trans "Firewall zone" %}
<div class="pull-right">
<span class="label label-warning">external</span>
<span class="label label-warning">{% trans "External" %}</span>
</div>
</div>
</div>

View File

@ -20,7 +20,7 @@
{% csrf_token %}
<input type="submit" class="btn btn-md btn-danger"
value="Delete {{ name }}"/>
value="{% blocktrans %}Delete {{ name }}{% endblocktrans %}"/>
</form>
{% endblock %}

View File

@ -34,7 +34,7 @@
}
.form-action button {
width: 7em;
min-width: 7em;
}
</style>
{% endblock %}

View File

@ -8,6 +8,7 @@ from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from django.views.decorators.http import require_POST
from django.views.generic.edit import FormView
@ -20,6 +21,98 @@ from .forms import (ConnectionTypeSelectForm, EthernetForm, GenericForm,
logger = logging.getLogger(__name__)
# i18n for device.state
# https://developer.gnome.org/libnm/1.29/libnm-nm-dbus-interface.html#NMDeviceState
CONNECTION_METHOD_STRINGS = {
'disabled': ugettext_lazy('disabled'),
'auto': ugettext_lazy('automatic'),
'manual': ugettext_lazy('manual'),
'shared': ugettext_lazy('shared'),
'link-local': ugettext_lazy('link-local'),
}
# i18n for device.state
# https://developer.gnome.org/libnm/1.29/libnm-nm-dbus-interface.html#NMDeviceState
DEVICE_STATE_STRINGS = {
'unknown': ugettext_lazy('unknown'),
'unmanaged': ugettext_lazy('unmanaged'),
'unavailable': ugettext_lazy('unavailable'),
'disconnected': ugettext_lazy('disconnected'),
'prepare': ugettext_lazy('preparing'),
'config': ugettext_lazy('connecting'),
'need-auth': ugettext_lazy('needs authentication'),
'ip-config': ugettext_lazy('requesting address'),
'ip-check': ugettext_lazy('checking'),
'secondaries': ugettext_lazy('waiting for secondary'),
'activated': ugettext_lazy('activated'),
'deactivating': ugettext_lazy('deactivating'),
'failed': ugettext_lazy('failed'),
}
# i18n for device.state_reason
# https://developer.gnome.org/libnm/1.29/libnm-nm-dbus-interface.html#NMDeviceStateReason
DEVICE_STATE_REASON_STRINGS = {
'none':
ugettext_lazy('no reason'),
'unknown':
ugettext_lazy('unknown error'),
'now-managed':
ugettext_lazy('device is now managed'),
'now-unmanaged':
ugettext_lazy('device is now unmanaged'),
'config-failed':
ugettext_lazy('configuration failed'),
'no-secrets':
ugettext_lazy('secrets required'),
'dhcp-start-failed':
ugettext_lazy('DHCP client failed to start'),
'dhcp-error':
ugettext_lazy('DHCP client error'),
'dhcp-failed':
ugettext_lazy('DHCP client failed'),
'shared-start-failed':
ugettext_lazy('shared connection service failed to start'),
'shared-failed':
ugettext_lazy('shared connection service failed'),
'removed':
ugettext_lazy('device was removed'),
'user-requested':
ugettext_lazy('device disconnected by user'),
'dependency-failed':
ugettext_lazy('a dependency of the connection failed'),
'ssid-not-found':
ugettext_lazy('Wi-Fi network not found'),
'secondary-connection-failed':
ugettext_lazy('a secondary connection failed'),
'new-activation':
ugettext_lazy('new connection activation was enqueued'),
'ip-address-duplicate':
ugettext_lazy('a duplicate IP address was detected'),
'ip-method-unsupported':
ugettext_lazy('selected IP method is not supported'),
}
# i18n for device.type
# https://developer.gnome.org/libnm/1.29/libnm-nm-dbus-interface.html#NMDeviceType
DEVICE_TYPE_STRINGS = {
'unknown': ugettext_lazy('unknown'),
'ethernet': ugettext_lazy('Ethernet'),
'wifi': ugettext_lazy('Wi-Fi'),
'generic': ugettext_lazy('generic'),
'tun': ugettext_lazy('TUN or TAP interface'),
'wireguard': ugettext_lazy('WireGuard'),
}
# i18n for wireless.mode
# https://developer.gnome.org/libnm/1.29/libnm-nm-dbus-interface.html#NM80211Mode
WIRELESS_MODE_STRINGS = {
'unknown': ugettext_lazy('unknown'),
'adhoc': ugettext_lazy('ad-hoc'),
'infra': ugettext_lazy('infrastructure'),
'ap': ugettext_lazy('access point'),
'mesh': ugettext_lazy('mesh point'),
}
def index(request):
"""Show connection list."""
@ -52,6 +145,14 @@ def show(request, uuid):
# Connection status
connection_status = network.get_status_from_connection(connection)
connection_status['zone_string'] = dict(network.ZONES).get(
connection_status['zone'], connection_status['zone'])
connection_status['ipv4']['method_string'] = CONNECTION_METHOD_STRINGS.get(
connection_status['ipv4']['method'],
connection_status['ipv4']['method'])
connection_status['ipv6']['method_string'] = CONNECTION_METHOD_STRINGS.get(
connection_status['ipv6']['method'],
connection_status['ipv6']['method'])
# Active connection status
try:
@ -72,12 +173,21 @@ def show(request, uuid):
device = network.get_device_by_interface_name(interface_name)
device_status = network.get_status_from_device(device)
device_status['state_string'] = DEVICE_STATE_STRINGS.get(
device_status['state'], device_status['state'])
device_status['state_reason_string'] = DEVICE_STATE_REASON_STRINGS.get(
device_status['state_reason'], device_status['state_reason'])
device_status['type_string'] = DEVICE_TYPE_STRINGS.get(
device_status['type'], device_status['type'])
# Access point status
access_point_status = None
if connection_status['type'] == '802-11-wireless':
access_point_status = network.get_status_from_wifi_access_point(
device, connection_status['wireless']['ssid'])
connection_status['wireless'][
'mode_string'] = WIRELESS_MODE_STRINGS.get(
connection['wireless']['mode'], connection['wireless']['mode'])
return TemplateResponse(
request, 'connection_show.html', {

View File

@ -20,9 +20,7 @@ from .manifest import backup, clients # noqa, pylint: disable=unused-import
version = 2
managed_packages = ['radicale', 'uwsgi', 'uwsgi-plugin-python3']
managed_services = ['uwsgi']
managed_packages = ['radicale']
_description = [
format_lazy(

View File

@ -19,7 +19,7 @@ from .manifest import (PUBLIC_ACCESS_SETTING_FILE, # noqa, pylint: disable=unus
version = 4
managed_packages = ['searx', 'uwsgi', 'uwsgi-plugin-python3']
managed_packages = ['searx']
_description = [
_('Searx is a privacy-respecting Internet metasearch engine. '

View File

@ -309,14 +309,14 @@ def warn_about_low_disk_space(request):
title = ugettext_noop('Low disk space')
data = {
'app_icon': 'fa-hdd-o',
'app_name': ugettext_noop('Storage'),
'app_name': 'translate:' + ugettext_noop('Storage'),
'percent_used': root_info['percent_used'],
'free_space': format_bytes(root_info['free_bytes'])
}
actions = [{
'type': 'link',
'class': 'primary',
'text': 'Go to {app_name}',
'text': ugettext_noop('Go to {app_name}'),
'url': 'storage:index'
}, {
'type': 'dismiss'
@ -338,7 +338,11 @@ def report_failing_drive(id, is_failing):
message = ugettext_noop(
'Disk {id} is reporting that it is likely to fail in the near future. '
'Copy any data while you still can and replace the drive.')
data = {'id': id}
data = {
'app_icon': 'fa-hdd-o',
'app_name': 'translate:' + ugettext_noop('Storage'),
'id': id
}
note = Notification.update_or_create(id=notification_id, app_id='storage',
severity='error', title=title,
message=message, actions=[{

View File

@ -14,11 +14,11 @@ from django.utils.translation import ugettext_noop
import plinth
from plinth import actions
from plinth import app as app_module
from plinth import cfg, glib, menu
from plinth import cfg, glib, kvstore, menu
from .manifest import backup # noqa, pylint: disable=unused-import
version = 7
version = 8
is_essential = True
@ -45,6 +45,8 @@ app = None
BACKPORTS_REQUESTED_KEY = 'upgrades_backports_requested'
DIST_UPGRADE_ENABLED_KEY = 'upgrades_dist_upgrade_enabled'
SOURCES_LIST = '/etc/apt/sources.list'
BACKPORTS_SOURCES_LIST = '/etc/apt/sources.list.d/freedombox2.list'
@ -100,7 +102,7 @@ class UpgradesApp(app_module.App):
data = {
'version': plinth.__version__,
'app_name': 'Update',
'app_name': 'translate:' + ugettext_noop('Updates'),
'app_icon': 'fa-refresh'
}
title = ugettext_noop('FreedomBox Updated')
@ -128,6 +130,11 @@ def setup(helper, old_version=None):
if old_version and old_version < 7:
set_backports_requested(can_activate_backports())
# Enable dist upgrade for new installs, and once when upgrading
# from version without flag.
if not old_version or old_version < 8:
set_dist_upgrade_enabled(can_enable_dist_upgrade())
# Try to setup apt repositories, if needed, if possible, on first install
# and on version increment.
helper.call('post', setup_repositories, None)
@ -152,26 +159,42 @@ def disable():
def setup_repositories(data):
"""Setup apt repositories for backports or new stable release."""
if is_backports_requested():
command = ['setup-repositories']
command = ['activate-backports']
if cfg.develop:
command += ['--develop']
command.append('--develop')
actions.superuser_run('upgrades', command)
if is_dist_upgrade_enabled():
command = ['dist-upgrade']
if cfg.develop:
command.append('--develop')
actions.superuser_run('upgrades', command)
def is_backports_requested():
"""Return whether user has chosen to activate backports."""
from plinth import kvstore
return kvstore.get_default(BACKPORTS_REQUESTED_KEY, False)
def set_backports_requested(requested):
"""Set whether user has chosen to activate backports."""
from plinth import kvstore
kvstore.set(BACKPORTS_REQUESTED_KEY, requested)
logger.info('Backports requested - %s', requested)
def is_dist_upgrade_enabled():
"""Return whether user has enabled dist upgrade."""
return kvstore.get_default(DIST_UPGRADE_ENABLED_KEY, False)
def set_dist_upgrade_enabled(enabled=True):
"""Set whether user has enabled dist upgrade."""
kvstore.set(DIST_UPGRADE_ENABLED_KEY, enabled)
logger.info('Distribution upgrade configured - %s', enabled)
def is_backports_enabled():
"""Return whether backports are enabled in the system configuration."""
return os.path.exists(BACKPORTS_SOURCES_LIST)
@ -208,3 +231,9 @@ def can_activate_backports():
return False
return True
def can_enable_dist_upgrade():
"""Return whether dist upgrade can be enabled."""
release, _ = get_current_release()
return release not in ['unstable', 'testing']

View File

@ -6,6 +6,8 @@ Forms for configuring unattended-upgrades.
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.modules import upgrades
class ConfigureForm(forms.Form):
"""Configuration form to enable/disable automatic upgrades."""
@ -13,6 +15,18 @@ class ConfigureForm(forms.Form):
label=_('Enable auto-update'), required=False, help_text=_(
'When enabled, FreedomBox automatically updates once a day.'))
dist_upgrade_enabled = forms.BooleanField(
label=_('Enable auto-update to next stable release'), required=False,
help_text=_('When enabled, FreedomBox will update to the next stable '
'distribution release when it is available.'))
def __init__(self, *args, **kwargs):
"""Disable options as necessary."""
super().__init__(*args, **kwargs)
self.fields['dist_upgrade_enabled'].disabled = \
not upgrades.can_enable_dist_upgrade()
class BackportsFirstbootForm(forms.Form):
"""Form to configure backports during first boot wizard."""

View File

@ -28,7 +28,10 @@ class UpgradesConfigurationView(AppView):
app_id = 'upgrades'
def get_initial(self):
return {'auto_upgrades_enabled': upgrades.is_enabled()}
return {
'auto_upgrades_enabled': upgrades.is_enabled(),
'dist_upgrade_enabled': upgrades.is_dist_upgrade_enabled()
}
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
@ -69,6 +72,17 @@ class UpgradesConfigurationView(AppView):
messages.success(self.request,
_('Automatic upgrades disabled'))
if old_status['dist_upgrade_enabled'] \
!= new_status['dist_upgrade_enabled']:
upgrades.set_dist_upgrade_enabled(
new_status['dist_upgrade_enabled'])
if new_status['dist_upgrade_enabled']:
messages.success(self.request,
_('Distribution upgrade enabled'))
else:
messages.success(self.request,
_('Distribution upgrade disabled'))
return super().form_valid(form)

View File

@ -21,6 +21,8 @@ logger = logging.getLogger(__name__)
_client = None
ZONES = [('external', _('External')), ('internal', _('Internal'))]
CONNECTION_TYPE_NAMES = collections.OrderedDict([
('802-3-ethernet', _('Ethernet')),
('802-11-wireless', _('Wi-Fi')),
@ -51,6 +53,7 @@ def ipv4_int_to_string(address_int):
def init():
"""Create and keep a network manager client instance."""
def new_callback(source_object, result, user_data):
"""Called when new() operation is complete."""
global _client

View File

@ -9,8 +9,10 @@ import subprocess
import threading
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from plinth import actions
from plinth.utils import format_lazy
logger = logging.getLogger(__name__)
@ -153,13 +155,14 @@ class Transaction(object):
status_map = {
'pmstatus':
_('installing'),
ugettext_lazy('installing'),
'dlstatus':
_('downloading'),
ugettext_lazy('downloading'),
'media-change':
_('media change'),
ugettext_lazy('media change'),
'pmconffile':
_('configuration file: {file}').format(file=parts[1]),
format_lazy(ugettext_lazy('configuration file: {file}'),
file=parts[1]),
}
self.status_string = status_map.get(parts[0], '')
self.percentage = int(float(parts[2]))

View File

@ -18,7 +18,7 @@
<div class="app-icon fa {{ note.data.app_icon }}"></div>
{% elif note.data.app_icon_filename %}
<img src="{% static 'theme/icons/' %}{{ note.data.app_icon_filename }}.svg"
alt="{{ note.data.app_name }}"
alt="{{ note.data.app_name }}"
class="notification-icon" />
{% endif %}

View File

@ -47,7 +47,7 @@
This application is currently not available in your distribution.
{% endblocktrans %}
<button type="submit" class="btn btn-default btn-sm" name="refresh-packages">
<span class="fa fa-refresh"></span> Check again
<span class="fa fa-refresh"></span> {% trans "Check again" %}
</button>
</div>
{% endif %}

View File

@ -152,7 +152,10 @@ def is_available(browser, site_name):
not_404 = '404' not in browser.title
# The site might have a default path after the sitename,
# e.g /mediawiki/Main_Page
no_redirect = browser.url.startswith(url_to_visit.strip('/'))
print('URL =', browser.url, url_to_visit, browser.title)
browser_url = browser.url.partition('://')[2]
url_to_visit_without_proto = url_to_visit.strip('/').partition('://')[2]
no_redirect = browser_url.startswith(url_to_visit_without_proto)
return not_404 and no_redirect