Initial revision

git-svn-id: svn://localhost/lasso-conform/trunk@2 2a3a78c3-912c-0410-af21-e1fb2d1df599
This commit is contained in:
fpeters 2006-11-06 14:05:16 +00:00
parent 07c8faa4be
commit ffbefbbfa4
43 changed files with 2128 additions and 0 deletions

1
lcs/.cvsignore Normal file
View File

@ -0,0 +1 @@
lcs_cfg.py

3
lcs/Defaults.py Normal file
View File

@ -0,0 +1,3 @@
APP_DIR = "/var/lib/lcs"
DATA_DIR = "/usr/share/lcs"
ERROR_LOG = None

10
lcs/__init__.py Normal file
View File

@ -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

2
lcs/admin/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from root import RootDirectory, register_page

165
lcs/admin/logger.ptl Normal file
View File

@ -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:
'<a href="download?logfile=%s">%s</a>' % (logfile, _('Download Raw Log File'))
else:
'<a href="download">%s</a>' % _('Download Raw Log File')
user_color_keys = {}
last_date = None
'<table id="logs">\n'
'<thead> <tr>'
' <th>%s</th>' % _('Time')
' <th>%s</th>' % _('User')
' <th>%s</th>' % _('Message')
'<tr></thead>\n'
'<tbody>\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)])
'<tr class="level-%s" style="background: #%s;">' % (
d['level'].lower(), user_color_keys[user_color_key])
if (last_date != d['date']):
' <td class="time">%s&nbsp;%s</td>' % (d['date'], d['hour'][:-4])
last_date = d['date']
else:
' <td class="time">%s</td>' % (d['hour'][:-4])
if d['user_id'] == 'anonymous':
userlabel = _('Anonymous')
ip = d['ip']
' <td class="userlabel"><span title="%s">%s</span></td>' % (ip, userlabel)
elif d['user_id'] == 'unlogged':
userlabel = _('Unlogged')
ip = d['ip']
' <td class="userlabel"><span title="%s">%s</span></td>' % (ip, userlabel)
else:
try:
user = User.get(d['user_id'])
except KeyError:
userlabel = _('Unknown')
else:
userlabel = htmltext(user.name.replace(str(' '), str('&nbsp;')))
' <td class="userlabel">%s</td>' % userlabel
' <td class="message">%s</td>' % d['message']
'</tr>\n'
'</tbody>\n'
'</table>\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'])})
'<form id="other-log-select">'
_('Select another logfile:')
'<select name="logfile">'
options.sort(lambda x,y: cmp(x['date'], y['date']))
options.reverse()
for option in options:
option['since'] = str(_('Since: %s') % option['date'])[:-4]
'<option value="%(lfile)s"%(selected)s>%(since)s</option>' % option
'</select>'
'<input type="submit" value="%s" />' % _('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'))
'<h2>%s - %s</h2>' % (_('User'), self.user.name)
last_date = None
'<table id="logs">'
'<thead> <tr>'
' <th>%s</th>' % _('Time')
' <th>%s</th>' % _('Message')
'<tr></thead>'
'<tbody>'
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
'<tr>'
if (last_date != d['date']):
' <td class="time">%s&nbsp;%s</td>' % (d['date'], d['hour'][:-4])
last_date = d['date']
else:
' <td class="time">%s</td>' % (d['hour'][:-4])
' <td><a href="%s">%s</a></td>' % (d['url'], d['message'])
'</tr>'
'</tbody>'
'</table>'

9
lcs/admin/menu.ptl Normal file
View File

@ -0,0 +1,9 @@
from qommon.admin.menu import html_top
def error_page [html] (section, error):
html_top(section, title = _('Error'))
'<div id="error-page">'
'<h2>%s</h2>' % _('Error')
'<p>%s</p>' % error
'</div>'

87
lcs/admin/root.ptl Normal file
View File

@ -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] ():
"""<p>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.</p>
<p>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.</p>
<p>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.</p>
"""
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('/')
'<blockquote>'
gpl()
'</blockquote>'
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))

