query:group=owner
Index: trac/ticket/query.py
===================================================================
--- trac/ticket/query.py (.../vendor/trac/tags/0.11b1) (revision 415)
+++ trac/ticket/query.py (.../patches/report-paginate-0.11) (revision 415)
@@ -16,6 +16,7 @@
# Author: Christopher Lenz
import csv
+from math import ceil
from datetime import datetime, timedelta
import re
from StringIO import StringIO
@@ -33,15 +34,17 @@
from trac.util.compat import groupby
from trac.util.datefmt import to_timestamp, utc
from trac.util.html import escape, unescape
+from trac.util.presentation import Paginator
from trac.util.text import shorten_line, CRLF
from trac.util.translation import _
from trac.web import IRequestHandler
from trac.web.href import Href
from trac.web.chrome import add_ctxtnav, add_link, add_script, add_stylesheet, \
INavigationContributor, Chrome
+
from trac.wiki.api import IWikiSyntaxProvider, parse_args
from trac.wiki.macros import WikiMacroBase # TODO: should be moved in .api
-from trac.config import Option
+from trac.config import Option, IntOption
class QuerySyntaxError(Exception):
"""Exception raised when a ticket query cannot be parsed from a string."""
@@ -49,9 +52,12 @@
class Query(object):
+ IntOption = IntOption('query', 'default_max', default=100, doc='Default max items per page')
+
def __init__(self, env, report=None, constraints=None, cols=None,
order=None, desc=0, group=None, groupdesc=0, verbose=0,
- rows=None, limit=None):
+ rows=None, page=1, max=None):
+
self.env = env
self.id = report # if not None, it's the corresponding saved query
self.constraints = constraints or {}
@@ -59,7 +65,40 @@
self.desc = desc
self.group = group
self.groupdesc = groupdesc
- self.limit = limit
+ self.default_page = 1
+ self.default_max = self.env.config.getint('query', 'default_max')
+
+ try:
+ if page is None:
+ page = self.default_page
+ elif int(page) < 1:
+ raise ValueError()
+ except ValueError:
+ raise TracError(_('Query parameter is invalid: [page = %(page)s]', str(page)))
+
+ if max == 'None' or max == '':
+ max = 0
+
+ try:
+ if max is None:
+ max = self.default_max
+ elif int(max) < 0:
+ raise ValueError()
+ except ValueError:
+ raise TracError(_('Query parameter is invalid [max = %(max)s]', str(max)))
+
+ self.page = 0
+ self.offset = 0
+ self.max = 0
+
+ if int(max) == 0:
+ self.has_more_pages = False
+ else:
+ self.has_more_pages = True
+ self.page = int(page)
+ self.offset = int(max) * (int(page) - 1)
+ self.max = int(max)
+
if rows == None:
rows = []
if verbose and 'description' not in rows: # 0.10 compatibility
@@ -68,7 +107,6 @@
field_names = [f['name'] for f in self.fields]
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:
# TODO: fix after adding time/changetime to the api.py
if order == 'created':
@@ -85,7 +123,7 @@
def from_string(cls, env, string, **kw):
filters = string.split('&')
- kw_strs = ['order', 'group', 'limit']
+ kw_strs = ['order', 'group', 'page', 'max']
kw_arys = ['rows']
kw_bools = ['desc', 'groupdesc', 'verbose']
constraints = {}
@@ -190,12 +228,28 @@
if not self.cols:
self.get_columns()
- sql, args = self.get_sql(req)
- self.env.log.debug("Query SQL: " + sql % tuple([repr(a) for a in args]))
-
if not db:
db = self.env.get_db_cnx()
cursor = db.cursor()
+
+ sql, args = self.get_sql(req)
+ count_sql = 'SELECT COUNT(*) FROM (' + sql + ') AS foo'
+ self.env.log.debug("Query SQL: " + count_sql % tuple([repr(a) for a in args]))
+ cursor.execute(count_sql, args);
+ for row in cursor:
+ pass
+ self.env.log.debug("Get Tickets Count: " + str(row[0]))
+ self.num_items = row[0]
+
+ if self.num_items <= self.max:
+ self.has_more_pages = False
+
+ if self.has_more_pages:
+ sql = sql + " LIMIT %s OFFSET %s" % (self.max, self.offset)
+ if self.page > int(ceil(float(self.num_items) / self.max)) and self.num_items != 0:
+ raise ValueError('Page number is too leage')
+
+ self.env.log.debug("Query SQL: " + sql % tuple([repr(a) for a in args]))
cursor.execute(sql, args)
columns = get_column_names(cursor)
fields = []
@@ -226,7 +280,7 @@
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, desc=None, format=None, max=None, page=None):
"""Create a link corresponding to this query.
:param href: the `Href` object used to build the URL
@@ -239,24 +293,41 @@
"""
if not isinstance(href, Href):
href = href.href # compatibility with the `req` of the 0.10 API
+
+ if format == 'rss':
+ max = self.default_max
+ page = self.default_page
+
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
+
cols = self.get_columns()
# don't specify the columns in the href if they correspond to
- # the default columns, in the same order. That keeps the query url
- # shorter in the common case where we just want the default columns.
+ # the default columns, page and max in the same order. That keeps the
+ # query url shorter in the common case where we just want the default columns.
if cols == self.get_default_columns():
cols = None
+ if page == self.default_page:
+ page = None
+ if max == self.default_max:
+ max = None
+
return href.query(report=id,
order=order, desc=desc and 1 or None,
group=self.group or None,
groupdesc=self.groupdesc and 1 or None,
col=cols,
row=self.rows,
+ max=max,
+ page=page,
format=format, **self.constraints)
def to_string(self):
@@ -452,16 +523,11 @@
if name == self.group and not name == self.order:
sql.append(",")
if self.order != 'id':
- sql.append(",t.id")
-
- # Limit number of records
- if self.limit:
- sql.append("\nLIMIT %s")
- args.append(self.limit)
+ sql.append(",t.id")
return "".join(sql), args
- def template_data(self, context, tickets, orig_list=None, orig_time=None):
+ def template_data(self, context, tickets, orig_list=None, orig_time=None, req=None):
constraints = {}
for k, v in self.constraints.items():
constraint = {'values': [], 'mode': ''}
@@ -529,6 +595,27 @@
groupsequence.append(group_key)
groupsequence = [(value, groups[value]) for value in groupsequence]
+ results = Paginator(tickets,
+ self.page - 1,
+ self.max,
+ self.num_items)
+
+ if results.has_next_page:
+ next_href = self.get_href(req.href, max=self.max, page=self.page + 1)
+ add_link(req, 'next', next_href, _('Next Page'))
+
+ if results.has_previous_page:
+ prev_href = self.get_href(req.href, max=self.max, page=self.page - 1)
+ add_link(req, 'prev', prev_href, _('Previous Page'))
+
+ pagedata = []
+ shown_pages = results.get_shown_pages(21)
+ for page in shown_pages:
+ pagedata.append([self.get_href(req.href, page=page), None, str(page), 'page ' + str(page)])
+
+ results.shown_pages = [{'href':p[0], 'class':p[1], 'string':p[2], 'title':p[3]} for p in pagedata]
+ results.current_page = {'href':None, 'class':'current', 'string':str(results.page + 1), 'title':None}
+
return {'query': self,
'context': context,
'col': cols,
@@ -539,9 +626,9 @@
'fields': fields,
'modes': modes,
'tickets': tickets,
- 'groups': groupsequence or [(None, tickets)]}
-
-
+ 'groups': groupsequence or [(None, tickets)],
+ 'paginator': results}
+
class QueryModule(Component):
implements(IRequestHandler, INavigationContributor, IWikiSyntaxProvider,
@@ -607,7 +694,7 @@
if user:
qstring = qstring.replace('$USER', user)
- self.log.debug('QueryModule: Using default query: %s', qstring)
+ self.log.debug('QueryModule: Using default query: %s', str(qstring))
constraints = Query.from_string(self.env, qstring).constraints
# Ensure no field constraints that depend on $USER are used
# if we have no username.
@@ -631,7 +718,8 @@
'desc' in req.args, req.args.get('group'),
'groupdesc' in req.args, 'verbose' in req.args,
rows,
- req.args.get('limit'))
+ req.args.get('page'),
+ req.args.get('max'))
if 'update' in req.args:
# Reset session vars
@@ -698,6 +786,8 @@
db = self.env.get_db_cnx()
tickets = query.execute(req, db)
+ self.env.log.debug("Display Tickets ID: " + ', '.join([str(t['id']) for t in tickets]))
+
# The most recent query is stored in the user session;
orig_list = rest_list = None
orig_time = datetime.now(utc)
@@ -718,7 +808,9 @@
# Find out which tickets originally in the query results no longer
# match the constraints
- if rest_list:
+ befor_href = req.session.get('query_href')
+ self.env.log.debug("Query href: " + str(query.get_href(Context.from_request(req, 'query').href)))
+ if rest_list and (befor_href == query.get_href(Context.from_request(req, 'query').href)):
for tid in [t['id'] for t in tickets if t['id'] in rest_list]:
rest_list.remove(tid)
for rest_id in rest_list:
@@ -734,7 +826,7 @@
tickets.insert(orig_list.index(rest_id), data)
context = Context.from_request(req, 'query')
- data = query.template_data(context, tickets, orig_list, orig_time)
+ data = query.template_data(context, tickets, orig_list, orig_time, req)
# For clients without JavaScript, we add a new constraint here if
# requested
@@ -815,7 +907,9 @@
query_href = req.abs_href.query(group=query.group,
groupdesc=(query.groupdesc and 1
or None),
- row=query.rows,
+ row=query.rows,
+ page=req.args.get('page'),
+ max=req.args.get('max'),
**query.constraints)
data = {
'context': Context.from_request(req, 'query', absurls=True),
Index: trac/ticket/templates/query.html
===================================================================
--- trac/ticket/templates/query.html (.../vendor/trac/tags/0.11b1) (revision 415)
+++ trac/ticket/templates/query.html (.../patches/report-paginate-0.11) (revision 415)
@@ -27,7 +27,7 @@