changeset 107:22b950fee548

Complete named queries support.
author Daniele Nicolodi <daniele@grinta.net>
date Tue, 23 Aug 2011 18:04:12 +0200
parents 0d259fd52ca0
children f075650d3e1d
files src/ltpdarepo/query.py src/ltpdarepo/static/style.css src/ltpdarepo/templates/namedquery.html src/ltpdarepo/templates/queries/view.html src/ltpdarepo/templates/query.html src/ltpdarepo/views/browse.py src/ltpdarepo/views/queries.py
diffstat 7 files changed, 91 insertions(+), 56 deletions(-) [+]
line wrap: on
line diff
--- a/src/ltpdarepo/query.py	Tue Aug 23 14:38:17 2011 +0200
+++ b/src/ltpdarepo/query.py	Tue Aug 23 18:04:12 2011 +0200
@@ -1,13 +1,13 @@
 import re
 
-from flask import g
+from flask import Markup, g, json
 from MySQLdb.cursors import DictCursor
-
-from ltpdarepo.form import Form
 from wtforms.fields import TextField, HiddenField
 from wtforms import validators
 from wtforms.validators import ValidationError
 
+from ltpdarepo.form import Form
+
 
 class IQuery(Form):
     query = HiddenField()
@@ -63,3 +63,10 @@
         curs = g.db.cursor()
         curs.execute("""DELETE FROM queries WHERE id=%s""", self.id)
         g.db.commit()
+
+    def tostring(self):
+        criteria = json.loads(self.query)
+        print criteria
+        query = ["%s %s '%s'" % (field, op, value) for field, op, value in criteria]
+        string = ' AND&nbsp;'.join(s.replace(' ', '&nbsp;') for s in query)
+        return Markup(string)
--- a/src/ltpdarepo/static/style.css	Tue Aug 23 14:38:17 2011 +0200
+++ b/src/ltpdarepo/static/style.css	Tue Aug 23 18:04:12 2011 +0200
@@ -505,6 +505,12 @@
     padding: 4px 4px 5px 5px;
 }
 
