From ffbefbbfa4a24f238e69ab62e38f58a5a0f14efb Mon Sep 17 00:00:00 2001 From: fpeters Date: Mon, 6 Nov 2006 14:05:16 +0000 Subject: [PATCH] Initial revision git-svn-id: svn://localhost/lasso-conform/trunk@2 2a3a78c3-912c-0410-af21-e1fb2d1df599 --- lcs/.cvsignore | 1 + lcs/Defaults.py | 3 + lcs/__init__.py | 10 + lcs/admin/__init__.py | 2 + lcs/admin/logger.ptl | 165 ++++++++++++++++ lcs/admin/menu.ptl | 9 + lcs/admin/root.ptl | 87 +++++++++ lcs/admin/settings.ptl | 283 +++++++++++++++++++++++++++ lcs/admin/users.ptl | 255 +++++++++++++++++++++++++ lcs/backoffice/__init__.py | 2 + lcs/backoffice/menu.ptl | 15 ++ lcs/backoffice/root.ptl | 49 +++++ lcs/ctl/__init__.py | 0 lcs/ctl/clean_sessions.py | 31 +++ lcs/ctl/start.py | 30 +++ lcs/liberty.ptl | 18 ++ lcs/publisher.py | 41 ++++ lcs/root.ptl | 137 +++++++++++++ lcs/sessions.py | 14 ++ lcs/users.py | 32 ++++ lcsctl.py | 27 +++ root/css/common.css | 258 +++++++++++++++++++++++++ root/css/dc2/admin.css | 305 +++++++++++++++++++++++++++++ root/css/dc2/head-bg.png | Bin 0 -> 356 bytes root/css/dc2/head-logo-empty.png | Bin 0 -> 829 bytes root/css/dc2/head-logo.png | Bin 0 -> 829 bytes root/css/dc2/page-bg.png | Bin 0 -> 224 bytes root/css/ico_user.png | Bin 0 -> 1331 bytes root/css/img/bulle.png | Bin 0 -> 199 bytes root/css/img/day-date.png | Bin 0 -> 86 bytes root/css/img/footer.jpg | Bin 0 -> 2586 bytes root/css/img/h2.png | Bin 0 -> 159 bytes root/css/img/li.png | Bin 0 -> 116 bytes root/css/img/linkscat.png | Bin 0 -> 101 bytes root/css/img/page.png | Bin 0 -> 180 bytes root/css/img/search.png | Bin 0 -> 135 bytes root/css/img/sidebarh2.png | Bin 0 -> 92 bytes root/css/img/top.jpg | Bin 0 -> 3995 bytes root/css/img/top.png | Bin 0 -> 1461 bytes root/css/lcs.css | 318 +++++++++++++++++++++++++++++++ root/css/required.png | Bin 0 -> 136 bytes root/css/warning.png | Bin 0 -> 808 bytes setup.py | 36 ++++ 43 files changed, 2128 insertions(+) create mode 100644 lcs/.cvsignore create mode 100644 lcs/Defaults.py create mode 100644 lcs/__init__.py create mode 100644 lcs/admin/__init__.py create mode 100644 lcs/admin/logger.ptl create mode 100644 lcs/admin/menu.ptl create mode 100644 lcs/admin/root.ptl create mode 100644 lcs/admin/settings.ptl create mode 100644 lcs/admin/users.ptl create mode 100644 lcs/backoffice/__init__.py create mode 100644 lcs/backoffice/menu.ptl create mode 100644 lcs/backoffice/root.ptl create mode 100644 lcs/ctl/__init__.py create mode 100644 lcs/ctl/clean_sessions.py create mode 100644 lcs/ctl/start.py create mode 100644 lcs/liberty.ptl create mode 100644 lcs/publisher.py create mode 100644 lcs/root.ptl create mode 100644 lcs/sessions.py create mode 100644 lcs/users.py create mode 100755 lcsctl.py create mode 100644 root/css/common.css create mode 100644 root/css/dc2/admin.css create mode 100644 root/css/dc2/head-bg.png create mode 100644 root/css/dc2/head-logo-empty.png create mode 100644 root/css/dc2/head-logo.png create mode 100644 root/css/dc2/page-bg.png create mode 100644 root/css/ico_user.png create mode 100644 root/css/img/bulle.png create mode 100644 root/css/img/day-date.png create mode 100644 root/css/img/footer.jpg create mode 100644 root/css/img/h2.png create mode 100644 root/css/img/li.png create mode 100644 root/css/img/linkscat.png create mode 100644 root/css/img/page.png create mode 100644 root/css/img/search.png create mode 100644 root/css/img/sidebarh2.png create mode 100644 root/css/img/top.jpg create mode 100644 root/css/img/top.png create mode 100644 root/css/lcs.css create mode 100644 root/css/required.png create mode 100644 root/css/warning.png create mode 100644 setup.py diff --git a/lcs/.cvsignore b/lcs/.cvsignore new file mode 100644 index 0000000..0cf7e5c --- /dev/null +++ b/lcs/.cvsignore @@ -0,0 +1 @@ +lcs_cfg.py diff --git a/lcs/Defaults.py b/lcs/Defaults.py new file mode 100644 index 0000000..a209227 --- /dev/null +++ b/lcs/Defaults.py @@ -0,0 +1,3 @@ +APP_DIR = "/var/lib/lcs" +DATA_DIR = "/usr/share/lcs" +ERROR_LOG = None diff --git a/lcs/__init__.py b/lcs/__init__.py new file mode 100644 index 0000000..2e83e21 --- /dev/null +++ b/lcs/__init__.py @@ -0,0 +1,10 @@ +import sys +import os +sys.path.insert(0, os.path.dirname(__file__)) + +import qommon + +import lasso +if not hasattr(lasso, 'SAML2_SUPPORT'): + lasso.SAML2_SUPPORT = False + diff --git a/lcs/admin/__init__.py b/lcs/admin/__init__.py new file mode 100644 index 0000000..b76c2b9 --- /dev/null +++ b/lcs/admin/__init__.py @@ -0,0 +1,2 @@ +from root import RootDirectory, register_page + diff --git a/lcs/admin/logger.ptl b/lcs/admin/logger.ptl new file mode 100644 index 0000000..01c8648 --- /dev/null +++ b/lcs/admin/logger.ptl @@ -0,0 +1,165 @@ +import random + +from quixote import get_publisher, get_request, get_response, redirect +from quixote.directory import Directory + +from menu import * + +import lcs +from qommon import logger +from qommon import misc +from qommon.form import * +from lcs.users import User +from qommon import template + + +class ByUserDirectory(Directory): + def _q_lookup(self, component): + return ByUserPages(component) + + +class LoggerDirectory(Directory): + _q_exports = ['', 'download', 'by_user'] + + by_user = ByUserDirectory() + + def _q_index [html] (self): + get_response().breadcrumb.append( ('logger/', _('Logs')) ) + html_top('logger', title = _('Logs')) + request = get_request() + logfile = request.get_field('logfile', 'lcs.log') + if not logfile.startswith(str('lcs.log')) or str('/') in str(logfile): + return template.error_page(_('Bad log file: %s') % logfile) + logfilename = str(os.path.join(get_publisher().app_dir, logfile)) + + if not os.path.exists(logfilename): + _('Nothing to show') + else: + if logfile: + '%s' % (logfile, _('Download Raw Log File')) + else: + '%s' % _('Download Raw Log File') + + user_color_keys = {} + last_date = None + '\n' + '' + ' ' % _('Time') + ' ' % _('User') + ' ' % _('Message') + '\n' + '\n' + i = 1 + for line in file(logfilename): + d = logger.readline(line) + if not d: + continue + user_color_key = d['user_id'] + if user_color_key == 'anonymous': + user_color_key += d['ip'] + if not user_color_keys.has_key(user_color_key): + user_color_keys[user_color_key] = ''.join( + ['%x' % random.randint(0xc, 0xf) for x in range(3)]) + '' % ( + d['level'].lower(), user_color_keys[user_color_key]) + if (last_date != d['date']): + ' ' % (d['date'], d['hour'][:-4]) + last_date = d['date'] + else: + ' ' % (d['hour'][:-4]) + if d['user_id'] == 'anonymous': + userlabel = _('Anonymous') + ip = d['ip'] + ' ' % (ip, userlabel) + elif d['user_id'] == 'unlogged': + userlabel = _('Unlogged') + ip = d['ip'] + ' ' % (ip, userlabel) + else: + try: + user = User.get(d['user_id']) + except KeyError: + userlabel = _('Unknown') + else: + userlabel = htmltext(user.name.replace(str(' '), str(' '))) + ' ' % userlabel + ' ' % d['message'] + '\n' + '\n' + '
%s%s%s
%s %s%s%s%s%s%s
\n' + + logfiles = [x for x in os.listdir(get_publisher().app_dir) if x.startswith(str('lcs.log'))] + if len(logfiles) > 1: + options = [] + for lfile in logfiles: + firstline = file(os.path.join(get_publisher().app_dir, lfile)).readline() + d = logger.readline(firstline) + if not d: + continue + if logfile == lfile: + selected = 'selected="selected" ' + else: + selected = '' + options.append({'selected': selected, 'lfile': lfile, + 'date': '%s %s' % (d['date'], d['hour'])}) + + '
' + _('Select another logfile:') + '' + '' % _('Submit') + + + def download(self): + request = get_request() + logfile = request.get_field('logfile', 'lcs.log') + if not logfile.startswith(str('lcs.log')) or str('/') in logfile: + return template.error_page(_('Bad log file: %s') % logfile) + logfilename = os.path.join(get_publisher().app_dir, logfile) + response = get_response() + response.set_content_type('text/x-log', 'iso-8859-1') + response.set_header('content-disposition', 'attachment; filename=%s' % logfile) + return open(logfilename).read() + + +class ByUserPages(Directory): + _q_exports = [''] + + def __init__(self, component): + try: + self.user = User.get(component) + except KeyError: + raise TraversalError() + + def _q_index [html] (self): + html_top('logger', title = _('Logs')) + '

