Index: trac/ticket/report.py
===================================================================
--- trac/ticket/report.py	(.../vendor/trac/tags/0.11b1)	(revision 415)
+++ trac/ticket/report.py	(.../patches/report-paginate-0.11)	(revision 415)
@@ -23,6 +23,7 @@
 
 from genshi.builder import tag
 
+from trac.config import Option, IntOption
 from trac.core import *
 from trac.db import get_column_names
 from trac.mimeview import Context
@@ -30,6 +31,7 @@
 from trac.resource import Resource, ResourceNotFound
 from trac.util import sorted
 from trac.util.datefmt import format_datetime, format_time
+from trac.util.presentation import Paginator
 from trac.util.text import to_unicode, unicode_urlencode
 from trac.util.translation import _
 from trac.web.api import IRequestHandler, RequestDone
@@ -43,6 +45,9 @@
     implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
                IWikiSyntaxProvider)
 
+    default_max = IntOption('report', 'default_max', default=100, doc='Default max items per page')
+    default_rss_max = IntOption('report', 'default_rss_max', default=0, doc='Default max items per page for RSS')
+    
     # INavigationContributor methods
 
     def get_active_navigation_item(self, req):
@@ -276,14 +281,44 @@
                 'report': {'id': id, 'resource': report_resource},
                 'context': context,
                 'title': title, 'description': description,
-                'args': args, 'message': None}
+                'args': args, 'message': None, 'paginator':None}
+
+        self.page = int(req.args.get('page', '1'))
+        if req.args.get('format', '') == 'rss':
+            self.limit = self.env.config.getint('report', 'default_rss_max')
+        else:
+            self.limit = self.env.config.getint('report', 'default_max')
+        self.offset = (self.page - 1) * self.limit
+
         try:
             cols, results = self.execute_report(req, db, id, sql, args)
             results = [list(row) for row in results]
+            numrows = len(results)
+
         except Exception, e:
             data['message'] = _('Report execution failed: %(error)s',
                                 error=to_unicode(e))
             return 'report_view.html', data, None
+        paginator = None
+        if id != -1 and self.limit > 0:
+            self.asc = req.args.get('asc', None)
+            self.sort = req.args.get('sort', None)
+            self.USER = req.args.get('USER', None)
+            paginator = Paginator(results, self.page - 1, self.limit, self.num_items)
+            data['paginator'] = paginator
+            if paginator.has_next_page:
+                next_href = req.href.report(id, asc=self.asc, sort=self.sort, USER=self.USER, page=self.page + 1)
+                add_link(req, 'next', next_href, _('Next Page'))
+            if paginator.has_previous_page:
+                prev_href = req.href.report(id, asc=self.asc, sort=self.sort, USER=self.USER, page=self.page - 1)
+                add_link(req, 'prev', prev_href, _('Previous Page'))
+            pagedata = []
+            shown_pages = paginator.get_shown_pages(21)
+            for page in shown_pages:
+                pagedata.append([req.href.report(id, asc=self.asc, sort=self.sort, USER=self.USER, page=page), None, str(page), 'page ' + str(page)])          
+            paginator.shown_pages = [{'href':p[0], 'class':p[1], 'string':p[2], 'title':p[3]} for p in pagedata]
+            paginator.current_page = {'href':None, 'class':'current', 'string':str(paginator.page + 1), 'title':None}
+            numrows = paginator.num_items
 
         sort_col = req.args.get('sort', '')
         asc = req.args.get('asc', 1)
@@ -303,12 +338,13 @@
 
             if col == sort_col:
                 header['asc'] = asc
-                def sortkey(row):
-                    val = row[idx]
-                    if isinstance(val, basestring):
-                        val = val.lower()
-                    return val
-                results = sorted(results, key=sortkey, reverse=(not asc))
+                if not paginator:
+                    def sortkey(row):
+                        val = row[idx]
+                        if isinstance(val, basestring):
+                            val = val.lower()
+                        return val
+                    results = sorted(results, key=sortkey, reverse=(not asc))
 
             header_group = header_groups[-1]
 
@@ -387,7 +423,7 @@
 
         data.update({'header_groups': header_groups,
                      'row_groups': row_groups,
-                     'numrows': len(results),
+                     'numrows': numrows,
                      'sorting_enabled': len(row_groups)==1,
                      'email_map': email_map})
 
@@ -416,7 +452,7 @@
                     req.session['query_tickets'] = \
                         ' '.join([str(int(row['id']))
                                   for rg in row_groups for row in rg[1]])
