Index: tg/decorators.py
===================================================================
--- tg/decorators.py	(revision 6552)
+++ tg/decorators.py	(working copy)
@@ -22,7 +22,7 @@
 from webhelpers.paginate import Page
 from pylons import config, request, response
 from pylons.controllers.util import abort
-from pylons import tmpl_context as c
+from tg import tmpl_context
 from tg.util import partial
 from repoze.what.plugins.pylonshq import ActionProtector, ControllerProtector
 
@@ -30,6 +30,7 @@
 from tg.flash import flash
 #from tg.controllers import redirect
 
+
 class Decoration(object):
     """ Simple class to support 'simple registration' type decorators
     """
@@ -304,6 +305,7 @@
                 self.content_type, self.engine, self.template, self.exclude_names)
         return func
 
+
 def use_custom_format(controller, custom_format):
     """Use use_custom_format in a controller in order to change
     the active @expose decorator when available."""
@@ -315,6 +317,7 @@
 
     deco.render_custom_format = custom_format
 
+
 def override_template(controller, template):
     """Use overide_template in a controller in order to change the
     template that will be used to render the response dictionary
@@ -384,9 +387,8 @@
         return func
 
 
-def paginate(name, items_per_page=10, use_prefix=False):
-    """
-    Paginate a given collection.
+class paginate(object):
+    """Paginate a given collection.
 
     This decorator is mainly exposing the functionality
     of :func:`webhelpers.paginate`.
@@ -405,10 +407,8 @@
 
     To render the actual pager, use::
 
-      ${c.paginators.<name>.pager()}
+      ${tmpl_context.paginators.<name>.pager()}
 
-    where c is the tmpl_context.
-
     It is possible to have several :func:`paginate`-decorators for
     one controller action to paginate several collections independently
     from each other. If this is desired, don't forget to set the :attr:`use_prefix`-parameter
@@ -417,61 +417,75 @@
     :Parameters:
       name
         the collection to be paginated.
-      items_per_page
-        the number of items to be rendered. Defaults to 10
       use_prefix
         if True, the parameters the paginate
         decorator renders and reacts to are prefixed with
         "<name>_". This allows for multi-pagination.
+      items_per_page
+        the number of items to be rendered. Defaults to 10.
+      max_items_per_page
+        the maximum number of items allowed to be set via parameter.
+        Defaults to 0 (does not allow to change that value).
 
     """
-    prefix = ""
-    if use_prefix:
-        prefix = name + "_"
-    own_parameters = dict(
-        page="%spage" % prefix,
-        items_per_page="%sitems_per_page" % prefix
-        )
-    #@decorator
-    def _d(f):
-        def _w(*args, **kwargs):
-            page = int(kwargs.pop(own_parameters["page"], 1))
-            real_items_per_page = int(
-                    kwargs.pop(
-                            own_parameters['items_per_page'],
-                            items_per_page))
 
-            res = f(*args, **kwargs)
-            if isinstance(res, dict) and name in res:
-                additional_parameters = MultiDict()
-                for key, value in request.str_params.iteritems():
-                    if key not in own_parameters:
-                        additional_parameters.add(key, value)
+    def __init__(self, name, use_prefix=False,
+            items_per_page=10, max_items_per_page=0):
+        self.name = name
+        prefix = use_prefix and name + '_' or ''
+        self.page_param = prefix + 'page'
+        self.items_per_page_param = prefix + 'items_per_page'
+        self.items_per_page = items_per_page
+        self.max_items_per_page = max_items_per_page
 
