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 &#x00AB;{{ session.username }}&#x00BB; password</h2>
+<h2>User &#x00AB;{{ username }}&#x00BB; 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&nbsp;'.join(s.replace(' ', '&nbsp;') 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