283
lcs/admin/settings.ptl Normal file
View File

@ -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'))
'<h2>%s</h2>' % _('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'))
'<dl> <dt><a href="identification/">%s</a></dt> <dd>%s</dd>' % (
_('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
'<dl> <dt><a href="identification/%s/">%s</a></dt> <dd>%s</dd>' % (
method, _(method_admin.title), _(method_admin.label))
'<h2>%s</h2>' % _('Customisation')
'<dl>'
'<dt><a href="sitename">%s</a></dt> <dd>%s</dd>' % (
_('Site Name'), _('Configure site name'))
'<dt><a href="language">%s</a></dt> <dd>%s</dd>' % (
_('Language'), _('Configure site language'))
'<dt><a href="themes">%s</a></dt> <dd>%s</dd>' % (
_('Theme'), _('Configure theme'))
'<dt><a href="template">%s</a></dt> <dd>%s</dd>' % (
_('Template'), _('Configure template'))
'<dt><a href="emails/">%s</a></dt> <dd>%s</dd>' % (
_('Emails'), _('Configure email settings'))
'</dl>'
'<h2>%s</h2>' % _('Misc')
'<dl>'
'<dt><a href="misc">%s</a></dt> <dd>%s</dd>' % (
_('Misc'), _('Configure misc options'))
'<dt><a href="debug_options">%s</a></dt> <dd>%s</dd>' % (
_('Debug Options'), _('Configure options useful for debugging'))
'</dl>'
'<h2>%s</h2>' % _('Import / Export')
'<dl>'
'<dt><a href="import">%s</a></dt> <dd>%s</dd>' % (
_('Import'), _('Import data from another site'))
'<dt><a href="export">%s</a></dt> <dd>%s</dd>' % (
_('Export'), _('Export data for another site'))
'</dl>'
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'))
"<h2>%s</h2>" % _('Themes')
'<form action="themes" enctype="multipart/form-data" method="post">'
theme_files = glob.glob(os.path.join(get_publisher().DATA_DIR, str('themes/*/desc.xml')))
theme_files.sort()
'<ul class="biglist">'
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 = ''
'<li>'
'<strong class="label">'
' <input name="theme" value="%s" type="radio"%s>%s</input></strong>' % (
theme, checked, getText(dom.getElementsByTagName('label')[0].childNodes))
icon_file = t.replace(str('desc.xml'), str('icon.png'))
if os.path.exists(icon_file):
'<img src="/themes/%s/icon.png" alt="" class="theme-icon" />' % theme
'<p class="details">%s<br/>by %s</p>' % (
getText(dom.getElementsByTagName('desc')[0].childNodes),
getText(dom.getElementsByTagName('author')[0].childNodes))
'</li>'
'</ul>'
'<div class="buttons">'
'<input type="submit" name="submit" value="%s" />' % _('Submit')
'</div>'
'</form>'
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'))
'<h2>%s</h2>' % _('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'))
'<h2>%s</h2>' % _('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'))
'<h2>%s</h2>' % _('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'))
'<h2>%s</h2>' % _('Site Name')
form.render()
else:
cfg_submit(form, 'misc', ['sitename'])
redirect('.')

255
lcs/admin/users.ptl Normal file
View File

