diff --git a/plinth/__main__.py b/plinth/__main__.py index 37e62ea4b..6b463d272 100644 --- a/plinth/__main__.py +++ b/plinth/__main__.py @@ -33,6 +33,7 @@ from cherrypy.process.plugins import Daemonizer from plinth import cfg from plinth import module_loader from plinth import service +from plinth import setup logger = logging.getLogger(__name__) @@ -57,6 +58,9 @@ def parse_arguments(): parser.add_argument( '--no-daemon', action='store_true', default=cfg.no_daemon, help='do not start as a daemon') + parser.add_argument( + '--setup', action='store_true', default=False, + help='run setup tasks on all essential modules and exit') parser.add_argument( '--diagnose', action='store_true', default=False, help='run diagnostic tests and exit') @@ -132,9 +136,7 @@ def setup_server(): cherrypy.tree.mount(None, manual_url, config) logger.debug('Serving manual images %s on %s', manual_dir, manual_url) - for module_import_path in module_loader.loaded_modules: - module = importlib.import_module(module_import_path) - module_name = module_import_path.split('.')[-1] + for module_name, module in module_loader.loaded_modules.items(): module_path = os.path.dirname(module.__file__) static_dir = os.path.join(module_path, 'static') if not os.path.isdir(static_dir): @@ -258,6 +260,7 @@ def configure_django(): 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'stronghold.middleware.LoginRequiredMiddleware', 'plinth.modules.first_boot.middleware.FirstBootMiddleware', + 'plinth.middleware.SetupMiddleware', ), ROOT_URLCONF='plinth.urls', SECURE_PROXY_SSL_HEADER=secure_proxy_ssl_header, @@ -278,6 +281,18 @@ def configure_django(): os.chmod(cfg.store_file, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) +def run_setup_and_exit(): + """Run setup on all essential modules and exit.""" + error_code = 0 + try: + setup.setup_all_modules(essential=True) + except Exception as exception: + logger.error('Error running setup - %s', exception) + error_code = 1 + + sys.exit(error_code) + + def run_diagnostics_and_exit(): """Run diagostics on all modules and exit.""" module = importlib.import_module('plinth.modules.diagnostics.diagnostics') @@ -315,6 +330,9 @@ def main(): module_loader.load_modules() + if arguments.setup: + run_setup_and_exit() + if arguments.diagnose: run_diagnostics_and_exit() diff --git a/plinth/middleware.py b/plinth/middleware.py new file mode 100644 index 000000000..b4ad0f0b4 --- /dev/null +++ b/plinth/middleware.py @@ -0,0 +1,75 @@ +# +# This file is part of Plinth. +# +# 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 . +# + +""" +Django middleware to show pre-setup message and setup progress. +""" + +from django.contrib import messages +from django.core.urlresolvers import resolve +from django.utils.translation import ugettext_lazy as _ +import logging + +import plinth +from plinth.package import PackageException +from . import views + + +logger = logging.getLogger(__name__) + + +class SetupMiddleware(object): + """Show setup page or progress if setup is neccessary or running.""" + + @staticmethod + def process_request(request): + """Handle a request as Django middleware request handler.""" + # Perform a URL resolution. This is slightly inefficient as + # Django will do this resolution again. + resolver_match = resolve(request.path_info) + if not resolver_match.namespaces or not len(resolver_match.namespaces): + # Requested URL does not belong to any application + return + + module_name = resolver_match.namespaces[0] + module = plinth.module_loader.loaded_modules[module_name] + + # Collect errors from any previous operations and show them + if module.setup_helper.is_finished: + exception = module.setup_helper.collect_result() + if not exception: + messages.success(request, _('Application installed.')) + else: + if isinstance(exception, PackageException): + error_string = getattr(exception, 'error_string', + str(exception)) + error_details = getattr(exception, 'error_details', '') + message = _('Error installing application: {string} ' + '{details}').format( + string=error_string, details=error_details) + else: + message = _('Error installing application: {error}') \ + .format(error=exception) + + messages.error(request, message) + + # Check if application is up-to-date + if module.setup_helper.get_state() == 'up-to-date': + return + + view = views.SetupView.as_view() + return view(request, setup_helper=module.setup_helper) diff --git a/plinth/migrations/0002_modulestore.py b/plinth/migrations/0002_modulestore.py new file mode 100644 index 000000000..43b81c064 --- /dev/null +++ b/plinth/migrations/0002_modulestore.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-02-10 12:30 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plinth', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Module', + fields=[ + ('name', models.TextField(primary_key=True, serialize=False)), + ('setup_version', models.IntegerField()), + ], + ), + ] diff --git a/plinth/models.py b/plinth/models.py index 8dc1cfd03..bf2bf3350 100644 --- a/plinth/models.py +++ b/plinth/models.py @@ -37,3 +37,9 @@ class KVStore(models.Model): def value(self, val): """Store the value of the key/value pair by JSON encoding it""" self.value_json = json.dumps(val) + + +class Module(models.Model): + """Model to store current setup versions of a module.""" + name = models.TextField(primary_key=True) + setup_version = models.IntegerField() diff --git a/plinth/module_loader.py b/plinth/module_loader.py index e8cfd6dc9..272c1ae78 100644 --- a/plinth/module_loader.py +++ b/plinth/module_loader.py @@ -19,6 +19,7 @@ Discover, load and manage Plinth modules """ +import collections import django import importlib import logging @@ -27,11 +28,12 @@ import re from plinth import cfg from plinth import urls +from plinth import setup from plinth.signals import pre_module_loading, post_module_loading -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) -loaded_modules = [] +loaded_modules = collections.OrderedDict() _modules_to_load = None @@ -42,16 +44,18 @@ def load_modules(): """ pre_module_loading.send_robust(sender="module_loader") modules = {} - for module_name in get_modules_to_load(): - LOGGER.info('Importing %s', module_name) + for module_import_path in get_modules_to_load(): + logger.info('Importing %s', module_import_path) + module_name = module_import_path.split('.')[-1] try: - modules[module_name] = importlib.import_module(module_name) + modules[module_name] = importlib.import_module(module_import_path) except Exception as exception: - LOGGER.exception('Could not import %s: %s', module_name, exception) + logger.exception('Could not import %s: %s', module_import_path, + exception) if cfg.debug: raise - _include_module_urls(module_name) + _include_module_urls(module_import_path, module_name) ordered_modules = [] remaining_modules = dict(modules) # Make a copy @@ -64,14 +68,14 @@ def load_modules(): _insert_modules(module_name, module, remaining_modules, ordered_modules) except KeyError: - LOGGER.error('Unsatified dependency for module - %s', + logger.error('Unsatified dependency for module - %s', module_name) - LOGGER.debug('Module load order - %s', ordered_modules) + logger.debug('Module load order - %s', ordered_modules) for module_name in ordered_modules: - _initialize_module(modules[module_name]) - loaded_modules.append(module_name) + _initialize_module(module_name, modules[module_name]) + loaded_modules[module_name] = modules[module_name] post_module_loading.send_robust(sender="module_loader") @@ -94,7 +98,7 @@ def _insert_modules(module_name, module, remaining_modules, ordered_modules): try: module = remaining_modules.pop(dependency) except KeyError: - LOGGER.error('Not found or circular dependency - %s, %s', + logger.error('Not found or circular dependency - %s, %s', module_name, dependency) raise @@ -103,32 +107,34 @@ def _insert_modules(module_name, module, remaining_modules, ordered_modules): ordered_modules.append(module_name) -def _include_module_urls(module_name): +def _include_module_urls(module_import_path, module_name): """Include the module's URLs in global project URLs list""" - namespace = module_name.split('.')[-1] - url_module = module_name + '.urls' + url_module = module_import_path + '.urls' try: urls.urlpatterns += [ django.conf.urls.url( - r'', django.conf.urls.include(url_module, namespace))] + r'', django.conf.urls.include(url_module, module_name))] except ImportError: - LOGGER.debug('No URLs for %s', module_name) + logger.debug('No URLs for %s', module_name) if cfg.debug: raise -def _initialize_module(module): +def _initialize_module(module_name, module): """Call initialization method in the module if it exists""" + # Perform setup related initialization on the module + setup.init(module_name, module) + try: init = module.init except AttributeError: - LOGGER.debug('No init() for module - %s', module.__name__) + logger.debug('No init() for module - %s', module.__name__) return try: init() except Exception as exception: - LOGGER.exception('Exception while running init for %s: %s', + logger.exception('Exception while running init for %s: %s', module, exception) if cfg.debug: raise diff --git a/plinth/modules/apps/__init__.py b/plinth/modules/apps/__init__.py index 28d22ea0b..b7edfd994 100644 --- a/plinth/modules/apps/__init__.py +++ b/plinth/modules/apps/__init__.py @@ -23,3 +23,7 @@ from . import apps from .apps import init __all__ = ['apps', 'init'] + +version = 1 + +is_essential = 1 diff --git a/plinth/modules/avahi/__init__.py b/plinth/modules/avahi/__init__.py index 7c8f5592c..c4c0970bc 100644 --- a/plinth/modules/avahi/__init__.py +++ b/plinth/modules/avahi/__init__.py @@ -20,16 +20,32 @@ Plinth module for service discovery. """ from django.utils.translation import ugettext_lazy as _ -import subprocess -from plinth import actions from plinth import action_utils from plinth import cfg from plinth import service as service_module +from plinth.utils import format_lazy # pylint: disable=C0103 -depends = ['plinth.modules.system'] +version = 1 + +is_essential = True + +depends = ['system'] + +title = _('Service Discovery') + +description = [ + format_lazy( + _('Service discovery allows other devices on the network to ' + 'discover your {{ box_name }} and services running on it. It ' + 'also allows {{ box_name }} to discover other devices and ' + 'services running on your local network. Service discovery is ' + 'not essential and works only on internal networks. It may be ' + 'disabled to improve security especially when connecting to a ' + 'hostile local network.'), box_name=_(cfg.box_name)) +] service = None @@ -37,13 +53,16 @@ service = None def init(): """Intialize the service discovery module.""" menu = cfg.main_menu.get('system:index') - menu.add_urlname(_('Service Discovery'), 'glyphicon-lamp', - 'avahi:index', 950) + menu.add_urlname(title, 'glyphicon-lamp', 'avahi:index', 950) global service # pylint: disable=W0603 service = service_module.Service( - 'avahi', _('Service Discovery'), ['mdns'], - is_external=False, enabled=is_enabled()) + 'avahi', title, ['mdns'], is_external=False, enabled=is_enabled()) + + +def setup(helper, old_version=False): + """Install and configure the module.""" + helper.install(['avahi-daemon']) def is_enabled(): diff --git a/plinth/modules/avahi/templates/avahi.html b/plinth/modules/avahi/templates/avahi.html index c3d1b98d7..193a1ac07 100644 --- a/plinth/modules/avahi/templates/avahi.html +++ b/plinth/modules/avahi/templates/avahi.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,21 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{% trans "Service Discovery" %}

- -

- {% blocktrans trimmed %} - Service discovery allows other devices on the network to - discover your {{ box_name }} and services running on it. It - also allows {{ box_name }} to discover other devices and - services running on your local network. Service discovery is - not essential and works only on internal networks. It may be - disabled to improve security especially when connecting to a - hostile local network. - {% endblocktrans %} -

+{% block configuration %}

{% trans "Status" %}

diff --git a/plinth/modules/avahi/views.py b/plinth/modules/avahi/views.py index e8fa4b61c..77751abca 100644 --- a/plinth/modules/avahi/views.py +++ b/plinth/modules/avahi/views.py @@ -26,14 +26,12 @@ import logging from .forms import ServiceDiscoveryForm from plinth import actions -from plinth import package from plinth.modules import avahi logger = logging.getLogger(__name__) # pylint: disable=C0103 -@package.required(['avahi-daemon']) def index(request): """Serve configuration page.""" status = get_status() @@ -50,7 +48,8 @@ def index(request): form = ServiceDiscoveryForm(initial=status, prefix='avahi') return TemplateResponse(request, 'avahi.html', - {'title': _('Service Discovery'), + {'title': avahi.title, + 'description': avahi.description, 'status': status, 'form': form}) diff --git a/plinth/modules/config/__init__.py b/plinth/modules/config/__init__.py index 12cb341ff..c1bf7de04 100644 --- a/plinth/modules/config/__init__.py +++ b/plinth/modules/config/__init__.py @@ -24,6 +24,8 @@ from .config import init __all__ = ['config', 'init'] -depends = ['plinth.modules.system', - 'plinth.modules.firewall', - 'plinth.modules.names'] +version = 1 + +is_essential = True + +depends = ['system', 'firewall', 'names'] diff --git a/plinth/modules/config/config.py b/plinth/modules/config/config.py index f730f83d1..54c687c2b 100644 --- a/plinth/modules/config/config.py +++ b/plinth/modules/config/config.py @@ -33,7 +33,7 @@ import socket from plinth import actions from plinth import cfg -from plinth.modules.firewall import firewall +from plinth.modules import firewall from plinth.modules.names import SERVICES from plinth.signals import pre_hostname_change, post_hostname_change from plinth.signals import domainname_change diff --git a/plinth/modules/config/urls.py b/plinth/modules/config/urls.py index 35f392f31..846f6e19f 100644 --- a/plinth/modules/config/urls.py +++ b/plinth/modules/config/urls.py @@ -19,7 +19,7 @@ URLs for the Configuration module """ -from django.conf.urls import patterns, url +from django.conf.urls import url from . import config as views diff --git a/plinth/modules/datetime/__init__.py b/plinth/modules/datetime/__init__.py index 147b95146..0c200169a 100644 --- a/plinth/modules/datetime/__init__.py +++ b/plinth/modules/datetime/__init__.py @@ -22,13 +22,23 @@ Plinth module to configure system date and time from django.utils.translation import ugettext_lazy as _ import subprocess -from plinth import actions from plinth import action_utils from plinth import cfg from plinth import service as service_module -depends = ['plinth.modules.system'] +version = 1 + +is_essential = True + +depends = ['system'] + +title = _('Date & Time') + +description = [ + _('Network time server is a program that maintians the system time ' + 'in synchronization with servers on the Internet.') +] service = None @@ -36,13 +46,17 @@ service = None def init(): """Intialize the date/time module.""" menu = cfg.main_menu.get('system:index') - menu.add_urlname(_('Date & Time'), 'glyphicon-time', - 'datetime:index', 900) + menu.add_urlname(title, 'glyphicon-time', 'datetime:index', 900) global service service = service_module.Service( - 'ntp', _('Network Time Server'), - is_external=False, enabled=is_enabled()) + 'ntp', title, is_external=False, enabled=is_enabled()) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['ntp']) + helper.call('post', service.notify_enabled, None, True) def is_enabled(): diff --git a/plinth/modules/datetime/templates/datetime.html b/plinth/modules/datetime/templates/datetime.html index bce9499f2..d0a7aa407 100644 --- a/plinth/modules/datetime/templates/datetime.html +++ b/plinth/modules/datetime/templates/datetime.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,16 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{% trans "Date & Time" %}

- -

- {% blocktrans trimmed %} - Network time server is a program that maintians the system time - in synchronization with servers on the Internet. - {% endblocktrans %} -

+{% block configuration %}

{% trans "Status" %}

