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 0000000..cfad198 Binary files /dev/null and b/root/css/dc2/head-bg.png differ diff --git a/root/css/dc2/head-logo-empty.png b/root/css/dc2/head-logo-empty.png new file mode 100644 index 0000000..684e8f8 Binary files /dev/null and b/root/css/dc2/head-logo-empty.png differ diff --git a/root/css/dc2/head-logo.png b/root/css/dc2/head-logo.png new file mode 100644 index 0000000..684e8f8 Binary files /dev/null and b/root/css/dc2/head-logo.png differ diff --git a/root/css/dc2/page-bg.png b/root/css/dc2/page-bg.png new file mode 100644 index 0000000..0c1756b Binary files /dev/null and b/root/css/dc2/page-bg.png differ diff --git a/root/css/ico_user.png b/root/css/ico_user.png new file mode 100644 index 0000000..764f505 Binary files /dev/null and b/root/css/ico_user.png differ diff --git a/root/css/img/bulle.png b/root/css/img/bulle.png new file mode 100644 index 0000000..83e856d Binary files /dev/null and b/root/css/img/bulle.png differ diff --git a/root/css/img/day-date.png b/root/css/img/day-date.png new file mode 100644 index 0000000..e66f360 Binary files /dev/null and b/root/css/img/day-date.png differ diff --git a/root/css/img/footer.jpg b/root/css/img/footer.jpg new file mode 100644 index 0000000..2c7a79a Binary files /dev/null and b/root/css/img/footer.jpg differ diff --git a/root/css/img/h2.png b/root/css/img/h2.png new file mode 100644 index 0000000..708a113 Binary files /dev/null and b/root/css/img/h2.png differ diff --git a/root/css/img/li.png b/root/css/img/li.png new file mode 100644 index 0000000..e5aa456 Binary files /dev/null and b/root/css/img/li.png differ diff --git a/root/css/img/linkscat.png b/root/css/img/linkscat.png new file mode 100644 index 0000000..f20debc Binary files /dev/null and b/root/css/img/linkscat.png differ diff --git a/root/css/img/page.png b/root/css/img/page.png new file mode 100644 index 0000000..bad8ca5 Binary files /dev/null and b/root/css/img/page.png differ diff --git a/root/css/img/search.png b/root/css/img/search.png new file mode 100644 index 0000000..b8836f8 Binary files /dev/null and b/root/css/img/search.png differ diff --git a/root/css/img/sidebarh2.png b/root/css/img/sidebarh2.png new file mode 100644 index 0000000..97c0f8e Binary files /dev/null and b/root/css/img/sidebarh2.png differ diff --git a/root/css/img/top.jpg b/root/css/img/top.jpg new file mode 100644 index 0000000..1a4418f Binary files /dev/null and b/root/css/img/top.jpg differ diff --git a/root/css/img/top.png b/root/css/img/top.png new file mode 100644 index 0000000..b211101 Binary files /dev/null and b/root/css/img/top.png differ diff --git a/root/css/lcs.css b/root/css/lcs.css new file mode 100644 index 0000000..d38cd44 --- /dev/null +++ b/root/css/lcs.css @@ -0,0 +1,318 @@ +@import url(common.css); +/* derived from soFresh, a DotClear theme by Maurice Svay (GPL) + * http://www.svay.com/files/soFresh/theme-sofresh-1.2.zip */ + +html, body { + font-family: sans-serif; + text-align: center; + background: #eee; + color: black; +} + +div#page { + width: 800px; + margin: 2em auto; + text-align: justify; + background: white url(img/page.png) repeat-y; + color: black; +} + +#top { + color: #09F; + background: #FFF url(img/top.jpg) no-repeat; + height: 100px; + margin: 0; +} + +#top h1 { + margin: 0; + padding-left: 30px; + padding-right: 30px; + line-height: 100px; + height: 100px; + overflow: hidden; +} + +#footer { + background: #FFF url(img/footer.jpg) no-repeat; + color: #999; + text-align: center; + min-height: 30px; +} + +div#main-content { + margin: 0 2em; +} + +div#main-content h1 { + color: #09F; +} + +#steps { + height: 32px; + margin-bottom: 1em; +} + +#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 { + border: 1px solid #ffa500; + margin-left: 2em; + background: #ffdb94; + margin-bottom: 1em; + padding: 0; + width: auto; +} + +#steps li li { + display: block; + width: auto; + float: none; + text-align: left; + margin-left: 1em; + font-size: 90%; +} + +#steps li li.current { + color: black; +} + + +#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 { + background: #f0f0f0; + color: #aaa; +} + +#steps ol ul { + display: none; +} + +#steps ol li.current ul { + display: block; +} + +form { + clear: both; +} + +p#receiver { + margin: 0; + margin-left: 2em; + margin-top: -0.7em; + margin-bottom: 1em; + padding: 2px 5px; + border: 1px solid #ccc; + float: left; + background: #ffe; +} + +p#login, +p#logout { + margin-top: 2em; + text-align: right; +} + +p#login a, p#logout a { + text-decoration: underline; + /* + text-decoration: none; + -moz-border-radius: 2em !important; + padding: 1px 6px !important; + border: 1px solid #ccc !important; + border-bottom: 2px solid #999 !important;*/ +} + +p#login a:hover { + background-color: #ddeeff; +} + +h2#submitted, h2#done { + margin-top: 2em; + color: #09F; + font-size: 130%; +} + +h2#done { + margin-top: 1em; +} + +ul li { + list-style-image: url(img/li.png); +} + +h2, h3 { + color: #09F; + margin-left: 0.5em; + margin-bottom: 0; +} + +h3 { + margin-left: 1em; +} + +table#listing { + margin: 1em 0; +} + +table#listing th { + text-align: left; + border-bottom: 1px solid #999; + background: #eee; +} + +table#listing td { + padding-right: 1ex; +} + +table#listing th a { + text-decoration: none; + color: #666; +} + +table.sortable span.sortarrow { + color: black; + text-decoration: none; +} + +table#listing tr.status-new { + background: #aea; +} + +table#listing tr.status-finished, +table#listing tr.status-rejected { + color: #444; +} + +table#listing tr.status-rejected td { + text-decoration: line-through; +} + +div.question p.label { + font-weight: bold; +} + +img.bar { + border: 1px solid black; +} + +div.question table { + margin-left: 10px; +} + +div.question table td { +} + +div.question table td.percent { + text-align: right; + padding: 0 1em; + width: 6em; +} + +div.question table td.label { + width: 6em; +} + +span.user { + margin-left: 1em; + font-style: italic; +} + +a.listing { + font-size: 80%; + padding-left: 1em; +} + +a { + text-decoration: none; + color: #113; +} + +a:hover { + color: #06C; + text-decoration: underline; +} + + +#prelude { + color: #aaa; + background: transparent; + text-align: right; + position: relative; + top: -85px; + margin: 0; +} + +p#breadcrumb { + margin-top: 0; +} + +div.error-page { + margin-bottom: 2em; +} + +div.hint { + display: inline; + padding-left: 1ex; + font-style: italic; +} + +a.standalone { + background: white url(img/h2.png) top left no-repeat; + padding-left: 20px; +} + +div#welcome-message a { + text-decoration: underline; +} + +input.cancel { + margin-left: 5em; +} + +hr { + border: none; + border-top: 1px solid #666; + height: 1px; + width: 80%; +} + +div.buttons { + clear: both; +} + diff --git a/root/css/required.png b/root/css/required.png new file mode 100644 index 0000000..f9ad758 Binary files /dev/null and b/root/css/required.png differ diff --git a/root/css/warning.png b/root/css/warning.png new file mode 100644 index 0000000..6ec364d Binary files /dev/null and b/root/css/warning.png differ 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)