Ticket #6127: Trac-0.11b1_reportpaginate_r412.patch
File Trac-0.11b1_reportpaginate_r412.patch, 34.2 KB (added by , 16 years ago) |
---|
-
trac/ticket/report.py
23 23 24 24 from genshi.builder import tag 25 25 26 from trac.config import Option, IntOption 26 27 from trac.core import * 27 28 from trac.db import get_column_names 28 29 from trac.mimeview import Context … … 30 31 from trac.resource import Resource, ResourceNotFound 31 32 from trac.util import sorted 32 33 from trac.util.datefmt import format_datetime, format_time 34 from trac.util.presentation import Paginator 33 35 from trac.util.text import to_unicode, unicode_urlencode 34 36 from trac.util.translation import _ 35 37 from trac.web.api import IRequestHandler, RequestDone … … 43 45 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 44 46 IWikiSyntaxProvider) 45 47 48 default_max = IntOption('report', 'default_max', default=100, doc='Default max items per page') 49 default_rss_max = IntOption('report', 'default_rss_max', default=0, doc='Default max items per page for RSS') 50 46 51 # INavigationContributor methods 47 52 48 53 def get_active_navigation_item(self, req): … … 276 281 'report': {'id': id, 'resource': report_resource}, 277 282 'context': context, 278 283 'title': title, 'description': description, 279 'args': args, 'message': None} 284 'args': args, 'message': None, 'paginator':None} 285 286 self.page = int(req.args.get('page', '1')) 287 if req.args.get('format', '') == 'rss': 288 self.limit = self.env.config.getint('report', 'default_rss_max') 289 else: 290 self.limit = self.env.config.getint('report', 'default_max') 291 self.offset = (self.page - 1) * self.limit 292 280 293 try: 281 294 cols, results = self.execute_report(req, db, id, sql, args) 282 295 results = [list(row) for row in results] 296 numrows = len(results) 297 283 298 except Exception, e: 284 299 data['message'] = _('Report execution failed: %(error)s', 285 300 error=to_unicode(e)) 286 301 return 'report_view.html', data, None 302 paginator = None 303 if id != -1 and self.limit > 0: 304 self.asc = req.args.get('asc', None) 305 self.sort = req.args.get('sort', None) 306 self.USER = req.args.get('USER', None) 307 paginator = Paginator(results, self.page - 1, self.limit, self.num_items) 308 data['paginator'] = paginator 309 if paginator.has_next_page: 310 next_href = req.href.report(id, asc=self.asc, sort=self.sort, USER=self.USER, page=self.page + 1) 311 add_link(req, 'next', next_href, _('Next Page')) 312 if paginator.has_previous_page: 313 prev_href = req.href.report(id, asc=self.asc, sort=self.sort, USER=self.USER, page=self.page - 1) 314 add_link(req, 'prev', prev_href, _('Previous Page')) 315 pagedata = [] 316 shown_pages = paginator.get_shown_pages(21) 317 for page in shown_pages: 318 pagedata.append([req.href.report(id, asc=self.asc, sort=self.sort, USER=self.USER, page=page), None, str(page), 'page ' + str(page)]) 319 paginator.shown_pages = [{'href':p[0], 'class':p[1], 'string':p[2], 'title':p[3]} for p in pagedata] 320 paginator.current_page = {'href':None, 'class':'current', 'string':str(paginator.page + 1), 'title':None} 321 numrows = paginator.num_items 287 322 288 323 sort_col = req.args.get('sort', '') 289 324 asc = req.args.get('asc', 1) … … 303 338 304 339 if col == sort_col: 305 340 header['asc'] = asc 306 def sortkey(row): 307 val = row[idx] 308 if isinstance(val, basestring): 309 val = val.lower() 310 return val 311 results = sorted(results, key=sortkey, reverse=(not asc)) 341 if not paginator: 342 def sortkey(row): 343 val = row[idx] 344 if isinstance(val, basestring): 345 val = val.lower() 346 return val 347 results = sorted(results, key=sortkey, reverse=(not asc)) 312 348 313 349 header_group = header_groups[-1] 314 350 … … 387 423 388 424 data.update({'header_groups': header_groups, 389 425 'row_groups': row_groups, 390 'numrows': len(results),426 'numrows': numrows, 391 427 'sorting_enabled': len(row_groups)==1, 392 428 'email_map': email_map}) 393 429 … … 416 452 req.session['query_tickets'] = \ 417 453 ' '.join([str(int(row['id'])) 418 454 for rg in row_groups for row in rg[1]]) 419 req.session['query_href'] = req.href.report(id )455 req.session['query_href'] = req.href.report(id, asc=self.asc, srot=self.sort, USER=self.USER, page=self.page) 420 456 # Kludge: we have to clear the other query session 421 457 # variables, but only if the above succeeded 422 458 for var in ('query_constraints', 'query_time'): … … 450 486 if not sql: 451 487 raise TracError(_('Report %(num)s has no SQL query.', num=id)) 452 488 self.log.debug('Executing report with SQL "%s" (%s)', sql, args) 489 self.log.debug('Request args' + str(req.args)) 490 cursor = db.cursor() 453 491 454 cursor = db.cursor() 492 if id != -1 and self.limit > 0: 493 # The number of tickets is obtained. 494 count_sql = 'SELECT COUNT(*) FROM (' + sql + ') AS tab' 495 cursor.execute(count_sql, args) 496 self.env.log.debug("Query SQL(Get num items): " + count_sql) 497 for row in cursor: 498 pass 499 self.num_items = row[0] 500 501 # The column name is obtained. 502 get_col_name_sql = 'SELECT * FROM ( ' + sql + ' ) AS tab LIMIT 1' 503 cursor.execute(get_col_name_sql, args) 504 self.env.log.debug("Query SQL(Get col names): " + get_col_name_sql) 505 cols = get_column_names(cursor) 506 507 sort_col = req.args.get('sort', '') 508 self.env.log.debug("Colnum Names %s, Sort column %s" % (str(cols), sort_col)) 509 order_cols = [] 510 try: 511 group_idx = cols.index('__group__') 512 order_cols.append(str(group_idx)) 513 except ValueError: 514 pass 515 516 if sort_col: 517 try: 518 sort_idx = cols.index(sort_col) + 1 519 order_cols.append(str(sort_idx)) 520 except ValueError: 521 raise TrackError(_('Query parameter "sort=%(sort_col)s" is invalid', sort_col)) 522 523 # The report-query results is obtained 524 asc_str = ['DESC', 'ASC'] 525 asc_idx = int(req.args.get('asc','0')) 526 order_by = '' 527 if len(order_cols) != 0: 528 dlmt = ", " 529 order = dlmt.join(order_cols) 530 order_by = " ".join([' ORDER BY' ,order, asc_str[asc_idx]]) 531 sql = " ".join(['SELECT * FROM (', sql, ') AS tab', order_by]) 532 sql =" ".join([sql, 'LIMIT', str(self.limit), 'OFFSET', str(self.offset)]) 533 self.env.log.debug("Query SQL: " + sql) 455 534 cursor.execute(sql, args) 456 535 self.env.log.debug("Query SQL: " + sql) 457 536 # FIXME: fetchall should probably not be used. 458 537 info = cursor.fetchall() or [] 459 538 cols = get_column_names(cursor) -
trac/ticket/tests/wikisyntax.py
197 197 <a class="query" href="/query?status=new&status=reopened&order=priority">query:status=new|reopened</a> 198 198 </p> 199 199 <p> 200 <a class="query" href="/query? order=priority&milestone=%21">query:milestone!=</a>200 <a class="query" href="/query?milestone=%21&order=priority">query:milestone!=</a> 201 201 </p> 202 202 <p> 203 <a class="query" href="/query? order=priority&milestone=1.0&milestone=2.0&owner=me">query:milestone=1.0|2.0&owner=me</a>203 <a class="query" href="/query?milestone=1.0&milestone=2.0&owner=me&order=priority">query:milestone=1.0|2.0&owner=me</a> 204 204 </p> 205 205 <p> 206 206 <a class="query" href="/query?group=owner&order=priority">query:group=owner</a> -
trac/ticket/query.py
16 16 # Author: Christopher Lenz <cmlenz@gmx.de> 17 17 18 18 import csv 19 from math import ceil 19 20 from datetime import datetime, timedelta 20 21 import re 21 22 from StringIO import StringIO … … 33 34 from trac.util.compat import groupby 34 35 from trac.util.datefmt import to_timestamp, utc 35 36 from trac.util.html import escape, unescape 37 from trac.util.presentation import Paginator 36 38 from trac.util.text import shorten_line, CRLF 37 39 from trac.util.translation import _ 38 40 from trac.web import IRequestHandler 39 41 from trac.web.href import Href 40 42 from trac.web.chrome import add_ctxtnav, add_link, add_script, add_stylesheet, \ 41 43 INavigationContributor, Chrome 44 42 45 from trac.wiki.api import IWikiSyntaxProvider, parse_args 43 46 from trac.wiki.macros import WikiMacroBase # TODO: should be moved in .api 44 from trac.config import Option 47 from trac.config import Option, IntOption 45 48 46 49 class QuerySyntaxError(Exception): 47 50 """Exception raised when a ticket query cannot be parsed from a string.""" … … 49 52 50 53 class Query(object): 51 54 55 IntOption = IntOption('query', 'default_max', default=100, doc='Default max items per page') 56 52 57 def __init__(self, env, report=None, constraints=None, cols=None, 53 58 order=None, desc=0, group=None, groupdesc=0, verbose=0, 54 rows=None, limit=None): 59 rows=None, page=1, max=None): 60 55 61 self.env = env 56 62 self.id = report # if not None, it's the corresponding saved query 57 63 self.constraints = constraints or {} … … 59 65 self.desc = desc 60 66 self.group = group 61 67 self.groupdesc = groupdesc 62 self.limit = limit 68 self.default_page = 1 69 self.default_max = self.env.config.getint('query', 'default_max') 70 71 try: 72 if page is None: 73 page = self.default_page 74 elif int(page) < 1: 75 raise ValueError() 76 except ValueError: 77 raise TracError(_('Query parameter is invalid: [page = %(page)s]', str(page))) 78 79 if max == 'None' or max == '': 80 max = 0 81 82 try: 83 if max is None: 84 max = self.default_max 85 elif int(max) < 0: 86 raise ValueError() 87 except ValueError: 88 raise TracError(_('Query parameter is invalid [max = %(max)s]', str(max))) 89 90 self.page = 0 91 self.offset = 0 92 self.max = 0 93 94 if int(max) == 0: 95 self.has_more_pages = False 96 else: 97 self.has_more_pages = True 98 self.page = int(page) 99 self.offset = int(max) * (int(page) - 1) 100 self.max = int(max) 101 63 102 if rows == None: 64 103 rows = [] 65 104 if verbose and 'description' not in rows: # 0.10 compatibility … … 68 107 field_names = [f['name'] for f in self.fields] 69 108 self.cols = [c for c in cols or [] if c in field_names or c == 'id'] 70 109 self.rows = [c for c in rows if c in field_names] 71 72 110 if self.order != 'id' and self.order not in field_names: 73 111 # TODO: fix after adding time/changetime to the api.py 74 112 if order == 'created': … … 85 123 86 124 def from_string(cls, env, string, **kw): 87 125 filters = string.split('&') 88 kw_strs = ['order', 'group', ' limit']126 kw_strs = ['order', 'group', 'page', 'max'] 89 127 kw_arys = ['rows'] 90 128 kw_bools = ['desc', 'groupdesc', 'verbose'] 91 129 constraints = {} … … 190 228 if not self.cols: 191 229 self.get_columns() 192 230 193 sql, args = self.get_sql(req)194 self.env.log.debug("Query SQL: " + sql % tuple([repr(a) for a in args]))195 196 231 if not db: 197 232 db = self.env.get_db_cnx() 198 233 cursor = db.cursor() 234 235 sql, args = self.get_sql(req) 236 count_sql = 'SELECT COUNT(*) FROM (' + sql + ') AS foo' 237 self.env.log.debug("Query SQL: " + count_sql % tuple([repr(a) for a in args])) 238 cursor.execute(count_sql, args); 239 for row in cursor: 240 pass 241 self.env.log.debug("Get Tickets Count: " + str(row[0])) 242 self.num_items = row[0] 243 244 if self.num_items <= self.max: 245 self.has_more_pages = False 246 247 if self.has_more_pages: 248 sql = sql + " LIMIT %s OFFSET %s" % (self.max, self.offset) 249 if self.page > int(ceil(float(self.num_items) / self.max)) and self.num_items != 0: 250 raise ValueError('Page number is too leage') 251 252 self.env.log.debug("Query SQL: " + sql % tuple([repr(a) for a in args])) 199 253 cursor.execute(sql, args) 200 254 columns = get_column_names(cursor) 201 255 fields = [] … … 226 280 cursor.close() 227 281 return results 228 282 229 def get_href(self, href, id=None, order=None, desc=None, format=None ):283 def get_href(self, href, id=None, order=None, desc=None, format=None, max=None, page=None): 230 284 """Create a link corresponding to this query. 231 285 232 286 :param href: the `Href` object used to build the URL … … 239 293 """ 240 294 if not isinstance(href, Href): 241 295 href = href.href # compatibility with the `req` of the 0.10 API 296 297 if format == 'rss': 298 max = self.default_max 299 page = self.default_page 300 242 301 if id is None: 243 302 id = self.id 244 303 if desc is None: 245 304 desc = self.desc 246 305 if order is None: 247 306 order = self.order 307 if max is None: 308 max = self.max 309 if page is None: 310 page = self.page 311 248 312 cols = self.get_columns() 249 313 # don't specify the columns in the href if they correspond to 250 # the default columns, in the same order. That keeps the query url251 # shorter in the common case where we just want the default columns.314 # the default columns, page and max in the same order. That keeps the 315 # query url shorter in the common case where we just want the default columns. 252 316 if cols == self.get_default_columns(): 253 317 cols = None 318 if page == self.default_page: 319 page = None 320 if max == self.default_max: 321 max = None 322 254 323 return href.query(report=id, 255 324 order=order, desc=desc and 1 or None, 256 325 group=self.group or None, 257 326 groupdesc=self.groupdesc and 1 or None, 258 327 col=cols, 259 328 row=self.rows, 329 max=max, 330 page=page, 260 331 format=format, **self.constraints) 261 332 262 333 def to_string(self): … … 452 523 if name == self.group and not name == self.order: 453 524 sql.append(",") 454 525 if self.order != 'id': 455 sql.append(",t.id") 456 457 # Limit number of records 458 if self.limit: 459 sql.append("\nLIMIT %s") 460 args.append(self.limit) 526 sql.append(",t.id") 461 527 462 528 return "".join(sql), args 463 529 464 def template_data(self, context, tickets, orig_list=None, orig_time=None ):530 def template_data(self, context, tickets, orig_list=None, orig_time=None, req=None): 465 531 constraints = {} 466 532 for k, v in self.constraints.items(): 467 533 constraint = {'values': [], 'mode': ''} … … 529 595 groupsequence.append(group_key) 530 596 groupsequence = [(value, groups[value]) for value in groupsequence] 531 597 598 results = Paginator(tickets, 599 self.page - 1, 600 self.max, 601 self.num_items) 602 603 if results.has_next_page: 604 next_href = self.get_href(req.href, max=self.max, page=self.page + 1) 605 add_link(req, 'next', next_href, _('Next Page')) 606 607 if results.has_previous_page: 608 prev_href = self.get_href(req.href, max=self.max, page=self.page - 1) 609 add_link(req, 'prev', prev_href, _('Previous Page')) 610 611 pagedata = [] 612 shown_pages = results.get_shown_pages(21) 613 for page in shown_pages: 614 pagedata.append([self.get_href(req.href, page=page), None, str(page), 'page ' + str(page)]) 615 616 results.shown_pages = [{'href':p[0], 'class':p[1], 'string':p[2], 'title':p[3]} for p in pagedata] 617 results.current_page = {'href':None, 'class':'current', 'string':str(results.page + 1), 'title':None} 618 532 619 return {'query': self, 533 620 'context': context, 534 621 'col': cols, … … 539 626 'fields': fields, 540 627 'modes': modes, 541 628 'tickets': tickets, 542 'groups': groupsequence or [(None, tickets)] }543 544 629 'groups': groupsequence or [(None, tickets)], 630 'paginator': results} 631 545 632 class QueryModule(Component): 546 633 547 634 implements(IRequestHandler, INavigationContributor, IWikiSyntaxProvider, … … 607 694 608 695 if user: 609 696 qstring = qstring.replace('$USER', user) 610 self.log.debug('QueryModule: Using default query: %s', qstring)697 self.log.debug('QueryModule: Using default query: %s', str(qstring)) 611 698 constraints = Query.from_string(self.env, qstring).constraints 612 699 # Ensure no field constraints that depend on $USER are used 613 700 # if we have no username. … … 631 718 'desc' in req.args, req.args.get('group'), 632 719 'groupdesc' in req.args, 'verbose' in req.args, 633 720 rows, 634 req.args.get('limit')) 721 req.args.get('page'), 722 req.args.get('max')) 635 723 636 724 if 'update' in req.args: 637 725 # Reset session vars … … 698 786 db = self.env.get_db_cnx() 699 787 tickets = query.execute(req, db) 700 788 789 self.env.log.debug("Display Tickets ID: " + ', '.join([str(t['id']) for t in tickets])) 790 701 791 # The most recent query is stored in the user session; 702 792 orig_list = rest_list = None 703 793 orig_time = datetime.now(utc) … … 718 808 719 809 # Find out which tickets originally in the query results no longer 720 810 # match the constraints 721 if rest_list: 811 befor_href = req.session.get('query_href') 812 self.env.log.debug("Query href: " + str(query.get_href(Context.from_request(req, 'query').href))) 813 if rest_list and (befor_href == query.get_href(Context.from_request(req, 'query').href)): 722 814 for tid in [t['id'] for t in tickets if t['id'] in rest_list]: 723 815 rest_list.remove(tid) 724 816 for rest_id in rest_list: … … 734 826 tickets.insert(orig_list.index(rest_id), data) 735 827 736 828 context = Context.from_request(req, 'query') 737 data = query.template_data(context, tickets, orig_list, orig_time )829 data = query.template_data(context, tickets, orig_list, orig_time, req) 738 830 739 831 # For clients without JavaScript, we add a new constraint here if 740 832 # requested … … 815 907 query_href = req.abs_href.query(group=query.group, 816 908 groupdesc=(query.groupdesc and 1 817 909 or None), 818 row=query.rows, 910 row=query.rows, 911 page=req.args.get('page'), 912 max=req.args.get('max'), 819 913 **query.constraints) 820 914 data = { 821 915 'context': Context.from_request(req, 'query', absurls=True), -
trac/ticket/templates/query.html
27 27 </py:def> 28 28 29 29 <div id="content" class="query"> 30 <h1>$title ${num_matches( len(tickets))}</h1>30 <h1>$title ${num_matches(query.num_items)}</h1> 31 31 32 32 <div py:if="description" id="description" xml:space="preserve"> 33 33 ${wiki_to_html(context(report_resource), description)} … … 169 169 </py:for> 170 170 </p> 171 171 172 <p class="option"> 173 <label for="max">Max items per page</label> 174 <input type="text" name="max" id="max" size="10" value="${query.max}" /> 175 </p> 176 172 177 <div class="buttons"> 173 178 <input py:if="report_resource" type="hidden" name="report" value="$report_resource.id" /> 174 179 <input type="hidden" name="order" value="$query.order" /> -
trac/ticket/templates/report_view.html
5 5 xmlns:py="http://genshi.edgewall.org/" 6 6 xmlns:xi="http://www.w3.org/2001/XInclude"> 7 7 <xi:include href="layout.html" /> 8 <xi:include href="presentation.html" /> 8 9 <head> 9 10 <title>$title</title> 10 11 </head> … … 40 41 </div> 41 42 </form> 42 43 </div> 43 44 <py:if test="report.id != -1"> 45 <h2 py:if="paginator.has_more_pages"> 46 Results 47 ${num_results(paginator)} 48 </h2> 49 ${paging(paginator, chrome)} 50 </py:if> 44 51 <py:for each="value_for_group, row_group in row_groups"> 45 52 <h2 py:if="value_for_group">$value_for_group 46 53 <span class="numrows" py:with="cnt = len(row_group)">(${cnt or 'No'} match${cnt != 1 and 'es' or ''})</span></h2> … … 145 152 </tbody> 146 153 </table> 147 154 </py:for> 148 155 <py:if test="report.id != -1"> 156 ${paging(paginator, chrome)} 157 </py:if> 149 158 <div py:if="report.id == -1 and 'REPORT_CREATE' in perm(report.resource)" class="buttons"> 150 159 <form action="" method="get"> 151 160 <div> -
trac/ticket/templates/query_results.html
17 17 xmlns:py="http://genshi.edgewall.org/" 18 18 xmlns:xi="http://www.w3.org/2001/XInclude"> 19 19 <xi:include href="macros.html" /> 20 20 <xi:include href="presentation.html" /> 21 21 22 <py:def function="num_matches(v)"> 22 23 <span class="numrows">(${v or 'No'} match${v != 1 and 'es' or ''})</span> 23 24 </py:def> 24 25 26 <h2 py:if="paginator.has_more_pages"> 27 Results 28 ${num_results(paginator)} 29 </h2> 30 ${paging(paginator, chrome)} 25 31 <py:for each="groupname, results in groups"> 26 32 <h2 py:if="groupname"> 27 33 ${fields[query.group].label}: $groupname ${num_matches(len(results))} … … 88 94 </tbody> 89 95 </table> 90 96 </py:for> 97 ${paging(paginator, chrome)} 91 98 </div> 92 99 100 -
trac/htdocs/css/trac.css
525 525 #content.error #traceback td code { white-space: pre; } 526 526 #content.error #traceback pre { font-size: 95%; } 527 527 528 #content .paging { margin: 0 0 2em; padding: .5em 0 0; 529 font-size: 85%; line-height: 2em; text-align: center; 530 } 531 #content .paging .current { 532 padding: .1em .3em; 533 border: 1px solid #333; 534 background: #999; color: #fff; 535 } 536 537 #content .paging :link, #content .paging :visited { 538 padding: .1em .3em; 539 border: 1px solid #666; 540 background: transparent; color: #666; 541 } 542 #content .paging :link:hover, #content .paging :visited:hover { 543 background: #999; color: #fff; border-color: #333; 544 } 545 #content .paging .previous a, 546 #content .paging .next a { 547 font-size: 150%; font-weight: bold; border: none; 548 } 549 #content .paging .previous a:hover, 550 #content .paging .next a:hover { 551 background: transparent; color: #666; 552 } 553 554 #content h2 .numresults { color: #666; font-size: 90%; } 555 528 556 /* Styles for search word highlighting */ 529 557 @media screen { 530 558 .searchword0 { background: #ff9 } -
trac/htdocs/css/search.css
5 5 #content.search hr { clear: left; margin-bottom: 0 } 6 6 #content.search #notfound { margin: 2em; font-size: 110% } 7 7 8 #content.search h2 .numresults { color: #666; font-size: 90%; }9 8 #content.search #results { margin-right: 3em } 10 9 #content.search #results dt { margin: 1.5em 0 0 } 11 10 #content.search #results dt a { color: #33c } … … 13 12 #content.search #results .author, #results .date { color: #090; } 14 13 15 14 #content.search #quickjump { font-style: italic; font-weight: bold; } 16 17 #content.search .paging { margin: 0 0 2em; padding: .5em 0 0;18 font-size: 85%; line-height: 2em; text-align: center;19 }20 #content.search .paging span .current {21 padding: .1em .3em;22 border: 1px solid #333;23 background: #999; color: #fff;24 }25 #content.search .paging :link, #content.search .paging :visited {26 padding: .1em .3em;27 border: 1px solid #666;28 background: transparent; color: #666;29 }30 #content.search .paging :link:hover, #content.search .paging :visited:hover {31 background: #999; color: #fff; border-color: #333;32 }33 #content.search .paging .previous a,34 #content.search .paging .next a {35 font-size: 150%; font-weight: bold; border: none;36 }37 #content.search .paging .previous a:hover,38 #content.search .paging .next a:hover {39 background: transparent; color: #666;40 } -
trac/templates/presentation.html
1 <div xmlns="http://www.w3.org/1999/xhtml" 2 xmlns:py="http://genshi.edgewall.org/" py:strip="True"> 3 4 <div py:def="paging(paginator, chrome)" class="paging" py:if="paginator.has_more_pages"> 5 <span py:if="paginator.has_previous_page" 6 py:with="prevlink = chrome.links.prev[0]" class="previous"> 7 <a href="${prevlink.href}" title="${prevlink.title}">←</a> 8 </span> 9 <py:for each="page in paginator.shown_pages"> 10 <span py:if="page['string'] == paginator.current_page['string']"> 11 <span py:if="page['string'] == paginator.current_page['string']" class="${paginator.current_page['class']}">${paginator.current_page['string']}</span> 12 </span> 13 <span py:if="page['string'] != paginator.current_page['string']"> 14 <a href="${page['href']}" title="${page['title']}">${page['string']}</a> 15 </span> 16 </py:for> 17 <span py:if="paginator.has_next_page" 18 py:with="nextlink = chrome.links.next[0]" class="next"> 19 <a href="${nextlink.href}" title="${nextlink.title}">→</a> 20 </span> 21 </div> 22 23 <span py:def="num_results(paginator)" class="numresults"> 24 (${paginator.span[0] + 1} - ${paginator.span[1]} of ${paginator.num_items}) 25 </span> 26 </div> -
trac/search/web_ui.py
Property changes on: trac/templates/presentation.html ___________________________________________________________________ Name: svn:eol-style + native
112 112 results[idx] = {'href': result[0], 'title': result[1], 113 113 'date': format_datetime(result[2]), 114 114 'author': result[3], 'excerpt': result[4]} 115 116 pagedata = [] 115 117 data['results'] = results 118 shown_pages = results.get_shown_pages(21) 119 for shown_page in shown_pages: 120 page_href = req.href.search(zip(filters, ['on'] * len(filters)), q=req.args.get('q'), page=shown_page, noquickjump=1) 121 pagedata.append([page_href, None, str(shown_page), 'page ' + str(shown_page)]) 116 122 123 results.shown_pages = [{'href':p[0], 'class':p[1], 'string':p[2], 'title':p[3]} for p in pagedata] 124 results.current_page = {'href':None, 'class':'current', 'string':str(results.page + 1), 'title':None} 125 117 126 if results.has_next_page: 118 127 next_href = req.href.search(zip(filters, ['on'] * len(filters)), 119 128 q=req.args.get('q'), page=page + 1, -
trac/search/templates/search.html
6 6 xmlns:xi="http://www.w3.org/2001/XInclude"> 7 7 <xi:include href="layout.html" /> 8 8 <xi:include href="macros.html" /> 9 <xi:include href="presentation.html" /> 9 10 <head> 10 11 <title>Search<py:if test="query"> Results</py:if></title> 11 12 <py:if test="results"> … … 17 18 $(document).ready(function() {$("#q").get(0).focus()}); 18 19 </script> 19 20 </head> 20 21 <div py:def="paging" class="paging" py:if="results.has_more_pages">22 <span py:if="results.has_previous_page"23 py:with="prevlink = chrome.links.prev[0]" class="previous">24 <a href="${prevlink.href}" title="${prevlink.title}">←</a>25 </span>26 <span py:for="num in range(max(0, results.page - 10),27 min(results.page + 11, results.num_pages))"28 py:choose="">29 <span py:when="num == results.page" class="current">${num+1}</span>30 <a py:otherwise="" href="${page_href}&page=${num + 1}">${num + 1}</a>31 </span>32 <span py:if="results.has_next_page"33 py:with="nextlink = chrome.links.next[0]" class="next">34 <a href="${nextlink.href}" title="${nextlink.title}">→</a>35 </span>36 </div>37 38 21 <body> 39 22 <div id="content" class="search"> 40 23 … … 57 40 <py:if test="results or quickjump"><hr /> 58 41 <h2> 59 42 Results 60 <span class="numresults"> 61 (${results.span[0] + 1} - ${results.span[1]} of ${results.num_items}) 62 </span> 43 ${num_results(results)} 63 44 </h2> 64 ${paging( )}45 ${paging(results, chrome)} 65 46 <div> 66 47 <dl id="results"> 67 48 <py:if test="quickjump"> … … 80 61 </py:for> 81 62 </dl> 82 63 </div> 83 ${paging( )}64 ${paging(results, chrome)} 84 65 </py:if> 85 66 86 67 <div id="notfound" py:if="query and not (results or quickjump)"> -
trac/util/presentation.py
19 19 from math import ceil 20 20 from itertools import izip, chain, repeat 21 21 22 __all__ = ['classes', 'first_last', 'group', 'istext', 'p aginate', 'Paginator']22 __all__ = ['classes', 'first_last', 'group', 'istext', 'prepared_paginate', 'paginate', 'Paginator'] 23 23 24 24 25 25 def classes(*args, **kwargs): … … 106 106 from genshi.core import Markup 107 107 return isinstance(text, basestring) and not isinstance(text, Markup) 108 108 109 def prepared_paginate(items, num_items, max_per_page): 110 if max_per_page == 0: 111 num_pages = 1 112 else: 113 num_pages = int(ceil(float(num_items) / max_per_page)) 114 return items, num_items, num_pages 109 115 110 116 def paginate(items, page=0, max_per_page=10): 111 117 """Simple generic pagination. … … 179 185 180 186 class Paginator(object): 181 187 182 def __init__(self, items, page=0, max_per_page=10 ):188 def __init__(self, items, page=0, max_per_page=10, num_items=None): 183 189 if not page: 184 190 page = 0 185 offset = page * max_per_page 191 192 if num_items is None: 193 items, num_items, num_pages = paginate(items, page, max_per_page) 194 else: 195 items, num_items, num_pages = prepared_paginate(items, num_items, max_per_page) 196 197 offset = page * max_per_page 186 198 self.page = page 187 199 self.max_per_page = max_per_page 188 189 items, num_items, num_pages = paginate(items, page, max_per_page)190 191 200 self.items = items 192 201 self.num_items = num_items 193 202 self.num_pages = num_pages … … 216 225 def has_previous_page(self): 217 226 return self.page > 0 218 227 has_previous_page = property(has_previous_page) 228 229 def get_shown_pages(self, page_index_count = 11): 230 if self.has_more_pages == False: 231 return range(1, 2) 219 232 233 min_page = 1 234 max_page = int(ceil(float(self.num_items) / self.max_per_page)) 235 current_page = self.page + 1 236 start_page = current_page - page_index_count / 2 237 end_page = current_page + page_index_count / 2 + (page_index_count % 2 - 1) 238 239 if start_page < min_page: 240 start_page = min_page 241 if end_page > max_page: 242 end_page = max_page 243 244 return range(start_page, end_page + 1) 245 220 246 def separated(items, sep=','): 221 247 """Yield `(item, sep)` tuples, one for each element in `items`. 222 248