-                collection = res[name]
-                page = Page(
-                    collection,
-                    page,
-                    items_per_page=real_items_per_page,
-                    **additional_parameters.dict_of_lists()
-                    )
-                # wrap the pager so that it will render
-                # the proper page-parameter
-                page.pager = partial(page.pager,
-                        page_param=own_parameters["page"])
-                res[name] = page
-                # this is a bit strange - it appears
-                # as if c returns an empty
-                # string for everything it dosen't know.
-                # I didn't find that documented, so I
-                # just put this in here and hope it works.
-                if not hasattr(c, 'paginators') or type(c.paginators) == str:
-                    c.paginators = Bunch()
-                c.paginators[name] = page
-            return res
-        return _w
-    return _d
+    def __call__(self, func):
+        decoration = Decoration.get_decoration(func)
+        decoration.register_hook('before_validate', self.before_validate)
+        decoration.register_hook('before_render', self.before_render)
+        return func
 
+    def before_validate(self, remainder, params):
+        page = params.pop(self.page_param, None)
+        if page:
+            try:
+                page = int(page)
+                if page < 1:
+                    raise ValueError
+            except ValueError:
+                page = 1
+        else:
+            page = 1
+        request.paginate_page = page or 1
+        items_per_page = params.pop(self.items_per_page_param, None)
+        if items_per_page:
+            try:
+                items_per_page = min(
+                    int(items_per_page), self.max_items_per_page)
+                if items_per_page < 1:
+                    raise ValueError
+            except ValueError:
+                items_per_page = self.items_per_page
+        else:
+            items_per_page = self.items_per_page
+        request.paginate_items_per_page = items_per_page
+        request.paginate_params = params.copy()
+        if items_per_page != self.items_per_page:
+            request.paginate_params[self.items_per_page_param] = items_per_page
+
+    def before_render(self, remainder, params, output):
+        if not isinstance(output, dict) or not self.name in output:
+            return
+        collection = output[self.name]
+        page = Page(collection, request.paginate_page,
+            request.paginate_items_per_page)
+        page.kwargs = request.paginate_params
+        if self.page_param != 'name':
+            page.pager = partial(page.pager, page_param=self.page_param)
+        if not getattr(tmpl_context, 'paginators', None):
+            tmpl_context.paginators = Bunch()
+        tmpl_context.paginators[self.name] = output[self.name] = page
+
+
 @decorator
 def postpone_commits(func, *args, **kwargs):
     """Turns sqlalchemy commits into flushes in the decorated method
@@ -488,6 +502,7 @@
     s.commit = old_commit
     return retval
 
+
 @decorator
 def without_trailing_slash(func, *args, **kwargs):
     """This decorator allows you to ensure that the URL does not end in "/"
@@ -512,6 +527,7 @@
         redirect(request.url[:-1])
     return func(*args, **kwargs)
 
+
 @decorator
 def with_trailing_slash(func, *args, **kwargs):
     """This decorator allows you to ensure that the URL ends in "/"
@@ -543,15 +559,15 @@
 class require(ActionProtector):
     """
     TurboGears-specific repoze.what-pylons action protector.
-    
+
     The default authorization denial handler of this protector will flash
     the message of the unmet predicate with ``warning`` or ``error`` as the
     flash status if the HTTP status code is 401 or 403, respectively.
-    
+
     See :class:`allow_only` for controller-wide authorization.
-    
+
     """
-    
+
     def default_denial_handler(self, reason):
         """Authorization denial handler for repoze.what-pylons protectors."""
         if response.status_int == 401:
@@ -566,19 +582,19 @@
 class allow_only(ControllerProtector):
     """
     TurboGears-specific repoze.what-pylons controller protector.
-    
+
     The default authorization denial handler of this protector will flash
     the message of the unmet predicate with ``warning`` or ``error`` as the
     flash status if the HTTP status code is 401 or 403, respectively, since
     by default the ``__before__`` method of the controller is decorated with
     :class:`require`.
-    
+
     If the controller class has the ``_failed_authorization`` *class method*,
     it will replace the default denial handler.
-    
+
     """
     protector = require
-    
+
     def __call__(self, cls, *args, **kwargs):
         if hasattr(cls, '_failed_authorization'):
             self.denial_handler = cls._failed_authorization

