Index: trac/ticket/query.py
===================================================================
--- trac/ticket/query.py	(revision 8019)
+++ trac/ticket/query.py	(working copy)
@@ -49,6 +49,7 @@
 
 class Query(object):
     substitutions = ['$USER']
+    clause_re = re.compile(r'^(?:(?P<clause>\d+)_)?(?P<field>.+)')
 
     def __init__(self, env, report=None, constraints=None, cols=None,
                  order=None, desc=0, group=None, groupdesc=0, verbose=0,
@@ -113,6 +114,14 @@
         if self.group not in field_names:
             self.group = None
 
+        constraint_cols = {}
+        for k, v in self.constraints.iteritems():
+            m = self.clause_re.match(k)
+            if m:
+                col = constraint_cols.setdefault(m.group('field'), [])
+                col.append(v)
+        self.constraint_cols = constraint_cols
+
     @classmethod
     def from_string(cls, env, string, **kw):
         filters = string.split('&')
@@ -185,7 +194,7 @@
                 cols.append(col)
 
         def sort_columns(col1, col2):
-            constrained_fields = self.constraints.keys()
+            constrained_fields = self.constraint_cols.keys()
             if 'id' in (col1, col2):
                 # Ticket ID is always the first column
                 return col1 == 'id' and -1 or 1
@@ -203,17 +212,23 @@
         cols = self.get_all_columns()
         
         # Semi-intelligently remove columns that are restricted to a single
-        # value by a query constraint.
-        for col in [k for k in self.constraints.keys()
+        # value by a query constraint.            
+        for col in [k for k in self.constraint_cols.keys()
                     if k != 'id' and k in cols]:
-            constraint = self.constraints[col]
-            if len(constraint) == 1 and constraint[0] \
-                    and not constraint[0][0] in '!~^$' and col in cols \
-                    and col not in self.time_fields:
+            constraints = self.constraint_cols[col]
+            for constraint in constraints:
+                if not (len(constraint) == 1 and constraint[0]
+                        and not constraint[0][0] in '!~^$' and col in cols
+                        and col not in self.time_fields):
+                    break
+            else:
                 cols.remove(col)
-            if col == 'status' and not 'closed' in constraint \
-                    and 'resolution' in cols:
-                cols.remove('resolution')
+            if col == 'status' and 'resolution' in cols:
+                for constraint in constraints:
+                    if 'closed' in constraint:
+                        break
+                else:
+                    cols.remove('resolution')
         if self.group in cols:
             cols.remove(self.group)
 
@@ -390,7 +405,7 @@
         if self.rows:
             add_cols('reporter', *self.rows)
         add_cols('status', 'priority', 'time', 'changetime', self.order)
-        cols.extend([c for c in self.constraints.keys() if not c in cols])
+        cols.extend([c for c in self.constraint_cols if not c in cols])
 
         custom_fields = [f['name'] for f in self.fields if 'custom' in f]
 
@@ -420,6 +435,8 @@
             sql.append("\n  LEFT OUTER JOIN %s ON (%s.name=%s)"
                        % (col, col, col))
 
+        db = self.env.get_db_cnx()
+
         def get_constraint_sql(name, value, mode, neg):
             if name not in custom_fields:
                 col = 't.' + name
@@ -468,73 +485,86 @@
                                               db.like()),
                     (value, ))
 
-        db = self.env.get_db_cnx()
-        clauses = []
+        conj_clauses = {}
+        for k, v in self.constraints.iteritems():
+            m = self.clause_re.match(k)
+            if m:   
+                clause_num = int(m.group('clause') or 0)
+                field = m.group('field')
+                conj_clauses.setdefault(clause_num, {})[field] = v
         args = []
-        for k, v in self.constraints.items():
-            if req:
-                v = [val.replace('$USER', req.authname) for val in v]
-            # Determine the match mode of the constraint (contains,
-            # starts-with, negation, etc.)
-            neg = v[0].startswith('!')
-            mode = ''
-            if len(v[0]) > neg and v[0][neg] in ('~', '^', '$'):
-                mode = v[0][neg]
+        def get_conj_clause_sql(constraints):
+            clauses = []
+            for k, v in constraints.iteritems():
+                if req:
+                    v = [val.replace('$USER', req.authname) for val in v]
+                # Determine the match mode of the constraint (contains,
+                # starts-with, negation, etc.)
+                neg = v[0].startswith('!')
+                mode = ''
+                if len(v[0]) > neg and v[0][neg] in ('~', '^', '$'):
+                    mode = v[0][neg]
 
-            # Special case id ranges
-            if k == 'id':
-                ranges = Ranges()
-                for r in v:
-                    r = r.replace('!', '')
-                    ranges.appendrange(r)
-                ids = []
-                id_clauses = []
-                for a,b in ranges.pairs:
-                    if a == b:
-                        ids.append(str(a))
+                # Special case id ranges
+                if k == 'id':
+                    ranges = Ranges()
+                    for r in v:
+                        r = r.replace('!', '')
+                        ranges.appendrange(r)
+                    ids = []
+                    id_clauses = []
+                    for a,b in ranges.pairs:
+                        if a == b:
+                            ids.append(str(a))
+                        else:
+                            id_clauses.append('id BETWEEN %s AND %s')
+                            args.append(a)
+                            args.append(b)
+                    if ids:
+                        id_clauses.append('id IN (%s)' % (','.join(ids)))
+                    if id_clauses:
+                        clauses.append('%s(%s)' % (neg and 'NOT ' or '',
+                                                   ' OR '.join(id_clauses)))
+                # Special case for exact matches on multiple values
+                elif not mode and len(v) > 1 and k not in self.time_fields:
+                    if k not in custom_fields:
+                        col = 't.' + k
                     else:
-                        id_clauses.append('id BETWEEN %s AND %s')
-                        args.append(a)
-                        args.append(b)
-                if ids:
-                    id_clauses.append('id IN (%s)' % (','.join(ids)))
-                if id_clauses:
-                    clauses.append('%s(%s)' % (neg and 'NOT ' or '',
-                                               ' OR '.join(id_clauses)))
-            # Special case for exact matches on multiple values
-            elif not mode and len(v) > 1 and k not in self.time_fields:
-                if k not in custom_fields:
-                    col = 't.' + k
-                else:
-                    col = k + '.value'
-                clauses.append("COALESCE(%s,'') %sIN (%s)"
-                               % (col, neg and 'NOT ' or '',
-                                  ','.join(['%s' for val in v])))
-                args += [val[neg:] for val in v]
-            elif len(v) > 1:
-                constraint_sql = filter(None,
-                                        [get_constraint_sql(k, val, mode, neg)
-                                         for val in v])
-                if not constraint_sql:
-                    continue
-                if neg:
-                    clauses.append("(" + " AND ".join(
-                        [item[0] for item in constraint_sql]) + ")")
-                else:
-                    clauses.append("(" + " OR ".join(
-                        [item[0] for item in constraint_sql]) + ")")
-                for item in constraint_sql:
-                    args.extend(item[1])
-            elif len(v) == 1:
-                constraint_sql = get_constraint_sql(k, v[0], mode, neg)
-                if constraint_sql:
-                    clauses.append(constraint_sql[0])
-                    args.extend(constraint_sql[1])
+                        col = k + '.value'
+                    clauses.append("COALESCE(%s,'') %sIN (%s)"
+                                   % (col, neg and 'NOT ' or '',
+                                      ','.join(['%s' for val in v])))
+                    args.extend([val[neg:] for val in v])
+                elif len(v) > 1:
+                    constraint_sql = filter(None,
+                                            [get_constraint_sql(k, val, mode, neg)
+                                             for val in v])
+                    if not constraint_sql:
+                        continue
+                    if neg:
+                        clauses.append("(" + " AND ".join(
+                            [item[0] for item in constraint_sql]) + ")")
+                    else:
+                        clauses.append("(" + " OR ".join(
+                            [item[0] for item in constraint_sql]) + ")")
+                    for item in constraint_sql:
+                        args.extend(item[1])
+                elif len(v) == 1:
+                    constraint_sql = get_constraint_sql(k, v[0], mode, neg)
+                    if constraint_sql:
+                        clauses.append(constraint_sql[0])
+                        args.extend(constraint_sql[1])
+            return " AND ".join(clauses)
 
-        clauses = filter(None, clauses)
-        if clauses:
+        conj_clauses = [get_conj_clause_sql(c)
+                        for c in conj_clauses.itervalues()]
+        conj_clauses = filter(None, conj_clauses)
+        if conj_clauses:
             sql.append("\nWHERE ")