+.querystring {
+    margin-top: 3em;
+    color: #888;
+    white-space: pre-wrap;
+}
+
 /* permisions display */
 
 .permissions {
--- a/src/ltpdarepo/templates/namedquery.html	Tue Aug 23 14:38:17 2011 +0200
+++ b/src/ltpdarepo/templates/namedquery.html	Tue Aug 23 18:04:12 2011 +0200
@@ -1,3 +1,6 @@
 {% extends "objs.html" %}
 {% block title %}{{ query.title }} &mdash; {{ database.id }}{% endblock %}
 {% block pagetitle %}{{ query.title }} &mdash; Database &#x00AB;{{ database.id }}&#x00BB;{% endblock %}
+{% block below %}
+<pre class="querystring">{{ querystring }}</pre>
+{% endblock %}
--- a/src/ltpdarepo/templates/queries/view.html	Tue Aug 23 14:38:17 2011 +0200
+++ b/src/ltpdarepo/templates/queries/view.html	Tue Aug 23 18:04:12 2011 +0200
@@ -3,7 +3,7 @@
 {% block body %}
 <h2>Query &laquo;{{ query.title }}&raquo;</h2>
 <p class="field"><span class="label">Database:</span> <tt>{{ query.db }}</tt></p>
-<p class="field"><span class="label">Criteria:</span> <tt>{{ query.query }}</tt></p>
+<p class="field"><span class="label">Query:</span> <tt>{{ query.tostring() }}</tt></p>
 <ul class="actions">
   <li><a href="{{ url_for('manage.queries.edit', id=query.id) }}">Edit</a></li>
   <li><a href="{{ url_for('manage.queries.drop', id=query.id) }}">Drop</a></li>
--- a/src/ltpdarepo/templates/query.html	Tue Aug 23 14:38:17 2011 +0200
+++ b/src/ltpdarepo/templates/query.html	Tue Aug 23 18:04:12 2011 +0200
@@ -11,6 +11,8 @@
         $.dropdown.replace($("select"));
         $("#save").live('click', function () {
           var query = $(".query form").serialize();
+          if (! query)
+            return false;
           var form = $(document.createElement('form')).attr({
             'action': '{{ url_for('manage.queries.create') }}',
             'method': 'POST' });
--- a/src/ltpdarepo/views/browse.py	Tue Aug 23 14:38:17 2011 +0200
+++ b/src/ltpdarepo/views/browse.py	Tue Aug 23 18:04:12 2011 +0200
@@ -4,7 +4,7 @@
 import dateutil.tz
 import dateutil.parser
 
-from flask import Blueprint, abort, g, request, render_template, json, make_response
+from flask import Blueprint, Markup, abort, g, request, render_template, json, make_response
 from MySQLdb.cursors import DictCursor
 from wtforms import Form
 from wtforms.fields import Field
@@ -26,7 +26,6 @@
 EXTRA = ('analysis', 'author', 'additional_authors', 'comments', 'reference_ids', 'created', )
 MORE = ('version', 'ip', 'hostname', 'os', 'validated', 'vdate')
 TIMESERIESFIELDS = ('id', 'name', 't0', 'nsecs', 'quantity', 'keywords', 'title', 'description')
-ALLFIELDS = FIELDS + EXTRA
 
 PAGESIZE = 20
 
@@ -238,6 +237,13 @@
         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"""
 
@@ -248,27 +254,35 @@
                   'enum': unicode,
                   'datetime': datetimefield}
 
-    def __init__(self, request, columns, indexes={}):
+    def __init__(self, formdata, columns, indexes={}):
         # incoming data
-        self.formdata = request.args
-        # column names mapping used to validate orderby parameter
+        self.formdata = formdata
+        # column names mapping. used to validate orderby parameter
         self.columns = columns
-        # indexes description used to validate query parameters
+        # indexes description. used to validate query parameters
         self.indexes = indexes
+
         # 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')
         values = formdata.getlist('value')
-        self.where, self.vals, self.criteria = self.parsequery(fields, ops, values)
-
-    def parsequery(self, fields, ops, values):
+        self.parsequery(zip(fields, ops, values))
 
+    def parsequery(self, items):
         query, vals, parsed = [], [], []
-
-        for field, op, value in zip(fields, ops, values):
+        for field, op, value in items:
             # index description
             index = self.indexes[field]
             validate = self.validators[index['type']]
@@ -287,7 +301,9 @@
                 # append to parse results
                 parsed.append((field, op, value, error))
 
-        return ' AND '.join(query), vals, parsed
+        self.where = ' AND '.join(query)
+        self.vals = vals
+        self.criteria = parsed
 
     @property
     def order(self):
@@ -309,6 +325,19 @@
     def query(self):
         return self.where, self.vals
 
+    def dumps(self):
+        dump = [(field, op, value) for field, op, value, err in self.criteria if not err]
+        return json.dumps(dump, cls=DateTimeJSONEncoder)
+
+    def loads(self, string):
+        criteria = json.loads(string)
+        self.parsequery(criteria)
+
+    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
@@ -386,7 +415,7 @@
             abort(404)
 
         # parse request
-        r = Request(request, Objs.columns)
+        r = Request(request.args, Objs.columns)
 
         count = Objs(database).count()
         batch = Pagination(r.page, size=PAGESIZE, count=count)
@@ -408,7 +437,7 @@
         if obj is None:
             # not found
             abort(404)
-        return render_template('obj.html', obj=obj, database=db, fields=FIELDS+EXTRA+MORE)
+        return render_template('obj.html', obj=obj, database=db, fields=FIELDS+EXTRA)
 
 
 @app.route('/<database>/<int:objid>/<frmt>')
@@ -457,7 +486,7 @@
             abort(404)
 
         # parse request
-        r = Request(request, Objs.columns)
+        r = Request(request.args, Objs.columns)
         q = '%' + request.args.get('q', '') + '%'
 
         # collect objects
@@ -482,7 +511,7 @@
         indexes = Indexes(database, Objs.columns)
 
         # parse request
-        r = Request(request, Objs.columns, indexes)
+        r = Request(request.args, Objs.columns, indexes)
 
         # collect objects
         count = Objs(database).filter(*r.query).count()
@@ -502,31 +531,28 @@
         if db is None:
             # not found
             abort(404)
+        query = Query.load(id)
+        if query is None:
+            # not found
+            abort(404)
 
-        query = Query.load(id)
-        params = json.loads(query.query)
-        fields, ops, values = [], [], []
-        for q in params:
-            fields.append(q[0])
-            ops.append(q[1])
-            values.append(q[2])
+        # applicable search criteria
+        indexes = Indexes(database, Objs.columns)
 
-        # build query string
-        q = ['`%s` %s %%s' % v for v in zip(fields, ops)]
-        where = ' AND '.join(q)
+        # parse request
+        r = Request(request.args, Objs.columns, indexes)
 
-        # simple string representation of the query
-        querystring = ' AND '.join("%s %s %s" % tuple(v) for v in params)
+        # load query
+        r.loads(query.query)
 
-        # get objects
-        r = Request()
-        count = Objs(database).filter(where, *values).count()
+        # collect objects
+        count = Objs(database).filter(*r.query).count()
         batch = Pagination(r.page, size=PAGESIZE, count=count)
-        objs  = Objs(database).filter(where, *values).orderby(*r.order).limit(*batch.limits).all()
+        objs  = Objs(database).filter(*r.query).orderby(*r.order).limit(*batch.limits).all()
 
         return render_template('namedquery.html', objs=objs, batch=batch,
                                fields=FIELDS, database=db,
-                               query=query, querystring=querystring)
+                               query=query, querystring=r.tostring())
 
 
 @app.route('/<database>/timeseries')
@@ -541,7 +567,7 @@
     indexes = Indexes(database, Timeseries.columns)
 
     # parse reuest
-    r = Request(request, Timeseries.columns, indexes)
+    r = Request(request.args, Timeseries.columns, indexes)
     t = Timerange(request.args)
 
     # collect objects
--- a/src/ltpdarepo/views/queries.py	Tue Aug 23 14:38:17 2011 +0200
+++ b/src/ltpdarepo/views/queries.py	Tue Aug 23 18:04:12 2011 +0200
@@ -1,15 +1,13 @@
-from flask import Blueprint, render_template, g, abort, redirect, request, json, session, url_for, flash
-
+from flask import Blueprint, render_template, g, abort, redirect, request, session, url_for, flash
+from wtforms.fields import SubmitField
 from MySQLdb.cursors import DictCursor
 
+from ltpdarepo.form import Form
 from ltpdarepo.security import require
 from ltpdarepo.query import Query, IQuery
-from ltpdarepo.database import Database
 
 app = Blueprint('manage.queries', __name__)
 
-from .browse import MAPPING, RMAPPING
-
 
 @app.route('/')
 @require('admin')
@@ -34,28 +32,23 @@
 @require('admin')
 def create():
     if 'title' not in request.values:
-        from urlparse import parse_qs
-        parsed = parse_qs(request.values['query'], keep_blank_values=True, strict_parsing=True)
-        fields = parsed['field']
-        ops = parsed['operator']
-        values = parsed['value']
-        # query json representation 
-        query = json.dumps(zip(fields, ops, values))
-        # simple string representation
-        querystring = ' AND '.join("%s %s '%s'" % (MAPPING.get(f), o, v) for f, o, v in zip(fields, ops, values))
+
+        from .browse import Indexes, Request, Objs
+        indexes = Indexes(request.values.get('db', ''), Objs.columns)
+        query = Request.fromqs(request.values.get('query', ''), Objs.columns, indexes).dumps()
 
         form = IQuery()
         form.query.data = query
         form.csrf.data = session['_token']
         return render_template('queries/create.html', form=form)
-        
+
     form = IQuery()
     if request.method == 'POST' and form.validate():
         query = Query()
         form.update(query)
         query.create()
         return redirect(url_for('.index'))
-    
+
     return render_template('queries/create.html', form=form, query='')
 
 
@@ -74,8 +67,6 @@
     return render_template('queries/edit.html', form=form)
 
 
-from ltpdarepo.form import Form
-from wtforms.fields import SubmitField
 class IConfirm(Form):
     ok = SubmitField()
     cancel = SubmitField()