diff --git a/plinth/modules/datetime/views.py b/plinth/modules/datetime/views.py index 51ea7e57c..f687e1506 100644 --- a/plinth/modules/datetime/views.py +++ b/plinth/modules/datetime/views.py @@ -26,18 +26,11 @@ import logging from .forms import DateTimeForm from plinth import actions -from plinth import package from plinth.modules import datetime logger = logging.getLogger(__name__) -def on_install(): - """Notify that the service is now enabled.""" - datetime.service.notify_enabled(None, True) - - -@package.required(['ntp'], on_install=on_install) def index(request): """Serve configuration page.""" status = get_status() @@ -55,7 +48,8 @@ def index(request): form = DateTimeForm(initial=status, prefix='datetime') return TemplateResponse(request, 'datetime.html', - {'title': _('Date & Time'), + {'title': datetime.title, + 'description': datetime.description, 'status': status, 'form': form}) diff --git a/plinth/modules/deluge/__init__.py b/plinth/modules/deluge/__init__.py index 7038d3696..f5c1b6f37 100644 --- a/plinth/modules/deluge/__init__.py +++ b/plinth/modules/deluge/__init__.py @@ -27,7 +27,20 @@ from plinth import cfg from plinth import service as service_module -depends = ['plinth.modules.apps'] +version = 1 + +depends = ['apps'] + +title = _('BitTorrent Web Client (Deluge)') + +description = [ + _('Deluge is a BitTorrent client that features a Web UI.'), + + _('When enabled, the Deluge web client will be available from ' + '/deluge path on the web server. The ' + 'default password is \'deluge\', but you should log in and change ' + 'it immediately after enabling this service.') +] service = None @@ -35,13 +48,19 @@ service = None def init(): """Initialize the Deluge module.""" menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('BitTorrent (Deluge)'), 'glyphicon-magnet', - 'deluge:index', 200) + menu.add_urlname(title, 'glyphicon-magnet', 'deluge:index', 200) global service service = service_module.Service( - 'deluge', _('Deluge BitTorrent'), ['http', 'https'], - is_external=True, enabled=is_enabled()) + 'deluge', title, ['http', 'https'], is_external=True, + enabled=is_enabled()) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['deluged', 'deluge-web']) + helper.call('post', actions.superuser_run, 'deluge', ['enable']) + helper.call('post', service.notify_enabled, None, True) def is_enabled(): diff --git a/plinth/modules/deluge/templates/deluge.html b/plinth/modules/deluge/templates/deluge.html index 51b54c8ef..019c91b64 100644 --- a/plinth/modules/deluge/templates/deluge.html +++ b/plinth/modules/deluge/templates/deluge.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,20 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{% trans "BitTorrent Web Client (Deluge)" %}

- -

{% trans "Deluge is a BitTorrent client that features a Web UI." %}

- -

- {% blocktrans trimmed %} - When enabled, the Deluge web client will be available from - /deluge path on the web server. The - default password is 'deluge', but you should log in and change - it immediately after enabling this service. - {% endblocktrans %} -

+{% block configuration %}

{% trans "Status" %}

diff --git a/plinth/modules/deluge/views.py b/plinth/modules/deluge/views.py index 475cffb30..0b48b2333 100644 --- a/plinth/modules/deluge/views.py +++ b/plinth/modules/deluge/views.py @@ -25,17 +25,9 @@ from django.utils.translation import ugettext as _ from .forms import DelugeForm from plinth import actions -from plinth import package from plinth.modules import deluge -def on_install(): - """Tasks to run after package install.""" - actions.superuser_run('deluge', ['enable']) - deluge.service.notify_enabled(None, True) - - -@package.required(['deluged', 'deluge-web'], on_install=on_install) def index(request): """Serve configuration page.""" status = get_status() @@ -53,7 +45,8 @@ def index(request): form = DelugeForm(initial=status, prefix='deluge') return TemplateResponse(request, 'deluge.html', - {'title': _('BitTorrent (Deluge)'), + {'title': deluge.title, + 'description': deluge.description, 'status': status, 'form': form}) diff --git a/plinth/modules/diagnostics/__init__.py b/plinth/modules/diagnostics/__init__.py index 8005882b3..f532db3ee 100644 --- a/plinth/modules/diagnostics/__init__.py +++ b/plinth/modules/diagnostics/__init__.py @@ -19,13 +19,30 @@ Plinth module for system diagnostics """ -from . import diagnostics -from .diagnostics import init +from django.utils.translation import ugettext_lazy as _ + from plinth import action_utils +from plinth import cfg -__all__ = ['diagnostics', 'init'] +version = 1 -depends = ['plinth.modules.system'] +is_essential = True + +title = _('Diagnostics') + +description = [ + _('The system diagnostic test will run a number of checks on your ' + 'system to confirm that applications and services are working as ' + 'expected.') +] + +depends = ['system'] + + +def init(): + """Initialize the module""" + menu = cfg.main_menu.get('system:index') + menu.add_urlname(title, 'glyphicon-screenshot', 'diagnostics:index', 30) def diagnose(): diff --git a/plinth/modules/diagnostics/diagnostics.py b/plinth/modules/diagnostics/diagnostics.py index 19ab23e7d..a44539afe 100644 --- a/plinth/modules/diagnostics/diagnostics.py +++ b/plinth/modules/diagnostics/diagnostics.py @@ -24,12 +24,11 @@ from django.http import Http404 from django.template.response import TemplateResponse from django.views.decorators.http import require_POST from django.utils.translation import ugettext_lazy as _ -import importlib import logging import threading -from plinth import cfg from plinth import module_loader +from plinth.modules import diagnostics logger = logging.Logger(__name__) @@ -39,20 +38,14 @@ current_results = {} _running_task = None -def init(): - """Initialize the module""" - menu = cfg.main_menu.get('system:index') - menu.add_urlname(_('Diagnostics'), 'glyphicon-screenshot', - 'diagnostics:index', 30) - - def index(request): """Serve the index page""" if request.method == 'POST' and not _running_task: _start_task() return TemplateResponse(request, 'diagnostics.html', - {'title': _('System Diagnostics'), + {'title': diagnostics.title, + 'description': diagnostics.description, 'is_running': _running_task is not None, 'results': current_results}) @@ -60,19 +53,14 @@ def index(request): @require_POST def module(request, module_name): """Return diagnostics for a particular module.""" - found = False - for module_import_path in module_loader.loaded_modules: - if module_name == module_import_path.split('.')[-1]: - found = True - break - - if not found: + try: + module = module_loader.loaded_modules[module_name] + except KeyError: raise Http404('Module does not exist or not loaded') - loaded_module = importlib.import_module(module_import_path) results = [] - if hasattr(loaded_module, 'diagnose'): - results = loaded_module.diagnose() + if hasattr(module, 'diagnose'): + results = module.diagnose() return TemplateResponse(request, 'diagnostics_module.html', {'title': _('Diagnostic Test'), @@ -110,17 +98,15 @@ def run_on_all_modules(): 'progress_percentage': 0} modules = [] - for module_import_path in module_loader.loaded_modules: - loaded_module = importlib.import_module(module_import_path) - if not hasattr(loaded_module, 'diagnose'): + for module_name, module in module_loader.loaded_modules.items(): + if not hasattr(module, 'diagnose'): continue - module_name = module_import_path.split('.')[-1] - modules.append((module_name, loaded_module)) + modules.append((module_name, module)) current_results['results'][module_name] = None current_results['modules'] = modules - for current_index, (module_name, loaded_module) in enumerate(modules): - current_results['results'][module_name] = loaded_module.diagnose() + for current_index, (module_name, module) in enumerate(modules): + current_results['results'][module_name] = module.diagnose() current_results['progress_percentage'] = \ int((current_index + 1) * 100 / len(modules)) diff --git a/plinth/modules/diagnostics/templates/diagnostics.html b/plinth/modules/diagnostics/templates/diagnostics.html index 2b369732e..066222347 100644 --- a/plinth/modules/diagnostics/templates/diagnostics.html +++ b/plinth/modules/diagnostics/templates/diagnostics.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'app.html' %} {% comment %} # # This file is part of Plinth. @@ -29,17 +29,7 @@ {% endblock %} -{% block content %} - -

{{ title }}

- -

- {% blocktrans trimmed %} - The system diagnostic test will run a number of checks on your - system to confirm that applications and services are working as - expected. - {% endblocktrans %} -

+{% block configuration %} {% if not is_running %}
' + 'gnudip server. Afterwards the Server will assign your DNS name ' + 'with the new IP and if someone from the Internet asks for your DNS ' + 'name he will get your current IP answered.') +] + + +def init(): + """Initialize the module.""" + menu = cfg.main_menu.get('apps:index') + menu.add_urlname(title, 'glyphicon-refresh', 'dynamicdns:index', 500) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['ez-ipupdate']) diff --git a/plinth/modules/dynamicdns/dynamicdns.py b/plinth/modules/dynamicdns/dynamicdns.py index 490ec98c6..b15e49b59 100644 --- a/plinth/modules/dynamicdns/dynamicdns.py +++ b/plinth/modules/dynamicdns/dynamicdns.py @@ -25,7 +25,7 @@ import logging from plinth import actions from plinth import cfg -from plinth import package +from plinth.modules import dynamicdns from plinth.utils import format_lazy logger = logging.getLogger(__name__) @@ -39,19 +39,11 @@ subsubmenu = [{'url': reverse_lazy('dynamicdns:index'), 'text': ugettext_lazy('Status')}] -def init(): - """Initialize the dynamicdns module""" - menu = cfg.main_menu.get('apps:index') - menu.add_urlname(ugettext_lazy('Dynamic DNS'), 'glyphicon-refresh', - 'dynamicdns:index', 500) - - -@package.required(['ez-ipupdate']) def index(request): """Serve Dynamic DNS page.""" - return TemplateResponse(request, 'dynamicdns.html', - {'title': _('Dynamic DNS'), + {'title': dynamicdns.title, + 'description': dynamicdns.description, 'subsubmenu': subsubmenu}) @@ -198,7 +190,6 @@ class ConfigureForm(forms.Form): raise forms.ValidationError(_('Please provide a password')) -@package.required(['ez-ipupdate']) def configure(request): """Serve the configuration form.""" status = get_status() @@ -219,7 +210,6 @@ def configure(request): 'subsubmenu': subsubmenu}) -@package.required(['ez-ipupdate']) def statuspage(request): """Serve the status page.""" check_nat = actions.run('dynamicdns', ['get-nat']) diff --git a/plinth/modules/dynamicdns/templates/dynamicdns.html b/plinth/modules/dynamicdns/templates/dynamicdns.html index 5ad1ec90b..1b3d1bfa7 100644 --- a/plinth/modules/dynamicdns/templates/dynamicdns.html +++ b/plinth/modules/dynamicdns/templates/dynamicdns.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -20,32 +20,7 @@ {% load i18n %} -{% block content %} - -

{% trans "Dynamic DNS Client" %}

- -

- {% blocktrans trimmed %} - If your internet provider changes your IP address periodic - (i.e. every 24h) it may be hard for others to find you in the - WEB. And for this reason nobody may find the services which are - provided by {{ box_name }}, such as ownCloud. - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - The solution is to assign a DNS name to your IP address and - update the DNS name every time your IP is changed by your - Internet provider. Dynamic DNS allows you to push your current - public IP address to an - gnudip - server. Afterwards the Server will assign your DNS name with the - new IP and if someone from the Internet asks for your DNS name - he will get your current IP answered. - {% endblocktrans %} -

- +{% block configuration %}

{% blocktrans trimmed %} If you are looking for a free dynamic DNS account, you may find diff --git a/plinth/modules/firewall/__init__.py b/plinth/modules/firewall/__init__.py index 27daf85a0..96cb449a1 100644 --- a/plinth/modules/firewall/__init__.py +++ b/plinth/modules/firewall/__init__.py @@ -19,9 +19,119 @@ Plinth module to configure a firewall """ -from . import firewall -from .firewall import init +from django.utils.translation import ugettext_lazy as _ +import logging -__all__ = ['firewall', 'init'] +from plinth import actions +from plinth import cfg +from plinth.signals import service_enabled +import plinth.service as service_module +from plinth.utils import format_lazy -depends = ['plinth.modules.system'] +version = 1 + +is_essential = True + +depends = ['system'] + +title = _('Firewall') + +description = [ + format_lazy( + _('Firewall is a security system that controls the incoming and ' + 'outgoing network traffic on your {box_name}. Keeping a ' + 'firewall enabled and properly configured reduces risk of ' + 'security threat from the Internet.'), box_name=cfg.box_name) +] + +LOGGER = logging.getLogger(__name__) + + +def init(): + """Initailze firewall module""" + menu = cfg.main_menu.get('system:index') + menu.add_urlname(title, 'glyphicon-fire', 'firewall:index', 50) + + service_enabled.connect(on_service_enabled) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['firewalld']) + + +def get_enabled_status(): + """Return whether firewall is enabled""" + output = _run(['get-status'], superuser=True) + return output.split()[0] == 'running' + + +def get_enabled_services(zone): + """Return the status of various services currently enabled""" + output = _run(['get-enabled-services', '--zone', zone], superuser=True) + return output.split() + + +def add_service(port, zone): + """Enable a service in firewall""" + _run(['add-service', port, '--zone', zone], superuser=True) + + +def remove_service(port, zone): + """Remove a service in firewall""" + _run(['remove-service', port, '--zone', zone], superuser=True) + + +def on_service_enabled(sender, service_id, enabled, **kwargs): + """ + Enable/disable firewall ports when a service is + enabled/disabled. + """ + del sender # Unused + del kwargs # Unused + + internal_enabled_services = get_enabled_services(zone='internal') + external_enabled_services = get_enabled_services(zone='external') + + LOGGER.info('Service enabled - %s, %s', service_id, enabled) + service = service_module.services[service_id] + for port in service.ports: + if enabled: + if port not in internal_enabled_services: + add_service(port, zone='internal') + + if (service.is_external and + port not in external_enabled_services): + add_service(port, zone='external') + else: + # service already configured. + pass + else: + if port in internal_enabled_services: + enabled_services_on_port = [ + service_.is_enabled() + for service_ in service_module.services.values() + if port in service_.ports and + service_id != service_.service_id] + if not any(enabled_services_on_port): + remove_service(port, zone='internal') + + if port in external_enabled_services: + enabled_services_on_port = [ + service_.is_enabled() + for service_ in service_module.services.values() + if port in service_.ports and + service_id != service_.service_id and + service_.is_external] + if not any(enabled_services_on_port): + remove_service(port, zone='external') + + +def _run(arguments, superuser=False): + """Run an given command and raise exception if there was an error""" + command = 'firewall' + + if superuser: + return actions.superuser_run(command, arguments) + else: + return actions.run(command, arguments) diff --git a/plinth/modules/firewall/firewall.py b/plinth/modules/firewall/firewall.py deleted file mode 100644 index dc693af90..000000000 --- a/plinth/modules/firewall/firewall.py +++ /dev/null @@ -1,137 +0,0 @@ -# -# This file is part of Plinth. -# -# 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 . -# - -""" -Plinth module to configure a firewall -""" - -from django.template.response import TemplateResponse -from django.utils.translation import ugettext_lazy as _ -import logging - -from plinth import actions -from plinth import cfg -from plinth import package -from plinth.signals import service_enabled -import plinth.service as service_module - - -LOGGER = logging.getLogger(__name__) - - -def init(): - """Initailze firewall module""" - menu = cfg.main_menu.get('system:index') - menu.add_urlname(_('Firewall'), 'glyphicon-fire', 'firewall:index', 50) - - service_enabled.connect(on_service_enabled) - - -@package.required(['firewalld']) -def index(request): - """Serve introcution page""" - if not get_enabled_status(): - return TemplateResponse(request, 'firewall.html', - {'title': _('Firewall'), - 'firewall_status': 'not_running'}) - - internal_enabled_services = get_enabled_services(zone='internal') - external_enabled_services = get_enabled_services(zone='external') - - return TemplateResponse( - request, 'firewall.html', - {'title': _('Firewall'), - 'services': list(service_module.services.values()), - 'internal_enabled_services': internal_enabled_services, - 'external_enabled_services': external_enabled_services}) - - -def get_enabled_status(): - """Return whether firewall is enabled""" - output = _run(['get-status'], superuser=True) - return output.split()[0] == 'running' - - -def get_enabled_services(zone): - """Return the status of various services currently enabled""" - output = _run(['get-enabled-services', '--zone', zone], superuser=True) - return output.split() - - -def add_service(port, zone): - """Enable a service in firewall""" - _run(['add-service', port, '--zone', zone], superuser=True) - - -def remove_service(port, zone): - """Remove a service in firewall""" - _run(['remove-service', port, '--zone', zone], superuser=True) - - -def on_service_enabled(sender, service_id, enabled, **kwargs): - """ - Enable/disable firewall ports when a service is - enabled/disabled. - """ - del sender # Unused - del kwargs # Unused - - internal_enabled_services = get_enabled_services(zone='internal') - external_enabled_services = get_enabled_services(zone='external') - - LOGGER.info('Service enabled - %s, %s', service_id, enabled) - service = service_module.services[service_id] - for port in service.ports: - if enabled: - if port not in internal_enabled_services: - add_service(port, zone='internal') - - if (service.is_external and - port not in external_enabled_services): - add_service(port, zone='external') - else: - # service already configured. - pass - else: - if port in internal_enabled_services: - enabled_services_on_port = [ - service_.is_enabled() - for service_ in service_module.services.values() - if port in service_.ports and - service_id != service_.service_id] - if not any(enabled_services_on_port): - remove_service(port, zone='internal') - - if port in external_enabled_services: - enabled_services_on_port = [ - service_.is_enabled() - for service_ in service_module.services.values() - if port in service_.ports and - service_id != service_.service_id and - service_.is_external] - if not any(enabled_services_on_port): - remove_service(port, zone='external') - - -def _run(arguments, superuser=False): - """Run an given command and raise exception if there was an error""" - command = 'firewall' - - if superuser: - return actions.superuser_run(command, arguments) - else: - return actions.run(command, arguments) diff --git a/plinth/modules/firewall/templates/firewall.html b/plinth/modules/firewall/templates/firewall.html index e59d4052d..ec2440702 100644 --- a/plinth/modules/firewall/templates/firewall.html +++ b/plinth/modules/firewall/templates/firewall.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -20,18 +20,7 @@ {% load i18n %} -{% block content %} - -

