Edgewall Software

Ticket #2647: advanced-queries-ticket-2647-r8019.patch

File advanced-queries-ticket-2647-r8019.patch, 47.5 KB (added by ebray, 3 years ago)

Updated with JavaScript that actually works.

  • trac/ticket/query.py

     
    4949 
    5050class Query(object): 
    5151    substitutions = ['$USER'] 
     52    clause_re = re.compile(r'^(?:(?P<clause>\d+)_)?(?P<field>.+)') 
    5253 
    5354    def __init__(self, env, report=None, constraints=None, cols=None, 
    5455                 order=None, desc=0, group=None, groupdesc=0, verbose=0, 
     
    113114        if self.group not in field_names: 
    114115            self.group = None 
    115116 
     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 
    116125    @classmethod 
    117126    def from_string(cls, env, string, **kw): 
    118127        filters = string.split('&') 
     
    185194                cols.append(col) 
    186195 
    187196        def sort_columns(col1, col2): 
    188             constrained_fields = self.constraints.keys() 
     197            constrained_fields = self.constraint_cols.keys() 
    189198            if 'id' in (col1, col2): 
    190199                # Ticket ID is always the first column 
    191200                return col1 == 'id' and -1 or 1 
     
    203212        cols = self.get_all_columns() 
    204213         
    205214        # 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.constraints.keys() 
     215        # value by a query constraint.             
     216        for col in [k for k in self.constraint_cols.keys() 
    208217                    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: 
    213225                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') 
    217232        if self.group in cols: 
    218233            cols.remove(self.group) 
    219234 
     
    390405        if self.rows: 
    391406            add_cols('reporter', *self.rows) 
    392407        add_cols('status', 'priority', 'time', 'changetime', self.order) 
    393         cols.extend([c for c in self.constraints.keys() if not c in cols]) 
     408        cols.extend([c for c in self.constraint_cols if not c in cols]) 
    394409 
    395410        custom_fields = [f['name'] for f in self.fields if 'custom' in f] 
    396411 
     
    420435            sql.append("\n  LEFT OUTER JOIN %s ON (%s.name=%s)" 
    421436                       % (col, col, col)) 
    422437 
     438        db = self.env.get_db_cnx() 
     439 
    423440        def get_constraint_sql(name, value, mode, neg): 
    424441            if name not in custom_fields: 
    425442                col = 't.' + name 
     
    468485                                              db.like()), 
    469486                    (value, )) 
    470487 
    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 
    473495        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] 
    483507 
    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 
    495532                    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) 
    533558 
    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: 
    536563            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]) 
    538568            if cached_ids: 
    539569                sql.append(" OR ") 
    540570                sql.append("id in (%s)" % (','.join( 
     
    544574        order_cols = [(self.order, self.desc)] 
    545575        if self.group and self.group != self.order: 
    546576            order_cols.insert(0, (self.group, self.groupdesc)) 
     577 
    547578        for name, desc in order_cols: 
    548579            if name in custom_fields or name in enum_columns: 
    549580                col = name + '.value' 
     
    581612    def template_data(self, context, tickets, orig_list=None, orig_time=None, 
    582613                      req=None): 
    583614        constraints = {} 
     615        clauses = {} 
    584616        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) 
    585620            constraint = {'values': [], 'mode': ''} 
    586621            for val in v: 
    587622                neg = val.startswith('!') 
     
    594629                constraint['mode'] = (neg and '!' or '') + mode 
    595630                constraint['values'].append(val) 
    596631            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] 
    597646 
    598647        cols = self.get_columns() 
    599648        labels = dict([(f['name'], f['label']) for f in self.fields]) 
     
    700749                'col': cols, 
    701750                'row': self.rows, 
    702751                'constraints': constraints, 
     752                'clauses': clauses, 
    703753                'labels': labels, 
    704754                'headers': headers, 
    705755                'fields': fields, 
     
    872922            else: 
    873923                remove_constraints[to_remove[0]] = -1 
    874924 
    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 
    877934            if not isinstance(vals, (list, tuple)): 
    878935                vals = [vals] 
    879936            if vals: 
    880                 mode = req.args.get(field + '_mode') 
     937                mode = req.args.get(pre + field + '_mode') 
    881938                if mode: 
    882939                    vals = [mode + x for x in vals] 
    883940                if field in time_fields: 
    884                     ends = req.args.getlist(field + '_end') 
     941                    ends = req.args.getlist(pre + field + '_end') 
    885942                    if ends: 
    886943                        vals = [start + ';' + end  
    887944                                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] 
    890947                    if idx >= 0: 
    891948                        del vals[idx] 
    892949                        if not vals: 
    893950                            continue 
    894951                    else: 
    895952                        continue 
    896                 constraints.setdefault(synonyms.get(field, field),  
    897                                       []).extend(vals) 
     953                field = synonyms.get(field, field) 
     954                constraints.setdefault(pre + field, []).extend(vals) 
    898955 
    899956        return constraints 
    900957 
     
    929986        # For clients without JavaScript, we add a new constraint here if 
    930987        # requested 
    931988        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 
    9371009                # FIXME: '' not always correct (e.g. checkboxes) 
    9381010 
    9391011        req.session['query_href'] = query.get_href(context.href) 
  • trac/ticket/tests/functional.py

     
    187187        self._tester.go_to_query() 
    188188        # We don't have the luxury of javascript, so this is a multi-step 
    189189        # 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') 
    192192        tc.formvalue('query', 'owner', 'nothing') 
    193193        tc.submit('rm_filter_owner_0') 
    194194        tc.formvalue('query', 'summary', 'TestTicketQueryLinks') 
  • trac/htdocs/css/report.css

     
    3434#query hr { clear: both; margin: 0; visibility: hidden } 
    3535 
    3636#filters table { width: 100% } 
     37#filters table.clause { border: 1px solid #d7d7d7; } 
    3738#filters tr { height: 2em } 
    3839#filters th, #filters td { padding: 0 .2em; vertical-align: middle } 
    3940#filters th { font-size: 11px; text-align: right; white-space: nowrap; } 
  • trac/ticket/templates/query.html

     
    3737          <input py:if="'id' in query.constraints" type="hidden" name="id" value="${query.constraints['id']}" /> 
    3838          <legend class="foldable">Filters</legend> 
    3939          <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>&mdash; OR &mdash;</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 
    5679                              </option> 
    5780                            </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> 
    6482 
    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> 
    6792 
    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}">$option 
    73                             </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> 
    76101 
    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> 
    86114 
    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> 
    95125 
    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_value  
    101                                                         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"><input 
    112                           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  
    120126            <tbody> 
    121127              <tr class="actions"> 
    122128                <td class="actions" colspan="4" style="text-align: right"> 
    123                   <label for="add_filter">Add filter</label>&nbsp; 
    124                   <select name="add_filter" id="add_filter"> 
     129                  <label for="add_filter_${clause_num}">Add filter</label>&nbsp; 
     130                  <select name="add_filter_${clause_num}" id="add_filter_${clause_num}"> 
    125131                    <option></option> 
    126132                    <option py:for="field_name in field_names" py:with="field = fields[field_name]" 
    127133                            value="$field_name" 
    128134                            disabled="${(field.type == 'radio' and 
    129                                          field_name in constraints and 
    130                                          len(constraints[field_name])) or None}"> 
     135                                         clause_pre + field_name in constraints and 
     136                                         len(constraints[clause_pre + field_name])) or None}"> 
    131137                      ${field.label} 
    132138                    </option> 
    133139                  </select> 
    134                   <input type="submit" name="add" value="+" /> 
     140                  <input type="submit" name="add_${clause_num}" value="+" /> 
    135141                </td> 
    136142              </tr> 
    137143            </tbody> 
    138           </table> 
     144          </table></td></tr> 
     145        </tbody></table> 
    139146        </fieldset> 
    140147 
    141148        <!--! Allow the user to decide what columns to include in the output of the query --> 
  • trac/htdocs/js/query.js

     
    1515   
    1616    // Removes an existing row from the filters table 
    1717    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("_"); 
    1825      var tr = getAncestorByTagName(button, "tr"); 
    1926      var label = document.getElementById("label_" + propertyName); 
    2027      if (label && (getAncestorByTagName(label, "tr") == tr)) { 
     
    4552          } 
    4653        } 
    4754      } 
    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          } 
    5375      } 
    5476       
    5577      if (propertyName) { 
    56         var select = document.forms["query"].elements["add_filter"]; 
     78        var select = document.forms["query"].elements["add_filter_" + clauseNum]; 
    5779        for (var i = 0; i < select.options.length; i++) { 
    5880          var option = select.options[i]; 
    59           if (option.value == propertyName) option.disabled = false; 
     81          if (option.value == field) option.disabled = false; 
    6082        } 
    6183      } 
    6284    } 
     
    93115        initializeFilter(input); 
    94116      } 
    95117    } 
    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); 
    109134      } 
    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; 
    118147        } 
    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"; 
    168160          } 
     161          return label; 
    169162        } 
    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; 
    185171        } 
    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            } 
    213211          } 
    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; 
    230213        } 
    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"; 
    233238        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          } 
    239278          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); 
    240299        } 
    241    
    242         // Add the selector or text input for the actual filter value 
     300     
     301        // Add the add and remove buttons 
    243302        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); 
    251309        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              } 
    276324            } 
    277325          } 
     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); 
    278330        } 
    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; 
    283339      } 
    284       if(focusElement) 
    285           focusElement.focus(); 
    286    
    287       // Disable the add filter in the drop-down list 
    288       if (property.type == "radio" || property.type == "checkbox") { 
    289         select.options[select.selectedIndex].disabled = true; 
    290       } 
    291       select.selectedIndex = 0; 
    292340    } 
     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    }); 
    293347  } 
    294348 
    295 })(jQuery); 
    296  No newline at end of file 
     349})(jQuery);