-                    req.session['query_href'] = req.href.report(id)
+                    req.session['query_href'] = req.href.report(id, asc=self.asc, srot=self.sort, USER=self.USER, page=self.page)
                     # Kludge: we have to clear the other query session
                     # variables, but only if the above succeeded 
                     for var in ('query_constraints', 'query_time'):
@@ -450,10 +486,53 @@
         if not sql:
             raise TracError(_('Report %(num)s has no SQL query.', num=id))
         self.log.debug('Executing report with SQL "%s" (%s)', sql, args)
+        self.log.debug('Request args' + str(req.args))
+        cursor = db.cursor()
 
-        cursor = db.cursor()
+        if id != -1 and self.limit > 0:
+            # The number of tickets is obtained.
+            count_sql = 'SELECT COUNT(*) FROM (' + sql + ') AS tab'
+            cursor.execute(count_sql, args)
+            self.env.log.debug("Query SQL(Get num items): " + count_sql)
+            for row in cursor:
+                pass
+            self.num_items = row[0]
+    
+            # The column name is obtained.
+            get_col_name_sql = 'SELECT * FROM ( ' + sql + ' ) AS tab LIMIT 1'
+            cursor.execute(get_col_name_sql, args)
+            self.env.log.debug("Query SQL(Get col names): " + get_col_name_sql)
+            cols = get_column_names(cursor)
+
+            sort_col = req.args.get('sort', '')
+            self.env.log.debug("Colnum Names %s, Sort column %s" % (str(cols), sort_col))
+            order_cols = []
+            try:
+                group_idx = cols.index('__group__')
+                order_cols.append(str(group_idx))
+            except ValueError:
+                pass
+
+            if sort_col:
+                try:
+                    sort_idx = cols.index(sort_col) + 1
+                    order_cols.append(str(sort_idx))
+                except ValueError:
+                    raise TrackError(_('Query parameter "sort=%(sort_col)s" is invalid', sort_col))
+
+            # The report-query results is obtained
+            asc_str = ['DESC', 'ASC']
+            asc_idx = int(req.args.get('asc','0'))
+            order_by = ''
+            if len(order_cols) != 0:
+                dlmt = ", "
+                order = dlmt.join(order_cols)
+                order_by = " ".join([' ORDER BY' ,order, asc_str[asc_idx]])
+            sql = " ".join(['SELECT * FROM (', sql, ') AS tab', order_by])
+            sql =" ".join([sql, 'LIMIT', str(self.limit), 'OFFSET', str(self.offset)])
+            self.env.log.debug("Query SQL: " + sql)
         cursor.execute(sql, args)
-
+        self.env.log.debug("Query SQL: " + sql)
         # FIXME: fetchall should probably not be used.
         info = cursor.fetchall() or []
         cols = get_column_names(cursor)
Index: trac/ticket/tests/wikisyntax.py
===================================================================
--- trac/ticket/tests/wikisyntax.py	(.../vendor/trac/tags/0.11b1)	(revision 415)
+++ trac/ticket/tests/wikisyntax.py	(.../patches/report-paginate-0.11)	(revision 415)
@@ -197,10 +197,10 @@
 <a class="query" href="/query?status=new&amp;status=reopened&amp;order=priority">query:status=new|reopened</a>
 </p>
 <p>
-<a class="query" href="/query?order=priority&amp;milestone=%21">query:milestone!=</a>
+<a class="query" href="/query?milestone=%21&amp;order=priority">query:milestone!=</a>
 </p>
 <p>
-<a class="query" href="/query?order=priority&amp;milestone=1.0&amp;milestone=2.0&amp;owner=me">query:milestone=1.0|2.0&amp;owner=me</a>
+<a class="query" href="/query?milestone=1.0&amp;milestone=2.0&amp;owner=me&amp;order=priority">query:milestone=1.0|2.0&amp;owner=me</a>
 </p>
 <p>
 <a class="query" href="/query?group=owner&amp;order=priority">query:group=owner</a>
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 <cmlenz@gmx.de>
 
 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 @@
     </py:def>
 
     <div id="content" class="query">
-      <h1>$title ${num_matches(len(tickets))}</h1>
+      <h1>$title ${num_matches(query.num_items)}</h1>
 
       <div py:if="description" id="description" xml:space="preserve">
         ${wiki_to_html(context(report_resource), description)}
@@ -169,6 +169,11 @@
           </py:for>
         </p>
 
+        <p class="option">
+          <label for="max">Max items per page</label>
+          <input type="text" name="max" id="max" size="10" value="${query.max}" />
+        </p>
+
         <div class="buttons">
           <input py:if="report_resource" type="hidden" name="report" value="$report_resource.id" />
           <input type="hidden" name="order" value="$query.order" />
