Index: trac/ticket/tests/wikisyntax.py
===================================================================
--- trac/ticket/tests/wikisyntax.py	(revision 10865)
+++ trac/ticket/tests/wikisyntax.py	(working copy)
@@ -224,22 +224,22 @@
 <a class="query" href="/query?type=r%C3%A9sum%C3%A9">query:?type=résumé</a>
 </p>
 <p>
-<a class="query" href="/query?status=new&amp;status=reopened&amp;order=priority">query:status=new|reopened</a>
+<a class="query" href="/query?status=new&amp;status=reopened&amp;group=&amp;order=priority">query:status=new|reopened</a>
 </p>
 <p>
-<a class="query" href="/query?reporter=!&amp;order=priority">query:reporter!=</a>
+<a class="query" href="/query?reporter=!&amp;group=&amp;order=priority">query:reporter!=</a>
 </p>
 <p>
-<a class="query" href="/query?owner=me&amp;reporter=joe&amp;reporter=jack&amp;order=priority">query:reporter=joe|jack&amp;owner=me</a>
+<a class="query" href="/query?owner=me&amp;reporter=joe&amp;reporter=jack&amp;group=&amp;order=priority">query:reporter=joe|jack&amp;owner=me</a>
 </p>
 <p>
 <a class="query" href="/query?group=owner&amp;order=priority">query:group=owner</a>
 </p>
 <p>
-<a class="query" href="/query?order=priority&amp;row=description">query:verbose=1</a>
+<a class="query" href="/query?group=&amp;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>
+<a class="query" href="/query?summary=r%C3%A9sum%C3%A9&amp;group=&amp;order=priority">query:summary=résumé</a>
 </p>
 ------------------------------
 ============================== TicketQuery macro: no results, list form
Index: trac/ticket/tests/query.py
===================================================================
--- trac/ticket/tests/query.py	(revision 10865)
+++ trac/ticket/tests/query.py	(working copy)
@@ -1,6 +1,6 @@
 from trac.mimeview import Context
 from trac.test import Mock, EnvironmentStub, MockPerm
-from trac.ticket.query import Query, QueryModule, TicketQueryMacro
+from trac.ticket.query import Query, QueryModule, TicketQueryMacro, OrderRule
 from trac.util.datefmt import utc
 from trac.web.href import Href
 from trac.wiki.formatter import LinkFormatter
@@ -39,7 +39,7 @@
         self.env.reset_db()
 
     def test_all_ordered_by_id(self):