{{ title }}

- -

- {% blocktrans trimmed %} - Firewall is a security system that controls the incoming and - outgoing network traffic on your {{ box_name }}. Keeping a - firewall enabled and properly configured reduces risk of - security threat from the Internet. - {% endblocktrans %} -

+{% block configuration %}

{% trans "Current status:" %}

diff --git a/plinth/modules/firewall/urls.py b/plinth/modules/firewall/urls.py index 6448e89f6..8c6765af9 100644 --- a/plinth/modules/firewall/urls.py +++ b/plinth/modules/firewall/urls.py @@ -21,7 +21,7 @@ URLs for the Firewall module from django.conf.urls import url -from . import firewall as views +from . import views urlpatterns = [ diff --git a/plinth/modules/firewall/views.py b/plinth/modules/firewall/views.py new file mode 100644 index 000000000..3dab988ab --- /dev/null +++ b/plinth/modules/firewall/views.py @@ -0,0 +1,45 @@ +# +# This file is part of Plinth. +# +# 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 . +# + +""" +Plinth module to configure a firewall +""" + +from django.template.response import TemplateResponse + +from plinth.modules import firewall +import plinth.service as service_module + + +def index(request): + """Serve introcution page""" + if not firewall.get_enabled_status(): + return TemplateResponse(request, 'firewall.html', + {'title': firewall.title, + 'description': firewall.description, + 'firewall_status': 'not_running'}) + + internal_enabled_services = firewall.get_enabled_services(zone='internal') + external_enabled_services = firewall.get_enabled_services(zone='external') + + return TemplateResponse( + request, 'firewall.html', + {'title': firewall.title, + 'description': firewall.description, + 'services': list(service_module.services.values()), + 'internal_enabled_services': internal_enabled_services, + 'external_enabled_services': external_enabled_services}) diff --git a/plinth/modules/first_boot/__init__.py b/plinth/modules/first_boot/__init__.py index 558116da9..55d9941a0 100644 --- a/plinth/modules/first_boot/__init__.py +++ b/plinth/modules/first_boot/__init__.py @@ -18,3 +18,7 @@ """ Plinth module for first boot wizard """ + +version = 1 + +is_essential = True diff --git a/plinth/modules/ikiwiki/__init__.py b/plinth/modules/ikiwiki/__init__.py index 402f4b6ae..05f105743 100644 --- a/plinth/modules/ikiwiki/__init__.py +++ b/plinth/modules/ikiwiki/__init__.py @@ -27,7 +27,16 @@ from plinth import cfg from plinth import service as service_module -depends = ['plinth.modules.apps'] +version = 1 + +depends = ['apps'] + +title = _('Wiki and Blog (ikiwiki)') + +description = [ + _('When enabled, the blogs and wikis will be available ' + 'from /ikiwiki.') +] service = None @@ -35,13 +44,25 @@ service = None def init(): """Initialize the ikiwiki module.""" menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('Wiki and Blog (ikiwiki)'), 'glyphicon-edit', - 'ikiwiki:index', 1100) + menu.add_urlname(title, 'glyphicon-edit', 'ikiwiki:index', 1100) global service service = service_module.Service( - 'ikiwiki', _('ikiwiki wikis and blogs'), ['http', 'https'], - is_external=True, enabled=is_enabled()) + 'ikiwiki', title, ['http', 'https'], is_external=True, + enabled=is_enabled()) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['ikiwiki', + 'gcc', + 'libc6-dev', + 'libtimedate-perl', + 'libcgi-formbuilder-perl', + 'libcgi-session-perl', + 'libxml-writer-perl']) + helper.call('post', actions.superuser_run, 'ikiwiki', ['setup']) + helper.call('post', service.notify_enabled, None, True) def is_enabled(): diff --git a/plinth/modules/ikiwiki/templates/ikiwiki.html b/plinth/modules/ikiwiki/templates/ikiwiki.html index 4cea868ad..e08738cf7 100644 --- a/plinth/modules/ikiwiki/templates/ikiwiki.html +++ b/plinth/modules/ikiwiki/templates/ikiwiki.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,14 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

- {% blocktrans trimmed %} - When enabled, the blogs and wikis will be available - from /ikiwiki. - {% endblocktrans %} -

+{% block configuration %} {% include "diagnostics_button.html" with module="ikiwiki" %} diff --git a/plinth/modules/ikiwiki/views.py b/plinth/modules/ikiwiki/views.py index 37bfaf5c3..27c1a855a 100644 --- a/plinth/modules/ikiwiki/views.py +++ b/plinth/modules/ikiwiki/views.py @@ -27,8 +27,6 @@ from django.utils.translation import ugettext as _, ugettext_lazy from .forms import IkiwikiForm, IkiwikiCreateForm from plinth import actions -from plinth import action_utils -from plinth import package from plinth.modules import ikiwiki @@ -40,20 +38,6 @@ subsubmenu = [{'url': reverse_lazy('ikiwiki:index'), 'text': ugettext_lazy('Create')}] -def on_install(): - """Enable ikiwiki on install.""" - actions.superuser_run('ikiwiki', ['setup']) - ikiwiki.service.notify_enabled(None, True) - - -@package.required(['ikiwiki', - 'gcc', - 'libc6-dev', - 'libtimedate-perl', - 'libcgi-formbuilder-perl', - 'libcgi-session-perl', - 'libxml-writer-perl'], - on_install=on_install) def index(request): """Serve configuration page.""" status = get_status() @@ -70,7 +54,8 @@ def index(request): form = IkiwikiForm(initial=status, prefix='ikiwiki') return TemplateResponse(request, 'ikiwiki.html', - {'title': _('Wiki and Blog'), + {'title': ikiwiki.title, + 'description': ikiwiki.description, 'status': status, 'form': form, 'subsubmenu': subsubmenu}) diff --git a/plinth/modules/letsencrypt/__init__.py b/plinth/modules/letsencrypt/__init__.py index 565d78bed..bfd630c16 100644 --- a/plinth/modules/letsencrypt/__init__.py +++ b/plinth/modules/letsencrypt/__init__.py @@ -20,20 +20,38 @@ Plinth module for using Let's Encrypt. """ from django.utils.translation import ugettext_lazy as _ -import json -from plinth import actions from plinth import action_utils from plinth import cfg -from plinth import service as service_module from plinth.modules import names +from plinth.utils import format_lazy -depends = [ - 'plinth.modules.apps', - 'plinth.modules.names' +version = 1 + +is_essential = True + +depends = ['apps', 'names'] + +title = _('Certificates (Let\'s Encrypt)') + +description = [ + format_lazy( + _('A digital certficate allows users of a web service to verify the ' + 'identity of the service and to securely communicate with it. ' + '{box_name} can automatically obtain and setup digital ' + 'certificates for each available domain. It does so by proving ' + 'itself to be the owner of a domain to Let\'s Encrypt, a ' + 'certficate authority (CA).'), box_name=_(cfg.box_name)), + + _('Let\'s Encrypt is a free, automated, and open certificate ' + 'authority, run for the public’s benefit by the Internet Security ' + 'Research Group (ISRG). Please read and agree with the ' + 'Let\'s Encrypt ' + 'Subscriber Agreement before using this service.') ] + service = None @@ -44,6 +62,11 @@ def init(): 'glyphicon-lock', 'letsencrypt:index', 20) +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['letsencrypt']) + + def diagnose(): """Run diagnostics and return the results.""" results = [] diff --git a/plinth/modules/letsencrypt/templates/letsencrypt.html b/plinth/modules/letsencrypt/templates/letsencrypt.html index 15043bdf2..1e3e29b2a 100644 --- a/plinth/modules/letsencrypt/templates/letsencrypt.html +++ b/plinth/modules/letsencrypt/templates/letsencrypt.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -33,30 +33,7 @@ {% endblock %} -{% block content %} - -

{% trans "Certificates (Let's Encrypt)" %}

- -

- {% blocktrans trimmed %} - A digital certficate allows users of a web service to verify the - identity of the service and to securely communicate with it. - {{ box_name }} can automatically obtain and setup digital - certificates for each available domain. It does so by proving - itself to be the owner of a domain to Let's Encrypt, a - certficate authority (CA). - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - Let's Encrypt is a free, automated, and open certificate - authority, run for the public’s benefit by the Internet Security - Research Group (ISRG). Please read and agree with the - Let's Encrypt - Subscriber Agreement before using this service. - {% endblocktrans %} -

+{% block configuration %}
diff --git a/plinth/modules/letsencrypt/views.py b/plinth/modules/letsencrypt/views.py index 46750c51b..c2a5ffbbd 100644 --- a/plinth/modules/letsencrypt/views.py +++ b/plinth/modules/letsencrypt/views.py @@ -29,20 +29,20 @@ import json import logging from plinth import actions -from plinth import package from plinth.errors import ActionError +from plinth.modules import letsencrypt from plinth.modules import names logger = logging.getLogger(__name__) -@package.required(['letsencrypt']) def index(request): """Serve configuration page.""" status = get_status() return TemplateResponse(request, 'letsencrypt.html', - {'title': _('Certificates (Let\'s Encrypt)'), + {'title': letsencrypt.title, + 'description': letsencrypt.description, 'status': status}) diff --git a/plinth/modules/monkeysphere/__init__.py b/plinth/modules/monkeysphere/__init__.py index d9501870d..09be4a195 100644 --- a/plinth/modules/monkeysphere/__init__.py +++ b/plinth/modules/monkeysphere/__init__.py @@ -23,7 +23,22 @@ from django.utils.translation import ugettext_lazy as _ from plinth import cfg -depends = ['plinth.modules.system'] +version = 1 + +depends = ['system'] + +title = _('Monkeysphere') + +description = [ + _('With Monkeysphere, a PGP key can be generated for each configured ' + 'domain serving SSH. The PGP public key can then be uploaded to the PGP ' + 'keyservers. Users connecting to this machine through SSH can verify ' + 'that they are connecting to the correct host. For users to trust the ' + 'key, at least one person (usually the machine owner) must sign the key ' + 'using the regular PGP key signing process. See the ' + ' ' + 'Monkeysphere SSH documentation for more details.') +] def init(): @@ -31,3 +46,8 @@ def init(): menu = cfg.main_menu.get('system:index') menu.add_urlname(_('Monkeysphere'), 'glyphicon-certificate', 'monkeysphere:index', 970) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['monkeysphere']) diff --git a/plinth/modules/monkeysphere/templates/monkeysphere.html b/plinth/modules/monkeysphere/templates/monkeysphere.html index 2e67c5204..ebe048590 100644 --- a/plinth/modules/monkeysphere/templates/monkeysphere.html +++ b/plinth/modules/monkeysphere/templates/monkeysphere.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -30,22 +30,7 @@ {% endblock %} -{% block content %} - -

{% trans "Monkeysphere" %}

- -

- {% blocktrans trimmed %} - With Monkeysphere, a PGP key can be generated for each configured domain - serving SSH. The PGP public key can then be uploaded to the PGP - keyservers. Users connecting to this machine through SSH can verify that - they are connecting to the correct host. For users to trust the key, at - least one person (usually the machine owner) must sign the key using the - regular PGP key signing process. See the - - Monkeysphere SSH documentation for more details. - {% endblocktrans %} -

+{% block configuration %} {% if running %}

diff --git a/plinth/modules/monkeysphere/views.py b/plinth/modules/monkeysphere/views.py index 777c64b25..2ce31d22d 100644 --- a/plinth/modules/monkeysphere/views.py +++ b/plinth/modules/monkeysphere/views.py @@ -28,20 +28,20 @@ from django.views.decorators.http import require_POST import json from plinth import actions -from plinth import package +from plinth.modules import monkeysphere from plinth.modules import names publish_process = None -@package.required(['monkeysphere']) def index(request): """Serve configuration page.""" _collect_publish_result(request) status = get_status() return TemplateResponse( request, 'monkeysphere.html', - {'title': _('Monkeysphere'), + {'title': monkeysphere.title, + 'description': monkeysphere.description, 'status': status, 'running': bool(publish_process)}) diff --git a/plinth/modules/mumble/__init__.py b/plinth/modules/mumble/__init__.py index 570a57cc8..b7f772494 100644 --- a/plinth/modules/mumble/__init__.py +++ b/plinth/modules/mumble/__init__.py @@ -21,13 +21,25 @@ Plinth module to configure Mumble server from django.utils.translation import ugettext_lazy as _ -from plinth import actions from plinth import action_utils from plinth import cfg from plinth import service as service_module -depends = ['plinth.modules.apps'] +version = 1 + +depends = ['apps'] + +title = _('Voice Chat (Mumble)') + +description = [ + _('Mumble is an open source, low-latency, encrypted, high quality ' + 'voice chat software.'), + + _('You can connect to your Mumble server on the regular Mumble port ' + '64738. Clients to connect to Mumble ' + 'from your desktop and Android devices are available.') +] service = None @@ -35,13 +47,17 @@ service = None def init(): """Intialize the Mumble module.""" menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('Voice Chat (Mumble)'), 'glyphicon-headphones', - 'mumble:index', 900) + menu.add_urlname(title, 'glyphicon-headphones', 'mumble:index', 900) global service service = service_module.Service( - 'mumble-plinth', _('Mumble Voice Chat Server'), - is_external=True, enabled=is_enabled()) + 'mumble-plinth', title, is_external=True, enabled=is_enabled()) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['mumble-server']) + helper.call('post', service.notify_enabled, None, True) def is_enabled(): diff --git a/plinth/modules/mumble/templates/mumble.html b/plinth/modules/mumble/templates/mumble.html index 8f9d1d725..96745dfd8 100644 --- a/plinth/modules/mumble/templates/mumble.html +++ b/plinth/modules/mumble/templates/mumble.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,25 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{% trans "Voice Chat (Mumble)" %}

- -

- {% blocktrans trimmed %} - Mumble is an open source, low-latency, encrypted, high quality - voice chat software. - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - You can connect to your Mumble server on the regular Mumble port 64738. - Clients to connect to Mumble - from your desktop and Android devices are available. - {% endblocktrans %} -

- +{% block configuration %}

{% trans "Status" %}

