Index: trac/ticket/query.py===================================================================--- trac/ticket/query.py (revision 4209)+++ trac/ticket/query.py (working copy)@@ -20,6 +20,7 @@ from StringIO import StringIO from trac.core import *+from trac.config import ListOption from trac.db import get_column_names from trac.mimeview.api import Mimeview, IContentConverter from trac.perm import IPermissionRequestor@@ -42,7 +43,7 @@ class Query(object): def __init__(self, env, constraints=None, order=None, desc=0, group=None,- groupdesc = 0, verbose=0):+ groupdesc = 0, verbose=0, columns=None): self.env = env self.constraints = constraints or {} self.order = order@@ -51,10 +52,12 @@ self.groupdesc = groupdesc self.verbose = verbose self.fields = TicketSystem(self.env).get_ticket_fields()- self.cols = [] # lazily initialized+ self.cols = columns # lazily initialized if empty- if self.order != 'id' \- and self.order not in [f['name'] for f in self.fields]:+ self.valid_cols = ['id', 'changetime', 'time'] + \+ [f['name'] for f in self.fields]++ if self.order not in self.valid_cols: # order by priority by default self.order = 'priority'@@ -94,6 +97,8 @@ kw[field] = processed_values[0] elif field in kw_bools: kw[field] = True+ elif field == 'columns':+ kw[field] = processed_values[0].split(',') else: constraints[field] = processed_values except UnicodeError:@@ -108,7 +113,14 @@ # FIXME: the user should be able to configure which columns should # be displayed cols = ['id']- cols += [f['name'] for f in self.fields if f['type'] != 'textarea']+ cols += [f['name'] for f in self.fields if f['type'] != 'textarea'+ and f['name'] not in cols]+ default_columns = QueryModule(self.env).default_columns+ for col in default_columns:+ if col not in cols:+ cols.append(col)+ # Filter out invalid column names from config or URL+ cols = [c for c in cols if c in self.valid_cols] for col in ('reporter', 'keywords', 'cc'): if col in cols: cols.remove(col)@@ -139,6 +151,8 @@ # Constrained columns appear before other columns elif col1 in constrained_fields or col2 in constrained_fields: return col1 in constrained_fields and -1 or 1+ elif col1 in default_columns or col2 in default_columns:+ return col1 in default_columns and -1 or 1 return 0 cols.sort(sort_columns)@@ -168,7 +182,7 @@ for row in cursor: id = int(row[0]) result = {'id': id, 'href': req.href.ticket(id)}- for i in range(1, len(columns)):+ for i in xrange(1, len(columns)): name, val = columns[i], row[i] if name == self.group: val = val or 'None'@@ -369,6 +383,8 @@ cols = self.get_columns() labels = dict([(f['name'], f['label']) for f in self.fields])+ labels['changetime'] = 'Modified'+ labels['time'] = 'Created' headers = [{ 'name': col, 'label': labels.get(col, 'Ticket'), 'href': self.get_href(req, order=col, desc=(col == self.order and@@ -429,6 +445,9 @@ implements(IRequestHandler, INavigationContributor, IWikiSyntaxProvider, IContentConverter)+ _default_columns = ListOption('ticket-query', 'default_columns', 'id',+ keep_empty=False)+ # IContentConverter methods def get_supported_conversions(self): yield ('rss', 'RSS Feed', 'xml',@@ -480,10 +499,13 @@ if email or name: constraints['cc'] = ('~%s' % email or name,)+ columns = [c.strip() for c in req.args.get('columns', '').split(',')+ if c.strip()] query = Query(self.env, constraints, req.args.get('order'), req.args.has_key('desc'), req.args.get('group'), req.args.has_key('groupdesc'),- req.args.has_key('verbose'))+ req.args.has_key('verbose'),+ self.expand_aliases(columns)) if req.args.has_key('update'): # Reset session vars@@ -506,6 +528,24 @@ return self.display_html(req, query)+ def expand_aliases(self, cols, aliases=None):+ if aliases is None:+ section = self.config['ticket-query-column-aliases']+ aliases = {}+ for key in section:+ aliases[key] = section.getlist(key, keep_empty=False)+ expanded = []+ for c in cols:+ if c in aliases:+ expanded.extend(self.expand_aliases(aliases[c], aliases))+ else:+ expanded.append(c)+ return expanded++ def default_columns(self):+ return self.expand_aliases(self._default_columns)+ default_columns = property(default_columns)+ # Internal methods def _get_constraints(self, req):Index: templates/query_div.html===================================================================--- templates/query_div.html (revision 4209)+++ templates/query_div.html (working copy)@@ -48,6 +48,7 @@ <td py:when="idx == 0" class="id"><a href="$result.href" title="View ticket">#$result.id</a></td> <td py:otherwise="" class="$header.name" py:choose=""> <a py:when="header.name == 'summary'" href="$result.href" title="View ticket">$result.summary</a>+ <span py:when="header.name in ('time', 'changetime')">${format_datetime(result[header.name])}</span> <span py:otherwise="">${result[header.name]}</span> </td> </py:for>