changeset 78:8cf9fc08d43d

Rework named query support.
author Daniele Nicolodi <daniele@grinta.net>
date Sun, 21 Aug 2011 14:41:20 +0200
parents f2d3c99d6052
children 18820d874f33
files src/ltpdarepo/__init__.py src/ltpdarepo/install.py src/ltpdarepo/query.py src/ltpdarepo/static/style.css src/ltpdarepo/templates/database.html src/ltpdarepo/templates/databases/view.html src/ltpdarepo/templates/forms.html src/ltpdarepo/templates/index.html src/ltpdarepo/templates/namedquery.html src/ltpdarepo/templates/objs.html src/ltpdarepo/templates/queries/create.html src/ltpdarepo/templates/queries/drop.html src/ltpdarepo/templates/queries/edit.html src/ltpdarepo/templates/queries/index.html src/ltpdarepo/templates/queries/view.html src/ltpdarepo/templates/query.html src/ltpdarepo/upgrade.py src/ltpdarepo/views/browse.py src/ltpdarepo/views/queries.py
diffstat 19 files changed, 178 insertions(+), 80 deletions(-) [+]
line wrap: on
line diff
--- a/src/ltpdarepo/__init__.py	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/__init__.py	Sun Aug 21 14:41:20 2011 +0200
@@ -132,7 +132,7 @@
 app.register_blueprint(module, url_prefix='/manage/databases')
 
 from .views.queries import module
-app.register_blueprint(module, url_prefix='/manage/databases')
+app.register_blueprint(module, url_prefix='/manage/queries')
 
 from .views.users import module
 app.register_blueprint(module, url_prefix='/manage/users')
--- a/src/ltpdarepo/install.py	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/install.py	Sun Aug 21 14:41:20 2011 +0200
@@ -38,9 +38,11 @@
                     ) ENGINE=MyISAM DEFAULT CHARSET=utf8""")
 
     curs.execute("""CREATE TABLE `queries` (
-                      `name` text,
-                      `db` text,
-                      `querystring` text
+                      `id` int NOT NULL AUTO_INCREMENT,
+                      `title` text NOT NULL,
+                      `db` text NOT NULL,
+                      `query` text NOT NULL,
+                      PRIMARY KEY (`id`)
                     ) ENGINE=MyISAM DEFAULT CHARSET=utf8""")
 
     curs.execute("""CREATE TABLE `options` (
--- a/src/ltpdarepo/query.py	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/query.py	Sun Aug 21 14:41:20 2011 +0200
@@ -1,5 +1,6 @@
+import re
+
 from flask import g
-
 from MySQLdb.cursors import DictCursor
 
 from ltpdarepo.form import Form
@@ -7,37 +8,58 @@
 from wtforms import validators
 from wtforms.validators import ValidationError
 
+
 class IQuery(Form):
     query = HiddenField()
-    database = HiddenField()
-    name = TextField("Name", validators=[validators.Required()])
+    title = TextField(validators=[validators.Required()])
+    db = TextField('Databases',
+                   description='Databaseses to which the query may be applied. '
+                               'Comma separated list or regular expression',
+                   validators=[validators.Required()])
 
-    def validate_name(form, field):
-        curs = g.db.cursor()
-        query = Query.load(database=form.database.data, name=field.data)
-        if query is not None:
-            raise ValidationError(u"Query with this name already exists.")
-    
+    def validate_db(form, field):
+        # automagically transform comma separated list into regular expression
+        if not re.match(r'\^\(.*\)\$', field.data):
+            field.data = '^(' + '|'.join(v.strip() for v in field.data.split(',')) + ')$'
+        # validate regular expression
+        try:
+            re.compile(field.data)
+        except re.error, error:
+            raise ValidationError('Invalid regular expression: ' + str(error))
+
 
 class Query(dict):
-    def __init__(self, name=None, database=None, query=None):
-        super(Query, self).__init__(name=name, database=database, query=query)
+    def __init__(self, title=None, db=None, query=None):
+        super(Query, self).__init__(title=title, db=db, query=query)
         self.__dict__ = self
 
-    def create(self):
-        curs = g.db.cursor()
-        curs.execute("""INSERT INTO queries (name, db, querystring)
-                        VALUES (%(name)s, %(database)s, %(query)s)""", dict(self))
-        g.db.commit()
-
     @staticmethod
-    def load(database, name):
+    def load(id):
         curs = g.db.cursor(DictCursor)
-        curs.execute("""SELECT querystring
-                        FROM queries WHERE db=%s and name=%s""", (database, name))
+        curs.execute("""SELECT id, title, db, query
+                        FROM queries WHERE id=%s""", (id, ))
         query = curs.fetchone()
         if query is None:
             return None
         obj = Query()
         obj.update(query)
         return obj
+
+    def create(self):
+        curs = g.db.cursor()
+        curs.execute("""INSERT INTO queries (title, db, query)
+                        VALUES (%(title)s, %(db)s, %(query)s)""", dict(self))
+        g.db.commit()
+        return curs.lastrowid
+
+    def save(self):
+        curs = g.db.cursor()
+        curs.execute("""UPDATE queries
+                        SET title=%(title)s, db=%(db)s, query=%(query)s
+                        WHERE id=%(id)s""", dict(self))
+        g.db.commit()
+
+    def drop(self):
+        curs = g.db.cursor()
+        curs.execute("""DELETE FROM queries WHERE id=%s""", self.id)
+        g.db.commit()
--- a/src/ltpdarepo/static/style.css	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/static/style.css	Sun Aug 21 14:41:20 2011 +0200
@@ -12,7 +12,7 @@
     color: #000;
 }
 
