From 1cb93e23779127fd0d1dc02b569d083d20a24739 Mon Sep 17 00:00:00 2001
From: Brian Moe <brian.moe@ligo.org>
Date: Wed, 19 Jun 2013 14:59:52 -0500
Subject: [PATCH] Query on event attributes. #933

Closes https://bugs.ligo.org/redmine/issues/933
---
 gracedb/forms.py                       |  5 ++-
 gracedb/query.py                       | 62 +++++++++++++++++---------
 templates/gracedb/query_help_frag.html | 10 +++++
 3 files changed, 53 insertions(+), 24 deletions(-)

diff --git a/gracedb/forms.py b/gracedb/forms.py
index e3ac69bbe..2e5497ae3 100644
--- a/gracedb/forms.py
+++ b/gracedb/forms.py
@@ -6,7 +6,8 @@ from models import Event, Group, Label
 from django.contrib.auth.models import User
 from django.core.exceptions import FieldError
 
-from query import parseQuery, ParseException
+from query import parseQuery
+from pyparsing import ParseException
 
 htmlEntityStar = "&#9733;"
 htmlEntityRightPointingHand = "&#9758;"
@@ -31,7 +32,7 @@ class GraceQueryField(forms.CharField):
             raise forms.ValidationError(mark_safe(err))
         except Exception, e:
             # What could this be and how can we handle it better? XXX
-            raise forms.ValidationError(str(e))
+            raise forms.ValidationError(str(e)+str(type(e)))
 
 class SimpleSearchForm(forms.Form):
     query = GraceQueryField(required=False, widget=forms.TextInput(attrs={'size':60}))
diff --git a/gracedb/query.py b/gracedb/query.py
index 60f8402f0..7abcf5f15 100644
--- a/gracedb/query.py
+++ b/gracedb/query.py
@@ -23,7 +23,7 @@ from pyparsing import \
     Word, nums, Literal, CaselessLiteral, delimitedList, Suppress, QuotedString, \
     Keyword, Combine, Or, Optional, OneOrMore, alphas, Regex, \
     opAssoc, operatorPrecedence, oneOf, \
-    stringStart, stringEnd, ParseException
+    stringStart, stringEnd
 
 def maybeRange(name, dbname=None):
     dbname = dbname or name
@@ -170,41 +170,59 @@ labelQ.setParseAction(lambda toks: ("label", toks[0]))
 ###########################
 # Query on event attributes
 
-attrNumExprOperators = { "<" :  "__lt",
-                         "<=":  "__lte",
-                         "=" :  "",
-                         ">" :  "__gt",
-                         ">=":  "__gte",
-                       }
+lparen = Suppress('(')
+rparen = Suppress(')')
 
-attrNumExprLhs = Keyword("far") | Keyword("gpstime")
+exprOperators = { "<" :  "__lt",
+                  "<=":  "__lte",
+                  "=" :  "",
+                  ">" :  "__gt",
+                  ">=":  "__gte",
+                }
+
+tableTranslations = {
+        'si': 'singleinspiral',
+        'ci': 'coincinspiralevent',
+        'mb': 'multiburstevent',
+        'coincinspiral': 'coincinspiralevent',
+        'multiburst': 'multiburstevent',
+        }
+
+def buildDjangoQueryField(toks):
+    toks = [name.lower() for name in toks]
+    return "__".join([tableTranslations.get(name, name) for name in toks])
 
 exponent = Combine(Word("Ee") + Optional(Word("+-"))+Word(nums))
-afloat = Combine( Word(nums) + Optional(Combine(Literal(".") + Word(nums))) )
 
-attrNumExprRhs = Combine( Optional(Word("+-")) + afloat + Optional(exponent) )
-attrNumExprRhs.setParseAction(lambda toks: float(toks[0]))
+afloat = Combine( Word(nums) + \
+         Optional(Combine(Literal(".") + Word(nums))) ) + \
+         Optional(exponent)
+afloat.setParseAction(lambda toks: float(toks[0]))
+
+lhs = delimitedList(Word(alphas+'_'), '.')
+lhs.setParseAction(buildDjangoQueryField)
 
-attrNumExprOp = Or(map(Literal, attrNumExprOperators.keys()))
-attrNumExprOp.setParseAction(lambda toks: attrNumExprOperators[toks[0]])
+rhs = afloat | QuotedString('"')
 
-attrNumExpr = attrNumExprLhs + attrNumExprOp + attrNumExprRhs
-attrNumExpr.setParseAction(lambda toks: Q(**{toks[0]+toks[1]: toks[2]}))
+op = Or(map(Literal, exprOperators.keys()))
+op.setParseAction(lambda toks: exprOperators[toks[0]])
 
-#attrIfoExpr = Keyword("ifos").suppress() + Literal("=").suppress() + Word("LVH12,")
-#attrIfoExpr.setParseAction(lambda toks: Q(instruments=toks[0]))
+simpleTerm = lhs + op + rhs
+simpleTerm.setParseAction(lambda toks: Q(**{toks[0]+toks[1]: toks[2]}))
 
-#attrExpr = attrIfoExpr | attrNumExpr 
-attrExpr = attrNumExpr 
+rangeTerm = lhs + Suppress('in') + rhs + Suppress(",") + rhs
+rangeTerm.setParseAction(lambda toks: Q(**{toks[0]+"__range": toks[1:]}))
 
-attrExprs = operatorPrecedence(attrExpr,
+term = simpleTerm | rangeTerm
+
+attrExpressions = operatorPrecedence(term,
     [(minusop, 1, opAssoc.RIGHT, lambda a,b,toks: ~toks[0][0]),
      (orop,    2, opAssoc.LEFT,  lambda a,b,toks: reduce(Q.__or__, toks[0].asList(), Q())),
      (andop,   2, opAssoc.LEFT,  lambda a,b,toks: reduce(Q.__and__, toks[0].asList(), Q())),
     ]).setParseAction(lambda toks: toks[0])
 
-
-attributeQ = Optional(Suppress(Keyword('attr:'))) + attrExprs.copy()
+#attributeQ = lparen + attrExpressions + rparen
+attributeQ = attrExpressions.copy()
 attributeQ.setParseAction(lambda toks: ("attr", toks[0]))
 
 
diff --git a/templates/gracedb/query_help_frag.html b/templates/gracedb/query_help_frag.html
index abe924f25..ae2b8b9a7 100644
--- a/templates/gracedb/query_help_frag.html
+++ b/templates/gracedb/query_help_frag.html
@@ -1,5 +1,15 @@
 
 <div id="hints" style="display: none;">
+ <h4>By Event Attributes</h4>
+ Relational and range queries can be made on event attributes.
+ <ul>
+    <li><code>instruments = "H1,L1,V1" &amp; far &lt; 1e-7</code></li>
+    <li><code>singleinspiral.mchirp &gt;= 1.6 &amp; eff_distance in 40.5,55 </code></li>
+    <li><code>(si.channel = "DMT-STRAIN" | si.channel = "DMT-PAIN") &amp; si.snr &lt; 5</code></li>
+    <li><code>mb.snr in 1,3 &amp; mb.central_freq &gt; 1000</code></li>
+ </ul>
+ Attributes in the common event object (eg gpstime, far, instruments) do not need qualifiers.  Attributes specific to inspiral or burst events, for example, require qualification.  Abbreviations are available: <code>si</code> for singleinspiral, <code>ci</code> for coincinspiral and <code>mb</code> for multiburst.
+
  <h4>By GPS Time</h4>
   Specifiy an exact GPS time, or a range.
   Integers will be assumed to be GPS times, making the <code>gpstime:</code>
-- 
GitLab