Ticket #2647: advanced-queries-ticket-2647-r8019.patch
| File advanced-queries-ticket-2647-r8019.patch, 47.5 KB (added by ebray, 3 years ago) |
|---|
-
trac/ticket/query.py
49 49 50 50 class Query(object): 51 51 substitutions = ['$USER'] 52 clause_re = re.compile(r'^(?:(?P<clause>\d+)_)?(?P<field>.+)') 52 53 53 54 def __init__(self, env, report=None, constraints=None, cols=None, 54 55 order=None, desc=0, group=None, groupdesc=0, verbose=0, … … 113 114 if self.group not in field_names: 114 115 self.group = None 115 116 117 constraint_cols = {} 118 for k, v in self.constraints.iteritems(): 119 m = self.clause_re.match(k) 120 if m: 121 col = constraint_cols.setdefault(m.group('field'), []) 122 col.append(v) 123 self.constraint_cols = constraint_cols 124 116 125 @classmethod 117 126 def from_string(cls, env, string, **kw): 118 127 filters = string.split('&') … … 185 194 cols.append(col) 186 195 187 196 def sort_columns(col1, col2): 188 constrained_fields = self.constraint s.keys()197 constrained_fields = self.constraint_cols.keys() 189 198 if 'id' in (col1, col2): 190 199 # Ticket ID is always the first column 191 200 return col1 == 'id' and -1 or 1 … … 203 212 cols = self.get_all_columns() 204 213 205 214 # Semi-intelligently remove columns that are restricted to a single 206 # value by a query constraint. 207 for col in [k for k in self.constraint s.keys()215 # value by a query constraint. 216 for col in [k for k in self.constraint_cols.keys() 208 217 if k != 'id' and k in cols]: 209 constraint = self.constraints[col] 210 if len(constraint) == 1 and constraint[0] \ 211 and not constraint[0][0] in '!~^$' and col in cols \ 212 and col not in self.time_fields: 218 constraints = self.constraint_cols[col] 219 for constraint in constraints: 220 if not (len(constraint) == 1 and constraint[0] 221 and not constraint[0][0] in '!~^$' and col in cols 222 and col not in self.time_fields): 223 break 224 else: 213 225 cols.remove(col) 214 if col == 'status' and not 'closed' in constraint \ 215 and 'resolution' in cols: 216 cols.remove('resolution') 226 if col == 'status' and 'resolution' in cols: 227 for constraint in constraints: 228 if 'closed' in constraint: 229 break 230 else: 231 cols.remove('resolution') 217 232 if self.group in cols: 218 233 cols.remove(self.group) 219 234 … … 390 405 if self.rows: 391 406 add_cols('reporter', *self.rows) 392 407 add_cols('status', 'priority', 'time', 'changetime', self.order) 393 cols.extend([c for c in self.constraint s.keys()if not c in cols])408 cols.extend([c for c in self.constraint_cols if not c in cols]) 394 409 395 410 custom_fields = [f['name'] for f in self.fields if 'custom' in f] 396 411 … … 420 435 sql.append("\n LEFT OUTER JOIN %s ON (%s.name=%s)" 421 436 % (col, col, col)) 422 437 438 db = self.env.get_db_cnx() 439 423 440 def get_constraint_sql(name, value, mode, neg): 424 441 if name not in custom_fields: 425 442 col = 't.' + name … … 468 485 db.like()), 469 486 (value, )) 470 487 471 db = self.env.get_db_cnx() 472 clauses = [] 488 conj_clauses = {} 489 for k, v in self.constraints.iteritems(): 490 m = self.clause_re.match(k) 491 if m: 492 clause_num = int(m.group('clause') or 0) 493 field = m.group('field') 494 conj_clauses.setdefault(clause_num, {})[field] = v 473 495 args = [] 474 for k, v in self.constraints.items(): 475 if req: 476 v = [val.replace('$USER', req.authname) for val in v] 477 # Determine the match mode of the constraint (contains, 478 # starts-with, negation, etc.) 479 neg = v[0].startswith('!') 480 mode = '' 481 if len(v[0]) > neg and v[0][neg] in ('~', '^', '$'): 482 mode = v[0][neg] 496 def get_conj_clause_sql(constraints): 497 clauses = [] 498 for k, v in constraints.iteritems(): 499 if req: 500 v = [val.replace('$USER', req.authname) for val in v] 501 # Determine the match mode of the constraint (contains, 502 # starts-with, negation, etc.) 503 neg = v[0].startswith('!') 504 mode = '' 505 if len(v[0]) > neg and v[0][neg] in ('~', '^', '$'): 506 mode = v[0][neg] 483 507 484 # Special case id ranges 485 if k == 'id': 486 ranges = Ranges() 487 for r in v: 488 r = r.replace('!', '') 489 ranges.appendrange(r) 490 ids = [] 491 id_clauses = [] 492 for a,b in ranges.pairs: 493 if a == b: 494 ids.append(str(a)) 508 # Special case id ranges 509 if k == 'id': 510 ranges = Ranges() 511 for r in v: 512 r = r.replace('!', '') 513 ranges.appendrange(r) 514 ids = [] 515 id_clauses = [] 516 for a,b in ranges.pairs: 517 if a == b: 518 ids.append(str(a)) 519 else: 520 id_clauses.append('id BETWEEN %s AND %s') 521 args.append(a) 522 args.append(b) 523 if ids: 524 id_clauses.append('id IN (%s)' % (','.join(ids))) 525 if id_clauses: 526 clauses.append('%s(%s)' % (neg and 'NOT ' or '', 527 ' OR '.join(id_clauses))) 528 # Special case for exact matches on multiple values 529 elif not mode and len(v) > 1 and k not in self.time_fields: 530 if k not in custom_fields: 531 col = 't.' + k 495 532 else: 496 id_clauses.append('id BETWEEN %s AND %s') 497 args.append(a) 498 args.append(b) 499 if ids: 500 id_clauses.append('id IN (%s)' % (','.join(ids))) 501 if id_clauses: 502 clauses.append('%s(%s)' % (neg and 'NOT ' or '', 503 ' OR '.join(id_clauses))) 504 # Special case for exact matches on multiple values 505 elif not mode and len(v) > 1 and k not in self.time_fields: 506 if k not in custom_fields: 507 col = 't.' + k 508 else: 509 col = k + '.value' 510 clauses.append("COALESCE(%s,'') %sIN (%s)" 511 % (col, neg and 'NOT ' or '', 512 ','.join(['%s' for val in v]))) 513 args += [val[neg:] for val in v] 514 elif len(v) > 1: 515 constraint_sql = filter(None, 516 [get_constraint_sql(k, val, mode, neg) 517 for val in v]) 518 if not constraint_sql: 519 continue 520 if neg: 521 clauses.append("(" + " AND ".join( 522 [item[0] for item in constraint_sql]) + ")") 523 else: 524 clauses.append("(" + " OR ".join( 525 [item[0] for item in constraint_sql]) + ")") 526 for item in constraint_sql: 527 args.extend(item[1]) 528 elif len(v) == 1: 529 constraint_sql = get_constraint_sql(k, v[0], mode, neg) 530 if constraint_sql: 531 clauses.append(constraint_sql[0]) 532 args.extend(constraint_sql[1]) 533 col = k + '.value' 534 clauses.append("COALESCE(%s,'') %sIN (%s)" 535 % (col, neg and 'NOT ' or '', 536 ','.join(['%s' for val in v]))) 537 args.extend([val[neg:] for val in v]) 538 elif len(v) > 1: 539 constraint_sql = filter(None, 540 [get_constraint_sql(k, val, mode, neg) 541 for val in v]) 542 if not constraint_sql: 543 continue 544 if neg: 545 clauses.append("(" + " AND ".join( 546 [item[0] for item in constraint_sql]) + ")") 547 else: 548 clauses.append("(" + " OR ".join( 549 [item[0] for item in constraint_sql]) + ")") 550 for item in constraint_sql: 551 args.extend(item[1]) 552 elif len(v) == 1: 553 constraint_sql = get_constraint_sql(k, v[0], mode, neg) 554 if constraint_sql: 555 clauses.append(constraint_sql[0]) 556 args.extend(constraint_sql[1]) 557 return " AND ".join(clauses) 533 558 534 clauses = filter(None, clauses) 535 if clauses: 559 conj_clauses = [get_conj_clause_sql(c) 560 for c in conj_clauses.itervalues()] 561 conj_clauses = filter(None, conj_clauses) 562 if conj_clauses: 536 563 sql.append("\nWHERE ") 537 sql.append(" AND ".join(clauses)) 564 if len(conj_clauses) > 1: 565 sql.append(" OR ".join(['(%s)' % c for c in conj_clauses])) 566 else: 567 sql.append(conj_clauses[0]) 538 568 if cached_ids: 539 569 sql.append(" OR ") 540 570 sql.append("id in (%s)" % (','.join( … … 544 574 order_cols = [(self.order, self.desc)] 545 575 if self.group and self.group != self.order: 546 576 order_cols.insert(0, (self.group, self.groupdesc)) 577 547 578 for name, desc in order_cols: 548 579 if name in custom_fields or name in enum_columns: 549 580 col = name + '.value' … … 581 612 def template_data(self, context, tickets, orig_list=None, orig_time=None, 582 613 req=None): 583 614 constraints = {} 615 clauses = {} 584 616 for k, v in self.constraints.items(): 617 m = self.clause_re.match(k) 618 clause_num = int(m.group('clause') or 0) 619 clauses.setdefault(clause_num, []).append(k) 585 620 constraint = {'values': [], 'mode': ''} 586 621 for val in v: 587 622 neg = val.startswith('!') … … 594 629 constraint['mode'] = (neg and '!' or '') + mode 595 630 constraint['values'].append(val) 596 631 constraints[k] = constraint 632 # Take the individual constraint keys and group them into lists 633 # according to which 'clause' they belong to, while collapsing down the 634 # clause numbers in case there are skipped clauses. 635 clauses = [v for k, v in sorted(clauses.items(), key=lambda x: x[0])] 636 for idx, csts in enumerate(clauses): 637 m = self.clause_re.match(csts[0]) 638 clause_num = int(m.group('clause') or 0) 639 if clause_num != idx: 640 pre = idx and '%s_' % idx or '' 641 for jdx, cst in enumerate(csts): 642 new = pre + self.clause_re.match(cst).group('field') 643 csts[jdx] = new 644 constraints[new] = constraints[cst] 645 del constraints[cst] 597 646 598 647 cols = self.get_columns() 599 648 labels = dict([(f['name'], f['label']) for f in self.fields]) … … 700 749 'col': cols, 701 750 'row': self.rows, 702 751 'constraints': constraints, 752 'clauses': clauses, 703 753 'labels': labels, 704 754 'headers': headers, 705 755 'fields': fields, … … 872 922 else: 873 923 remove_constraints[to_remove[0]] = -1 874 924 875 for field in [k for k in req.args.keys() if k in ticket_fields]: 876 vals = req.args[field] 925 for k, vals in req.args.iteritems(): 926 m = Query.clause_re.match(k) 927 if not m: 928 continue 929 field = m.group('field') 930 clause_num = int(m.group('clause') or 0) 931 pre = clause_num and '%s_' % clause_num or '' 932 if field not in ticket_fields: 933 continue 877 934 if not isinstance(vals, (list, tuple)): 878 935 vals = [vals] 879 936 if vals: 880 mode = req.args.get( field + '_mode')937 mode = req.args.get(pre + field + '_mode') 881 938 if mode: 882 939 vals = [mode + x for x in vals] 883 940 if field in time_fields: 884 ends = req.args.getlist( field + '_end')941 ends = req.args.getlist(pre + field + '_end') 885 942 if ends: 886 943 vals = [start + ';' + end 887 944 for (start, end) in zip(vals, ends)] 888 if field in remove_constraints:889 idx = remove_constraints[ field]945 if pre + field in remove_constraints: 946 idx = remove_constraints[pre + field] 890 947 if idx >= 0: 891 948 del vals[idx] 892 949 if not vals: 893 950 continue 894 951 else: 895 952 continue 896 constraints.setdefault(synonyms.get(field, field),897 []).extend(vals)953 field = synonyms.get(field, field) 954 constraints.setdefault(pre + field, []).extend(vals) 898 955 899 956 return constraints 900 957 … … 929 986 # For clients without JavaScript, we add a new constraint here if 930 987 # requested 931 988 constraints = data['constraints'] 932 if 'add' in req.args: 933 field = req.args.get('add_filter') 934 if field: 935 constraint = constraints.setdefault(field, {}) 936 constraint.setdefault('values', []).append('') 989 clauses = data['clauses'] 990 for arg in req.args: 991 if arg.startswith('add_'): 992 clause_num = arg.rsplit('_', 1)[1] 993 try: 994 clause_num = int(clause_num) 995 except ValueError: 996 continue 997 field = req.args['add_filter_%s' % clause_num] 998 if field: 999 pre = clause_num and '%s_' % clause_num or '' 1000 field = pre + field 1001 constraint = constraints.setdefault(field, {}) 1002 constraint.setdefault('values', []).append('') 1003 if clause_num == len(clauses): 1004 clauses.append([field]) 1005 else: 1006 clauses[clause_num].append(field) 1007 clauses[clause_num].sort() 1008 break 937 1009 # FIXME: '' not always correct (e.g. checkboxes) 938 1010 939 1011 req.session['query_href'] = query.get_href(context.href) -
trac/ticket/tests/functional.py
187 187 self._tester.go_to_query() 188 188 # We don't have the luxury of javascript, so this is a multi-step 189 189 # process 190 tc.formvalue('query', 'add_filter ', 'summary')191 tc.submit('add ')190 tc.formvalue('query', 'add_filter_0', 'summary') 191 tc.submit('add_0') 192 192 tc.formvalue('query', 'owner', 'nothing') 193 193 tc.submit('rm_filter_owner_0') 194 194 tc.formvalue('query', 'summary', 'TestTicketQueryLinks') -
trac/htdocs/css/report.css
34 34 #query hr { clear: both; margin: 0; visibility: hidden } 35 35 36 36 #filters table { width: 100% } 37 #filters table.clause { border: 1px solid #d7d7d7; } 37 38 #filters tr { height: 2em } 38 39 #filters th, #filters td { padding: 0 .2em; vertical-align: middle } 39 40 #filters th { font-size: 11px; text-align: right; white-space: nowrap; } -
trac/ticket/templates/query.html
37 37 <input py:if="'id' in query.constraints" type="hidden" name="id" value="${query.constraints['id']}" /> 38 38 <legend class="foldable">Filters</legend> 39 39 <table summary="Query filters"> 40 <tbody> 41 <tr style="height: 1px"><td colspan="4"></td></tr> 42 </tbody> 43 <py:for each="field_name in field_names" py:with="field = fields[field_name]"> 44 <py:for each="constraint_name, constraint in constraints.items()"> 45 <tbody py:if="field_name == constraint_name" 46 py:with="multiline = field.type in ('select', 'text', 'textarea', 'time')"> 47 <py:for each="constraint_idx, constraint_value in enumerate(constraint['values'])"> 48 <tr class="${field_name}" py:if="multiline or constraint_idx == 0"> 49 <py:choose test="constraint_idx"> 50 <py:when test="0"> 51 <th scope="row"><label id="label_${field_name}">$field.label</label></th> 52 <td py:if="field.type not in ('radio', 'checkbox', 'time')" class="mode"> 53 <select name="${field_name}_mode"> 54 <option py:for="mode in modes[field.type]" value="$mode.value" 55 selected="${mode.value == constraint.mode and 'selected' or None}">$mode.name 40 <tbody py:for="clause_num, clause in enumerate(clauses + [[]])" 41 py:with="clause_pre = clause_num and '%s_' % clause_num or ''"> 42 <tr py:if="clause_num"><td>— OR —</td></tr> 43 <tr><td><table class="clause"> 44 <tbody> 45 <tr style="height: 1px"><td colspan="4"></td></tr> 46 </tbody> 47 <py:for each="field_name in field_names" py:with="field = fields[field_name]"> 48 <py:for each="constraint_name in clause"> 49 <tbody py:if="clause_pre + field_name == constraint_name" 50 py:with="multiline = field.type in ('select', 'text', 'textarea', 'time'); 51 constraint = constraints[constraint_name]"> 52 <py:for each="constraint_idx, constraint_value in enumerate(constraint['values'])"> 53 <tr class="${field_name}" 54 py:if="multiline or constraint_idx == 0"> 55 <py:choose test="constraint_idx"> 56 <py:when test="0"> 57 <th scope="row"><label id="label_${constraint_name}">$field.label</label></th> 58 <td py:if="field.type not in ('radio', 'checkbox', 'time')" class="mode"> 59 <select name="${constraint_name}_mode"> 60 <option py:for="mode in modes[field.type]" value="$mode.value" 61 selected="${mode.value == constraint.mode and 'selected' or None}">$mode.name 62 </option> 63 </select> 64 </td> 65 </py:when> 66 <py:otherwise><!--! not the first line of a multiline constraint --> 67 <th colspan="${field.type == 'time' and 1 or 2}"><label>or</label></th> 68 </py:otherwise> 69 </py:choose> 70 71 <td class="filter" colspan="${field.type in ('radio', 'checkbox', 'time') and 2 or None}" 72 py:choose=""> 73 74 <py:when test="field.type == 'select'"> 75 <select name="${constraint_name}"> 76 <option></option> 77 <option py:for="option in field.options" 78 selected="${option == constraint_value and 'selected' or None}">$option 56 79 </option> 57 80 </select> 58 </td> 59 </py:when> 60 <py:otherwise><!--! not the first line of a multiline constraint --> 61 <th colspan="${field.type == 'time' and 1 or 2}"><label>or</label></th> 62 </py:otherwise> 63 </py:choose> 81 </py:when> 64 82 65 <td class="filter" colspan="${field.type in ('radio', 'checkbox', 'time') and 2 or None}" 66 py:choose=""> 83 <py:when test="field.type == 'radio'"> 84 <py:for each="option in field.options"> 85 <input type="checkbox" id="${constraint_name}_$option" name="${constraint_name}" 86 value="$option" 87 checked="${any([(value == option) == (constraint.mode == '') 88 for value in constraint['values']]) and 'checked' or None}" /> 89 <label for="${constraint_name}_$option" class="control">${option or 'none'}</label> 90 </py:for> 91 </py:when> 67 92 68 <py:when test="field.type == 'select'">69 <select name="${constraint_name}">70 <option></option>71 < option py:for="option in field.options"72 selected="${option == constraint_value and 'selected' or None}">$option73 </option>74 </select>75 </py:when>93 <py:when test="field.type == 'checkbox'"> 94 <input type="radio" id="${constraint_name}_on" name="$constraint_name" value="1" 95 checked="${constraint.mode != '!' or constraint_value == '1' or None}" /> 96 <label for="${field_name}_on" class="control">yes</label> 97 <input type="radio" id="${constraint_name}_off" name="$constraint_name" value="0" 98 checked="${constraint.mode == '!' or constraint_value != '1' or None}" /> 99 <label for="${field_name}_off" class="control">no</label> 100 </py:when> 76 101 77 <py:when test="field.type == 'radio'"> 78 <py:for each="option in field.options"> 79 <input type="checkbox" id="${field_name}_$option" name="${field_name}" 80 value="$option" 81 checked="${any([(value == option) == (constraint.mode == '') 82 for value in constraint['values']]) and 'checked' or None}" /> 83 <label for="${field_name}_$option" class="control">${option or 'none'}</label> 84 </py:for> 85 </py:when> 102 <py:when test="field.type in ('text', 'textarea')"> 103 <input type="text" name="${constraint_name}" value="$constraint_value" size="42" /> 104 </py:when> 105 106 <py:when test="'time'" py:with="(start, end) = ';' in constraint_value 107 and constraint_value.split(';', 1) 108 or (constraint_value, '')"> 109 <label>between</label> 110 <input type="text" name="${constraint_name}" value="$start" size="14" /> 111 <label>and</label> 112 <input type="text" name="${constraint_name}_end" value="$end" size="14" /> 113 </py:when> 86 114 87 <py:when test="field.type == 'checkbox'"> 88 <input type="radio" id="${field_name}_on" name="$field_name" value="1" 89 checked="${constraint.mode != '!' or constraint_value == '1' or None}" /> 90 <label for="${field_name}_on" class="control">yes</label> 91 <input type="radio" id="${field_name}_off" name="$field_name" value="0" 92 checked="${constraint.mode == '!' or constraint_value != '1' or None}" /> 93 <label for="${field_name}_off" class="control">no</label> 94 </py:when> 115 </td> 116 <td class="actions" 117 py:with="rm_idx = multiline and constraint_idx or len(constraint['values']) - 1"><input 118 type="submit" name="rm_filter_${constraint_name}${field.type != 'radio' and '_%d' % rm_idx or ''}" 119 value="-" /></td> 120 </tr> 121 </py:for> 122 </tbody> 123 </py:for> 124 </py:for> 95 125 96 <py:when test="field.type in ('text', 'textarea')">97 <input type="text" name="${field_name}" value="$constraint_value" size="42" />98 </py:when>99 100 <py:when test="'time'" py:with="(start, end) = ';' in constraint_value101 and constraint_value.split(';', 1)102 or (constraint_value, '')">103 <label>between</label>104 <input type="text" name="${field_name}" value="$start" size="14" />105 <label>and</label>106 <input type="text" name="${field_name}_end" value="$end" size="14" />107 </py:when>108 109 </td>110 <td class="actions"111 py:with="rm_idx = multiline and constraint_idx or len(constraint['values']) - 1"><input112 type="submit" name="rm_filter_${field_name}${field.type != 'radio' and '_%d' % rm_idx or ''}"113 value="-" /></td>114 </tr>115 </py:for>116 </tbody>117 </py:for>118 </py:for>119 120 126 <tbody> 121 127 <tr class="actions"> 122 128 <td class="actions" colspan="4" style="text-align: right"> 123 <label for="add_filter ">Add filter</label> 124 <select name="add_filter " id="add_filter">129 <label for="add_filter_${clause_num}">Add filter</label> 130 <select name="add_filter_${clause_num}" id="add_filter_${clause_num}"> 125 131 <option></option> 126 132 <option py:for="field_name in field_names" py:with="field = fields[field_name]" 127 133 value="$field_name" 128 134 disabled="${(field.type == 'radio' and 129 field_name in constraints and130 len(constraints[ field_name])) or None}">135 clause_pre + field_name in constraints and 136 len(constraints[clause_pre + field_name])) or None}"> 131 137 ${field.label} 132 138 </option> 133 139 </select> 134 <input type="submit" name="add " value="+" />140 <input type="submit" name="add_${clause_num}" value="+" /> 135 141 </td> 136 142 </tr> 137 143 </tbody> 138 </table> 144 </table></td></tr> 145 </tbody></table> 139 146 </fieldset> 140 147 141 148 <!--! Allow the user to decide what columns to include in the output of the query --> -
trac/htdocs/js/query.js
15 15 16 16 // Removes an existing row from the filters table 17 17 function removeRow(button, propertyName) { 18 var field = propertyName.split("_"); 19 if (isNaN(field[0])) { 20 var clauseNum = 0; 21 } else { 22 var clauseNum = field.shift(); 23 } 24 field = field.join("_"); 18 25 var tr = getAncestorByTagName(button, "tr"); 19 26 var label = document.getElementById("label_" + propertyName); 20 27 if (label && (getAncestorByTagName(label, "tr") == tr)) { … … 45 52 } 46 53 } 47 54 } 48 49 var tBody = tr.parentNode; 50 tBody.deleteRow(tr.sectionRowIndex); 51 if (!tBody.rows.length) { 52 tBody.parentNode.removeChild(tBody); 55 56 tr = $(tr); 57 var tBody = tr.parent(); 58 tr.remove(); 59 if (!tBody.children().length) { 60 var table = tBody.parents("table.clause"); 61 tBody.remove(); 62 if (table.children().length < 3 && $("table.clause").length > 1) { 63 //As long two clauses remain, remove empty clauses 64 //Also, don't remove the last clause. 65 tBody = table.parents("tbody").eq(0); 66 //Remove the "OR" text from the next clause if this is the first clause 67 if (!tBody.prev().length) { 68 tBody.next().children().eq(0).remove(); 69 } 70 if (tBody.next().length) { 71 tBody.remove(); 72 } 73 return; 74 } 53 75 } 54 76 55 77 if (propertyName) { 56 var select = document.forms["query"].elements["add_filter "];78 var select = document.forms["query"].elements["add_filter_" + clauseNum]; 57 79 for (var i = 0; i < select.options.length; i++) { 58 80 var option = select.options[i]; 59 if (option.value == propertyName) option.disabled = false;81 if (option.value == field) option.disabled = false; 60 82 } 61 83 } 62 84 } … … 93 115 initializeFilter(input); 94 116 } 95 117 } 96 97 // Make the drop-down menu for adding a filter a client-side trigger 98 var addButton = document.forms["query"].elements["add"]; 99 addButton.parentNode.removeChild(addButton); 100 var select = document.getElementById("add_filter"); 101 select.onchange = function() { 102 if (select.selectedIndex < 1) return; 103 104 if (select.options[select.selectedIndex].disabled) { 105 // Neither IE nor Safari supported disabled options at the time this was 106 // written, so alert the user 107 alert("A filter already exists for that property"); 108 return; 118 119 function addClause(select) { 120 // Determine if this is the bottom clause. If so, append a new empty 121 // clause to the end of the list. 122 select = $(select); 123 var clauses = $("table.clause"); 124 var clauseIndex = clauses.index(select.parents("table.clause")); 125 if (clauseIndex == clauses.length - 1) { 126 var clauseNum = select.attr("name").split("_").pop(); 127 var tBody = select.parents("tbody").eq(1); 128 var copy = tBody.clone(); 129 var newId = "add_filter_" + (clauseNum + 1); 130 $("label", copy).attr("for", newId); 131 $("select", copy).attr("id", newId).attr("name", newId); 132 tBody.after(copy); 133 addFilter(clauseIndex + 1); 109 134 } 110 111 // Convenience function for creating a <label> 112 function createLabel(text, htmlFor) { 113 var label = document.createElement("label"); 114 if (text) label.appendChild(document.createTextNode(text)); 115 if (htmlFor) { 116 label.htmlFor = htmlFor; 117 label.className = "control"; 135 } 136 137 function addFilter(clauseIndex) { 138 var select = $("select[name^=add_filter_]")[clauseIndex]; 139 select.onchange = function() { 140 if (select.selectedIndex < 1) return; 141 142 if (select.options[select.selectedIndex].disabled) { 143 // Neither IE nor Safari supported disabled options at the time this was 144 // written, so alert the user 145 alert("A filter already exists for that property"); 146 return; 118 147 } 119 return label; 120 } 121 122 // Convenience function for creating an <input type="text"> 123 function createText(name, size) { 124 var input = document.createElement("input"); 125 input.type = "text"; 126 if (name) input.name = name; 127 if (size) input.size = size; 128 return input; 129 } 130 131 // Convenience function for creating an <input type="checkbox"> 132 function createCheckbox(name, value, id) { 133 var input = document.createElement("input"); 134 input.type = "checkbox"; 135 if (name) input.name = name; 136 if (value) input.value = value; 137 if (id) input.id = id; 138 return input; 139 } 140 141 // Convenience function for creating an <input type="radio"> 142 function createRadio(name, value, id) { 143 var str = '<input type="radio"'; 144 if (name) str += ' name="' + name + '"'; 145 if (value) str += ' value="' + value + '"'; 146 if (id) str += ' id="' + id + '"'; 147 str += '/>'; 148 var span = document.createElement('span'); 149 // create radio button with innerHTML to avoid IE mangling it. 150 span.innerHTML = str; 151 return span; 152 } 153 154 // Convenience function for creating a <select> 155 function createSelect(name, options, optional) { 156 var e = document.createElement("select"); 157 if (name) e.name = name; 158 if (optional) e.options[0] = new Option(); 159 if (options) { 160 for (var i = 0; i < options.length; i++) { 161 var option; 162 if (typeof(options[i]) == "object") { 163 option = new Option(options[i].text, options[i].value); 164 } else { 165 option = new Option(options[i], options[i]); 166 } 167 e.options[e.options.length] = option; 148 149 150 // Add a new empty clause (only applies if this is the previous empty clause 151 addClause(select); 152 153 // Convenience function for creating a <label> 154 function createLabel(text, htmlFor) { 155 var label = document.createElement("label"); 156 if (text) label.appendChild(document.createTextNode(text)); 157 if (htmlFor) { 158 label.htmlFor = htmlFor; 159 label.className = "control"; 168 160 } 161 return label; 169 162 } 170 return e; 171 } 172 173 var propertyName = select.options[select.selectedIndex].value; 174 var property = properties[propertyName]; 175 var table = document.getElementById("filters").getElementsByTagName("table")[0]; 176 var tr = document.createElement("tr"); 177 tr.className = propertyName; 178 179 var alreadyPresent = false; 180 for (var i = 0; i < table.rows.length; i++) { 181 if (table.rows[i].className == propertyName) { 182 var existingTBody = table.rows[i].parentNode; 183 alreadyPresent = true; 184 break; 163 164 // Convenience function for creating an <input type="text"> 165 function createText(name, size) { 166 var input = document.createElement("input"); 167 input.type = "text"; 168 if (name) input.name = name; 169 if (size) input.size = size; 170 return input; 185 171 } 186 } 187 188 // Add the row header 189 var th = document.createElement("th"); 190 th.scope = "row"; 191 if (!alreadyPresent) { 192 var label = createLabel(property.label); 193 label.id = "label_" + propertyName; 194 th.appendChild(label); 195 } else { 196 th.colSpan = property.type == "time"? 1: 2; 197 th.appendChild(createLabel("or")); 198 } 199 tr.appendChild(th); 200 201 var td = document.createElement("td"); 202 var focusElement = null; 203 if (property.type == "radio" || property.type == "checkbox" || property.type == "time") { 204 td.colSpan = 2; 205 td.className = "filter"; 206 if (property.type == "radio") { 207 for (var i = 0; i < property.options.length; i++) { 208 var option = property.options[i]; 209 td.appendChild(createCheckbox(propertyName, option, 210 propertyName + "_" + option)); 211 td.appendChild(createLabel(option ? option : "none", 212 propertyName + "_" + option)); 172 173 // Convenience function for creating an <input type="checkbox"> 174 function createCheckbox(name, value, id) { 175 var input = document.createElement("input"); 176 input.type = "checkbox"; 177 if (name) input.name = name; 178 if (value) input.value = value; 179 if (id) input.id = id; 180 return input; 181 } 182 183 // Convenience function for creating an <input type="radio"> 184 function createRadio(name, value, id) { 185 var str = '<input type="radio"'; 186 if (name) str += ' name="' + name + '"'; 187 if (value) str += ' value="' + value + '"'; 188 if (id) str += ' id="' + id + '"'; 189 str += '/>'; 190 var span = document.createElement('span'); 191 // create radio button with innerHTML to avoid IE mangling it. 192 span.innerHTML = str; 193 return span; 194 } 195 196 // Convenience function for creating a <select> 197 function createSelect(name, options, optional) { 198 var e = document.createElement("select"); 199 if (name) e.name = name; 200 if (optional) e.options[0] = new Option(); 201 if (options) { 202 for (var i = 0; i < options.length; i++) { 203 var option; 204 if (typeof(options[i]) == "object") { 205 option = new Option(options[i].text, options[i].value); 206 } else { 207 option = new Option(options[i], options[i]); 208 } 209 e.options[e.options.length] = option; 210 } 213 211 } 214 } else if (property.type == "checkbox") { 215 td.appendChild(createRadio(propertyName, "1", propertyName + "_on")); 216 td.appendChild(document.createTextNode(" ")); 217 td.appendChild(createLabel("yes", propertyName + "_on")); 218 td.appendChild(createRadio(propertyName, "0", propertyName + "_off")); 219 td.appendChild(document.createTextNode(" ")); 220 td.appendChild(createLabel("no", propertyName + "_off")); 221 } else if (property.type == "time") { 222 td.appendChild(createLabel("between")); 223 td.appendChild(document.createTextNode(" ")); 224 focusElement = createText(propertyName, 14); 225 td.appendChild(focusElement); 226 td.appendChild(document.createTextNode(" ")); 227 td.appendChild(createLabel("and")); 228 td.appendChild(document.createTextNode(" ")); 229 td.appendChild(createText(propertyName + "_end", 14)); 212 return e; 230 213 } 231 tr.appendChild(td); 232 } else { 214 215 var propertyName = select.options[select.selectedIndex].value; 216 var property = properties[propertyName]; 217 var table = $(select).parents("table.clause")[0]; 218 var tr = document.createElement("tr"); 219 tr.className = propertyName; 220 221 var alreadyPresent = false; 222 for (var i = 0; i < table.rows.length; i++) { 223 if (table.rows[i].className == propertyName) { 224 var existingTBody = table.rows[i].parentNode; 225 alreadyPresent = true; 226 break; 227 } 228 } 229 230 var clauseNum = $(select).attr("name").split("_").pop(); 231 if (clauseNum) { 232 propertyName = clauseNum + "_" + propertyName; 233 } 234 235 // Add the row header 236 var th = document.createElement("th"); 237 th.scope = "row"; 233 238 if (!alreadyPresent) { 234 // Add the mode selector 235 td.className = "mode"; 236 var modeSelect = createSelect(propertyName + "_mode", 237 modes[property.type]); 238 td.appendChild(modeSelect); 239 var label = createLabel(property.label); 240 label.id = "label_" + propertyName; 241 th.appendChild(label); 242 } else { 243 th.colSpan = property.type == "time"? 1: 2; 244 th.appendChild(createLabel("or")); 245 } 246 tr.appendChild(th); 247 248 var td = document.createElement("td"); 249 var focusElement = null; 250 if (property.type == "radio" || property.type == "checkbox" || property.type == "time") { 251 td.colSpan = 2; 252 td.className = "filter"; 253 if (property.type == "radio") { 254 for (var i = 0; i < property.options.length; i++) { 255 var option = property.options[i]; 256 td.appendChild(createCheckbox(propertyName, option, 257 propertyName + "_" + option)); 258 td.appendChild(createLabel(option ? option : "none", 259 propertyName + "_" + option)); 260 } 261 } else if (property.type == "checkbox") { 262 td.appendChild(createRadio(propertyName, "1", propertyName + "_on")); 263 td.appendChild(document.createTextNode(" ")); 264 td.appendChild(createLabel("yes", propertyName + "_on")); 265 td.appendChild(createRadio(propertyName, "0", propertyName + "_off")); 266 td.appendChild(document.createTextNode(" ")); 267 td.appendChild(createLabel("no", propertyName + "_off")); 268 } else if (property.type == "time") { 269 td.appendChild(createLabel("between")); 270 td.appendChild(document.createTextNode(" ")); 271 focusElement = createText(propertyName, 14); 272 td.appendChild(focusElement); 273 td.appendChild(document.createTextNode(" ")); 274 td.appendChild(createLabel("and")); 275 td.appendChild(document.createTextNode(" ")); 276 td.appendChild(createText(propertyName + "_end", 14)); 277 } 239 278 tr.appendChild(td); 279 } else { 280 if (!alreadyPresent) { 281 // Add the mode selector 282 td.className = "mode"; 283 var modeSelect = createSelect(propertyName + "_mode", 284 modes[property.type]); 285 td.appendChild(modeSelect); 286 tr.appendChild(td); 287 } 288 289 // Add the selector or text input for the actual filter value 290 td = document.createElement("td"); 291 td.className = "filter"; 292 if (property.type == "select") { 293 focusElement = createSelect(propertyName, property.options, true); 294 } else if ((property.type == "text") || (property.type == "textarea")) { 295 focusElement = createText(propertyName, 42); 296 } 297 td.appendChild(focusElement); 298 tr.appendChild(td); 240 299 } 241 242 // Add the selector or text input for the actual filter value300 301 // Add the add and remove buttons 243 302 td = document.createElement("td"); 244 td.className = "filter"; 245 if (property.type == "select") { 246 focusElement = createSelect(propertyName, property.options, true); 247 } else if ((property.type == "text") || (property.type == "textarea")) { 248 focusElement = createText(propertyName, 42); 249 } 250 td.appendChild(focusElement); 303 td.className = "actions"; 304 var removeButton = document.createElement("input"); 305 removeButton.type = "button"; 306 removeButton.value = "-"; 307 removeButton.onclick = function() { removeRow(removeButton, propertyName) }; 308 td.appendChild(removeButton); 251 309 tr.appendChild(td); 252 } 253 254 // Add the add and remove buttons 255 td = document.createElement("td"); 256 td.className = "actions"; 257 var removeButton = document.createElement("input"); 258 removeButton.type = "button"; 259 removeButton.value = "-"; 260 removeButton.onclick = function() { removeRow(removeButton, propertyName) }; 261 td.appendChild(removeButton); 262 tr.appendChild(td); 263 264 if (alreadyPresent) { 265 existingTBody.appendChild(tr); 266 } else { 267 // Find the insertion point for the new row. We try to keep the filter rows 268 // in the same order as the options in the 'Add filter' drop-down, because 269 // that's the order they'll appear in when submitted. 270 var insertionPoint = getAncestorByTagName(select, "tbody"); 271 outer: for (var i = select.selectedIndex + 1; i < select.options.length; i++) { 272 for (var j = 0; j < table.tBodies.length; j++) { 273 if (table.tBodies[j].rows[0].className == select.options[i].value) { 274 insertionPoint = table.tBodies[j]; 275 break outer; 310 311 if (alreadyPresent) { 312 existingTBody.appendChild(tr); 313 } else { 314 // Find the insertion point for the new row. We try to keep the filter rows 315 // in the same order as the options in the 'Add filter' drop-down, because 316 // that's the order they'll appear in when submitted. 317 var insertionPoint = getAncestorByTagName(select, "tbody"); 318 outer: for (var i = select.selectedIndex + 1; i < select.options.length; i++) { 319 for (var j = 0; j < table.tBodies.length; j++) { 320 if (table.tBodies[j].rows[0].className == select.options[i].value) { 321 insertionPoint = table.tBodies[j]; 322 break outer; 323 } 276 324 } 277 325 } 326 // Finally add the new row to the table 327 var tbody = document.createElement("tbody"); 328 tbody.appendChild(tr); 329 insertionPoint.parentNode.insertBefore(tbody, insertionPoint); 278 330 } 279 // Finally add the new row to the table 280 var tbody = document.createElement("tbody"); 281 tbody.appendChild(tr); 282 insertionPoint.parentNode.insertBefore(tbody, insertionPoint); 331 if(focusElement) 332 focusElement.focus(); 333 334 // Disable the add filter in the drop-down list 335 if (property.type == "radio" || property.type == "checkbox") { 336 select.options[select.selectedIndex].disabled = true; 337 } 338 select.selectedIndex = 0; 283 339 } 284 if(focusElement)285 focusElement.focus();286 287 // Disable the add filter in the drop-down list288 if (property.type == "radio" || property.type == "checkbox") {289 select.options[select.selectedIndex].disabled = true;290 }291 select.selectedIndex = 0;292 340 } 341 342 // Make the drop-down menu for adding a filter a client-side trigger 343 $("#query input[name^=add_][type=submit]").each(function(idx) { 344 $(this).remove(); 345 addFilter(idx); 346 }); 293 347 } 294 348 295 })(jQuery); 296 No newline at end of file 349 })(jQuery);