-            sql.append(" AND ".join(clauses))
+            if len(conj_clauses) > 1:
+                sql.append(" OR ".join(['(%s)' % c for c in conj_clauses]))
+            else:
+                sql.append(conj_clauses[0])
             if cached_ids:
                 sql.append(" OR ")
                 sql.append("id in (%s)" % (','.join(
@@ -544,6 +574,7 @@
         order_cols = [(self.order, self.desc)]
         if self.group and self.group != self.order:
             order_cols.insert(0, (self.group, self.groupdesc))
+
         for name, desc in order_cols:
             if name in custom_fields or name in enum_columns:
                 col = name + '.value'
@@ -581,7 +612,11 @@
     def template_data(self, context, tickets, orig_list=None, orig_time=None,
                       req=None):
         constraints = {}
+        clauses = {}
         for k, v in self.constraints.items():
+            m = self.clause_re.match(k)
+            clause_num = int(m.group('clause') or 0)
+            clauses.setdefault(clause_num, []).append(k)
             constraint = {'values': [], 'mode': ''}
             for val in v:
                 neg = val.startswith('!')
@@ -594,6 +629,20 @@
                 constraint['mode'] = (neg and '!' or '') + mode
                 constraint['values'].append(val)
             constraints[k] = constraint
+        # Take the individual constraint keys and group them into lists
+        # according to which 'clause' they belong to, while collapsing down the
+        # clause numbers in case there are skipped clauses.        
+        clauses = [v for k, v in sorted(clauses.items(), key=lambda x: x[0])]
+        for idx, csts in enumerate(clauses):
+            m = self.clause_re.match(csts[0])
+            clause_num = int(m.group('clause') or 0)
+            if clause_num != idx:
+                pre = idx and '%s_' % idx or ''
+                for jdx, cst in enumerate(csts):
+                    new = pre + self.clause_re.match(cst).group('field')
+                    csts[jdx] = new
+                    constraints[new] = constraints[cst]
+                    del constraints[cst]
 
         cols = self.get_columns()
         labels = dict([(f['name'], f['label']) for f in self.fields])
@@ -700,6 +749,7 @@
                 'col': cols,
                 'row': self.rows,
                 'constraints': constraints,
+                'clauses': clauses,
                 'labels': labels,
                 'headers': headers,
                 'fields': fields,
@@ -872,29 +922,36 @@
             else:
                 remove_constraints[to_remove[0]] = -1
 
-        for field in [k for k in req.args.keys() if k in ticket_fields]:
-            vals = req.args[field]
+        for k, vals in req.args.iteritems():
+            m = Query.clause_re.match(k)
+            if not m:
+                continue
+            field = m.group('field')
+            clause_num = int(m.group('clause') or 0)
+            pre = clause_num and '%s_' % clause_num or ''
+            if field not in ticket_fields:
+                continue
             if not isinstance(vals, (list, tuple)):
                 vals = [vals]
             if vals:
-                mode = req.args.get(field + '_mode')
+                mode = req.args.get(pre + field + '_mode')
                 if mode:
                     vals = [mode + x for x in vals]
                 if field in time_fields:
-                    ends = req.args.getlist(field + '_end')
+                    ends = req.args.getlist(pre + field + '_end')
                     if ends:
                         vals = [start + ';' + end 
                                 for (start, end) in zip(vals, ends)]
-                if field in remove_constraints:
-                    idx = remove_constraints[field]
+                if pre + field in remove_constraints:
+                    idx = remove_constraints[pre + field]
                     if idx >= 0:
                         del vals[idx]
                         if not vals:
                             continue
                     else:
                         continue
-                constraints.setdefault(synonyms.get(field, field), 
-                                       []).extend(vals)
+                field = synonyms.get(field, field)
+                constraints.setdefault(pre + field, []).extend(vals)
 
         return constraints
 
@@ -929,11 +986,26 @@
         # For clients without JavaScript, we add a new constraint here if
         # requested
         constraints = data['constraints']
-        if 'add' in req.args:
-            field = req.args.get('add_filter')
-            if field:
-                constraint = constraints.setdefault(field, {})
-                constraint.setdefault('values', []).append('')
+        clauses = data['clauses']
+        for arg in req.args:
+            if arg.startswith('add_'):
+                clause_num = arg.rsplit('_', 1)[1]
+                try:
+                    clause_num = int(clause_num)
+                except ValueError:
+                    continue
+                field = req.args['add_filter_%s' % clause_num]
+                if field:
+                    pre = clause_num and '%s_' % clause_num or ''
+                    field = pre + field
+                    constraint = constraints.setdefault(field, {})
+                    constraint.setdefault('values', []).append('')
+                    if clause_num == len(clauses):
+                        clauses.append([field])
+                    else:
+                        clauses[clause_num].append(field)
+                        clauses[clause_num].sort()
+                    break
                 # FIXME: '' not always correct (e.g. checkboxes)
 
         req.session['query_href'] = query.get_href(context.href)
Index: trac/ticket/tests/functional.py
===================================================================
--- trac/ticket/tests/functional.py	(revision 8019)
+++ trac/ticket/tests/functional.py	(working copy)
@@ -187,8 +187,8 @@
         self._tester.go_to_query()
         # We don't have the luxury of javascript, so this is a multi-step
         # process
-        tc.formvalue('query', 'add_filter', 'summary')
-        tc.submit('add')
+        tc.formvalue('query', 'add_filter_0', 'summary')
+        tc.submit('add_0')
         tc.formvalue('query', 'owner', 'nothing')
         tc.submit('rm_filter_owner_0')
         tc.formvalue('query', 'summary', 'TestTicketQueryLinks')
Index: trac/htdocs/css/report.css
===================================================================
--- trac/htdocs/css/report.css	(revision 8019)
+++ trac/htdocs/css/report.css	(working copy)
@@ -34,6 +34,7 @@
 #query hr { clear: both; margin: 0; visibility: hidden }
 
 #filters table { width: 100% }
+#filters table.clause { border: 1px solid #d7d7d7; }
 #filters tr { height: 2em }
 #filters th, #filters td { padding: 0 .2em; vertical-align: middle }
 #filters th { font-size: 11px; text-align: right; white-space: nowrap; }
Index: trac/ticket/templates/query.html
===================================================================
--- trac/ticket/templates/query.html	(revision 8019)
+++ trac/ticket/templates/query.html	(working copy)
@@ -37,105 +37,112 @@
           <input py:if="'id' in query.constraints" type="hidden" name="id" value="${query.constraints['id']}" />
           <legend class="foldable">Filters</legend>
           <table summary="Query filters">
-            <tbody>
-              <tr style="height: 1px"><td colspan="4"></td></tr>
-            </tbody>
-            <py:for each="field_name in field_names" py:with="field = fields[field_name]">
-              <py:for each="constraint_name, constraint in constraints.items()">
-                <tbody py:if="field_name == constraint_name"
-                  py:with="multiline = field.type in ('select', 'text', 'textarea', 'time')">
-                  <py:for each="constraint_idx, constraint_value in enumerate(constraint['values'])">
-                    <tr class="${field_name}" py:if="multiline or constraint_idx == 0">
-                      <py:choose test="constraint_idx">
-                        <py:when test="0">
-                          <th scope="row"><label id="label_${field_name}">$field.label</label></th>
-                          <td py:if="field.type not in ('radio', 'checkbox', 'time')" class="mode">
-                            <select name="${field_name}_mode">
-                              <option py:for="mode in modes[field.type]" value="$mode.value"
-                                selected="${mode.value == constraint.mode and 'selected' or None}">$mode.name
+            <tbody py:for="clause_num, clause in enumerate(clauses + [[]])"
+                   py:with="clause_pre = clause_num and '%s_' % clause_num or ''">
+              <tr py:if="clause_num"><td>&mdash; OR &mdash;</td></tr>
+              <tr><td><table class="clause">
+                <tbody>
+                  <tr style="height: 1px"><td colspan="4"></td></tr>
+                </tbody>            
+                <py:for each="field_name in field_names" py:with="field = fields[field_name]">
+                  <py:for each="constraint_name in clause">
+                    <tbody py:if="clause_pre + field_name == constraint_name"
+                      py:with="multiline = field.type in ('select', 'text', 'textarea', 'time');
+                               constraint = constraints[constraint_name]">
+                      <py:for each="constraint_idx, constraint_value in enumerate(constraint['values'])">
+                        <tr class="${field_name}"
+                            py:if="multiline or constraint_idx == 0">
+                          <py:choose test="constraint_idx">
+                            <py:when test="0">
+                              <th scope="row"><label id="label_${constraint_name}">$field.label</label></th>
+                              <td py:if="field.type not in ('radio', 'checkbox', 'time')" class="mode">
+                                <select name="${constraint_name}_mode">
+                                  <option py:for="mode in modes[field.type]" value="$mode.value"
+                                    selected="${mode.value == constraint.mode and 'selected' or None}">$mode.name
+                                  </option>
+                                </select>
+                              </td>
+                            </py:when>
+                            <py:otherwise><!--! not the first line of a multiline constraint -->
+                              <th colspan="${field.type == 'time' and 1 or 2}"><label>or</label></th>
+                            </py:otherwise>
+                          </py:choose>
+
+                          <td class="filter" colspan="${field.type in ('radio', 'checkbox', 'time') and 2 or None}"
+                              py:choose="">
+
+                          <py:when test="field.type == 'select'">
+                            <select name="${constraint_name}">
+                              <option></option>
+                              <option py:for="option in field.options"
+                                selected="${option == constraint_value and 'selected' or None}">$option
                               </option>
                             </select>
-                          </td>
-                        </py:when>
-                        <py:otherwise><!--! not the first line of a multiline constraint -->
-                          <th colspan="${field.type == 'time' and 1 or 2}"><label>or</label></th>
-                        </py:otherwise>
-                      </py:choose>
+                          </py:when>
 
-                      <td class="filter" colspan="${field.type in ('radio', 'checkbox', 'time') and 2 or None}"
-                          py:choose="">
+                          <py:when test="field.type == 'radio'">
+                            <py:for each="option in field.options">
+                              <input type="checkbox" id="${constraint_name}_$option" name="${constraint_name}"
+                                value="$option"
+                               checked="${any([(value == option) == (constraint.mode == '')
+                                                for value in constraint['values']]) and 'checked' or None}" />
+                              <label for="${constraint_name}_$option" class="control">${option or 'none'}</label>
+                           </py:for>
+                          </py:when>
 