-        query = Query(self.env, order='id')
+        query = Query(self.env, order=OrderRule(self.env, '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
@@ -50,7 +50,7 @@
         tickets = query.execute(self.req)
 
     def test_all_ordered_by_id_desc(self):
-        query = Query(self.env, order='id', desc=1)
+        query = Query(self.env, order=OrderRule(self.env, 'id', True))
         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
@@ -61,7 +61,7 @@
         tickets = query.execute(self.req)
 
     def test_all_ordered_by_id_verbose(self):
-        query = Query(self.env, order='id', verbose=1)
+        query = Query(self.env, order=OrderRule(self.env, 'id'), verbose=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.reporter AS reporter,t.description AS description,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
@@ -95,7 +95,7 @@
         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=OrderRule(self.env, None, True)) # priority is default order
         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
@@ -107,7 +107,7 @@
         tickets = query.execute(self.req)
 
     def test_all_ordered_by_version(self):
-        query = Query(self.env, order='version')
+        query = Query(self.env, order=OrderRule(self.env, '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.version AS version,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
@@ -119,7 +119,7 @@
         tickets = query.execute(self.req)
 
     def test_all_ordered_by_version_desc(self):
-        query = Query(self.env, order='version', desc=1)
+        query = Query(self.env, order=OrderRule(self.env, 'version', True))
         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.version AS version,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
@@ -143,7 +143,7 @@
         tickets = query.execute(self.req)
 
     def test_all_grouped_by_milestone(self):
-        query = Query(self.env, order='id', group='milestone')
+        query = Query(self.env, order=OrderRule(self.env, 'id'), group='milestone')
         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.component AS component,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
@@ -155,7 +155,7 @@
         tickets = query.execute(self.req)
 
     def test_all_grouped_by_milestone_desc(self):
-        query = Query(self.env, order='id', group='milestone', groupdesc=1)
+        query = Query(self.env, order=OrderRule(self.env, 'id'), group='milestone', groupdesc=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.component AS component,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
@@ -268,7 +268,7 @@
 
     def test_grouped_by_custom_field(self):
         self.env.config.set('ticket-custom', 'foo', 'text')
-        query = Query(self.env, group='foo', order='id')
+        query = Query(self.env, group='foo', order=OrderRule(self.env, 'id'))
         sql, args = query.get_sql()
         foo = self.db.quote('foo')
         self.assertEqualSQL(sql,
@@ -502,6 +502,161 @@
                                 query)
         self.assertEqual('col1\r\n"value, needs escaped"\r\n',
                          content)
+    
+    def test_default_group_no_config_with_request(self):
+        self.env.config.remove('query', 'default_group')
+        query = Query(self.env, order=OrderRule(self.env, 'summary'))
+        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.summary,'')='',t.summary,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,
+"""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,'')='',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=OrderRule(self.env, 'summary'))
+        sql, args = query.get_sql()
+        self.assertEqualSQL(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.component AS component,t.type AS type,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.type,'')='',t.type,COALESCE(t.summary,'')='',t.summary,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): 
+        self.env.config.set('query', 'default_group', 'time')
+        query = Query(self.env)
+        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.time,0)=0,t.time,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_order_no_config_with_request(self):
+        self.env.config.remove('query', 'default_order')
+        query = Query(self.env, order=OrderRule(self.env, 'summary'))
+        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.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,
+"""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,'')='',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=OrderRule(self.env, 'summary'))
+        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.summary,'')='',t.summary,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(self.req)
+        self.env.config.remove('query', 'default_order')
+        
+    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()
+        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.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.time,0)=0,t.time,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(self.req)
+        self.env.config.remove('query', 'default_order')
+        
+        
+    def test_default_cols_with_config_no_request(self): 
+        self.env.config.set('query', 'default_cols', 'id,time,changetime,summary,milestone,type')
+        query = Query(self.env)
+        sql, args = query.get_sql()
+        self.assertEqualSQL(sql,
+"""SELECT t.id AS id,t.time AS time,t.changetime AS changetime,t.summary AS summary,t.milestone AS milestone,t.type AS type,t.priority AS priority,t.status AS status,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,'')='',CAST(priority.value AS integer),t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(self.req)
+        self.env.config.remove('query', 'default_cols')
+        
+        
+    def test_default_cols_with_config_with_request(self): 
+        self.env.config.set('query', 'default_cols', 'id,time,changetime,summary,milestone,type')
+        query = Query(self.env, cols=['id','time','changetime'])
+        sql, args = query.get_sql()
+        self.assertEqualSQL(sql,
+"""SELECT t.id AS id,t.time AS time,t.changetime AS changetime,t.status AS status,t.priority AS priority,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,'')='',CAST(priority.value AS integer),t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(self.req)
+        self.env.config.remove('query', 'default_cols')
+        
+        
+    def test_default_cols_no_config_no_request(self): 
+        self.env.config.remove('query', 'default_cols')
+        query = Query(self.env)
+        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,'')='',CAST(priority.value AS integer),t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(self.req)
+        
+        
+    def test_default_cols_no_config_with_request(self): 
+        self.env.config.remove('query', 'default_cols')
+        query = Query(self.env, cols=['id','time','changetime'])
+        sql, args = query.get_sql()
+        self.assertEqualSQL(sql,
+"""SELECT t.id AS id,t.time AS time,t.changetime AS changetime,t.status AS status,t.priority AS priority,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,'')='',CAST(priority.value AS integer),t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(self.req)
+                
 
     def test_template_data(self):
         req = Mock(href=self.env.href, perm=MockPerm(), authname='anonymous',
@@ -542,8 +697,14 @@
 
 class TicketQueryMacroTestCase(unittest.TestCase):
 
+    def setUp(self):
+        self.env = EnvironmentStub(default_data=True)
+        
+    def tearDown(self):
+        self.env.reset_db()
+
     def assertQueryIs(self, content, query, kwargs, format):
-        qs, kw, f = TicketQueryMacro.parse_args(content)
+        qs, kw, f = TicketQueryMacro.parse_args(self.env, content)
         self.assertEqual(query, qs)
         self.assertEqual(kwargs, kw)
         self.assertEqual(format, f)
@@ -551,31 +712,37 @@
     def test_owner_and_milestone(self):
         self.assertQueryIs('owner=joe, milestone=milestone1',
                            'owner=joe&milestone=milestone1',
-                           dict(col='status|summary', max='0', order='id'),
+                           dict(col='status|summary', max='0', order=OrderRule(self.env, 'id')),
                            'list')
 
     def test_owner_or_milestone(self):
         self.assertQueryIs('owner=joe, or, milestone=milestone1',
                            'owner=joe&or&milestone=milestone1',
-                           dict(col='status|summary', max='0', order='id'),
+                           dict(col='status|summary', max='0', order=OrderRule(self.env, 'id')),
                            'list')
     
     def test_format_arguments(self):
         self.assertQueryIs('owner=joe, milestone=milestone1, col=component|severity, max=15, order=component, format=compact',
                            'owner=joe&milestone=milestone1',
-                           dict(col='status|summary|component|severity', max='15', order='component'),
+                           dict(col='status|summary|component|severity', max='15', order=OrderRule(self.env, 'component')),
                            'compact')
         self.assertQueryIs('owner=joe, milestone=milestone1, col=id|summary|component, max=30, order=component, format=table',
                            'owner=joe&milestone=milestone1',
-                           dict(col='id|summary|component', max='30', order='component'),
+                           dict(col='id|summary|component', max='30', order=OrderRule(self.env, 'component')),
                            'table')
 
     def test_special_char_escaping(self):
         self.assertQueryIs(r'owner=joe|jack, milestone=this\&that\|here\,now',
                            r'owner=joe|jack&milestone=this\&that\|here,now',
-                           dict(col='status|summary', max='0', order='id'),
+                           dict(col='status|summary', max='0', order=OrderRule(self.env, 'id')),
                            'list')
         
+    def test_order_by_component_desc(self):
+        self.assertQueryIs('owner=joe, milestone=milestone1, col=id|summary|component, max=30, order=-component',
+                           'owner=joe&milestone=milestone1',
+                           dict(col='status|summary|id|summary|component', max='30', order=OrderRule(self.env, 'component', True)),
+                           'list')
+        
 
 def suite():
     suite = unittest.TestSuite()
Index: trac/ticket/query.py
===================================================================
--- trac/ticket/query.py	(revision 10865)
+++ trac/ticket/query.py	(working copy)
@@ -24,7 +24,7 @@
 
 from genshi.builder import tag
 
-from trac.config import Option, IntOption 
+from trac.config import Option, IntOption, ListOption 
 from trac.core import *
 from trac.db import get_column_names
 from trac.mimeview.api import Mimeview, IContentConverter, Context
@@ -55,13 +55,60 @@
         TracError.__init__(self, _('Invalid query constraint value'))
         self.errors = errors
 
+class OrderRule(object):
+    def __init__(self, env, name=None, desc=None):
+        self.env = env
+        self.name = name
+        self.desc = desc
+        
+    def __str__(self):
+        if self.desc:
+            return "-%s" % self.name
+        return self.name
+    
+    def __getattribute__(self, name):
+        value = object.__getattribute__(self, name)
+        if name == 'name':
+            value = value or 'priority'
+        elif name == 'desc':
+            value = value or False
+        return value
+    
+    def __setattr__(self, name, value):
+        if name == 'name':
+            synonyms = TicketSystem(self.env).get_field_synonyms()
+            value = synonyms.get(value, value)     # 0.11 compatibility
+        object.__setattr__(self, name, value)
+        
+    def __nonzero__(self):
+        if self.name and self.desc != None:
+            return True
+        return False
+    
+    def __eq__(self, other):
+        if type(self) is not type(other):
+            return False
+        
+        return self.name == other.name and self.desc == other.desc
+    
+    def __ne__(self, other):
+        return not self == other
+    
+    @classmethod
+    def from_string(cls, env, string, **kw):
+        if string:
+            if string[0] == '-':
+                return cls(env, string[1:], True)
+            else:
+                return cls(env, string, False)
+        return cls(env)
 
 class Query(object):
     substitutions = ['$USER']
     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=0, verbose=0,
+                 order=None, group=None, groupdesc=0, 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
@@ -69,9 +116,7 @@
         if isinstance(constraints, dict):
             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.order = order
         self.group = group
         self.groupdesc = groupdesc
         self.format = format
@@ -121,11 +166,32 @@
         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]
-        if self.order != 'id' and self.order not in field_names:
-            self.order = 'priority'
-
-        if self.group not in field_names:
+        if not self.order or (self.order.name != 'id' and self.order.name not in field_names):
+            default_order = QueryModule(self.env).default_order
+            default_order = OrderRule.from_string(self.env, default_order)
+            if default_order and (default_order.name == 'id' or default_order.name in field_names):
+                self.order = default_order
+            else:
+                # Fall back to 'priority' on invalid config
+                self.order = OrderRule(self.env)
+        
+        # Handle empty string as a request to not group the result
+        if self.group == '':
             self.group = None
+        elif not self.group or self.group not in field_names:
+            default_group = QueryModule(self.env).default_group
+            
+            if default_group:
+                if default_group[0] == '-':
+                    self.group = default_group[1:]
+                    self.groupdesc = True
+                else:
+                    self.group = default_group
+                    self.groupdesc = False
+            else:
+                # Fall back to non-grouped query on invalid config
+                self.group = None
+                self.groupdesc = False
 
         constraint_cols = {}
         for clause in self.constraints:
@@ -141,7 +207,7 @@
     
     @classmethod
     def from_string(cls, env, string, **kw):
-        kw_strs = ['order', 'group', 'page', 'max', 'format']
+        kw_strs = ['group', 'page', 'max', 'format']
         kw_arys = ['rows']
         kw_bools = ['desc', 'groupdesc', 'verbose']
         kw_synonyms = {'row': 'rows'}
@@ -150,6 +216,7 @@
         constraints = [{}]
         cols = []
         report = None
+        order = None
         def as_str(s):
             if isinstance(s, unicode):
                 return s.encode('utf-8')
@@ -189,12 +256,16 @@
                             for value in processed_values)
             elif field == 'report':
                 report = processed_values[0]
+            elif field == 'order':
+                order = OrderRule.from_string(env, processed_values[0])
             else:
                 constraints[-1].setdefault(synonyms.get(field, field), 
                                            []).extend(processed_values)
         constraints = filter(None, constraints)
         report = kw.pop('report', report)
-        return cls(env, report, constraints=constraints, cols=cols, **kw)
+        # Handle legacy way of defining order
+        legacy_order = OrderRule(env, kw.pop('order', None), as_bool(kw.pop('desc', None))) 
+        return cls(env, report, constraints=constraints, cols=cols, order=order or legacy_order, **kw)
 
     def get_columns(self):
         if not self.cols:
@@ -232,35 +303,55 @@
         return cols
 
     def get_default_columns(self):
-        cols = self.get_all_columns()
+        cols = QueryModule(self.env).default_cols
         
-        # Semi-intelligently remove columns that are restricted to a single
-        # value by a query constraint.
-        for col in [k for k in self.constraint_cols.keys()
-                    if k != 'id' and k in cols]:
-            constraints = self.constraint_cols[col]
-            for constraint in constraints:
-                if not (len(constraint) == 1 and constraint[0]
-                        and not constraint[0][0] in '!~^$' and col in cols
-                        and col not in self.time_fields):
-                    break
-            else:
-                cols.remove(col)
-            if col == 'status' and 'resolution' in cols:
+        # Use the columns defined in the config
+        if cols:
+            # Make sure the column we order by is visible.
+            if self.order and self.order.name not in cols:
+                cols.append(self.order.name)
+    
+            # Make sure to not show the column we group on.            
+            if self.group in cols:
+                cols.remove(self.group)
+                
+        # Try to automatically calculate what to show
+        else:
+            cols = self.get_all_columns()
+            
+            # Semi-intelligently remove columns that are restricted to a single
+            # value by a query constraint.
+            for col in [k for k in self.constraint_cols.keys()
+                        if k != 'id' and k in cols]:
+                constraints = self.constraint_cols[col]
                 for constraint in constraints:
-                    if 'closed' in constraint:
+                    if not (len(constraint) == 1 and constraint[0]
+                            and not constraint[0][0] in '!~^$' and col in cols
+                            and col not in self.time_fields):
                         break
                 else:
-                    cols.remove('resolution')
-        if self.group in cols:
-            cols.remove(self.group)
-
-        # Only display the first seven columns by default
-        cols = cols[:7]
-        # Make sure the column we order by is visible, if it isn't also
-        # the column we group by
-        if not self.order in cols and not self.order == self.group:
-            cols[-1] = self.order
+                    cols.remove(col)
+                if col == 'status' and 'resolution' in cols:
+                    for constraint in constraints:
+                        if 'closed' in constraint:
+                            break
+                    else:
+                        cols.remove('resolution')
+    
+            # Make sure to not show the column we group on.            
+            if self.group in cols:
+                cols.remove(self.group)
+            
+            # Only display the first seven columns by default
+            cols = cols[:7]
+            
+            # Make sure the column we order by is visible.
+            if self.order.name not in cols and self.order.name != self.group:
+                if len(cols) < 7:
+                    cols.append(self.order.name)
+                else:
+                    cols[-1] = self.order.name
+        
         return cols
 
     def count(self, req=None, db=None, cached_ids=None, authname=None,
@@ -350,14 +441,13 @@
         cursor.close()
         return results
 
-    def get_href(self, href, id=None, order=None, desc=None, format=None,
+    def get_href(self, href, id=None, order=None, format=None,
                  max=None, page=None):
         """Create a link corresponding to this query.
 
         :param href: the `Href` object used to build the URL
         :param id: optionally set or override the report `id`
         :param order: optionally override the order parameter of the query
-        :param desc: optionally override the desc parameter
         :param format: optionally override the format of the query
         :param max: optionally override the max items per page
         :param page: optionally specify which page of results (defaults to
@@ -376,8 +466,6 @@
 
         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:
@@ -405,8 +493,8 @@
         
         return href.query(constraints,
                           report=id,
-                          order=order, desc=desc and 1 or None,
-                          group=self.group or None,
+                          order=str(order) if order else None,
+                          group=self.group or "",
                           groupdesc=self.groupdesc and 1 or None,
                           col=cols,
                           row=self.rows,
@@ -443,7 +531,7 @@
             add_cols(self.group)
         if self.rows:
             add_cols('reporter', *self.rows)
-        add_cols('status', 'priority', 'time', 'changetime', self.order)
+        add_cols('status', 'priority', 'time', 'changetime', self.order.name)
         cols.extend([c for c in self.constraint_cols if not c in cols])
 
         custom_fields = [f['name'] for f in self.fields if 'custom' in f]
@@ -464,14 +552,14 @@
 
         # 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 == self.order.name or c == self.group 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]:
+                    if c == self.order.name or c == self.group]:
             sql.append("\n  LEFT OUTER JOIN %s ON (%s.name=%s)"
                        % (col, col, col))
 
@@ -622,8 +710,8 @@
                            (','.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 = [(self.order.name, self.order.desc)]
+        if self.group and self.group != self.order.name:
             order_cols.insert(0, (self.group, self.groupdesc))
 
         for name, desc in order_cols:
@@ -655,9 +743,9 @@
                            % (desc, desc, col, desc))
             else:
                 sql.append("%s%s" % (col, desc))
-            if name == self.group and not name == self.order:
+            if name == self.group and not name == self.order.name:
                 sql.append(",")
-        if self.order != 'id':
+        if self.order.name != 'id':
             sql.append(",t.id")  
 
         if errors:
@@ -719,8 +807,8 @@
         headers = [{
             '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=OrderRule(self.env, col,
+                                                                col == self.order.name and not self.order.desc))
         } for col in cols]
 
         fields = {'id': {'type': 'id', 'label': _("Ticket")}}