diff --git a/plinth/modules/mumble/views.py b/plinth/modules/mumble/views.py index 3ecadd7cc..4d67f6458 100644 --- a/plinth/modules/mumble/views.py +++ b/plinth/modules/mumble/views.py @@ -26,18 +26,11 @@ import logging from .forms import MumbleForm from plinth import actions -from plinth import package from plinth.modules import mumble logger = logging.getLogger(__name__) -def on_install(): - """Notify that the service is now enabled.""" - mumble.service.notify_enabled(None, True) - - -@package.required(['mumble-server'], on_install=on_install) def index(request): """Serve configuration page.""" status = get_status() @@ -55,7 +48,8 @@ def index(request): form = MumbleForm(initial=status, prefix='mumble') return TemplateResponse(request, 'mumble.html', - {'title': _('Voice Chat (Mumble)'), + {'title': mumble.title, + 'description': mumble.description, 'status': status, 'form': form}) diff --git a/plinth/modules/names/__init__.py b/plinth/modules/names/__init__.py index 841b95f7f..d2c5e9565 100644 --- a/plinth/modules/names/__init__.py +++ b/plinth/modules/names/__init__.py @@ -31,7 +31,13 @@ SERVICES = ( ('ssh', _('SSH'), 22), ) -depends = ['plinth.modules.system'] +version = 1 + +is_essential = True + +depends = ['system'] + +title = _('Name Services') domain_types = {} domains = {} @@ -42,8 +48,7 @@ logger = logging.getLogger(__name__) def init(): """Initialize the names module.""" menu = cfg.main_menu.get('system:index') - menu.add_urlname(_('Name Services'), 'glyphicon-tag', - 'names:index', 19) + menu.add_urlname(title, 'glyphicon-tag', 'names:index', 19) domain_added.connect(on_domain_added) domain_removed.connect(on_domain_removed) @@ -54,6 +59,7 @@ def on_domain_added(sender, domain_type, name='', description='', """Add domain to global list.""" if not domain_type: return + domain_types[domain_type] = description if not name: diff --git a/plinth/modules/names/templates/names.html b/plinth/modules/names/templates/names.html index fffc51897..bbdc0107b 100644 --- a/plinth/modules/names/templates/names.html +++ b/plinth/modules/names/templates/names.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,9 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{{ title }}

+{% block configuration %}
diff --git a/plinth/modules/names/views.py b/plinth/modules/names/views.py index c95ea0b00..d8daecd1f 100644 --- a/plinth/modules/names/views.py +++ b/plinth/modules/names/views.py @@ -24,6 +24,7 @@ from django.utils.translation import ugettext as _ from . import SERVICES, get_domain_types, get_description from . import get_domain, get_services_status +from plinth.modules import names def index(request): @@ -31,7 +32,7 @@ def index(request): status = get_status() return TemplateResponse(request, 'names.html', - {'title': _('Name Services'), + {'title': names.title, 'status': status}) diff --git a/plinth/modules/networks/__init__.py b/plinth/modules/networks/__init__.py index a3a679823..d72425a19 100644 --- a/plinth/modules/networks/__init__.py +++ b/plinth/modules/networks/__init__.py @@ -19,23 +19,37 @@ Plinth module to interface with network-manager """ -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy as _ from logging import Logger import subprocess -from . import networks -from .networks import init from plinth import action_utils +from plinth import cfg from plinth import network -__all__ = ['networks', 'init'] +version = 1 -depends = ['plinth.modules.system'] +is_essential = True + +depends = ['system'] + +title = _('Networks') logger = Logger(__name__) +def init(): + """Initialize the Networks module.""" + menu = cfg.main_menu.get('system:index') + menu.add_urlname(title, 'glyphicon-signal', 'networks:index', 18) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['network-manager']) + + def diagnose(): """Run diagnostics and return the results.""" results = [] @@ -44,8 +58,10 @@ def diagnose(): addresses = _get_interface_addresses(interfaces) for address in addresses: - results.append(action_utils.diagnose_port_listening(53, 'tcp', address)) - results.append(action_utils.diagnose_port_listening(53, 'udp', address)) + results.append( + action_utils.diagnose_port_listening(53, 'tcp', address)) + results.append( + action_utils.diagnose_port_listening(53, 'udp', address)) results.append(_diagnose_dnssec('4')) results.append(_diagnose_dnssec('6')) diff --git a/plinth/modules/networks/networks.py b/plinth/modules/networks/networks.py index 88034ee9f..522b08aa1 100644 --- a/plinth/modules/networks/networks.py +++ b/plinth/modules/networks/networks.py @@ -25,9 +25,7 @@ from logging import Logger from .forms import (ConnectionTypeSelectForm, EthernetForm, PPPoEForm, WifiForm) -from plinth import cfg from plinth import network -from plinth import package logger = Logger(__name__) @@ -40,14 +38,6 @@ subsubmenu = [{'url': reverse_lazy('networks:index'), 'text': ugettext_lazy('Add Connection')}] -def init(): - """Initialize the Networks module.""" - menu = cfg.main_menu.get('system:index') - menu.add_urlname(ugettext_lazy('Networks'), 'glyphicon-signal', - 'networks:index', 18) - - -@package.required(['network-manager']) def index(request): """Show connection list.""" connections = network.get_connection_list() diff --git a/plinth/modules/openvpn/__init__.py b/plinth/modules/openvpn/__init__.py index 20be7aabe..c035abb99 100644 --- a/plinth/modules/openvpn/__init__.py +++ b/plinth/modules/openvpn/__init__.py @@ -25,9 +25,25 @@ from plinth import actions from plinth import action_utils from plinth import cfg from plinth import service as service_module +from plinth.utils import format_lazy -depends = ['plinth.modules.apps'] +version = 1 + +depends = ['apps'] + +title = _('Virtual Private Network (OpenVPN)') + +description = [ + format_lazy( + _('Virtual Private Network (VPN) is a technique for securely ' + 'connecting two devices in order to access resources of a ' + 'private network. While you are away from home, you can connect ' + 'to your {box_name} in order to join your home network and ' + 'access private/internal services provided by {box_name}. ' + 'You can also access the rest of the Internet via {box_name} ' + 'for added security and anonymity.'), box_name=_(cfg.box_name)) +] service = None @@ -35,13 +51,16 @@ service = None def init(): """Intialize the OpenVPN module.""" menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('Virtual Private Network (OpenVPN)'), 'glyphicon-lock', - 'openvpn:index', 850) + menu.add_urlname(title, 'glyphicon-lock', 'openvpn:index', 850) global service service = service_module.Service( - 'openvpn', _('OpenVPN'), ['openvpn'], - is_external=True, enabled=is_enabled()) + 'openvpn', title, ['openvpn'], is_external=True, enabled=is_enabled()) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['openvpn', 'easy-rsa']) def is_enabled(): diff --git a/plinth/modules/openvpn/templates/openvpn.html b/plinth/modules/openvpn/templates/openvpn.html index d2ff0c5d8..422a9e69f 100644 --- a/plinth/modules/openvpn/templates/openvpn.html +++ b/plinth/modules/openvpn/templates/openvpn.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -30,21 +30,7 @@ {% endblock %} -{% block content %} - -

{% trans "Virtual Private Network (OpenVPN)" %}

- -

- {% blocktrans trimmed %} - Virtual Private Network (VPN) is a technique for securely - connecting two devices in order to access resources of a - private network. While you are away from home, you can connect - to your {{ box_name }} in order to join your home network and - access private/internal services provided by {{ box_name }}. - You can also access the rest of the Internet via {{ box_name }} - for added security and anonymity. - {% endblocktrans %} -

+{% block configuration %} {% if status.is_setup %} diff --git a/plinth/modules/openvpn/views.py b/plinth/modules/openvpn/views.py index f4cd85ed8..ea1c6aefa 100644 --- a/plinth/modules/openvpn/views.py +++ b/plinth/modules/openvpn/views.py @@ -29,7 +29,6 @@ import logging from .forms import OpenVpnForm from plinth import actions -from plinth import package from plinth.modules import openvpn from plinth.modules.config import config @@ -38,7 +37,6 @@ logger = logging.getLogger(__name__) setup_process = None -@package.required(['openvpn', 'easy-rsa']) def index(request): """Serve configuration page.""" status = get_status() @@ -59,7 +57,8 @@ def index(request): form = OpenVpnForm(initial=status, prefix='openvpn') return TemplateResponse(request, 'openvpn.html', - {'title': _('Virtual Private Network (OpenVPN)'), + {'title': openvpn.title, + 'description': openvpn.description, 'status': status, 'form': form}) diff --git a/plinth/modules/owncloud/__init__.py b/plinth/modules/owncloud/__init__.py index e48fcae2d..c5fbdd59a 100644 --- a/plinth/modules/owncloud/__init__.py +++ b/plinth/modules/owncloud/__init__.py @@ -19,14 +19,60 @@ Plinth module to configure ownCloud """ -from . import owncloud -from .owncloud import init +from django.utils.translation import ugettext_lazy as _ +from plinth import actions from plinth import action_utils +from plinth import cfg +from plinth import service as service_module -__all__ = ['owncloud', 'init'] +version = 1 -depends = ['plinth.modules.apps'] +depends = ['apps'] + +title = _('File Hosting (ownCloud)') + +description = [ + _('ownCloud gives you universal access to your files through a web ' + 'interface or WebDAV. It also provides a platform to easily view ' + '& sync your contacts, calendars and bookmarks across all your ' + 'devices and enables basic editing right on the web. Installation ' + 'has minimal server requirements, doesn\'t need special ' + 'permissions and is quick. ownCloud is extendable via a simple ' + 'but powerful API for applications and plugins.'), + + _('When enabled, the ownCloud installation will be available ' + 'from /owncloud path on the web server. ' + 'Visit this URL to set up the initial administration account for ' + 'ownCloud.') +] + +service = None + + +def init(): + """Initialize the ownCloud module""" + menu = cfg.main_menu.get('apps:index') + menu.add_urlname(title, 'glyphicon-picture', 'owncloud:index', 700) + + global service + service = service_module.Service( + 'owncloud', title, ['http', 'https'], is_external=True, + enabled=is_enabled()) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['postgresql', 'php5-pgsql', 'owncloud', 'php-dropbox', + 'php-google-api-php-client']) + helper.call('post', actions.superuser_run, 'owncloud-setup', ['enable']) + helper.call('post', service.notify_enabled, None, True) + + +def is_enabled(): + """Return whether the module is enabled.""" + output = actions.run('owncloud-setup', ['status']) + return 'enable' in output.split() def diagnose(): diff --git a/plinth/modules/owncloud/forms.py b/plinth/modules/owncloud/forms.py new file mode 100644 index 000000000..3ea4c6f8c --- /dev/null +++ b/plinth/modules/owncloud/forms.py @@ -0,0 +1,30 @@ +# +# This file is part of Plinth. +# +# 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 . +# + +""" +Forms for configuring ownCloud. +""" + +from django import forms +from django.utils.translation import ugettext_lazy as _ + + +class OwnCloudForm(forms.Form): # pylint: disable-msg=W0232 + """ownCloud configuration form""" + enabled = forms.BooleanField( + label=_('Enable ownCloud'), + required=False) diff --git a/plinth/modules/owncloud/templates/owncloud.html b/plinth/modules/owncloud/templates/owncloud.html index 410071f08..266967d3d 100644 --- a/plinth/modules/owncloud/templates/owncloud.html +++ b/plinth/modules/owncloud/templates/owncloud.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,30 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{% trans "File Hosting (ownCloud)" %}

- -

- {% blocktrans trimmed %} - ownCloud gives you universal access to your files through a web - interface or WebDAV. It also provides a platform to easily view - & sync your contacts, calendars and bookmarks across all your - devices and enables basic editing right on the web. Installation - has minimal server requirements, doesn't need special - permissions and is quick. ownCloud is extendable via a simple - but powerful API for applications and plugins. - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - When enabled, the ownCloud installation will be available - from /owncloud path on the web server. - Visit this URL to set up the initial administration account for - ownCloud. - {% endblocktrans %} -

+{% block configuration %} {% include "diagnostics_button.html" with module="owncloud" %} diff --git a/plinth/modules/owncloud/urls.py b/plinth/modules/owncloud/urls.py index d69ac14ef..338ad6cc5 100644 --- a/plinth/modules/owncloud/urls.py +++ b/plinth/modules/owncloud/urls.py @@ -21,7 +21,7 @@ URLs for the ownCloud module from django.conf.urls import url -from . import owncloud as views +from . import views urlpatterns = [ diff --git a/plinth/modules/owncloud/owncloud.py b/plinth/modules/owncloud/views.py similarity index 62% rename from plinth/modules/owncloud/owncloud.py rename to plinth/modules/owncloud/views.py index f510965d6..b2e77077b 100644 --- a/plinth/modules/owncloud/owncloud.py +++ b/plinth/modules/owncloud/views.py @@ -19,47 +19,15 @@ Plinth module for configuring ownCloud. """ -from django import forms from django.contrib import messages from django.template.response import TemplateResponse from django.utils.translation import ugettext_lazy as _ +from .forms import OwnCloudForm from plinth import actions -from plinth import cfg -from plinth import package -from plinth import service as service_module +from plinth.modules import owncloud -service = None - - -class OwnCloudForm(forms.Form): # pylint: disable-msg=W0232 - """ownCloud configuration form""" - enabled = forms.BooleanField(label=_('Enable ownCloud'), required=False) - - -def init(): - """Initialize the ownCloud module""" - menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('File Hosting (ownCloud)'), 'glyphicon-picture', - 'owncloud:index', 700) - - status = get_status() - - global service # pylint: disable-msg=W0603 - service = service_module.Service( - 'owncloud', _('ownCloud'), ['http', 'https'], is_external=True, - enabled=status['enabled']) - - -def on_install(): - """Tasks to run after package install.""" - actions.superuser_run('owncloud-setup', ['enable']) - service.notify_enabled(None, True) - - -@package.required(['postgresql', 'php5-pgsql', 'owncloud', 'php-dropbox', - 'php-google-api-php-client'], on_install=on_install) def index(request): """Serve the ownCloud configuration page""" status = get_status() @@ -77,14 +45,14 @@ def index(request): form = OwnCloudForm(initial=status, prefix='owncloud') return TemplateResponse(request, 'owncloud.html', - {'title': _('ownCloud'), + {'title': owncloud.title, + 'description': owncloud.description, 'form': form}) def get_status(): """Return the current status""" - output = actions.run('owncloud-setup', ['status']) - return {'enabled': 'enable' in output.split()} + return {'enabled': owncloud.is_enabled()} def _apply_changes(request, old_status, new_status): @@ -104,4 +72,4 @@ def _apply_changes(request, old_status, new_status): # Send a signal to other modules that the service is # enabled/disabled - service.notify_enabled(None, new_status['enabled']) + owncloud.service.notify_enabled(None, new_status['enabled']) diff --git a/plinth/modules/pagekite/__init__.py b/plinth/modules/pagekite/__init__.py index b83c7e5ae..6862c1b68 100644 --- a/plinth/modules/pagekite/__init__.py +++ b/plinth/modules/pagekite/__init__.py @@ -21,19 +21,59 @@ Plinth module to configure PageKite from django.utils.translation import ugettext_lazy as _ from plinth import cfg +from plinth.utils import format_lazy from . import utils -__all__ = ['init'] +version = 1 -depends = ['plinth.modules.apps', 'plinth.modules.names'] +depends = ['apps', 'names'] + +title = _('Public Visibility (PageKite)') + +description = [ + format_lazy( + _('PageKite is a system for exposing {box_name} services when ' + 'you don\'t have a direct connection to the Internet. You only ' + 'need this if your {box_name} services are unreachable from ' + 'the rest of the Internet. This includes the following ' + 'situations:'), box_name=_(cfg.box_name)), + + format_lazy( + _('{box_name} is behind a restricted firewall.'), + box_name=_(cfg.box_name)), + + format_lazy( + _('{box_name} is connected to a (wireless) router which you ' + 'don\'t control.'), box_name=_(cfg.box_name)), + + _('Your ISP does not provide you an external IP address and ' + 'instead provides Internet connection through NAT.'), + + _('Your ISP does not provide you a static IP address and your IP ' + 'address changes evertime you connect to Internet.'), + + _('Your ISP limits incoming connections.'), + + format_lazy( + _('PageKite works around NAT, firewalls and IP-address limitations ' + 'by using a combination of tunnels and reverse proxies. You can ' + 'use any pagekite service provider, for example ' + 'pagekite.net. In future it ' + 'might be possible to use your buddy\'s {box_name} for this.'), + box_name=_(cfg.box_name)) +] def init(): """Intialize the PageKite module""" menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('Public Visibility (PageKite)'), - 'glyphicon-flag', 'pagekite:index', 800) + menu.add_urlname(title, 'glyphicon-flag', 'pagekite:index', 800) # Register kite name with Name Services module. utils.update_names_module(initial_registration=True) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['pagekite']) diff --git a/plinth/modules/pagekite/templates/pagekite_introduction.html b/plinth/modules/pagekite/templates/pagekite_introduction.html index 92682064d..0961c1923 100644 --- a/plinth/modules/pagekite/templates/pagekite_introduction.html +++ b/plinth/modules/pagekite/templates/pagekite_introduction.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -20,58 +20,7 @@ {% load i18n %} -{% block content %} - -

- {% blocktrans trimmed %} - PageKite is a system for exposing {{ box_name }} services when - you don't have a direct connection to the Internet. You only - need this if your {{ box_name }} services are unreachable from - the rest of the Internet. This includes the following - situations: - {% endblocktrans %} -

- -
    -
  • - {% blocktrans trimmed %} - {{ box_name }} is behind a restricted firewall. - {% endblocktrans %} -
  • - -
  • - {% blocktrans trimmed %} - {{ box_name }} is connected to a (wireless) router which you - don't control. - {% endblocktrans %} -
  • - -
  • - {% blocktrans trimmed %} - Your ISP does not provide you an external IP address and - instead provides Internet connection through NAT. - {% endblocktrans %} -
  • - -
  • - {% blocktrans trimmed %} - Your ISP does not provide you a static IP address and your IP - address changes evertime you connect to Internet. - {% endblocktrans %} -
  • - -
  • {% trans "Your ISP limits incoming connections." %}
  • -
- -

- {% blocktrans trimmed %} - PageKite works around NAT, firewalls and IP-address limitations - by using a combination of tunnels and reverse proxies. You can - use any pagekite service provider, for example - pagekite.net. In future it - might be possible to use your buddy's {{ box_name }} for this. - {% endblocktrans %} -

+{% block configuration %}

diff --git a/plinth/modules/pagekite/views.py b/plinth/modules/pagekite/views.py index 99fe0fd46..583f072dc 100644 --- a/plinth/modules/pagekite/views.py +++ b/plinth/modules/pagekite/views.py @@ -18,18 +18,16 @@ from django.core.urlresolvers import reverse, reverse_lazy from django.http.response import HttpResponseRedirect from django.template.response import TemplateResponse -from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ from django.views.generic import View, TemplateView from django.views.generic.edit import FormView -from plinth import package from . import utils from .forms import ConfigurationForm, StandardServiceForm, \ AddCustomServiceForm, DeleteCustomServiceForm +from plinth.modules import pagekite -required_packages = ('pagekite',) subsubmenu = [{'url': reverse_lazy('pagekite:index'), 'text': _('About PageKite')}, {'url': reverse_lazy('pagekite:configure'), @@ -43,7 +41,8 @@ subsubmenu = [{'url': reverse_lazy('pagekite:index'), def index(request): """Serve introduction page""" return TemplateResponse(request, 'pagekite_introduction.html', - {'title': _('Public Visibility (PageKite)'), + {'title': pagekite.title, + 'description': pagekite.description, 'subsubmenu': subsubmenu}) @@ -59,7 +58,6 @@ class ContextMixin(object): context['subsubmenu'] = subsubmenu return context - @method_decorator(package.required(required_packages)) def dispatch(self, *args, **kwargs): return super(ContextMixin, self).dispatch(*args, **kwargs) @@ -81,8 +79,9 @@ class CustomServiceView(ContextMixin, TemplateView): unused, custom_services = utils.get_pagekite_services() for service in custom_services: service['form'] = AddCustomServiceForm(initial=service) - context['custom_services'] = [utils.prepare_service_for_display(service) - for service in custom_services] + context['custom_services'] = [ + utils.prepare_service_for_display(service) + for service in custom_services] context.update(utils.get_kite_details()) return context diff --git a/plinth/modules/power/__init__.py b/plinth/modules/power/__init__.py index f8f4c4e56..9cf5d0aef 100644 --- a/plinth/modules/power/__init__.py +++ b/plinth/modules/power/__init__.py @@ -23,11 +23,20 @@ from django.utils.translation import ugettext_lazy as _ from plinth import cfg -depends = ['plinth.modules.system'] +version = 1 + +is_essential = True + +depends = ['system'] + +title = _('Power') + +description = [ + _('Restart or shut down the system.') +] def init(): """Initialize the power module.""" menu = cfg.main_menu.get('system:index') - menu.add_urlname(_('Power'), 'glyphicon-off', - 'power:index', 1000) + menu.add_urlname(title, 'glyphicon-off', 'power:index', 1000) diff --git a/plinth/modules/power/templates/power.html b/plinth/modules/power/templates/power.html index 642aea911..89553d389 100644 --- a/plinth/modules/power/templates/power.html +++ b/plinth/modules/power/templates/power.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,13 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{{ title }}

- -

- {% blocktrans trimmed %}Restart or shut down the system.{% endblocktrans %} -

+{% block configuration %}

diff --git a/plinth/modules/power/views.py b/plinth/modules/power/views.py index e1d835f6a..ae18d4506 100644 --- a/plinth/modules/power/views.py +++ b/plinth/modules/power/views.py @@ -26,11 +26,14 @@ from django.template.response import TemplateResponse from django.utils.translation import ugettext as _ from plinth import actions +from plinth.modules import power def index(request): """Serve power controls page.""" - return TemplateResponse(request, 'power.html', {'title': _('Power')}) + return TemplateResponse(request, 'power.html', + {'title': power.title, + 'description': power.description}) def restart(request): diff --git a/plinth/modules/privoxy/__init__.py b/plinth/modules/privoxy/__init__.py index 545020a0f..eaf0d20d1 100644 --- a/plinth/modules/privoxy/__init__.py +++ b/plinth/modules/privoxy/__init__.py @@ -20,15 +20,36 @@ Plinth module to configure Privoxy. """ from django.utils.translation import ugettext_lazy as _ -import json from plinth import actions from plinth import action_utils from plinth import cfg from plinth import service as service_module +from plinth.utils import format_lazy -depends = ['plinth.modules.apps'] +version = 1 + +is_essential = False + +depends = ['apps'] + +title = _('Web Proxy (Privoxy)') + +description = [ + _('Privoxy is a non-caching web proxy with advanced filtering ' + 'capabilities for enhancing privacy, modifying web page data and ' + 'HTTP headers, controlling access, and removing ads and other ' + 'obnoxious Internet junk. '), + + format_lazy( + _('You can use Privoxy by modifying your browser proxy settings to ' + 'your {box_name} hostname (or IP address) with port 8118. ' + 'While using Privoxy, you can see its configuration details and ' + 'documentation at ' + 'http://config.privoxy.org/ ' + 'or http://p.p.'), box_name=_(cfg.box_name)) +] service = None @@ -36,13 +57,18 @@ service = None def init(): """Intialize the module.""" menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('Web Proxy (Privoxy)'), 'glyphicon-cloud-upload', - 'privoxy:index', 1000) + menu.add_urlname(title, 'glyphicon-cloud-upload', 'privoxy:index', 1000) global service service = service_module.Service( - 'privoxy', _('Privoxy Web Proxy'), - is_external=False, enabled=is_enabled()) + 'privoxy', title, is_external=False, enabled=is_enabled()) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['privoxy']) + helper.call('post', actions.superuser_run, 'privoxy', ['setup']) + helper.call('post', service.notify_enabled, None, True) def is_enabled(): @@ -84,9 +110,7 @@ def diagnose_url_with_proxy(): result = action_utils.diagnose_url(url, kind=address['kind'], env=env) result[0] = _('Access {url} with proxy {proxy} on tcp{kind}') \ - .format(url=url, proxy=proxy, kind=address['kind']) + .format(url=url, proxy=proxy, kind=address['kind']) results.append(result) return results - - diff --git a/plinth/modules/privoxy/templates/privoxy.html b/plinth/modules/privoxy/templates/privoxy.html index 4a4f82c00..d80099eb4 100644 --- a/plinth/modules/privoxy/templates/privoxy.html +++ b/plinth/modules/privoxy/templates/privoxy.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,29 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{% trans "Web Proxy (Privoxy)" %}

