Source

trac-t10425 / new_format3.diff

Full commit
# HG changeset patch
# Parent 880c69d664ea90da118425d85ccc83f6e1ed210a

diff --git a/trac/ticket/query.py b/trac/ticket/query.py
--- a/trac/ticket/query.py
+++ b/trac/ticket/query.py
@@ -74,10 +74,6 @@ class Query(object):
             constraints = [constraints]
         self.constraints = constraints
         synonyms = TicketSystem(self.env).get_field_synonyms()
-        self.order = synonyms.get(order, order)     # 0.11 compatibility
-        self.desc = desc
-        self.group = group
-        self.groupdesc = groupdesc
         self.format = format
         self.default_page = 1
         self.items_per_page = QueryModule(self.env).items_per_page
@@ -126,23 +122,30 @@ class Query(object):
                      c == 'id']
         self.rows = [c for c in rows if c in field_names]
 
-        self.order = self.order or QueryModule(self.env).default_order
-        if self.order:
-            if self.order[0] == '-':
-                (self.order, self.desc) = (self.order[1:], 1)
-            elif self.order[0] == '+':
-                (self.order, self.desc) = (self.order[1:], 0)
-        if self.order != 'id' and self.order not in field_names:
-            (self.order, self.desc) = ('priority', 0)
-
-        self.group = self.group or QueryModule(self.env).default_group
-        if self.group:
-            if self.group[0] == '-':
-                (self.group, self.groupdesc) = (self.group[1:], 1)
-            elif self.group[0] == '+':
-                (self.group, self.groupdesc) = (self.group[1:], 0)
-        if self.group not in field_names:
-            (self.group, self.groupdesc) = (None, 0)
+        self.order = []
+        self.group = False
+        group = group or QueryModule(self.env).default_group
+        if group:
+            if groupdesc and group[0] not in ('-', '+'):
+                # 0.11 / 0.12 compatibility
+                group = '-' + group
+            if (group in field_names) \
+                    or (group[0] in ('-', '+') and group[1:] in field_names):
+                self.order.append(group)
+                self.group = True
+        if order and isinstance(order, basestring):
+            order = synonyms.get(order, order)
+            if desc and order and order[0] not in ('-', '+'):
+                # 0.11 / 0.12 compatibility
+                order = ['-' + order]
+            else:
+                order = [order]
+        if not order:
+            order = ['priority']
+        valid_order_fields = field_names.union(('id',))
+        self.order.extend([o for o in order if \
+                (o[0] in ('-', '+') and o[1:] in valid_order_fields) \
+                or (o in valid_order_fields)])
 
         constraint_cols = {}
         for clause in self.constraints:
@@ -277,16 +280,30 @@ class Query(object):
             # Only display the first seven columns by default
             cols = cols[:7]
 
-        # Make sure the column we order by is visible.
-        if self.order and self.order not in cols:
-            cols.append(self.order)
-
-        # Make sure to not show the column we group on.
-        if self.group and self.group in cols:
-            cols.remove(self.group)
+        # Make sure we don't show group column, but show all order by columns
+        for index, (order_col, _desc) in enumerate(self.get_order()):
+            if self.group and index == 0:
+                if order_col in cols:
+                    cols.remove(order_col)
+            elif not order_col in cols:
+                cols.append(order_col)
 
         return cols
 