Index: trac/ticket/templates/report_view.html
===================================================================
--- trac/ticket/templates/report_view.html	(.../vendor/trac/tags/0.11b1)	(revision 415)
+++ trac/ticket/templates/report_view.html	(.../patches/report-paginate-0.11)	(revision 415)
@@ -5,6 +5,7 @@
       xmlns:py="http://genshi.edgewall.org/"
       xmlns:xi="http://www.w3.org/2001/XInclude">
   <xi:include href="layout.html" />
+  <xi:include href="presentation.html" />
   <head>
     <title>$title</title>
   </head>
@@ -40,7 +41,13 @@
           </div>
         </form>
       </div>
-
+      <py:if test="report.id != -1">
+        <h2 py:if="paginator.has_more_pages">
+          Results
+          ${num_results(paginator)}
+        </h2>
+        ${paging(paginator, chrome)}
+      </py:if>
       <py:for each="value_for_group, row_group in row_groups">
         <h2 py:if="value_for_group">$value_for_group
         <span class="numrows" py:with="cnt = len(row_group)">(${cnt or 'No'} match${cnt != 1 and 'es' or ''})</span></h2>
@@ -145,7 +152,9 @@
           </tbody>
         </table>
       </py:for>
-
+      <py:if test="report.id != -1">
+        ${paging(paginator, chrome)}
+      </py:if>
       <div py:if="report.id == -1 and 'REPORT_CREATE' in perm(report.resource)" class="buttons">
         <form action="" method="get">
           <div>
Index: trac/ticket/templates/query_results.html
===================================================================
--- trac/ticket/templates/query_results.html	(.../vendor/trac/tags/0.11b1)	(revision 415)
+++ trac/ticket/templates/query_results.html	(.../patches/report-paginate-0.11)	(revision 415)
@@ -17,11 +17,17 @@
      xmlns:py="http://genshi.edgewall.org/"
      xmlns:xi="http://www.w3.org/2001/XInclude">
   <xi:include href="macros.html" />
-
+  <xi:include href="presentation.html" />
+  
   <py:def function="num_matches(v)">
     <span class="numrows">(${v or 'No'} match${v != 1 and 'es' or ''})</span>
   </py:def>
-
+  
+  <h2 py:if="paginator.has_more_pages">
+    Results
+    ${num_results(paginator)}
+  </h2>
+  ${paging(paginator, chrome)}
   <py:for each="groupname, results in groups">
     <h2 py:if="groupname">
       ${fields[query.group].label}: $groupname ${num_matches(len(results))}
@@ -88,5 +94,7 @@
       </tbody>
     </table>
   </py:for>
+  ${paging(paginator, chrome)}
 </div>
 
+
Index: trac/htdocs/css/trac.css
===================================================================
--- trac/htdocs/css/trac.css	(.../vendor/trac/tags/0.11b1)	(revision 415)
+++ trac/htdocs/css/trac.css	(.../patches/report-paginate-0.11)	(revision 415)
@@ -525,6 +525,34 @@
 #content.error #traceback td code { white-space: pre; }
 #content.error #traceback pre { font-size: 95%; }
 