- -

- {% blocktrans trimmed %} - Privoxy is a non-caching web proxy with advanced filtering - capabilities for enhancing privacy, modifying web page data and - HTTP headers, controlling access, and removing ads and other - obnoxious Internet junk. - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - You can use Privoxy by modifying your browser proxy settings to - your {{ box_name }} hostname (or IP address) with port 8118. - While using Privoxy, you can see its configuration details and - documentation at - http://config.privoxy.org/ - or http://p.p. - {% endblocktrans %} -

+{% block configuration %}

{% trans "Status" %}

diff --git a/plinth/modules/privoxy/views.py b/plinth/modules/privoxy/views.py index 47bb844c7..853f8709f 100644 --- a/plinth/modules/privoxy/views.py +++ b/plinth/modules/privoxy/views.py @@ -26,19 +26,11 @@ import logging from .forms import PrivoxyForm from plinth import actions -from plinth import package from plinth.modules import privoxy logger = logging.getLogger(__name__) -def on_install(): - """Notify that the service is now enabled.""" - actions.superuser_run('privoxy', ['setup']) - privoxy.service.notify_enabled(None, True) - - -@package.required(['privoxy'], on_install=on_install) def index(request): """Serve configuration page.""" status = get_status() @@ -56,7 +48,8 @@ def index(request): form = PrivoxyForm(initial=status, prefix='privoxy') return TemplateResponse(request, 'privoxy.html', - {'title': _('Web Proxy (Privoxy)'), + {'title': privoxy.title, + 'description': privoxy.description, 'status': status, 'form': form}) diff --git a/plinth/modules/quassel/__init__.py b/plinth/modules/quassel/__init__.py index 3e8c1b03a..c30f0abcc 100644 --- a/plinth/modules/quassel/__init__.py +++ b/plinth/modules/quassel/__init__.py @@ -16,7 +16,7 @@ # """ -Plinth module for quassel. +Plinth module for Quassel. """ from django.utils.translation import ugettext_lazy as _ @@ -24,8 +24,30 @@ from django.utils.translation import ugettext_lazy as _ from plinth import action_utils from plinth import cfg from plinth import service as service_module +from plinth.utils import format_lazy -depends = ['plinth.modules.apps'] +version = 1 + +depends = ['apps'] + +title = _('IRC Client (Quassel)') + +description = [ + format_lazy( + _('Quassel is an IRC application that is split into two parts, a ' + '"core" and a "client". This allows the core to remain connected ' + 'to IRC servers, and to continue receiving messages, even when ' + 'the client is disconnected. {box_name} can run the Quassel ' + 'core service keeping you always online and one or more Quassel ' + 'clients from a desktop or a mobile can be used to connect and ' + 'disconnect from it.'), box_name=_(cfg.box_name)), + + _('You can connect to your Quassel core on the default Quassel port ' + '4242. Clients to connect to Quassel from your ' + 'desktop and ' + 'mobile devices ' + 'are available.') +] service = None @@ -33,13 +55,17 @@ service = None def init(): """Initialize the quassel module.""" menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('IRC Client (Quassel)'), 'glyphicon-retweet', - 'quassel:index', 730) + menu.add_urlname(title, 'glyphicon-retweet', 'quassel:index', 730) global service service = service_module.Service( - 'quassel-plinth', _('Quassel IRC Client'), - is_external=True, enabled=is_enabled()) + 'quassel-plinth', title, is_external=True, enabled=is_enabled()) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['quassel-core']) + helper.call('post', service.notify_enabled, None, True) def is_enabled(): diff --git a/plinth/modules/quassel/templates/quassel.html b/plinth/modules/quassel/templates/quassel.html index c2f47ce0c..227769463 100644 --- a/plinth/modules/quassel/templates/quassel.html +++ b/plinth/modules/quassel/templates/quassel.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,31 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{% trans "IRC Client (Quassel)" %}

- -

- {% blocktrans trimmed %} - Quassel is an IRC application that is split into two parts, a - "core" and a "client". This allows the core to remain connected - to IRC servers, and to continue receiving messages, even when - the client is disconnected. {{ box_name }} can run the Quassel - core service keeping you always online and one or more Quassel - clients from a desktop or a mobile can be used to connect and - disconnect from it. - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - You can connect to your Quassel core on the default Quassel port - 4242. Clients to connect to Quassel from your - desktop and - mobile devices - are available. - {% endblocktrans %} -

+{% block configuration %}

{% trans "Status" %}

diff --git a/plinth/modules/quassel/views.py b/plinth/modules/quassel/views.py index 3918c849b..2adee5260 100644 --- a/plinth/modules/quassel/views.py +++ b/plinth/modules/quassel/views.py @@ -25,16 +25,9 @@ from django.utils.translation import ugettext as _ from .forms import QuasselForm from plinth import actions -from plinth import package from plinth.modules import quassel -def on_install(): - """Notify that the service is now enabled.""" - quassel.service.notify_enabled(None, True) - - -@package.required(['quassel-core'], on_install=on_install) def index(request): """Serve configuration page.""" status = get_status() @@ -51,7 +44,8 @@ def index(request): form = QuasselForm(initial=status, prefix='quassel') return TemplateResponse(request, 'quassel.html', - {'title': _('IRC Client (Quassel)'), + {'title': quassel.title, + 'description': quassel.description, 'status': status, 'form': form}) diff --git a/plinth/modules/repro/__init__.py b/plinth/modules/repro/__init__.py index 7517052f8..0a72adcf8 100644 --- a/plinth/modules/repro/__init__.py +++ b/plinth/modules/repro/__init__.py @@ -21,11 +21,37 @@ Plinth module for repro. from django.utils.translation import ugettext_lazy as _ +from plinth import actions from plinth import action_utils from plinth import cfg from plinth import service as service_module -depends = ['plinth.modules.apps'] +version = 1 + +depends = ['apps'] + +title = _('SIP Server (repro)') + +description = [ + _('repro provides various SIP services that a SIP softphone can utilize ' + 'to provide audio and video calls as well as presence and instant ' + 'messaging. repro provides a server and SIP user accounts that clients ' + 'can use to let their presence known. It also acts as a proxy to ' + 'federate SIP communications to other servers on the Internet similar ' + 'to email.'), + + _('To make SIP calls, a client application is needed. Available clients ' + 'include Jitsi (for computers) and ' + ' ' + 'CSipSimple (for Android phones).'), + + _('Note: Before using repro, domains and users will ' + 'need to be configured using the ' + 'web-based configuration panel. Users in the admin group ' + 'will be able to log in to the repro configuration panel. After setting ' + 'the domain, it is required to restart the repro service. Disable the ' + 'service and re-enable it.'), +] service = None @@ -33,13 +59,19 @@ service = None def init(): """Initialize the repro module.""" menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('SIP Server (repro)'), 'glyphicon-phone-alt', - 'repro:index', 825) + menu.add_urlname(title, 'glyphicon-phone-alt', 'repro:index', 825) global service service = service_module.Service( - 'repro', _('repro SIP Server'), ['sip-plinth', 'sip-tls-plinth'], - is_external=True, enabled=is_enabled()) + 'repro', title, ['sip-plinth', 'sip-tls-plinth'], is_external=True, + enabled=is_enabled()) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['repro']) + helper.call('post', actions.superuser_run, 'repro', ['setup']) + helper.call('post', service.notify_enabled, None, True) def is_enabled(): diff --git a/plinth/modules/repro/templates/repro.html b/plinth/modules/repro/templates/repro.html index 1d4b2fe7d..df5109cb0 100644 --- a/plinth/modules/repro/templates/repro.html +++ b/plinth/modules/repro/templates/repro.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,39 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{% trans "SIP Server (repro)" %}

