Index: templates/query_form.html
===================================================================
--- templates/query_form.html	(revision 149)
+++ templates/query_form.html	(working copy)
@@ -46,10 +46,10 @@
 
    <p>
     <select name="signature_search">
-     <option value="signature" selected="${c.params.signature_search == 'signature' and 'selected' or ''}">
+     <option value="signature" selected="${c.params.getSignatureSearch() == 'signature' and 'selected' or ''}">
       Stack Signature
      </option>
-     <option value="stack" selected="${c.params.signature_search == 'stack' and 'selected' or ''}">
+     <option value="stack" selected="${c.params.getSignatureSearch() == 'stack' and 'selected' or ''}">
       Top 10 Stack Frames
      </option>
     </select>
@@ -58,7 +58,7 @@
      <option py:for="(value, desc) in (('contains', 'contains'),
                                        ('exact', 'is exactly'))"
              value="${value}"
-             selected="${c.params.signature_type == value and 'selected' or ''}">
+             selected="${c.params.getSignatureType() == value and 'selected' or ''}">
       ${desc}
      </option>
     </select>
@@ -70,10 +70,10 @@
     <input type="text" name="date" size="20" value="${c.params.date}" />
     <br />
     <label for="range_value">Date range: </label>
-    <input type="text" name="range_value" value="${c.params.range_value}" size="2" /><select name="range_unit">
+    <input type="text" name="range_value" value="${c.params.getRange()[0]}" size="2" /><select name="range_unit">
       <option py:for="interval in ('hours', 'days', 'weeks', 'months')"
               value="${interval}"
-              selected="${c.params.range_unit == interval and 'selected' or ''}">
+              selected="${c.params.getRange()[1] == interval and 'selected' or ''}">
        ${interval}
       </option>
     </select>
@@ -82,7 +82,7 @@
    <input type="submit" />
   </form>
 
-  <py:if test="c.reports">
+  <py:if test="c.reports is not None">
    <h2>Query Results</h2>
 
    <p>${str(c.params)}</p>
Index: controllers/report.py
===================================================================
--- controllers/report.py	(revision 150)
+++ controllers/report.py	(working copy)
@@ -1,10 +1,14 @@
 from socorro.lib.base import *
 from socorro.lib.processor import Processor
+from socorro.lib.queryparams import QueryParamValidator, QueryParams
 from socorro.models import Report
 import socorro.lib.collect as collect
 from sqlalchemy import *
 from sqlalchemy.databases.postgres import *
+import formvalidator
 
+validator = QueryParamsValidator()
+
 class ReportController(BaseController):
   def index(self, id):
     c.report = Report.get_by(uuid=id)
Index: controllers/query.py
===================================================================
--- controllers/query.py	(revision 149)
+++ controllers/query.py	(working copy)
@@ -1,18 +1,20 @@
 from socorro.models import Branch
 from socorro.lib.base import BaseController
-from socorro.lib.queryparams import QueryParamsValidator, QueryParams
+from socorro.lib.queryparams import QueryLimit
 from pylons import c, session, request
 from pylons.templating import render_response
+from pylons.database import create_engine
 
-validator = QueryParamsValidator()
-
 class QueryController(BaseController):
   def query(self):
-    if request.params.get('do_query', '') != '':
-      c.params = validator.to_python(request.params)
+    e = create_engine()
+    e.echo = True
+
+    c.params = QueryLimit()
+
+    if request.params.get('do_query', None):
+      c.params.setFromParams(request.params)
       c.reports = c.params.query().list()
-    else:
-      c.params = QueryParams()
 
     c.products = Branch.getProducts()
     c.branches = Branch.getBranches()
Index: lib/queryparams.py
===================================================================
--- lib/queryparams.py	(revision 149)
+++ lib/queryparams.py	(working copy)
@@ -4,34 +4,148 @@
 from sqlalchemy import sql, func, select
 from sqlalchemy.databases.postgres import PGInterval
 import re
+from pylons import h
 
-class QueryParams(object):
-  """An object representing query conditions for end-user searches."""
-  def __init__(self):
-    self.signature = ''
-    self.signature_search = 'signature'
-    self.signature_type = 'contains'
-    self.date = ''
-    self.range_value = 1
-    self.range_unit = 'weeks'
-    self.products = ()
-    self.branches = ()
-    self.versions = ()
+class ProductVersionValidator(formencode.FancyValidator):
+  """A custom validator which processes 'product:version' into (product, version)"""
 