+    def get_order(self):
+        # Returns a list of (field, desc_bool) items
+        order = []
+        for o in self.order:
+            if o[0] in ('-', '+'):
+                order.append((o[1:], True if o[0] == '-' else False))
+            else:
+                order.append((o, False))
+        return order
+
+    def get_group(self):
+        if self.group:
+            return self.get_order()[0][0] # Group is first order item
+    
     def count(self, req=None, db=None, cached_ids=None, authname=None,
               tzinfo=None, locale=None):
         """Get the number of matching tickets for the present query.
@@ -393,9 +410,10 @@ class Query(object):
 
         if id is None:
             id = self.id
-        if desc is None:
-            desc = self.desc
-        if order is None:
+        if order:
+            # If order, also use the passed desc
+            order = '-' + order if desc else order
+        else:
             order = self.order
         if max is None:
             max = self.max
@@ -422,9 +440,8 @@ class Query(object):
         
         return href.query(constraints,
                           report=id,
-                          order=order, desc=1 if desc else None,
-                          group=self.group,
-                          groupdesc=1 if self.groupdesc else None,
+                          order=order,
+                          group=self.order[0] if self.group else None,
                           col=cols,
                           row=self.rows,
                           max=max,
@@ -458,11 +475,10 @@ class Query(object):
             for col in args:
                 if not col in cols:
                     cols.append(col)
-        if self.group and not self.group in cols:
-            add_cols(self.group)
         if self.rows:
             add_cols('reporter', *self.rows)
-        add_cols('status', 'priority', 'time', 'changetime', self.order)
+        order_cols = [order for order, _desc in self.get_order()]
+        add_cols('status', 'priority', 'time', 'changetime', *order_cols)
         cols.extend([c for c in self.constraint_cols if not c in cols])
 
         custom_fields = [f['name'] for f in self.fields if f.get('custom')]
@@ -483,14 +499,13 @@ class Query(object):
 
         # Join with the enum table for proper sorting
         for col in [c for c in enum_columns
-                    if c == self.order or c == self.group or c == 'priority']:
+                        if c in order_cols or c == 'priority']:
             sql.append("\n  LEFT OUTER JOIN enum AS %s ON "
                        "(%s.type='%s' AND %s.name=%s)"
                        % (col, col, col, col, col))
 
         # Join with the version/milestone tables for proper sorting
-        for col in [c for c in ['milestone', 'version']
-                    if c == self.order or c == self.group]:
+        for col in [c for c in ['milestone', 'version'] if c in order_cols]:
             sql.append("\n  LEFT OUTER JOIN %s ON (%s.name=%s)"
                        % (col, col, col))
 
@@ -641,11 +656,7 @@ class Query(object):
                            (','.join([str(id) for id in cached_ids])))
             
         sql.append("\nORDER BY ")
-        order_cols = [(self.order, self.desc)]
-        if self.group and self.group != self.order:
-            order_cols.insert(0, (self.group, self.groupdesc))
-
-        for name, desc in order_cols:
+        for index, (name, desc) in enumerate(self.get_order()):
             if name in enum_columns:
                 col = name + '.value'
             elif name in custom_fields:
@@ -673,10 +684,10 @@ class Query(object):
                            % (desc, desc, col, desc))
             else:
                 sql.append("%s%s" % (col, desc))
-            if name == self.group and not name == self.order:
+            if not index+1 == len(order_cols):
                 sql.append(",")
-        if self.order != 'id':
-            sql.append(",t.id")  
+        if 'id' not in order_cols:
+            sql.append(",t.id")
 
         if errors:
             raise QueryValueError(errors)
@@ -760,6 +771,7 @@ class Query(object):
                     for (label, milestones) in groups]
             fields[name] = field
 
+        group = self.get_group()
         groups = {}
         groupsequence = []
         for ticket in tickets:
@@ -770,8 +782,8 @@ class Query(object):
                     ticket['added'] = True
                 elif ticket['changetime'] > orig_time:
                     ticket['changed'] = True
-            if self.group:
-                group_key = ticket[self.group]
+            if group:
+                group_key = ticket[group]
                 groups.setdefault(group_key, []).append(ticket)
                 if not groupsequence or group_key not in groupsequence:
                     groupsequence.append(group_key)
@@ -861,15 +873,19 @@ class QueryModule(Component):
     default_columns = ListOption('query', 'default_columns',
         default=None,
         doc="""List of columns to show in query unless defined by the query.
-            Default is to let Trac calculate this internally. (''since 0.13'')""")
+            Default is to let Trac calculate this internally.
+            (''since 0.13'')""")
     
-    default_order = Option('query', 'default_order',
+    default_order = ListOption('query', 'default_order',
         default='priority',
-        doc="""The default order to use for queries. (''since 0.13'')""")
+        doc="""The default order to use for queries. Use `-field` for
+            descending sort order. Multiple order fields supported.
+            (''since 0.13'')""")
     
     default_group = Option('query', 'default_group',
         default=None,
-        doc="""The default group to use for queries. (''since 0.13'')""")
+        doc="""The default group to use for queries. Use `-field` for
+            descending sort order. (''since 0.13'')""")
 
     # IContentConverter methods
 
@@ -932,10 +948,6 @@ class QueryModule(Component):
                 query = Query.from_string(self.env, qstring)
                 args = {'order': query.order, 'group': query.group,
                         'col': query.cols, 'max': query.max}