- -

- {% blocktrans trimmed %} - repro provides various SIP services that a SIP softphone can utilize to - provide audio and video calls as well as presence and instant messaging. - repro provides a server and SIP user accounts that clients can use to let - their presence known. It also acts as a proxy to federate SIP - communications to other servers on the Internet similar to email. - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - To make SIP calls, a client application is needed. Available clients - include Jitsi (for computers) and - - CSipSimple (for Android phones). - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - Note: Before using repro, domains and users will need - to be configured using the web-based - configuration panel. Users in the admin group will be able - to log in to the repro configuration panel. After setting the domain, it - is required to restart the repro service. Disable the service and - re-enable it. - {% endblocktrans %} -

+{% block configuration %}

{% trans "Status" %}

diff --git a/plinth/modules/repro/views.py b/plinth/modules/repro/views.py index 40392f48a..407a96b0c 100644 --- a/plinth/modules/repro/views.py +++ b/plinth/modules/repro/views.py @@ -25,17 +25,9 @@ from django.utils.translation import ugettext as _ from .forms import ReproForm from plinth import actions -from plinth import package from plinth.modules import repro -def on_install(): - """Notify that the service is now enabled.""" - actions.superuser_run('repro', ['setup']) - repro.service.notify_enabled(None, True) - - -@package.required(['repro'], on_install=on_install) def index(request): """Serve configuration page.""" status = get_status() @@ -52,7 +44,8 @@ def index(request): form = ReproForm(initial=status, prefix='repro') return TemplateResponse(request, 'repro.html', - {'title': _('SIP Server (repro)'), + {'title': repro.title, + 'description': repro.description, 'status': status, 'form': form}) diff --git a/plinth/modules/restore/__init__.py b/plinth/modules/restore/__init__.py index 21bb030d4..dff18b3d4 100644 --- a/plinth/modules/restore/__init__.py +++ b/plinth/modules/restore/__init__.py @@ -22,24 +22,44 @@ Plinth module to configure reStore. from django.utils.translation import ugettext_lazy as _ from plinth import action_utils, cfg from plinth import service as service_module +from plinth.utils import format_lazy service = None -__all__ = ['init'] +version = 1 -depends = ['plinth.modules.apps'] +depends = ['apps'] + +title = _('Unhosted Storage (reStore)') + +description = [ + format_lazy( + _('reStore is a server for ' + 'unhosted web applications. The idea is to uncouple web ' + 'applications from data. No matter where a web application is ' + 'served from, the data can be stored on an unhosted storage ' + 'server of user\'s choice. With reStore, your {box_name} becomes ' + 'your unhosted storage server.'), box_name=_(cfg.box_name)), + + _('You can create and edit accounts in the ' + 'reStore web-interface.') +] def init(): """Initialize the reStore module.""" menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('Unhosted Storage (reStore)'), 'glyphicon-hdd', - 'restore:index', 750) + menu.add_urlname(title, 'glyphicon-hdd', 'restore:index', 750) global service service = service_module.Service( - 'node-restore', _('reStore'), ['http', 'https'], - is_external=False, enabled=is_enabled()) + 'node-restore', title, ['http', 'https'], is_external=False, + enabled=is_enabled()) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['node-restore']) def is_enabled(): diff --git a/plinth/modules/restore/templates/restore_index.html b/plinth/modules/restore/templates/restore_index.html index 8c9402ba7..4148a047c 100644 --- a/plinth/modules/restore/templates/restore_index.html +++ b/plinth/modules/restore/templates/restore_index.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,27 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{% trans "Unhosted Storage (reStore)" %}

- -

- {% blocktrans trimmed %} - reStore is a server for unhosted - web applications. The idea is to uncouple web applications from - data. No matter where a web application is served from, the - data can be stored on an unhosted storage server of user's - choice. With reStore, your {{ box_name }} becomes your - unhosted storage server. - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - You can create and edit accounts in the - reStore web-interface. - {% endblocktrans %} -

+{% block configuration %}

Configuration

diff --git a/plinth/modules/restore/views.py b/plinth/modules/restore/views.py index e0c75fbd6..0e91904a4 100644 --- a/plinth/modules/restore/views.py +++ b/plinth/modules/restore/views.py @@ -24,11 +24,10 @@ from django.template.response import TemplateResponse from django.utils.translation import ugettext as _ from .forms import ReStoreForm -from plinth import actions, package +from plinth import actions from plinth.modules import restore -@package.required(['node-restore']) def index(request): """Serve configuration page.""" status = get_status() @@ -43,7 +42,8 @@ def index(request): form = ReStoreForm(initial=status, prefix='restore') return TemplateResponse(request, 'restore_index.html', - {'title': _('Unhosted Storage (reStore)'), + {'title': restore.title, + 'description': restore.description, 'status': status, 'form': form}) diff --git a/plinth/modules/roundcube/__init__.py b/plinth/modules/roundcube/__init__.py index 5d9bd1e40..1c11596af 100644 --- a/plinth/modules/roundcube/__init__.py +++ b/plinth/modules/roundcube/__init__.py @@ -24,17 +24,49 @@ from django.utils.translation import ugettext_lazy as _ from plinth import actions from plinth import action_utils from plinth import cfg -from plinth import service as service_module -depends = ['plinth.modules.apps'] +version = 1 + +depends = ['apps'] + +title = _('Email Client (Roundcube)') + +description = [ + _('Roundcube webmail is a browser-based multilingual IMAP ' + 'client with an application-like user interface. It provides ' + 'full functionality you expect from an email client, including ' + 'MIME support, address book, folder manipulation, message ' + 'searching and spell checking.'), + + _('You can access Roundcube from ' + '/roundcube. Provide the username and password of the email ' + 'account you wish to access followed by the domain name of the ' + 'IMAP server for your email provider, like imap.example.com' + '. For IMAP over SSL (recommended), fill the server field ' + 'like imaps://imap.example.com.'), + + _('For Gmail, username will be your Gmail address, password will be ' + 'your Google account password and server will be ' + 'imaps://imap.gmail.com. Note that you will also need ' + 'to enable "Less secure apps" in your Google account settings ' + '(https://www.google.com/settings/security/lesssecureapps).'), +] def init(): """Intialize the module.""" menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('Email Client (Roundcube)'), 'glyphicon-envelope', - 'roundcube:index', 600) + menu.add_urlname(title, 'glyphicon-envelope', 'roundcube:index', 600) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.call('pre', actions.superuser_run, 'roundcube', ['pre-install']) + helper.install(['sqlite3', 'roundcube', 'roundcube-sqlite3']) + helper.call('pre', actions.superuser_run, 'roundcube', ['setup']) + def is_enabled(): """Return whether the module is enabled.""" diff --git a/plinth/modules/roundcube/templates/roundcube.html b/plinth/modules/roundcube/templates/roundcube.html index fea5d2c92..6028f7aba 100644 --- a/plinth/modules/roundcube/templates/roundcube.html +++ b/plinth/modules/roundcube/templates/roundcube.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,41 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{% trans "Email Client (Roundcube)" %}

- -

- {% blocktrans trimmed %} - Roundcube webmail is a browser-based multilingual IMAP client - with an application-like user interface. It provides full - functionality you expect from an email client, including MIME - support, address book, folder manipulation, message searching - and spell checking. - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - You can access Roundcube from /roundcube. - Provide the username and password of the email account you wish - to access followed by the domain name of the IMAP server for - your email provider, like imap.example.com. For - IMAP over SSL (recommended), fill the server field like - imaps://imap.example.com. - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - For Gmail, username will be your Gmail address, password will be - your Google account password and server will be - imaps://imap.gmail.com. Note that you will also need - to enable "Less secure apps" in your Google account settings - (https://www.google.com/settings/security/lesssecureapps). - {% endblocktrans %} -

+{% block configuration %} {% include "diagnostics_button.html" with module="roundcube" %} diff --git a/plinth/modules/roundcube/views.py b/plinth/modules/roundcube/views.py index 1790d8868..610fdb8c0 100644 --- a/plinth/modules/roundcube/views.py +++ b/plinth/modules/roundcube/views.py @@ -26,24 +26,11 @@ import logging from .forms import RoundcubeForm from plinth import actions -from plinth import package from plinth.modules import roundcube logger = logging.getLogger(__name__) -def before_install(): - """Preseed debconf values before the packages are installed.""" - actions.superuser_run('roundcube', ['pre-install']) - - -def on_install(): - """Setup Roundcube Apache configuration.""" - actions.superuser_run('roundcube', ['setup']) - - -@package.required(['sqlite3', 'roundcube', 'roundcube-sqlite3'], - before_install=before_install, on_install=on_install) def index(request): """Serve configuration page.""" status = get_status() @@ -61,7 +48,8 @@ def index(request): form = RoundcubeForm(initial=status, prefix='roundcube') return TemplateResponse(request, 'roundcube.html', - {'title': _('Email Client (Roundcube)'), + {'title': roundcube.title, + 'description': roundcube.description, 'status': status, 'form': form}) diff --git a/plinth/modules/shaarli/__init__.py b/plinth/modules/shaarli/__init__.py index e23af9186..2210da998 100644 --- a/plinth/modules/shaarli/__init__.py +++ b/plinth/modules/shaarli/__init__.py @@ -26,7 +26,20 @@ from plinth import cfg from plinth import service as service_module -depends = ['plinth.modules.apps'] +version = 1 + +depends = ['apps'] + +title = _('Bookmarks (Shaarli)') + +description = [ + _('Shaarli allows you to save and share bookmarks.'), + + _('When enabled, Shaarli will be available from ' + '/shaarli path on the web server. Note that Shaarli only supports a ' + 'single user account, which you will need to setup on the initial ' + 'visit.'), +] service = None @@ -34,13 +47,18 @@ service = None def init(): """Initialize the module.""" menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('Bookmarks (Shaarli)'), 'glyphicon-bookmark', - 'shaarli:index', 350) + menu.add_urlname(title, 'glyphicon-bookmark', 'shaarli:index', 350) global service service = service_module.Service( - 'shaarli', _('Shaarli'), ['http', 'https'], - is_external=True, enabled=is_enabled()) + 'shaarli', title, ['http', 'https'], is_external=True, + enabled=is_enabled()) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['shaarli']) + helper.call('post', service.notify_enabled, None, True) def is_enabled(): diff --git a/plinth/modules/shaarli/templates/shaarli.html b/plinth/modules/shaarli/templates/shaarli.html index 391c25466..9a6c55b0a 100644 --- a/plinth/modules/shaarli/templates/shaarli.html +++ b/plinth/modules/shaarli/templates/shaarli.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,21 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{% trans "Bookmarks (Shaarli)" %}

- -

{% trans "Shaarli allows you to save and share bookmarks." %}

- -

- {% blocktrans trimmed %} - - When enabled, Shaarli will be available from /shaarli - path on the web server. Note that Shaarli only supports a single - user account, which you will need to setup on the initial visit. - - {% endblocktrans %} -

+{% block configuration %}

{% trans "Configuration" %}

diff --git a/plinth/modules/shaarli/views.py b/plinth/modules/shaarli/views.py index 6182db539..de174faa7 100644 --- a/plinth/modules/shaarli/views.py +++ b/plinth/modules/shaarli/views.py @@ -25,14 +25,9 @@ from django.utils.translation import ugettext as _ from .forms import ShaarliForm from plinth import actions -from plinth import package from plinth.modules import shaarli -def on_install(): - """Notify that the service is now enabled.""" - shaarli.service.notify_enabled(None, True) -@package.required(['shaarli'], on_install=on_install) def index(request): """Serve configuration page.""" status = get_status() @@ -49,7 +44,8 @@ def index(request): form = ShaarliForm(initial=status, prefix='shaarli') return TemplateResponse(request, 'shaarli.html', - {'title': _('Bookmarks (Shaarli)'), + {'title': shaarli.title, + 'description': shaarli.description, 'status': status, 'form': form}) diff --git a/plinth/modules/system/__init__.py b/plinth/modules/system/__init__.py index ec0f9e62d..7b75d3976 100644 --- a/plinth/modules/system/__init__.py +++ b/plinth/modules/system/__init__.py @@ -19,8 +19,29 @@ Plinth module for system section page """ -from . import system -from .system import init +from django.utils.translation import ugettext_lazy as _ + +from plinth import cfg +from plinth.utils import format_lazy -__all__ = ['system', 'init'] +version = 1 + +is_essential = 1 + +title = _('System Configuration') + +description = [ + format_lazy( + _('Here you can administrate the underlying system of your ' + '{box_name}.'), box_name=_(cfg.box_name)), + + format_lazy( + _('The options affect the {box_name} at its most general level, ' + 'so be careful!'), box_name=_(cfg.box_name)) +] + + +def init(): + """Initialize the system module""" + cfg.main_menu.add_urlname(title, 'glyphicon-cog', 'system:index', 100) diff --git a/plinth/modules/system/templates/system.html b/plinth/modules/system/templates/system.html index e8c2a46f3..a60745d8f 100644 --- a/plinth/modules/system/templates/system.html +++ b/plinth/modules/system/templates/system.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'app.html' %} {% comment %} # # This file is part of Plinth. @@ -17,25 +17,3 @@ # along with this program. If not, see . # {% endcomment %} - -{% load i18n %} - -{% block content %} - -

{% trans "System Configuration" %}

- -

- {% blocktrans trimmed %} - Here you can administrate the underlying system of your - {{ box_name }}. - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - The options affect the {{ box_name }} at its most general level, - so be careful! - {% endblocktrans %} -

- -{% endblock %} diff --git a/plinth/modules/system/urls.py b/plinth/modules/system/urls.py index aceecc21d..8e75a6af5 100644 --- a/plinth/modules/system/urls.py +++ b/plinth/modules/system/urls.py @@ -21,7 +21,7 @@ URLs for the System module from django.conf.urls import url -from . import system as views +from . import views urlpatterns = [ diff --git a/plinth/modules/system/system.py b/plinth/modules/system/views.py similarity index 73% rename from plinth/modules/system/system.py rename to plinth/modules/system/views.py index c7a68b7f9..bc4dd1953 100644 --- a/plinth/modules/system/system.py +++ b/plinth/modules/system/views.py @@ -16,18 +16,12 @@ # from django.template.response import TemplateResponse -from django.utils.translation import ugettext_lazy as _ -from plinth import cfg - - -def init(): - """Initialize the system module""" - cfg.main_menu.add_urlname(_('System'), 'glyphicon-cog', 'system:index', - 100) +from plinth.modules import system def index(request): """Serve the index page""" return TemplateResponse(request, 'system.html', - {'title': _('System Configuration')}) + {'title': system.title, + 'description': system.description}) diff --git a/plinth/modules/tor/__init__.py b/plinth/modules/tor/__init__.py index bd7ef694e..e12773198 100644 --- a/plinth/modules/tor/__init__.py +++ b/plinth/modules/tor/__init__.py @@ -20,7 +20,7 @@ Plinth module to configure Tor. """ import augeas -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy as _ import glob import itertools @@ -32,7 +32,20 @@ from plinth.modules.names import SERVICES from plinth.signals import domain_added -depends = ['plinth.modules.apps', 'plinth.modules.names'] +version = 1 + +depends = ['apps', 'names'] + +title = _('Anonymity Network (Tor)') + +description = [ + _('Tor is an anonymous communication system. You can learn more ' + 'about it from the Tor ' + 'Project website. For best protection when web surfing, the ' + 'Tor Project recommends that you use the ' + '' + 'Tor Browser.') +] socks_service = None bridge_service = None @@ -45,8 +58,7 @@ APT_TOR_PREFIX = 'tor+' def init(): """Initialize the module.""" menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('Anonymity Network (Tor)'), 'glyphicon-eye-close', - 'tor:index', 100) + menu.add_urlname(title, 'glyphicon-eye-close', 'tor:index', 100) global socks_service socks_service = service_module.Service( @@ -77,6 +89,17 @@ def init(): services=hs_services) +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['tor', 'tor-geoipdb', 'torsocks', 'obfs4proxy', + 'apt-transport-tor']) + helper.call('post', actions.superuser_run, 'tor', ['setup']) + helper.call('post', actions.superuser_run, 'tor', + ['configure', '--apt-transport-tor', 'enable']) + helper.call('post', socks_service.notify_enabled, None, True) + helper.call('post', bridge_service.notify_enabled, None, True) + + def is_enabled(): """Return whether the module is enabled.""" return action_utils.service_is_enabled('tor') diff --git a/plinth/modules/tor/templates/tor.html b/plinth/modules/tor/templates/tor.html index 2b1b0f07e..9b6db3ed5 100644 --- a/plinth/modules/tor/templates/tor.html +++ b/plinth/modules/tor/templates/tor.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -30,20 +30,7 @@ {% endblock %} -{% block content %} - -