-pre {
+tt, pre {
     font-family: andale mono, monospace;
 }
 
--- a/src/ltpdarepo/templates/database.html	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/templates/database.html	Sun Aug 21 14:41:20 2011 +0200
@@ -16,12 +16,12 @@
   <input type="submit" value="search"></input>
 </form>
 {% if queries %}
-<h2>Saved queries</h2>
-<p class="discrete">Saved queries:</p>
-<ol>
-  {% for name in queries %}
-  <li><a href="{{ url_for('browse.query', database=database.id, name=name) }}">{{ name }}</a></li>
+<h2>Named queries</h2>
+<p class="discrete">Named queries that apply to this database:</p>
+<ul>
+  {% for query in queries %}
+  <li><a href="{{ url_for('browse.namedquery', database=database.id, id=query.id) }}">{{ query.title }}</a></li>
   {% endfor %}
-</ol>
+</ul>
 {% endif %}
 {% endblock %}
--- a/src/ltpdarepo/templates/databases/view.html	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/templates/databases/view.html	Sun Aug 21 14:41:20 2011 +0200
@@ -7,7 +7,6 @@
 <ul class="actions">
   <li><a href="{{ url_for('manage.databases.edit', database=database.id) }}">Edit</a></li>
   <li><a href="{{ url_for('manage.databases.permissions', database=database.id) }}">Permissions</a></li>
-  <li><a href="{{ url_for('manage.queries.index', database=database.id) }}">Saved queries</a></li>
   <li><a href="{{ url_for('manage.databases.drop', database=database.id) }}">Drop</a></li>
 </ul>
 {% endblock %}
--- a/src/ltpdarepo/templates/forms.html	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/templates/forms.html	Sun Aug 21 14:41:20 2011 +0200
@@ -4,15 +4,19 @@
       {% for field in form if field.type not in ('SubmitField') %}
         {{ render_form_field(field) }}
       {% endfor %}
+      {% for field in form if field.type in ('SubmitField') %}
+        {{ render_form_field(field) }}
+      {% else %}
       <div class="field">
         <div class="widget"><input id="submit" name="submit" type="submit" value="save" /></div>
       </div>
+      {% endfor %}
     </fieldset>
   </form>
 {%- endmacro %}
 
 {% macro render_form_field(field) %}
-        {% if field.type == "HiddenField" %}
+        {% if field.type in ('HiddenField', 'SubmitField') %}
           {{ field }}
         {% else %}
           {% if field.errors %}
--- a/src/ltpdarepo/templates/index.html	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/templates/index.html	Sun Aug 21 14:41:20 2011 +0200
@@ -16,6 +16,7 @@
     <li><a href="{{ url_for('manage.databases.create') }}">Create database</a></li>
     <li><a href="{{ url_for('manage.users.index') }}">Users</a></li>
     <li><a href="{{ url_for('manage.users.create') }}">New user</a></li>
+    <li><a href="{{ url_for('manage.queries.index') }}">Named queries</a></li>
   </ul>
   {% endif %}
 {% endblock %}
--- a/src/ltpdarepo/templates/namedquery.html	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/templates/namedquery.html	Sun Aug 21 14:41:20 2011 +0200
@@ -1,7 +1,3 @@
 {% extends "objs.html" %}
-
-{% block above %}
-<div class="querystring">
-  <pre>{{ query }}</pre>
-</div>
-{% endblock %}
+{% block title %}{{ query.title }} &mdash; {{ database.id }}{% endblock %}
+{% block pagetitle %}{{ query.title }} &mdash; Database &#x00AB;{{ database.id }}&#x00BB;{% endblock %}
--- a/src/ltpdarepo/templates/objs.html	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/templates/objs.html	Sun Aug 21 14:41:20 2011 +0200
@@ -2,7 +2,7 @@
 {% extends "layout.html" %}
 {% block title %}{{ database.id }}{% endblock %}
 {% block body %}
-<h2>Database &#x00AB;{{ database.id }}&#x00BB;</h2>
+<h2>{% block pagetitle %}Database &#x00AB;{{ database.id }}&#x00BB;{% endblock %}</h2>
 <p class="discrete">{{ database.description|default('&nbsp;'|safe, true) }}</p>
 <p class="discrete">{{ batch.count }} objects</p>
 
--- a/src/ltpdarepo/templates/queries/create.html	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/templates/queries/create.html	Sun Aug 21 14:41:20 2011 +0200
@@ -2,8 +2,6 @@
 {% extends "layout.html" %}
 {% block title %}Save query{% endblock %}
 {% block body %}
-<h2>Query for database &laquo;{{ database.id }}&raquo;</h2>
-{# <p class="discrete">{{ database.description }}</p> #}
-<pre class="querystring">{{ query }}</pre>
+<h2>Create named query</h2>
 {{ forms.render(form) }}
 {% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ltpdarepo/templates/queries/drop.html	Sun Aug 21 14:41:20 2011 +0200
@@ -0,0 +1,8 @@
+{% import 'forms.html' as forms %}
+{% extends "layout.html" %}
+{% block title %}Drop query{% endblock %}
+{% block body %}
+<h2>Drop query &#x00AB;{{ query.title }}&#x00BB;</h2>
+<p class="discrete">Are you sure you want to drop this named query?</p>
+{{ forms.render(form) }}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ltpdarepo/templates/queries/edit.html	Sun Aug 21 14:41:20 2011 +0200
@@ -0,0 +1,7 @@
+{% import 'forms.html' as forms %}
+{% extends "layout.html" %}
+{% block title %}Edit query{% endblock %}
+{% block body %}
+<h2>Edit named query</h2>
+{{ forms.render(form) }}
+{% endblock %}
--- a/src/ltpdarepo/templates/queries/index.html	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/templates/queries/index.html	Sun Aug 21 14:41:20 2011 +0200
@@ -2,10 +2,16 @@
 {% block title %} Queries {% endblock %}
 {% block body %}
 <h2>Queries</h2>
-<p class="discrete">Queries for database &#x00AB;{{ database }}&#x00BB;</p>
+<p class="discrete">Manage named queries:</p>
 <ul>
 {% for query in queries %}
-  <li><a href="{{ url_for('manage.queries.view', database=database, query=query) }}">{{ query }}</a></li>
+  <li>
+    <a href="{{ url_for('manage.queries.view', id=query.id) }}">{{ query.title }}</a>
+    <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>
+    </ul>
+  </li>
 {% endfor %}
 </ul>
 {% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ltpdarepo/templates/queries/view.html	Sun Aug 21 14:41:20 2011 +0200
@@ -0,0 +1,11 @@
+{% extends "layout.html" %}
+{% block title %}{{ query.title }}{% endblock %}
+{% 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>
+<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>
+</ul>
+{% endblock %}
--- a/src/ltpdarepo/templates/query.html	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/templates/query.html	Sun Aug 21 14:41:20 2011 +0200
@@ -12,12 +12,16 @@
         $("#save").live('click', function () {
           var query = $(".query form").serialize();
           var form = $(document.createElement('form')).attr({
-            'action': '{{ url_for('manage.queries.create', database=database.id) }}',
+            'action': '{{ url_for('manage.queries.create') }}',
             'method': 'POST' });
           form.append($(document.createElement('input')).attr({
             'type': 'hidden',
             'name': 'query',
             'value': query }));
+          form.append($(document.createElement('input')).attr({
+            'type': 'hidden',
+            'name': 'db',
+            'value': '{{ database.id }}' }));
           $("body").append(form);
           form.submit();
           form.remove();
--- a/src/ltpdarepo/upgrade.py	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/upgrade.py	Sun Aug 21 14:41:20 2011 +0200
@@ -96,7 +96,10 @@
 
     # crerate "queries" table
     curs.execute("""CREATE TABLE IF NOT EXISTS
-                    queries (name TEXT, db TEXT, querystring TEXT)""")
+                    queries (id INT NOT NULL AUTO_INCREMENT,
+                             title TEXT NOT NULL,
+                             db TEXT NOT NULL,
+                             query TEXT NOT NULL) CHARSET=utf8""")
 
     # for each registered database
     curs.execute("SELECT db_name FROM available_dbs")
--- a/src/ltpdarepo/views/browse.py	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/views/browse.py	Sun Aug 21 14:41:20 2011 +0200
@@ -8,6 +8,7 @@
 
 from ltpdarepo.security import require, view
 from ltpdarepo.database import Database
+from ltpdarepo.query import Query
 from ltpdarepo.pagination import Pagination
 
 try:
@@ -149,9 +150,9 @@
         if db is None:
             # not found
             abort(404)
-        curs = g.db.cursor()
-        curs.execute("""SELECT name FROM queries WHERE db=%s""", database)
-        queries = [row[0] for row in curs.fetchall()]
+        curs = g.db.cursor(DictCursor)
+        curs.execute("""SELECT id, title, db FROM queries WHERE %s RLIKE db""", database)
+        queries = curs.fetchall()
         return render_template('database.html', database=db, queries=queries)
 
 
@@ -340,20 +341,19 @@
                                criteria=criteria, database=db, fields=FIELDS, batch=batch)
 
 
-@app.route('/<database>/query/<name>')
+@app.route('/<database>/query/<int:id>')
 @require('user')
-def namedquery(database, name):
+def namedquery(database, id):
     with view('database', database):
         db = Database().load(database)
         if db is None:
             # not found
             abort(404)
 
-        curs = g.db.cursor()
-        curs.execute("SELECT querystring FROM queries WHERE db=%s AND name=%s", (database, name))
-        query = json.loads(curs.fetchone()[0])
+        query = Query.load(id)
+        params = json.loads(query.query)
         fields, ops, values = [], [], []
-        for q in query:
+        for q in params:
             fields.append(q[0])
             ops.append(q[1])
             values.append(q[2])
@@ -363,14 +363,15 @@
         where = ' AND '.join(q)
 
         # simple string representation of the query
-        querystring = ' AND '.join("%s %s %s" % tuple(v) for v in query)
+        querystring = ' AND '.join("%s %s %s" % tuple(v) for v in params)
 
         # get objects
         count = Objs(database=database).filter(where, *values).count()
         batch = Pagination(_current_page(), size=PAGESIZE, count=count)
         objs  = Objs(database=database).filter(where, *values).orderby(*_current_ordering()).limit(*batch.limits).all()
 
-        return render_template('namedquery.html', objs=objs, database=db, fields=FIELDS, batch=batch, query=querystring)
+        return render_template('namedquery.html', objs=objs, database=db, fields=FIELDS, batch=batch,
+                               query=query, querystring=querystring)
 
 
 class Timeseries(Objs):
--- a/src/ltpdarepo/views/queries.py	Mon Aug 15 20:08:25 2011 +0200
+++ b/src/ltpdarepo/views/queries.py	Sun Aug 21 14:41:20 2011 +0200
@@ -1,4 +1,6 @@
-from flask import Blueprint, render_template, g, abort, redirect, request, json, session, url_for
+from flask import Blueprint, render_template, g, abort, redirect, request, json, session, url_for, flash
+
+from MySQLdb.cursors import DictCursor
 
 from ltpdarepo.security import require
 from ltpdarepo.query import Query, IQuery
@@ -9,34 +11,29 @@
 from .browse import MAPPING, RMAPPING
 
 
-@app.route('/<database>/queries/')
+@app.route('/')
 @require('admin')
-def index(database):
-    curs = g.db.cursor()
-    curs.execute("""SELECT name FROM queries WHERE db=%s""", database)
-    queries = [row[0] for row in curs.fetchall()]
-    return render_template('queries/index.html', queries=queries, database=database)
+def index():
+    curs = g.db.cursor(DictCursor)
+    curs.execute("""SELECT id, title FROM queries""")
+    queries = curs.fetchall()
+    return render_template('queries/index.html', queries=queries)
 
 
-@app.route('/<database>/queries/<query>')
+@app.route('/<int:id>/')
 @require('admin')
-def view(database, query):
-    query = Query.load(database=database, name=query)
+def view(id):
+    query = Query.load(id=id)
     if query is None:
         # not found
         abort(404)
-    return query.querystring
+    return render_template('queries/view.html', query=query)
 
 
-@app.route('/<database>/queries/+', methods=['GET', 'POST'])
+@app.route('/+', methods=['GET', 'POST'])
 @require('admin')
-def create(database):
-    db = Database.load(database)
-    if db is None:
-        # not found
-        abort(404)
-
-    if 'name' not in request.values:
+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']
@@ -49,18 +46,57 @@
 
         form = IQuery()
         form.query.data = query
-        form.database.data = database
         form.csrf.data = session['_token']
-        return render_template('queries/create.html', form=form, query=querystring, database=db)
+        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', database=database))
+        return redirect(url_for('.index'))
     
     return render_template('queries/create.html', form=form, query='')
 
 
+@app.route('/<int:id>/edit', methods=['GET', 'POST'])
+@require('admin')
+def edit(id):
+    query = Query.load(id)
+    if query is None:
+        # not found
+        abort(404)
+    form = IQuery(obj=query)
+    if request.method == 'POST' and form.validate():
+        form.update(query)
+        query.save()
+        return redirect(url_for('.index'))
+    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()
+
+
+@app.route('/<int:id>/drop', methods=['GET', 'POST'])
+@require('admin')
+def drop(id):
+    query = Query.load(id)
+    if query is None:
+        # not found
+        abort(404)
+    form = IConfirm()
+    if request.method == 'POST' and form.validate():
+        if request.form.get('ok'):
+            query.drop()
+            flash('Named query deleted.')
+            return redirect(url_for('manage.queries.index'))
+        flash('Operation cancelled.')
+        return redirect(url_for('manage.queries.index'))
+    return render_template('queries/drop.html', form=form, query=query)
+
+
 module = app