app: Separate app enable/disable form from config form

- Introduce new API to mark an app that it can't be disabled.

- Mark jsxc, storage, config, upgrade and firewall apps as can't be disabled.

- Fixed functional tests

- Replaced AppForm with forms.Form in all modules' forms.py.

- Remove app.template.js.

- Remove unused styles.

- Remove app status checks in form_valid of Deluge, Diaspora, Matrix, Ejabberd,
MediaWiki, Storage, Transmission, Quassel

- Purge unused is_enabled context variables (Ikiwiki)

- ejabberd: Minor cleanup in template

- jsxc: Cleanup unneeded overrides

- tahoe: Cleanup unnecessary overrides

Tests performed:

- For all apps affected, test enable/disable button works and submitting
configuration form works: with changes updates message and without changes
'settings unchanged' message.
  - avahi
  - bind
  - cockpit
  - SKIP: coquelicot
  - datetime
  - deluge
  - SKIP: diaspora
  - ejabberd
  - gitweb
  - i2p
  - infinoted
  - ikiwiki
  - matrixsynapse
  - mediawiki
  - minetest
  - minidlna
  - mldonkey
  - mumble
  - pagekite
  - privoxy
  - quassel
  - radicale
  - roundcube
  - SKIP: samba
  - searx
  - SKIP: shaarli
  - shadowsocks
  - ssh
  - tahoe
  - transmission
  - FAIL: tt-rss (not installable)
  - wireguard
- Deluge test that configuration changes when app is disabled work
- Quassel test that setting the domain works when app is diabled
- Transmission test that setting the domain works when app is diabled
- Ikiwiki create form works properly
- Enable/disable button appears as expected when enabled and when disabled
- Enable/disable button works without Javascript
- Functional tests work for affected apps, Tor and OpenVPN
- AppForm is removed from developer documentation
  - Forms reference
  - Customizing tutorial
- Test all apps using directory select form
  - Transmission
  - Deluge
- Visit each template that overrides block configuration and ensure that it is
loaded properly and the display is as expected.
- All apps that use AppView that are not tested above should not have an
enable/disable button. That is JSXC, update, config, firewall, storage, users.

Signed-off-by: Alice Kile <buoyantair@protonmail.com>
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Veiko Aasa <veiko17@disroot.org>
This commit is contained in:
Alice Kile 2020-03-13 10:58:54 +05:30 committed by Veiko Aasa
parent bd8aee17d5
commit 0b5b384651
No known key found for this signature in database
GPG Key ID: 478539CAE680674E
47 changed files with 178 additions and 309 deletions

View File

@ -3,9 +3,6 @@
Forms
-----
.. autoclass:: plinth.forms.AppForm
:members:
.. autoclass:: plinth.forms.DomainSelectionForm
:members:

View File