-  def query(self):
-    q = Report.query().order_by(sql.desc(Report.c.date)).limit(500)
-    
-    if self.date is None:
-      enddate = func.now()
+  pattern = re.compile('^([^:]+):(.+)$')
+
+  def _to_python(self, value, state):
+    (product, version) = self.pattern.match(value).groups()
+    return (product, version)
+
+class ListValidator(formencode.FancyValidator):
+  def __init__(self, validator=None, separator=','):
+    self.separator = separator
+    if validator:
+      self.subvalidator = validator
     else:
-      enddate = sql.cast(self.date, sqlalchemy.types.DateTime)
-    startdate = enddate - sql.cast("%s %s" % (self.range_value, self.range_unit), PGInterval)
-    q = q.filter(Report.c.date.between(startdate, enddate))
+      self.subvalidator = formencode.validators.String()
+  
+  def _to_python(self, value, state):
+    return [self.subvalidator.to_python(v)
+            for v in str(value).split(self.separator)]
 
-    if self.signature != '':
-      if self.signature_type == 'contains':
+class BaseLimit(object):
+  """A base class which validates date/branch/product/version conditions for
+  multiple searches."""
+
+  datetime_validator = formencode.validators.Regex(
+    '^\\d{4}-\\d{2}-\\d{2}( \\d{2}:\\d{2}(:\\d{2})?)?$', strip=True
+  )
+  range_unit_validator = formencode.validators.OneOf(
+    ['hours', 'days', 'weeks', 'months']
+  )
+  version_validator = ListValidator(ProductVersionValidator())
+  stringlist_validator = ListValidator(formencode.validators.String(strip=True))
+
+  def __init__(self, date=None, range=None,
+               products=None, branches=None, versions=None):
+    self.date = date
+    self.range = range   # _range is a tuple (number, interval)
+    self.products = products or []
+    self.branches = branches or []
+    self.versions = versions or []
+
+  def setFromParams(self, params):
+    """Set the values of this object from a request.params instance."""
+
+    self.date = self.datetime_validator.to_python(params.get('date'), None)
+    
+    if 'range_value' in params and 'range_unit' in params:
+      self.range = (formencode.validators.Int.to_python(params.get('range_value')),
+                    self.range_unit_validator.to_python(params.get('range_unit')))
+    for products in params.getall('product'):
+      self.products.extend(self.stringlist_validator.to_python(products))
+
+    for branches in params.getall('branch'):
+      self.branches.extend(self.stringlist_validator.to_python(branches))
+
+    for versions in params.getall('version'):
+      self.versions.extend(self.version_validator.to_python(versions))
+
+  def getSQLDateEnd(self):
+    if self.date is not None:
+      return self.date
+    return func.now()
+
+  def getRange(self):
+    if self.range:
+      return self.range
+    return (1, 'weeks')
+
+  def getSQLRange(self):
+    return sql.cast('%s %s' % self.getRange(), PGInterval)
+    
+  def getSQLDateStart(self):
+    return self.getSQLDateEnd() - self.getSQLRange()
+
+  def filterByDate(self, q):
+    return q.filter(Report.c.date.between(self.getSQLDateStart(),
+                                          self.getSQLDateEnd()))
+
+  def filterByProduct(self, q):
+    if len(self.products):
+      q = q.filter(Report.c.product.in_(*self.products))
+    return q
+
+  def filterByBranch(self, q):
+    if len(self.branches):
+      q = q.filter(sql.and_(Branch.c.branch.in_(*self.branches),
+                            Branch.c.product == Report.c.product,
+                            Branch.c.version == Report.c.version))
+    return q
+
+  def filterByVersion(self, q):
+    if len(self.versions):
+      q = q.filter(sql.or_(*[sql.and_(Report.c.product == product,
+                                      Report.c.version == version)
+                             for (product, version) in self.versions]))
+    return q
+
+  def filter(self, q):
+    q = self.filterByDate(q)
+    q = self.filterByProduct(q)
+    q = self.filterByBranch(q)
+    q = self.filterByVersion(q)
+    return q
+
+class QueryLimit(BaseLimit):
+  query_validator = formencode.validators.OneOf(['signature', 'stack'])
+  type_validator = formencode.validators.OneOf(['exact', 'contains'])
+  
+  """An object representing query conditions for end-user searches."""
+  def __init__(self, signature=None, signature_search=None,
+               signature_type=None, **kwargs):
+    BaseLimit.__init__(self, **kwargs)
+    self.signature = signature
+    self.signature_search = signature_search
+    self.signature_type = signature_type
+
+  def setFromParams(self, params):
+    BaseLimit.setFromParams(self, params)
+
+    self.signature = params.get('signature', None)
+    self.signature_search = self.query_validator.to_python(params.get('signature_search', None))
+    self.signature_type = self.type_validator.to_python(params.get('signature_typeee', None))
+
+  def getSignatureSearch(self):
+    if self.signature_search is not None:
+      return self.signature_search
+    return 'signature'
+
+  def getSignatureType(self):
+    if self.signature_type is not None:
+      return self.signature_type
+    return 'contains'
+
+  def filter(self, q):
+    q = BaseLimit.filter(self, q)
+    if self.signature is not None:
+      if self.getSignatureType() == 'contains':
         pattern = '%' + self.signature.replace('%', '%%') + '%'