%s - %s

' % (_('User'), self.user.name) + + last_date = None + '' + '' + ' ' % _('Time') + ' ' % _('Message') + '' + '' + if os.path.exists(logger.logfile): + for line in file(logger.logfile): + d = logger.readline(line) + if not d or d['user_id'] != str(self.user.id): + continue + '' + if (last_date != d['date']): + ' ' % (d['date'], d['hour'][:-4]) + last_date = d['date'] + else: + ' ' % (d['hour'][:-4]) + ' ' % (d['url'], d['message']) + '' + '' + '
%s%s
%s %s%s%s
' + diff --git a/lcs/admin/menu.ptl b/lcs/admin/menu.ptl new file mode 100644 index 0000000..85173e0 --- /dev/null +++ b/lcs/admin/menu.ptl @@ -0,0 +1,9 @@ +from qommon.admin.menu import html_top + +def error_page [html] (section, error): + html_top(section, title = _('Error')) + '
' + '

%s

' % _('Error') + '

%s

' % error + '
' + diff --git a/lcs/admin/root.ptl b/lcs/admin/root.ptl new file mode 100644 index 0000000..1706e5e --- /dev/null +++ b/lcs/admin/root.ptl @@ -0,0 +1,87 @@ +import os + +from quixote import get_session, get_publisher, get_request, get_response +from quixote.directory import Directory, AccessControlled + +import settings +import users +import logger + +from qommon import errors + +def gpl [html] (): + """

This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 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 General Public License + for more details.

+ +

You should have received a copy of the GNU General Public License along with + this program; if not, write to the Free Software Foundation, Inc., 59 Temple + Place - Suite 330, Boston, MA 02111-1307, USA.

