Mercurial > hg > ltpdarepo
changeset 249:863e3e81498c
Merge with stable
author | Daniele Nicolodi <daniele@grinta.net> |
---|---|
date | Tue, 27 Dec 2011 19:00:04 +0100 |
parents | 16f095c74706 (current diff) fbfd3129fe4d (diff) |
children | 239c7d077f20 |
files | |
diffstat | 17 files changed, 372 insertions(+), 89 deletions(-) [+] |
line wrap: on
line diff
--- a/.hgtags Mon Dec 12 16:11:47 2011 +0100 +++ b/.hgtags Tue Dec 27 19:00:04 2011 +0100 @@ -2,3 +2,5 @@ e55537dfbe2beff5a5ecb5cdd7cc58fbef560fd8 0.3 a6f2c9eae21785e769281e2f00bc11f662b0c6ed 0.4 951bc04dfb19020101e6480aeef9bb893b033e5f 0.5 +48adc0d70d227058e20f504f8e4e5ec83218c56d 0.6 +ce09aed4a90bf76d3c5cc9cda23ed697b29fb236 0.7
--- a/setup.py Mon Dec 12 16:11:47 2011 +0100 +++ b/setup.py Tue Dec 27 19:00:04 2011 +0100 @@ -1,7 +1,7 @@ from setuptools import setup, find_packages import os.path, subprocess -version = '0.7dev' +version = '0.8dev' requires = [ 'distribute',
--- a/src/ltpdarepo/__init__.py Mon Dec 12 16:11:47 2011 +0100 +++ b/src/ltpdarepo/__init__.py Tue Dec 27 19:00:04 2011 +0100 @@ -16,6 +16,7 @@ import dateutil.tz from .security import secure, require, authenticate +from .utils import datetimetz from .views.browse import module as browse from .views.databases import module as databases from .views.feed import url_for_atom_feed, module as feed @@ -27,24 +28,26 @@ SCHEMA = 31 -class datetimeutc(datetime): - # subclass of `datetime.datetime` with default string - # representation including the timezone name - def __str__(self): - return self.strftime('%Y-%m-%d %H:%M:%S %Z') - - -# customize mysql types conversion for datetime fields to return -# timezone aware objects in the UTC timezone -def datetime_or_none_utc(s): +# customize mysql types conversion from and to DATETIME fields to +# return timezone aware datetime objects in the UTC timezone and +# correclty convert timezone aware datetime objects to UTC timezone +def datetime_or_none(s): value = converters.DateTime_or_None(s) if value is not None: - value = datetimeutc(value.year, value.month, value.day, value.hour, - value.minute, value.second, value.microsecond, - tzinfo=dateutil.tz.tzutc()) + value = datetimetz(value.year, value.month, value.day, value.hour, + value.minute, value.second, value.microsecond, + tzinfo=dateutil.tz.tzutc()) return value + +def datetime_to_literal(value, c): + if value.tzinfo is not None: + value = value.astimezone(dateutil.tz.tzutc()) + return converters.DateTime2literal(value, c) + conversions = converters.conversions.copy() -conversions[mysql.constants.FIELD_TYPE.DATETIME] = datetime_or_none_utc +conversions[mysql.constants.FIELD_TYPE.DATETIME] = datetime_or_none +conversions[datetime] = datetime_to_literal +conversions[datetimetz] = datetime_to_literal def before_request():
--- a/src/ltpdarepo/admin.py Mon Dec 12 16:11:47 2011 +0100 +++ b/src/ltpdarepo/admin.py Tue Dec 27 19:00:04 2011 +0100 @@ -139,7 +139,7 @@ verbosity = args.pop('verbosity') if verbosity: logger = logging.getLogger('ltpdarepo') - logger.setLevel(logger.level - 10 * verbosity) + logger.setLevel(max(logger.level - 10 * verbosity, logging.DEBUG)) # common parameters username = args.pop('_username', None)
--- a/src/ltpdarepo/templates/users/password.html Mon Dec 12 16:11:47 2011 +0100 +++ b/src/ltpdarepo/templates/users/password.html Tue Dec 27 19:00:04 2011 +0100 @@ -1,8 +1,8 @@ {% import 'forms.html' as forms %} {% extends "layout.html" %} -{% block title %}User {{ session.username }}{% endblock %} +{% block title %}User {{ username }}{% endblock %} {% block body %} -<h2>New user «{{ session.username }}» password</h2> +<h2>User «{{ username }}» password</h2> <p class="discrete">Please choose a safe password</p> {{ forms.render(form) }} {% endblock %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/tests/browse-activity.txt Tue Dec 27 19:00:04 2011 +0100 @@ -0,0 +1,24 @@ +Test setup:: + + >>> from urllib import urlencode + >>> from ltpdarepo.tests.utils import Browser + >>> USERNAME, PASSWORD = 'u1', 'u1' + >>> browser = Browser() + >>> browser.login(USERNAME, PASSWORD) + +Actiity view:: + + >>> browser.open('/browse/db1/activity') + +Activity view for specific month:: + + >>> browser.open('/browse/db1/activity/2011-11') + +Activity view for specific day:: + + >>> browser.open('/browse/db1/activity/2011-11-11') + + +# Local Variables: +# mode: doctest +# End:
--- a/src/ltpdarepo/tests/browse-database.txt Mon Dec 12 16:11:47 2011 +0100 +++ b/src/ltpdarepo/tests/browse-database.txt Tue Dec 27 19:00:04 2011 +0100 @@ -60,10 +60,10 @@ Activity view:: - # >>> browser.open('/browse/db1') - # >>> browser.getLink('Show activity').click() - # >>> browser.url - # 'http://localhost/browse/db1/activity' + >>> browser.open('/browse/db1') + >>> browser.getLink('Activity').click() + >>> browser.url + 'http://localhost/browse/db1/activity' # Local Variables:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/tests/browse-feed.txt Tue Dec 27 19:00:04 2011 +0100 @@ -0,0 +1,37 @@ +Test setup:: + + >>> from ltpdarepo.tests.utils import Browser + >>> USERNAME, PASSWORD = 'u1', 'u1' + >>> browser = Browser() + >>> browser.login(USERNAME, PASSWORD) + >>> browser.open('/') + +Obtain link to Atom Feed:: + + >>> browser.open('/browse/db1/') + >>> browser.getLink(url='atom.xml').url + 'http://localhost/browse/db1/.../atom.xml' + +Check that Atom Feed renders correctly:: + + >>> browser.getLink(url='atom.xml').click() + +Check Atom Feed properties:: + + >>> browser.contents # title is database name + '...<title type="text">db1</title>...' + + >>> browser.contents # subtitle is database description + '...<subtitle type="text">Test database One</subtitle>...' + + >>> browser.contents # id should be an absolute url + '...<id>http://localhost/browse/db1/.../atom.xml</id>...' + + >>> browser.contents # as well as other links + '...<link href="http://localhost/browse/db1/" />...' + +Check that the authorization token is verified:: + + >>> browser.open('/browse/db1/xxx/atom.xml') + Traceback (most recent call last): + HTTPError: HTTP Error 403: FORBIDDEN
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/tests/browse-search.txt Tue Dec 27 19:00:04 2011 +0100 @@ -0,0 +1,71 @@ +Test setup:: + + >>> from urllib import urlencode + >>> from ltpdarepo.tests.utils import Browser + >>> USERNAME, PASSWORD = 'u1', 'u1' + >>> browser = Browser() + >>> browser.login(USERNAME, PASSWORD) + +Build a simple query without using the web interface:: + + >>> params = urlencode((('field', 'id'),('operator', '<'),('value', '11'), + ... ('field', 'name'),('operator', 'LIKE'),('value', '%'))) + >>> browser.open('/browse/db1/query?' + params) + +We should obtain 10 objects:: + + >>> browser.contents.count('<td class="id">') + 10 + +Try different query builder interface field type parsers:: + + >>> params = urlencode((('field', 'submitted'),('operator', '>'),('value', '1970-01-01 00:00:00 UTC'), # datetime + ... ('field', 'type'),('operator', '='),('value', 'ao'))) # enum + >>> browser.open('/browse/db1/query?' + params) + +We should obtain all 30 objects:: + + >>> browser.contents.count('<td class="id">') + 30 + +Same query through the timespan search interface:: + + >>> params = urlencode((('field', 'id'),('operator', '<'),('value', '11'), + ... ('field', 'name'),('operator', 'LIKE'),('value', '%'))) + >>> browser.open('/browse/db1/timeseries?' + params) + +We should still obtain 10 objects:: + + >>> browser.contents.count('<td class="id">') + 10 + +Add timespan constraints:: + + >>> params = urlencode((('field', 'id'),('operator', '<'),('value', '11'), + ... ('field', 'name'),('operator', 'LIKE'),('value', '%'), + ... ('t1', '1970-01-01 00:00:00 UTC'), + ... ('t2', '1970-01-01 00:10:00 UTC'))) + >>> browser.open('/browse/db1/timeseries?' + params) + +This time we should obtain just 7 objects:: + + >>> browser.contents.count('<td class="id">') + 7 + +Timespan constraints in CET timezone:: + + >>> params = urlencode((('field', 'id'),('operator', '<'),('value', '11'), + ... ('field', 'name'),('operator', 'LIKE'),('value', '%'), + ... ('t1', '1970-01-01 01:00:00 CET'), + ... ('t2', '1970-01-01 01:10:00 CET'))) + >>> browser.open('/browse/db1/timeseries?' + params) + +We should obtain the same 7 objects:: + + >>> browser.contents.count('<td class="id">') + 7 + + +# Local Variables: +# mode: doctest +# End:
--- a/src/ltpdarepo/tests/manage-databases.txt Mon Dec 12 16:11:47 2011 +0100 +++ b/src/ltpdarepo/tests/manage-databases.txt Tue Dec 27 19:00:04 2011 +0100 @@ -82,7 +82,7 @@ >>> app.privileges('u1', 'database1') {'insert': True, 'update': False, 'select': True, 'delete': False} -that the form is updated accordingly:: +and that the form is updated accordingly:: >>> browser.follow('Permissions') >>> browser.getControl(name='u1:select').value
--- a/src/ltpdarepo/tests/manage-queries.txt Mon Dec 12 16:11:47 2011 +0100 +++ b/src/ltpdarepo/tests/manage-queries.txt Tue Dec 27 19:00:04 2011 +0100 @@ -128,6 +128,30 @@ Traceback (most recent call last): LinkNotFoundError +Checkj that queries built from the timeseries search interface +properly record the time parameters and link to the correct view. To +do so add 't1' and 't2' parameters to query data:: + + >>> params = urlencode((('field', 'id'),('operator', '>'),('value', '0'), + ... ('field', 'name'),('operator', 'LIKE'),('value', '%'), + ... ('t1', '1970-01-01 00:00:00'), + ... ('t2', '1970-01-01 00:10:00'))) + >>> data = urlencode({'query': params, 'db': 'db1'}) + +Post to the named query creation form:: + + >>> browser.post('/manage/queries/+', data) + >>> browser.url + 'http://localhost/manage/queries/+' + +Save query setting title and the databases where it operates:: + + >>> browser.getControl(name='title').value = 'Query' + >>> browser.getControl(name='db').value = 'db1' + >>> browser.getControl(name='submit').click() + >>> browser.url + 'http://localhost/manage/queries/' + # Local Variables: # mode: doctest
--- a/src/ltpdarepo/tests/manage-users.txt Mon Dec 12 16:11:47 2011 +0100 +++ b/src/ltpdarepo/tests/manage-users.txt Tue Dec 27 19:00:04 2011 +0100 @@ -119,11 +119,97 @@ >>> browser.url 'http://localhost/' -Drop just created user:: +Test permissions. The user should have no permissions on existing databases:: + + >>> from ltpdarepo import admin + >>> app = admin.Application() + >>> app.privileges('user1', 'db1') + {'insert': False, 'update': False, 'select': False, 'delete': False} + +Assign permissions:: >>> browser.open('/manage/users/user1') >>> browser.url 'http://localhost/manage/users/user1' + >>> browser.follow('Permissions') + >>> browser.url + 'http://localhost/manage/users/user1/permissions' + + >>> browser.getControl(name='db1:select').value = True + >>> browser.getControl(name='db1:insert').value = True + >>> browser.getControl(name='submit').click() + >>> browser.url + 'http://localhost/manage/users/user1' + +Check that the permissions have been updated:: + + >>> app.privileges('user1', 'db1') + {'insert': True, 'update': False, 'select': True, 'delete': False} + + >>> conn = app.connect() + >>> curs = conn.cursor() + >>> rows = curs.execute("SHOW GRANTS FOR 'user1'@'%'") + >>> privs = [row[0] for row in curs.fetchall()] + >>> "GRANT INSERT ON `db1`.`transactions` TO 'user1'@'%'" in privs + True + +and that the form is updated accordingly:: + + >>> browser.follow('Permissions') + >>> browser.getControl(name='db1:select').value + ['Y'] + >>> browser.getControl(name='db1:insert').value + ['Y'] + >>> browser.getControl(name='db1:update').value + [] + >>> browser.getControl(name='db1:delete').value + [] + +and that we can revoke permissions:: + + >>> browser.getControl(name='db1:insert').value = False + >>> browser.getControl(name='submit').click() + >>> browser.url + 'http://localhost/manage/users/user1' + + >>> app.privileges('user1', 'db1') + {'insert': False, 'update': False, 'select': True, 'delete': False} + +even all permissions:: + + >>> browser.follow('Permissions') + >>> browser.getControl(name='db1:select').value = False + >>> browser.getControl(name='submit').click() + >>> browser.url + 'http://localhost/manage/users/user1' + + >>> app.privileges('user1', 'db1') + {'insert': False, 'update': False, 'select': False, 'delete': False} + + >>> conn = app.connect() + >>> curs = conn.cursor() + >>> rows = curs.execute("SHOW GRANTS FOR 'user1'@'%'") + >>> privs = [row[0] for row in curs.fetchall()] + >>> "GRANT INSERT ON `db1`.`transactions` TO 'user1'@'%'" in privs + False + + +Drop operation can be cancelled:: + + >>> browser.open('/manage/users/user1') + >>> browser.url + 'http://localhost/manage/users/user1' + >>> browser.getLink('Drop').click() + >>> browser.url + 'http://localhost/manage/users/user1/drop' + + >>> browser.getControl(name='cancel').click() + >>> browser.url + 'http://localhost/manage/users/user1' + >>> browser.contents + '...<div class="flash message">Operation cancelled.</div>...' + +Drop just created user:: >>> browser.getLink('Drop').click() >>> browser.url @@ -132,6 +218,8 @@ >>> browser.getControl(name='ok').click() >>> browser.url 'http://localhost/manage/users/' + >>> browser.contents + '...<div class="flash message">User deleted.</div>...' View an unexisting object results in a 404 erorr:: @@ -143,6 +231,10 @@ Traceback (most recent call last): HTTPError: HTTP Error 404: NOT FOUND + >>> browser.open('/manage/users/foo/permissions') + 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
--- a/src/ltpdarepo/tests/test_doctests.py Mon Dec 12 16:11:47 2011 +0100 +++ b/src/ltpdarepo/tests/test_doctests.py Tue Dec 27 19:00:04 2011 +0100 @@ -7,7 +7,7 @@ app = admin.Application() app.wipe() app.install() - app.createdb('db1') + app.createdb('db1', description='Test database One') app.populate('db1', 30) app.useradd('u1', admin=True) app.passwd('u1', 'u1') @@ -24,11 +24,14 @@ suite = unittest.TestSuite() suite.addTest( doctest.DocFileSuite( - 'browse-user.txt', + 'browse-activity.txt', 'browse-database.txt', - 'manage-users.txt', + 'browse-feed.txt', + 'browse-search.txt', + 'browse-user.txt', 'manage-databases.txt', 'manage-queries.txt', + 'manage-users.txt', setUp=doctestSetUp, tearDown=doctestTearDown, optionflags=doctest.ELLIPSIS | doctest.REPORT_ONLY_FIRST_FAILURE)) return suite
--- a/src/ltpdarepo/tests/test_objs.py Mon Dec 12 16:11:47 2011 +0100 +++ b/src/ltpdarepo/tests/test_objs.py Tue Dec 27 19:00:04 2011 +0100 @@ -1,8 +1,8 @@ from ltpdarepo.tests.utils import RequestContextTestCase -from ltpdarepo.views.browse import Objs +from ltpdarepo.views.browse import Objs, Timeseries -class TestCase(RequestContextTestCase): +class ObjsTestCase(RequestContextTestCase): @classmethod def setUpClass(self): @@ -35,3 +35,38 @@ def test_where(self): objs = Objs(database='db1').filter('obj_id > %s AND obj_id < %s', (10, 20)) self.assertEqual(objs[0]['id'], 11) + + +class TimeseriesTestCase(RequestContextTestCase): + + @classmethod + def setUpClass(self): + from ltpdarepo import admin + app = admin.Application() + app.wipe() + app.setup() + + def test_simple(self): + objs = Timeseries(database='db1') + self.assertEqual(len(objs), objs.count()) + self.assertEqual(len(objs.all()), objs.count()) + + def test_limit(self): + objs = Timeseries(database='db1').limit(20) + self.assertEqual(objs[0]['id'], 1) + self.assertEqual(len(objs.all()), 20) + + objs = Timeseries(database='db1').limit(20, 2) + self.assertEqual(objs[0]['id'], 21) + self.assertEqual(len(objs.all()), 2) + + def test_orderby(self): + objs = Timeseries(database='db1').orderby('objmeta.obj_id') + self.assertEqual(objs[0]['id'], 1) + + objs = Timeseries(database='db1').orderby('objmeta.obj_id', desc=True) + self.assertEqual(objs[0]['id'], 30) + + def test_where(self): + objs = Timeseries(database='db1').filter('objmeta.obj_id > %s AND objmeta.obj_id < %s', (10, 20)) + self.assertEqual(objs[0]['id'], 11)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ltpdarepo/utils.py Tue Dec 27 19:00:04 2011 +0100 @@ -0,0 +1,40 @@ +# Copyright 2011 Daniele Nicolodi <nicolodi@science.unitn.it> +# +# This software may be used and distributed according to the terms of +# the GNU Affero General Public License version 3 or any later version. + + +from __future__ import absolute_import +from datetime import * + +import dateutil.parser +import dateutil.tz + + +class datetimetz(datetime): + # subclass of `datetime.datetime` with default string + # representation including the timezone name + def __str__(self): + return self.strftime('%Y-%m-%d %H:%M:%S %Z') + + +def parsedatetime(string): + # parse datetime string representation and returns a timezone + # aware subclass of datetime with default string representation + # including the timezone name + + # parsing default is midnight today in UTC timezone + default = datetime.utcnow().replace( + tzinfo=dateutil.tz.tzutc(), hour=0, minute=0, second=0, microsecond=0) + + value = dateutil.parser.parse(string, dayfirst=True, yearfirst=True, default=default) + return datetimetz(value.year, value.month, value.day, value.hour, + value.minute, value.second, value.microsecond, value.tzinfo) + + +def toDATETIME(value): + if not isinstance(value, datetime): + return value + if value.tzinfo is not None: + value = value.astimezone(dateutil.tz.tzutc()) + return value.strftime('%Y-%m-%d %H:%M:%S')
--- a/src/ltpdarepo/views/browse.py Mon Dec 12 16:11:47 2011 +0100 +++ b/src/ltpdarepo/views/browse.py Tue Dec 27 19:00:04 2011 +0100 @@ -7,20 +7,18 @@ from datetime import datetime from dateutil.relativedelta import relativedelta -import dateutil.tz -import dateutil.parser - -from flask import Blueprint, Markup, abort, g, request, render_template, json, make_response, url_for, redirect +from flask import Blueprint, abort, g, request, render_template, make_response, url_for, redirect from MySQLdb.cursors import DictCursor from wtforms import Form from wtforms.fields import Field from wtforms.widgets import TextInput from wtforms.validators import ValidationError, Optional +from ltpdarepo.database import Database +from ltpdarepo.pagination import Pagination +from ltpdarepo.query import Query from ltpdarepo.security import require, view -from ltpdarepo.database import Database -from ltpdarepo.query import Query -from ltpdarepo.pagination import Pagination +from ltpdarepo.utils import parsedatetime, toDATETIME try: from collections import OrderedDict @@ -135,8 +133,8 @@ self._end = None def timespan(self, start=None, end=None): - self._start = start - self._end = end + self._start = toDATETIME(start) + self._end = toDATETIME(end) return self @property @@ -168,8 +166,6 @@ query += " AND tsdata.t0 + INTERVAL tsdata.nsecs SECOND >= '%s'" % self._start if self._where: query += " AND %s" % self._where - if self._limit: - query += " LIMIT %d,%d" % self._limit return query @@ -222,31 +218,6 @@ return column, kind, values -class datetimefield(datetime): - """Timezone aware subclass of `datetime.datetime` with a new - constructor that parses strings and a default string - representation including the timezone name.""" - - def __new__(cls, string): - # parsing default is midnight today in UTC timezone - default = datetime.utcnow().replace( - tzinfo=dateutil.tz.tzutc(), hour=0, minute=0, second=0, microsecond=0) - - value = dateutil.parser.parse(string, dayfirst=True, yearfirst=True, default=default) - return datetime.__new__(cls, value.year, value.month, value.day, value.hour, - value.minute, value.second, value.microsecond, value.tzinfo) - - def __str__(self): - return self.strftime('%Y-%m-%d %H:%M:%S %Z') - - -class DateTimeJSONEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, datetime): - return obj.strftime('%Y-%m-%dT%H:%M:%S%Z') - return super(DateTimeJSONEncoder, self).default(obj) - - class Request(object): """Retrieves and validates query parameters from the request""" @@ -255,7 +226,7 @@ 'double': float, 'text': unicode, 'enum': unicode, - 'datetime': datetimefield} + 'datetime': parsedatetime} def __init__(self, formdata, columns, indexes={}): # incoming data @@ -268,15 +239,6 @@ # process incoming data self.process(self.formdata) - @staticmethod - def fromqs(string, columns, indexes={}): - from urlparse import parse_qs - from werkzeug.datastructures import MultiDict - # parse query string - formdata = parse_qs(string, keep_blank_values=True, strict_parsing=True) - formdata = MultiDict(formdata) - return Request(formdata, columns, indexes) - def process(self, formdata): fields = formdata.getlist('field') ops = formdata.getlist('operator') @@ -334,11 +296,6 @@ def query(self): return self.where, self.vals - def tostring(self): - query = ["%s %s '%s'" % (field, op, value) for field, op, value, err in self.criteria if not err] - string = ' AND '.join(s.replace(' ', ' ') for s in query) - return Markup(string) - class DateTimeField(Field): """Text input and that uses `dateutil.parser.parse` to parse the @@ -354,7 +311,7 @@ def _value(self): if self.data: - return self.data.strftime('%Y-%m-%d %H:%M:%S %Z') + return str(self.data) return self.raw_data and u' '.join(self.raw_data) or u'' def process_formdata(self, valuelist): @@ -363,12 +320,8 @@ if not datestr: self.data = None raise ValidationError(self.gettext(u'Input a datetime value')) - - # parsing default is midnight today in UTC timezone - default = datetime.utcnow().replace( - tzinfo=dateutil.tz.tzutc(), hour=0, minute=0, second=0, microsecond=0) try: - self.data = dateutil.parser.parse(datestr, default=default, **self.parseargs) + self.data = parsedatetime(datestr) except ValueError: self.data = None raise ValidationError(self.gettext(u'Invalid datetime input')) @@ -659,7 +612,6 @@ span = 'MONTH' today = datetime.strptime(when, '%Y-%m').date() - if span == 'MONTH': begin = today + relativedelta(day=1) end = begin + relativedelta(months=1, days=-1) @@ -695,7 +647,7 @@ nmax = max(num for day, num in activity) or 1 base = 10**floor(log10(nmax)) if base < 10: base = 10.0 - nmax = ceil(nmax / base)*base + nmax = ceil(nmax / base) * base return render_template('activity.html', database=db, activity=activity, nmax=nmax, curr=today, prev=prev, next=next, span=span, dt=dt)
--- a/src/ltpdarepo/views/profile.py Mon Dec 12 16:11:47 2011 +0100 +++ b/src/ltpdarepo/views/profile.py Tue Dec 27 19:00:04 2011 +0100 @@ -121,6 +121,6 @@ user.passwd(form.password.data) flash('Password set.') return redirect(url_for('user.view', username=username)) - return render_template('users/password.html', form=form) + return render_template('users/password.html', username=username, form=form) module = app