@ -63,10 +63,8 @@ provide options to the user. Add the following to ``forms.py``.
from django import forms
from plinth.forms import AppForm
class TransmissionForm(AppForm): # pylint: disable=W0232
class TransmissionForm(forms.Form): # pylint: disable=W0232
"""Transmission configuration form"""
download_dir = forms.CharField(
label='Download directory',

View File

@ -91,14 +91,17 @@ def is_installed(browser, app_name):
def _change_app_status(browser, app_name, change_status_to='enabled'):
"""Enable or disable application."""
button = browser.find_by_id('app-toggle-button')
checkbox_id = get_app_checkbox_id(app_name)
checkbox = browser.find_by_id(checkbox_id)
button = browser.find_by_css('button[name="app_enable_disable_button"]')
if button:
if checkbox.checked and change_status_to == 'disabled' or (
not checkbox.checked and change_status_to == 'enabled'):
should_enable_field = browser.find_by_id('id_should_enable')
if (should_enable_field.value == 'False'
and change_status_to == 'disabled') or (
should_enable_field.value == 'True'
and change_status_to == 'enabled'):
interface.submit(browser, element=button)
else:
checkbox_id = get_app_checkbox_id(app_name)
_change_status(browser, app_name, checkbox_id, change_status_to)
if app_name in apps_with_loaders:

View File

@ -14,14 +14,21 @@ class App:
variation their behavior by choosing which components to have and by
customizing the components themselves.
'app_id' property of the app must string that is a globally unique ID. This
is typically also the name of the python module handling the app. So, it
should be all lower-case English alphabet and digits without any special
'app_id' property of the app must be a string that is a globally unique ID.
This is typically also the name of the python module handling the app. So,
it should be all lower-case English alphabet and digits without any special
characters.
'can_be_disabled' is a boolean indicating whether an app can be disabled by
the user. Enable/disable button for this app will not be shown. Default
value is True, so the app can be disabled.
"""
app_id = None
can_be_disabled = True
_all_apps = collections.OrderedDict()
def __init__(self):

View File

@ -17,11 +17,10 @@ from django.utils.translation import ugettext_lazy as _
import plinth
class AppForm(forms.Form):
"""Generic configuration form for an app."""
is_enabled = forms.BooleanField(
widget=CheckboxInput(attrs={'id': 'app-toggle-input'}),
label=_('Enable application'), required=False)
class AppEnableDisableForm(forms.Form):
"""Form to enable / disable an app."""
should_enable = forms.BooleanField(widget=forms.HiddenInput,
required=False)
class DomainSelectionForm(forms.Form):

View File

@ -7,8 +7,6 @@ from django import forms
from django.core.validators import validate_ipv46_address
from django.utils.translation import ugettext_lazy as _
from plinth.forms import AppForm
def validate_ips(ips):
"""Validate that ips is a list of IP addresses, separated by space."""
@ -16,7 +14,7 @@ def validate_ips(ips):
validate_ipv46_address(ip_addr)
class BindForm(AppForm):
class BindForm(forms.Form):
"""BIND configuration form"""
forwarders = forms.CharField(
label=_('Forwarders'), required=False, validators=[validate_ips],

View File

@ -42,6 +42,8 @@ class ConfigApp(app_module.App):
app_id = 'config'
can_be_disabled = False
def __init__(self):
"""Create components for the app."""
super().__init__()

View File

@ -6,10 +6,8 @@ Plinth form for configuring Coquelicot.
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.forms import AppForm
class CoquelicotForm(AppForm): # pylint: disable=W0232
class CoquelicotForm(forms.Form): # pylint: disable=W0232
"""Coquelicot configuration form."""
upload_password = forms.CharField(
label=_('Upload Password'),

View File

@ -9,12 +9,10 @@ import subprocess
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.forms import AppForm
logger = logging.getLogger(__name__)
class DateTimeForm(AppForm):
class DateTimeForm(forms.Form):
"""Date/time configuration form."""
time_zone = forms.ChoiceField(
label=_('Time Zone'),

View File

@ -32,15 +32,13 @@ class DelugeAppView(views.AppView):
old_status = form.initial
new_status = form.cleaned_data
# don't change the configuration if the application was disabled
if new_status['is_enabled'] or not old_status['is_enabled']:
if old_status['storage_path'] != new_status['storage_path']:
new_configuration = [
'download_location', new_status['storage_path']
]
if old_status['storage_path'] != new_status['storage_path']:
new_configuration = [
'download_location', new_status['storage_path']
]
actions.superuser_run('deluge', ['set-configuration'] +
new_configuration)
messages.success(self.request, _('Configuration updated'))
actions.superuser_run('deluge',
['set-configuration'] + new_configuration)
messages.success(self.request, _('Configuration updated'))
return super().form_valid(form)

View File

@ -6,10 +6,8 @@ Forms for configuring diaspora*
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.forms import AppForm
class DiasporaAppForm(AppForm):
class DiasporaAppForm(forms.Form):
"""Service Form with additional fields for diaspora*"""
is_user_registrations_enabled = forms.BooleanField(
label=_('Enable new user registrations'), required=False)

View File

@ -65,16 +65,10 @@ class DiasporaAppView(AppView):
def form_valid(self, form):
"""Enable/disable user registrations"""
old_enabled = form.initial['is_enabled']
new_enabled = form.cleaned_data['is_enabled']
old_registration = form.initial['is_user_registrations_enabled']
new_registration = form.cleaned_data['is_user_registrations_enabled']
if old_registration == new_registration:
if old_enabled == new_enabled:
if not self.request._messages._queued_messages:
messages.info(self.request, _('Setting unchanged'))
else:
if old_registration != new_registration:
if new_registration:
diaspora.enable_user_registrations()
messages.success(self.request, _('User registrations enabled'))

View File

@ -7,11 +7,10 @@ from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth import cfg
from plinth.forms import AppForm
from plinth.utils import format_lazy
class EjabberdForm(AppForm):
class EjabberdForm(forms.Form):
"""Ejabberd configuration form."""
MAM_enabled = forms.BooleanField(
label=_('Enable Message Archive Management'), required=False,

View File

@ -28,18 +28,3 @@
</p>
{% endblock %}
{% block configuration %}
<h3>{% trans "Configuration" %}</h3>
<form id="app-form" class="form form-configuration" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary"
value="{% trans "Update setup" %}"/>
</form>
{% endblock %}

View File

@ -36,21 +36,8 @@ class EjabberdAppView(AppView):
"""Enable/disable a service and set messages."""
old_status = form.initial
new_status = form.cleaned_data
app_same = old_status['is_enabled'] == new_status['is_enabled']
mam_same = old_status['MAM_enabled'] == new_status['MAM_enabled']
if app_same and mam_same:
# TODO: find a more reliable/official way to check whether the
# request has messages attached.
if not self.request._messages._queued_messages:
messages.info(self.request, _('Setting unchanged'))
elif not app_same:
if new_status['is_enabled']:
self.app.enable()
else:
self.app.disable()
if not mam_same:
if old_status['MAM_enabled'] != new_status['MAM_enabled']:
# note ejabberd action "enable" or "disable" restarts, if running
if new_status['MAM_enabled']:
actions.superuser_run('ejabberd', ['mam', 'enable'])

View File

@ -56,6 +56,8 @@ class FirewallApp(app_module.App):
app_id = 'firewall'
can_be_disabled = False
def __init__(self):
"""Create components for the app."""
super().__init__()

View File

@ -54,12 +54,10 @@ def create(request):
else:
form = IkiwikiCreateForm(prefix='ikiwiki')
return TemplateResponse(
request, 'ikiwiki_create.html', {
'title': ikiwiki.app.info.name,
'form': form,
'is_enabled': ikiwiki.app.is_enabled(),
})
return TemplateResponse(request, 'ikiwiki_create.html', {
'title': ikiwiki.app.info.name,
'form': form
})
def _create_wiki(request, name, admin_name, admin_password):

View File

@ -34,6 +34,8 @@ class JSXCApp(app_module.App):
app_id = 'jsxc'
can_be_disabled = False
def __init__(self):
"""Create components for the app."""
super().__init__()

View File

@ -1,9 +0,0 @@
{% extends "app.html" %}
{% comment %}
# SPDX-License-Identifier: AGPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block configuration %}
{% endblock %}

View File

@ -6,9 +6,11 @@ URLs for the JSXC module
from django.conf.urls import url
from stronghold.decorators import public
from .views import JSXCAppView, JsxcView
from plinth.views import AppView
from .views import JsxcView
urlpatterns = [
url(r'^apps/jsxc/$', JSXCAppView.as_view(), name='index'),
url(r'^apps/jsxc/$', AppView.as_view(app_id='jsxc'), name='index'),
url(r'^apps/jsxc/jsxc/$', public(JsxcView.as_view()), name='jsxc')
]

View File

@ -6,13 +6,6 @@ Views for the JSXC module
from django.views.generic import TemplateView
from plinth.modules import config
from plinth.views import AppView
class JSXCAppView(AppView):
"""Show ejabberd as an app."""
app_id = 'jsxc'
template_name = 'jsxc.html'
class JsxcView(TemplateView):

View File

@ -6,10 +6,8 @@ Forms for the Matrix Synapse module.
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.forms import AppForm
class MatrixSynapseForm(AppForm):
class MatrixSynapseForm(forms.Form):
enable_public_registration = forms.BooleanField(
label=_('Enable Public Registration'), required=False, help_text=_(
'Enabling public registration means that anyone on the Internet '

View File

@ -74,21 +74,9 @@ class MatrixSynapseAppView(AppView):
"""Handle valid form submission."""
old_config = self.get_initial()
new_config = form.cleaned_data
app_same = old_config['is_enabled'] == new_config['is_enabled']
pubreg_same = old_config['enable_public_registration'] == \
new_config['enable_public_registration']
if app_same and pubreg_same:
# TODO: find a more reliable/official way to check whether the
# request has messages attached.
if not self.request._messages._queued_messages:
messages.info(self.request, _('Setting unchanged'))
elif not app_same:
if new_config['is_enabled']:
self.app.enable()
else:
self.app.disable()
if not pubreg_same:
# note action public-registration restarts, if running now
if new_config['enable_public_registration']:

View File

@ -8,8 +8,6 @@ import pathlib
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.forms import AppForm
def get_skins():
"""Return a list of available skins as choice field values."""
@ -21,7 +19,7 @@ def get_skins():
if skin.is_dir()]
class MediaWikiForm(AppForm): # pylint: disable=W0232
class MediaWikiForm(forms.Form): # pylint: disable=W0232
"""MediaWiki configuration form."""
password = forms.CharField(
label=_('Administrator Password'), help_text=_(

View File

@ -42,27 +42,12 @@ class MediaWikiAppView(views.AppView):
def is_unchanged(key):
return old_config[key] == new_config[key]
app_same = is_unchanged('is_enabled')
pub_reg_same = is_unchanged('enable_public_registrations')
private_mode_same = is_unchanged('enable_private_mode')
default_skin_same = is_unchanged('default_skin')
if new_config['password']:
actions.superuser_run('mediawiki', ['change-password'],
input=new_config['password'].encode())
messages.success(self.request, _('Password updated'))
if (app_same and pub_reg_same and private_mode_same
and default_skin_same):
if not self.request._messages._queued_messages:
messages.info(self.request, _('Setting unchanged'))
elif not app_same:
if new_config['is_enabled']:
self.app.enable()
else:
self.app.disable()
if not pub_reg_same:
if not is_unchanged('enable_public_registrations'):
# note action public-registration restarts, if running now
if new_config['enable_public_registrations']:
if not new_config['enable_private_mode']:
@ -80,7 +65,7 @@ class MediaWikiAppView(views.AppView):
messages.success(self.request,
_('Public registrations disabled'))
if not private_mode_same:
if not is_unchanged('enable_private_mode'):
if new_config['enable_private_mode']:
actions.superuser_run('mediawiki', ['private-mode', 'enable'])
messages.success(self.request, _('Private mode enabled'))
@ -95,7 +80,7 @@ class MediaWikiAppView(views.AppView):
shortcut = mediawiki.app.get_component('shortcut-mediawiki')
shortcut.login_required = new_config['enable_private_mode']
if not default_skin_same:
if not is_unchanged('default_skin'):
actions.superuser_run(
'mediawiki', ['set-default-skin', new_config['default_skin']])
messages.success(self.request, _('Default skin changed'))

View File

@ -6,10 +6,8 @@ Forms for minetest module.
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.forms import AppForm
class MinetestForm(AppForm):
class MinetestForm(forms.Form):
"""Minetest configuration form"""
max_players = forms.IntegerField(
label=_('Maximum number of players'), required=True, min_value=1,

View File

@ -6,10 +6,8 @@ FreedomBox configuration form for MiniDLNA server.
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.forms import AppForm
class MiniDLNAServerForm(AppForm):
class MiniDLNAServerForm(forms.Form):
"""MiniDLNA server configuration form."""
media_dir = forms.CharField(
label=_('Media Files Directory'),

View File

@ -6,10 +6,8 @@ Mumble server configuration form
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.forms import AppForm
class MumbleForm(AppForm):
class MumbleForm(forms.Form):
"""Mumble server configuration"""
super_user_password = forms.CharField(
max_length=20,
@ -17,7 +15,6 @@ class MumbleForm(AppForm):
widget=forms.PasswordInput,
help_text=_(
'Optional. Leave this field blank to keep the current password. '
'SuperUser password can be used to manage permissions in Mumble.'
),
'SuperUser password can be used to manage permissions in Mumble.'),
required=False,
)

View File

@ -10,7 +10,6 @@ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from plinth.errors import ActionError
from plinth.forms import AppForm
from . import utils
@ -41,7 +40,7 @@ class SubdomainWidget(forms.widgets.TextInput):
</div>""".format(inputfield, self.domain)
class ConfigurationForm(AppForm):
class ConfigurationForm(forms.Form):
"""Configure PageKite credentials and frontend"""
server_domain = forms.CharField(

View File

@ -6,7 +6,6 @@ Forms for Quassel app.
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.forms import AppForm
from plinth.modules import quassel
@ -15,7 +14,7 @@ def get_domain_choices():
return ((domain, domain) for domain in quassel.get_available_domains())
class QuasselForm(AppForm):
class QuasselForm(forms.Form):
"""Form to select a TLS domain for Quassel."""
domain = forms.ChoiceField(

View File

@ -1,5 +1,8 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
from django.contrib import messages
from django.utils.translation import ugettext_lazy as _
from plinth.modules import quassel
from plinth.views import AppView
@ -20,11 +23,10 @@ class QuasselAppView(AppView):
def form_valid(self, form):
"""Change the access control of Radicale service."""
data = form.cleaned_data
app_disable = form.initial['is_enabled'] and not data['is_enabled']
if not app_disable and quassel.get_domain() != data['domain']:
if quassel.get_domain() != data['domain']:
quassel.set_domain(data['domain'])
quassel.app.get_component(
'letsencrypt-quassel').setup_certificates()
messages.success(self.request, _('Configuration updated'))
return super().form_valid(form)

View File

@ -7,7 +7,6 @@ from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth import cfg
from plinth.forms import AppForm
from plinth.utils import format_lazy
CHOICES = [
@ -26,7 +25,7 @@ CHOICES = [
]
class RadicaleForm(AppForm):
class RadicaleForm(forms.Form):
"""Specialized configuration form for radicale service."""
access_rights = forms.ChoiceField(choices=CHOICES, required=True,
widget=forms.RadioSelect())

View File

@ -6,10 +6,8 @@ Django form for configuring Searx.
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.forms import AppForm
class SearxForm(AppForm):
class SearxForm(forms.Form):
"""Searx configuration form."""
safe_search = forms.ChoiceField(
label=_('Safe Search'), help_text=_(

View File

@ -6,7 +6,6 @@ FreedomBox app for configuring Shadowsocks.
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.forms import AppForm
from plinth.utils import format_lazy
METHODS = [('chacha20-ietf-poly1305',
@ -32,7 +31,7 @@ class TrimmedCharField(forms.CharField):
return super(TrimmedCharField, self).clean(value)
class ShadowsocksForm(AppForm):
class ShadowsocksForm(forms.Form):
"""Shadowsocks configuration form"""
server = TrimmedCharField(label=_('Server'),
help_text=_('Server hostname or IP address'))

View File

@ -6,10 +6,8 @@ FreedomBox configuration form for OpenSSH server.
from django import forms
from django.utils.translation import ugettext_lazy as _
from plinth.forms import AppForm
class SSHServerForm(AppForm):
class SSHServerForm(forms.Form):
"""SSH server configuration form."""
password_auth_disabled = forms.BooleanField(
label=_('Disable password authentication'),

View File

@ -45,6 +45,8 @@ class StorageApp(app_module.App):
app_id = 'storage'
can_be_disabled = False
def __init__(self):
"""Create components for the app."""
super().__init__()

View File

@ -11,7 +11,6 @@ from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from plinth import actions, module_loader
from plinth.forms import AppForm
from plinth.modules import storage
@ -78,8 +77,8 @@ class DirectoryValidator:
if 'ValidationError' in output:
error_nr = int(output.strip().split()[1])
if error_nr == 1:
raise ValidationError(
_('Directory does not exist.'), 'invalid')
raise ValidationError(_('Directory does not exist.'),
'invalid')
elif error_nr == 2:
raise ValidationError(_('Path is not a directory.'), 'invalid')
elif error_nr == 3:
@ -90,12 +89,12 @@ class DirectoryValidator:
_('Directory is not writable by the user.'), 'invalid')
class DirectorySelectForm(AppForm):
class DirectorySelectForm(forms.Form):
"""Directory selection form."""
storage_dir = forms.ChoiceField(choices=[], label=_('Directory'),
required=True)
storage_subdir = forms.CharField(
label=_('Subdirectory (optional)'), required=False)
storage_subdir = forms.CharField(label=_('Subdirectory (optional)'),
required=False)
def __init__(self, title=None, default='/', validator=DirectoryValidator,
*args, **kwargs):
@ -108,16 +107,15 @@ class DirectorySelectForm(AppForm):
def clean(self):
"""Clean and validate form data."""
if self.cleaned_data['is_enabled'] or not self.initial['is_enabled']:
storage_dir = self.cleaned_data['storage_dir']
storage_subdir = self.cleaned_data['storage_subdir']
if storage_dir != '/':
storage_subdir = storage_subdir.lstrip('/')
storage_path = os.path.realpath(
os.path.join(storage_dir, storage_subdir))
if self.validator:
self.validator(storage_path)
self.cleaned_data.update({'storage_path': storage_path})
storage_dir = self.cleaned_data['storage_dir']
storage_subdir = self.cleaned_data['storage_subdir']
if storage_dir != '/':
storage_subdir = storage_subdir.lstrip('/')
storage_path = os.path.realpath(
os.path.join(storage_dir, storage_subdir))
if self.validator:
self.validator(storage_path)
self.cleaned_data.update({'storage_path': storage_path})
def get_initial(self, choices):
"""Get initial form data."""

View File

@ -24,17 +24,7 @@
{% endblock %}
{% block configuration %}
<h3>{% trans "Configuration" %}</h3>
<form class="form form-configuration" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary"
value="{% trans "Update setup" %}"/>
</form>
<br/>
{{ block.super }}
<h4>{% trans "Local introducer" %}</h4>
<table class="table table-bordered local-introducers">

View File

@ -37,16 +37,13 @@ class TransmissionAppView(views.AppView):
"""Apply the changes submitted in the form."""
old_status = form.initial
new_status = form.cleaned_data
if old_status['storage_path'] != new_status['storage_path']:
new_configuration = {
'download-dir': new_status['storage_path'],
}
if new_status['is_enabled'] or not old_status['is_enabled']:
if old_status['storage_path'] != new_status['storage_path']:
new_configuration = {
'download-dir': new_status['storage_path'],
}
actions.superuser_run(
'transmission', ['merge-configuration'],
input=json.dumps(new_configuration).encode())
messages.success(self.request, _('Configuration updated'))
actions.superuser_run('transmission', ['merge-configuration'],
input=json.dumps(new_configuration).encode())
messages.success(self.request, _('Configuration updated'))
return super().form_valid(form)

View File

@ -31,6 +31,8 @@ class UpgradesApp(app_module.App):
app_id = 'upgrades'
can_be_disabled = False
def __init__(self):
"""Create components for the app."""
super().__init__()

View File

@ -7,7 +7,6 @@ from django.contrib import messages
from django.template.response import TemplateResponse
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic.edit import FormView
from plinth import actions
from plinth.errors import ActionError
@ -52,10 +51,8 @@ class UpgradesConfigurationView(AppView):
else:
messages.success(self.request,
_('Automatic upgrades disabled'))
else:
messages.info(self.request, _('Settings unchanged'))
return FormView.form_valid(self, form)
return super().form_valid(form)
def is_package_manager_busy():

View File

@ -56,6 +56,8 @@ class UsersApp(app_module.App):
app_id = 'users'
can_be_disabled = False
def __init__(self):
"""Create components for the app."""
super().__init__()

View File

@ -28,15 +28,17 @@
{% endif %}
</div>
{% endblock %}
<div id='app-toggle-container' class="app-toggle-container">
{% if is_enabled %}
<button id="app-toggle-button" value="False"
class="btn toggle-button toggle-button--toggled"></button>
{% else %}
<button id="app-toggle-button" value="True"
class="btn toggle-button"></button>
{% endif %}
</div>
{% if app_enable_disable_form %}
<form class="form form-app-enable-disable" method="post">
{% csrf_token %}
{{ app_enable_disable_form }}
<button name="app_enable_disable_button" type="submit"
class="btn toggle-button {{ is_enabled|yesno:'toggle-button--toggled,' }}">
</button>
</form>
{% endif %}
</section>
{% block description %}

View File

@ -42,16 +42,18 @@
{% endblock %}
{% block configuration %}
<h3>{% trans "Configuration" %}</h3>
<form id="app-form" class="form form-configuration" method="post">
{% csrf_token %}
{% if form %}
<h3>{% trans "Configuration" %}</h3>
{{ form|bootstrap }}
<form id="app-form" class="form form-configuration" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-primary"
value="{% trans "Update setup" %}"/>
</form>
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary"
value="{% trans "Update setup" %}"/>
</form>
{% endif %}
{% endblock %}
<script src="{% static 'theme/js/app.template.js' %}"></script>
{% endblock %}

View File

@ -7,7 +7,7 @@ import time
from django.contrib import messages
from django.core.exceptions import ImproperlyConfigured
from django.http import Http404, HttpResponseRedirect
from django.http import Http404, HttpResponseBadRequest, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.http import is_safe_url
@ -114,8 +114,8 @@ class AppView(FormView):
retrieve the App instance for the app that is needed for basic information
and operations such as enabling/disabling the app.
'form_class' is the Django form class that is used by this view. By default
the AppForm class is used.
'form_class' is the Django form class that is used by this view. It may be
None if the app does not have a configuration form. Default is None.
'template_name' is the template used to render this view. By default it is
app.html. It may be overridden with a template that derives from app.html
@ -127,7 +127,7 @@ class AppView(FormView):
forward ports on their router for this app to work properly.
"""
form_class = forms.AppForm
form_class = None
app_id = None
template_name = 'app.html'
port_forwarding_info = None
@ -137,6 +137,17 @@ class AppView(FormView):
super().__init__(*args, **kwargs)
self._common_status = None
def post(self, request, *args, **kwargs):
"""Handle app enable/disable button separately."""
if 'app_enable_disable_button' not in request.POST:
return super().post(request, *args, **kwargs)
form = forms.AppEnableDisableForm(data=self.request.POST)
if form.is_valid():
return self.enable_disable_form_valid(form)
return HttpResponseBadRequest('Invalid form submission')
@property
def success_url(self):
return self.request.path
@ -149,6 +160,17 @@ class AppView(FormView):
return app.App.get(self.app_id)
def get_form(self, *args, **kwargs):
"""Return an instance of this view's form.
Also the form_class for this view to be None.
"""
if not self.form_class:
return None
return super().get_form(*args, **kwargs)
def _get_common_status(self):
"""Return the status needed for form and template.
@ -170,24 +192,36 @@ class AppView(FormView):
def form_valid(self, form):
"""Enable/disable a service and set messages."""
old_status = form.initial
new_status = form.cleaned_data
if old_status['is_enabled'] == new_status['is_enabled']:
# TODO: find a more reliable/official way to check whether the
# request has messages attached.
if not self.request._messages._queued_messages:
messages.info(self.request, _('Setting unchanged'))
else:
if new_status['is_enabled']:
self.app.enable()
messages.success(self.request, _('Application enabled'))
else:
self.app.disable()
messages.success(self.request, _('Application disabled'))
if not self.request._messages._queued_messages:
messages.info(self.request, _('Setting unchanged'))
return super().form_valid(form)
def get_enable_disable_form(self):
"""Return an instance of the app enable/disable form.
If the app can't be disabled by the user, return None.
"""
if not self.app.can_be_disabled:
return None
initial = {
'should_enable': not self._get_common_status()['is_enabled']
}
return forms.AppEnableDisableForm(initial=initial)
def enable_disable_form_valid(self, form):
"""Form handling for enabling / disabling apps."""
should_enable = form.cleaned_data['should_enable']
if should_enable != self.app.is_enabled():
if should_enable:
self.app.enable()
else:
self.app.disable()
return HttpResponseRedirect(self.request.path)
def get_context_data(self, *args, **kwargs):
"""Add service to the context data."""
context = super().get_context_data(*args, **kwargs)
@ -197,6 +231,7 @@ class AppView(FormView):
context['app_info'] = self.app.info
context['has_diagnostics'] = self.app.has_diagnostics()
context['port_forwarding_info'] = self.port_forwarding_info
context['app_enable_disable_form'] = self.get_enable_disable_form()
from plinth.modules.firewall.components import Firewall
context['firewall'] = self.app.get_components_of_type(Firewall)

View File

@ -571,10 +571,6 @@ a.menu_link_active {
justify-content: space-between;
}
.app-titles .app-toggle-container {
margin: auto 0;
}
.app-titles h3 {
margin-top: -0.5rem;
margin-bottom: 1.6rem;
@ -583,10 +579,8 @@ a.menu_link_active {
font-style: italic;
}
.app-toggle-container {
display: none;
flex-flow: row;
justify-content: flex-end;
.form-app-enable-disable {
margin: auto 0;
}
@media screen and (max-width: 767px) {
@ -612,7 +606,7 @@ a.menu_link_active {
margin: 0 0 15px 0;
}
.app-titles .app-toggle-container {
.app-titles .form-app-enable-disable {
margin: 30px auto;
}

View File

@ -1,57 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
/**
* @licstart The following is the entire license notice for the JavaScript
* code in this page.
*
* This file is part of FreedomBox.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @licend The above is the entire license notice for the JavaScript code
* in this page.
*/
var appForm = document.querySelector('#app-form');
var appToggleContainer = document.querySelector('#app-toggle-container');
var appToggleButton = document.querySelector('#app-toggle-button');
var appToggleInput = document.querySelector('#app-toggle-input');
if (appForm && appToggleButton && appToggleInput && appToggleContainer) {
var onSubmit = (e) => {
e.preventDefault;
appToggleInput.checked = !appToggleInput.checked;
appForm.submit();
};
appToggleButton.addEventListener('click', onSubmit);
/**
* if javascript is enabled, this script will run and show the toggle button
*/
appToggleInput.parentElement.style.display = 'none';
appToggleContainer.style.display = 'flex';
/* A basic form has only three elements:
* 1. An input tag with CSRF token
* 2. A div with form elements
* 3. A Submit button
*
* This kind of form can be completely hidden.
*/
if (appForm.children.length === 3) {
appForm.style.display = 'none';
appForm.previousElementSibling.style.display = 'none';
}
}