@ -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))
'<h2>%s - %s</h2>' % (_('User'), self.user.name)
'<div class="form">'
'<div class="title">%s</div>' % _('Name')
'<div class="StringWidget content">%s</div>' % self.user.name
if self.user.email:
'<div class="title">%s</div>' % _('Email')
'<div class="StringWidget content">%s</div>' % self.user.email
if self.user.is_admin:
'<div class="title">%s</div>' % _('Roles')
'<div class="StringWidget content"><ul>'
if self.user.is_admin:
'<li><strong>%s</strong></li>' % _('Site Administrator')
'</ul></div>'
if self.user.lasso_dump:
identity = lasso.Identity.newFromDump(self.user.lasso_dump)
server = misc.get_lasso_server()
if len(identity.providerIds) and server:
'<h3>%s</h3>' % _('Liberty Alliance Details')
'<div class="StringWidget content"><ul>'
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)
'<li>'
_('Account federated with %s') % label
'<br />'
if federation.localNameIdentifier:
_("local: ") + federation.localNameIdentifier.content
if federation.remoteNameIdentifier:
_("remote: ") + federation.remoteNameIdentifier.content
'</li>'
'</ul></div>'
if get_cfg('debug', {}).get('debug_mode', False):
'<h4>%s</h4>' % _('Lasso Identity Dump')
'<pre>%s</pre>' % self.user.lasso_dump
'</div>'
def debug [html] (self):
get_response().breadcrumb.append( ('debug', _('Debug')) )
html_top('users', 'Debug')
"<h2>Debug - %s</h2>" % self.user.name
"<pre>"
self.user.lasso_dump
"</pre>"
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'))
'<h2>%s</h2>' % _('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('<p>%s</p>' % _(
"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'))
'<h2>%s %s</h2>' % (_('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:
'<p>%s</p>' % _('Liberty support must be setup before creating users.')
else:
"""<ul id="nav-users-admin">
<li><a href="new">%s</a></li>
</ul>""" % (_('New User'))
debug_cfg = get_cfg('debug', {})
users = User.select(order_by = 'name')
'<ul class="biglist">'
for user in users:
'<li>'
'<strong class="label">%s</strong>' % user.display_name
if user.email:
'<p class="details">'
user.email
'</p>'
'<p class="commands">'
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')
'</p></li>'
'</ul>'
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'))
'<h2>%s</h2>' % _('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()

View File

@ -0,0 +1,2 @@
from root import RootDirectory, register_directory, register_menu_item

15
lcs/backoffice/menu.ptl Normal file
View File

@ -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'))
'<div id="error-page">'
'<h2>%s</h2>' % _('Error')
'<p>%s</p>' % error
'</div>'

49
lcs/backoffice/root.ptl Normal file
View File

@ -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)

0
lcs/ctl/__init__.py Normal file
View File

31
lcs/ctl/clean_sessions.py Normal file
View File

@ -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()

30
lcs/ctl/start.py Normal file
View File

@ -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)

18
lcs/liberty.ptl Normal file
View File

@ -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

41
lcs/publisher.py Normal file
View File

@ -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'))

137
lcs/root.ptl Normal file
View File

@ -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 = '<br/>')
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'))
'<p>%s</p>' % _('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()

14
lcs/sessions.py Normal file
View File

@ -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

32
lcs/users.py Normal file
View File

@ -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)

27
lcsctl.py Executable file
View File

@ -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()

258
root/css/common.css Normal file
View File

@ -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;
}

305
root/css/dc2/admin.css Normal file
View File

@ -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;
}

BIN
root/css/dc2/head-bg.png Normal file

Binary file not shown.

Binary file not shown.

BIN
root/css/dc2/head-logo.png Normal file

Binary file not shown.

BIN
root/css/dc2/page-bg.png Normal file

Binary file not shown.

BIN
root/css/ico_user.png Normal file

Binary file not shown.

BIN
root/css/img/bulle.png Normal file

Binary file not shown.

BIN
root/css/img/day-date.png Normal file

Binary file not shown.

BIN
root/css/img/footer.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
root/css/img/h2.png Normal file

Binary file not shown.

BIN
root/css/img/li.png Normal file

Binary file not shown.

BIN
root/css/img/linkscat.png Normal file

Binary file not shown.

BIN
root/css/img/page.png Normal file

Binary file not shown.

BIN
root/css/img/search.png Normal file

Binary file not shown.

BIN
root/css/img/sidebarh2.png Normal file

Binary file not shown.

BIN
root/css/img/top.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
root/css/img/top.png Normal file

Binary file not shown.

318
root/css/lcs.css Normal file
View File

@ -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;
}

BIN
root/css/required.png Normal file

Binary file not shown.

BIN
root/css/warning.png Normal file

Binary file not shown.

36
setup.py Normal file
View File

@ -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)