diff --git a/doc/dev/reference/forms.rst b/doc/dev/reference/forms.rst index 914438e59..bb3a8ccd4 100644 --- a/doc/dev/reference/forms.rst +++ b/doc/dev/reference/forms.rst @@ -3,9 +3,6 @@ Forms ----- -.. autoclass:: plinth.forms.AppForm - :members: - .. autoclass:: plinth.forms.DomainSelectionForm :members: diff --git a/doc/dev/tutorial/customizing.rst b/doc/dev/tutorial/customizing.rst index a7738f72c..e68fa4f08 100644 --- a/doc/dev/tutorial/customizing.rst +++ b/doc/dev/tutorial/customizing.rst @@ -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', diff --git a/functional_tests/support/application.py b/functional_tests/support/application.py index 33553b8e8..c6e559d56 100644 --- a/functional_tests/support/application.py +++ b/functional_tests/support/application.py @@ -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: diff --git a/plinth/app.py b/plinth/app.py index 2f762b056..27dd2e5e3 100644 --- a/plinth/app.py +++ b/plinth/app.py @@ -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): diff --git a/plinth/forms.py b/plinth/forms.py index 8fbda5983..f487b1158 100644 --- a/plinth/forms.py +++ b/plinth/forms.py @@ -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): diff --git a/plinth/modules/bind/forms.py b/plinth/modules/bind/forms.py index 75dc51151..10f127789 100644 --- a/plinth/modules/bind/forms.py +++ b/plinth/modules/bind/forms.py @@ -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], diff --git a/plinth/modules/config/__init__.py b/plinth/modules/config/__init__.py index 015eff837..0ad1bbf06 100644 --- a/plinth/modules/config/__init__.py +++ b/plinth/modules/config/__init__.py @@ -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__() diff --git a/plinth/modules/coquelicot/forms.py b/plinth/modules/coquelicot/forms.py index 6b4b7d96a..b418b844c 100644 --- a/plinth/modules/coquelicot/forms.py +++ b/plinth/modules/coquelicot/forms.py @@ -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'), diff --git a/plinth/modules/datetime/forms.py b/plinth/modules/datetime/forms.py index 6602d1664..092f3e19a 100644 --- a/plinth/modules/datetime/forms.py +++ b/plinth/modules/datetime/forms.py @@ -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'), diff --git a/plinth/modules/deluge/views.py b/plinth/modules/deluge/views.py index 4547d6ab7..4a15c9f82 100644 --- a/plinth/modules/deluge/views.py +++ b/plinth/modules/deluge/views.py @@ -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) diff --git a/plinth/modules/diaspora/forms.py b/plinth/modules/diaspora/forms.py index 153fb5cf1..15947a22b 100644 --- a/plinth/modules/diaspora/forms.py +++ b/plinth/modules/diaspora/forms.py @@ -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) diff --git a/plinth/modules/diaspora/views.py b/plinth/modules/diaspora/views.py index 1635b5f28..6d05bcd79 100644 --- a/plinth/modules/diaspora/views.py +++ b/plinth/modules/diaspora/views.py @@ -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')) diff --git a/plinth/modules/ejabberd/forms.py b/plinth/modules/ejabberd/forms.py index 98c052363..d5faea6e1 100644 --- a/plinth/modules/ejabberd/forms.py +++ b/plinth/modules/ejabberd/forms.py @@ -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, diff --git a/plinth/modules/ejabberd/templates/ejabberd.html b/plinth/modules/ejabberd/templates/ejabberd.html index eb81e9774..fcf3fba51 100644 --- a/plinth/modules/ejabberd/templates/ejabberd.html +++ b/plinth/modules/ejabberd/templates/ejabberd.html @@ -28,18 +28,3 @@

{% endblock %} - - -{% block configuration %} - -

{% trans "Configuration" %}

