# HG changeset patch # User Daniele Nicolodi # Date 1307618184 -7200 # Node ID c812c3020b634ea1e07e07024a5adabb1d330b6c Initial import. diff -r 000000000000 -r c812c3020b63 .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,11 @@ +eggs/ +parts/ +bin/ + +syntax: glob +*.egg-info +*.pyc +._buildout.cfg +.installed.cfg +.\#* +*~ diff -r 000000000000 -r c812c3020b63 bootstrap.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bootstrap.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,121 @@ +############################################################################## +# +# Copyright (c) 2006 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Bootstrap a buildout-based project + +Simply run this script in a directory containing a buildout.cfg. +The script accepts buildout command-line options, so you can +use the -c option to specify an alternate configuration file. + +$Id: bootstrap.py 105417 2009-11-01 15:15:20Z tarek $ +""" + +import os, shutil, sys, tempfile, urllib2 +from optparse import OptionParser + +tmpeggs = tempfile.mkdtemp() + +is_jython = sys.platform.startswith('java') + +# parsing arguments +parser = OptionParser() +parser.add_option("-v", "--version", dest="version", + help="use a specific zc.buildout version") +parser.add_option("-d", "--distribute", + action="store_true", dest="distribute", default=False, + help="Use Disribute rather than Setuptools.") + +parser.add_option("-c", None, action="store", dest="config_file", + help=("Specify the path to the buildout configuration " + "file to be used.")) + +options, args = parser.parse_args() + +# if -c was provided, we push it back into args for buildout' main function +if options.config_file is not None: + args += ['-c', options.config_file] + +if options.version is not None: + VERSION = '==%s' % options.version +else: + VERSION = '' + +USE_DISTRIBUTE = options.distribute +args = args + ['bootstrap'] + +to_reload = False +try: + import pkg_resources + if not hasattr(pkg_resources, '_distribute'): + to_reload = True + raise ImportError +except ImportError: + ez = {} + if USE_DISTRIBUTE: + exec urllib2.urlopen('http://python-distribute.org/distribute_setup.py' + ).read() in ez + ez['use_setuptools'](to_dir=tmpeggs, download_delay=0, no_fake=True) + else: + exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py' + ).read() in ez + ez['use_setuptools'](to_dir=tmpeggs, download_delay=0) + + if to_reload: + reload(pkg_resources) + else: + import pkg_resources + +if sys.platform == 'win32': + def quote(c): + if ' ' in c: + return '"%s"' % c # work around spawn lamosity on windows + else: + return c +else: + def quote (c): + return c + +cmd = 'from setuptools.command.easy_install import main; main()' +ws = pkg_resources.working_set + +if USE_DISTRIBUTE: + requirement = 'distribute' +else: + requirement = 'setuptools' + +if is_jython: + import subprocess + + assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd', + quote(tmpeggs), 'zc.buildout' + VERSION], + env=dict(os.environ, + PYTHONPATH= + ws.find(pkg_resources.Requirement.parse(requirement)).location + ), + ).wait() == 0 + +else: + assert os.spawnle( + os.P_WAIT, sys.executable, quote (sys.executable), + '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout' + VERSION, + dict(os.environ, + PYTHONPATH= + ws.find(pkg_resources.Requirement.parse(requirement)).location + ), + ) == 0 + +ws.add_entry(tmpeggs) +ws.require('zc.buildout' + VERSION) +import zc.buildout.buildout +zc.buildout.buildout.main(args) +shutil.rmtree(tmpeggs) diff -r 000000000000 -r c812c3020b63 buildout.cfg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/buildout.cfg Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,13 @@ +[buildout] +parts = flask wsgi +develop = src + +[flask] +recipe = zc.recipe.egg +eggs = + Flask + WTForms + ordereddict + ltpdarepo + zope.testbrowser [wsgi] +interpreter = python diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/__init__.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,121 @@ +from flask import Flask, g, request, session, render_template, Markup, redirect, flash +from pkg_resources import get_distribution +import MySQLdb as mysql + +from .security import secure, require, authenticate + + +SCHEMA = 2.5 + + +app = Flask(__name__) +secure(app) + + +def connection(): + conn = getattr(g, 'db', None) + if conn is None: + conn = g.db = mysql.connect(host=app.config['HOSTNAME'], db=app.config['DATABASE'], + user=app.config['USERNAME'], passwd=app.config['PASSWORD'], + charset='utf8') + return conn + + +# open a database connection at each request +@app.before_request +def dbconnect(): + # open database connection + conn = connection() + + # get version information from package + g.version = get_distribution('ltpdarepo').version + + # validate schema revision + curs = conn.cursor() + curs.execute("""SELECT value+0 FROM options WHERE name='version'""") + g.schema = curs.fetchone()[0] + #if g.schema != SCHEMA and '/static/' not in request.url: + # return render_template('error.html', error=u'500: Needs upgrade'), 500 + + +# close it at the end of the request +@app.after_request +def dbclose(response): + g.db.close() + return response + + +# non authorized error handler +@app.errorhandler(403) +def non_authorized(error): + return render_template('error.html', error=error), 403 + + +# not found error handler +@app.errorhandler(404) +def not_found(error): + return render_template('error.html', error=error), 404 + + +@app.template_filter('breadcrumbs') +def breadcrumbs(path): + url = ['', ] + parts = [] + for item in path.split('/')[1:-1]: + url.append(item) + if item: + parts.append((item, '/'.join(url))) + out = ['home', ] + for name, href in parts[1:]: + out.append('%s' % (href, name)) + if len(out) > 1: + return Markup(u' » '.join(out)) + return '' + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + if authenticate(request.form['username'], request.form['password']): + session['username'] = request.form['username'] + url = request.args.get('next', '/') + return redirect(url) + flash('Login failed.', category='error') + + return render_template('login.html') + + +@app.route('/logout') +def logout(): + session.pop('username', None) + return redirect('/') + + +@app.route('/') +@require('user') +def index(): + curs = g.db.cursor() + curs.execute("""SELECT DISTINCT Db FROM mysql.db, available_dbs + WHERE Select_priv='Y' AND User=%s AND Db=db_name + ORDER BY Db""", session['username']) + dbs = [row[0] for row in curs.fetchall()] + return render_template('index.html', databases=dbs) + + +from .views.browse import module +app.register_module(module, url_prefix='/browse') + +from .views.profile import module +app.register_module(module, url_prefix='/user') + +from .views.databases import module +app.register_module(module, url_prefix='/manage/databases') + +from .views.users import module +app.register_module(module, url_prefix='/manage/users') + + +def main(): + app.config.from_pyfile('config.py') + app.run(debug=True) + diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/admin.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/admin.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,479 @@ +import MySQLdb as mysql +from pprint import pprint + +from config import HOSTNAME, DATABASE, USERNAME, PASSWORD + +from install import install +from upgrade import upgrade + + +def adduser(username, password='', name='', surname='', email='', telephone='', institution=''): + conn = mysql.connect(host=HOSTNAME, db=DATABASE, user=USERNAME, passwd=PASSWORD) + curs = conn.cursor() + + for host in ('localhost', '%'): + curs.execute('''CREATE USER %s@%s IDENTIFIED BY %s''', (username, host, password)) + + curs.execute('''INSERT INTO users (username, given_name, family_name, + email, telephone, institution, is_admin) + VALUES (%s, %s, %s, %s, %s, %s, 0)''', + (username, name, surname, email, telephone, institution)) + + conn.commit() + conn.close() + + +def deluser(username): + conn = mysql.connect(host=HOSTNAME, db=DATABASE, user=USERNAME, passwd=PASSWORD) + curs = conn.cursor() + + curs.execute('''DELETE FROM users WHERE username=%s''', username) + curs.execute('''SELECT Host FROM mysql.user WHERE User=%s''', username) + hosts = [row[0] for row in curs.fetchall()] + for host in hosts: + curs.execute('''DROP USER %s@%s''', (username, host)) + + conn.commit() + conn.close() + + +def passwd(username, password): + conn = mysql.connect(host=HOSTNAME, db=DATABASE, user=USERNAME, passwd=PASSWORD) + curs = conn.cursor() + + curs.execute('''SELECT Host FROM mysql.user WHERE User=%s''', username) + hosts = [row[0] for row in curs.fetchall()] + for host in hosts: + curs.execute('''SET PASSWORD FOR %s@%s = PASSWORD(%s)''', (username, host, password)) + + conn.commit() + conn.close() + + +def grant(username, database, privs): + conn = mysql.connect(host=HOSTNAME, db=DATABASE, user=USERNAME, passwd=PASSWORD) + curs = conn.cursor() + + privs = set([p.upper() for p in privs.split(',')]) + if privs.difference(frozenset(['SELECT', 'INSERT', 'UPDATE', 'DELETE'])): + raise ValueError + + for priv in privs: + curs.execute('''GRANT %s ON %s.* TO %%s@%%s''' % (priv, database), (username, '%')) + + conn.commit() + conn.close() + + +def privileges(username, verbose=False): + conn = mysql.connect(host=HOSTNAME, db=DATABASE, user=USERNAME, passwd=PASSWORD) + curs = conn.cursor() + + curs.execute('''SELECT DISTINCT Db, Select_priv, Insert_priv, Update_priv, Delete_priv + FROM mysql.db WHERE User=%s''', username) + + privs = {} + for row in curs.fetchall(): + privs[row[0]] = {'select': row[1] == 'Y', + 'insert': row[2] == 'Y', + 'update': row[3] == 'Y', + 'delete': row[4] == 'Y'} + + conn.close() + + if verbose: + pprint(privs) + return privs + + +def admin(username): + conn = mysql.connect(host=HOSTNAME, db=DATABASE, user=USERNAME, passwd=PASSWORD) + curs = conn.cursor() + + curs.execute('''UPDATE users SET is_admin = 1 WHERE username=%s''', username) + + conn.commit() + conn.close() + + +def initdb_v01(database): + conn = mysql.connect(host=HOSTNAME, db=database, user=USERNAME, passwd=PASSWORD) + curs = conn.cursor() + + curs.execute("""CREATE TABLE `ao` ( + `id` int(10) unsigned NOT NULL auto_increment COMMENT 'Unique ID of every data set in this table', + `obj_id` int(11) default NULL COMMENT 'ID of the object this data set belongs to', + `data_type` text COMMENT 'Data type of the object, see corresponding table', + `data_id` int(11) default NULL COMMENT 'ID of the data set in the corresponding table', + `description` text COMMENT 'Description of the object', + `mfilename` text, + `mdlfilename` text, + PRIMARY KEY (`id`) + ) ENGINE=MyISAM DEFAULT CHARSET=latin1""") + + curs.execute("""CREATE TABLE `bobjs` ( + `id` int(10) unsigned NOT NULL auto_increment COMMENT 'Unique ID of every data set in this table', + `obj_id` int(11) default NULL COMMENT 'ID of the object this data set belongs to', + `mat` longblob COMMENT 'Binary version of the object', + PRIMARY KEY (`id`), + KEY `object_index` (`obj_id`) + ) ENGINE=MyISAM DEFAULT CHARSET=latin1""") + + curs.execute("""CREATE TABLE `cdata` ( + `id` int(10) unsigned NOT NULL auto_increment COMMENT 'Unique ID of every data set in this table', + `xunits` text COMMENT 'Units of the x axis', + `yunits` text COMMENT 'Units of the y axis', + PRIMARY KEY (`id`) + ) ENGINE=MyISAM DEFAULT CHARSET=latin1""") + + curs.execute("""CREATE TABLE `collections` ( + `id` int(10) unsigned NOT NULL auto_increment COMMENT 'Unique ID of every data set in this table', + `nobjs` int(11) default NULL COMMENT 'Number of objects in a collection', + `obj_ids` text COMMENT 'List of objects in a collection', + PRIMARY KEY (`id`) + ) ENGINE=MyISAM DEFAULT CHARSET=latin1""") + + curs.execute("""CREATE TABLE `fsdata` ( + `id` int(10) unsigned NOT NULL auto_increment COMMENT 'Unique ID of every data set in this table', + `xunits` text COMMENT 'Units of the x axis', + `yunits` text COMMENT 'Units of the y axis', + `fs` DOUBLE default NULL, + PRIMARY KEY (`id`) + ) ENGINE=MyISAM DEFAULT CHARSET=latin1""") + + curs.execute("""CREATE TABLE `mfir` ( + `id` int(10) unsigned NOT NULL auto_increment COMMENT 'Unique ID of every data set in this table', + `obj_id` int(11) default NULL COMMENT 'The ID of the object this data set belongs to', + `in_file` text, + `fs` DOUBLE default NULL, + PRIMARY KEY (`id`) + ) ENGINE=MyISAM DEFAULT CHARSET=latin1""") + + curs.execute("""CREATE TABLE `miir` ( + `id` int(10) unsigned NOT NULL auto_increment COMMENT 'Unique ID of every data set in this table', + `obj_id` int(11) default NULL COMMENT 'ID of the object this data set belongs to', + `in_file` text, + `fs` DOUBLE default NULL, + PRIMARY KEY (`id`) + ) ENGINE=MyISAM DEFAULT CHARSET=latin1""") + + curs.execute("""CREATE TABLE `objmeta` ( + `id` int(10) unsigned NOT NULL auto_increment COMMENT 'A unique ID of every data set in this table', + `obj_id` int(11) default NULL COMMENT 'The ID of the object this data set belongs to', + `obj_type` text COMMENT 'Object type, e.g. ao, mfir, miir', + `name` text COMMENT 'Name of an object', + `created` datetime default NULL COMMENT 'Creation time of an object', + `version` text COMMENT 'Version string of an object', + `ip` text COMMENT 'IP address of the creator', + `hostname` text COMMENT 'Hostname of the ceator', + `os` text COMMENT 'Operating system of the creator', + `submitted` datetime default NULL COMMENT 'Submission time of an object', + `experiment_title` text COMMENT 'Experiment title', + `experiment_desc` text COMMENT 'Experiment description', + `analysis_desc` text COMMENT 'Analysis description', + `quantity` text COMMENT 'Quantity', + `additional_authors` text COMMENT 'Additional authors of an object', + `additional_comments` text COMMENT 'Additional comments to an object', + `keywords` text COMMENT 'Keywords', + `reference_ids` text COMMENT 'Reference IDs', + `validated` tinyint(4) default NULL COMMENT 'Validated', + `vdate` datetime default NULL COMMENT 'Validation time', + `author` TEXT DEFAULT NULL COMMENT 'Author of the object', + PRIMARY KEY (`id`) + ) ENGINE=MyISAM DEFAULT CHARSET=latin1""") + + curs.execute("""CREATE TABLE `objs` ( + `id` int(10) unsigned NOT NULL auto_increment COMMENT 'Unique ID of every object in this database', + `xml` longtext COMMENT 'Raw XML representation of the object', + `uuid` text COMMENT 'Unique Global Identifier for this object', + `hash` text COMMENT 'MD5 hash of an object', + PRIMARY KEY (`id`) + ) ENGINE=MyISAM DEFAULT CHARSET=latin1""") + + curs.execute("""CREATE TABLE `transactions` ( + `id` int(10) unsigned NOT NULL auto_increment COMMENT 'Unique ID of every data set in this table', + `obj_id` int(11) default NULL COMMENT 'ID of the object the transaction belongs to', + `user_id` int(11) default NULL COMMENT 'ID of the User of the transactions', + `transdate` datetime default NULL COMMENT 'Date and time of the transaction', + `direction` text COMMENT 'Direction of the transaction', + PRIMARY KEY (`id`) + ) ENGINE=MyISAM DEFAULT CHARSET=latin1""") + + curs.execute("""CREATE TABLE `tsdata` ( + `id` int(10) unsigned NOT NULL auto_increment COMMENT 'Unique ID of every data set in this table', + `xunits` text COMMENT 'Units of the x axis', + `yunits` text COMMENT 'Units of the y axis', + `fs` DOUBLE default NULL COMMENT 'Sample frequency [Hz]', + `nsecs` DOUBLE default NULL COMMENT 'Number of nanoseconds', + `t0` datetime default NULL COMMENT 'Starting time of the time series', + PRIMARY KEY (`id`) + ) ENGINE=MyISAM DEFAULT CHARSET=latin1""") + + curs.execute("""CREATE TABLE `users` ( + `id` int(10) unsigned NOT NULL auto_increment COMMENT 'Unique ID of every data set in this table', + `firstname` text COMMENT 'The first name of the user', + `familyname` text COMMENT 'The family name of the user', + `username` text COMMENT 'The username/login of the user', + `email` text COMMENT 'The email address of the user', + `telephone` text COMMENT 'Telephone number of the user', + `institution` text COMMENT 'Institution of the user', + PRIMARY KEY (`id`) + ) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=latin1""") + + curs.execute("""CREATE TABLE `xydata` ( + `id` int(10) unsigned NOT NULL auto_increment COMMENT 'Unique ID of every data set in this table', + `xunits` text COMMENT 'Units of the x axis', + `yunits` text COMMENT 'Units of the y axis', + PRIMARY KEY (`id`) + ) ENGINE=MyISAM DEFAULT CHARSET=latin1""") + + conn.commit() + conn.close() + + +def initdb(database): + initdb_v01(database) + + +def create_database(database, name='', description=''): + conn = mysql.connect(host=HOSTNAME, db=DATABASE, user=USERNAME, passwd=PASSWORD) + curs = conn.cursor() + + curs.execute("""CREATE DATABASE `%s`""" % database) + curs.execute("""INSERT INTO available_dbs (db_name, name, description) + VALUES (%s, %s, %s)""", (database, name, description)) + + initdb(database) + + conn.commit() + conn.close() + + +def drop_database(database): + conn = mysql.connect(host=HOSTNAME, db=DATABASE, user=USERNAME, passwd=PASSWORD) + conn.close() + + +def setup(): + install() + upgrade() + adduser('u1', 'u1') + admin('u1') + create_database('db1', description=u'Test database One') + create_database('db2', description=u'Test database Two \2766') + populate('db1', 30) + grant('u1', 'db1', 'select') + + +def wipe(): + # delete all possible content generated during testing including + # LTPDA repository management database + + conn = mysql.connect(host=HOSTNAME, db='', user=USERNAME, passwd=PASSWORD) + curs = conn.cursor() + + # databases list + curs.execute("""SHOW DATABASES""") + databases = [row[0] for row in curs.fetchall()] + + # delete databases + for db in databases: + if db not in ('mysql', 'information_schema'): + curs.execute("""DROP DATABASE `%s`""" % db) + + # delete users + curs.execute("""DELETE FROM mysql.user + WHERE user <> 'root' and user <> ''""") + + # delete privileges + curs.execute("""DELETE FROM mysql.db""") + + # flush privileges + curs.execute("""FLUSH PRIVILEGES""") + + conn.commit() + conn.close() + + +def populate(database, nobjs): + # populate a dababase witn nobjs fake objects + + nobjs = int(nobjs) + + conn = mysql.connect(host=HOSTNAME, db=database, user=USERNAME, passwd=PASSWORD) + curs = conn.cursor() + + from datetime import datetime + import uuid + import random + import re + + words = re.split('\W+', lorem) + sentences = [s + '.' for s in [s.strip() for s in lorem.split('.')] if s] + + for i in range(nobjs): + + name = random.choice(words) + title = ' '.join(words[0:2]) + description = random.choice(sentences) + analysis = random.choice(sentences) + + curs.execute("""INSERT INTO objs (uuid) VALUES (%s)""", (str(uuid.uuid4()))) + curs.execute("""INSERT INTO objmeta (obj_id, obj_type, name, created, + version, ip, hostname, os, submitted, + experiment_title, experiment_desc, analysis_desc, + quantity, additional_authors, additional_comments, + keywords, reference_ids, validated, vdate, author) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, + %s, %s, %s, %s, %s, %s, %s, %s, %s)""", + (curs.lastrowid, 'ao', name, datetime.now(), + '0.1', '127.0.0.1', 'localhost', 'any', datetime.now(), + title, description, analysis, 'FOO', '', '', + 'testing', '', None, None, None)) + + conn.commit() + + +def main(): + import sys + import functools + + actions = {'install': install, + 'adduser': adduser, + 'deluser': deluser, + 'passwd': passwd, + 'grant': grant, + 'privs': functools.partial(privileges, verbose=True), + 'admin': admin, + 'create-database': create_database, + 'drop-database': drop_database, + 'upgrade': upgrade, + 'wipe': wipe, + 'setup': setup, + 'populate': populate} + + if len(sys.argv) > 1: + action = actions.get(sys.argv[1]) + if action is not None: + action(*sys.argv[2:]) + return + + for action in sorted(actions.keys()): + print action #, actions[action].__doc__ + + +if __name__ == '__main__': + main() + + +lorem = """Lorem ipsum dolor sit amet, consectetur adipiscing +elit. Nam at velit lacus, quis interdum ligula. Fusce imperdiet +aliquam augue, ut volutpat mi adipiscing nec. Quisque vitae augue +felis, ut ultrices sapien. Etiam accumsan convallis dignissim. Nulla +ullamcorper consequat urna, a tempor diam volutpat vitae. Nunc +sollicitudin dapibus auctor. Cras a risus neque. Duis cursus tempus +metus vel pharetra. Morbi euismod neque eget justo dapibus sit amet +fermentum arcu fringilla. Sed gravida, sem tincidunt gravida +scelerisque, turpis ligula adipiscing lorem, vitae cursus ipsum eros +ut elit. Duis non mattis diam. Curabitur eget mauris vitae lacus +sodales elementum. Maecenas auctor dapibus molestie. Sed id diam sed +eros tincidunt semper. + +Integer in massa lorem. Nam eleifend euismod ipsum a convallis. Cras +odio orci, ornare non mattis eget, pellentesque non turpis. Ut feugiat +nunc eget mi venenatis hendrerit. Ut blandit, quam id accumsan +commodo, turpis orci sollicitudin lacus, eu iaculis arcu ligula quis +dolor. Donec posuere, mauris in pellentesque aliquam, est nibh +pharetra lacus, et hendrerit diam erat sit amet purus. Curabitur et +vehicula urna. Etiam quis enim est. Nunc sagittis urna sit amet augue +euismod interdum tincidunt sem feugiat. Phasellus nisl est, ultrices +at cursus vitae, auctor at diam. + +Vivamus non diam urna. Phasellus aliquet, eros molestie vestibulum +imperdiet, eros est semper purus, et aliquet lectus eros non +eros. Vivamus laoreet diam sit amet nisi euismod sed sollicitudin +ipsum vehicula. Mauris ut tristique magna. Donec augue felis, +dignissim at mollis et, ultricies dictum magna. Mauris ac leo +mauris. Vivamus pulvinar, tellus in ullamcorper euismod, ipsum magna +rutrum erat, eu convallis nibh dui id ante. Proin eu tincidunt +velit. Fusce ipsum massa, luctus quis malesuada at, porta sed +nibh. Nam molestie fringilla mi, eget sagittis purus accumsan +ut. Donec luctus faucibus tempus. Aliquam erat volutpat. Vivamus +egestas accumsan libero, eu rutrum nunc placerat sit amet. Quisque +iaculis dictum lorem, eu adipiscing mi aliquet placerat. Maecenas +eleifend, felis vel placerat viverra, lectus nisl aliquam purus, nec +dictum orci nisi mollis sapien. Duis rhoncus diam in felis tempor +rhoncus. Fusce lectus arcu, viverra in fringilla sit amet, ullamcorper +at mauris. + +Nunc ac ornare felis. In hac habitasse platea dictumst. Nam ac turpis +vitae lectus porta placerat imperdiet ac urna. Integer sed varius +diam. Praesent at neque sed arcu dictum dapibus. Quisque aliquet +adipiscing ullamcorper. Nullam adipiscing vulputate libero in +lobortis. Morbi turpis neque, vehicula et placerat et, gravida +venenatis mi. Etiam tincidunt nunc et nulla ullamcorper vestibulum ac +vitae sem. Ut malesuada adipiscing nisl et scelerisque. Nulla non +risus purus. Fusce vulputate, urna a egestas laoreet, tellus dui +convallis magna, sit amet facilisis ipsum metus id nisi. + +Suspendisse condimentum ultricies sapien, eget viverra lorem luctus +vel. Pellentesque augue sapien, rutrum sit amet volutpat ut, +ullamcorper nec odio. Donec vehicula lacus non risus mollis at +vulputate libero tristique. Ut commodo pretium nisl ut malesuada. Sed +molestie, est et varius vestibulum, sem nunc venenatis quam, a euismod +elit lorem non ligula. Proin interdum molestie placerat. Duis pulvinar +lorem sed elit ullamcorper hendrerit pellentesque enim interdum. Morbi +sit amet dolor in felis ullamcorper dictum in vel arcu. Sed fringilla +sapien nisi, vel iaculis mauris. Duis molestie luctus eleifend. Nunc +lacus tortor, mattis eget consectetur ac, volutpat et nisi. Donec in +nisi magna. + +Morbi sed risus est, et fringilla tortor. Proin ipsum ipsum, tincidunt +id scelerisque sed, ultricies vitae elit. Curabitur lobortis ipsum nec +enim egestas aliquam semper justo mattis. Duis pulvinar, augue nec +suscipit porta, nulla ante egestas turpis, ut tincidunt erat libero +vitae enim. Duis ullamcorper feugiat dui, ut dapibus urna eleifend +tristique. In vehicula, risus ut rutrum euismod, ipsum libero +dignissim leo, in mollis ante lectus nec purus. Quisque non dui vel +nunc ullamcorper iaculis. Vestibulum ante ipsum primis in faucibus +orci luctus et ultrices posuere cubilia Curae; Pellentesque habitant +morbi tristique senectus et netus et malesuada fames ac turpis +egestas. Sed condimentum elementum orci, id semper arcu egestas +nec. Nam commodo feugiat tortor, nec porttitor tortor congue sit +amet. Suspendisse aliquet, ligula ac mattis feugiat, leo odio gravida +nunc, sed accumsan nunc mauris porttitor leo. Fusce imperdiet +vestibulum sem, nec varius metus porttitor nec. Nulla facilisi. Morbi +venenatis ante at felis molestie ut placerat leo imperdiet. + +Suspendisse condimentum neque nec massa volutpat luctus. Integer a +ante non mauris rutrum lacinia et nec ligula. Proin tincidunt laoreet +sapien eu iaculis. Fusce tempus commodo metus, quis adipiscing ligula +volutpat eu. Vestibulum ultrices, magna non congue pharetra, mi lacus +imperdiet quam, accumsan malesuada magna sapien ac arcu. Vestibulum +sagittis rhoncus egestas. Class aptent taciti sociosqu ad litora +torquent per conubia nostra, per inceptos himenaeos. Nam gravida +congue sagittis. Aliquam adipiscing, tellus pulvinar interdum tempus, +eros massa elementum mi, at pulvinar mauris nisl ac diam. Lorem ipsum +dolor sit amet, consectetur adipiscing elit. Curabitur scelerisque +risus id odio viverra ac tincidunt elit lobortis. Sed rhoncus dapibus +magna, sed ultricies enim ultrices a. Vestibulum nec nibh +eros. Quisque volutpat eleifend ultricies. Mauris et orci dolor. Ut +orci nisl, sagittis vel cursus at, rutrum sit amet odio. Mauris +malesuada massa sit amet ligula lacinia lacinia. Sed a nisi libero, at +pulvinar lorem. + +Curabitur egestas commodo purus, non imperdiet diam auctor sit +amet. Duis eget lectus leo. Pellentesque lobortis risus et libero +ultricies ornare. In lobortis pharetra sapien, id pretium turpis +tempus eget. Suspendisse mollis, mauris non bibendum placerat, nisi +velit ullamcorper magna, vel imperdiet tellus ligula mattis +tortor. Sed et urna eu ipsum interdum commodo. Suspendisse vehicula +sagittis nibh, vitae congue arcu placerat at. Nulla nulla risus, +mattis quis imperdiet non, pharetra at lorem. Curabitur viverra +eleifend sem, ac dictum nulla elementum in. Pellentesque orci metus, +vestibulum sit amet ultrices et, tincidunt sed velit. Cras sapien +lectus, semper sed tincidunt a, dignissim sit amet nisl. In ultricies +odio ac est consequat nec posuere nisi scelerisque. Vivamus in sapien +eu lacus consequat tempor. Proin in purus purus. Curabitur et velit +vitae risus viverra fringilla vitae at nunc.""" + diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/config.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,8 @@ +# secret key used for session cookie encryption +SECRET_KEY = 'development' + +# database connection parameters +HOSTNAME = 'localhost' +USERNAME = 'root' +PASSWORD = '' +DATABASE = 'ltpda' diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/database.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/database.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,67 @@ +from MySQLdb.cursors import DictCursor +from flask import g +from ltpdarepo.form import Form +from wtforms.fields import TextField +from wtforms import validators +from wtforms.validators import ValidationError + + +class IDatabase(Form): + id = TextField("Id", validators=[validators.Required(), ]) + name = TextField("Name") + description = TextField("Description") + + def validate_id(form, field): + import re + expr = r'^[0-9a-zA-Z\-\._]+$' + if not re.match(expr, field.data): + raise ValidationError(u"Invalid identifier.") + + +class Database(object): + __slots__ = ('id', 'name', 'description') + + def __init__(self, id='', name='', description=''): + self.id = None + self.name = None + self.description = None + + def load(self, id): + curs = g.db.cursor(DictCursor) + curs.execute("""SELECT db_name AS id, name, description + FROM available_dbs WHERE db_name=%s""", id) + db = curs.fetchone() + if db is None: + return None + self.update(db) + return self + + def update(self, vals): + for key, value in vals.iteritems(): + setattr(self, key, value) + + def create(self): + from ltpdarepo.admin import create_database + create_database(self.id, self.name, self.description) + + def save(self): + curs = g.db.cursor() + curs.execute("""UPDATE available_dbs + SET name=%s, description=%s + WHERE db_name=%s""", (self.name, self.description, self.id)) + g.db.commit() + + def drop(self): + conn = g.db + curs = conn.cursor() + + # remove database from ltpda databases list + curs.execute('DELETE FROM available_dbs WHERE db_name=%s', self.id) + # drop database + curs.execute('DROP DATABASE `%s`' % self.id) + # revoke privileges assigned for the database + curs.execute('DELETE FROM mysql.db WHERE Db=%s', self.id) + # flush privileges + curs.execute('FLUSH PRIVILEGES') + + conn.commit() diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/form.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/form.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,55 @@ +import uuid +import wtforms +from flask import abort, request, session + +CSRF_SESSION_KEY = '_token' + + +def _generate_csrf_token(): + return str(uuid.uuid4()) + + +class Form(wtforms.Form): + """ + Subclass of WTForms `Form` class. Flask `request.form` is passed + as `formdata` argument to the constructor so can handle request + data implicitly. In addition this `Form` implementation has + automatic CSRF handling. + """ + + # token field + csrf = wtforms.fields.HiddenField() + + def __init__(self, formdata=None, *args, **kwargs): + # set token + token = session.get(CSRF_SESSION_KEY, None) + if token is None: + token = _generate_csrf_token() + session[CSRF_SESSION_KEY] = token + super(Form, self).__init__(formdata, csrf=token, *args, **kwargs) + + def process(self, formdata=None, obj=None, **kwargs): + if request.method in ('PUT', 'POST'): + if formdata is None: + formdata = request.form + # handle the case where the POST data is empty + if not formdata: + kwargs['csrf'] = None + super(Form, self).process(formdata, obj, **kwargs) + + def omit(self, *args): + for field in args: + delattr(self, field) + return self + + def update(self, obj): + for name, field in self._fields.iteritems(): + try: + field.populate_obj(obj, name) + except: + pass + + def validate_csrf(self, field): + token = session.get(CSRF_SESSION_KEY, None) + if not token or field.data != token: + abort(403) diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/install.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/install.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,80 @@ +import MySQLdb as db + +from config import HOSTNAME, DATABASE, USERNAME, PASSWORD + + +def install(): + conn = db.connect(host=HOSTNAME, db='', user=USERNAME, passwd=PASSWORD) + curs = conn.cursor() + # curs.execute('DROP DATABASE IF EXISTS `%s`' % DATABASE) + curs.execute('CREATE DATABASE `%s`' % DATABASE) + conn.close() + + conn = db.connect(host=HOSTNAME, db=DATABASE, user=USERNAME, passwd=PASSWORD) + curs = conn.cursor() + + curs.execute(''' + CREATE TABLE `available_dbs` ( + `id` int(10) NOT NULL auto_increment, + `db_name` varchar(50) NOT NULL, + `name` varchar(50) NOT NULL, + `description` text NOT NULL, + `version` INT DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `database` (`db_name`) + ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8''') + + curs.execute(''' + CREATE TABLE `user_access` ( + `user_id` int(10) NOT NULL, + `db_name` varchar(50) NOT NULL, + `select_priv` tinyint(1) NOT NULL, + `insert_priv` tinyint(1) NOT NULL, + `update_priv` tinyint(1) NOT NULL, + `delete_priv` tinyint(1) NOT NULL, + PRIMARY KEY (`user_id`, `db_name`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8''') + + curs.execute(''' + CREATE TABLE `user_hosts` ( + `user_id` int(10) NOT NULL, + `hostname` varchar(100) NOT NULL + ) ENGINE=MyISAM DEFAULT CHARSET=utf8''') + + curs.execute('''INSERT INTO user_hosts (user_id, hostname) + VALUES (0, "localhost")''') + + curs.execute(''' + CREATE TABLE `users` ( + `id` int(11) NOT NULL auto_increment, + `username` varchar(50) NOT NULL, + `password` varchar(50) NOT NULL, + `family_name` varchar(50) NOT NULL, + `given_name` varchar(50) NOT NULL, + `email` varchar(80) NOT NULL, + `institution` varchar(150) NOT NULL, + `telephone` varchar(50) NOT NULL, + `is_admin` tinyint(1) NOT NULL, + PRIMARY KEY (`id`) + ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8''') + + curs.execute(''' + CREATE TABLE `options` ( + `name` varchar(50) NOT NULL, + `value` text NOT NULL, + PRIMARY KEY (`name`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8''') + + # curs.execute(''' + # INSERT INTO `options` (`name`, `value`) + # VALUES ("plot_path", "/var/www/html/ltpdarepo/plots")''') + + # curs.execute(''' + # INSERT INTO `options` (`name`, `value`) + # VALUES ("robot_path", "/var/www/html/ltpdarepo/ltpdareporobot.rb")''') + + curs.execute(''' + INSERT INTO `options` (`name`, `value`) VALUES ("version", "2.4")''') + + conn.commit() + conn.close() diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/memoize.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/memoize.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,27 @@ +import functools + +class memoize(object): + """Decorator that caches a function's return value each time it is called. + """ + def __init__(self, func): + self.func = func + self.cache = {} + + def __call__(self, *args): + try: + return self.cache[args] + except KeyError: + value = self.func(*args) + self.cache[args] = value + return value + except TypeError: + # uncachable. better to not cache than to blow up entirely + return self.func(*args) + + def __repr__(self): + """return the function's docstring""" + return self.func.__doc__ + + def __get__(self, obj, objtype): + """support instance methods""" + return functools.partial(self.__call__, obj) diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/pagination.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/pagination.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,64 @@ +from math import ceil, floor + +class Pagination(object): + def __init__(self, current, bsize, total, items=9): + self.current = current + self.bsize = bsize + self.total = total + self.items = items + + @property + def page(self): + return self.current + + @property + def pages(self): + return int(ceil(self.total / float(self.bsize))) + + @property + def has_prev(self): + return self.current > 1 + + @property + def has_next(self): + return self.current < self.pages + + def __iter__(self): + # cache number of pages + npages = self.pages + + # return early if no ellipsization is needed + if npages < self.items: + return iter(range(1, npages + 1)) + + # begin and end of range surrounding the current page + surrounding = float(self.items - 3) / 2 + begin = self.current - int(floor(surrounding)) + end = self.current + int(ceil(surrounding)) + + # shift right within bounds + if begin <= 2: + offset = 2 - begin + begin += offset + end += offset + + # shift left within bounds + elif end >= npages - 1: + offset = npages - end - 1 + begin += offset + end += offset + + # number range augmented with first and last page + pages = range(begin, end + 1) + pages.insert(0, 1) + pages.append(npages) + + # left ellipsization if needed (with size of gap as negative number) + if pages[1] != 2: + pages[1] = 2 - pages[2] + + # right ellipsization if needed (with size of gap as negative number) + if pages[-2] != npages - 1: + pages[-2] = 1 + pages[-3] - npages + + return iter(pages) diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/security.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/security.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,144 @@ +from functools import partial, wraps +from flask import g, session, request, abort, redirect, url_for + +from .memoize import memoize + + +def authenticate(username, password): + curs = g.db.cursor() + rows = curs.execute("""SELECT User FROM mysql.user + WHERE User=%s AND Password=PASSWORD(%s)""", + (username, password)) + if rows: + return True + return False + + +def _set_identity(): + if 'username' in session: + g.identity = Identity(session['username']) + + +def secure(app): + app.before_request(_set_identity) + return app + + +class Secure(object): + def __init__(self, app): + # funny assignement required because of __setattr__ + self.__dict__['app'] = app + app.before_request(_set_identity) + + def require(self, role): + return SecurityWrapper(role) + + def route(self, *args, **kwargs): + role = kwargs.get('require', None) + if role is not None: + return RoutingWrapper(self.app, *args, **kwargs) + return self.app.route(*args, **kwargs) + + def __getattr__(self, name): + return getattr(self.app, name) + + def __setattr__(self, name, value): + return setattr(self.app, name, value) + + +def require(role): + return SecurityWrapper(role) + + +class SecurityWrapper(object): + def __init__(self, role): + self.role = role + + def __call__(self, func): + @wraps(func) + def decorated(*args, **kwargs): + if 'username' not in session: + url = request.path + if url == '/': + url = None + return redirect(url_for('.login', next=url)) + if self.role not in g.identity.roles: + abort(403) + return func(*args, **kwargs) + return decorated + + +class RoutingWrapper(SecurityWrapper): + def __init__(self, app, *args, **kwargs): + role = kwargs.pop('require') + super(RoutingWrapper, self).__init__(role=role) + self.route = app.route(*args, **kwargs) + + def __call__(self, func): + func = super(RoutingWrapper, self).__call__(func) + self.route(func) + + +class Permissions(object): + def __init__(self, username): + self.username = username + + @memoize + def __contains__(self, perm): + if perm.objtype == 'database': + curs = g.db.cursor() + curs.execute("""SELECT COUNT(*) FROM mysql.db + WHERE User=%s AND Db=%s AND Select_priv='Y'""", + (self.username, perm.objid, )) + if curs.fetchone()[0] > 0: + return True + + if perm.objtype == 'user': + if perm.objid == g.identity.username: + return True + + return False + + +class Identity(object): + def __init__(self, username): + self.username = username + + def can(self, what): + return what in self.permissions + + @property + @memoize + def roles(self): + curs = g.db.cursor() + rows = curs.execute("""SELECT id FROM users + WHERE is_admin=1 + AND username=%s""", self.username) + if rows > 0: + return set(('user', 'admin', )) + return set(('user', )) + + @property + @memoize + def permissions(self): + return Permissions(self.username) + + +class permission(object): + def __init__(self, perm, objtype, objid): + self.perm = perm + self.objtype = objtype + self.objid = objid + + def __enter__(self): + if g.identity.can(self): + return self + abort(403) + + def __exit__(self, exctype, excvalue, trace): + pass + + +view = partial(permission, 'view') +edit = partial(permission, 'edit') +delete = partial(permission, 'delete') diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/static/jquery.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/static/jquery.js Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,18 @@ +/*! + * jQuery JavaScript Library v1.6.1 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Thu May 12 15:04:36 2011 -0400 + */ +(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cv(a){if(!cj[a]){var b=f("<"+a+">").appendTo("body"),d=b.css("display");b.remove();if(d==="none"||d===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),c.body.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write("");b=cl.createElement(a),cl.body.appendChild(b),d=f.css(b,"display"),c.body.removeChild(ck)}cj[a]=d}return cj[a]}function cu(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function ct(){cq=b}function cs(){setTimeout(ct,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g=0===c})}function W(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function O(a,b){return(a&&a!=="*"?a+".":"")+b.replace(A,"`").replace(B,"&")}function N(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;ic)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function L(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function F(){return!0}function E(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(j,"$1-$2").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(g){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function H(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(H,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=d.userAgent,x,y,z,A=Object.prototype.toString,B=Object.prototype.hasOwnProperty,C=Array.prototype.push,D=Array.prototype.slice,E=String.prototype.trim,F=Array.prototype.indexOf,G={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.6.1",length:0,size:function(){return this.length},toArray:function(){return D.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?C.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),y.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(D.apply(this,arguments),"slice",D.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:C,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;y.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!y){y=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",z,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",z),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&H()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):G[A.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;if(a.constructor&&!B.call(a,"constructor")&&!B.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a);return c===b||B.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(b,c,d){a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),d=c.documentElement,(!d||!d.nodeName||d.nodeName==="parsererror")&&e.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;c
a",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};f=c.createElement("select"),g=f.appendChild(c.createElement("option")),h=a.getElementsByTagName("input")[0],j={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55$/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:h.value==="on",optSelected:g.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},h.checked=!0,j.noCloneChecked=h.cloneNode(!0).checked,f.disabled=!0,j.optDisabled=!g.disabled;try{delete a.test}catch(s){j.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function b(){j.noCloneEvent=!1,a.detachEvent("onclick",b)}),a.cloneNode(!0).fireEvent("onclick")),h=c.createElement("input"),h.value="t",h.setAttribute("type","radio"),j.radioValue=h.value==="t",h.setAttribute("checked","checked"),a.appendChild(h),k=c.createDocumentFragment(),k.appendChild(a.firstChild),j.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",l=c.createElement("body"),m={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"};for(q in m)l.style[q]=m[q];l.appendChild(a),b.insertBefore(l,b.firstChild),j.appendChecked=h.checked,j.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,j.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="
",j.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
t
",n=a.getElementsByTagName("td"),r=n[0].offsetHeight===0,n[0].style.display="",n[1].style.display="none",j.reliableHiddenOffsets=r&&n[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(i=c.createElement("div"),i.style.width="0",i.style.marginRight="0",a.appendChild(i),j.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(i,null)||{marginRight:0}).marginRight,10)||0)===0),l.innerHTML="",b.removeChild(l);if(a.attachEvent)for(q in{submit:1,change:1,focusin:1})p="on"+q,r=p in a,r||(a.setAttribute(p,"return;"),r=typeof a[p]=="function"),j[q+"Bubbles"]=r;return j}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([a-z])([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g=f.expando,h=typeof c=="string",i,j=a.nodeType,k=j?f.cache:a,l=j?a[f.expando]:a[f.expando]&&f.expando;if((!l||e&&l&&!k[l][g])&&h&&d===b)return;l||(j?a[f.expando]=l=++f.uuid:l=f.expando),k[l]||(k[l]={},j||(k[l].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?k[l][g]=f.extend(k[l][g],c):k[l]=f.extend(k[l],c);i=k[l],e&&(i[g]||(i[g]={}),i=i[g]),d!==b&&(i[f.camelCase(c)]=d);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[f.camelCase(c)]:i}},removeData:function(b,c,d){if(!!f.acceptData(b)){var e=f.expando,g=b.nodeType,h=g?f.cache:b,i=g?b[f.expando]:f.expando;if(!h[i])return;if(c){var j=d?h[i][e]:h[i];if(j){delete j[c];if(!l(j))return}}if(d){delete h[i][e];if(!l(h[i]))return}var k=h[i][e];f.support.deleteExpando||h!=a?delete h[i]:h[i]=null,k?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=k):g&&(f.support.deleteExpando?delete b[f.expando]:b.removeAttribute?b.removeAttribute(f.expando):b[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;return(e.value||"").replace(p,"")}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c=a.selectedIndex,d=[],e=a.options,g=a.type==="select-one";if(c<0)return null;for(var h=g?c:0,i=g?c+1:e.length;h=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attrFix:{tabindex:"tabIndex"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);var h,i,j=g!==1||!f.isXMLDoc(a);c=j&&f.attrFix[c]||c,i=f.attrHooks[c],i||(!t.test(c)||typeof d!="boolean"&&d!==b&&d.toLowerCase()!==c.toLowerCase()?v&&(f.nodeName(a,"form")||u.test(c))&&(i=v):i=w);if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j)return i.get(a,c);h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){var c;a.nodeType===1&&(b=f.attrFix[b]||b,f.support.getSetAttribute?a.removeAttribute(b):(f.attr(a,b,""),a.removeAttributeNode(a.getAttributeNode(b))),t.test(b)&&(c=f.propFix[b]||b)in a&&(a[c]=!1))},attrHooks:{type:{set:function(a,b){if(q.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},tabIndex:{get:function(a){var c=a.getAttributeNode("tabIndex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);c=i&&f.propFix[c]||c,h=f.propHooks[c];return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==b?g:a[c]},propHooks:{}}),w={get:function(a,c){return a[f.propFix[c]||c]?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=b),a.setAttribute(c,c.toLowerCase()));return c}},f.attrHooks.value={get:function(a,b){if(v&&f.nodeName(a,"button"))return v.get(a,b);return a.value},set:function(a,b,c){if(v&&f.nodeName(a,"button"))return v.set(a,b,c);a.value=b}},f.support.getSetAttribute||(f.attrFix=f.propFix,v=f.attrHooks.name=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&d.nodeValue!==""?d.nodeValue:b},set:function(a,b,c){var d=a.getAttributeNode(c);if(d){d.nodeValue=b;return b}}},f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})})),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}})),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var x=Object.prototype.hasOwnProperty,y=/\.(.*)$/,z=/^(?:textarea|input|select)$/i,A=/\./g,B=/ /g,C=/[^\w\s.|`]/g,D=function(a){return a.replace(C,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=E;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=E);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+f.map(m.slice(0).sort(),D).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j=0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem +)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},K=function(c){var d=c.target,e,g;if(!!z.test(d.nodeName)&&!d.readOnly){e=f._data(d,"_change_data"),g=J(d),(c.type!=="focusout"||d.type!=="radio")&&f._data(d,"_change_data",g);if(e===b||g===e)return;if(e!=null||g)c.type="change",c.liveFired=b,f.event.trigger(c,arguments[1],d)}};f.event.special.change={filters:{focusout:K,beforedeactivate:K,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&K.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&K.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",J(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in I)f.event.add(this,c+".specialChange",I[c]);return z.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return z.test(this.nodeName)}},I=f.event.special.change.filters,I.focus=I.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=d++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g0)for(h=g;h0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=U.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a=="string")return f.inArray(this[0],a?f(a):this.parent().children());return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(W(c[0])||W(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=T.call(arguments);P.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!V[a]?f.unique(e):e,(this.length>1||R.test(d))&&Q.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var Y=/ jQuery\d+="(?:\d+|null)"/g,Z=/^\s+/,$=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,_=/<([\w:]+)/,ba=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]};bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Y,""):null;if(typeof a=="string"&&!bc.test(a)&&(f.support.leadingWhitespace||!Z.test(a))&&!bg[(_.exec(a)||["",""])[1].toLowerCase()]){a=a.replace($,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bj(a,d),e=bk(a),g=bk(d);for(h=0;e[h];++h)bj(e[h],g[h])}if(b){bi(a,d);if(c){e=bk(a),g=bk(d);for(h=0;e[h];++h)bi(e[h],g[h])}}return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument|| +b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!bb.test(k))k=b.createTextNode(k);else{k=k.replace($,"<$1>");var l=(_.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=ba.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&Z.test(k)&&o.insertBefore(b.createTextNode(Z.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bp.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle;c.zoom=1;var e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.filter=bo.test(g)?g.replace(bo,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,c){var d,e,g;c=c.replace(br,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bs.test(d)&&bt.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bE=/%20/g,bF=/\[\]$/,bG=/\r?\n/g,bH=/#.*$/,bI=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bJ=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bK=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,bL=/^(?:GET|HEAD)$/,bM=/^\/\//,bN=/\?/,bO=/)<[^<]*)*<\/script>/gi,bP=/^(?:select|textarea)/i,bQ=/\s+/,bR=/([?&])_=[^&]*/,bS=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bT=f.fn.load,bU={},bV={},bW,bX;try{bW=e.href}catch(bY){bW=c.createElement("a"),bW.href="",bW=bW.href}bX=bS.exec(bW.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bT)return bT.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bO,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bP.test(this.nodeName)||bJ.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bG,"\r\n")}}):{name:b.name,value:c.replace(bG,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?f.extend(!0,a,f.ajaxSettings,b):(b=a,a=f.extend(!0,f.ajaxSettings,b));for(var c in{context:1,url:1})c in b?a[c]=b[c]:c in f.ajaxSettings&&(a[c]=f.ajaxSettings[c]);return a},ajaxSettings:{url:bW,isLocal:bK.test(bX[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML}},ajaxPrefilter:bZ(bU),ajaxTransport:bZ(bV),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a?4:0;var o,r,u,w=l?ca(d,v,l):b,x,y;if(a>=200&&a<300||a===304){if(d.ifModified){if(x=v.getResponseHeader("Last-Modified"))f.lastModified[k]=x;if(y=v.getResponseHeader("Etag"))f.etag[k]=y}if(a===304)c="notmodified",o=!0;else try{r=cb(d,w),c="success",o=!0}catch(z){c="parsererror",u=z}}else{u=c;if(!c||a)c="error",a<0&&(a=0)}v.status=a,v.statusText=c,o?h.resolveWith(e,[r,c,v]):h.rejectWith(e,[v,c,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.resolveWith(e,[v,c]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f._Deferred(),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bI.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.done,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bH,"").replace(bM,bX[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bQ),d.crossDomain==null&&(r=bS.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bX[1]&&r[2]==bX[2]&&(r[3]||(r[1]==="http:"?80:443))==(bX[3]||(bX[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bU,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bL.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bN.test(d.url)?"&":"?")+d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bR,"$1_="+x);d.url=y+(y===d.url?(bN.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", */*; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bV,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){status<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bE,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq,cr=a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g=e.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),e.animatedProperties[this.prop]=!0;for(g in e.animatedProperties)e.animatedProperties[g]!==!0&&(c=!1);if(c){e.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){d.style["overflow"+b]=e.overflow[a]}),e.hide&&f(d).hide();if(e.hide||e.show)for(var i in e.animatedProperties)f.style(d,i,e.orig[i]);e.complete.call(d)}return!1}e.duration==Infinity?this.now=b:(h=b-this.startTime,this.state=h/e.duration,this.pos=f.easing[e.animatedProperties[this.prop]](this.state,h,0,1,e.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){for(var a=f.timers,b=0;b
";f.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),d=b.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,this.doesNotAddBorder=e.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,e.style.position="fixed",e.style.top="20px",this.supportsFixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",this.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),f.offset.initialize=f.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.offset.initialize(),f.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){return this[0]?parseFloat(f.css(this[0],d,"padding")):null},f.fn["outer"+c]=function(a){return this[0]?parseFloat(f.css(this[0],d,a?"margin":"border")):null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c];return e.document.compatMode==="CSS1Compat"&&g||e.document.body["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var h=f.css(e,d),i=parseFloat(h);return f.isNaN(i)?h:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window); \ No newline at end of file diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/static/style.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/static/style.css Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,416 @@ +/* color palette inspired by http://www.colourlovers.com/palette/1133393/Green_Shades */ + +html, body { + height: 100%; + margin: 0; + padding: 0; +} + +div.left { + float: left; +} + +div.right { + float: right; +} + +body { + font-family: verdana, sans-serif; + font-size: 13px; + color: #000; +} + +a { + color: #004B6B; +} + +div.content { + margin: 0px 10em; +} + +div.footer { + margin: 0px 9.5em; +} + +div.header { + padding: 0.7em 0 1.2em 0; + background: #CFF09E; + border-bottom: #BFE08E solid 5px; +} + +div.header div { + display: block; + margin: 0px 10em; + color: #555; +} + +div.header span { + font-size: 90%; +} + +h1, h2, h3 { + font-weight: normal; +} + +h1 { + margin: 0; +} + +h1 a { + text-decoration: none; + color: inherit; +} + +h2 { + margin: 1em 0 0 0; + padding: 0; +} + +p { + margin: 0; + padding: 0; + line-height: 1.4; +} + +ul { + margin: 15px 0 15px 0; + padding: 0; + line-height: 1.4; + list-style: none; +} + +ul li:before { + content: "\00BB\0020"; + color: #666; + position: absolute; + margin-left: -19px; +} + +ul li:hover:before { + color: #000; +} + +ol { + margin: 15px 0 15px 30px; + padding: 0; + line-height: 1.4; +} + +.discrete { + color: #666; +} + +.important { + color: #BB0000; + margin: 1em 0; +} + +div.breadcrumbs, div.user { + float: left; + padding-top: 0.4em; + font-size: 90%; + color: #666; +} + +div.breadcrumbs { + float: left; +} + +div.user { + float: right; +} + +div.breadcrumbs a, div.user a { + color: inherit; + text-decoration: none; +} + +div.breadcrumbs a:hover, div.user a:hover { + border-bottom: 1px solid #666; +} + +div.clear { + width: 100%; + height: 0; + clear: both; +} + +/* data display styling */ + +p.field { + padding: 0; + margin: 0.2em 0; +} + +span.label { + color: #666; +} + +/* tables styling */ + +th { + text-align: left; + font-weight: normal; + padding: 0.2em 1em; + border-bottom: 1px solid #BFBFBF; + text-transform: lowercase; + color: #666; +} + +td { + text-align: center; +} + +/* footer */ + +div.footer { + padding: 10em 0 2em 0; + color: #AAAAAA; +} + +div.footer hr { + border: none; + height: 1px; + background-color: #BFBFBF; + padding: 0; + margin: 5px 0; + width: 100%; +} + +div.footer p { + font-size: 70%; + padding: 0; + margin: 0 5px; +} + +/* flash messages styling */ + +div.flash { + margin: 0; + padding: 0.5em 0 0 0; + color: #666; + font-size: 90%; +} + +div.flash:before { + content: "\00BB\0020"; + position: absolute; + margin-left: -19px; +} + +div.message { + color: #466719; +} + +div.error { + color: red; +} + +/* forms styling */ + +fieldset { + margin: 0; + padding: 0; + border: none; +} + +div.help { + color: #666; + font-size: 80%; +} + +div.widget { + padding-top: 0.2em; +} + +div.field { + margin-top: 0.7em; +} + +input[type=submit], #submit { + display: inline-block; + background: #E6E6E6; + border: 1px solid #BFBFBF; + padding: 5px 10px; + font-family: verdana, sans-serif; + /* font-size: 100%; */ + margin: 0.3em; +} + +input[type=submit]:hover, #submit:hover { + background: #CFF09E; +} + +/* data table */ + +table.listing { + border-collapse: collapse; + font-size: 90%; + width: 100%; + padding: 0; + margin: 2em 0 0.7em 0; +} + +.listing td, th { + padding: 0.3em 0.5em; + margin: 0; + white-space: nowrap; + border: 1px solid #ccc; + text-align: center; +} + +.listing tr:hover > td { + border-bottom: 1px solid #BB0000; +} + +td.id { + color: #BB0000; +} + +td.id, td.name { + font-family: monospace; +} + +td.name, td.title, td.description { + text-align: left; +} + +td.description { + width: 100%; +} + +tbody tr.odd { + background-color: #F7F7F7; +} + +tbody tr.even { + background-color: #FCFCFC; +} + +td a { + text-decoration: none; + color: inherit; +} + +tbody td a:hover { + border-bottom: 1px solid #666; +} + +tr.details { + display: none; +} + +/* pagination */ + +div.pagination { + font-size: 90%; + color: #666; +} + +div.pagination span { + display: inline-block; + width: 1.5em; + height: 1.5em; + text-align: center; +} + +div.pagination span.current { + color: #BB0000; +} + +div.pagination a { + display: inline-block; + text-decoration: none; + width: 100%; + text-align: center; + color: #000; +} + +.activity { + margin: 1em; +} + +.activity table { + width: 100%; + border-collapse: collapse; +} + +.activity td { + margin: 0; + padding: 0.3em; + border: none; + font-size: 90%; +} + +.activity td.bars { + padding: 0 2px; + vertical-align: bottom; + /* border-bottom: 1px solid #555; */ +} + +.activity td.bars:hover { + /* border-bottom: 1px solid #BB0000; */ +} + +.activity td div { + background: #DDD; + border-bottom: 2px solid #DDD; + width: 100%; +} + +/* search form */ + +.search { + margin: 1.2em 0; + border: 4px solid #EEE; + float: left; + -moz-border-radius: 4px; +} + +.search form { + margin: 0; + padding: 0; +} + +.search input { + margin: 0; + padding: 4px; +} + +.search input[type=text] { + border: 1px solid #BFBFBF; + border-right: none; + font-size: 100%; + height: 22px; + width: 20em; +} + +.search input[type=submit] { + border: 1px solid #BFBFBF; + background: #CFF09E; + text-align: center; + font-size: 120%; + color: #555; + height: 32px; + width: 36px; +} + +.wrapper { + display: block; +} + +.wrapper:after { + clear: both; + content: " "; + display: block; + height: 0; + overflow: hidden; + visibility: hidden; +} + +/* actions */ + +ul.actions li { + margin-left: 2em; + line-height: 1.6em; +} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/activity.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/activity.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,57 @@ +{% extends "layout.html" %} +{% block title %}{{ database.id }}{% endblock %} +{% block head %} + + +{% endblock %} +{% block body %} +

Database «{{ database.id }}»

+

{{ database.description }}

+{# +
+ +
+ +#} +
+ + + + {% set nmax = 300 %} + {% for when, n in activity %} + {% set height = n * 300 / nmax %} + + {% endfor %} + + + + + {% for when, number in activity %} + + {% endfor %} + + +
 
«{{ when }}»
+
+ +{# +
    + {% for when, number in activity %} +
  • {{ when }}: {{ number }}
  • + {% endfor %} +
+#} +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/browse.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/browse.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,51 @@ +{% extends "layout.html" %} +{% block title %}{{ database.id }}{% endblock %} +{# block head %} + + +{% endblock #} +{% block body %} +

Database «{{ database.id }}»

+

{{ database.description }}

+{% if not objs %} +

+{% else %} + + + + {% for field in fields %} + + {% endfor %} + + + + {% for obj in objs %} + + {% for field in fields %} + {% if field == 'name' %} + + {% else %} + + {% endif %} + {% endfor %} + + + + + {% endfor %} + +
{{ field }}
{{ obj[field] }}{{ obj[field]|string|truncate(60, False, '…') }}
details
+{% import "pagination.html" as p %} +{% if pagination is defined %} +{{ p.render(pagination) }} +{% endif %} +{% endif %} +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/database.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/database.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,19 @@ +{% extends "layout.html" %} +{% block title %}{{ database.id }}{% endblock %} +{% block body %} +

Database «{{ database.id }}»

+

{{ database.description|default('—'|safe, true) }}

+ +

Search database «{{ database.id }}»

+

Search objects by name

+ + +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/databases/create.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/databases/create.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,7 @@ +{% import 'forms.html' as forms %} +{% extends "layout.html" %} +{% block title %}Create database{% endblock %} +{% block body %} +

Create database

+{{ forms.render(form) }} +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/databases/drop.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/databases/drop.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,21 @@ +{% import 'forms.html' as forms %} +{% extends "layout.html" %} +{% block body %} +

Drop database «{{ database.id }}»

+

+ Are you sure you want to drop this database? + All contained data will be permanently lost. +

+
+
+{% for field in form %} +{{ forms.render_form_field(field) }} +{% endfor %} +
+
+
+
+ +
+
+{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/databases/edit.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/databases/edit.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,6 @@ +{% import 'forms.html' as forms %} +{% extends "layout.html" %} +{% block body %} +

Database «{{ database.id }}»

+{{ forms.render(form) }} +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/databases/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/databases/index.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,15 @@ +{% extends "layout.html" %} +{% block title %} Databases {% endblock %} +{% block body %} +

Databases

+

Manage existing databases:

+
    +{% for db in databases %} +
  • {{ db.id }}
  • +{% endfor %} +
+

Actions

+ +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/databases/permissions.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/databases/permissions.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,31 @@ +{% extends "layout.html" %} +{% block body %} +

Permissions for database «{{ database }}»

+
+ + + + + + + + + {% for user, priv in permissions.items() %} + + + + + + + + {% endfor %} +
UserSelectInsertUpdateDelete
+ {{ user }}
+ + +
+{% endblock %} \ No newline at end of file diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/databases/view.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/databases/view.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,12 @@ +{% extends "layout.html" %} +{% block title %}{{ database.id }}{% endblock %} +{% block body %} +

Database «{{ database.id }}»

+

Name: {{database.name }}

+

Description: {{ database.description }}

+ +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/error.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/error.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,5 @@ +{% extends "layout.html" %} +{% block title %}{{ error }}{% endblock %} +{% block body %} +

{{ error }}

+{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/form.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/form.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,5 @@ +{% import 'forms.html' as forms %} +{% extends "layout.html" %} +{% block body %} +{{ forms.render(form) }} +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/forms.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/forms.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,54 @@ +{% macro render(form) -%} +
+
+ {% for field in form %} + {{ render_form_field(field) }} + {% endfor %} +
+
+
+
+
+{%- endmacro %} + +{% macro render_form_field(field) %} + {% if field.type == "HiddenField" %} + {{ field }} + {% elif field.type == "SubmitField" %} +
+
{{ field }}
+
+ {% else %} + {% if field.errors %} +
+ {% else %} +
+ {% endif %} + {{ field.label }} {% if field.flags.required %}*{% endif %} + {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} + {% if field.description %} +
{{ field.description }}
+ {% endif %} +
{{ field }}
+
+ {% endif %} +{% endmacro %} + +{% macro view(form) -%} + {% for field in form %} + {% if field.type == "HiddenField" %} + {% elif field.type == "SubmitField" %} + {% elif field.type == "PasswordField" %} + {% else %} +

{{ field.label }}: {{ field.data }}

+ {% endif %} + {% endfor %} + + +{%- endmacro %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/index.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,21 @@ +{% extends "layout.html" %} +{% block title %} LTPDA Repository {% endblock %} +{% block body %} +

Databases

+

You have access to the following databases:

+
    + {% for db in databases %} +
  • {{ db }}
  • + {% endfor %} +
+ {% if 'admin' in g.identity.roles %} +

Manage

+

LTPDA Repository management interface:

+ + {% endif %} +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/layout.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/layout.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,47 @@ + + + + + {% block title %} {% endblock %} — LTPDA Repository + + {%- block head %}{% endblock %} + + + +
+

LTPDA Repository

+ {{ request.host }} +
+ +
+ {% block page %} + + +
+ {% if session.username is defined %} + {{ session.username }} + — + logout + {% endif %} +
+ +
 
+ + {% for category, message in get_flashed_messages(True) %} +
{{ message }}
+ {% endfor %} + + {% block body %}{% endblock %} + + {% endblock %} +
+ + + + + diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/login.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/login.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,20 @@ +{% extends "layout.html" %} +{% block title %} Login {% endblock %} +{% block body %} +

Login

+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/obj.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/obj.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,8 @@ +{% extends "layout.html" %} +{% block title %}{{ dbname }}{% endblock %} +{% block body %} +

{{ database.id }} - {{ obj.name }}

+{% for name, value in obj.iteritems() %} +

{{ name }}: {{ value }}

+{% endfor %} +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/pagination.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/pagination.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,25 @@ +{% macro render(pagination) %} + +{% endmacro %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/user.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/user.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,13 @@ +{% extends "layout.html" %} +{% block title %}{{ session.username }}{% endblock %} +{% block body %} +

User «{{ session.username }}»

+

Name:{{ user.name }} {{ user.surname }}

+

Email: {{ user.email }}

+

Institution: {{ user.institution }}

+

Telephone: {{ user.telephone }}

+ +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/users/create.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/users/create.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,7 @@ +{% import 'forms.html' as forms %} +{% extends "layout.html" %} +{% block title %} Create user {% endblock %} +{% block body %} +

Create user

+{{ forms.render(form) }} +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/users/drop.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/users/drop.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,20 @@ +{% import 'forms.html' as forms %} +{% extends "layout.html" %} +{% block body %} +

Drop user «{{ user.username }}»

+

+ Are you sure you want to drop this user? +

+
+
+{% for field in form %} +{{ forms.render_form_field(field) }} +{% endfor %} +
+
+
+
+
+ + +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/users/edit.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/users/edit.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,6 @@ +{% import 'forms.html' as forms %} +{% extends "layout.html" %} +{% block body %} +

User «{{ username }}»

+{{ forms.render(form) }} +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/users/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/users/index.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,18 @@ +{% extends "layout.html" %} +{% block title %} Users {% endblock %} +{% block body %} +

Users

+

Manage user accounts:

+ +

Actions

+ +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/users/password.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/users/password.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,7 @@ +{% import 'forms.html' as forms %} +{% extends "layout.html" %} +{% block body %} +

User «{{ session.username }}» password

+

+{{ forms.render(form) }} +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/templates/users/view.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/templates/users/view.html Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,10 @@ +{% import 'forms.html' as forms %} +{% extends "layout.html" %} +{% block body %} +

User «{{ username }}»

+{{ forms.view(form) }} + +{% endblock %} diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/tests/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/tests/__init__.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,63 @@ +import unittest +import doctest + +import zope.testbrowser.wsgi + + +class Browser(zope.testbrowser.wsgi.Browser): + def __init__(self, url='http://localhost/'): + + from ltpdarepo import app + import logging + handler = logging.StreamHandler() + handler.setLevel(logging.DEBUG) + app.logger.addHandler(handler) + + super(Browser, self).__init__(url, wsgi_app=app) + + +USERNAME = 'u1' +PASSWORD = 'u1' + + +def doctestSetUp(self): + from ltpdarepo.admin import wipe, install, upgrade, adduser, admin, create_database + wipe() + install() + upgrade() + adduser('u1', 'u1') + admin('u1') + create_database('db1') + + +def doctestTearDown(self): + from ltpdarepo.admin import wipe + wipe() + + +def suite(): + suite = unittest.TestSuite() + suite.addTest( + doctest.DocFileSuite( + 'manage-users.txt', + 'manage-databases.txt', + setUp=doctestSetUp, tearDown=doctestTearDown, + optionflags=doctest.ELLIPSIS | doctest.REPORT_ONLY_FIRST_FAILURE)) + + from ltpdarepo.tests import test_csrf + suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(test_csrf)) + + from ltpdarepo.tests import test_users + suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(test_users)) + + from ltpdarepo.tests import test_security + suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(test_security)) + + from ltpdarepo.tests import test_pagination + suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(test_pagination)) + + return suite + + +def main(): + unittest.TextTestRunner().run(suite()) diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/tests/manage-databases.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/tests/manage-databases.txt Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,68 @@ +Test setup:: + + >>> from ltpdarepo.tests import Browser + >>> from ltpdarepo.tests import USERNAME, PASSWORD + >>> browser = Browser() + +Login:: + + >>> rv = browser.open('/') + >>> browser.url + 'http://localhost/login' + + >>> browser.getControl(name='username').value = USERNAME + >>> browser.getControl(name='password').value = PASSWORD + >>> browser.getControl(name='login').click() + >>> browser.url + 'http://localhost/' + +Get databases management interface:: + + >>> browser.getLink('Databases').click() + >>> browser.url + 'http://localhost/manage/databases/' + +Create a new database:: + + >>> browser.getLink('Create new database').click() + >>> browser.url + 'http://localhost/manage/databases/create' + + >>> browser.getControl(name='id').value = 'database1' + >>> browser.getControl(name='submit').click() + >>> browser.url + 'http://localhost/manage/databases/' + >>> browser.contents + '...
Database "database1" created.
...' + +View database:: + + >>> browser.getLink('database1').click() + >>> browser.url + 'http://localhost/manage/databases/database1' + +Edit database:: + + >>> browser.getLink('Edit').click() + >>> browser.url + 'http://localhost/manage/databases/database1/edit' + + >>> browser.getControl(name='description').value = 'Test Database One' + >>> browser.getControl(name='submit').click() + >>> browser.url + 'http://localhost/manage/databases/database1' + >>> browser.contents + '...
Database "database1" modified.
...' + >>> browser.contents + '...

Description: Test Database One

...' + +Drop database:: + + >>> browser.getLink('Drop').click() + >>> browser.url + 'http://localhost/manage/databases/database1/drop' + >>> browser.getControl(name='ok').click() + >>> browser.url + 'http://localhost/manage/databases/' + >>> browser.contents + '...
Database "database1" deleted.
...' diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/tests/manage-users.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/tests/manage-users.txt Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,114 @@ +Test setup:: + + >>> from ltpdarepo.tests import Browser + >>> from ltpdarepo.tests import USERNAME, PASSWORD + >>> browser = Browser() + +Login:: + + >>> browser.open('/') + >>> browser.url + 'http://localhost/login' + + >>> browser.getControl(name='username').value = USERNAME + >>> browser.getControl(name='password').value = PASSWORD + >>> browser.getControl(name='login').click() + >>> browser.url + 'http://localhost/' + +Get users management interface:: + + >>> browser.getLink('Users').click() + >>> browser.url + 'http://localhost/manage/users/' + +Create a new user:: + + >>> browser.getLink('Create new user').click() + >>> browser.url + 'http://localhost/manage/users/create' + + >>> browser.getControl(name='username').value = 'user1' + >>> browser.getControl(name='email').value = 'user1@example.org' + >>> browser.getControl(name='submit').click() + >>> browser.url + 'http://localhost/manage/users/' + >>> browser.contents + '...
User "user1" created.
...' + +View user:: + + >>> browser.getLink('user1').click() + >>> browser.url + 'http://localhost/manage/users/user1' + +Edit user properties:: + + >>> browser.getLink('Edit').click() + >>> browser.url + 'http://localhost/manage/users/user1/edit' + + >>> browser.getControl(name='submit').click() + >>> browser.url + 'http://localhost/manage/users/user1' + +Set password:: + + >>> from ltpdarepo.admin import passwd + >>> passwd('user1', 'passwd1') + +Login as the new user:: + + >>> browser.open('/logout') + >>> browser.url + 'http://localhost/login' + >>> browser.getControl(name='username').value = 'user1' + >>> browser.getControl(name='password').value = 'passwd1' + >>> browser.getControl(name='login').click() + >>> browser.url + 'http://localhost/' + +Login as administrator user:: + + >>> browser.open('/logout') + >>> browser.url + 'http://localhost/login' + >>> browser.getControl(name='username').value = USERNAME + >>> browser.getControl(name='password').value = PASSWORD + >>> browser.getControl(name='login').click() + >>> browser.url + 'http://localhost/' + +Get users management interface:: + + >>> browser.getLink('Users').click() + >>> browser.url + 'http://localhost/manage/users/' + +Drop just created user:: + + >>> browser.getLink('user1').click() + >>> browser.url + 'http://localhost/manage/users/user1' + + >>> browser.getLink('Drop').click() + >>> browser.url + 'http://localhost/manage/users/user1/drop' + + >>> browser.getControl(name='ok').click() + >>> browser.url + 'http://localhost/manage/users/' + +View an unexisting object results in a 404 erorr:: + + >>> browser.open('/manage/users/foo') + Traceback (most recent call last): + HTTPError: HTTP Error 404: NOT FOUND + + >>> browser.open('/manage/users/foo/edit') + Traceback (most recent call last): + HTTPError: HTTP Error 404: NOT FOUND + + >>> browser.open('/manage/users/foo/drop') + Traceback (most recent call last): + HTTPError: HTTP Error 404: NOT FOUND diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/tests/test_base.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/tests/test_base.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,34 @@ +import unittest +import mechanize + +import ltpdarepo + +USERNAME = 'u1' +PASSWORD = 'u1' + +class TestFactory(unittest.TestCase): + + def setUp(self): + self.app = ltpdarepo.app.test_client() + + def tearDown(self): + pass + + def test_foo(self): + rv = self.app.get('/') + print rv.data + print rv.status_code + print rv.headers + print rv.mimetype + import pdb; pdb.set_trace() + + def test_login(self): + rv = self.app.get('/login') + + rv = self.app.get('/login') + +def test_suite(): + return unittest.defaultTestLoader.loadTestsFromName(__name__) + +if __name__ == '__main__': + unittest.main() diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/tests/test_browser.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/tests/test_browser.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,51 @@ +import unittest + +from ltpdarepo.tests import Browser +from ltpdarepo.tests import USERNAME, PASSWORD + + +class TestCase(unittest.TestCase): + + def setUp(self): + self.browser = Browser('http://localhost/') + + def test_login(self): + self.browser.open('/') + self.assertEqual(self.browser.url, 'http://localhost/login') + + self.browser.getForm().getControl(name='username').value = USERNAME + self.browser.getForm().getControl(name='password').value = PASSWORD + self.browser.getForm().submit() + + self.assertEqual(self.browser.url, 'http://localhost/') + + def test_login_redirect(self): + self.browser.open('/manage/databases/') + print self.browser.url + #self.assertEqual(self.browser.url, 'http://localhost/login') + + self.browser.getForm().getControl(name='username').value = USERNAME + self.browser.getForm().getControl(name='password').value = PASSWORD + self.browser.getForm().submit() + + self.assertEqual(self.browser.url, 'http://localhost/manage/databases/') + + def test_login_bad_redirect(self): + self.browser.open('/login?next=http://google.com') + print self.browser.url + self.browser.getForm().getControl(name='username').value = USERNAME + self.browser.getForm().getControl(name='password').value = PASSWORD + self.browser.getForm().submit() + print self.browser.url + + def test_foo(self): + self.browser.open('/login?next=http%3A%2F%2Fgoogle.com') + print self.browser.url + self.browser.getForm().getControl(name='username').value = USERNAME + self.browser.getForm().getControl(name='password').value = PASSWORD + self.browser.getForm().submit() + print self.browser.url + + +if __name__ == '__main__': + unittest.main() diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/tests/test_csrf.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/tests/test_csrf.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,51 @@ +import unittest +from flask import Flask, request, session + +from ltpdarepo.form import Form +from wtforms.fields import TextField + + +class FTest(Form): + """a simple form to test CSRF protection""" + test = TextField() + + +class TestCase(unittest.TestCase): + + def setUp(self): + app = Flask(__name__) + app.secret_key = 'testing' + @app.route('/', methods=('GET', 'POST')) + def index(): + form = FTest() + if request.method == 'POST' and form.validate(): + return form.test.data + return form.csrf.data + self.app = app.test_client() + + + def test_csrf_protection(self): + rv = self.app.get('/') + self.assertEqual(rv.status_code, 200) + + rv = self.app.post('/') + self.assertEqual(rv.status_code, 403) + + rv = self.app.post('/', data={'test': 1}) + self.assertEqual(rv.status_code, 403) + + rv = self.app.post('/', data={'test': 1, 'csrf': ''}) + self.assertEqual(rv.status_code, 403) + + rv = self.app.get('/') + self.assertEqual(rv.status_code, 200) + rv = self.app.post('/', data={'test': 2, 'csrf': rv.data}) + self.assertEqual(rv.status_code, 200) + self.assertEqual(rv.data, '2') + + +def suite(): + suite = unittest.TestLoader() + +if __name__ == '__main__': + unittest.main() diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/tests/test_pagination.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/tests/test_pagination.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,83 @@ +import unittest + +from ltpdarepo.pagination import Pagination + +SIZE = 20 + + +class TestCase(unittest.TestCase): + + def test_zero_elements(self): + p = Pagination(1, SIZE, 0) + l = list(p) + self.assertEqual(l, []) + + def test_one_page(self): + p = Pagination(1, SIZE, 1) + l = list(p) + self.assertEqual(l, [1]) + + p = Pagination(1, SIZE, 2) + l = list(p) + self.assertEqual(l, [1]) + + def test_two_pages(self): + p = Pagination(1, SIZE, 30) + l = list(p) + self.assertEqual(l, [1, 2]) + + p = Pagination(1, SIZE, 40) + l = list(p) + self.assertEqual(l, [1, 2]) + + def test_pages_without_ellipsis(self): + for i in range(1, 10): + p = Pagination(1, SIZE, SIZE*i) + l = list(p) + self.assertEqual(l, range(1, i + 1)) + + def test_pages_with_ellipsis(self): + + npages = 15 + nitems = SIZE*npages + + for i in range(1, 6): + p = Pagination(i, SIZE, nitems) + l = list(p) + self.assertEqual(len(l), 9) + self.assertEqual(l, [1, 2, 3, 4, 5, 6, 7, -7, 15]) + + p = Pagination(6, SIZE, nitems) + l = list(p) + self.assertEqual(len(l), 9) + self.assertEqual(l, [1, -2, 4, 5, 6, 7, 8, -6, 15]) + + p = Pagination(7, SIZE, nitems) + l = list(p) + self.assertEqual(len(l), 9) + self.assertEqual(l, [1, -3, 5, 6, 7, 8, 9, -5, 15]) + + p = Pagination(8, SIZE, nitems) + l = list(p) + self.assertEqual(len(l), 9) + self.assertEqual(l, [1, -4, 6, 7, 8, 9, 10, -4, 15]) + + p = Pagination(9, SIZE, nitems) + l = list(p) + self.assertEqual(len(l), 9) + self.assertEqual(l, [1, -5, 7, 8, 9, 10, 11, -3, 15]) + + p = Pagination(10, SIZE, nitems) + l = list(p) + self.assertEqual(len(l), 9) + self.assertEqual(l, [1, -6, 8, 9, 10, 11, 12, -2, 15]) + + for i in range(11, 15): + p = Pagination(i, SIZE, nitems) + l = list(p) + self.assertEqual(len(l), 9) + self.assertEqual(l, [1, -7, 9, 10, 11, 12, 13, 14, 15]) + + +if __name__ == '__main__': + unittest.main() diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/tests/test_security.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/tests/test_security.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,41 @@ +import unittest + +class TestCase(unittest.TestCase): + + def test_setup(self): + from ltpdarepo.security import Secure + from flask import Flask + + app = Flask(__name__) + app = Secure(app) + + import logging + handler = logging.StreamHandler() + handler.setLevel(logging.DEBUG) + app.logger.addHandler(handler) + + @app.route('/1') + @app.require('user') + def endpoint1(): + return '' + + @app.route('/2', require='user') + def endpoint2(): + return '' + + @app.route('/login') + def login(): + return 'login' + + client = app.test_client() + + response = client.get('/1') + self.assertEqual(response.status_code, 302) + + response = client.get('/2') + self.assertEqual(response.status_code, 302) + + +if __name__ == '__main__': + unittest.main() + diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/tests/test_users.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/tests/test_users.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,85 @@ +import unittest +import MySQLdb as mysql + +from ltpdarepo.user import User, _generate_password + + +class TestCase(unittest.TestCase): + + def test_generate_password(self): + p1 = _generate_password() + self.assertEqual(len(p1), 8) + p2 = _generate_password() + self.assertEqual(len(p2), 8) + self.assertNotEqual(p1, p2) + + def test_users(self): + u = User() + self.assertEqual(u.username, '') + self.assertEqual(u.password, '') + self.assertEqual(u.name, '') + self.assertEqual(u['username'], '') + self.assertEqual(u['password'], '') + self.assertEqual(u['name'], '') + + u = User(username='foo', name='Foo') + self.assertEqual(u.username, 'foo') + self.assertEqual(u.password, '') + self.assertEqual(u.name, 'Foo') + self.assertEqual(u['username'], 'foo') + self.assertEqual(u['password'], '') + self.assertEqual(u['name'], 'Foo') + + +class DatabaseTestCase(unittest.TestCase): + + def setUp(self): + from ltpdarepo.admin import wipe, install, upgrade, adduser + wipe() + install() + upgrade() + adduser('u1', 'u1') + + from ltpdarepo import app + self.app = app + self.app.config.update(HOSTNAME='localhost') + self.ctx = self.app.test_request_context() + self.ctx.push() + + def tearDown(self): + self.ctx.pop() + from ltpdarepo.admin import wipe + wipe() + + def test_user_load(self): + u1 = User().load('u1') + self.assertEqual(u1.username, 'u1') + + def test_user_create(self): + u2 = User(username='u2', password='u2') + u2.create() + u3 = User().load('u2') + self.assertEqual(u2.username, u3.username) + + def test_user_create_password(self): + u2 = User(username='u2') + u2.create() + self.assertEqual(len(u2.password), 8) + + def test_user_login(self): + u2 = User(username='u2', password='u2') + u2.create() + # test that the user can connect to the database + conn = mysql.connect(host=self.app.config['HOSTNAME'], + user=u2.username, passwd=u2.password) + + def test_user_login_generated_password(self): + u2 = User(username='u2') + u2.create() + # test that the user can connect to the database + conn = mysql.connect(host=self.app.config['HOSTNAME'], + user=u2.username, passwd=u2.password) + + +if __name__ == '__main__': + unittest.main() diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/tests/utils.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/tests/utils.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,4 @@ +import MySQLdb as mysql +from ..config import HOSTNAME, DATABASE, USERNAME, PASSWORD + + diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/upgrade.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/upgrade.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,88 @@ +import MySQLdb as db + +import itertools +from functools import partial + +from .config import HOSTNAME, DATABASE, USERNAME, PASSWORD + +# counter +counter = itertools.count() + +# upgrade steps register +steps = [] + + +def register(r0, r1, func=None, count=None): + if func == None: + return partial(register, r0, r1) + steps.append((r0, r1, count, func)) + return func + + +def upgrade(): + conn = db.connect(host=HOSTNAME, db=DATABASE, + user=USERNAME, passwd=PASSWORD) + curs = conn.cursor() + + # current schema version + curs.execute("""SELECT value+0 FROM options WHERE name='version'""") + schema = curs.fetchone()[0] + + # filter applicable upgrade steps + todo = filter(lambda x: x[0] >= schema, steps) + + # iter upgrade steps + for r, to, count, step in sorted(todo): + + # run upgrade step + # print 'from %g to %g: %s.%s' % (r, to, step.__module__, step.__name__) + step(conn) + + # update schema version + curs.execute("""UPDATE options SET value=%s + WHERE name='version'""", str(to)) + + conn.commit() + conn.close() + + +@register(2.4, 2.41) +def set_strict_mode(conn): + curs = conn.cursor() + curs.execute("""SET GLOBAL sql_mode='STRICT_TRANS_TABLES'""") + conn.commit() + + +@register(2.41, 2.5) +def upgrade_24_to_25(conn): + curs = conn.cursor() + + # consolidate privileges: there is no need to specify grants + # both for 'localhost' and for '%' hosts. drop privileges granted + # for 'localhost' + curs.execute("""DELETE mysql.db FROM mysql.db, users + WHERE User=username AND Host='localhost'""") + + # drop privileges granted explicitly on transactions tables + curs.execute("""DELETE mysql.tables_priv FROM mysql.tables_priv, users + WHERE User=username AND Table_name='transactions'""") + + # tell mysql to reload grant tables + curs.execute("FLUSH PRIVILEGES") + + # drop unused tables + curs.execute("DROP TABLE IF EXISTS user_access") + curs.execute("DROP TABLE IF EXISTS user_hosts") + + # drop password column from users table in administrative + # database: authentication is done using mysql database + curs.execute("ALTER TABLE users DROP COLUMN password") + + conn.commit() + + +# @register(2.5, 2.6) +def upgrade_25_to_26(conn): + curs = conn.cursor() + + conn.commit() diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/user.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/user.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,127 @@ +from MySQLdb.cursors import DictCursor +from ltpdarepo import connection +from ltpdarepo.form import Form +from wtforms.fields import TextField, PasswordField, BooleanField +from wtforms import validators + + +def _generate_password(): + import random + import string + chars = string.letters + string.digits + return "".join([random.choice(chars) for i in range(8)]) + + +class IUser(Form): + username = TextField("Username", validators=[validators.Required(), + validators.Regexp(r'^[a-zA-Z][0-9a-zA-Z\-\._]+$', + message=u'Invalid identifier.')]) + name = TextField("Given name") + surname = TextField("Family name") + email = TextField("Email", validators=[validators.Required(), + validators.Email()]) + telephone = TextField("Telephone") + institution = TextField("Institution") + admin = BooleanField("Admin") + + +class IPassword(Form): + password = PasswordField() + confirm = PasswordField() + + def validate_password(form, field): + if not form.password.data == form.confirm.data: + raise validators.ValidationError(u"Passwords do not match.") + + +class User(object): + __slots__ = ('username', 'password', 'name', 'surname', 'email', 'telephone', 'institution', 'admin') + + def __init__(self, username='', password='', name='', surname='', email='', telephone='', institution='', admin=False): + self.username = username + self.password = password + self.name = name + self.surname = surname + self.email = email + self.telephone = telephone + self.institution = institution + self.admin = bool(admin) + + def __getitem__(self, name): + return getattr(self, name) + + def load(self, username): + conn = connection() + curs = conn.cursor(DictCursor) + curs.execute("""SELECT username, + given_name AS name, + family_name AS surname, + email, institution, telephone, + is_admin AS admin + FROM users WHERE username=%s""", username) + user = curs.fetchone() + if user is None: + return user + for key, value in user.iteritems(): + setattr(self, key, value) + return self + + def create(self): + if not self.password: + self.password = _generate_password() + + conn = connection() + curs = conn.cursor() + + for host in ('localhost', '%'): + curs.execute("""CREATE USER %s@%s IDENTIFIED BY %s""", + (self.username, host, self.password)) + + curs.execute("""INSERT INTO users (username, given_name, family_name, + email, telephone, institution, is_admin) + VALUES (%s, %s, %s, %s, %s, %s, %s)""", + (self.username, self.name, self.surname, + self.email, self.telephone, self.institution, self.admin)) + + conn.commit() + + def delete(self): + conn = connection() + curs = conn.cursor() + + curs.execute("""DELETE FROM users WHERE username=%s""", self.username) + curs.execute("""SELECT Host FROM mysql.user WHERE User=%s""", self.username) + hosts = [row[0] for row in curs.fetchall()] + for host in hosts: + curs.execute("""DROP USER %s@%s""", (self.username, host)) + + conn.commit() + + def save(self): + conn = connection() + curs = conn.cursor() + + curs.execute("""UPDATE users SET given_name=%s, family_name=%s, email=%s, + institution=%s, telephone=%s, is_admin=%s + WHERE username=%s""", + (self.name, self.surname, self.email, + self.telephone, self.institution, self.admin, self.username)) + + conn.commit() + + def passwd(self, password=''): + if not password: + password = _generate_password() + self.password = password + + conn = connection() + curs = conn.cursor() + + curs.execute("""SELECT Host FROM mysql.user WHERE User=%s""", + (self.username, )) + hosts = [row[0] for row in curs.fetchall()] + for host in hosts: + curs.execute("""SET PASSWORD FOR %s@%s = PASSWORD(%s)""", + (self.username, host, self.password)) + + conn.commit() diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/views/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/views/__init__.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,1 @@ +# diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/views/base.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/views/base.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,36 @@ +from flask import Module, request, session, redirect, flash, render_template, g + +from ltpdarepo.security import require, authenticate + +app = Module(__name__) + + +@app.route('/login', methods=('GET', 'POST')) +def login(): + if request.method == 'POST': + if authenticate(request.form['username'], request.form['password']): + session['username'] = request.form['username'] + url = request.args.get('next', '/') + return redirect(url) + flash('Login failed.', category='error') + + return render_template('login.html') + + +@app.route('/logout') +def logout(): + session.pop('username', None) + return redirect('/') + + +@app.route('/') +@require('user') +def index(): + curs = g.db.cursor() + curs.execute("""SELECT DISTINCT Db FROM mysql.db, available_dbs + WHERE Select_priv='Y' AND User=%s AND Db=db_name + ORDER BY Db""", session['username']) + dbs = [row[0] for row in curs.fetchall()] + return render_template('index.html', databases=dbs) + +module = app diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/views/browse.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/views/browse.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,194 @@ +from flask import Module, abort, g, request, render_template, current_app, url_for +from MySQLdb.cursors import DictCursor + +from ltpdarepo.security import require, view +from ltpdarepo.database import Database +from ltpdarepo.pagination import Pagination + +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict + + +PAGESIZE = 20 +FIELDS = ('id', 'name', 'type', 'quantity', 'keywords', + 'submitted', 'title', 'description',) +#extra = ( 'analysis', +# 'additional_authors', +# 'comments', +# 'created', +# 'reference_ids', +# 'version', 'ip', 'hostname', 'os',) +#unused = ('validated', 'vdate', 'author',) + + +app = Module(__name__, 'browse') + + +@app.route('//') +@require('user') +def database(database): + with view('database', database): + db = Database().load(database) + if db is None: + # not found + abort(404) + return render_template('database.html', database=db) + + +@app.route('//objs') +@require('user') +def browse(database): + with view('database', database): + db = Database().load(database) + if db is None: + # not found + abort(404) + + curs = g.db.cursor() + curs.execute("""SELECT COUNT(*) FROM `%s`.objmeta""" % database) + count = curs.fetchone()[0] + page = int(request.args.get('p', 1)) + pagination = Pagination(page, PAGESIZE, count) + + def url_for_other_page(page): + args = request.view_args.copy() + args['p'] = page + return url_for(request.endpoint, **args) + current_app.jinja_env.globals['url_for_other_page'] = url_for_other_page + + curs = g.db.cursor(DictCursor) + objs = Objs(database=database, orderby='id', limit=((page-1)*PAGESIZE, PAGESIZE)) + curs.execute(objs.query) + objs = curs.fetchall() + + return render_template('browse.html', objs=objs, fields=FIELDS, + database=db, pagination=pagination) + + +@app.route('//activity') +@require('user') +def activity(database): + with view('database', database): + db = Database().load(database) + if db is None: + # not found + abort(404) + + curs = g.db.cursor() + + import datetime + today = datetime.date.today() + activity = OrderedDict() + + activity = [] + + for i in range(0, 7): + start = today - datetime.timedelta(days=1 * i) + stop = today - datetime.timedelta(days=1 * (i - 1)) + curs.execute("""SELECT COUNT(*) AS n + FROM `%s`.objmeta + WHERE submitted > %%s + AND submitted < %%s""" % database, (start, stop)) + n = curs.fetchone()[0] + activity.append((str(start), n)) + + return render_template('activity.html', database=db, + activity=activity) + + +@app.route('//') +@require('user') +def obj(database, objid): + with view('database', database): + db = Database().load(database) + if db is None: + # not found + abort(404) + curs = g.db.cursor(DictCursor) + curs.execute(''' + SELECT obj_id AS id, name, obj_type AS type, experiment_title, experiment_desc, analysis_desc, + quantity, keywords, submitted, created FROM `%s`.objmeta WHERE obj_id = %%s''' % database, objid) + obj = curs.fetchone() + return render_template('obj.html', obj=obj, database=db) + + +@app.route('//search') +@require('user') +def search(database): + with view('database', database): + db = Database().load(database) + if db is None: + # not found + abort(404) + + # search criteria + q = request.args.get('q', None) + + if q is not None: + # build query + curs = g.db.cursor(DictCursor) + objs = Objs(database=database, orderby='id', where='name LIKE %s', limit=(0, 50)) + curs.execute(objs.query, ('%%%s%%' % q,)) + objs = curs.fetchall() + + def url_for_other_page(page): + print request.args.get('q') + args = {} + for key, value in request.args.iteritems(): + args[key] = value + args.update(request.view_args.copy()) + #args = dict(request.args) + print args + args['p'] = page + return url_for(request.endpoint, **args) + current_app.jinja_env.globals['url_for_other_page'] = url_for_other_page + + return render_template('browse.html', objs=objs, fields=FIELDS, database=db) + +module = app + + +class Objs(object): + + _query = """SELECT obj_id AS id, + obj_type AS type, + name, + created, + version, + ip, + hostname, + os, + submitted, + experiment_title AS title, + experiment_desc AS description, + analysis_desc AS analysis, + quantity, + additional_authors, + additional_comments AS comments, + keywords, + reference_ids, + validated, + vdate, + author FROM `%s`.objmeta""" + + def __init__(self, database='', orderby=None, where=None, limit=None): + self.database = database + self.orderby = orderby + self.where = where + self.limit = limit + + def __str__(self): + query = self._query % self.database + if self.where: + query += " WHERE %s" % self.where + if self.orderby: + query += " ORDER BY %s" % self.orderby + if self.limit: + query += " LIMIT %d,%d""" % self.limit + return query + + @property + def query(self): + return str(self) diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/views/databases.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/views/databases.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,141 @@ +from flask import Module, abort, g, render_template, request, redirect, url_for, flash + +from MySQLdb.cursors import DictCursor + +from ltpdarepo.form import Form +from ltpdarepo.security import require +from ltpdarepo.database import Database, IDatabase + +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict + + +app = Module(__name__, 'manage.databases') + + +@app.route('/') +@require('admin') +def index(): + curs = g.db.cursor(DictCursor) + curs.execute("""SELECT db_name AS id FROM available_dbs ORDER BY id""") + dbs = curs.fetchall() + return render_template('databases/index.html', databases=dbs) + + +@app.route('/') +@require('admin') +def view(database): + db = Database().load(id=database) + if db is None: + # not found + abort(404) + return render_template('databases/view.html', database=db) + + +@app.route('/create', methods=['GET', 'POST']) +@require('admin') +def create(): + form = IDatabase() + if request.method == 'POST' and form.validate(): + db = Database() + form.update(db) + db.create() + flash(u'Database "%s" created.' % db.id) + return redirect(url_for('manage.databases.index')) + return render_template('databases/create.html', form=form) + + +@app.route('//edit', methods=['GET', 'POST']) +@require('admin') +def edit(database): + db = Database().load(id=database) + if db is None: + # not found + abort(404) + form = IDatabase(obj=db).omit('id') + if request.method == 'POST' and form.validate(): + form.update(db) + db.save() + flash('Database "%s" modified.' % db.id) + return redirect(url_for('manage.databases.view', database=database)) + return render_template('databases/edit.html', database=db, form=form) + + +@app.route('//drop', methods=['GET', 'POST']) +@require('admin') +def drop(database): + db = Database().load(id=database) + if db is None: + # not found + abort(404) + # use an empty form to have CSRF protection + form = Form() + if request.method == 'POST' and form.validate(): + if request.form.get('ok'): + db.drop() + flash('Database "%s" deleted.' % db.id) + return redirect(url_for('manage.databases.index')) + flask.flash("Operation cancelled.") + return redirect(url_for('manage.databases.view', database=database)) + return render_template('databases/drop.html', database=db, form=form) + + +def _get_permissions(database): + # this may be probably obtained with some join magic + curs = g.db.cursor() + curs.execute("""SELECT username FROM users ORDER BY username""") + users = [row[0] for row in curs.fetchall()] + privs = OrderedDict() + for user in users: + curs.execute("""SELECT Select_priv, Insert_priv, Update_priv, Delete_priv + FROM mysql.db WHERE User=%s AND Db=%s""", (user, database, )) + row = curs.fetchone() + if row is None: + row = ('N', 'N', 'N', 'N') + privs[user] = {'select': row[0] == 'Y', + 'insert': row[1] == 'Y', + 'update': row[2] == 'Y', + 'delete': row[3] == 'Y'} + return privs + + +def _permissions(permissions, formdata): + users = permissions.keys() + updates = [] + print formdata + for user in users: + permissions[user]['modified'] = False + if user in formdata: + for act in ('select', 'insert', 'update', 'delete'): + p = bool(formdata.get('%s:%s' % (user, act), False)) + if permissions[user][act] != p: + updates.append({'user': user, 'priv': act, 'grant': p}) + return updates + + +def _set_permissions(database, updates): + curs = g.db.cursor() + for update in updates: + if update['grant']: + curs.execute("""GRANT %s ON `%s`.* TO %%s@%%s""" + % (update['priv'], database), (update['user'], '%')) + else: + curs.execute("""REVOKE %s ON `%s`.* FROM %%s@%%s""" + % (update['priv'], database), (update['user'], '%')) + + +@app.route('//permissions', methods=['GET', 'POST']) +@require('admin') +def permissions(database): + permissions = _get_permissions(database) + if request.method == 'POST': + updates = _permissions(permissions, request.form) + _set_permissions(database, updates) + flash('Permissions updated.') + return redirect(url_for('manage.databases.permissions', database=database)) + return render_template('databases/permissions.html', database=database, permissions=permissions) + + +module = app diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/views/profile.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/views/profile.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,58 @@ +from flask import Module, abort, flash, render_template, request, redirect, url_for + +from ltpdarepo.security import require, permission +from ltpdarepo.form import Form +from wtforms.fields import PasswordField +from wtforms.validators import ValidationError + +from ltpdarepo.user import User, IUser, IPassword + +app = Module(__name__, 'user') + + +@app.route('/') +@require('user') +def view(username): + with permission('view', 'user', username): + user = User().load(username) + if user is None: + # not found + abort(404) + return render_template('user.html', user=user) + + +@app.route('//edit', methods=('GET', 'POST')) +@require('user') +def edit(username): + with permission('edit', 'user', username): + user = User().load(username) + if user is None: + # not found + abort(404) + # users can not set admin role for themself + form = IUser(obj=user).omit('username', 'admin') + if request.method == 'POST' and form.validate(): + form.update(user) + user.save() + flash('User data saved.') + return redirect(url_for('user.view', username=username)) + return render_template('users/edit.html', username=username, form=form) + + +@app.route('//password', methods=('GET', 'POST')) +@require('user') +def password(username): + with permission('edit', 'user', username): + user = User().load(username) + if user is None: + # not found + abort(404) + form = IPassword() + if request.method == 'POST' and form.validate(): + # set password + user.passwd(username, form.password.data) + flash('Password changed.') + return redirect(url_for('user.view', username=username)) + return render_template('users/password.html', form=form) + +module = app diff -r 000000000000 -r c812c3020b63 src/ltpdarepo/views/users.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/views/users.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,80 @@ +from flask import Module, abort, flash, g, render_template, request, redirect, url_for + +from ltpdarepo.security import require +from ltpdarepo.user import User, IUser +from ltpdarepo.form import Form + +from MySQLdb.cursors import DictCursor + +app = Module(__name__, 'manage.users') + + +@app.route('/') +@require('admin') +def index(): + curs = g.db.cursor(DictCursor) + curs.execute("""SELECT username, + CONCAT(given_name, ' ', family_name) AS name, + email + FROM users""") + users = curs.fetchall() + return render_template('users/index.html', users=users) + + +@app.route('/') +@require('admin') +def view(username): + user = User().load(username) + if user is None: + # not found + abort(404) + form = IUser(obj=user) + return render_template('users/view.html', username=username, form=form) + + +@app.route('//edit', methods=('GET', 'POST')) +@require('admin') +def edit(username): + user = User().load(username) + if user is None: + # not found + abort(404) + form = IUser(obj=user).omit('username') + if request.method == 'POST' and form.validate(): + form.update(user) + user.save() + flash('User data saved.') + return redirect(url_for('manage.users.view', username=username)) + return render_template('users/edit.html', username=username, form=form) + + +@app.route('/create', methods=('GET', 'POST')) +@require('admin') +def create(): + form = IUser() + if request.method == 'POST' and form.validate(): + user = User() + form.update(user) + user.create() + flash('User "%s" created.' % form.data['username']) + return redirect(url_for('manage.users.index')) + return render_template('users/create.html', form=form) + + +@app.route('//drop', methods=('GET', 'POST')) +@require('admin') +def drop(username): + user = User().load(username) + if user is None: + # not found + abort(404) + # use an empty form for CSRF protection + form = Form() + if request.method == 'POST' and form.validate(): + if request.form.get('ok'): + user.delete() + flash('User "%s" deleted.' % username) + return redirect(url_for('manage.users.index')) + return render_template('users/drop.html', form=form, user=user) + +module = app diff -r 000000000000 -r c812c3020b63 src/setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/setup.py Thu Jun 09 13:16:24 2011 +0200 @@ -0,0 +1,9 @@ +from setuptools import setup + +setup( + name='ltpdarepo', + version='0.1dev', + entry_points={'console_scripts': [ 'serve = ltpdarepo:main', + 'admin = ltpdarepo.admin:main', + 'test = ltpdarepo.tests:main']} +)