-        if self.signature_search == 'signature':
+        if self.getSignatureSearch() == 'signature':
           q = q.filter(Report.c.signature.like(pattern))
         else:
           q = q.filter(
@@ -39,7 +153,7 @@
                        sql.and_(Frame.c.signature.like(pattern),
                                 Frame.c.report_id == Report.c.id)))
       else:
-        if self.signature_search == 'signature':
+        if self.getSignatureSearch() == 'signature':
           q = q.filter(Report.c.signature == self.signature)
         else:
           q = q.filter(
@@ -47,27 +161,19 @@
                        sql.and_(Frame.c.signature == self.signature,
                                 Frame.c.report_id == Report.c.id)))
 
-    if len(self.products) > 0:
-      q = q.filter(Report.c.product.in_(*self.products))
-    
-    if len(self.branches) > 0:
-      q = q.filter(sql.and_(Branch.c.branch.in_(*self.branches),
-                            Branch.c.product == Report.c.product,
-                            Branch.c.version == Report.c.version))
-
-    for (product, version) in self.versions:
-      q = q.filter(sql.and_(Branch.c.product == product,
-                            Branch.c.version == version))
-    
     return q
 
+  def query(self):
+    q = Report.query().order_by(sql.desc(Report.c.date)).limit(500)
+    return self.filter(q)
+
   def __str__(self):
     if self.date is None:
       enddate = 'now'
     else:
       enddate = self.date
       
-    str = "Results within %s %s of %s" % (self.range_value, self.range_unit, enddate)
+    str = "Results within %s %s of %s" % (self.getRange()[0], self.getRange()[1], enddate)
 
     if self.signature != '':
       sigtype = {'exact': 'is exactly',
@@ -89,39 +195,3 @@
 
     str += '.'
     return str
-
-class ProductVersionValidator(formencode.FancyValidator):
-  """A custom validator which processes 'product:version' into (product, version)"""
-
-  pattern = re.compile('^([^:]+):(.+)$')
-
-  def _to_python(self, value, state):
-    (product, version) = self.pattern.match(value).groups()
-    return (product, version)
-
-class QueryParamsValidator(formencode.FancyValidator):
-  """A custom formvalidator which processes request.params into a QueryParams
-  instance."""
-
-  query_validator = formencode.validators.OneOf(['signature', 'stack'])
-  type_validator = formencode.validators.OneOf(['exact', 'contains'])
-  datetime_validator = formencode.validators.Regex('^\\d{4}-\\d{2}-\\d{2}( \\d{2}:\\d{2}(:\\d{2})?)?$', strip=True)
-  range_unit_validator = formencode.validators.OneOf(['hours', 'days', 'weeks', 'months'])
-  string_validator = formencode.validators.String(strip=True)
-  version_validator = ProductVersionValidator()
-
-  def _to_python(self, value, state):
-    q = QueryParams()
-    q.signature = value.get('signature', '')
-    q.signature_search = self.query_validator.to_python(value.get('signature_search', 'signature'))
-    q.signature_type = self.type_validator.to_python(value.get('signature_type', 'exact'))
-    q.date = self.datetime_validator.to_python(value.get('date'), '')
-    q.range_value = formencode.validators.Int.to_python(value.get('range_value', '1'))
-    q.range_unit = self.range_unit_validator.to_python(value.get('range_unit', 'weeks'))
-    q.products = [self.string_validator.to_python(product) for
-                  product in value.getall('product')]
-    q.branches = [self.string_validator.to_python(branch) for
-                  branch in value.getall('branch')]
-    q.versions = [self.version_validator.to_python(version) for
-                  version in value.getall('version')]
-    return q
