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" %}
-
-
-{% 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" %}
-
-
-
+ {{ 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 %}
-
-
+ {{ 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';
- }
-}