mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-28 08:03:36 +00:00
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:
parent
bd8aee17d5
commit
0b5b384651
@ -3,9 +3,6 @@
|
||||
Forms
|
||||
-----
|
||||
|
||||
.. autoclass:: plinth.forms.AppForm
|
||||
:members:
|
||||
|
||||
.. autoclass:: plinth.forms.DomainSelectionForm
|
||||
:members:
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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__()
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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'))
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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'])
|
||||
|
||||
@ -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__()
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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__()
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
{% extends "app.html" %}
|
||||
{% comment %}
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block configuration %}
|
||||
{% endblock %}
|
||||
@ -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')
|
||||
]
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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 '
|
||||
|
||||
@ -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']:
|
||||
|
||||
@ -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=_(
|
||||
|
||||
@ -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'))
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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=_(
|
||||
|
||||
@ -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'))
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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__()
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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__()
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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__()
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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';
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user