+#content .paging { margin: 0 0 2em; padding: .5em 0 0;
+  font-size: 85%; line-height: 2em; text-align: center;
+}
+#content .paging .current { 
+  padding: .1em .3em;
+  border: 1px solid #333;
+  background: #999; color: #fff; 
+}
+
+#content .paging :link, #content .paging :visited {
+  padding: .1em .3em;
+  border: 1px solid #666;
+  background: transparent; color: #666;
+}
+#content .paging :link:hover, #content .paging :visited:hover {
+  background: #999; color: #fff;  border-color: #333;
+}
+#content .paging .previous a, 
+#content .paging .next a {
+  font-size: 150%; font-weight: bold; border: none;
+}
+#content .paging .previous a:hover,
+#content .paging .next a:hover {
+  background: transparent; color: #666;
+}
+
+#content h2 .numresults { color: #666; font-size: 90%; }
+
 /* Styles for search word highlighting */
 @media screen {
  .searchword0 { background: #ff9 }
Index: trac/htdocs/css/search.css
===================================================================
--- trac/htdocs/css/search.css	(.../vendor/trac/tags/0.11b1)	(revision 415)
+++ trac/htdocs/css/search.css	(.../patches/report-paginate-0.11)	(revision 415)
@@ -5,7 +5,6 @@
 #content.search hr { clear: left; margin-bottom: 0 }
 #content.search #notfound { margin: 2em; font-size: 110% }
 
-#content.search h2 .numresults { color: #666; font-size: 90%; }
 #content.search #results { margin-right: 3em }
 #content.search #results dt { margin: 1.5em 0 0 }
 #content.search #results dt a { color: #33c }
@@ -13,28 +12,3 @@
 #content.search #results .author, #results .date { color: #090; }
 
 #content.search #quickjump { font-style: italic; font-weight: bold; }
-
-#content.search .paging { margin: 0 0 2em; padding: .5em 0 0;
-  font-size: 85%; line-height: 2em; text-align: center;
-}
-#content.search .paging span .current { 
-  padding: .1em .3em;
-  border: 1px solid #333;
-  background: #999; color: #fff; 
-}
-#content.search .paging :link, #content.search .paging :visited {
-  padding: .1em .3em;
-  border: 1px solid #666;
-  background: transparent; color: #666;
-}
-#content.search .paging :link:hover, #content.search .paging :visited:hover {
-  background: #999; color: #fff;  border-color: #333;
-}
-#content.search .paging .previous a, 
-#content.search .paging .next a {
-  font-size: 150%; font-weight: bold; border: none;
-}
-#content.search .paging .previous a:hover,
-#content.search .paging .next a:hover {
-  background: transparent; color: #666;
-}
Index: trac/templates/presentation.html
===================================================================
--- trac/templates/presentation.html	(.../vendor/trac/tags/0.11b1)	(revision 0)
+++ trac/templates/presentation.html	(.../patches/report-paginate-0.11)	(revision 415)
@@ -0,0 +1,26 @@
+<div xmlns="http://www.w3.org/1999/xhtml"
+     xmlns:py="http://genshi.edgewall.org/" py:strip="True">
+
+  <div py:def="paging(paginator, chrome)" class="paging" py:if="paginator.has_more_pages">
+    <span py:if="paginator.has_previous_page"
+          py:with="prevlink = chrome.links.prev[0]" class="previous">
+      <a href="${prevlink.href}" title="${prevlink.title}">&larr;</a>
+    </span>
+    <py:for each="page in paginator.shown_pages">
+      <span py:if="page['string'] == paginator.current_page['string']">
+        <span py:if="page['string'] == paginator.current_page['string']" class="${paginator.current_page['class']}">${paginator.current_page['string']}</span>
+      </span>
+      <span py:if="page['string'] != paginator.current_page['string']">
+        <a href="${page['href']}" title="${page['title']}">${page['string']}</a>
+      </span>
+    </py:for>
+    <span py:if="paginator.has_next_page"
+          py:with="nextlink = chrome.links.next[0]" class="next">
+      <a href="${nextlink.href}" title="${nextlink.title}">&rarr;</a>
+    </span>
+  </div>
+
+  <span py:def="num_results(paginator)"  class="numresults">
+    (${paginator.span[0] + 1} - ${paginator.span[1]} of ${paginator.num_items})
+  </span>
+</div>

Property changes on: trac/templates/presentation.html
___________________________________________________________________
Name: svn:eol-style
   + native

Index: trac/search/web_ui.py
===================================================================
--- trac/search/web_ui.py	(.../vendor/trac/tags/0.11b1)	(revision 415)
+++ trac/search/web_ui.py	(.../patches/report-paginate-0.11)	(revision 415)
@@ -112,8 +112,17 @@
                 results[idx] = {'href': result[0], 'title': result[1],
                                 'date': format_datetime(result[2]),
                                 'author': result[3], 'excerpt': result[4]}
+            
+            pagedata = []    
             data['results'] = results