- -
- {% csrf_token %} - - {{ form|bootstrap }} - - -
-{% endblock %} diff --git a/plinth/modules/ejabberd/views.py b/plinth/modules/ejabberd/views.py index bcfae851c..3714ae744 100644 --- a/plinth/modules/ejabberd/views.py +++ b/plinth/modules/ejabberd/views.py @@ -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']) diff --git a/plinth/modules/firewall/__init__.py b/plinth/modules/firewall/__init__.py index 391f03ee2..4617b12ab 100644 --- a/plinth/modules/firewall/__init__.py +++ b/plinth/modules/firewall/__init__.py @@ -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__() diff --git a/plinth/modules/ikiwiki/views.py b/plinth/modules/ikiwiki/views.py index 82b02747e..8af12ea26 100644 --- a/plinth/modules/ikiwiki/views.py +++ b/plinth/modules/ikiwiki/views.py @@ -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): diff --git a/plinth/modules/jsxc/__init__.py b/plinth/modules/jsxc/__init__.py index e58bddb40..a8f15102c 100644 --- a/plinth/modules/jsxc/__init__.py +++ b/plinth/modules/jsxc/__init__.py @@ -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__() diff --git a/plinth/modules/jsxc/templates/jsxc.html b/plinth/modules/jsxc/templates/jsxc.html deleted file mode 100644 index c7f20d0fc..000000000 --- a/plinth/modules/jsxc/templates/jsxc.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "app.html" %} -{% comment %} -# SPDX-License-Identifier: AGPL-3.0-or-later -{% endcomment %} - -{% load i18n %} - -{% block configuration %} -{% endblock %} diff --git a/plinth/modules/jsxc/urls.py b/plinth/modules/jsxc/urls.py index 229a9a65f..97bdd3fe6 100644 --- a/plinth/modules/jsxc/urls.py +++ b/plinth/modules/jsxc/urls.py @@ -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') ] diff --git a/plinth/modules/jsxc/views.py b/plinth/modules/jsxc/views.py index b025b9263..547ce1f61 100644 --- a/plinth/modules/jsxc/views.py +++ b/plinth/modules/jsxc/views.py @@ -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): diff --git a/plinth/modules/matrixsynapse/forms.py b/plinth/modules/matrixsynapse/forms.py index 8ae05b01d..67ae1b47e 100644 --- a/plinth/modules/matrixsynapse/forms.py +++ b/plinth/modules/matrixsynapse/forms.py @@ -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 ' diff --git a/plinth/modules/matrixsynapse/views.py b/plinth/modules/matrixsynapse/views.py index 4fab1ebad..c1a6e4a62 100644 --- a/plinth/modules/matrixsynapse/views.py +++ b/plinth/modules/matrixsynapse/views.py @@ -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']: diff --git a/plinth/modules/mediawiki/forms.py b/plinth/modules/mediawiki/forms.py index c18820e87..9c4823998 100644 --- a/plinth/modules/mediawiki/forms.py +++ b/plinth/modules/mediawiki/forms.py @@ -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=_( diff --git a/plinth/modules/mediawiki/views.py b/plinth/modules/mediawiki/views.py index 689824944..0abacb969 100644 --- a/plinth/modules/mediawiki/views.py +++ b/plinth/modules/mediawiki/views.py @@ -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')) diff --git a/plinth/modules/minetest/forms.py b/plinth/modules/minetest/forms.py index 62914fbf5..b89c893c3 100644 --- a/plinth/modules/minetest/forms.py +++ b/plinth/modules/minetest/forms.py @@ -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, diff --git a/plinth/modules/minidlna/forms.py b/plinth/modules/minidlna/forms.py index cbadd8ce5..cc893874f 100644 --- a/plinth/modules/minidlna/forms.py +++ b/plinth/modules/minidlna/forms.py @@ -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'), diff --git a/plinth/modules/mumble/forms.py b/plinth/modules/mumble/forms.py index ea32dc6df..fa05b25b5 100644 --- a/plinth/modules/mumble/forms.py +++ b/plinth/modules/mumble/forms.py @@ -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, ) diff --git a/plinth/modules/pagekite/forms.py b/plinth/modules/pagekite/forms.py index 534f85d1c..caaae4c12 100644 --- a/plinth/modules/pagekite/forms.py +++ b/plinth/modules/pagekite/forms.py @@ -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): """.format(inputfield, self.domain) -class ConfigurationForm(AppForm): +class ConfigurationForm(forms.Form): """Configure PageKite credentials and frontend""" server_domain = forms.CharField( diff --git a/plinth/modules/quassel/forms.py b/plinth/modules/quassel/forms.py index 76c75b357..f1a9ce436 100644 --- a/plinth/modules/quassel/forms.py +++ b/plinth/modules/quassel/forms.py @@ -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( diff --git a/plinth/modules/quassel/views.py b/plinth/modules/quassel/views.py index cad5ed3de..f8266b44c 100644 --- a/plinth/modules/quassel/views.py +++ b/plinth/modules/quassel/views.py @@ -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) diff --git a/plinth/modules/radicale/forms.py b/plinth/modules/radicale/forms.py index 160c09375..6536b9e92 100644 --- a/plinth/modules/radicale/forms.py +++ b/plinth/modules/radicale/forms.py @@ -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()) diff --git a/plinth/modules/searx/forms.py b/plinth/modules/searx/forms.py index e71784e0f..87e2426f7 100644 --- a/plinth/modules/searx/forms.py +++ b/plinth/modules/searx/forms.py @@ -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=_( diff --git a/plinth/modules/shadowsocks/forms.py b/plinth/modules/shadowsocks/forms.py index 7726f99e6..593ae647f 100644 --- a/plinth/modules/shadowsocks/forms.py +++ b/plinth/modules/shadowsocks/forms.py @@ -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')) diff --git a/plinth/modules/ssh/forms.py b/plinth/modules/ssh/forms.py index 0e42d4491..dd6e1ccb2 100644 --- a/plinth/modules/ssh/forms.py +++ b/plinth/modules/ssh/forms.py @@ -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'), diff --git a/plinth/modules/storage/__init__.py b/plinth/modules/storage/__init__.py index a5e759107..7b6fc7651 100644 --- a/plinth/modules/storage/__init__.py +++ b/plinth/modules/storage/__init__.py @@ -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__() diff --git a/plinth/modules/storage/forms.py b/plinth/modules/storage/forms.py index d871dd3b5..b825bc4b1 100644 --- a/plinth/modules/storage/forms.py +++ b/plinth/modules/storage/forms.py @@ -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.""" diff --git a/plinth/modules/tahoe/templates/tahoe-post-setup.html b/plinth/modules/tahoe/templates/tahoe-post-setup.html index ecfa1e146..5ad56edf6 100644 --- a/plinth/modules/tahoe/templates/tahoe-post-setup.html +++ b/plinth/modules/tahoe/templates/tahoe-post-setup.html @@ -24,17 +24,7 @@ {% endblock %} {% block configuration %} -