-                if query.desc:
-                    args['desc'] = '1'
-                if query.groupdesc:
-                    args['groupdesc'] = '1'
                 constraints = query.constraints
 
             # Substitute $USER, or ensure no field constraints that depend
@@ -949,6 +961,14 @@ class QueryModule(Component):
                             del clause[field]
                             break
 
+        # FIXME: Multiple order is not yet supported through UI
+        # 0.12 desc + groupdesc compat
+        for _field, _desc in (('order', 'desc'), ('group', 'groupdesc')):
+            field = args.get(_field)
+            desc = True if args.get(_desc) else False
+            if field and desc and not field[0] in ['-', '+']:
+                args[_field] = '-' + _field
+
         cols = args.get('col')
         if isinstance(cols, basestring):
             cols = [cols]
@@ -963,13 +983,11 @@ class QueryModule(Component):
         max = args.get('max')
         if max is None and format in ('csv', 'tab'):
             max = 0 # unlimited unless specified explicitly
-        query = Query(self.env, req.args.get('report'),
-                      constraints, cols, args.get('order'),
-                      'desc' in args, args.get('group'),
-                      'groupdesc' in args, 'verbose' in args,
-                      rows,
-                      args.get('page'), 
-                      max)
+        query = Query(self.env, report=req.args.get('report'),
+                      constraints=constraints, cols=cols,
+                      order=order, group=args.get('group'),
+                      verbose='verbose' in args, rows=rows,
+                      page=args.get('page'), max=max)
 
         if 'update' in req.args:
             # Reset session vars
@@ -1423,19 +1441,21 @@ class TicketQueryMacro(WikiMacroBase):
 
         def ticket_groups():
             groups = []
-            for v, g in groupby(tickets, lambda t: t[query.group]):
+            group = query.get_group()
+            for v, g in groupby(tickets, lambda t: t[group]):
                 q = Query.from_string(self.env, query_string)
                 # produce the hint for the group
-                q.group = q.groupdesc = None
-                order = q.order
-                q.order = None
+                # FIXME: Gaah. What does these lines do - and why?
+                q.group = q.groupdesc = None     # Huh?
+                order = q.order                  # Huh?
+                q.order = None                   # Huh?
                 title = _("%(groupvalue)s %(groupname)s tickets matching "
-                          "%(query)s", groupvalue=v, groupname=query.group,
+                          "%(query)s", groupvalue=v, groupname=group,
                           query=q.to_string())
                 # produce the href for the query corresponding to the group
                 for constraint in q.constraints:
-                    constraint[str(query.group)] = v
-                q.order = order
+                    constraint[str(group)] = v
+                q.order = order                  # Huh?
                 href = q.get_href(formatter.context)
                 groups.append((v, [t for t in g], href, title))
             return groups
@@ -1456,7 +1476,7 @@ class TicketQueryMacro(WikiMacroBase):
                     [(tag.p(tag_('%(groupvalue)s %(groupname)s tickets:',
                                  groupvalue=tag.a(v, href=href, class_='query',
                                                   title=title),
-                                 groupname=query.group)),
+                                 groupname=query.get_group())),
                       tag.dl([(tag.dt(ticket_anchor(t)),
                                tag.dd(t['summary'])) for t in g],
                              class_='wiki compact'))
diff --git a/trac/ticket/tests/query.py b/trac/ticket/tests/query.py
--- a/trac/ticket/tests/query.py
+++ b/trac/ticket/tests/query.py
@@ -741,6 +741,17 @@ ORDER BY COALESCE(priority.value,'')='',
         self.assertEqual([], args)
         tickets = query.execute(self.req)
 
+    def test_get_order(self):
+        query = Query(self.env, group='-milestone', order=['-priority', 'id'])
+        self.assertEqual(['-milestone', '-priority', 'id'], query.order)
+        self.assertEqual([('milestone', True), ('priority', True),
+                          ('id', False)], query.get_order())
+    
+    def test_get_group(self):
+        query = Query(self.env, group='-milestone', order=['-priority', 'id'])
+        self.assertEqual(True, query.group)
+        self.assertEqual('milestone', query.get_group())
+
     def test_csv_escape(self):
         query = Mock(get_columns=lambda: ['col1'],
                      execute=lambda r: [{'id': 1,