Commits

Azoff committed e807b2e

Added patch to support new format of parameter 'order'.
Added a few new test cases.
Cleanup of existing test cases regarding defalt_*.

  • Participants
  • Parent commits 3001635

Comments (0)

Files changed (6)

File default_columns.diff

  FROM ticket AS t
    LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
  ORDER BY COALESCE(priority.value,'')='',%(cast_priority)s,t.id""" % {
-@@ -498,6 +498,56 @@ ORDER BY COALESCE(t.id,0)=0,t.id""")
+@@ -498,6 +498,52 @@ ORDER BY COALESCE(t.id,0)=0,t.id""")
          self.assertEqual(['anonymous'], args)
          tickets = query.execute(self.req)
  
 +ORDER BY COALESCE(priority.value,'')='',CAST(priority.value AS integer),t.id""")
 +        self.assertEqual([], args)
 +        tickets = query.execute(self.req)
-+        self.env.config.remove('query', 'default_columns')
 +
 +    def test_default_cols_with_config_with_request(self):
 +        self.env.config.set('query', 'default_columns', 'id,time,changetime,summary,milestone,type')
 +ORDER BY COALESCE(priority.value,'')='',CAST(priority.value AS integer),t.id""")
 +        self.assertEqual([], args)
 +        tickets = query.execute(self.req)
-+        self.env.config.remove('query', 'default_columns')
 +
 +    def test_default_cols_no_config_no_request(self):
-+        self.env.config.remove('query', 'default_columns')
 +        query = Query(self.env)
 +        sql, args = query.get_sql()
 +        self.assertEqualSQL(sql,
 +        tickets = query.execute(self.req)
 +
 +    def test_default_cols_no_config_with_request(self):
-+        self.env.config.remove('query', 'default_columns')
 +        query = Query(self.env, cols=['id','time','changetime'])
 +        sql, args = query.get_sql()
 +        self.assertEqualSQL(sql,

File default_group.diff

 # HG changeset patch
-# Parent 6152bec84f88e32c81e223d1b6058172ab764cfd
+# Parent 0f218217af29877eb550aff35c8079d842ad31fb
 
 diff --git a/trac/ticket/query.py b/trac/ticket/query.py
 --- a/trac/ticket/query.py
 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
-@@ -497,6 +497,109 @@ WHERE ((COALESCE(t.owner,'')=%s))
- ORDER BY COALESCE(t.id,0)=0,t.id""")
+@@ -498,6 +498,105 @@ ORDER BY COALESCE(t.id,0)=0,t.id""")
          self.assertEqual(['anonymous'], args)
          tickets = query.execute(self.req)
-+        
+ 
 +    def test_default_group_no_config_with_request(self):
-+        self.env.config.remove('query', 'default_group')
 +        query = Query(self.env, group='summary')
 +        sql, args = query.get_sql()
 +        self.assertEqualSQL(sql,
 +ORDER BY COALESCE(t.summary,'')='',t.summary,COALESCE(priority.value,'')='',CAST(priority.value AS integer),t.id""")
 +        self.assertEqual([], args)
 +        tickets = query.execute(self.req)
-+    
++
 +    def test_default_group_no_config_no_request(self):
-+        self.env.config.remove('query', 'default_group')
 +        query = Query(self.env)
 +        sql, args = query.get_sql()
 +        self.assertEqualSQL(sql,
 +ORDER BY COALESCE(priority.value,'')='',CAST(priority.value AS integer),t.id""")
 +        self.assertEqual([], args)
 +        tickets = query.execute(self.req)
-+        
++
 +    def test_default_group_with_config_with_request(self):
 +        self.env.config.set('query', 'default_group', 'type')
 +        query = Query(self.env, order='component', group='milestone')
 +ORDER BY COALESCE(t.milestone,'')='',COALESCE(milestone.completed,0)=0,milestone.completed,COALESCE(milestone.due,0)=0,milestone.due,t.milestone,COALESCE(t.component,'')='',t.component,t.id""")
 +        self.assertEqual([], args)
 +        tickets = query.execute(self.req)