-                        <py:when test="field.type == 'select'">
-                          <select name="${constraint_name}">
-                            <option></option>
-                            <option py:for="option in field.options"
-                              selected="${option == constraint_value and 'selected' or None}">$option
-                            </option>
-                          </select>
-                        </py:when>
+                          <py:when test="field.type == 'checkbox'">
+                            <input type="radio" id="${constraint_name}_on" name="$constraint_name" value="1"
+                                   checked="${constraint.mode != '!' or constraint_value == '1' or None}" />
+                            <label for="${field_name}_on" class="control">yes</label>
+                            <input type="radio" id="${constraint_name}_off" name="$constraint_name" value="0"
+                                   checked="${constraint.mode == '!' or constraint_value != '1' or None}" />
+                            <label for="${field_name}_off" class="control">no</label>
+                          </py:when>
 
-                        <py:when test="field.type == 'radio'">
-                          <py:for each="option in field.options">
-                            <input type="checkbox" id="${field_name}_$option" name="${field_name}"
-                              value="$option"
-                              checked="${any([(value == option) == (constraint.mode == '')
-                                              for value in constraint['values']]) and 'checked' or None}" />
-                            <label for="${field_name}_$option" class="control">${option or 'none'}</label>
-                          </py:for>
-                        </py:when>
+                          <py:when test="field.type in ('text', 'textarea')">
+                            <input type="text" name="${constraint_name}" value="$constraint_value" size="42" />
+                          </py:when>
+                          
+                          <py:when test="'time'" py:with="(start, end) = ';' in constraint_value 
+                                                          and constraint_value.split(';', 1)
+                                                          or (constraint_value, '')">
+                            <label>between</label>
+                            <input type="text" name="${constraint_name}" value="$start" size="14" />
+                            <label>and</label>
+                            <input type="text" name="${constraint_name}_end" value="$end" size="14" />
+                          </py:when>
 