{% trans "Anonymity Network (Tor)" %}

- -

- {% blocktrans trimmed %} - Tor is an anonymous communication system. You can learn more - about it from the Tor - Project website. For best protection when web surfing, the - Tor Project recommends that you use the - - Tor Browser. - {% endblocktrans %} -

+{% block configuration %}

{% trans "Status" %}

diff --git a/plinth/modules/tor/views.py b/plinth/modules/tor/views.py index 120e42133..58e513a1f 100644 --- a/plinth/modules/tor/views.py +++ b/plinth/modules/tor/views.py @@ -25,7 +25,6 @@ from django.utils.translation import ugettext_lazy as _ from .forms import TorForm from plinth import actions -from plinth import package from plinth.errors import ActionError from plinth.modules import tor from plinth.modules.names import SERVICES @@ -34,18 +33,6 @@ from plinth.signals import domain_added, domain_removed config_process = None -def on_install(): - """Setup Tor configuration as soon as it is installed.""" - actions.superuser_run('tor', ['setup']) - actions.superuser_run('tor', - ['configure', '--apt-transport-tor', 'enable']) - tor.socks_service.notify_enabled(None, True) - tor.bridge_service.notify_enabled(None, True) - - -@package.required(['tor', 'tor-geoipdb', 'torsocks', 'obfs4proxy', - 'apt-transport-tor'], - on_install=on_install) def index(request): """Serve configuration page.""" if config_process: @@ -65,7 +52,8 @@ def index(request): form = TorForm(initial=status, prefix='tor') return TemplateResponse(request, 'tor.html', - {'title': _('Tor Control Panel'), + {'title': tor.title, + 'description': tor.description, 'status': status, 'config_running': bool(config_process), 'form': form}) diff --git a/plinth/modules/transmission/__init__.py b/plinth/modules/transmission/__init__.py index b57797b99..aed1380ed 100644 --- a/plinth/modules/transmission/__init__.py +++ b/plinth/modules/transmission/__init__.py @@ -20,6 +20,7 @@ Plinth module to configure Transmission server """ from django.utils.translation import ugettext_lazy as _ +import json from plinth import actions from plinth import action_utils @@ -27,7 +28,17 @@ from plinth import cfg from plinth import service as service_module -depends = ['plinth.modules.apps'] +version = 1 + +depends = ['apps'] + +title = _('BitTorrent (Transmission)') + +description = [ + _('BitTorrent is a peer-to-peer file sharing protocol. ' + 'Transmission daemon handles Bitorrent file sharing. Note that ' + 'BitTorrent is not anonymous.') +] service = None @@ -35,13 +46,25 @@ service = None def init(): """Intialize the Transmission module.""" menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('BitTorrent (Transmission)'), 'glyphicon-save', - 'transmission:index', 300) + menu.add_urlname(title, 'glyphicon-save', 'transmission:index', 300) global service service = service_module.Service( - 'transmission', _('Transmission BitTorrent'), ['http', 'https'], - is_external=True, enabled=is_enabled()) + 'transmission', title, ['http', 'https'], is_external=True, + enabled=is_enabled()) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['transmission-daemon']) + + new_configuration = {'rpc-whitelist-enabled': False} + helper.call('post', actions.superuser_run, 'transmission', + ['merge-configuration'], + input=json.dumps(new_configuration).encode()) + + helper.call('post', actions.superuser_run, 'transmission', ['enable']) + helper.call('post', service.notify_enabled, None, True) def is_enabled(): diff --git a/plinth/modules/transmission/templates/transmission.html b/plinth/modules/transmission/templates/transmission.html index de67c4908..0adf4648f 100644 --- a/plinth/modules/transmission/templates/transmission.html +++ b/plinth/modules/transmission/templates/transmission.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,17 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{% trans "BitTorrent (Transmission)" %}

- -

- {% blocktrans trimmed %} - BitTorrent is a peer-to-peer file sharing protocol. - Transmission daemon handles Bitorrent file sharing. Note that - BitTorrent is not anonymous. - {% endblocktrans %} -

+{% block configuration %}

{% blocktrans trimmed %} diff --git a/plinth/modules/transmission/views.py b/plinth/modules/transmission/views.py index 213e69da8..c65c81013 100644 --- a/plinth/modules/transmission/views.py +++ b/plinth/modules/transmission/views.py @@ -28,7 +28,6 @@ import socket from .forms import TransmissionForm from plinth import actions -from plinth import package from plinth.modules import transmission logger = logging.getLogger(__name__) @@ -36,17 +35,6 @@ logger = logging.getLogger(__name__) TRANSMISSION_CONFIG = '/etc/transmission-daemon/settings.json' -def on_install(): - """Enable transmission as soon as it is installed.""" - new_configuration = {'rpc-whitelist-enabled': False} - actions.superuser_run('transmission', ['merge-configuration'], - input=json.dumps(new_configuration).encode()) - - actions.superuser_run('transmission', ['enable']) - transmission.service.notify_enabled(None, True) - - -@package.required(['transmission-daemon'], on_install=on_install) def index(request): """Serve configuration page.""" status = get_status() @@ -64,7 +52,8 @@ def index(request): form = TransmissionForm(initial=status, prefix='transmission') return TemplateResponse(request, 'transmission.html', - {'title': _('BitTorrent (Transmission)'), + {'title': transmission.title, + 'description': transmission.description, 'status': status, 'form': form}) diff --git a/plinth/modules/upgrades/__init__.py b/plinth/modules/upgrades/__init__.py index 28b9f7c4a..795b88e1b 100644 --- a/plinth/modules/upgrades/__init__.py +++ b/plinth/modules/upgrades/__init__.py @@ -21,14 +21,32 @@ Plinth module for upgrades from django.utils.translation import ugettext_lazy as _ +from plinth import actions from plinth import cfg -depends = ['plinth.modules.system'] +version = 1 + +is_essential = 1 + +depends = ['system'] + +title = _('Software Upgrades') + +description = [ + _('Upgrades install the latest software and security updates. When ' + 'automatic upgrades are enabled, upgrades are automatically run every ' + 'night. You don\'t normally need to start the upgrade process.') +] def init(): """Initialize the module.""" menu = cfg.main_menu.get('system:index') - menu.add_urlname(_('Software Upgrades'), 'glyphicon-refresh', - 'upgrades:index', 21) + menu.add_urlname(title, 'glyphicon-refresh', 'upgrades:index', 21) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(['unattended-upgrades']) + helper.call('post', actions.superuser_run, 'upgrades', ['enable-auto']) diff --git a/plinth/modules/upgrades/templates/upgrades.html b/plinth/modules/upgrades/templates/upgrades.html index 15b0167b2..4cc47be98 100644 --- a/plinth/modules/upgrades/templates/upgrades.html +++ b/plinth/modules/upgrades/templates/upgrades.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'app.html' %} {% comment %} # # This file is part of Plinth. @@ -29,17 +29,7 @@ {% endblock %} -{% block content %} - -

{{ title }}

- -

- {% blocktrans trimmed %} - Upgrades install the latest software and security updates. When automatic - upgrades are enabled, upgrades are automatically run every night. You - don't normally need to start the upgrade process. - {% endblocktrans %} -

+{% block configuration %}

{% blocktrans trimmed %} diff --git a/plinth/modules/upgrades/templates/upgrades_configure.html b/plinth/modules/upgrades/templates/upgrades_configure.html index 1ee94ec32..4a4e1c3fe 100644 --- a/plinth/modules/upgrades/templates/upgrades_configure.html +++ b/plinth/modules/upgrades/templates/upgrades_configure.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,9 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{{ title }}

+{% block configuration %} {% csrf_token %} diff --git a/plinth/modules/upgrades/views.py b/plinth/modules/upgrades/views.py index d08d6f291..81b0a4ba3 100644 --- a/plinth/modules/upgrades/views.py +++ b/plinth/modules/upgrades/views.py @@ -21,16 +21,14 @@ Plinth module for upgrades from django.contrib import messages from django.core.urlresolvers import reverse_lazy -from django.shortcuts import redirect from django.template.response import TemplateResponse from django.utils.translation import ugettext as _, ugettext_lazy -from django.views.decorators.http import require_POST import subprocess from .forms import ConfigureForm from plinth import actions -from plinth import package from plinth.errors import ActionError +from plinth.modules import upgrades subsubmenu = [{'url': reverse_lazy('upgrades:index'), 'text': ugettext_lazy('Automatic Upgrades')}, @@ -41,12 +39,6 @@ LOG_FILE = '/var/log/unattended-upgrades/unattended-upgrades.log' LOCK_FILE = '/var/log/dpkg/lock' -def on_install(): - """Enable automatic upgrades after install.""" - actions.superuser_run('upgrades', ['enable-auto']) - - -@package.required(['unattended-upgrades'], on_install=on_install) def index(request): """Serve the configuration form.""" status = get_status() @@ -63,10 +55,12 @@ def index(request): form = ConfigureForm(initial=status, prefix='upgrades') return TemplateResponse(request, 'upgrades_configure.html', - {'title': _('Automatic Upgrades'), + {'title': upgrades.title, + 'description': upgrades.description, 'form': form, 'subsubmenu': subsubmenu}) + def is_package_manager_busy(): """Return whether a package manager is running.""" try: @@ -85,7 +79,6 @@ def get_log(): return None -@package.required(['unattended-upgrades'], on_install=on_install) def upgrade(request): """Serve the upgrade page.""" is_busy = is_package_manager_busy() @@ -99,7 +92,8 @@ def upgrade(request): messages.error(request, _('Starting upgrade failed.')) return TemplateResponse(request, 'upgrades.html', - {'title': _('Package Upgrades'), + {'title': upgrades.title, + 'description': upgrades.description, 'subsubmenu': subsubmenu, 'is_busy': is_busy, 'log': get_log()}) diff --git a/plinth/modules/users/__init__.py b/plinth/modules/users/__init__.py index 7232efaa1..b7c6c8ca1 100644 --- a/plinth/modules/users/__init__.py +++ b/plinth/modules/users/__init__.py @@ -20,21 +20,24 @@ Plinth module to manage users """ from django.utils.translation import ugettext_lazy as _ -import json import subprocess from plinth import cfg -from plinth import actions from plinth import action_utils -depends = ['plinth.modules.system'] +version = 1 + +is_essential = True + +depends = ['system'] + +title = _('Users and Groups') def init(): """Intialize the user module.""" menu = cfg.main_menu.get('system:index') - menu.add_urlname(_('Users and Groups'), 'glyphicon-user', 'users:index', - 15) + menu.add_urlname(title, 'glyphicon-user', 'users:index', 15) def diagnose(): diff --git a/plinth/modules/xmpp/__init__.py b/plinth/modules/xmpp/__init__.py index d0851c24a..1365f1d98 100644 --- a/plinth/modules/xmpp/__init__.py +++ b/plinth/modules/xmpp/__init__.py @@ -20,7 +20,8 @@ Plinth module to configure XMPP server """ from django.utils.translation import ugettext_lazy as _ -import json +import logging +import socket from plinth import actions from plinth import action_utils @@ -30,21 +31,35 @@ from plinth.signals import pre_hostname_change, post_hostname_change from plinth.signals import domainname_change -depends = ['plinth.modules.apps'] +version = 1 + +depends = ['apps'] + +title = _('Chat Server (XMPP)') + +description = [ + _('XMPP is an open and standardized communication protocol. Here ' + 'you can run and configure your XMPP server, called ejabberd.'), + + _('To actually communicate, you can use the web ' + 'client or any other ' + 'XMPP client.') +] service = None +logger = logging.getLogger(__name__) + def init(): """Initialize the XMPP module""" menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('Chat Server (XMPP)'), 'glyphicon-comment', - 'xmpp:index', 400) + menu.add_urlname(title, 'glyphicon-comment', 'xmpp:index', 400) global service service = service_module.Service( - 'xmpp', _('Chat Server (XMPP)'), - ['xmpp-client', 'xmpp-server', 'xmpp-bosh'], + 'xmpp', title, ['xmpp-client', 'xmpp-server', 'xmpp-bosh'], is_external=True, enabled=is_enabled()) pre_hostname_change.connect(on_pre_hostname_change) @@ -52,6 +67,18 @@ def init(): domainname_change.connect(on_domainname_change) +def setup(helper, old_version=None): + """Install and configure the module.""" + domainname = get_domainname() + logger.info('XMPP service domainname - %s', domainname) + + helper.call('pre', actions.superuser_run, 'xmpp', + ['pre-install', '--domainname', domainname]) + helper.install(['jwchat', 'ejabberd']) + helper.call('post', actions.superuser_run, 'xmpp', ['setup']) + helper.call('post', service.notify_enabled, None, True) + + def is_enabled(): """Return whether the module is enabled.""" return (action_utils.service_is_enabled('ejabberd') and @@ -63,6 +90,12 @@ def is_running(): return action_utils.service_is_running('ejabberd') +def get_domainname(): + """Return the domainname""" + fqdn = socket.getfqdn() + return '.'.join(fqdn.split('.')[1:]) + + def on_pre_hostname_change(sender, old_hostname, new_hostname, **kwargs): """ Backup ejabberd database before hostname is changed. diff --git a/plinth/modules/xmpp/templates/xmpp.html b/plinth/modules/xmpp/templates/xmpp.html index b0f3ac56b..31e387be2 100644 --- a/plinth/modules/xmpp/templates/xmpp.html +++ b/plinth/modules/xmpp/templates/xmpp.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "app.html" %} {% comment %} # # This file is part of Plinth. @@ -21,24 +21,7 @@ {% load bootstrap %} {% load i18n %} -{% block content %} - -

{% trans "Chat Server (XMPP)" %}

- -

- {% blocktrans trimmed %} - XMPP is an open and standardized communication protocol. Here - you can run and configure your XMPP server, called ejabberd. - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - To actually communicate, you can use the web - client or any other XMPP client. - {% endblocktrans %} -

+{% block configuration %}