-+        self.env.config.remove('query', 'default_group')
-+        
-+    def test_default_group_with_config_no_request(self): 
++
++    def test_default_group_with_config_no_request(self):
 +        self.env.config.set('query', 'default_group', 'status')
 +        query = Query(self.env)
 +        sql, args = query.get_sql()
 +ORDER BY COALESCE(t.status,'')='',t.status,COALESCE(priority.value,'')='',CAST(priority.value AS integer),t.id""")
 +        self.assertEqual([], args)
 +        tickets = query.execute(self.req)
-+        
++
++    def test_default_group_with_config_no_request_desc(self):
 +        self.env.config.set('query', 'default_group', '-time')
 +        query = Query(self.env)
 +        sql, args = query.get_sql()
 +ORDER BY COALESCE(t.time,0)=0 DESC,t.time DESC,COALESCE(priority.value,'')='',CAST(priority.value AS integer),t.id""")
 +        self.assertEqual([], args)
 +        tickets = query.execute(self.req)
-+        self.env.config.remove('query', 'default_group')
 +
 +    def test_default_group_invalid_field_1(self):
 +        # invalid argument and no config
-+        query = Query(self.env, group='-foo')
++        query = Query(self.env, group='foo', groupdesc=1)
 +        self.assertEqual(None, query.group)
 +        self.assertEqual(0, query.groupdesc)
-+        
++
 +    def test_default_group_invalid_field_2(self):
 +        # valid argument and no config
 +        query = Query(self.env, group='status')
 +        self.assertEqual('status', query.group)
 +        self.assertEqual(0, query.groupdesc)
-+        
++
 +    def test_default_group_invalid_field_3(self):
 +        # no argument and invalid config
 +        self.env.config.set('query', 'default_group', '-foo')
 +        query = Query(self.env)
 +        self.assertEqual(None, query.group)
 +        self.assertEqual(0, query.groupdesc)
-+        
++
 +    def test_default_group_invalid_field_4(self):
 +        # invalid argument and invalid config
 +        self.env.config.set('query', 'default_group', '-foo')
 +        query = Query(self.env, group='bar')
 +        self.assertEqual(None, query.group)
 +        self.assertEqual(0, query.groupdesc)
-+        
++
 +    def test_default_group_invalid_field_5(self):
 +        # invalid argument but valid config
 +        self.env.config.set('query', 'default_group', '-milestone')
 +        query = Query(self.env, group='bar')
 +        self.assertEqual('milestone', query.group)
 +        self.assertEqual(1, query.groupdesc)
-+        
++
 +    def test_default_group_invalid_field_6(self):
 +        # valid argument and valid config
 +        self.env.config.set('query', 'default_group', '-milestone')
 +        self.assertEqual('type', query.group)
 +        self.assertEqual(0, query.groupdesc)
 +
- 
      def test_default_order_no_config_with_request(self):
-         self.env.config.remove('query', 'default_order')
+         query = Query(self.env, order='summary')
+         sql, args = query.get_sql()

File default_order.diff

 # HG changeset patch
-# Parent 17446fc1c44c36f48a2cb206e1b2cf613dcc19d2
+# Parent c635414a19fd3e87c5015e861b3b12f0f924621d
 
 diff --git a/trac/ticket/query.py b/trac/ticket/query.py
 --- a/trac/ticket/query.py
 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
-@@ -498,6 +498,107 @@ ORDER BY COALESCE(t.id,0)=0,t.id""")
+@@ -101,7 +101,7 @@ ORDER BY COALESCE(priority.value,'')='',
+         tickets = query.execute(self.req)
+ 
+     def test_all_ordered_by_priority_desc(self):
+-        query = Query(self.env, desc=1) # priority is default order
++        query = Query(self.env, order='priority', desc=1)
+         sql, args = query.get_sql()
+         self.assertEqualSQL(sql,
+ """SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+@@ -498,6 +498,104 @@ ORDER BY COALESCE(t.id,0)=0,t.id""")
          self.assertEqual(['anonymous'], args)
          tickets = query.execute(self.req)
  
 +    def test_default_order_no_config_with_request(self):
-+        self.env.config.remove('query', 'default_order')
 +        query = Query(self.env, order='summary')
 +        sql, args = query.get_sql()
 +        self.assertEqualSQL(sql,
 +ORDER BY COALESCE(t.summary,'')='',t.summary,t.id""")
 +        self.assertEqual([], args)
 +        tickets = query.execute(self.req)
-+    
++
 +    def test_default_order_no_config_no_request(self):
