From ab80dc34d6374e99528a52b372d2910ae7ecc6ff Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 19 Jun 2014 17:50:41 +0200 Subject: [PATCH 1/8] Add option to run as non-daemon mode - Pass on debug option with Django - Cleanup option parsing --- cfg.py | 2 ++ logger.py | 10 +++++-- plinth.py | 88 +++++++++++++++++++++++-------------------------------- 3 files changed, 46 insertions(+), 54 deletions(-) diff --git a/cfg.py b/cfg.py index 124bb0ebc..deb3f943a 100644 --- a/cfg.py +++ b/cfg.py @@ -31,6 +31,8 @@ access_log_file = get_item(parser, 'Path', 'access_log_file') pidfile = get_item(parser, 'Path', 'pidfile') host = get_item(parser, 'Network', 'host') port = int(get_item(parser, 'Network', 'port')) +debug = False +no_daemon = False main_menu = Menu() diff --git a/logger.py b/logger.py index 9a753948e..581255ebe 100644 --- a/logger.py +++ b/logger.py @@ -2,9 +2,13 @@ import cherrypy import inspect import cfg -cherrypy.log.error_file = cfg.status_log_file -cherrypy.log.access_file = cfg.access_log_file -cherrypy.log.screen = False + +def init(): + """Initialize logging""" + cherrypy.log.error_file = cfg.status_log_file + cherrypy.log.access_file = cfg.access_log_file + if not cfg.no_daemon: + cherrypy.log.screen = False class Logger(object): diff --git a/plinth.py b/plinth.py index ddcce62f0..35234e8b5 100755 --- a/plinth.py +++ b/plinth.py @@ -10,12 +10,12 @@ if not os.path.join(cfg.file_root, "vendor") in sys.path: import cherrypy from cherrypy import _cpserver from cherrypy.process.plugins import Daemonizer -Daemonizer(cherrypy.engine).subscribe() import module_loader import plugin_mount import service +import logger from logger import Logger __version__ = "0.2.14" @@ -28,42 +28,27 @@ __status__ = "Development" def parse_arguments(): - parser = argparse.ArgumentParser(description='Plinth web interface for the FreedomBox.') - parser.add_argument('--pidfile', - help='specify a file in which the server may write its pid') - # FIXME make this work with basehref for static files. - parser.add_argument('--server_dir', - help='specify where to host the server.') - parser.add_argument("--debug", action="store_true", - help="Debug flag. Don't use.") + """Parse command line arguments""" + parser = argparse.ArgumentParser( + description='Plinth web interface for FreedomBox') + parser.add_argument( + '--pidfile', default='plinth.pid', + help='specify a file in which the server may write its pid') + parser.add_argument( + '--server_dir', default='/', + help='web server path under which to serve') + parser.add_argument( + '--debug', action='store_true', default=False, + help='enable debugging and run server *insecurely*') + parser.add_argument( + '--no-daemon', action='store_true', default=False, + help='do not start as a daemon') - args=parser.parse_args() - set_config(args, "pidfile", "plinth.pid") - set_config(args, "server_dir", "/") - set_config(args, "debug", False) - - return cfg - -def set_config(args, element, default): - """Sets *cfg* elements based on *args* values, or uses a reasonable default. - - - If values are passed in from *args*, use those. - - If values are read from the config file, use those. - - If no values have been given, use default. - - """ - try: - # cfg.(element) = args.(element) - setattr(cfg, element, getattr(args, element)) - except AttributeError: - # if it fails, we didn't receive that argument. - try: - # if not cfg.(element): cfg.(element) = default - if not getattr(cfg, element): - setattr(cfg, element, default) - except AttributeError: - # it wasn't in the config file, but set the default anyway. - setattr(cfg, element, default) + args = parser.parse_args() + cfg.pidfile = args.pidfile + cfg.server_dir = args.server_dir + cfg.debug = args.debug + cfg.no_daemon = args.no_daemon def setup_logging(): @@ -71,21 +56,16 @@ def setup_logging(): cfg.log = Logger() -def setup_configuration(): - cfg = parse_arguments() - - try: - if cfg.pidfile: - from cherrypy.process.plugins import PIDFile - PIDFile(cherrypy.engine, cfg.pidfile).subscribe() - except AttributeError: - pass - - os.chdir(cfg.python_root) - - def setup_server(): """Setup CherryPy server""" + # Set the PID file path + try: + if cfg.pidfile: + from cherrypy.process.plugins import PIDFile + PIDFile(cherrypy.engine, cfg.pidfile).subscribe() + except AttributeError: + pass + # Add an extra server server = _cpserver.Server() server.socket_host = '127.0.0.1' @@ -107,6 +87,9 @@ def setup_server(): 'tools.staticdir.dir': '.'}} cherrypy.tree.mount(None, cfg.server_dir + '/static', config) + if not cfg.no_daemon: + Daemonizer(cherrypy.engine).subscribe() + cherrypy.engine.signal_handler.subscribe() @@ -141,7 +124,7 @@ def configure_django(): template_directories = module_loader.get_template_directories() sessions_directory = os.path.join(cfg.data_dir, 'sessions') django.conf.settings.configure( - DEBUG=False, + DEBUG=cfg.debug, ALLOWED_HOSTS=['127.0.0.1', 'localhost'], TEMPLATE_DIRS=template_directories, INSTALLED_APPS=['bootstrapform'], @@ -154,11 +137,14 @@ def configure_django(): def main(): """Intialize and start the application""" + parse_arguments() + setup_logging() + logger.init() service.init() - setup_configuration() + os.chdir(cfg.python_root) configure_django() From 78406f16e853ca8f1809741564fe7d93ac60ad38 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sat, 21 Jun 2014 11:44:21 +0200 Subject: [PATCH 2/8] Refactor global code from plinth.py --- plinth.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/plinth.py b/plinth.py index 35234e8b5..2f1521396 100755 --- a/plinth.py +++ b/plinth.py @@ -1,11 +1,11 @@ #!/usr/bin/env python -import os, sys, argparse +import argparse +import os +import sys import cfg import django.conf import django.core.wsgi -if not os.path.join(cfg.file_root, "vendor") in sys.path: - sys.path.append(os.path.join(cfg.file_root, "vendor")) import cherrypy from cherrypy import _cpserver @@ -54,6 +54,14 @@ def parse_arguments(): def setup_logging(): """Setup logging framework""" cfg.log = Logger() + logger.init() + + +def setup_paths(): + """Setup current directory and python import paths""" + os.chdir(cfg.python_root) + if not os.path.join(cfg.file_root, 'vendor') in sys.path: + sys.path.append(os.path.join(cfg.file_root, 'vendor')) def setup_server(): @@ -140,11 +148,10 @@ def main(): parse_arguments() setup_logging() - logger.init() service.init() - os.chdir(cfg.python_root) + setup_paths() configure_django() From fc37293ac66ae8e18c51eae32863ac134a89cdda Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sat, 21 Jun 2014 11:44:43 +0200 Subject: [PATCH 3/8] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index a30ed9e0c..1cbb5d668 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ current-*.tar.gz *.tiny.css data/*.log data/cherrypy_sessions +data/sessions data/store.sqlite3 doc/*.tex doc/*.pdf @@ -27,3 +28,4 @@ data/users.sqlite3 predepend build/ *.pid +.emacs.desktop* \ No newline at end of file From b76a74e684dd6e41197fd41ac15dd77814519ee2 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sun, 22 Jun 2014 11:49:39 +0200 Subject: [PATCH 4/8] Use Django messages module for showing messages --- modules/config/config.py | 21 +++++----- modules/config/templates/config.html | 2 - modules/expert_mode/expert_mode.py | 17 ++++---- .../expert_mode/templates/expert_mode.html | 2 - modules/first_boot/first_boot.py | 15 ++++--- .../templates/firstboot_state0.html | 2 - modules/owncloud/owncloud.py | 15 ++++--- modules/owncloud/templates/owncloud.html | 2 - modules/packages/packages.py | 34 ++++++++------- modules/packages/templates/packages.html | 2 - modules/pagekite/pagekite.py | 24 +++++------ .../templates/pagekite_configure.html | 2 - modules/users/templates/users_add.html | 2 - modules/users/templates/users_edit.html | 2 - modules/users/users.py | 41 ++++++++----------- modules/xmpp/templates/xmpp_configure.html | 2 - modules/xmpp/templates/xmpp_register.html | 2 - modules/xmpp/xmpp.py | 30 ++++++-------- plinth.py | 3 +- templates/base.html | 3 ++ templates/messages.html | 4 +- 21 files changed, 96 insertions(+), 131 deletions(-) diff --git a/modules/config/config.py b/modules/config/config.py index fe87dee70..81f1688a2 100644 --- a/modules/config/config.py +++ b/modules/config/config.py @@ -20,6 +20,7 @@ Plinth module for configuring timezone, hostname etc. """ from django import forms +from django.contrib import messages from django.core import validators from django.template.response import TemplateResponse from gettext import gettext as _ @@ -100,14 +101,13 @@ def index(request): status = get_status() form = None - messages = [] is_expert = cfg.users.expert(request=request) if request.method == 'POST' and is_expert: form = ConfigurationForm(request.POST, prefix='configuration') # pylint: disable-msg=E1101 if form.is_valid(): - _apply_changes(status, form.cleaned_data, messages) + _apply_changes(request, status, form.cleaned_data) status = get_status() form = ConfigurationForm(initial=status, prefix='configuration') @@ -117,7 +117,6 @@ def index(request): return TemplateResponse(request, 'config.html', {'title': _('General Configuration'), 'form': form, - 'messages_': messages, 'is_expert': is_expert}) @@ -127,27 +126,27 @@ def get_status(): 'time_zone': util.slurp('/etc/timezone').rstrip()} -def _apply_changes(old_status, new_status, messages): +def _apply_changes(request, old_status, new_status): """Apply the form changes""" if old_status['hostname'] != new_status['hostname']: if not set_hostname(new_status['hostname']): - messages.append(('error', _('Setting hostname failed'))) + messages.error(request, _('Setting hostname failed')) else: - messages.append(('success', _('Hostname set'))) + messages.success(request, _('Hostname set')) else: - messages.append(('info', _('Hostname is unchanged'))) + messages.info(request, _('Hostname is unchanged')) if old_status['time_zone'] != new_status['time_zone']: output, error = actions.superuser_run('timezone-change', [new_status['time_zone']]) del output # Unused if error: - messages.append(('error', - _('Error setting time zone - %s') % error)) + messages.error(request, + _('Error setting time zone - %s') % error) else: - messages.append(('success', _('Time zone set'))) + messages.success(request, _('Time zone set')) else: - messages.append(('info', _('Time zone is unchanged'))) + messages.info(request, _('Time zone is unchanged')) def set_hostname(hostname): diff --git a/modules/config/templates/config.html b/modules/config/templates/config.html index 591ad5e5b..4401bdcd3 100644 --- a/modules/config/templates/config.html +++ b/modules/config/templates/config.html @@ -24,8 +24,6 @@ {% if is_expert %} - {% include 'messages.html' %} -
{% csrf_token %} diff --git a/modules/expert_mode/expert_mode.py b/modules/expert_mode/expert_mode.py index 389aa8fe0..e4401d7bd 100644 --- a/modules/expert_mode/expert_mode.py +++ b/modules/expert_mode/expert_mode.py @@ -1,4 +1,5 @@ from django import forms +from django.contrib import messages from django.template.response import TemplateResponse from gettext import gettext as _ @@ -24,13 +25,12 @@ def index(request): status = get_status(request) form = None - messages = [] if request.method == 'POST': form = ExpertsForm(request.POST, prefix='experts') # pylint: disable-msg=E1101 if form.is_valid(): - _apply_changes(request, form.cleaned_data, messages) + _apply_changes(request, form.cleaned_data) status = get_status(request) form = ExpertsForm(initial=status, prefix='experts') else: @@ -38,8 +38,7 @@ def index(request): return TemplateResponse(request, 'expert_mode.html', {'title': _('Expert Mode'), - 'form': form, - 'messages_': messages}) + 'form': form}) def get_status(request): @@ -47,20 +46,20 @@ def get_status(request): return {'expert_mode': cfg.users.expert(request=request)} -def _apply_changes(request, new_status, messages): +def _apply_changes(request, new_status): """Apply expert mode configuration""" - message = ('info', _('Settings unchanged')) + message = (messages.info, _('Settings unchanged')) user = cfg.users.current(request=request) if new_status['expert_mode']: if not 'expert' in user['groups']: user['groups'].append('expert') - message = ('success', _('Expert mode enabled')) + message = (messages.success, _('Expert mode enabled')) else: if 'expert' in user['groups']: user['groups'].remove('expert') - message = ('success', _('Expert mode disabled')) + message = (messages.success, _('Expert mode disabled')) cfg.users.set(user['username'], user) - messages.append(message) + message[0](request, message[1]) diff --git a/modules/expert_mode/templates/expert_mode.html b/modules/expert_mode/templates/expert_mode.html index c6df98b36..0db5565ab 100644 --- a/modules/expert_mode/templates/expert_mode.html +++ b/modules/expert_mode/templates/expert_mode.html @@ -22,8 +22,6 @@ {% block main_block %} - {% include 'messages.html' %} -