+            shown_pages = results.get_shown_pages(21)
+            for shown_page in shown_pages:
+                page_href = req.href.search(zip(filters, ['on'] * len(filters)), q=req.args.get('q'), page=shown_page, noquickjump=1)
+                pagedata.append([page_href, None, str(shown_page), 'page ' + str(shown_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}
+
             if results.has_next_page:
                 next_href = req.href.search(zip(filters, ['on'] * len(filters)),
                                             q=req.args.get('q'), page=page + 1,
Index: trac/search/templates/search.html
===================================================================
--- trac/search/templates/search.html	(.../vendor/trac/tags/0.11b1)	(revision 415)
+++ trac/search/templates/search.html	(.../patches/report-paginate-0.11)	(revision 415)
@@ -6,6 +6,7 @@
       xmlns:xi="http://www.w3.org/2001/XInclude">
   <xi:include href="layout.html" />
   <xi:include href="macros.html" />
+  <xi:include href="presentation.html" />
   <head>
     <title>Search<py:if test="query"> Results</py:if></title>
     <py:if test="results">
@@ -17,24 +18,6 @@
       $(document).ready(function() {$("#q").get(0).focus()});
     </script>
   </head>
-
-  <div py:def="paging" class="paging" py:if="results.has_more_pages">
-    <span py:if="results.has_previous_page"
-          py:with="prevlink = chrome.links.prev[0]" class="previous">
-      <a href="${prevlink.href}" title="${prevlink.title}">&larr;</a>
-    </span>
-    <span py:for="num in range(max(0, results.page - 10),
-                               min(results.page + 11, results.num_pages))"
-      py:choose="">
-      <span py:when="num == results.page" class="current">${num+1}</span>
-      <a py:otherwise="" href="${page_href}&amp;page=${num + 1}">${num + 1}</a>
-    </span>
-    <span py:if="results.has_next_page"
-          py:with="nextlink = chrome.links.next[0]" class="next">
-      <a href="${nextlink.href}" title="${nextlink.title}">&rarr;</a>
-    </span>
-  </div>
-
   <body>
     <div id="content" class="search">
 
@@ -57,11 +40,9 @@
       <py:if test="results or quickjump"><hr />
         <h2>
           Results
-          <span class="numresults">
-            (${results.span[0] + 1} - ${results.span[1]} of ${results.num_items})
-          </span>
+          ${num_results(results)}
         </h2>
-        ${paging()}
+        ${paging(results, chrome)}
         <div>
           <dl id="results">
             <py:if test="quickjump">
@@ -80,7 +61,7 @@
             </py:for>
           </dl>
         </div>
-        ${paging()}
+        ${paging(results, chrome)}
       </py:if>
 
       <div id="notfound" py:if="query and not (results or quickjump)">
Index: trac/util/presentation.py
===================================================================
--- trac/util/presentation.py	(.../vendor/trac/tags/0.11b1)	(revision 415)
+++ trac/util/presentation.py	(.../patches/report-paginate-0.11)	(revision 415)
@@ -19,7 +19,7 @@
 from math import ceil
 from itertools import izip, chain, repeat
 
-__all__ = ['classes', 'first_last', 'group', 'istext', 'paginate', 'Paginator']
+__all__ = ['classes', 'first_last', 'group', 'istext', 'prepared_paginate', 'paginate', 'Paginator']
 
 
 def classes(*args, **kwargs):
@@ -106,6 +106,12 @@
     from genshi.core import Markup
     return isinstance(text, basestring) and not isinstance(text, Markup)
 
+def prepared_paginate(items, num_items, max_per_page):
+    if max_per_page == 0:
+        num_pages = 1
+    else:
+        num_pages = int(ceil(float(num_items) / max_per_page))
+    return items, num_items, num_pages
 
 def paginate(items, page=0, max_per_page=10):
     """Simple generic pagination.
@@ -179,15 +185,18 @@
 
 class Paginator(object):
 
-    def __init__(self, items, page=0, max_per_page=10):
+    def __init__(self, items, page=0, max_per_page=10, num_items=None):
         if not page:
             page = 0
-        offset = page * max_per_page
+
+        if num_items is None:
+            items, num_items, num_pages = paginate(items, page, max_per_page)
+        else:
+            items, num_items, num_pages = prepared_paginate(items, num_items, max_per_page)
+
+        offset = page * max_per_page        
         self.page = page
         self.max_per_page = max_per_page
-
-        items, num_items, num_pages = paginate(items, page, max_per_page)
-
         self.items = items
         self.num_items = num_items
         self.num_pages = num_pages
@@ -216,7 +225,24 @@
     def has_previous_page(self):
         return self.page > 0
     has_previous_page = property(has_previous_page)
+   
+    def get_shown_pages(self, page_index_count = 11):
+        if self.has_more_pages == False:
+            return range(1, 2)
 
+        min_page = 1
+        max_page = int(ceil(float(self.num_items) / self.max_per_page))
+        current_page = self.page + 1
+        start_page = current_page - page_index_count / 2
+        end_page = current_page + page_index_count / 2 + (page_index_count % 2 - 1)
+
+        if start_page < min_page:
+            start_page = min_page
+        if end_page > max_page:
+            end_page = max_page
+
+        return range(start_page, end_page + 1)
+
 def separated(items, sep=','):
     """Yield `(item, sep)` tuples, one for each element in `items`.
 