-+        self.env.config.remove('query', 'default_order')
 +        query = Query(self.env)
 +        sql, args = query.get_sql()
 +        self.assertEqualSQL(sql,
 +ORDER BY COALESCE(priority.value,'')='',CAST(priority.value AS integer),t.id""")
 +        self.assertEqual([], args)
 +        tickets = query.execute(self.req)
-+        
++
 +    def test_default_order_with_config_with_request(self):
 +        self.env.config.set('query', 'default_order', 'type')
 +        query = Query(self.env, order='summary')
 +ORDER BY COALESCE(t.summary,'')='',t.summary,t.id""")
 +        self.assertEqual([], args)
 +        tickets = query.execute(self.req)
-+        self.env.config.remove('query', 'default_orders')
-+        
-+    def test_default_order_with_config_no_request(self): 
++
++    def test_default_order_with_config_no_request(self):
 +        self.env.config.set('query', 'default_order', 'time')
 +        query = Query(self.env)
 +        sql, args = query.get_sql()
 +ORDER BY COALESCE(t.time,0)=0,t.time,t.id""")
 +        self.assertEqual([], args)
 +        tickets = query.execute(self.req)
-+        
++
++    def test_default_order_with_config_no_request_desc(self):
 +        self.env.config.set('query', 'default_order', '-time')
 +        query = Query(self.env)
 +        sql, args = query.get_sql()
 +ORDER BY COALESCE(t.time,0)=0 DESC,t.time DESC,t.id""")
 +        self.assertEqual([], args)
 +        tickets = query.execute(self.req)
-+        self.env.config.remove('query', 'default_order')
 +
 +    def test_default_order_invalid_field_1(self):
 +        # invalid argument and no config
 +        query = Query(self.env, order='foo', desc=1)
 +        self.assertEqual('priority', query.order)
 +        self.assertEqual(0, query.desc)
-+        
++
 +    def test_default_order_invalid_field_2(self):
 +        # valid argument and no config
 +        query = Query(self.env, order='status')
 +        self.assertEqual('status', query.order)
 +        self.assertEqual(0, query.desc)
-+        
++
 +    def test_default_order_invalid_field_3(self):
 +        # no argument and invalid config
 +        self.env.config.set('query', 'default_order', '-foo')
 +        query = Query(self.env)
 +        self.assertEqual('priority', query.order)
 +        self.assertEqual(0, query.desc)
-+        
++
 +    def test_default_order_invalid_field_4(self):
 +        # invalid argument and invalid config
 +        self.env.config.set('query', 'default_order', '-foo')
 +        query = Query(self.env, order='bar')
 +        self.assertEqual('priority', query.order)
 +        self.assertEqual(0, query.desc)
-+        
++
 +    def test_default_order_invalid_field_5(self):
 +        # invalid argument but valid config
 +        self.env.config.set('query', 'default_order', '-milestone')
 +        query = Query(self.env, order='bar')
 +        self.assertEqual('milestone', query.order)
 +        self.assertEqual(1, query.desc)
-+        
++
 +    def test_default_order_invalid_field_6(self):
 +        # valid argument and valid config
 +        self.env.config.set('query', 'default_order', '-milestone')

File new_group_format.diff

 # HG changeset patch
-# Parent f527de7c5cf44a74cac34499df6cb59173b8254b
+# Parent b293006ed3e01873d1c70574c655bb1014b2bda6
 
 diff --git a/trac/ticket/query.py b/trac/ticket/query.py
 --- a/trac/ticket/query.py
 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
-@@ -172,6 +172,17 @@ ORDER BY COALESCE(t.milestone,'')='' DES
-         self.assertEqual([], args)
+@@ -161,6 +161,18 @@ ORDER BY COALESCE(t.milestone,'')='',COA
          tickets = query.execute(self.req)
  