@@ -829,6 +917,22 @@
     items_per_page = IntOption('query', 'items_per_page', 100,
         """Number of tickets displayed per page in ticket queries,
         by default (''since 0.11'')""")
+    
+    default_cols = ListOption('query', 'default_cols',  
+        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.12.3'')""")
+    
+    default_order = Option('query', 'default_order',
+        default='priority',
+        doc="""The default order to use for queries.
+        (''since 0.12.3'')""")
+    
+    default_group = Option('query', 'default_group',
+        default=None,
+        doc="""The default group to use for queries.
+        (''since 0.12.3'')""")
 
     # IContentConverter methods
 
@@ -888,26 +992,18 @@
                 args = arg_list_to_args(arg_list)
                 constraints = self._get_constraints(arg_list=arg_list)
             else:
-                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
+                constraints = Query.from_string(self.env, qstring).constraints
+                # Substitute $USER, or ensure no field constraints that depend
+                # on $USER are used if we have no username.
+                for clause in constraints:
+                    for field, vals in clause.items():
+                        for (i, val) in enumerate(vals):
+                            if user:
+                                vals[i] = val.replace('$USER', user)
+                            elif val.endswith('$USER'):
+                                del clause[field]
+                                break
 
-            # Substitute $USER, or ensure no field constraints that depend
-            # on $USER are used if we have no username.
-            for clause in constraints:
-                for field, vals in clause.items():
-                    for (i, val) in enumerate(vals):
-                        if user:
-                            vals[i] = val.replace('$USER', user)
-                        elif val.endswith('$USER'):
-                            del clause[field]
-                            break
-
         cols = args.get('col')
         if isinstance(cols, basestring):
             cols = [cols]
@@ -923,8 +1019,9 @@
         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'),
+                      constraints, cols,
+                      OrderRule.from_string(self.env, args.get('order')),
+                      args.get('group'),
                       'groupdesc' in args, 'verbose' in args,
                       rows,
                       args.get('page'), 
@@ -1210,12 +1307,9 @@
     The `max` parameter can be used to limit the number of tickets shown
     (defaults to '''0''', i.e. no maximum).
 
-    The `order` parameter sets the field used for ordering tickets
-    (defaults to '''id''').
+    The `order` parameter sets the field used for ordering tickets including
+    the sort direction (defaults to '''id''').
 
-    The `desc` parameter indicates whether the order of the tickets
-    should be reversed (defaults to '''false''').
-
     The `group` parameter sets the field used for grouping tickets
     (defaults to not being set).
 
@@ -1238,7 +1332,7 @@
     _comma_splitter = re.compile(r'(?<!\\),')
     
     @staticmethod
-    def parse_args(content):
+    def parse_args(env, content):
         """Parse macro arguments and translate them to a query string."""
         clauses = [{}]
         argv = []
@@ -1249,8 +1343,10 @@
             if m:
                 kw = arg[:m.end() - 1].strip()
                 value = arg[m.end():]
-                if kw in ('order', 'max', 'format', 'col'):
+                if kw in ('max', 'format', 'col'):
                     kwargs[kw] = value
+                elif kw == 'order':
+                    kwargs[kw] = OrderRule.from_string(env, value)
                 else:
                     clauses[-1][kw] = value
             elif arg.strip() == 'or':
@@ -1262,7 +1358,7 @@
         if len(argv) > 0 and not 'format' in kwargs: # 0.10 compatibility hack
             kwargs['format'] = argv[0]
         if 'order' not in kwargs:
-            kwargs['order'] = 'id'
+            kwargs['order'] = OrderRule(env, 'id')
         if 'max' not in kwargs:
             kwargs['max'] = '0' # unlimited by default
 
@@ -1280,7 +1376,7 @@
     
     def expand_macro(self, formatter, name, content):
         req = formatter.req
-        query_string, kwargs, format = self.parse_args(content)
+        query_string, kwargs, format = self.parse_args(self.env, content)
         if query_string:
             query_string += '&'
         query_string += '&'.join('%s=%s' % item
@@ -1295,8 +1391,7 @@
         tickets = query.execute(req)
 
         if format == 'table':
-            data = query.template_data(formatter.context, tickets,
-                                       req=formatter.context.req)
+            data = query.template_data(formatter.context, tickets)
 
             add_stylesheet(req, 'common/css/report.css')
             
@@ -1324,15 +1419,12 @@
                 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
                 title = _("%(groupvalue)s %(groupname)s tickets matching "
                           "%(query)s", groupvalue=v, groupname=query.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
                 href = q.get_href(formatter.context)
                 groups.append((v, [t for t in g], href, title))
             return groups
Index: trac/ticket/templates/query_results.html
===================================================================
--- trac/ticket/templates/query_results.html	(revision 10865)
+++ trac/ticket/templates/query_results.html	(working copy)
@@ -31,10 +31,10 @@
   <py:def function="column_headers()">
     <tr class="trac-columns">
       <th py:for="header in headers"
-          class="$header.name${query.order == header.name and (query.desc and ' desc' or ' asc') or ''}">
+          class="$header.name${query.order.name == header.name and (query.order.desc and ' desc' or ' asc') or ''}">
         <?python asc = _('(ascending)'); desc = _('(descending)') ?>
         <a title="${_('Sort by %(col)s %(direction)s', col=header.label,
-                      direction=(query.order == header.name and not query.desc and desc or asc))}"
+                      direction=(query.order.name == header.name and query.order.desc and desc or asc))}"
            href="$header.href">${header.label}</a>
       </th>
     </tr>
Index: trac/ticket/templates/query.html
===================================================================
--- trac/ticket/templates/query.html	(revision 10865)
+++ trac/ticket/templates/query.html	(working copy)
@@ -206,7 +206,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 />
Index: trac/wiki/default-pages/TracQuery
===================================================================
--- trac/wiki/default-pages/TracQuery	(revision 10865)
+++ trac/wiki/default-pages/TracQuery	(working copy)
@@ -87,21 +87,21 @@
 You can also customize the columns displayed in the table format (''format=table'') by using ''col=<field>'' - you can specify multiple fields and what order they are displayed by placing pipes (`|`) between the columns like below:
 
 {{{
-[[TicketQuery(max=3,status=closed,order=id,desc=1,format=table,col=resolution|summary|owner|reporter)]]
+[[TicketQuery(max=3,status=closed,order=-id,format=table,col=resolution|summary|owner|reporter)]]
 }}}
 
 This is displayed as:
-[[TicketQuery(max=3,status=closed,order=id,desc=1,format=table,col=resolution|summary|owner|reporter)]]
+[[TicketQuery(max=3,status=closed,order=-id,format=table,col=resolution|summary|owner|reporter)]]
 
 ==== Full rows ====
 In ''table'' format you can also have full rows by using ''rows=<field>'' like below:
 
 {{{
-[[TicketQuery(max=3,status=closed,order=id,desc=1,format=table,col=resolution|summary|owner|reporter,rows=description)]]
+[[TicketQuery(max=3,status=closed,order=-id,format=table,col=resolution|summary|owner|reporter,rows=description)]]
 }}}
 
 This is displayed as:
-[[TicketQuery(max=3,status=closed,order=id,desc=1,format=table,col=resolution|summary|owner|reporter,rows=description)]]
+[[TicketQuery(max=3,status=closed,order=-id,format=table,col=resolution|summary|owner|reporter,rows=description)]]
 
 
 === Query Language ===