-                        <py:when test="field.type == 'checkbox'">
-                          <input type="radio" id="${field_name}_on" name="$field_name" value="1"
-                                 checked="${constraint.mode != '!' or constraint_value == '1' or None}" />
-                          <label for="${field_name}_on" class="control">yes</label>
-                          <input type="radio" id="${field_name}_off" name="$field_name" value="0"
-                                 checked="${constraint.mode == '!' or constraint_value != '1' or None}" />
-                          <label for="${field_name}_off" class="control">no</label>
-                        </py:when>
+                        </td>
+                        <td class="actions"
+                            py:with="rm_idx = multiline and constraint_idx or len(constraint['values']) - 1"><input
+                            type="submit" name="rm_filter_${constraint_name}${field.type != 'radio' and '_%d' % rm_idx or ''}"
+                            value="-" /></td>
+                      </tr>
+                    </py:for>
+                  </tbody>
+                </py:for>
+             </py:for>
 
-                        <py:when test="field.type in ('text', 'textarea')">
-                          <input type="text" name="${field_name}" value="$constraint_value" size="42" />
-                        </py:when>
-                        
-                        <py:when test="'time'" py:with="(start, end) = ';' in constraint_value 
-                                                        and constraint_value.split(';', 1)
-                                                        or (constraint_value, '')">
-                          <label>between</label>
-                          <input type="text" name="${field_name}" value="$start" size="14" />
-                          <label>and</label>
-                          <input type="text" name="${field_name}_end" value="$end" size="14" />
-                        </py:when>
-
-                      </td>
-                      <td class="actions"
-                          py:with="rm_idx = multiline and constraint_idx or len(constraint['values']) - 1"><input
-                          type="submit" name="rm_filter_${field_name}${field.type != 'radio' and '_%d' % rm_idx or ''}"
-                          value="-" /></td>
-                    </tr>
-                  </py:for>
-                </tbody>
-              </py:for>
-            </py:for>
-
             <tbody>
               <tr class="actions">
                 <td class="actions" colspan="4" style="text-align: right">
-                  <label for="add_filter">Add filter</label>&nbsp;
-                  <select name="add_filter" id="add_filter">
+                  <label for="add_filter_${clause_num}">Add filter</label>&nbsp;
+                  <select name="add_filter_${clause_num}" id="add_filter_${clause_num}">
                     <option></option>
                     <option py:for="field_name in field_names" py:with="field = fields[field_name]"
                             value="$field_name"
                             disabled="${(field.type == 'radio' and
-                                         field_name in constraints and
-                                         len(constraints[field_name])) or None}">
+                                         clause_pre + field_name in constraints and
+                                         len(constraints[clause_pre + field_name])) or None}">
                       ${field.label}
                     </option>
                   </select>
-                  <input type="submit" name="add" value="+" />
+                  <input type="submit" name="add_${clause_num}" value="+" />
                 </td>
               </tr>
             </tbody>
-          </table>
+          </table></td></tr>
+        </tbody></table>
         </fieldset>
 
         <!--! Allow the user to decide what columns to include in the output of the query -->
Index: trac/htdocs/js/query.js
===================================================================
--- trac/htdocs/js/query.js	(revision 8019)
+++ trac/htdocs/js/query.js	(working copy)
@@ -15,6 +15,13 @@
   
     // Removes an existing row from the filters table
     function removeRow(button, propertyName) {
+      var field = propertyName.split("_");
+      if (isNaN(field[0])) {
+        var clauseNum = 0;
+      } else {
+        var clauseNum = field.shift();
+      }
+      field = field.join("_");
       var tr = getAncestorByTagName(button, "tr");
       var label = document.getElementById("label_" + propertyName);
       if (label && (getAncestorByTagName(label, "tr") == tr)) {
@@ -45,18 +52,33 @@
           }
         }
       }
-  
-      var tBody = tr.parentNode;
-      tBody.deleteRow(tr.sectionRowIndex);
-      if (!tBody.rows.length) {
-          tBody.parentNode.removeChild(tBody);
+
+      tr = $(tr);
+      var tBody = tr.parent();
+      tr.remove();
+      if (!tBody.children().length) {
+          var table = tBody.parents("table.clause");
+          tBody.remove();
+          if (table.children().length < 3 && $("table.clause").length > 1) {
+            //As long two clauses remain, remove empty clauses
+            //Also, don't remove the last clause.
+            tBody = table.parents("tbody").eq(0);
+            //Remove the "OR" text from the next clause if this is the first clause
+            if (!tBody.prev().length) {
+              tBody.next().children().eq(0).remove();
+            }
+            if (tBody.next().length) {
+              tBody.remove();
+            }
+            return;
+          }
       }
       
       if (propertyName) {
-        var select = document.forms["query"].elements["add_filter"];
+        var select = document.forms["query"].elements["add_filter_" + clauseNum];
         for (var i = 0; i < select.options.length; i++) {
           var option = select.options[i];
-          if (option.value == propertyName) option.disabled = false;
+          if (option.value == field) option.disabled = false;
         }
       }
     }
