Edgewall Software

Ticket #4775: multiple_keywords_ticket_query.diff

File multiple_keywords_ticket_query.diff, 7.1 kB (added by wijister@…, 19 months ago)

Added support for multiple-keywords filtering - unfortunately breaks filtering JS

  • htdocs/js/query.js

     
    219219      td.className = "filter"; 
    220220      if (property.type == "select") { 
    221221        var element = createSelect(propertyName, property.options, true); 
    222       } else if (property.type == "text") { 
     222      } else if (property.type == "text" || property.type == "keywords") { 
    223223        var element = document.createElement("input"); 
    224224        element.type = "text"; 
    225225        element.name = propertyName; 
  • trac/ticket/api.py

     
    172172            fields.append(field) 
    173173 
    174174        # Advanced text fields 
    175         for name in ('keywords', 'cc', ): 
    176             field = {'name': name, 'type': 'text', 'label': name.title()} 
    177             fields.append(field) 
     175        fields.append({'name': 'keywords', 'type': 'keywords', 'label': 'Keywords'}) 
     176        fields.append({'name': 'cc', 'type': 'text', 'label': 'Cc'}) 
    178177 
    179178        for field in self.get_custom_fields(): 
    180179            if field['name'] in [f['name'] for f in fields]: 
  • trac/ticket/tests/query.py

     
    312312        self.assertEqual([], args) 
    313313        tickets = query.execute(Mock(href=self.env.href)) 
    314314 
     315    def test_constrained_allof_keywords(self): 
     316        query = Query.from_string(self.env, None, 
     317                                  'keywords@=foo bar', 
     318                                  order='id') 
     319        sql, args = query.get_sql() 
     320        self.assertEqual(sql, 
     321"""SELECT t.id AS id,t.summary AS summary,t.keywords AS keywords,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.time AS time,t.changetime AS changetime,priority.value AS priority_value 
     322FROM ticket AS t 
     323  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority) 
     324WHERE (COALESCE(t.keywords,'') LIKE %s ESCAPE '/' OR COALESCE(t.keywords,'') LIKE %s ESCAPE '/') 
     325ORDER BY COALESCE(t.id,0)=0,t.id""") 
     326        self.assertEqual(['%foo%', '%bar%'], args) 
     327        tickets = query.execute(Mock(href=self.env.href)) 
     328 
    315329    def test_csv_escape(self): 
    316330        query = Mock(get_columns=lambda: ['col1'], 
    317331                     execute=lambda r,c: [{'col1': 'value, needs escaped'}]) 
     
    336350                         '<em class="error">[Error: Query filter requires ' 
    337351                         'field and constraints separated by a "="]</em>') 
    338352 
    339  
    340353def suite(): 
    341354    suite = unittest.TestSuite() 
    342355    suite.addTest(unittest.makeSuite(QueryTestCase, 'test')) 
  • trac/ticket/query.py

     
    9191                raise QuerySyntaxError('Query filter requires field name') 
    9292            # from last char of `field`, get the mode of comparison 
    9393            mode, neg = '', '' 
    94             if field[-1] in ('~', '^', '$'): 
     94            if field[-1] in ('~', '^', '$', '@'): 
    9595                mode = field[-1] 
    9696                field = field[:-1] 
    9797            if field[-1] == '!': 
     
    138138                    if k != 'id' and k in cols]: 
    139139            constraint = self.constraints[col] 
    140140            if len(constraint) == 1 and constraint[0] \ 
    141                     and not constraint[0][0] in ('!', '~', '^', '$'): 
     141                    and not constraint[0][0] in ('!', '~', '^', '$', '@'): 
    142142                if col in cols: 
    143143                    cols.remove(col) 
    144144            if col == 'status' and not 'closed' in constraint \ 
     
    301301 
    302302            if mode == '': 
    303303                return ("COALESCE(%s,'')%s=%%s" % (name, neg and '!' or ''), 
    304                         value) 
     304                        [value]) 
    305305            if not value: 
    306306                return None 
    307307            db = self.env.get_db_cnx() 
    308308            value = db.like_escape(value) 
     309 
     310            # special case - search for keywords separated by space 
     311            if mode == '@': 
     312                words = value.split(' ') 
     313                con = "(" 
     314                count = 0 
     315                args = [] 
     316                 
     317                # iterate the words 
     318                for w in words: 
     319                    # word is empty, let's skip it 
     320                    if w == '': 
     321                        continue 
     322                    if count > 0: 
     323                        # need to append to the OR sequence 
     324                        con = con + " OR "; 
     325                    con = con + "COALESCE(%s,'') %s" % (name, db.like()) 
     326                    args.append('%' + w + '%') 
     327                    count = count + 1 
     328 
     329                if (count == 0): 
     330                    # there are no words to filter by 
     331                    return None 
     332                con = con + ")" 
     333                return (con, args) 
    309334            if mode == '~': 
    310335                value = '%' + value + '%' 
    311336            elif mode == '^': 
     
    314339                value = '%' + value 
    315340            return ("COALESCE(%s,'') %s%s" % (name, neg and 'NOT ' or '', 
    316341                                              db.like()), 
    317                     value) 
     342                    [value]) 
    318343 
    319344        clauses = [] 
    320345        args = [] 
     
    323348            # starts-with, negation, etc.) 
    324349            neg = v[0].startswith('!') 
    325350            mode = '' 
    326             if len(v[0]) > neg and v[0][neg] in ('~', '^', '$'): 
     351            if len(v[0]) > neg and v[0][neg] in ('~', '^', '$', '@'): 
    327352                mode = v[0][neg] 
    328353 
    329354            # Special case id ranges 
     
    368393                else: 
    369394                    clauses.append("(" + " OR ".join( 
    370395                        [item[0] for item in constraint_sql]) + ")") 
    371                 args += [item[1] for item in constraint_sql] 
     396                for item in constraint_sql: 
     397                    args.extend(item[1]) 
    372398            elif len(v) == 1: 
    373399                constraint_sql = get_constraint_sql(k, v[0], mode, neg) 
    374400                if constraint_sql: 
    375401                    clauses.append(constraint_sql[0]) 
    376                     args.append(constraint_sql[1]) 
     402                    args.extend(constraint_sql[1]) 
    377403 
    378404        clauses = filter(None, clauses) 
    379405        if clauses: 
     
    438464                if neg: 
    439465                    val = val[1:] 
    440466                mode = '' 
    441                 if val[:1] in ('~', '^', '$'): 
     467                if val[:1] in ('~', '^', '$', '@'): 
    442468                    mode, val = val[:1], val[1:] 
    443469                constraint['mode'] = (neg and '!' or '') + mode 
    444470                constraint['values'].append(val) 
     
    479505            {'name': "is", 'value': ""}, 
    480506            {'name': "is not", 'value': "!"} 
    481507        ] 
     508        modes['keywords'] = [{'name': "contains all of", 'value': "@"}] 
     509        modes['keywords'].extend(modes['text']) 
    482510 
    483511        groups = {} 
    484512        groupsequence = []