The {{ cfg.box_name }} can be administered in two modes, 'basic' and 'expert'. Basic mode hides a lot of features and configuration options that most users will never need to think about. Expert mode diff --git a/modules/first_boot/first_boot.py b/modules/first_boot/first_boot.py index d38f042a5..c9ecf5ff9 100644 --- a/modules/first_boot/first_boot.py +++ b/modules/first_boot/first_boot.py @@ -19,6 +19,7 @@ The Plinth first-connection process has several stages: """ from django import forms +from django.contrib import messages from django.core import validators from django.http.response import HttpResponseRedirect from django.template.response import TemplateResponse @@ -101,13 +102,12 @@ def state0(request): status = get_state0() form = None - messages = [] if request.method == 'POST': form = State0Form(request.POST, prefix='firstboot') # pylint: disable-msg=E1101 if form.is_valid(): - success = _apply_state0(status, form.cleaned_data, messages) + success = _apply_state0(request, status, form.cleaned_data) if success: # Everything is good, permanently mark and move to page 2 @@ -119,8 +119,7 @@ def state0(request): return TemplateResponse(request, 'firstboot_state0.html', {'title': _('First Boot!'), - 'form': form, - 'messages_': messages}) + 'form': form}) def get_state0(): @@ -131,7 +130,7 @@ def get_state0(): 'box_key': database.get('box_key', None)} -def _apply_state0(old_state, new_state, messages): +def _apply_state0(request, old_state, new_state): """Apply changes in state 0 form""" success = True with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as \ @@ -149,11 +148,11 @@ def _apply_state0(old_state, new_state, messages): error = add_user(new_state['username'], new_state['password'], 'First user, please change', '', True) if error: - messages.append( - ('error', _('User account creation failed: %s') % error)) + messages.error( + request, _('User account creation failed: %s') % error) success = False else: - messages.append(('success', _('User account created'))) + messages.success(request, _('User account created')) return success diff --git a/modules/first_boot/templates/firstboot_state0.html b/modules/first_boot/templates/firstboot_state0.html index 656fe406f..02d47cde6 100644 --- a/modules/first_boot/templates/firstboot_state0.html +++ b/modules/first_boot/templates/firstboot_state0.html @@ -24,8 +24,6 @@

Welcome to Your FreedomBox!

- {% include 'messages.html' %} -

Welcome. It looks like this FreedomBox isn't set up yet. We'll need to ask you a just few questions to get started.

diff --git a/modules/owncloud/owncloud.py b/modules/owncloud/owncloud.py index f2b1afff7..153c13e2a 100644 --- a/modules/owncloud/owncloud.py +++ b/modules/owncloud/owncloud.py @@ -1,4 +1,5 @@ from django import forms +from django.contrib import messages from django.template.response import TemplateResponse from gettext import gettext as _ @@ -34,13 +35,12 @@ def index(request): status = get_status() form = None - messages = [] if request.method == 'POST': form = OwnCloudForm(request.POST, prefix='owncloud') # pylint: disable-msg=E1101 if form.is_valid(): - _apply_changes(status, form.cleaned_data, messages) + _apply_changes(request, status, form.cleaned_data) status = get_status() form = OwnCloudForm(initial=status, prefix='owncloud') else: @@ -48,8 +48,7 @@ def index(request): return TemplateResponse(request, 'owncloud.html', {'title': _('ownCloud'), - 'form': form, - 'messages_': messages}) + 'form': form}) def get_status(): @@ -61,17 +60,17 @@ def get_status(): return {'enabled': 'enable' in output.split()} -def _apply_changes(old_status, new_status, messages): +def _apply_changes(request, old_status, new_status): """Apply the changes""" if old_status['enabled'] == new_status['enabled']: - messages.append(('info', _('Setting unchanged'))) + messages.info(request, _('Setting unchanged')) return if new_status['enabled']: - messages.append(('success', _('ownCloud enabled'))) + messages.success(request, _('ownCloud enabled')) option = 'enable' else: - messages.append(('success', _('ownCloud disabled'))) + messages.success(request, _('ownCloud disabled')) option = 'noenable' actions.superuser_run('owncloud-setup', [option], async=True) diff --git a/modules/owncloud/templates/owncloud.html b/modules/owncloud/templates/owncloud.html index 1e7efebd0..303846327 100644 --- a/modules/owncloud/templates/owncloud.html +++ b/modules/owncloud/templates/owncloud.html @@ -22,8 +22,6 @@ {% block main_block %} - {% include 'messages.html' %} - {% csrf_token %} diff --git a/modules/packages/packages.py b/modules/packages/packages.py index 8db985c85..8776da9da 100644 --- a/modules/packages/packages.py +++ b/modules/packages/packages.py @@ -1,4 +1,5 @@ from django import forms +from django.contrib import messages from django.template.response import TemplateResponse from gettext import gettext as _ @@ -51,13 +52,12 @@ def index(request): status = get_status() form = None - messages = [] if request.method == 'POST': form = PackagesForm(request.POST, prefix='packages') # pylint: disable-msg=E1101 if form.is_valid(): - _apply_changes(status, form.cleaned_data, messages) + _apply_changes(request, status, form.cleaned_data) status = get_status() form = PackagesForm(initial=status, prefix='packages') else: @@ -65,8 +65,7 @@ def index(request): return TemplateResponse(request, 'packages.html', {'title': _('Add/Remove Plugins'), - 'form': form, - 'messages_': messages}) + 'form': form}) def get_status(): @@ -78,7 +77,7 @@ def get_status(): for module in modules_available} -def _apply_changes(old_status, new_status, messages): +def _apply_changes(request, old_status, new_status): """Apply form changes""" for field, enabled in new_status.items(): if not field.endswith('_enabled'): @@ -96,13 +95,13 @@ def _apply_changes(old_status, new_status, messages): # TODO: need to get plinth to load the module we just # enabled if error: - messages.append( - ('error', _('Error enabling module - {module}').format( - module=module))) + messages.error( + request, _('Error enabling module - {module}').format( + module=module)) else: - messages.append( - ('success', _('Module enabled - {module}').format( - module=module))) + messages.success( + request, _('Module enabled - {module}').format( + module=module)) else: output, error = actions.superuser_run( 'module-manager', ['disable', cfg.python_root, module]) @@ -111,11 +110,10 @@ def _apply_changes(old_status, new_status, messages): # TODO: need a smoother way for plinth to unload the # module if error: - messages.append( - ('error', - _('Error disabling module - {module}').format( - module=module))) + messages.error( + request, _('Error disabling module - {module}').format( + module=module)) else: - messages.append( - ('success', _('Module disabled - {module}').format( - module=module))) + messages.success( + request, _('Module disabled - {module}').format( + module=module)) diff --git a/modules/packages/templates/packages.html b/modules/packages/templates/packages.html index d8d7d6164..d6eb85e22 100644 --- a/modules/packages/templates/packages.html +++ b/modules/packages/templates/packages.html @@ -22,8 +22,6 @@ {% block main_block %} - {% include 'messages.html' %} -

aptitude purge modules

aptitude install modules

diff --git a/modules/pagekite/pagekite.py b/modules/pagekite/pagekite.py index 8ad48587b..0f1329973 100644 --- a/modules/pagekite/pagekite.py +++ b/modules/pagekite/pagekite.py @@ -20,6 +20,7 @@ Plinth module for configuring PageKite service """ from django import forms +from django.contrib import messages from django.core import validators from django.template import RequestContext from django.template.loader import render_to_string @@ -104,13 +105,12 @@ def configure(request): status = get_status() form = None - messages = [] if request.method == 'POST': form = ConfigureForm(request.POST, prefix='pagekite') # pylint: disable-msg=E1101 if form.is_valid(): - _apply_changes(status, form.cleaned_data, messages) + _apply_changes(request, status, form.cleaned_data) status = get_status() form = ConfigureForm(initial=status, prefix='pagekite') else: @@ -118,8 +118,7 @@ def configure(request): return TemplateResponse(request, 'pagekite_configure.html', {'title': _('Configure PageKite'), - 'form': form, - 'messages_': messages}) + 'form': form}) def get_status(): @@ -154,7 +153,7 @@ def get_status(): return status -def _apply_changes(old_status, new_status, messages): +def _apply_changes(request, old_status, new_status): """Apply the changes to PageKite configuration""" cfg.log.info('New status is - %s' % new_status) @@ -164,29 +163,28 @@ def _apply_changes(old_status, new_status, messages): if old_status['enabled'] != new_status['enabled']: if new_status['enabled']: _run(['set-status', 'enable']) - messages.append(('success', _('PageKite enabled'))) + messages.success(request, _('PageKite enabled')) else: _run(['set-status', 'disable']) - messages.append(('success', _('PageKite disabled'))) + messages.success(request, _('PageKite disabled')) if old_status['kite_name'] != new_status['kite_name'] or \ old_status['kite_secret'] != new_status['kite_secret']: _run(['set-kite', '--kite-name', new_status['kite_name'], '--kite-secret', new_status['kite_secret']]) - messages.append(('success', _('Kite details set'))) + messages.success(request, _('Kite details set')) for service in ['http', 'ssh']: if old_status[service + '_enabled'] != \ new_status[service + '_enabled']: if new_status[service + '_enabled']: _run(['set-service-status', service, 'enable']) - messages.append(('success', _('Service enabled: {service}') - .format(service=service))) + messages.success(request, _('Service enabled: {service}') + .format(service=service)) else: _run(['set-service-status', service, 'disable']) - messages.append(('success', - _('Service disabled: {service}') - .format(service=service))) + messages.success(request, _('Service disabled: {service}') + .format(service=service)) if old_status != new_status: _run(['start']) diff --git a/modules/pagekite/templates/pagekite_configure.html b/modules/pagekite/templates/pagekite_configure.html index dd31c47de..9bcc25fe1 100644 --- a/modules/pagekite/templates/pagekite_configure.html +++ b/modules/pagekite/templates/pagekite_configure.html @@ -31,8 +31,6 @@ {% else %} - {% include 'messages.html' %} - {% csrf_token %} diff --git a/modules/users/templates/users_add.html b/modules/users/templates/users_add.html index 4afbc68c9..3df856188 100644 --- a/modules/users/templates/users_add.html +++ b/modules/users/templates/users_add.html @@ -22,8 +22,6 @@ {% block main_block %} - {% include 'messages.html' %} - {% csrf_token %} diff --git a/modules/users/templates/users_edit.html b/modules/users/templates/users_edit.html index da1ef4136..82905b6b9 100644 --- a/modules/users/templates/users_edit.html +++ b/modules/users/templates/users_edit.html @@ -22,8 +22,6 @@ {% block main_block %} - {% include 'messages.html' %} - {% csrf_token %} diff --git a/modules/users/users.py b/modules/users/users.py index 0103c7c53..8c4f5081b 100644 --- a/modules/users/users.py +++ b/modules/users/users.py @@ -1,4 +1,5 @@ from django import forms +from django.contrib import messages from django.core import validators from django.template import RequestContext from django.template.loader import render_to_string @@ -54,36 +55,32 @@ and alphabet'), def add(request): """Serve the form""" form = None - messages = [] if request.method == 'POST': form = UserAddForm(request.POST, prefix='user') # pylint: disable-msg=E1101 if form.is_valid(): - _add_user(form.cleaned_data, messages) + _add_user(request, form.cleaned_data) form = UserAddForm(prefix='user') else: form = UserAddForm(prefix='user') return TemplateResponse(request, 'users_add.html', {'title': _('Add User'), - 'form': form, - 'messages_': messages}) + 'form': form}) -def _add_user(data, messages): +def _add_user(request, data): """Add a user""" if cfg.users.exists(data['username']): - messages.append( - ('error', _('User "{username}" already exists').format( - username=data['username']))) + messages.error(request, _('User "{username}" already exists').format( + username=data['username'])) return add_user(data['username'], data['password'], data['full_name'], data['email'], False) - messages.append( - ('success', _('User "{username}" added').format( - username=data['username']))) + messages.success(request, _('User "{username}" added').format( + username=data['username'])) class UserEditForm(forms.Form): # pylint: disable-msg=W0232 @@ -106,24 +103,22 @@ class UserEditForm(forms.Form): # pylint: disable-msg=W0232 def edit(request): """Serve the edit form""" form = None - messages = [] if request.method == 'POST': form = UserEditForm(request.POST, prefix='user') # pylint: disable-msg=E1101 if form.is_valid(): - _apply_edit_changes(request, form.cleaned_data, messages) + _apply_edit_changes(request, form.cleaned_data) form = UserEditForm(prefix='user') else: form = UserEditForm(prefix='user') return TemplateResponse(request, 'users_edit.html', {'title': _('Edit or Delete User'), - 'form': form, - 'messages_': messages}) + 'form': form}) -def _apply_edit_changes(request, data, messages): +def _apply_edit_changes(request, data): """Apply form changes""" for field, value in data.items(): if not value: @@ -139,19 +134,17 @@ def _apply_edit_changes(request, data, messages): (requesting_user, username)) if username == cfg.users.current(request=request, name=True): - messages.append( - ('error', - _('Can not delete current account - "%s"') % username)) + messages.error( + request, _('Can not delete current account - "%s"') % username) continue if not cfg.users.exists(username): - messages.append(('error', - _('User "%s" does not exist') % username)) + messages.error(request, _('User "%s" does not exist') % username) continue try: cfg.users.remove(username) - messages.append(('success', _('User "%s" deleted') % username)) + messages.success(request, _('User "%s" deleted') % username) except IOError as exception: - messages.append(('error', _('Error deleting "%s" - %s') % - (username, exception))) + messages.error(request, _('Error deleting "%s" - %s') % + (username, exception)) diff --git a/modules/xmpp/templates/xmpp_configure.html b/modules/xmpp/templates/xmpp_configure.html index 718d7d353..509cdabeb 100644 --- a/modules/xmpp/templates/xmpp_configure.html +++ b/modules/xmpp/templates/xmpp_configure.html @@ -22,8 +22,6 @@ {% block main_block %} - {% include 'messages.html' %} - {% csrf_token %} diff --git a/modules/xmpp/templates/xmpp_register.html b/modules/xmpp/templates/xmpp_register.html index bfb7c3de4..ed2c2ed6a 100644 --- a/modules/xmpp/templates/xmpp_register.html +++ b/modules/xmpp/templates/xmpp_register.html @@ -22,8 +22,6 @@ {% block main_block %} - {% include 'messages.html' %} - {% csrf_token %} diff --git a/modules/xmpp/xmpp.py b/modules/xmpp/xmpp.py index 2974607bb..47ee9c517 100644 --- a/modules/xmpp/xmpp.py +++ b/modules/xmpp/xmpp.py @@ -1,4 +1,5 @@ from django import forms +from django.contrib import messages from django.template import RequestContext from django.template.loader import render_to_string from django.template.response import TemplateResponse @@ -62,13 +63,12 @@ def configure(request): status = get_status() form = None - messages = [] if request.method == 'POST': form = ConfigureForm(request.POST, prefix='xmpp') # pylint: disable-msg=E1101 if form.is_valid(): - _apply_changes(status, form.cleaned_data, messages) + _apply_changes(request, status, form.cleaned_data) status = get_status() form = ConfigureForm(initial=status, prefix='xmpp') else: @@ -80,7 +80,6 @@ def configure(request): return TemplateResponse(request, 'xmpp_configure.html', {'title': _('Configure XMPP Server'), 'form': form, - 'messages_': messages, 'sidebar_right': sidebar_right}) @@ -93,19 +92,19 @@ def get_status(): return {'inband_enabled': 'inband_enable' in output.split()} -def _apply_changes(old_status, new_status, messages): +def _apply_changes(request, old_status, new_status): """Apply the form changes""" cfg.log.info('Status - %s, %s' % (old_status, new_status)) if old_status['inband_enabled'] == new_status['inband_enabled']: - messages.append(('info', _('Setting unchanged'))) + messages.info(request, _('Setting unchanged')) return if new_status['inband_enabled']: - messages.append(('success', _('Inband registration enabled'))) + messages.success(request, _('Inband registration enabled')) option = 'inband_enable' else: - messages.append(('success', _('Inband registration disabled'))) + messages.success(request, _('Inband registration disabled')) option = 'noinband_enable' cfg.log.info('Option - %s' % option) @@ -128,13 +127,12 @@ class RegisterForm(forms.Form): # pylint: disable-msg=W0232 def register(request): """Serve the registration form""" form = None - messages = [] if request.method == 'POST': form = RegisterForm(request.POST, prefix='xmpp') # pylint: disable-msg=E1101 if form.is_valid(): - _register_user(form.cleaned_data, messages) + _register_user(request, form.cleaned_data) form = RegisterForm(prefix='xmpp') else: form = RegisterForm(prefix='xmpp') @@ -145,11 +143,10 @@ def register(request): return TemplateResponse(request, 'xmpp_register.html', {'title': _('Register XMPP Account'), 'form': form, - 'messages_': messages, 'sidebar_right': sidebar_right}) -def _register_user(data, messages): +def _register_user(request, data): """Register a new XMPP user""" output, error = actions.superuser_run( 'xmpp-register', [data['username'], data['password']]) @@ -157,10 +154,9 @@ def _register_user(data, messages): raise Exception('Error registering user - %s' % error) if 'successfully registered' in output: - messages.append(('success', - _('Registered account for %s' % - data['username']))) + messages.success(request, _('Registered account for %s') % + data['username']) else: - messages.append(('error', - _('Failed to register account for %s: %s') % - (data['username'], output))) + messages.error(request, + _('Failed to register account for %s: %s') % + (data['username'], output)) diff --git a/plinth.py b/plinth.py index 2f1521396..19d0398c5 100755 --- a/plinth.py +++ b/plinth.py @@ -135,7 +135,8 @@ def configure_django(): DEBUG=cfg.debug, ALLOWED_HOSTS=['127.0.0.1', 'localhost'], TEMPLATE_DIRS=template_directories, - INSTALLED_APPS=['bootstrapform'], + INSTALLED_APPS=['bootstrapform', + 'django.contrib.messages'], ROOT_URLCONF='urls', SESSION_ENGINE='django.contrib.sessions.backends.file', SESSION_FILE_PATH=sessions_directory, diff --git a/templates/base.html b/templates/base.html index e26f099b3..91b0d3ce2 100644 --- a/templates/base.html +++ b/templates/base.html @@ -99,6 +99,9 @@ {{ title }} {% endblock %} + + {% include 'messages.html' %} + {% block main_block %} {{ main|safe }} {% endblock %} diff --git a/templates/messages.html b/templates/messages.html index 4aac25d88..34559a873 100644 --- a/templates/messages.html +++ b/templates/messages.html @@ -17,8 +17,8 @@ # {% endcomment %} -{% for severity, message in messages_ %} -
+{% for message in messages %} +
× {{ message }}
From d9bebe67f5b7f57437e472974cf5742e37182e92 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 23 Jun 2014 00:07:00 +0200 Subject: [PATCH 5/8] Remove unused misc utility methods --- util.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/util.py b/util.py index b5d66d932..1841700cc 100644 --- a/util.py +++ b/util.py @@ -24,35 +24,17 @@ def mkdir(newdir): #print "mkdir %s" % repr(newdir) if tail: os.mkdir(newdir) -def is_string(obj): - isinstance(obj, basestring) -def is_ascii(s): - return all(ord(c) < 128 for c in s) -def is_alphanumeric(string): - for c in string: - o = ord(c) - if not o in range(48, 58) + range(41, 91) + [95] + range(97, 123): - return False - return True + def slurp(filespec): with open(filespec) as x: f = x.read() return f + def unslurp(filespec, msg): with open(filespec, 'w') as x: x.write(msg) -def find_in_seq(func, seq): - "Return first item in seq for which func(item) returns True." - for i in seq: - if func(i): - return i - -def find_keys(dic, val): - """return the key of dictionary dic given the value""" - return [k for k, v in dic.iteritems() if v == val] - def filedict_con(filespec=None, table='dict'): """TODO: better error handling in filedict_con""" From f4fe85ae28e93669558e6e6bd6fff2860f137404 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 23 Jun 2014 12:22:46 +0200 Subject: [PATCH 6/8] Refactor global code in configuration module --- cfg.py | 88 +++++++++++++++++++++++++++++---------------- modules/lib/auth.py | 2 -- plinth.py | 4 ++- 3 files changed, 60 insertions(+), 34 deletions(-) diff --git a/cfg.py b/cfg.py index deb3f943a..bab92ead3 100644 --- a/cfg.py +++ b/cfg.py @@ -4,39 +4,65 @@ import os import ConfigParser from ConfigParser import SafeConfigParser -def get_item(parser, section, name): - try: - return parser.get(section, name) - except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): - print ("Configuration does not contain the {}.{} option.".format( - section, name)) - raise - -parser = SafeConfigParser( - defaults={ - 'root':os.path.dirname(os.path.realpath(__file__)), - }) -parser.read(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'plinth.config')) - -product_name = get_item(parser, 'Name', 'product_name') -box_name = get_item(parser, 'Name', 'box_name') -root = get_item(parser, 'Path', 'root') -file_root = get_item(parser, 'Path', 'file_root') -python_root = get_item(parser, 'Path', 'python_root') -data_dir = get_item(parser, 'Path', 'data_dir') -store_file = get_item(parser, 'Path', 'store_file') -user_db = get_item(parser, 'Path', 'user_db') -status_log_file = get_item(parser, 'Path', 'status_log_file') -access_log_file = get_item(parser, 'Path', 'access_log_file') -pidfile = get_item(parser, 'Path', 'pidfile') -host = get_item(parser, 'Network', 'host') -port = int(get_item(parser, 'Network', 'port')) +product_name = None +box_name = None +root = None +file_root = None +python_root = None +data_dir = None +store_file = None +user_db = None +status_log_file = None +access_log_file = None +pidfile = None +host = None +port = None debug = False no_daemon = False +session_key = '_username' main_menu = Menu() -if store_file.endswith(".sqlite3"): - store_file = os.path.splitext(store_file)[0] -if user_db.endswith(".sqlite3"): - user_db = os.path.splitext(user_db)[0] + +def read(): + """Read configuration""" + directory = os.path.dirname(os.path.realpath(__file__)) + parser = SafeConfigParser( + defaults={ + 'root': directory, + }) + parser.read(os.path.join(directory, 'plinth.config')) + + config_items = {('Name', 'product_name'), + ('Name', 'box_name'), + ('Path', 'root'), + ('Path', 'file_root'), + ('Path', 'python_root'), + ('Path', 'data_dir'), + ('Path', 'store_file'), + ('Path', 'user_db'), + ('Path', 'status_log_file'), + ('Path', 'access_log_file'), + ('Path', 'pidfile'), + ('Network', 'host'), + ('Network', 'port')} + + for section, name in config_items: + try: + value = parser.get(section, name) + globals()[name] = value + except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): + print ('Configuration does not contain the {}.{} option.' + .format(section, name)) + raise + + global port # pylint: disable-msg=W0603 + port = int(port) + + global store_file # pylint: disable-msg=W0603 + if store_file.endswith(".sqlite3"): + store_file = os.path.splitext(store_file)[0] + + global user_db # pylint: disable-msg=W0603 + if user_db.endswith(".sqlite3"): + user_db = os.path.splitext(user_db)[0] diff --git a/modules/lib/auth.py b/modules/lib/auth.py index 61f5e0702..5625511e0 100644 --- a/modules/lib/auth.py +++ b/modules/lib/auth.py @@ -6,8 +6,6 @@ from passlib.exc import PasswordSizeError import cfg from model import User -cfg.session_key = '_username' - def add_user(username, passphrase, name='', email='', expert=False): """Add a new user with specified username and passphrase. diff --git a/plinth.py b/plinth.py index 19d0398c5..ca1e85642 100755 --- a/plinth.py +++ b/plinth.py @@ -3,7 +3,6 @@ import argparse import os import sys -import cfg import django.conf import django.core.wsgi @@ -11,6 +10,7 @@ import cherrypy from cherrypy import _cpserver from cherrypy.process.plugins import Daemonizer +import cfg import module_loader import plugin_mount import service @@ -148,6 +148,8 @@ def main(): """Intialize and start the application""" parse_arguments() + cfg.read() + setup_logging() service.init() From fb2f91d4b4ab0990e8c17c421e403524e1d84f37 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 24 Jun 2014 11:40:49 +0200 Subject: [PATCH 7/8] Merge login_nav template with base template --- LICENSES | 1 - modules/apps/templates/apps.html | 2 +- modules/config/templates/config.html | 2 +- .../diagnostics/templates/diagnostics.html | 2 +- .../templates/diagnostics_test.html | 2 +- .../expert_mode/templates/expert_mode.html | 2 +- modules/firewall/templates/firewall.html | 2 +- modules/help/help.py | 2 +- modules/help/templates/about.html | 2 +- modules/help/templates/help.html | 2 +- modules/owncloud/templates/owncloud.html | 2 +- modules/packages/templates/packages.html | 2 +- .../templates/pagekite_configure.html | 2 +- .../templates/pagekite_introduction.html | 2 +- modules/system/templates/system.html | 2 +- modules/tor/templates/tor.html | 2 +- modules/users/templates/users_add.html | 2 +- modules/users/templates/users_edit.html | 2 +- modules/users/users.py | 2 +- modules/xmpp/templates/xmpp_configure.html | 2 +- modules/xmpp/templates/xmpp_register.html | 2 +- modules/xmpp/xmpp.py | 2 +- templates/404.html | 2 +- templates/500.html | 2 +- templates/base.html | 38 ++++++++++++++++++- templates/err.html | 2 +- templates/form.html | 2 +- templates/login_nav.html | 27 ------------- 28 files changed, 61 insertions(+), 55 deletions(-) delete mode 100644 templates/login_nav.html diff --git a/LICENSES b/LICENSES index 55f212f99..9536f0505 100644 --- a/LICENSES +++ b/LICENSES @@ -68,7 +68,6 @@ specified and linked otherwise. - sudoers/plinth :: - - templates/base.html :: [[file:templates/base.tmpl::the%20GNU%20Affero%20General%20Public][GNU Affero General Public License, Version 3 or later]] - templates/err.html :: - -- templates/login_nav.html :: - - templates/two_col.html :: - - tests/actions_test.py :: - - tests/auth_test.py :: - diff --git a/modules/apps/templates/apps.html b/modules/apps/templates/apps.html index a05062282..9dd2a0df2 100644 --- a/modules/apps/templates/apps.html +++ b/modules/apps/templates/apps.html @@ -1,4 +1,4 @@ -{% extends 'login_nav.html' %} +{% extends 'base.html' %} {% comment %} # # This file is part of Plinth. diff --git a/modules/config/templates/config.html b/modules/config/templates/config.html index 4401bdcd3..9f4e4f68a 100644 --- a/modules/config/templates/config.html +++ b/modules/config/templates/config.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. diff --git a/modules/diagnostics/templates/diagnostics.html b/modules/diagnostics/templates/diagnostics.html index afb597e34..f104ef2f8 100644 --- a/modules/diagnostics/templates/diagnostics.html +++ b/modules/diagnostics/templates/diagnostics.html @@ -1,4 +1,4 @@ -{% extends 'login_nav.html' %} +{% extends 'base.html' %} {% comment %} # # This file is part of Plinth. diff --git a/modules/diagnostics/templates/diagnostics_test.html b/modules/diagnostics/templates/diagnostics_test.html index 93012ab24..04f5b0036 100644 --- a/modules/diagnostics/templates/diagnostics_test.html +++ b/modules/diagnostics/templates/diagnostics_test.html @@ -1,4 +1,4 @@ -{% extends 'login_nav.html' %} +{% extends 'base.html' %} {% comment %} # # This file is part of Plinth. diff --git a/modules/expert_mode/templates/expert_mode.html b/modules/expert_mode/templates/expert_mode.html index 0db5565ab..8e447ffbb 100644 --- a/modules/expert_mode/templates/expert_mode.html +++ b/modules/expert_mode/templates/expert_mode.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. diff --git a/modules/firewall/templates/firewall.html b/modules/firewall/templates/firewall.html index 560169a2b..34684d595 100644 --- a/modules/firewall/templates/firewall.html +++ b/modules/firewall/templates/firewall.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. diff --git a/modules/help/help.py b/modules/help/help.py index 4dadd45d5..ce72c6d84 100644 --- a/modules/help/help.py +++ b/modules/help/help.py @@ -36,5 +36,5 @@ def default(request, page=''): main = input_file.read() title = _('%s Documentation') % cfg.product_name - return TemplateResponse(request, 'login_nav.html', + return TemplateResponse(request, 'base.html', {'title': title, 'main': main}) diff --git a/modules/help/templates/about.html b/modules/help/templates/about.html index 2f4a39b81..740afa925 100644 --- a/modules/help/templates/about.html +++ b/modules/help/templates/about.html @@ -1,4 +1,4 @@ -{% extends 'login_nav.html' %} +{% extends 'base.html' %} {% comment %} # # This file is part of Plinth. diff --git a/modules/help/templates/help.html b/modules/help/templates/help.html index 17aaedea4..6fd191d75 100644 --- a/modules/help/templates/help.html +++ b/modules/help/templates/help.html @@ -1,4 +1,4 @@ -{% extends 'login_nav.html' %} +{% extends 'base.html' %} {% comment %} # # This file is part of Plinth. diff --git a/modules/owncloud/templates/owncloud.html b/modules/owncloud/templates/owncloud.html index 303846327..5dd7c618c 100644 --- a/modules/owncloud/templates/owncloud.html +++ b/modules/owncloud/templates/owncloud.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. diff --git a/modules/packages/templates/packages.html b/modules/packages/templates/packages.html index d6eb85e22..90dd247a6 100644 --- a/modules/packages/templates/packages.html +++ b/modules/packages/templates/packages.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. diff --git a/modules/pagekite/templates/pagekite_configure.html b/modules/pagekite/templates/pagekite_configure.html index 9bcc25fe1..e8544420b 100644 --- a/modules/pagekite/templates/pagekite_configure.html +++ b/modules/pagekite/templates/pagekite_configure.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. diff --git a/modules/pagekite/templates/pagekite_introduction.html b/modules/pagekite/templates/pagekite_introduction.html index f22e1ec35..21668bdea 100644 --- a/modules/pagekite/templates/pagekite_introduction.html +++ b/modules/pagekite/templates/pagekite_introduction.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. diff --git a/modules/system/templates/system.html b/modules/system/templates/system.html index 45d842319..24f53b350 100644 --- a/modules/system/templates/system.html +++ b/modules/system/templates/system.html @@ -1,4 +1,4 @@ -{% extends 'login_nav.html' %} +{% extends 'base.html' %} {% comment %} # # This file is part of Plinth. diff --git a/modules/tor/templates/tor.html b/modules/tor/templates/tor.html index 85ebe5745..c97aafded 100644 --- a/modules/tor/templates/tor.html +++ b/modules/tor/templates/tor.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. diff --git a/modules/users/templates/users_add.html b/modules/users/templates/users_add.html index 3df856188..1be72d630 100644 --- a/modules/users/templates/users_add.html +++ b/modules/users/templates/users_add.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. diff --git a/modules/users/templates/users_edit.html b/modules/users/templates/users_edit.html index 82905b6b9..4c695679b 100644 --- a/modules/users/templates/users_edit.html +++ b/modules/users/templates/users_edit.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. diff --git a/modules/users/users.py b/modules/users/users.py index 8c4f5081b..b3cee4154 100644 --- a/modules/users/users.py +++ b/modules/users/users.py @@ -29,7 +29,7 @@ def index(request): sidebar_right = render_to_string('menu_block.html', {'menu': menu}, RequestContext(request)) - return TemplateResponse(request, 'login_nav.html', + return TemplateResponse(request, 'base.html', {'title': _('Manage Users and Groups'), 'sidebar_right': sidebar_right}) diff --git a/modules/xmpp/templates/xmpp_configure.html b/modules/xmpp/templates/xmpp_configure.html index 509cdabeb..38c898cbf 100644 --- a/modules/xmpp/templates/xmpp_configure.html +++ b/modules/xmpp/templates/xmpp_configure.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. diff --git a/modules/xmpp/templates/xmpp_register.html b/modules/xmpp/templates/xmpp_register.html index ed2c2ed6a..9b0620aeb 100644 --- a/modules/xmpp/templates/xmpp_register.html +++ b/modules/xmpp/templates/xmpp_register.html @@ -1,4 +1,4 @@ -{% extends "login_nav.html" %} +{% extends "base.html" %} {% comment %} # # This file is part of Plinth. diff --git a/modules/xmpp/xmpp.py b/modules/xmpp/xmpp.py index 47ee9c517..c03666e28 100644 --- a/modules/xmpp/xmpp.py +++ b/modules/xmpp/xmpp.py @@ -43,7 +43,7 @@ def index(request): sidebar_right = render_to_string('menu_block.html', {'menu': SIDE_MENU}, RequestContext(request)) - return TemplateResponse(request, 'login_nav.html', + return TemplateResponse(request, 'base.html', {'title': _('XMPP Server'), 'main': main, 'sidebar_right': sidebar_right}) diff --git a/templates/404.html b/templates/404.html index b4e9a30d1..8c0fcc69c 100644 --- a/templates/404.html +++ b/templates/404.html @@ -1,4 +1,4 @@ -{% extends 'login_nav.html' %} +{% extends 'base.html' %} {% comment %} # # This file is part of Plinth. diff --git a/templates/500.html b/templates/500.html index 485f3df3c..0863ec630 100644 --- a/templates/500.html +++ b/templates/500.html @@ -1,4 +1,4 @@ -{% extends 'login_nav.html' %} +{% extends 'base.html' %} {% comment %} # # This file is part of Plinth. diff --git a/templates/base.html b/templates/base.html index 91b0d3ce2..eeebc7d80 100644 --- a/templates/base.html +++ b/templates/base.html @@ -53,16 +53,50 @@