@@ -93,203 +115,235 @@
         initializeFilter(input);
       }
     }
-  
-    // Make the drop-down menu for adding a filter a client-side trigger
-    var addButton = document.forms["query"].elements["add"];
-    addButton.parentNode.removeChild(addButton);
-    var select = document.getElementById("add_filter");
-    select.onchange = function() {
-      if (select.selectedIndex < 1) return;
-  
-      if (select.options[select.selectedIndex].disabled) {
-        // Neither IE nor Safari supported disabled options at the time this was
-        // written, so alert the user
-        alert("A filter already exists for that property");
-        return;
+
+    function addClause(select) {
+      // Determine if this is the bottom clause.  If so, append a new empty
+      // clause to the end of the list.
+      select = $(select);
+      var clauses = $("table.clause");
+      var clauseIndex = clauses.index(select.parents("table.clause"));
+      if (clauseIndex == clauses.length - 1) {
+        var clauseNum = select.attr("name").split("_").pop();
+        var tBody = select.parents("tbody").eq(1);
+        var copy = tBody.clone();
+        var newId = "add_filter_" + (clauseNum + 1);
+        $("label", copy).attr("for", newId);
+        $("select", copy).attr("id", newId).attr("name", newId);
+        tBody.after(copy);
+        addFilter(clauseIndex + 1);
       }
-  
-      // Convenience function for creating a <label>
-      function createLabel(text, htmlFor) {
-        var label = document.createElement("label");
-        if (text) label.appendChild(document.createTextNode(text));
-        if (htmlFor) {
-          label.htmlFor = htmlFor;
-          label.className = "control";
+    }
+
+    function addFilter(clauseIndex) {
+      var select = $("select[name^=add_filter_]")[clauseIndex];
+      select.onchange = function() {
+        if (select.selectedIndex < 1) return;
+    
+        if (select.options[select.selectedIndex].disabled) {
+          // Neither IE nor Safari supported disabled options at the time this was
+          // written, so alert the user
+          alert("A filter already exists for that property");
+          return;
         }
-        return label;
-      }
-  
-      // Convenience function for creating an <input type="text">
-      function createText(name, size) {
-        var input = document.createElement("input");
-        input.type = "text";
-        if (name) input.name = name;
-        if (size) input.size = size;
-        return input;
-      }
-      
-      // Convenience function for creating an <input type="checkbox">
-      function createCheckbox(name, value, id) {
-        var input = document.createElement("input");
-        input.type = "checkbox";
-        if (name) input.name = name;
-        if (value) input.value = value;
-        if (id) input.id = id;
-        return input;
-      }
-  
-      // Convenience function for creating an <input type="radio">
-      function createRadio(name, value, id) {
-        var str = '<input type="radio"';
-        if (name) str += ' name="' + name + '"';
-        if (value) str += ' value="' + value + '"';
-        if (id) str += ' id="' + id + '"'; 
-        str += '/>';
-        var span = document.createElement('span');
-        // create radio button with innerHTML to avoid IE mangling it.
-        span.innerHTML = str; 
-        return span;
-      }
-  
-      // Convenience function for creating a <select>
-      function createSelect(name, options, optional) {
-        var e = document.createElement("select");
-        if (name) e.name = name;
-        if (optional) e.options[0] = new Option();
-        if (options) {
-          for (var i = 0; i < options.length; i++) {
-            var option;
-            if (typeof(options[i]) == "object") {
-              option = new Option(options[i].text, options[i].value);
-            } else {
-              option = new Option(options[i], options[i]);
-            }
-            e.options[e.options.length] = option;
+
+
+        // Add a new empty clause (only applies if this is the previous empty clause
+        addClause(select);
+    
+        // Convenience function for creating a <label>
+        function createLabel(text, htmlFor) {
+          var label = document.createElement("label");
+          if (text) label.appendChild(document.createTextNode(text));
+          if (htmlFor) {
+            label.htmlFor = htmlFor;
+            label.className = "control";
           }
+          return label;
         }
-        return e;
-      }
-  
-      var propertyName = select.options[select.selectedIndex].value;
-      var property = properties[propertyName];
-      var table = document.getElementById("filters").getElementsByTagName("table")[0];
-      var tr = document.createElement("tr");
-      tr.className = propertyName;
-  
-      var alreadyPresent = false;
-      for (var i = 0; i < table.rows.length; i++) {
-        if (table.rows[i].className == propertyName) {
-          var existingTBody = table.rows[i].parentNode;
-          alreadyPresent = true;
-          break;
+    
+        // Convenience function for creating an <input type="text">
+        function createText(name, size) {
+          var input = document.createElement("input");
+          input.type = "text";
+          if (name) input.name = name;
+          if (size) input.size = size;
+          return input;
         }
-      }
-  
-      // Add the row header
-      var th = document.createElement("th");
-      th.scope = "row";
-      if (!alreadyPresent) {
-        var label = createLabel(property.label);
-        label.id = "label_" + propertyName;
-        th.appendChild(label);
-      } else {
-        th.colSpan = property.type == "time"? 1: 2;
-        th.appendChild(createLabel("or"));
-      }
-      tr.appendChild(th);
-  
-      var td = document.createElement("td");
-      var focusElement = null;
-      if (property.type == "radio" || property.type == "checkbox" || property.type == "time") {
-        td.colSpan = 2;
-        td.className = "filter";
-        if (property.type == "radio") {
-          for (var i = 0; i < property.options.length; i++) {
-            var option = property.options[i];
-            td.appendChild(createCheckbox(propertyName, option,
-              propertyName + "_" + option));
-            td.appendChild(createLabel(option ? option : "none",
-              propertyName + "_" + option));
+        
+        // Convenience function for creating an <input type="checkbox">
+        function createCheckbox(name, value, id) {
+          var input = document.createElement("input");
+          input.type = "checkbox";
+          if (name) input.name = name;
+          if (value) input.value = value;
+          if (id) input.id = id;
+          return input;
+        }
+    
+        // Convenience function for creating an <input type="radio">
+        function createRadio(name, value, id) {
+          var str = '<input type="radio"';
+          if (name) str += ' name="' + name + '"';
+          if (value) str += ' value="' + value + '"';
+          if (id) str += ' id="' + id + '"'; 
+          str += '/>';
+          var span = document.createElement('span');
+          // create radio button with innerHTML to avoid IE mangling it.
+          span.innerHTML = str; 
+          return span;
+        }
+    
+        // Convenience function for creating a <select>
+        function createSelect(name, options, optional) {
+          var e = document.createElement("select");
+          if (name) e.name = name;
+          if (optional) e.options[0] = new Option();
+          if (options) {
+            for (var i = 0; i < options.length; i++) {
+              var option;
+              if (typeof(options[i]) == "object") {
+                option = new Option(options[i].text, options[i].value);
+              } else {
+                option = new Option(options[i], options[i]);
+              }
+              e.options[e.options.length] = option;
+            }
           }
-        } else if (property.type == "checkbox") {
-          td.appendChild(createRadio(propertyName, "1", propertyName + "_on"));
-          td.appendChild(document.createTextNode(" "));
-          td.appendChild(createLabel("yes", propertyName + "_on"));
-          td.appendChild(createRadio(propertyName, "0", propertyName + "_off"));
-          td.appendChild(document.createTextNode(" "));
-          td.appendChild(createLabel("no", propertyName + "_off"));
-        } else if (property.type == "time") {
-          td.appendChild(createLabel("between"));
-          td.appendChild(document.createTextNode(" "));
-          focusElement = createText(propertyName, 14);
-          td.appendChild(focusElement);
-          td.appendChild(document.createTextNode(" "));
-          td.appendChild(createLabel("and"));
-          td.appendChild(document.createTextNode(" "));
-          td.appendChild(createText(propertyName + "_end", 14));
+          return e;
         }
-        tr.appendChild(td);
-      } else {
+    
+        var propertyName = select.options[select.selectedIndex].value;
+        var property = properties[propertyName];
+        var table = $(select).parents("table.clause")[0];
+        var tr = document.createElement("tr");
+        tr.className = propertyName;
+    
+        var alreadyPresent = false;
+        for (var i = 0; i < table.rows.length; i++) {
+          if (table.rows[i].className == propertyName) {
+            var existingTBody = table.rows[i].parentNode;
+            alreadyPresent = true;
+            break;
+          }
+        }
+
+        var clauseNum = $(select).attr("name").split("_").pop();
+        if (clauseNum) {
+          propertyName = clauseNum + "_" + propertyName;
+        }
+
+        // Add the row header
+        var th = document.createElement("th");
+        th.scope = "row";
         if (!alreadyPresent) {
-          // Add the mode selector
-          td.className = "mode";
-          var modeSelect = createSelect(propertyName + "_mode",
-                                        modes[property.type]);
-          td.appendChild(modeSelect);
+          var label = createLabel(property.label);
+          label.id = "label_" + propertyName;
+          th.appendChild(label);
+        } else {
+          th.colSpan = property.type == "time"? 1: 2;
+          th.appendChild(createLabel("or"));
+        }
+        tr.appendChild(th);
+    
+        var td = document.createElement("td");
+        var focusElement = null;
+        if (property.type == "radio" || property.type == "checkbox" || property.type == "time") {
+          td.colSpan = 2;
+          td.className = "filter";
+          if (property.type == "radio") {
+            for (var i = 0; i < property.options.length; i++) {
+              var option = property.options[i];
+              td.appendChild(createCheckbox(propertyName, option,
+                propertyName + "_" + option));
+              td.appendChild(createLabel(option ? option : "none",
+                propertyName + "_" + option));
+            }
+          } else if (property.type == "checkbox") {
+            td.appendChild(createRadio(propertyName, "1", propertyName + "_on"));
+            td.appendChild(document.createTextNode(" "));
+            td.appendChild(createLabel("yes", propertyName + "_on"));
+            td.appendChild(createRadio(propertyName, "0", propertyName + "_off"));
+            td.appendChild(document.createTextNode(" "));
+            td.appendChild(createLabel("no", propertyName + "_off"));
+          } else if (property.type == "time") {
+            td.appendChild(createLabel("between"));
+            td.appendChild(document.createTextNode(" "));
+            focusElement = createText(propertyName, 14);
+            td.appendChild(focusElement);
+            td.appendChild(document.createTextNode(" "));
+            td.appendChild(createLabel("and"));
+            td.appendChild(document.createTextNode(" "));
+            td.appendChild(createText(propertyName + "_end", 14));
+          }
           tr.appendChild(td);
+        } else {
+          if (!alreadyPresent) {
+            // Add the mode selector
+            td.className = "mode";
+            var modeSelect = createSelect(propertyName + "_mode",
+                                          modes[property.type]);
+            td.appendChild(modeSelect);
+            tr.appendChild(td);
+          }
+    
+          // Add the selector or text input for the actual filter value
+          td = document.createElement("td");
+          td.className = "filter";
+          if (property.type == "select") {
+            focusElement = createSelect(propertyName, property.options, true);
+          } else if ((property.type == "text") || (property.type == "textarea")) {
+            focusElement = createText(propertyName, 42);
+          }
+          td.appendChild(focusElement);
+          tr.appendChild(td);
         }
-  
-        // Add the selector or text input for the actual filter value
+    
+        // Add the add and remove buttons
         td = document.createElement("td");
-        td.className = "filter";
-        if (property.type == "select") {
-          focusElement = createSelect(propertyName, property.options, true);
-        } else if ((property.type == "text") || (property.type == "textarea")) {
-          focusElement = createText(propertyName, 42);
-        }
-        td.appendChild(focusElement);
+        td.className = "actions";
+        var removeButton = document.createElement("input");
+        removeButton.type = "button";
+        removeButton.value = "-";
+        removeButton.onclick = function() { removeRow(removeButton, propertyName) };
+        td.appendChild(removeButton);
         tr.appendChild(td);
-      }
-  
-      // Add the add and remove buttons
-      td = document.createElement("td");
-      td.className = "actions";
-      var removeButton = document.createElement("input");
-      removeButton.type = "button";
-      removeButton.value = "-";
-      removeButton.onclick = function() { removeRow(removeButton, propertyName) };
-      td.appendChild(removeButton);
-      tr.appendChild(td);
-  
-      if (alreadyPresent) {
-        existingTBody.appendChild(tr);
-      } else {
-        // Find the insertion point for the new row. We try to keep the filter rows
-        // in the same order as the options in the 'Add filter' drop-down, because
-        // that's the order they'll appear in when submitted.
-        var insertionPoint = getAncestorByTagName(select, "tbody");
-        outer: for (var i = select.selectedIndex + 1; i < select.options.length; i++) {
-          for (var j = 0; j < table.tBodies.length; j++) {
-            if (table.tBodies[j].rows[0].className == select.options[i].value) {
-              insertionPoint = table.tBodies[j];
-              break outer;
+    
+        if (alreadyPresent) {
+          existingTBody.appendChild(tr);
+        } else {
+          // Find the insertion point for the new row. We try to keep the filter rows
+          // in the same order as the options in the 'Add filter' drop-down, because
+          // that's the order they'll appear in when submitted.
+          var insertionPoint = getAncestorByTagName(select, "tbody");
+          outer: for (var i = select.selectedIndex + 1; i < select.options.length; i++) {
+            for (var j = 0; j < table.tBodies.length; j++) {
+              if (table.tBodies[j].rows[0].className == select.options[i].value) {
+                insertionPoint = table.tBodies[j];
+                break outer;
+              }
             }
           }
+          // Finally add the new row to the table
+          var tbody = document.createElement("tbody");
+          tbody.appendChild(tr);
+          insertionPoint.parentNode.insertBefore(tbody, insertionPoint);
         }
-        // Finally add the new row to the table
-        var tbody = document.createElement("tbody");
-        tbody.appendChild(tr);
-        insertionPoint.parentNode.insertBefore(tbody, insertionPoint);
+        if(focusElement)
+            focusElement.focus();
+    
+        // Disable the add filter in the drop-down list
+        if (property.type == "radio" || property.type == "checkbox") {
+          select.options[select.selectedIndex].disabled = true;
+        }
+        select.selectedIndex = 0;
       }
-      if(focusElement)
-          focusElement.focus();
-  
-      // Disable the add filter in the drop-down list
-      if (property.type == "radio" || property.type == "checkbox") {
-        select.options[select.selectedIndex].disabled = true;
-      }
-      select.selectedIndex = 0;
     }
+
+    // Make the drop-down menu for adding a filter a client-side trigger
+    $("#query input[name^=add_][type=submit]").each(function(idx) {
+      $(this).remove();
+      addFilter(idx);
+    });
   }
 
-})(jQuery);
\ No newline at end of file
+})(jQuery);