{% trans "Configuration" %}

- -
- {% csrf_token %} - - {{ form|bootstrap }} - - -
-
+ {{ block.super }}

{% trans "Local introducer" %}

diff --git a/plinth/modules/transmission/views.py b/plinth/modules/transmission/views.py index 84319ecfb..d0372be2a 100644 --- a/plinth/modules/transmission/views.py +++ b/plinth/modules/transmission/views.py @@ -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) diff --git a/plinth/modules/upgrades/__init__.py b/plinth/modules/upgrades/__init__.py index de1f794a6..989bba4f5 100644 --- a/plinth/modules/upgrades/__init__.py +++ b/plinth/modules/upgrades/__init__.py @@ -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__() diff --git a/plinth/modules/upgrades/views.py b/plinth/modules/upgrades/views.py index 08d9786c8..1da6c734d 100644 --- a/plinth/modules/upgrades/views.py +++ b/plinth/modules/upgrades/views.py @@ -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(): diff --git a/plinth/modules/users/__init__.py b/plinth/modules/users/__init__.py index e8d3e8e99..84e702a31 100644 --- a/plinth/modules/users/__init__.py +++ b/plinth/modules/users/__init__.py @@ -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__() diff --git a/plinth/templates/app-header.html b/plinth/templates/app-header.html index d071abe72..d98c9a630 100644 --- a/plinth/templates/app-header.html +++ b/plinth/templates/app-header.html @@ -28,15 +28,17 @@ {% endif %} {% endblock %} -
- {% if is_enabled %} - - {% else %} - - {% endif %} -
+ {% if app_enable_disable_form %} + + {% csrf_token %} + + {{ app_enable_disable_form }} + + + + {% endif %} {% block description %} diff --git a/plinth/templates/app.html b/plinth/templates/app.html index 4eac53460..21eb4d50d 100644 --- a/plinth/templates/app.html +++ b/plinth/templates/app.html @@ -42,16 +42,18 @@ {% endblock %} {% block configuration %} -

{% trans "Configuration" %}

- - {% csrf_token %} + {% if form %} +

{% trans "Configuration" %}

- {{ form|bootstrap }} + + {% csrf_token %} - - + {{ form|bootstrap }} + + + + {% endif %} {% endblock %} - {% endblock %} diff --git a/plinth/views.py b/plinth/views.py index fb6c3ccdd..8e4fa5b70 100644 --- a/plinth/views.py +++ b/plinth/views.py @@ -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) diff --git a/static/themes/default/css/main.css b/static/themes/default/css/main.css index f3da16ee2..40137df88 100644 --- a/static/themes/default/css/main.css +++ b/static/themes/default/css/main.css @@ -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; } diff --git a/static/themes/default/js/app.template.js b/static/themes/default/js/app.template.js deleted file mode 100644 index fdfe85e37..000000000 --- a/static/themes/default/js/app.template.js +++ /dev/null @@ -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 . - * - * @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'; - } -}