{% url 'config:index' as index_url %} diff --git a/plinth/modules/xmpp/views.py b/plinth/modules/xmpp/views.py index 817417177..ca1841535 100644 --- a/plinth/modules/xmpp/views.py +++ b/plinth/modules/xmpp/views.py @@ -23,39 +23,15 @@ from django.contrib import messages from django.template.response import TemplateResponse from django.utils.translation import ugettext as _ import logging -import socket from .forms import XmppForm from plinth import actions -from plinth import package from plinth.modules import xmpp logger = logging.getLogger(__name__) -def get_domainname(): - """Return the domainname""" - fqdn = socket.getfqdn() - return '.'.join(fqdn.split('.')[1:]) - - -def before_install(): - """Preseed debconf values before the packages are installed.""" - domainname = get_domainname() - logger.info('XMPP service domainname - %s', domainname) - actions.superuser_run('xmpp', ['pre-install', '--domainname', domainname]) - - -def on_install(): - """Setup jwchat apache conf""" - actions.superuser_run('xmpp', ['setup']) - xmpp.service.notify_enabled(None, True) - - -@package.required(['jwchat', 'ejabberd'], - before_install=before_install, - on_install=on_install) def index(request): """Serve configuration page""" status = get_status() @@ -72,7 +48,8 @@ def index(request): form = XmppForm(initial=status, prefix='xmpp') return TemplateResponse(request, 'xmpp.html', - {'title': _('Chat Server (XMPP)'), + {'title': xmpp.title, + 'description': xmpp.description, 'status': status, 'form': form}) @@ -81,7 +58,7 @@ def get_status(): """Get the current settings.""" status = {'enabled': xmpp.is_enabled(), 'is_running': xmpp.is_running(), - 'domainname': get_domainname()} + 'domainname': xmpp.get_domainname()} return status diff --git a/plinth/package.py b/plinth/package.py index 75a8bfca0..8a46401ca 100644 --- a/plinth/package.py +++ b/plinth/package.py @@ -19,13 +19,9 @@ Framework for installing and updating distribution packages """ -from django.contrib import messages from django.utils.translation import ugettext as _ -import functools import logging -import threading -import plinth from plinth.utils import import_from_gi glib = import_from_gi('GLib', '2.0') packagekit = import_from_gi('PackageKitGlib', '1.0') @@ -46,19 +42,21 @@ class PackageException(Exception): self.error_string = error_string self.error_details = error_details + def __str__(self): + """Return the strin representation of the exception.""" + return 'PackageException(error_string="{0}", error_details="{1}")' \ + .format(self.error_string, self.error_details) + class Transaction(object): """Information about an ongoing transaction.""" - def __init__(self, package_names, before_install=None, on_install=None): + def __init__(self, package_names): """Initialize transaction object. Set most values to None until they are sent as progress update. """ self.package_names = package_names - # XXX: This is hack, remove after implementing proper setup mechanism. - self.before_install = before_install - self.on_install = on_install # Progress self.allow_cancel = None @@ -74,10 +72,6 @@ class Transaction(object): self.download_size_remaining = None self.speed = None - # Completion - self.is_finished = False - self.exception = None - def get_id(self): """Return a identifier to use as a key in a map of transactions.""" return frozenset(self.package_names) @@ -89,43 +83,12 @@ class Transaction(object): self.package_names, self.allow_cancel, self.status_string, self.percentage, self.package, self.item_progress) - def start_install(self): - """Start a PackageKit transaction to install given list of packages. - - This operation is non-blocking at it spawns a new thread. - """ - thread = threading.Thread(target=self._install) - thread.start() - - def _install(self): + def install(self): """Run a PackageKit transaction to install given packages.""" - try: - if self.before_install: - self.before_install() - except Exception as exception: - logger.exception('Error during setup before install - %s', - exception) - self.finish(exception) - return - try: self._do_install() - except PackageException as exception: - self.finish(exception) - return except glib.Error as exception: - self.finish(PackageException(exception.message)) - return - - try: - if self.on_install: - self.on_install() - except Exception as exception: - logger.exception('Error during setup - %s', exception) - self.finish(exception) - return - - self.finish() + raise PackageException(exception.message) from exception def _do_install(self): """Run a PackageKit transaction to install given packages. @@ -203,115 +166,3 @@ class Transaction(object): else: logger.info('Unhandle packagekit progress callback - %s, %s', progress, progress_type) - - def finish(self, exception=None): - """Mark transaction as complected and store exception if any.""" - self.is_finished = True - self.exception = exception - - def collect_result(self): - """Retrieve the result of this transaction. - - Also remove self from global transactions list. - """ - assert self.is_finished - - del transactions[self.get_id()] - return self.exception - - -def required(package_names, before_install=None, on_install=None): - """Decorate a view to check and install required packages.""" - - def wrapper2(func): - """Return a function to check and install packages.""" - - @functools.wraps(func) - def wrapper(request, *args, **kwargs): - """Check and install packages required by a view.""" - if not _should_show_install_view(request, package_names): - return func(request, *args, **kwargs) - - view = plinth.views.PackageInstallView.as_view() - return view(request, package_names=package_names, - before_install=before_install, on_install=on_install, - *args, **kwargs) - - return wrapper - - return wrapper2 - - -def _should_show_install_view(request, package_names): - """Return whether the installation view should be shown.""" - transaction_id = frozenset(package_names) - - # No transaction in progress - if transaction_id not in transactions: - is_installed = check_installed(package_names) - return not is_installed - - # Installing - transaction = transactions[transaction_id] - if not transaction.is_finished: - return True - - # Transaction finished, waiting to show the result - exception = transaction.collect_result() - if not exception: - messages.success(request, - _('Installed and configured packages successfully.')) - return False - else: - error_string = getattr(exception, 'error_string', str(exception)) - error_details = getattr(exception, 'error_details', '') - messages.error(request, _('Error installing packages: {string} {details}') - .format(string=error_string, details=error_details)) - return True - - -def check_installed(package_names): - """Return a boolean installed status of package. - - This operation is blocking and waits until the check is finished. - """ - def _callback(progress, progress_type, user_data): - """Process progress updates on package resolve operation.""" - pass - - client = packagekit.Client() - response = client.resolve(packagekit.FilterEnum.INSTALLED, - tuple(package_names) + (None, ), None, - _callback, None) - - installed_package_names = [] - for package in response.get_package_array(): - if package.get_info() == packagekit.InfoEnum.INSTALLED: - installed_package_names.append(package.get_name()) - - packages_resolved[package.get_name()] = package - - # When package names could not be resolved - for package_name in package_names: - if package_name not in packages_resolved: - packages_resolved[package_name] = None - - return set(installed_package_names) == set(package_names) - - -def is_installing(package_names): - """Return whether a set of packages are currently being installed.""" - return frozenset(package_names) in transactions - - -def start_install(package_names, before_install=None, on_install=None): - """Start a PackageKit transaction to install given list of packages. - - This operation is non-blocking at it spawns a new thread. - """ - transaction = Transaction(package_names, - before_install=before_install, - on_install=on_install) - transactions[frozenset(package_names)] = transaction - - transaction.start_install() diff --git a/plinth/setup.py b/plinth/setup.py new file mode 100644 index 000000000..c8fd747d2 --- /dev/null +++ b/plinth/setup.py @@ -0,0 +1,160 @@ +# +# This file is part of Plinth. +# +# 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 . +# + +""" +Plinth module with utilites for performing application setup operations. +""" + +import logging +import threading + +from . import package +import plinth + +logger = logging.getLogger(__name__) + + +class Helper(object): + """Helper routines for modules to show progress.""" + + def __init__(self, module_name, module): + """Initialize the object.""" + self.module_name = module_name + self.module = module + self.current_operation = None + self.is_finished = None + self.exception = None + + def run_in_thread(self): + """Execute the setup process in a thread.""" + thread = threading.Thread(target=self._run) + thread.start() + + def _run(self): + """Collect exceptions when running in a thread.""" + try: + self.run() + except Exception as exception: + self.exception = exception + + def collect_result(self): + """Return the exception if any.""" + exception = self.exception + self.exception = None + self.is_finished = None + return exception + + def run(self): + """Execute the setup process.""" + # Setup for the module is already running + if self.current_operation: + return + + current_version = self.get_setup_version() + if current_version >= self.module.version: + return + + self.exception = None + self.current_operation = None + self.is_finished = False + try: + if hasattr(self.module, 'setup'): + logger.info('Running module setup - %s', self.module_name) + self.module.setup(self, old_version=current_version) + else: + logger.info('Module does not require setup - %s', + self.module_name) + except Exception as exception: + logger.exception('Error running setup - %s', exception) + raise exception + else: + self.set_setup_version(self.module.version) + finally: + self.is_finished = True + self.current_operation = None + + def install(self, package_names): + """Install a set of packages marking progress.""" + logger.info('Running install for module - %s, packages - %s', + self.module_name, package_names) + transaction = package.Transaction(package_names) + self.current_operation = { + 'step': 'install', + 'transaction': transaction, + } + + transaction.install() + + def call(self, step, method, *args, **kwargs): + """Call an arbitrary method during setup and note down its stage.""" + logger.info('Running step for module - %s, step - %s', + self.module_name, step) + self.current_operation = {'step': step} + return method(*args, **kwargs) + + def get_state(self): + """Return whether the module is not setup or needs upgrade.""" + current_version = self.get_setup_version() + if current_version and self.module.version <= current_version: + return 'up-to-date' + + # If a module need installing/updating but no setup method is + # available, then automatically set version. + # + # Minor violation of 'get' only discipline for convenience. + if not hasattr(self.module, 'setup'): + self.set_setup_version(self.module.version) + return 'up-to-date' + + if not current_version: + return 'needs-setup' + else: + return 'needs-update' + + def get_setup_version(self): + """Return the setup version of a module.""" + # XXX: Optimize version gets + from . import models + + try: + module_entry = models.Module.objects.get(pk=self.module_name) + return module_entry.setup_version + except models.Module.DoesNotExist: + return 0 + + def set_setup_version(self, version): + """Set a module's setup version.""" + from . import models + + models.Module.objects.update_or_create( + pk=self.module_name, defaults={'setup_version': version}) + + +def init(module_name, module): + """Create a setup helper for a module for later use.""" + if not hasattr(module, 'setup_helper'): + module.setup_helper = Helper(module_name, module) + + +def setup_all_modules(essential=False): + """Run setup on all essential modules and exit.""" + logger.info('Running setup for all modules, essential - %s', essential) + for module_name, module in plinth.module_loader.loaded_modules.items(): + if essential and not getattr(module, 'is_essential', False): + continue + + module.setup_helper.run() diff --git a/plinth/templates/app.html b/plinth/templates/app.html new file mode 100644 index 000000000..023171c08 --- /dev/null +++ b/plinth/templates/app.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} +{% comment %} +# +# This file is part of Plinth. +# +# 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 . +# +{% endcomment %} + +{% load i18n %} + +{% block content %} +

{{ title }}

+ + {% for paragraph in description %} +

{{ paragraph|safe }}

+ {% endfor %} + + {% block configuration %} + {% endblock %} + +{% endblock %} diff --git a/plinth/templates/package_install.html b/plinth/templates/package_install.html deleted file mode 100644 index 7df7bb8c6..000000000 --- a/plinth/templates/package_install.html +++ /dev/null @@ -1,94 +0,0 @@ -{% extends "base.html" %} -{% comment %} -# -# This file is part of Plinth. -# -# 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 . -# -{% endcomment %} - -{% load bootstrap %} -{% load i18n %} - -{% block page_head %} - - {% if is_installing %} - - {% endif %} - -{% endblock %} - - -{% block content %} - -

{% trans "Installation" %}

- - {% if not is_installing %} - -

- {% blocktrans trimmed %} - This feature requires addtional packages to be installed. Do - you wish to install them? - {% endblocktrans %} -

- - - - - - - - - - {% for package_name, package in packages.items %} - - - - - {% endfor %} - -
{% trans "Package" %}{% trans "Summary" %}
{{ package_name }}{{ package.get_summary }}
- - - {% csrf_token %} - - - - - {% else %} - - {% for key, transaction in transactions.items %} -
- {% blocktrans trimmed with package_names=transaction.package_names|join:", " status=transaction.status_string %} - Installing {{ package_names }}: {{ status }} - {% endblocktrans %} -
-
-
- - {% blocktrans trimmed with percentage=transaction.percentage %} - {{ percentage }}% complete - {% endblocktrans %} - -
-
- {% endfor %} - - {% endif %} - -{% endblock %} diff --git a/plinth/templates/setup.html b/plinth/templates/setup.html new file mode 100644 index 000000000..4cd25ee0f --- /dev/null +++ b/plinth/templates/setup.html @@ -0,0 +1,101 @@ +{% extends "base.html" %} +{% comment %} +# +# This file is part of Plinth. +# +# 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 . +# +{% endcomment %} + +{% load bootstrap %} +{% load i18n %} + +{% block page_head %} + + {% if setup_helper.current_operation %} + + {% endif %} + +{% endblock %} + + +{% block content %} + +

{% trans "Installation" %}: {{ setup_helper.module.title }}

+ + {% for paragraph in setup_helper.module.description %} +

{{ paragraph|safe }}

+ {% endfor %} + + {% if not setup_helper.current_operation %} + + {% if setup_helper.get_state == 'needs-setup' %} +

+ {% blocktrans trimmed %} + Install this application? + {% endblocktrans %} +

+ +
+ {% csrf_token %} + + +
+ {% elif setup_helper.get_state == 'needs-update' %} +

+ {% blocktrans trimmed %} + This application needs an update. Update now? + {% endblocktrans %} +

+ +
+ {% csrf_token %} + + +
+ {% endif %} + + {% else %} + + {% if setup_helper.current_operation.step == 'pre' %} + {% trans "Performing pre-install operation" %} + {% elif setup_helper.current_operation.step == 'post' %} + {% trans "Performing post-install operation" %} + {% elif setup_helper.current_operation.step == 'install' %} + {% with transaction=setup_helper.current_operation.transaction %} +
+ {% blocktrans trimmed with package_names=transaction.package_names|join:", " status=transaction.status_string %} + Installing {{ package_names }}: {{ status }} + {% endblocktrans %} +
+
+
+ + {% blocktrans trimmed with percentage=transaction.percentage %} + {{ percentage }}% complete + {% endblocktrans %} + +
+
+ {% endwith %} + {% endif %} + + {% endif %} + +{% endblock %} diff --git a/plinth/urls.py b/plinth/urls.py index 5ab1c866e..44be0a509 100644 --- a/plinth/urls.py +++ b/plinth/urls.py @@ -19,10 +19,9 @@ Django URLconf file containing all urls """ -from django.conf.urls import patterns, url +from django.conf.urls import url +from . import views - -urlpatterns = patterns( # pylint: disable-msg=C0103 - 'plinth.views', - url(r'^$', 'index', name='index') -) +urlpatterns = [ + url(r'^$', views.index, name='index') +] diff --git a/plinth/views.py b/plinth/views.py index cfbbd608b..a5b9a7680 100644 --- a/plinth/views.py +++ b/plinth/views.py @@ -22,8 +22,7 @@ Main Plinth views from django.core.urlresolvers import reverse from django.http.response import HttpResponseRedirect from django.views.generic import TemplateView - -from plinth import package as package_module +import time def index(request): @@ -31,33 +30,26 @@ def index(request): return HttpResponseRedirect(reverse('apps:index')) -class PackageInstallView(TemplateView): - """View to prompt and install packages.""" - template_name = 'package_install.html' +class SetupView(TemplateView): + """View to prompt and setup applications.""" + template_name = 'setup.html' def get_context_data(self, **kwargs): """Return the context data rendering the template.""" - context = super(PackageInstallView, self).get_context_data(**kwargs) - - if 'packages_names' not in context: - context['package_names'] = self.kwargs.get('package_names', []) - context['packages'] = { - package_name: package_module.packages_resolved[package_name] - for package_name in context['package_names']} - context['is_installing'] = \ - package_module.is_installing(context['package_names']) - context['transactions'] = package_module.transactions - + context = super(SetupView, self).get_context_data(**kwargs) + context['setup_helper'] = self.kwargs['setup_helper'] return context def post(self, *args, **kwargs): - """Handle installing packages + """Handle installing/upgrading applications. - Start the package installation, and refresh the page every x seconds to - keep displaying PackageInstallView.get() with the installation status. + Start the application setup, and refresh the page every few + seconds to keep displaying the status. """ - package_module.start_install( - self.kwargs['package_names'], - before_install=self.kwargs.get('before_install'), - on_install=self.kwargs.get('on_install')) + self.kwargs['setup_helper'].run_in_thread() + + # Give a moment for the setup process to start and show + # meaningful status. + time.sleep(1) + return self.render_to_response(self.get_context_data())