+ """ + + +class RootDirectory(AccessControlled, Directory): + _q_exports = ['', 'settings', 'users', 'logger'] + + settings = settings.SettingsDirectory() + users = users.UsersDirectory() + logger = logger.LoggerDirectory() + + items = [ + ('users/', N_('Users')), + ('logger/', N_('Logs')), + ('settings/', N_('Settings')), + ('/', N_('Lasso Conformance Event Service Provider'))] + + def _q_access(self): + get_response().breadcrumb.append( ('admin/', _('Administration')) ) + + if os.path.exists(os.path.join(get_publisher().app_dir, 'ADMIN_FOR_ALL')): + return + session = get_session() + + req = get_request() + if req.user: + if users.User.count() == 0: + # this means user logged in anonymously + pass + elif not req.user.is_admin: + raise errors.AccessForbiddenError() + else: + if users.User.count() > 0: + raise errors.AccessUnauthorizedError() + + return + + + def _q_index [html] (self): + from menu import html_top + html_top('/') + '
' + gpl() + '
' + + +def register_page(url_name, directory = None, label = None): + first_time = False + if not url_name in RootDirectory._q_exports: + RootDirectory._q_exports.append(url_name) + first_time = True + + if directory: + setattr(RootDirectory, url_name, directory) + if not label: + label = directory.label + + if first_time: + logger_index = RootDirectory.items.index(('logger/', 'Logs')) + if directory: + url_name += '/' + RootDirectory.items.insert(logger_index, (url_name, label)) + diff --git a/lcs/admin/settings.ptl b/lcs/admin/settings.ptl new file mode 100644 index 0000000..3539b5e --- /dev/null +++ b/lcs/admin/settings.ptl @@ -0,0 +1,283 @@ +import cStringIO +import cPickle +import re +import os +import lasso +import glob +import zipfile +import base64 + +from quixote import get_publisher, get_request, get_response, redirect +from quixote.directory import Directory, AccessControlled + +from menu import * + +from qommon import misc, get_cfg +from qommon.form import * + +from qommon.admin.emails import EmailsDirectory +import qommon.ident + +def cfg_submit(form, cfg_key, fields): + get_publisher().reload_cfg() + cfg_key = str(cfg_key) + cfg_dict = get_cfg(cfg_key, {}) + for k in fields: + cfg_dict[str(k)] = form.get_widget(k).parse() + get_publisher().cfg[cfg_key] = cfg_dict + get_publisher().write_cfg() + +class IdentificationDirectory(Directory): + _q_exports = [''] + + def _q_index [html] (self): + get_response().breadcrumb.append( ('identification/', _('Identification')) ) + identification_cfg = get_cfg('identification', {}) + form = Form(enctype='multipart/form-data') + form.add(CheckboxesWidget, 'methods', title = _('Methods'), + value = identification_cfg.get('methods'), + elements = [ + ('idp', _('Delegated to Liberty/SAML2 identity provider')), + ('password', _('Simple local username / password')), + ], + inline = False) + + form.add_submit('submit', _('Submit')) + form.add_submit('cancel', _('Cancel')) + if form.get_widget('cancel').parse(): + return redirect('..') + + if not form.is_submitted() or form.has_errors(): + html_top('settings', title = _('Identification')) + '

%s

' % _('Identification') + form.render() + else: + cfg_submit(form, 'identification', ['methods']) + return redirect('..') + + def _q_lookup(self, component): + get_response().breadcrumb.append( ('identification/', _('Identification')) ) + return qommon.ident.get_method_admin_directory(component) + + +class SettingsDirectory(AccessControlled, Directory): + _q_exports = ['', 'themes', 'users', + 'template', 'emails', 'debug_options', 'language', + 'identification', 'sitename'] + + emails = EmailsDirectory() + identification = IdentificationDirectory() + + def _q_access(self): + get_response().breadcrumb.append( ('settings/', _('Settings')) ) + + def _q_index [html] (self): + html_top('settings', title = _('Settings')) + + '
%s
%s
' % ( + _('Identification'), _('Configure identification parameters')) + + identification_cfg = get_cfg('identification', {}) + for method in identification_cfg.get('methods', []): + try: + method_admin = qommon.ident.get_method_admin_directory(method) + except AttributeError: + continue + + '
%s
%s
' % ( + method, _(method_admin.title), _(method_admin.label)) + + '

%s

' % _('Customisation') + + '
' + '
%s
%s
' % ( + _('Site Name'), _('Configure site name')) + '
%s
%s
' % ( + _('Language'), _('Configure site language')) + '
%s
%s
' % ( + _('Theme'), _('Configure theme')) + '
%s
%s
' % ( + _('Template'), _('Configure template')) + '
%s
%s
' % ( + _('Emails'), _('Configure email settings')) + '
' + + '

%s

' % _('Misc') + '
' + '
%s
%s
' % ( + _('Misc'), _('Configure misc options')) + '
%s
%s
' % ( + _('Debug Options'), _('Configure options useful for debugging')) + '
' + + '

%s

' % _('Import / Export') + + '
' + '
%s
%s
' % ( + _('Import'), _('Import data from another site')) + '
%s
%s
' % ( + _('Export'), _('Export data for another site')) + '
' + + def themes [html] (self): + import xml.dom.minidom + + def getText(nodelist): + rc = "" + for node in nodelist: + if node.nodeType == node.TEXT_NODE: + rc = rc + node.data + return rc.encode('iso-8859-1') + + request = get_request() + + if not request.form.has_key('theme'): + current_theme = get_cfg('branding', {}).get('theme', 'default') + + get_response().breadcrumb.append(('themes', _('Themes'))) + html_top('settings', title = _('Themes')) + "

%s

" % _('Themes') + '' + theme_files = glob.glob(os.path.join(get_publisher().DATA_DIR, str('themes/*/desc.xml'))) + theme_files.sort() + '
    ' + for t in theme_files: + dom = xml.dom.minidom.parseString(open(t).read()) + theme = dom.getElementsByTagName('theme')[0].attributes['name'].value.encode( + str('iso-8859-1')) + if current_theme == theme: + checked = ' checked="checked"' + else: + checked = '' + '
  • ' + '' + ' %s' % ( + theme, checked, getText(dom.getElementsByTagName('label')[0].childNodes)) + icon_file = t.replace(str('desc.xml'), str('icon.png')) + if os.path.exists(icon_file): + '' % theme + '

    %s
    by %s

    ' % ( + getText(dom.getElementsByTagName('desc')[0].childNodes), + getText(dom.getElementsByTagName('author')[0].childNodes)) + '
  • ' + '
' + '
' + '' % _('Submit') + '
' + '' + else: + if not misc.cfg.has_key('branding'): + misc.cfg[str('branding')] = {} + misc.cfg[str('branding')][str('theme')] = str(request.form['theme']) + misc.write_cfg() + return redirect('.') + + + def template [html] (self): + from qommon.template import DEFAULT_TEMPLATE_EZT + branding_cfg = get_cfg('branding', {}) + template = branding_cfg.get('template', DEFAULT_TEMPLATE_EZT) + form = Form(enctype="multipart/form-data") + form.add(TextWidget, 'template', title = _('Site Template'), value = template, + cols = 80, rows = 25) + form.add_submit("submit", _("Submit")) + form.add_submit("cancel", _("Cancel")) + if form.get_widget('cancel').parse(): + return redirect('.') + + if not form.is_submitted() or form.has_errors(): + get_response().breadcrumb.append(('template', _('Template'))) + html_top('settings', title = _('Template')) + '

%s

' % _('Template') + form.render() + else: + self.template_submit(form) + redirect('.') + + + def template_submit(self, form): + get_publisher().reload_cfg() + branding_cfg = get_cfg('branding', {}) + template = form.get_widget('template').parse() + if template == DEFAULT_TEMPLATE_EZT or not template: + if branding_cfg.has_key('template'): + del branding_cfg['template'] + else: + branding_cfg['template'] = template + misc.cfg['branding'] = branding_cfg + get_publisher().write_cfg() + + + def debug_options [html] (self): + form = Form(enctype="multipart/form-data") + debug_cfg = get_cfg('debug', {}) + form.add(StringWidget, 'error_email', title = _('Email for Tracebacks'), + value = debug_cfg.get('error_email', '')) + form.add(SingleSelectWidget, 'display_exceptions', title = _('Display Exceptions'), + value = debug_cfg.get('display_exceptions', ''), + options = [ (str(''), _('No display')), + (str('text'), _('Display as Text')), + (str('text-in-html'), _('Display as Text in HTML an error page')), + (str('html'), _('Display as HTML')) ]) + form.add(CheckboxWidget, 'logger', title = _('Logger'), + value = debug_cfg.get('logger', False)) + form.add(CheckboxWidget, 'debug_mode', title = _('Enable debug mode'), + value = debug_cfg.get('debug_mode', False)) + form.add_submit('submit', _('Submit')) + form.add_submit('cancel', _('Cancel')) + + if form.get_widget('cancel').parse(): + return redirect('.') + + if not form.is_submitted() or form.has_errors(): + get_response().breadcrumb.append(('debug_options', _('Debug Options'))) + html_top('settings', title = _('Debug Options')) + '

%s

' % _('Debug Options') + form.render() + else: + cfg_submit(form, 'debug', ('error_email', 'display_exceptions', 'logger', 'debug_mode')) + redirect('.') + + def language [html] (self): + form = Form(enctype='multipart/form-data') + language_cfg = get_cfg('language', {}) + form.add(SingleSelectWidget, 'language', title = _('Language'), + value = language_cfg.get('language'), + options = [ (None, _('System Default')), + (str('HTTP'), _('From HTTP Accept-Language header')), + (str('en'), _('English')) ] ) + + form.add_submit('submit', _('Submit')) + form.add_submit('cancel', _('Cancel')) + if form.get_widget('cancel').parse(): + return redirect('.') + + if not form.is_submitted() or form.has_errors(): + get_response().breadcrumb.append(('language', _('Language'))) + html_top('settings', title = _('Language')) + '

%s

' % _('Language') + form.render() + else: + cfg_submit(form, 'language', ['language']) + redirect('.') + + def sitename [html] (self): + form = Form(enctype='multipart/form-data') + misc_cfg = get_cfg('misc', {}) + form.add(StringWidget, 'sitename', title = _('Site Name'), + value = get_cfg('sitename')) + + form.add_submit('submit', _('Submit')) + form.add_submit('cancel', _('Cancel')) + if form.get_widget('cancel').parse(): + return redirect('.') + + if not form.is_submitted() or form.has_errors(): + get_response().breadcrumb.append(('sitename', _('Site Name'))) + html_top('settings', title = _('Site Name')) + '

%s

' % _('Site Name') + form.render() + else: + cfg_submit(form, 'misc', ['sitename']) + redirect('.') + diff --git a/lcs/admin/users.ptl b/lcs/admin/users.ptl new file mode 100644 index 0000000..070b8ba --- /dev/null +++ b/lcs/admin/users.ptl @@ -0,0 +1,255 @@ +import random +import lasso + +from quixote import get_response, get_request, get_session, redirect +from quixote.directory import Directory + +from menu import * + +from qommon import emails +from qommon import errors +from qommon import misc, get_cfg +from lcs.users import User + +import qommon.ident +from qommon.form import * + + +class UserUI: + def __init__(self, user): + self.user = user + + def form(self): + ident_methods = get_cfg('identification', {}).get('methods', []) + formdef = User.get_formdef() + users_cfg = get_cfg('users', {}) + + form = Form(enctype='multipart/form-data') + if not formdef or not users_cfg.get('field_name'): + form.add(StringWidget, 'name', title = _('Name'), required = True, size=30, + value = self.user.name) + if not formdef or not users_cfg.get('field_email'): + form.add(EmailWidget, 'email', title = _('Email'), required = False, size=30, + value = self.user.email) + if formdef: + formdef.add_fields_to_form(form, form_data = self.user.form_data) + form.add(CheckboxWidget, 'is_admin', title = _('Administrator Account'), + value = self.user.is_admin) + + for klass in [x for x in qommon.ident.get_method_classes() if x.key in ident_methods]: + if klass.method_admin_widget: + value = klass().get_value(self.user) + form.add(klass.method_admin_widget, 'method_%s' % klass.key, required = True, + value = value) + + form.add_submit('submit', _('Submit')) + form.add_submit('cancel', _('Cancel')) + return form + + def submit_form(self, form): + formdef = User.get_formdef() + if not self.user: + self.user = User() + for f in ('name', 'email', 'is_admin'): + widget = form.get_widget(f) + if widget: + setattr(self.user, f, widget.parse()) + if formdef: + data = formdef.get_data(form) + users_cfg = get_cfg('users', {}) + if users_cfg.get('field_name'): + self.user.name = data.get(users_cfg.get('field_name')) + if users_cfg.get('field_email'): + self.user.email = data.get(users_cfg.get('field_email')) + self.user.form_data = data + + # user is stored first so it get an id; necessary for some ident + # methods + self.user.store() + + ident_methods = get_cfg('identification', {}).get('methods', []) + for klass in [x for x in qommon.ident.get_method_classes() if x.key in ident_methods]: + widget = form.get_widget('method_%s' % klass.key) + if widget: + klass().submit(self.user, widget) + + # XXX: and store! + # XXX 2: but pay attention to errors set on widget (think + # "duplicate username") (and the calling method will also + # have to check this) + + +class UserPage(Directory): + _q_exports = ['', 'edit', 'delete', 'token', 'debug'] + + def __init__(self, component): + self.user = User.get(component) + self.user_ui = UserUI(self.user) + get_response().breadcrumb.append((component + '/', self.user.name)) + + def _q_index [html] (self): + html_top('users', '%s - %s' % (_('User'), self.user.name)) + '

%s - %s

' % (_('User'), self.user.name) + '
' + '
%s
' % _('Name') + '
%s
' % self.user.name + if self.user.email: + '
%s
' % _('Email') + '
%s
' % self.user.email + if self.user.is_admin: + '
%s
' % _('Roles') + '
    ' + if self.user.is_admin: + '
  • %s
  • ' % _('Site Administrator') + '
' + if self.user.lasso_dump: + identity = lasso.Identity.newFromDump(self.user.lasso_dump) + server = misc.get_lasso_server() + if len(identity.providerIds) and server: + '

%s

' % _('Liberty Alliance Details') + '
    ' + for pid in identity.providerIds: + provider = server.getProvider(pid) + label = misc.get_provider_label(provider) + if label: + label = '%s (%s)' % (label, pid) + else: + label = pid + federation = identity.getFederation(pid) + '
  • ' + _('Account federated with %s') % label + '
    ' + if federation.localNameIdentifier: + _("local: ") + federation.localNameIdentifier.content + if federation.remoteNameIdentifier: + _("remote: ") + federation.remoteNameIdentifier.content + '
  • ' + '
' + + if get_cfg('debug', {}).get('debug_mode', False): + '

%s

' % _('Lasso Identity Dump') + '
%s
' % self.user.lasso_dump + '
' + + def debug [html] (self): + get_response().breadcrumb.append( ('debug', _('Debug')) ) + html_top('users', 'Debug') + "

Debug - %s

" % self.user.name + "
"
+        self.user.lasso_dump
+        "
" + + def edit [html] (self): + form = self.user_ui.form() + if form.get_widget('cancel').parse(): + return redirect('..') + + display_form = (not form.is_submitted() or form.has_errors()) + + if display_form: + get_response().breadcrumb.append( ('edit', _('Edit')) ) + html_top('users', title = _('Edit User')) + '

%s

' % _('Edit User') + form.render() + else: + self.user_ui.submit_form(form) + return redirect('..') + + def delete [html] (self): + form = Form(enctype="multipart/form-data") + form.widgets.append(HtmlWidget('

%s

' % _( + "You are about to irrevocably delete this user."))) + form.add_submit("submit", _("Submit")) + form.add_submit("cancel", _("Cancel")) + if form.get_widget('cancel').parse(): + return redirect('..') + if not form.is_submitted() or form.has_errors(): + get_response().breadcrumb.append(('delete', _('Delete'))) + html_top('users', title = _('Delete User')) + '

%s %s

' % (_('Deleting User:'), self.user.name) + form.render() + else: + ident_methods = get_cfg('identification', {}).get('methods', []) + for klass in [x for x in qommon.ident.get_method_classes() if x.key in ident_methods]: + klass().delete(self.user) + self.user.remove_self() + return redirect('..') + + +class UsersDirectory(Directory): + _q_exports = ['', 'new'] + + def _q_index [html] (self): + get_response().breadcrumb.append( ('users/', _('Users')) ) + html_top('users', title = _('Users')) + + ident_methods = get_cfg('identification', {}).get('methods', []) + if ident_methods == ['idp'] and len(get_cfg('idp', {}).items()) == 0: + '

%s

' % _('Liberty support must be setup before creating users.') + else: + """""" % (_('New User')) + + debug_cfg = get_cfg('debug', {}) + + users = User.select(order_by = 'name') + + '
    ' + for user in users: + '
  • ' + '%s' % user.display_name + if user.email: + '

    ' + user.email + '

    ' + + '

    ' + command_icon('%s/' % user.id, 'view') + command_icon('%s/edit' % user.id, 'edit') + command_icon('%s/delete' % user.id, 'remove') + if debug_cfg.get('logger', False): + command_icon('../logger/by_user/%s/' % user.id, 'logs', + label = _('Logs'), icon = 'stock_harddisk_16.png') + '

  • ' + '
' + + def new [html] (self): + get_response().breadcrumb.append( ('users/', _('Users')) ) + get_response().breadcrumb.append( ('new', _('New')) ) + ident_methods = get_cfg('identification', {}).get('methods', []) + if ident_methods == ['idp'] and len(get_cfg('idp', {}).items()) == 0: + return error_page('users', + _('Liberty support must be setup before creating users.')) + # XXX: user must be logged in to get here + user = User() + user_ui = UserUI(user) + first_user = User.count() == 0 + if first_user: + user.is_admin = first_user + form = user_ui.form() + if form.get_widget('cancel').parse(): + return redirect('.') + + if not form.is_submitted() or form.has_errors(): + html_top('users', title = _('New User')) + '

%s

' % _('New User') + form.render() + else: + user_ui.submit_form(form) + if first_user: + req = get_request() + if req.user: + user_ui.user.name_identifiers = req.user.name_identifiers + user_ui.user.lasso_dump = req.user.lasso_dump + user_ui.user.store() + get_session().set_user(user_ui.user.id) + return redirect('.') + + def _q_lookup(self, component): + get_response().breadcrumb.append( ('users/', _('Users')) ) + try: + return UserPage(component) + except KeyError: + raise errors.TraversalError() + diff --git a/lcs/backoffice/__init__.py b/lcs/backoffice/__init__.py new file mode 100644 index 0000000..59cfd57 --- /dev/null +++ b/lcs/backoffice/__init__.py @@ -0,0 +1,2 @@ +from root import RootDirectory, register_directory, register_menu_item + diff --git a/lcs/backoffice/menu.ptl b/lcs/backoffice/menu.ptl new file mode 100644 index 0000000..1e2ade7 --- /dev/null +++ b/lcs/backoffice/menu.ptl @@ -0,0 +1,15 @@ +import quixote +from quixote import get_response +from quixote.html import htmltext + +from qommon import get_cfg + +from lcs.users import User + +def error_page [html] (section, error): + html_top(section, title = _('Error')) + '
' + '

%s

' % _('Error') + '

%s

' % error + '
' + diff --git a/lcs/backoffice/root.ptl b/lcs/backoffice/root.ptl new file mode 100644 index 0000000..45558a7 --- /dev/null +++ b/lcs/backoffice/root.ptl @@ -0,0 +1,49 @@ +import os +import csv +import cStringIO + +from quixote import get_session, get_session_manager, get_publisher, get_request, get_response +from quixote.directory import Directory, AccessControlled + +from qommon.backoffice.menu import html_top + +from qommon import misc, get_logger + +from qommon import errors + +from lcs.users import User + +def ellipsize(s, length = 30): + if not s or len(s) < length: + return s + return s[:length-5] + ' (...)' + + +def register_directory(urlname, directory): + if not urlname in RootDirectory._q_exports: + RootDirectory._q_exports.append(urlname) + setattr(RootDirectory, urlname, directory) + +def register_menu_item(url, title): + RootDirectory.items.append((url, title)) + + +class RootDirectory(AccessControlled, Directory): + _q_exports = [''] + + items = [ + ('/', N_('Lasso Conformance Event Service Provider'))] + + def _q_access(self): + user = get_request().user + if not user: + raise errors.AccessUnauthorizedError() + + def _q_index [html] (self): + html_top('', _('Back office')) + 'XXX' + + def _q_traverse(self, path): + get_response().breadcrumb.append( ('backoffice/', _('Back Office')) ) + return Directory._q_traverse(self, path) + diff --git a/lcs/ctl/__init__.py b/lcs/ctl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lcs/ctl/clean_sessions.py b/lcs/ctl/clean_sessions.py new file mode 100644 index 0000000..777b4bf --- /dev/null +++ b/lcs/ctl/clean_sessions.py @@ -0,0 +1,31 @@ +import time +import sys +import os + +import lcs +from lcs import sessions + +def clean_vhost_sessions(): + manager = sessions.StorageSessionManager() + one_week_ago = time.time() - 2*86400 + one_month_ago = time.time() - 30*86400 + for session_key in manager.keys(): + try: + session = manager.get(session_key) + except AttributeError: + del manager[session_key] + continue + if session._access_time < one_week_ago or session._creation_time < one_month_ago: + del manager[session.id] + +def clean_sessions(args): + publisher = lcs.create_publisher() + + if '--single_host' in args: + clean_vhost_sessions() + else: + hostnames = os.listdir(lcs.APP_DIR) + for hostname in hostnames: + publisher.app_dir = os.path.join(lcs.APP_DIR, hostname) + clean_vhost_sessions() + diff --git a/lcs/ctl/start.py b/lcs/ctl/start.py new file mode 100644 index 0000000..88a8022 --- /dev/null +++ b/lcs/ctl/start.py @@ -0,0 +1,30 @@ +import socket +import sys +from quixote.server.scgi_server import run + +import publisher + +def start(args): + port = 3010 + single_host = False + + i = 0 + while i < len(args): + if args[i] == '--port': + port = int(args[i+1]) + i += 1 + elif args[i] == '--extra': + publisher.LcsPublisher.register_extra_dir(args[i+1]) + i += 1 + i += 1 + + try: + run(publisher.LcsPublisher.create_publisher, port=port, script_name = '') + except socket.error, e: + if e[0] == 98: + print >> sys.stderr, 'address already in use' + sys.exit(1) + raise + except KeyboardInterrupt: + sys.exit(1) + diff --git a/lcs/liberty.ptl b/lcs/liberty.ptl new file mode 100644 index 0000000..16d767c --- /dev/null +++ b/lcs/liberty.ptl @@ -0,0 +1,18 @@ +from qommon import get_cfg + +import qommon.liberty + +class LibertyDirectory(qommon.liberty.LibertyDirectory): + + def lookup_user(self, session, login): + ni = login.nameIdentifier.content + session.name_identifier = ni + nis = list(get_publisher().user_class.select(lambda x: ni in x.name_identifiers)) + if nis: + user = nis[0] + else: + return None + user.lasso_dump = login.identity.dump() + user.store() + return user + diff --git a/lcs/publisher.py b/lcs/publisher.py new file mode 100644 index 0000000..56b999c --- /dev/null +++ b/lcs/publisher.py @@ -0,0 +1,41 @@ +import os + +from Defaults import * + +try: + from lcs_cfg import * +except ImportError: + pass + +from qommon import set_publisher_class +from qommon.publisher import QommonPublisher + +from root import RootDirectory +import sessions + +from users import User + +class LcsPublisher(QommonPublisher): + APP_NAME = 'lcs' + APP_DIR = APP_DIR + DATA_DIR = DATA_DIR + ERROR_LOG = ERROR_LOG + + root_directory_class = RootDirectory + session_manager_class = sessions.StorageSessionManager + user_class = User + + def get_backoffice(cls): + import backoffice + return backoffice + get_backoffice = classmethod(get_backoffice) + + def get_admin(cls): + import admin + return admin + get_admin = classmethod(get_admin) + + +set_publisher_class(LcsPublisher) +LcsPublisher.register_extra_dir(os.path.join(os.path.dirname(__file__), 'extra')) + diff --git a/lcs/root.ptl b/lcs/root.ptl new file mode 100644 index 0000000..791bbf8 --- /dev/null +++ b/lcs/root.ptl @@ -0,0 +1,137 @@ +import os + +from quixote import get_publisher, get_response, get_session, redirect, get_session_manager +from quixote.directory import Directory +from quixote.util import StaticDirectory + +import admin +import backoffice +import liberty +from qommon import saml2 + +from qommon import errors +from qommon import logger +from qommon import get_cfg +from qommon import template +from qommon.form import * +import qommon.ident + +from users import User + + +class IdentDirectory(Directory): + def _q_lookup(self, component): + get_response().breadcrumb.append(('ident/', None)) + return qommon.ident.get_method_directory(component) + + +class LoginDirectory(Directory): + _q_exports = [''] + + def _q_index [html] (self): + logger.info('login') + ident_methods = get_cfg('identification', {}).get('methods', []) + + if len(ident_methods) == 0: + idps = get_cfg('idp', {}) + if len(idps) == 0: + return template.error_page(_('Authentication subsystem is not yet configured.')) + ident_methods = ['idp'] # fallback to old behaviour; liberty. + + if len(ident_methods) == 1: + method = ident_methods[0] + return qommon.ident.login(method) + else: + form = Form(enctype='multipart/form-data') + form.add(RadiobuttonsWidget, 'method', + options = [(x.key, _(x.description)) \ + for x in qommon.ident.get_method_classes() if \ + x.key in ident_methods], + delim = '
') + form.add_submit('submit', _('Submit')) + + if form.is_submitted() and not form.has_errors(): + method = form.get_widget('method').parse() + if qommon.ident.base.ident_classes[method]().is_interactive(): + return redirect('../ident/%s/login' % method) + else: + return qommon.ident.login(method) + else: + template.html_top(_('Login')) + '

%s

' % _('Select the identification method you want to use :') + form.render() + +class RegisterDirectory(Directory): + _q_exports = [''] + + def _q_index [html] (self): + logger.info('register') + ident_methods = get_cfg('identification', {}).get('methods', []) + + if len(ident_methods) == 0: + idps = get_cfg('idp', {}) + if len(idps) == 0: + return template.error_page(_('Authentication subsystem is not yet configured.')) + ident_methods = ['idp'] # fallback to old behaviour; liberty. + + if len(ident_methods) == 1: + method = ident_methods[0] + return qommon.ident.register(method) + else: + pass # XXX: register page when there is more than one ident method + + def _q_lookup(self, component): + return qommon.ident.get_method_directory(component) + + +class RootDirectory(Directory): + _q_exports = ['', 'admin', 'backoffice', 'login', 'logout', 'liberty', 'saml', + 'ident', 'register'] + + def _q_index [html] (self): + template.html_top('Lasso Conformance SP') + + + def logout(self): + logger.info('logout') + session = get_session() + if not session: + return redirect('/') + ident_methods = get_cfg('identification', {}).get('methods', []) + if not 'idp' in ident_methods: + get_session_manager().expire_session() + return redirect('/') + + # add settings to disable single logout? + # (and to set it as none/get/soap?) + return self.liberty.singleLogout() + + def _q_traverse(self, path): + session = get_session() + if session: + get_request().user = session.get_user() + else: + get_request().user = None + + response = get_response() + response.filter = {} + if not hasattr(response, 'breadcrumb'): + response.breadcrumb = [ ('', _('Home')) ] + + return Directory._q_traverse(self, path) + + def _q_lookup(self, component): + if component == 'themes': + dirname = os.path.join(get_publisher().data_dir, 'themes') + return StaticDirectory(dirname, follow_symlinks = True) + + raise errors.TraversalError() + + admin = admin.RootDirectory() + backoffice = backoffice.RootDirectory() + saml = saml2.Saml2Directory() + liberty = liberty.LibertyDirectory() + login = LoginDirectory() + register = RegisterDirectory() + ident = IdentDirectory() + diff --git a/lcs/sessions.py b/lcs/sessions.py new file mode 100644 index 0000000..b726aa6 --- /dev/null +++ b/lcs/sessions.py @@ -0,0 +1,14 @@ +#from quixote.session import Session +from quixote.html import htmltext +import users + +import random + +import qommon.sessions +from qommon.sessions import Session + +class BasicSession(Session): + pass + +qommon.sessions.BasicSession = BasicSession +StorageSessionManager = qommon.sessions.StorageSessionManager diff --git a/lcs/users.py b/lcs/users.py new file mode 100644 index 0000000..a9b620d --- /dev/null +++ b/lcs/users.py @@ -0,0 +1,32 @@ +import os + +from quixote import get_publisher + +from qommon.storage import StorableObject + +class User(StorableObject): + _names = 'users' + + name = None + email = None + is_admin = False + anonymous = False + + name_identifiers = None + identification_token = None + lasso_dump = None + + def __init__(self, name = None): + StorableObject.__init__(self) + self.name = name + self.name_identifiers = [] + self.roles = [] + + def get_display_name(self): + if self.name: + return self.name + if self.email: + return self.email + return _('Unknown User') + display_name = property(get_display_name) + diff --git a/lcsctl.py b/lcsctl.py new file mode 100755 index 0000000..f70d81a --- /dev/null +++ b/lcsctl.py @@ -0,0 +1,27 @@ +#! /usr/bin/env python + +import sys + +def print_usage(): + print 'Usage: lcsctl.py command [...]' + print '' + print 'Commands:' + print ' start start server' + print ' clean_sessions clean old sessions' + + +if len(sys.argv) < 2: + print_usage() + sys.exit(1) +else: + command = sys.argv[1] + + if command == 'start': + from lcs.ctl.start import start + start(sys.argv[2:]) + elif command == 'clean_sessions': + from lcs.ctl.clean_sessions import clean_sessions + clean_sessions(sys.argv[2:]) + else: + print_usage() + diff --git a/root/css/common.css b/root/css/common.css new file mode 100644 index 0000000..deed4d0 --- /dev/null +++ b/root/css/common.css @@ -0,0 +1,258 @@ +a { + color: #028; +} + +div.content { + margin-left: 5px; +} + +div.TextWidget textarea, +div.StringWidget input, +div.IntWidget input, +div.DateWidget input, +div.WcsExtraStringWidget input, +div.RegexStringWidget input, +div.EmailWidget input, +div.PasswordWidget input { + border: 1px inset #ccc; + margin: 1px; + padding: 2px 3px; +} + +div.SingleSelectWidget select { + margin: 1px; +} + +div.widget input.prefill-button { + border: 1px outset #ccc; + margin: 0 0 0 1em; + padding: 0px 0px; +} + +div.widget input.prefill-button:focus { + border: 1px outset #ccc; + margin: 0 0 0 1em; + padding: 0; +} + + +div.widget textarea.readonly, +div.widget input.readonly { + border: 1px solid #ccc; + background: #eee; + margin: 0 0 0 1em; +} + +div.TextWidget textarea:focus, +div.DateWidget input:focus, +div.StringWidget input:focus, +div.IntWidget input:focus, +div.WcsExtraStringWidget input:focus, +div.RegexStringWidget input:focus, +div.EmailWidget input:focus, +div.PasswordWidget input:focus { + border: 2px solid #ccf; + /*margin: 0px; */ + padding: 1px 2px; +} + +div.AccountSettingWidget label { + padding-right: 2em; +} + +div.SubmitWidget input, input[type=submit] { + margin-top: 1em; + border: 1px outset #ccc; +} + +div.form .title, form.quixote .title { + font-weight: bold; +} + +div.errornotice { + background: #fd6; + border: 1px solid #ffae15; + margin: 0em 1em 1em 1em; + padding: 5px; +} + +div.infonotice { + background: #7b95a6; + border: 1px solid #153eaf; + margin: 0em 1em 1em 1em; + padding: 5px; +} + +div.error { + color: black; + font-weight: bold; + background: transparent url(warning.png) top left no-repeat; + padding-left: 20px; +} + +div.buttons div.SubmitWidget, +div.buttons div.SubmitWidget div.content { + display: inline; +} + +div.buttons br { display: none; } + +div.widget { + margin-bottom: 0.5em; + clear: both; +} + +input[type="submit"][name="submit"] { + font-weight: bold; +} + +div.form pre { + overflow: scroll; +} + + +div#error h1 { + margin: 0; +} + +div#error { + width: 40em; + max-width: 500px; + margin: 15% auto; + background: white; + border: 1px solid #999; + padding: 1em; +} + +div.hint { + font-size: 80%; +} + +span.required { + background: transparent url(required.png) 0px 0.5ex no-repeat; + padding: 0 0 0 24px; + margin-left: 1ex; + overflow: hidden; + color: white; +} + +div.buttons { + margin-top: 1em; +} + +div.RadiobuttonsWidget div.content { + display: block; +} + +div.error-page { + margin: 1em; +} + +pre#exception { + overflow: scroll; + padding: 1em; + border: 1px solid #bbb; + background: #f0f0f0; + font-size: 90%; +} + +div.StringWidget ul { + margin: 0; + padding-left: 2em; + list-style: circle; +} + +div.inline-first div.title, +div.inline div.title { + display: inline; + float: left; + max-width: 20em; + text-align: left; + padding-top: 6px; +} + +div.inline-first div.title span.required, +div.inline div.title span.required { + margin-left: 1ex; + padding-left: 12px; +} + +div.inline-first div.content, +div.inline div.content { + margin-left: 1ex; +} + +div.inline-first div.hint, +div.inline div.hint { + display: none; +} + +div.inline-first { + float: left; + clear: both; +} + +div.inline { + float: left; + clear: none; +} + +div.inline-first div.content, +div.inline div.content { + margin-right: 1.5em; +} + + + +div.inline-first div.content, +div.inline div.content { + display: inline; +} + +.clear-both { + clear: both; +} + +div.CheckboxesWidget div.content ul { + list-style: none; + padding: 0; + margin: 0; +} + +div.CheckboxesWidget div.content ul.inline li { + display: inline; + margin-right: 2em; +} + +div.dataview { + clear: both; +} + +div.dataview span.label { + font-weight: bold; + display: block; +} + +div.dataview span.value { + display: block; + margin-left: 1em; +} + +form div.page, +div.dataview div.page { + border: 1px solid #aaa; + padding: 1ex; + margin-bottom: 1em; +} + +form div.page p, +div.dataview div.page p { + margin-top: 0; +} + +form div.page h3, +div.dataview div.page h3 { + margin: 0; + margin-bottom: 1ex; +} + diff --git a/root/css/dc2/admin.css b/root/css/dc2/admin.css new file mode 100644 index 0000000..dde1419 --- /dev/null +++ b/root/css/dc2/admin.css @@ -0,0 +1,305 @@ +@import url(../common.css); + +html, body { + margin: 0; + background: white url(page-bg.png) repeat-y; +} + +div#main-content { + margin-left: 160px; + margin-top: -10px; + margin-right: 20px; +} + +div#main-content h1 { + color: #006699; + font-size: 120%; +} + +div#main-content h2 { + color: #006699; + font-size: 115%; +} + +div#main-content h3 { + color: #006699; + font-size: 108% +} + + + +div#header { + margin: 0; + background: white url(head-bg.png) repeat-x; + height: 58px; +} + +ul#menu { + background: transparent url(head-logo.png) no-repeat; + width: 177px; + margin: 0; + padding: 80px 0 0 5px; +} + +a { + color: #0066cc; + text-decoration: none; + border-bottom: 1px dotted #ff9900; +} + +p.commands a { + border: 0; +} + +ul#menu a { + font-weight: bold; +} + +ul#menu li.active a { + border-bottom: 1px solid #ff9900; +} + +ul#menu li { + font-size: 90%; + margin-bottom: 1em; + max-width: 130px; +} + +div#footer { + display: none; +} + +ul.user-info { + position: absolute; + margin: 0; + padding: 0; + right: 25px; + top: 13px; + font-size: 70%; + font-weight: bold; +} + +ul.user-info li { + display: inline; + padding-left: 10px; +} + +/** end of dc2 changes **/ + + + +ul.biglist { + margin: 0; + padding: 0; +} + +ul.biglist li { + list-style-type: none; + margin: 4px 0; + padding: 0 2px; + border: 1px solid #888; + background: #ffe; + clear: both; +} + +ul.biglist li p.details { + display: block; + margin: 0; + color: #555; + font-size: 80%; +} + + +ul.biglist li p.commands { + float: right; + margin-top: -17px; +} + +ul.biglist li p.commands img { + padding-right: 5px; +} + +a img { + border: 0; +} + +td.time { + text-align: right; +} + +ul.biglist li.disabled, ul.biglist li.disabled p.details { + color: #999; + background: #ddd; +} + + +dl dt { + margin : 0; + padding : 0 0 0 0; +} + +dl dd { + margin : 0.3em 0 1.5em 10px; +} + + +img.theme-icon { + float: right; + margin: -16px 4px 0px 3px; + border: 1px solid #999; +} + +div#new-field table { + margin: 0; + padding: 0; +} + +div#new-field div.widget { + margin: 0; + padding: 0; +} + +div#new-field div.buttons { + margin: 0; + padding: 0; +} + +div#new-field div.buttons input { + margin: 0; + padding: 0; +} + +div#new-field { + border: 1px solid #888; + background: #ffe; + margin: 2em 0 4px 0; + padding: 0 2px; +} + +div#new-field div.widget { +} + +div#new-field h3 { + margin: 0; + font-size: 100%; +} + +div#new-field br { + display: none; +} + +div#new-field p.commands { + float: right; + margin-top: -17px; + margin-right: 3px; +} + +div.WorkflowStatusWidget { + border-left: 1px solid black; +} + +p#breadcrumb { + background: #e6e6e6; + -moz-border-radius: 6px; + padding: 3px 8px; + font-size: 80%; + border: 1px solid #bfbfbf; +} + +/** steps **/ +#steps { + height: 32px; + margin-bottom: 1em; + background: #f0f0f0; + color: #aaa; +} + +#steps ol { + list-style: none; + padding: 0 20px; +} + +#steps li { + display: inline; + padding-right: 1em; + display: block; + float: left; + width: 30%; + list-style: none; +} + +#steps ol ul { + display: none; +} + +#steps span.marker { + font-size: 26px; + padding: 2px 9px; + font-weight: bold; + color: white; + text-align: center; + background: #ddd; + border: 1px solid #ddd; + -moz-border-radius: 0.7ex; +} + +#steps li.current span.marker { + background: #ffa500; + border: 1px solid #ffc400; +} + +#steps span.label { + font-size: 90%; +} + +#steps li.current span.label { + color: black; +} + +#steps ol ul { + display: none; +} + + +/** logs **/ +form#other-log-select { + margin-top: 2em; + padding-top: 1em; + border-top: 1px solid #999; +} + +form#other-log-select select { + margin: 0 1em; +} + +tr.level-error td { + border: 1px solid #800; + background: red; +} + +tr.level-error td.message { + font-weight: bold; +} + +table.stats { + margin: 1ex 0; +} + +table.stats thead th { + text-align: left; +} + +table.stats td { + padding-left: 1em; +} + +form.inplaceeditor-form { + margin: 0; + padding: 0; +} + +form.inplaceeditor-form input { + margin: 0; + padding: 0; +} + diff --git a/root/css/dc2/head-bg.png b/root/css/dc2/head-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..cfad19815bd44310d94d35a7c24e69404c10d1cc GIT binary patch literal 356 zcmeAS@N?&q;$mQ6;PUiv2?EhTY{kJ0B==3TPy$lS#ZI0f92^|CANoH4a-#x#LR|m< z|Nr#yrKgX8=;D)y7al)2|MKj_<8<;Zx4o?QEPyne2 z&QB{TPb^Ah@J&oE%Fj(r$xKvm%PdMQ&o9a@R`8A3JyA;qs5ZpY#WAE}&fQ}>MVlBH zSPzBBr%?CiI{N7&|MM7Vre`bJ01P{X=w`HQKw9e!ILY2`iAlS<7_)jt*X vNXPHgv|ksUZZ5Xy*dB3It?Z*<{X_0Ak43*$r=^?#n$O_r>gTe~DWM4f_#UJI literal 0 HcmV?d00001 diff --git a/root/css/dc2/head-logo-empty.png b/root/css/dc2/head-logo-empty.png new file mode 100644 index 0000000000000000000000000000000000000000..684e8f8e5df5c43bbb7fe399b82e1fda0b1b1a4f GIT binary patch literal 829 zcmV-D1H$}?P)i706GW)0001RJTHj=000JJOGiWi{{a60 z|De66lK=n!32;bRa{vGf5&!@T5&_cPe*6Fc00(qQO+^RR2M!hj4wq?e_5c6^%Sl8* zRCwC$o6Sy>K@^6IQM3!?&j5iK7a)}d7~SYX1H=SygBR`AYw#N2n=?P>Z^lJkXk#?N zw)raWa|4r1pS*fvvvYXfXO}qDgF!GtGl_^ zT5D^q3R-KopB-S~RZul|_puc2V=Hbg1->g^U>nR-)y#}VRMgB=ZLGz=IRO@p*4p^v z7ZGMIETt4KA|fneYF>Xt1^UEY5kf^(trX5Vr<_yDDP?A6W)byqr>Mw@yD~3aRYf$H z5@U)KDaII6N?AlibbaIL-ri_)YkOs7b$fdUed12c3uex_#Fzq6=mJs9xja~Y`1ak& z{)^Gt+WJ&>QLq4*nTj&=*ao5)0);^C_2l&Y==k+C-zV^nxgaCMnT05Ifx52ix)IFn2S&lb#R!e>G+#1E<86O z&?kCI^;&B)F)1Rfs&;U2I6o(#L_f=fyQ!O)s_NSM#_HKg8 literal 0 HcmV?d00001 diff --git a/root/css/dc2/head-logo.png b/root/css/dc2/head-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..684e8f8e5df5c43bbb7fe399b82e1fda0b1b1a4f GIT binary patch literal 829 zcmV-D1H$}?P)i706GW)0001RJTHj=000JJOGiWi{{a60 z|De66lK=n!32;bRa{vGf5&!@T5&_cPe*6Fc00(qQO+^RR2M!hj4wq?e_5c6^%Sl8* zRCwC$o6Sy>K@^6IQM3!?&j5iK7a)}d7~SYX1H=SygBR`AYw#N2n=?P>Z^lJkXk#?N zw)raWa|4r1pS*fvvvYXfXO}qDgF!GtGl_^ zT5D^q3R-KopB-S~RZul|_puc2V=Hbg1->g^U>nR-)y#}VRMgB=ZLGz=IRO@p*4p^v z7ZGMIETt4KA|fneYF>Xt1^UEY5kf^(trX5Vr<_yDDP?A6W)byqr>Mw@yD~3aRYf$H z5@U)KDaII6N?AlibbaIL-ri_)YkOs7b$fdUed12c3uex_#Fzq6=mJs9xja~Y`1ak& z{)^Gt+WJ&>QLq4*nTj&=*ao5)0);^C_2l&Y==k+C-zV^nxgaCMnT05Ifx52ix)IFn2S&lb#R!e>G+#1E<86O z&?kCI^;&B)F)1Rfs&;U2I6o(#L_f=fyQ!O)s_NSM#_HKg8 literal 0 HcmV?d00001 diff --git a/root/css/dc2/page-bg.png b/root/css/dc2/page-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..0c1756b6e91cadf45ff3754240dccd4b1ee986ee GIT binary patch literal 224 zcmeAS@N?&q;$mQ6;PUiv2?EkRAT|p#ketH4`UH?-E_U(^;o#u7{m}mbkSi466XN>j z)xCRnE?ziyWZ#}m%a+WUJgLib-f~T#EN6j7WHAE+w-5+3UbFS$q5T&477F!9h?%F?AEJzU4lVffJOR->ZPkdH4L7v KelF{r5}E+~OGg&~ literal 0 HcmV?d00001 diff --git a/root/css/ico_user.png b/root/css/ico_user.png new file mode 100644 index 0000000000000000000000000000000000000000..764f505d3306a28a9d85a3ed33c7a73de2aec810 GIT binary patch literal 1331 zcmeAS@N?&q;$mQ6;PUiv2?EkWAk4uGBohVdQh^k6v6E*A2M5RPhyD*37?`F6_=LFr z|Ns5>|Nnn}eg6IH<)1HCK79W6>BH+!?;qW}cklGo2lp?oKXc~H>C>lAojP^$Tq!+3_V@igPa+L`(@v<;@2lvh^8*3R0KD;`%ht*op}b(&#Z^^BtO68#DK zaphCDXNYY}7mF;Ll2=q3Ryrl7Xiiwkr2PDX1(~wCUAiH~6GMt71{Y3<$#2NX$Ywj#-E)$oK5DOMoZ|N9)4pnn6 zX?tM}V^a@p9z8=Yb6p-e0|QAtbzdb0RaI3hF$N1U1}P~i9SIg$L1syQW_~ViPG$yX zCPqd^U>aot;?gxgRspFyB|(0{{}F%z!5khe-{LD@fd1qx@Q5sCV36Je!i>ANFW3zf zR4j3gC<)F_D=AMbN@eg(OfJgLO-;#6RB+2IN-fVX$}Uzg(KC3Zre4axz^LTu;uumf zC;34e^ZyeX(E?`~#4kQfND#a3&cXRfi^F*1#?2Ys2U?``=FUH!nwFfev4l%>R+ThI z*r}B(D?76sT2$7@?ydZMs?^+fuY_iH$Ap8P{Vy`^pAJ2>Yx2`rMJGP)IdkXMew)L0 z#!7W}oRH2EQ}Y!&EOHJ{{CoVm-~Eljf;oSt>|RR&Q zouqM^jBz82--kuIug%s)Zq9xEF4eN>!iPsw1I#yUF}^lQu!Tj0p>AiI^u=axM_|x0 Nc)I$ztaD0e0swqsyfpv- literal 0 HcmV?d00001 diff --git a/root/css/img/bulle.png b/root/css/img/bulle.png new file mode 100644 index 0000000000000000000000000000000000000000..83e856d39a68f5d38f27672cd2f6924f47d2d4be GIT binary patch literal 199 zcmeAS@N?&q;$mQ6;PUiv2?Eg|%)tyKcfGOi2U7L{J|V9E|NoykbLP8u?|%IJ@$uuw zKY#xK!H=ImzkUG%5b^QD$B!RA{QdJ6DDx8tegMJGFJHd`C4mxrMZIoIB}V>hu5ks$2@sOs3^9FJNw-oP6%uu2pe!9|WGBnSFBEruClNR2B<9 mS8e<7=GdLHm)BSH+vi2dF}gcQotFff$KdJe=d#Wzp$PyqnrpxS literal 0 HcmV?d00001 diff --git a/root/css/img/day-date.png b/root/css/img/day-date.png new file mode 100644 index 0000000000000000000000000000000000000000..e66f36031426ffda47f773bbd96aed47bfc27c8f GIT binary patch literal 86 zcmeAS@N?&q;$mQ6;PUiv2?EkgAk4@NBt0tTrvoXr0G|-o|NsA=IdjJ1)v9Ko2#=?W iV@SoVe;t4Me|`PoPm%kKM{oSSH>_E=Z(dqRT-=fG)iQ;TRNfT) zU1(`ttJZ(-S;U2%3yYnewVvgkC2pdZ5i_W{C;z@nYk%_gd-VFlUpxN!RLZxh%v&_& z%5qi<&4Yei1;I^oL>>$d&PrKV;WmYikFWBv4E zMn<5x&3=3T?KZW4AAb7t*Y8f@_rEoF_piS4;j4Xd-(SnmrEhi@zWdK$`}_0L*H^zi z`CR&Mz5n6MfB*K?H}8M^`OD9buj=X_Ki+O1w|Ad>o{yQ!;gn%d_#ggLQ~Lb%?d`|k z9z6c}`=7eF`Es(gOB6Yr1X{=JOr(YsS~uDAH!(~AEL%YQA^ z^a`4Shw%RAh)aRq&i6%o;xE+MuD)aZ_4zl|0JZ4{l@IsV>aE>&I~O1Nmv*}}@!^p_ z>trkn9v*JzqaJmBG9-0_!squFNGgW25k;>+1Hn=ZsKA35MJx=SDw7DI1_l-(LTV;) z0Ba?JMkxsJ!&87j?CWpBBH#I@z5c%cif(t@%UgHr*ScQ&&yehOeU<5k)Y}&07#^WA3ss{K;7VfNK+XWvMc%`%t#W$tA=>GNi<>x;}zd|K^%NKx-U!}R?t ze{V{1dLEs+-zrTa*TD#F)`T_mXL2t5yjSUd?`BR8mfip4cHgYtoIW+mcL)NXe8zN=0m) TrXR35ULKwi1duWUhojP#f80SF+GY=jv25ko>buObdSwK|`p00i_ I>zopr0P}z$P5=M^ literal 0 HcmV?d00001 diff --git a/root/css/img/linkscat.png b/root/css/img/linkscat.png new file mode 100644 index 0000000000000000000000000000000000000000..f20debc49b06a256815c1cfe2ad70443cd74a9d1 GIT binary patch literal 101 zcmeAS@N?&q;$mQ6;PUiv2?EkQAk4@NBy)D3TLPrm0(?ST|Ns9#bLPyx0Iy9z5ou2s x$B>F!$q5Y1W)eo$2NLQV9QQRg%Aa9nX3$;6v}aO8#5SNZ22WQ%mvv4FO#l`w9R2_R literal 0 HcmV?d00001 diff --git a/root/css/img/page.png b/root/css/img/page.png new file mode 100644 index 0000000000000000000000000000000000000000..bad8ca5e56f042b553919225f04050c1cc9158ee GIT binary patch literal 180 zcmeAS@N?&q;$mQ6;PUiv31VPiRsgc)SeSvN+Oc$LAf*`K6XJUM^yy2NF5SF&^X}cd zj~+dG_UzfqmoHzxe*O0C+jsBY{r~@8;`_eyK*j!^E{-7;x87cK6l74~Ibh&$$|tec zSte>m_uGkH8P6(j%C5?H-&XTNR6v1=$;F|8gF}FkQ3(fokz8$I2kX(T!3u^zqZvG1 L{an^LB{Ts5gBe3{ literal 0 HcmV?d00001 diff --git a/root/css/img/search.png b/root/css/img/search.png new file mode 100644 index 0000000000000000000000000000000000000000..b8836f8cc0845ca9e00620c7c54ecbd78621513f GIT binary patch literal 135 zcmeAS@N?&q;$mQ6;PUiv2?Eg|%)|^NKg6i(0V$pUpAgso|NqaNIdk{!-S6JL)2*M{ z1Qcg13GxeOaCmkj4am{*ba4!+xRo5>n~*T0xVdrRftJRDjjT@o44*C>m2O&79(B0`=n ojv*Ddk{=`pBm^WBBxEo!FfC&E8R1+Y1e9g)boFyt=akR{07se{XaE2J literal 0 HcmV?d00001 diff --git a/root/css/img/top.jpg b/root/css/img/top.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1a4418f8b24dad35ff00cf78b0a4da51ce95abc5 GIT binary patch literal 3995 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 Z4B~!_AA4OU`~k`^c)I$ztaD0e0su?BBRl{A literal 0 HcmV?d00001 diff --git a/root/css/warning.png b/root/css/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..6ec364d42246016151c438b914403f8cd23532a7 GIT binary patch literal 808 zcmeAS@N?&q;$mQ6;PUiv2?EjrAk4uAB;`N1t?NkKg;GdDLeSzRHxBsDWHwMa*SE2E^Oz)D{q zBB7UCTAZ1jk(iUCmt4igm0XmXSOPS*JhLQ2p(G~oeXMW6h;>-^h00{icHX20c_ylRsA z``9bLE^jf}u~B-~zwP&5r&@h`Ilm$Hz|DKXZ?m4yGFh;rBzuzb^G7$P2xPBXVEE!$ zu+myPwM&5y7$>k?a6R$!#Hy^jcN3iwxwbTK{K&~LX{C-*)9uv9PUT$PHEql8;H~Pwej$VLn@(K9D%rsJ!W#`wZpavB zaR2UbJ(bSBOtjr<`hzXm%r~+bZzY{QZFu#^oT)wkv>yNXcl8OE(ee2BTp_N?wOSV% zwl6%!HA$N@_=~Mi!H(yu=D+u!W7Js7adjFai;lyqcbgUWE!lL!$7tbByW^dALtUEY zNfz!?sFC<}a`nN&hKVmjF0kJDFyq#~{KfNgFLJ%oiVgYkX2vo{wYljPI)^V5v{~!# zEByaeVzKZ}rUITFX$KD3vAUkxvEHmg3{pzW`xuN$-=>OlTpZ4XV?O9odQ h%DmeCzr6mJD|6;bNn6Ha3BVN0;OXk;vd$@?2>{H9SZM$N literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..441939e --- /dev/null +++ b/setup.py @@ -0,0 +1,36 @@ +#! /usr/bin/env python + +import os +import distutils.core +from quixote.ptl.qx_distutils import qx_build_py + +local_cfg = None +if os.path.exists('lcs/lcs_cfg.py'): + local_cfg = file('lcs/lcs_cfg.py').read() + os.unlink('lcs/lcs_cfg.py') + +def data_tree(destdir, sourcedir): + extensions = ['.css', '.png', '.jpeg', '.jpg', '.xml', '.html', '.js'] + r = [] + for root, dirs, files in os.walk(sourcedir): + l = [os.path.join(root, x) for x in files if os.path.splitext(x)[1] in extensions] + r.append( (root.replace(sourcedir, destdir, 1), l) ) + if 'CVS' in dirs: + dirs.remove('CVS') + return r + +distutils.core.setup( + name = 'lcs', + version = '0.0.0', + maintainer = 'Frederic Peters', + maintainer_email = 'fpeters@entrouvert.com', + url = 'http://lasso.entrouvert.org', + package_dir = { 'lcs': 'lcs' }, + packages = ['lcs', 'lcs.admin', 'lcs.backoffice', 'lcs.ctl', 'lcs.qommon'], + cmdclass = {'build_py': qx_build_py}, + scripts = ['lcsctl.py'], + data_files = data_tree('share/lcs/web/', 'root/') + ) + +if local_cfg: + file('lcs/lcs_cfg.py', 'w').write(local_cfg)