+     def test_all_grouped_by_milestone_desc(self):
 +        query = Query(self.env, order='id', group='-milestone')
 +        sql, args = query.get_sql()
 +        self.assertEqualSQL(sql,
 +        self.assertEqual([], args)
 +        tickets = query.execute(self.req)
 +
-     def test_grouped_by_priority(self):
-         query = Query(self.env, group='priority')
++    def test_all_grouped_by_milestone_desc_old_format(self):
+         query = Query(self.env, order='id', group='milestone', groupdesc=1)
          sql, args = query.get_sql()
-@@ -564,41 +575,35 @@ ORDER BY COALESCE(t.time,0)=0 DESC,t.tim
+         self.assertEqualSQL(sql,
+@@ -559,43 +571,42 @@ ORDER BY COALESCE(t.time,0)=0 DESC,t.tim
+ 
+     def test_default_group_invalid_field_1(self):
          # invalid argument and no config
-         query = Query(self.env, group='-foo')
++        query = Query(self.env, group='-foo')
++        self.assertEqual(None, query.group)
++
++    def test_default_group_invalid_field_1_old_format(self):
++        # invalid argument and no config
+         query = Query(self.env, group='foo', groupdesc=1)
          self.assertEqual(None, query.group)
 -        self.assertEqual(0, query.groupdesc)
-         
+ 
      def test_default_group_invalid_field_2(self):
          # valid argument and no config
          query = Query(self.env, group='status')
 -        self.assertEqual('status', query.group)
 -        self.assertEqual(0, query.groupdesc)
 +        self.assertEqual({'field': 'status', 'desc': False}, query.group)
-         
+ 
      def test_default_group_invalid_field_3(self):
          # no argument and invalid config
          self.env.config.set('query', 'default_group', '-foo')
          query = Query(self.env)
          self.assertEqual(None, query.group)
 -        self.assertEqual(0, query.groupdesc)
-         
+ 
      def test_default_group_invalid_field_4(self):
          # invalid argument and invalid config
          self.env.config.set('query', 'default_group', '-foo')
          query = Query(self.env, group='bar')
          self.assertEqual(None, query.group)
 -        self.assertEqual(0, query.groupdesc)
-         
+ 
      def test_default_group_invalid_field_5(self):
          # invalid argument but valid config
          self.env.config.set('query', 'default_group', '-milestone')
 -        self.assertEqual('milestone', query.group)
 -        self.assertEqual(1, query.groupdesc)
 +        self.assertEqual({'field': 'milestone', 'desc': True}, query.group)
-         
+ 
      def test_default_group_invalid_field_6(self):
          # valid argument and valid config
          self.env.config.set('query', 'default_group', '-milestone')
 -        self.assertEqual(0, query.groupdesc)
 +        self.assertEqual({'field': 'type', 'desc': False}, query.group)
  
- 
      def test_default_order_no_config_with_request(self):
+         query = Query(self.env, order='summary')
 diff --git a/trac/ticket/tests/wikisyntax.py b/trac/ticket/tests/wikisyntax.py
 --- a/trac/ticket/tests/wikisyntax.py
 +++ b/trac/ticket/tests/wikisyntax.py

File new_order_format.diff

+# HG changeset patch
+# Parent b587d2c48654d657e5ad73d8ba21773ecec3b470
+
+diff --git a/trac/ticket/query.py b/trac/ticket/query.py
+--- a/trac/ticket/query.py
++++ b/trac/ticket/query.py
+@@ -88,7 +88,7 @@ class Query(object):
+     clause_re = re.compile(r'(?P<clause>\d+)_(?P<field>.+)$')
+ 
+     def __init__(self, env, report=None, constraints=None, cols=None,
+-                 order=None, desc=0, group=None, groupdesc=None, verbose=0,
++                 order=None, desc=None, group=None, groupdesc=None, verbose=0,
+                  rows=None, page=None, max=None, format=None):
+         self.env = env
+         self.id = report # if not None, it's the corresponding saved query
+@@ -97,8 +97,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.format = format
+         self.default_page = 1
+         self.items_per_page = QueryModule(self.env).items_per_page
+@@ -146,15 +144,30 @@ class Query(object):
+         self.cols = [c for c in cols or [] if c in field_names or 
+                      c == 'id']
+         self.rows = [c for c in rows if c in field_names]
++        
++        self.order = field_from_string(order)
++        if self.order:
++            # 0.11 compatibility
++            field = self.order['field']
++            self.order['field'] = synonyms.get(field, field)
++        
++            # 0.12 compatibility
++            if desc is not None:
++                self.order['desc'] = True if desc else False
+ 
+-        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)
++        if not self.order or (self.order['field'] != 'id' and \
++                              self.order['field'] not in field_names):
++            self.order = field_from_string(QueryModule(self.env).default_order)
++            
++            # 0.11 compatibility
++            if self.order:
++                field = self.order['field']
++                self.order['field'] = synonyms.get(field, field)
++            
++            if not self.order or (self.order['field'] != 'id' and \
++                                  self.order['field'] not in field_names):
++                self.order = field_from_string('priority')
++
+ 
+         if group == '':
+             self.group = group
+@@ -307,8 +320,8 @@ class Query(object):
+             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)
++        if self.order and self.order['field'] not in cols:
++            cols.append(self.order['field'])
+ 
+         # Make sure to not show the column we group on.
+         if self.group and self.group.get('field') in cols:
+@@ -422,14 +435,14 @@ class Query(object):
+ 
+         if id is None:
+             id = self.id
+-        if desc is None:
+-            desc = self.desc
+-        if order is None:
+-            order = self.order
+         if max is None:
+             max = self.max
+         if page is None:
+             page = self.page
++        
++        order = field_from_string(order)
++        if order and desc is not None:              # 0.12 compatibility
++            order['desc'] = True if desc else False
+ 
+         cols = self.get_columns()
+         # don't specify the columns in the href if they correspond to
+@@ -456,7 +469,7 @@ class Query(object):
+         
+         return href.query(constraints,
+                           report=id,
+-                          order=order, desc=1 if desc else None,
++                          order=field_to_string(order or self.order),
+                           group=group,
+                           col=cols,
+                           row=self.rows,
+@@ -496,7 +509,7 @@ class Query(object):
+             add_cols(group_field)
+         if self.rows:
+             add_cols('reporter', *self.rows)
+-        add_cols('status', 'priority', 'time', 'changetime', self.order)
++        add_cols('status', 'priority', 'time', 'changetime', self.order['field'])
+         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')]
+@@ -517,14 +530,14 @@ class Query(object):
+ 
+         # Join with the enum table for proper sorting
+         for col in [c for c in enum_columns
+-                    if c in (self.order, group_field, 'priority')]:
++                    if c in (self.order['field'], group_field, '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 in (self.order, group_field)]:
++                    if c in (self.order['field'], group_field)]:
+             sql.append("\n  LEFT OUTER JOIN %s ON (%s.name=%s)"
+                        % (col, col, col))
+ 
+@@ -675,8 +688,8 @@ 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.get('field') != self.order:
++        order_cols = [(self.order['field'], self.order['desc'])]
++        if self.group and self.group.get('field') != self.order['field']:
+             order_cols.insert(0, (self.group.get('field'), self.group.get('desc')))
+ 
+         for name, desc in order_cols:
+@@ -707,9 +720,9 @@ class Query(object):
+                            % (desc, desc, col, desc))
+             else:
+                 sql.append("%s%s" % (col, desc))
+-            if name == group_field and not name == self.order:
++            if name == group_field and not name == self.order['field']:
+                 sql.append(",")
+-        if self.order != 'id':
++        if self.order['field'] != 'id':
+             sql.append(",t.id")  
+ 
+         if errors:
+@@ -768,11 +781,16 @@ class Query(object):
+         wikify = set(f['name'] for f in self.fields 
+                      if f['type'] == 'text' and f.get('format') == 'wiki')
+ 
++        def get_order_string(col):
++            order = field_from_string(col)
++            if col == self.order['field']:
++                order['desc'] = not self.order['desc']
++            return field_to_string(order)
+         headers = [{
+-            'name': col, 'label': labels.get(col, _('Ticket')),
++            'name': col,
++            'label': labels.get(col, _('Ticket')),
+             'wikify': col in wikify,
+-            'href': self.get_href(context.href, order=col,
+-                                  desc=(col == self.order and not self.desc))
++            'href': self.get_href(context.href, order=get_order_string(col))
+         } for col in cols]
+ 
+         fields = {'id': {'type': 'id', 'label': _("Ticket")}}
+@@ -964,12 +982,10 @@ class QueryModule(Component):
+                 constraints = self._get_constraints(arg_list=arg_list)
+             else:
+                 query = Query.from_string(self.env, qstring)
+-                args = {'order': query.order,
++                args = {'order': field_to_string(query.order),
+                         'group': field_to_string(query.group),
+                         'col': query.cols,
+                         'max': query.max}
+-                if query.desc:
+-                    args['desc'] = '1'
+                 constraints = query.constraints
+ 
+             # Substitute $USER, or ensure no field constraints that depend
+@@ -983,6 +999,11 @@ class QueryModule(Component):
+                             del clause[field]
+                             break
+ 
++        order = field_from_string(args.get('order'))
++        if order and 'desc' in args:
++            order['desc'] = True if args.get('desc') else False
++            args['order'] = field_to_string(order)
++
+         group = field_from_string(args.get('group'))
+         if group and 'groupdesc' in args:
+             group['desc'] = True if args.get('groupdesc') else False
+@@ -1004,7 +1025,7 @@ class QueryModule(Component):
+             max = 0 # unlimited unless specified explicitly
+         query = Query(self.env, report=req.args.get('report'),
+                       constraints=constraints, cols=cols,
+-                      order=args.get('order'), desc='desc' in args,
++                      order=args.get('order'),
+                       group=args.get('group'),
+                       verbose='verbose' in args, rows=rows,
+                       page=args.get('page'), max=max)
+diff --git a/trac/ticket/templates/query.html b/trac/ticket/templates/query.html
+--- a/trac/ticket/templates/query.html
++++ b/trac/ticket/templates/query.html
+@@ -211,7 +211,6 @@
+         <div class="buttons">
+           <input py:if="report_resource" type="hidden" name="report" value="$report_resource.id" />
+           <input type="hidden" name="order" value="$query.order" />
+-          <input py:if="query.desc" type="hidden" name="desc" value="1" />
+           <input type="submit" name="update" value="${_('Update')}" />
+         </div>
+         <hr />
+diff --git a/trac/ticket/templates/query_results.html b/trac/ticket/templates/query_results.html
+--- a/trac/ticket/templates/query_results.html
++++ b/trac/ticket/templates/query_results.html
+@@ -33,10 +33,10 @@
+   <py:def function="column_headers()">
+     <tr class="trac-columns">
+       <th py:for="header in headers"
+-          class="$header.name${(' desc' if query.desc else ' asc') if query.order == header.name else ''}">
++          class="$header.name${(' desc' if query.order['desc'] else ' asc') if query.order['field'] == header.name else ''}">
+         <?python asc = _('(ascending)'); desc = _('(descending)') ?>
+         <a title="${_('Sort by %(col)s %(direction)s', col=header.label,
+-                      direction=(desc if query.order == header.name and not query.desc else asc))}"
++                      direction=(desc if query.order['field'] == header.name and not query.order['desc'] else asc))}"
+            href="$header.href">${header.label}</a>
+       </th>
+     </tr>
+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
+@@ -56,6 +56,17 @@ ORDER BY COALESCE(t.id,0)=0,t.id""")
+         tickets = query.execute(self.req)
+ 
+     def test_all_ordered_by_id_desc(self):
++        query = Query(self.env, order='-id')
++        sql, args = query.get_sql()
++        self.assertEqualSQL(sql,
++"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
++FROM ticket AS t
++  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
++ORDER BY COALESCE(t.id,0)=0 DESC,t.id DESC""")
++        self.assertEqual([], args)
++        tickets = query.execute(self.req)
++
++    def test_all_ordered_by_id_desc_old_format(self):
+         query = Query(self.env, order='id', desc=1)
+         sql, args = query.get_sql()
+         self.assertEqualSQL(sql,
+@@ -101,6 +112,18 @@ ORDER BY COALESCE(priority.value,'')='',
+         tickets = query.execute(self.req)
+ 
+     def test_all_ordered_by_priority_desc(self):
++        query = Query(self.env, order='-priority')
++        sql, args = query.get_sql()
++        self.assertEqualSQL(sql,
++"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
++FROM ticket AS t
++  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
++ORDER BY COALESCE(priority.value,'')='' DESC,%(cast_priority)s DESC,t.id""" % {
++          'cast_priority': self.env.get_read_db().cast('priority.value', 'int')})
++        self.assertEqual([], args)
++        tickets = query.execute(self.req)
++
++    def test_all_ordered_by_priority_desc_old_format(self):
+         query = Query(self.env, order='priority', desc=1)
+         sql, args = query.get_sql()
+         self.assertEqualSQL(sql,
+@@ -125,6 +148,18 @@ ORDER BY COALESCE(t.version,'')='',COALE
+         tickets = query.execute(self.req)
+ 
+     def test_all_ordered_by_version_desc(self):
++        query = Query(self.env, order='-version')
++        sql, args = query.get_sql()
++        self.assertEqualSQL(sql,
++"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.version AS version,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
++FROM ticket AS t
++  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
++  LEFT OUTER JOIN version ON (version.name=version)
++ORDER BY COALESCE(t.version,'')='' DESC,COALESCE(version.time,0)=0 DESC,version.time DESC,t.version DESC,t.id""")
++        self.assertEqual([], args)
++        tickets = query.execute(self.req)
++
++    def test_all_ordered_by_version_desc_old_format(self):
+         query = Query(self.env, order='version', desc=1)
+         sql, args = query.get_sql()
+         self.assertEqualSQL(sql,
+@@ -668,43 +703,42 @@ ORDER BY COALESCE(t.time,0)=0 DESC,t.tim
+ 
+     def test_default_order_invalid_field_1(self):
+         # invalid argument and no config
++        query = Query(self.env, order='-foo')
++        self.assertEqual({'field': 'priority', 'desc': False}, query.order)
++
++    def test_default_order_invalid_field_1_old_format(self):
++        # invalid argument and no config
+         query = Query(self.env, order='foo', desc=1)
+-        self.assertEqual('priority', query.order)
+-        self.assertEqual(0, query.desc)
++        self.assertEqual({'field': 'priority', 'desc': False}, query.order)
+ 
+     def test_default_order_invalid_field_2(self):
+         # valid argument and no config
+         query = Query(self.env, order='status')
+-        self.assertEqual('status', query.order)
+-        self.assertEqual(0, query.desc)
++        self.assertEqual({'field': 'status', 'desc': False}, query.order)
+ 
+     def test_default_order_invalid_field_3(self):
+         # no argument and invalid config
+         self.env.config.set('query', 'default_order', '-foo')
+         query = Query(self.env)
+-        self.assertEqual('priority', query.order)
+-        self.assertEqual(0, query.desc)
++        self.assertEqual({'field': 'priority', 'desc': False}, query.order)
+ 
+     def test_default_order_invalid_field_4(self):
+         # invalid argument and invalid config
+         self.env.config.set('query', 'default_order', '-foo')
+         query = Query(self.env, order='bar')
+-        self.assertEqual('priority', query.order)
+-        self.assertEqual(0, query.desc)
++        self.assertEqual({'field': 'priority', 'desc': False}, query.order)
+ 
+     def test_default_order_invalid_field_5(self):
+         # invalid argument but valid config
+         self.env.config.set('query', 'default_order', '-milestone')
+         query = Query(self.env, order='bar')
+-        self.assertEqual('milestone', query.order)
+-        self.assertEqual(1, query.desc)
++        self.assertEqual({'field': 'milestone', 'desc': True}, query.order)
+ 
+     def test_default_order_invalid_field_6(self):
+         # valid argument and valid config
+         self.env.config.set('query', 'default_order', '-milestone')
+         query = Query(self.env, order='type')
+-        self.assertEqual('type', query.order)
+-        self.assertEqual(0, query.desc)
++        self.assertEqual({'field': 'type', 'desc': False}, query.order)
+ 
+     def test_default_cols_with_config_no_request(self):
+         self.env.config.set('query', 'default_columns', 'id,time,changetime,summary,milestone,type')
+diff --git a/trac/ticket/tests/wikisyntax.py b/trac/ticket/tests/wikisyntax.py
+--- a/trac/ticket/tests/wikisyntax.py
++++ b/trac/ticket/tests/wikisyntax.py
+@@ -235,7 +235,7 @@ query:summary=résumé
+ <a class="query" href="/query?group=owner&amp;order=priority">query:group=owner</a>
+ </p>
+ <p>
+-<a class="query" href="/query?row=description&amp;order=priority">query:verbose=1</a>
++<a class="query" href="/query?order=priority&amp;row=description">query:verbose=1</a>
+ </p>
+ <p>
+ <a class="query" href="/query?summary=r%C3%A9sum%C3%A9&amp;order=priority">query:summary=résumé</a>
 default_order.diff
 default_group.diff
 new_group_format.diff
+new_order_format.diff