Commits

Anonymous committed c900f35

New URL generator algorithm for Paginate.

Comments (0)

Files changed (6)

 
 * webhelpers.paginate:
 
+ - New URL generation algorithm for ``Page.pager()``. You can pass a callback
+   function to the constructor, or it will fall back to ``pylons.url.current``
+   or ``routes.url_for`` (in that order). It will raise ``NotImplementedError`` if
+   none of these are available.
  - Don't allow extra positional args in constructor. The implementation does
    nothing with them, so it shouldn't allow them.
 
 ------------
 
 WebHelpers does not have any install dependencies, but some functions depend
-on third-party libraries.
+on third-party libraries (Routes_, Pylons_, WebOb_, unidecode_) as specified in
+their documentation.
 
-Routes_
-
-    Version >= 1.7 must be installed and running in the current
-    web framework for:
-
-    - webhelpers.html.tags (required only for ``javascript_link()``,
-      ``stylesheet_link()``, or ``auto_discovery_link()`` functions).
-    - webhelpers.paginate
-    - the regression tests in the source distribution
-
-    Currently Pylons_, TurboGears_, and Aquarium_ support Routes.
-
-Pylons_
-
-    The helpers in ``webhelpers.pylonslib`` depend on Pylons context variables
-    (e.g., ``pylons.session``).  They can easily be reimplemented in another
-    web framework if desired.
 
 .. _Routes:  http://routes.groovie.org/
 .. _Pylons:  http://pylonshq.com/
-.. _TurboGears:  http://turbogears.org/
-.. _Aquarium:  http://aquarium.sourceforge.net/
+.. _WebOb:   http://pythonpaste.org/WebOb
+.. _unidecode:  http://python.org/pypi/Unidecode/

docs/whats_new.rst

 ++++++++++++++++
 
 **The following deprecated packages were removed: rails, commands, hinclude,
-htmlgen, paginate, and string24.** Most of the functionality of the rails
+htmlgen, pagination, and string24.** Most of the functionality of the rails
 helpers was replaced by new helpers in the ``date``, ``html``, ``misc``,
 ``number``, and ``text`` packages. Prototype and Scriptaculous are not
 replaced; WebHelpers no longer ships with Javascript libraries.  ``pagination``
 webhelpers.paginate
 +++++++++++++++++++
 
-``webhelpers.paginate`` has some enhancements for Javascript, works with all
-versions of SQLAlchemy 0.4 and higher, and has a presliced list option.
+``webhelpers.paginate`` has a new algorithm for generating URLs for page links,
+has some enhancements for Javascript, works with all versions of SQLAlchemy 0.4
+and higher, and has a presliced list option.
+
+On Pylons it will use ``pylons.url.current`` as the URL generator, or fall back
+to ``routes.url_for`` if that is not available. You can also pass a callback
+function to the constructor to implement a custom generator. If none of these
+are available, you'll get a ``NotImplementedError``. Previous versions of
+WebHelpers (through 1.0b5) used ``routes.url_for`` unconditionally, but that
+function is deprecated and is not supported in Pylons 1.x.
 
 webhelpers.pylonslib
 ++++++++++++++++++++
 
 ``webhelpers.html.grid`` and ``webhelpers.pylonslib.grid`` contain helpers to
 make an HTML table from a list of objects such as database records. It has
-a demo program and an optional stylesheet.  It's "experimental" because the API
-needs some changes and the docstrings aren't very clear. But it works.
-The next version will add support for more input types: a list of sequences, a
-list of dicts, or a single dict.
+a demo program and an optional stylesheet.  It's "experimental" because the
+docs aren't very clear and the API could maybe do with some changes.  But it works.
 
 ``webhelpers.pylonslib.minify`` contains versions of ``javascript_link()`` and
 ``stylesheet_link()`` that compress their files. It's experimental because

tests/test_paginate.py

 from routes import Mapper
 
 from webhelpers.paginate import Page
+from webhelpers.util import update_params
 
 
 def test_empty_list():
     assert page.pager() == ''
     assert page.pager(show_if_single_page=True) == '<span class="pager_curpage">1</span>'
 
+def my_url_generator(**kw):
+    return update_params("/content", **kw)
+
 def test_many_pages():
     """Test that 100 items fit on seven 15-item pages."""
-    # Create routes mapper so that webhelper can create URLs
-    # using webhelpers.url_for()
-    mapper = Mapper()
-    mapper.connect(':controller')
-
     items = range(100)
-    page = Page(items, page=0, items_per_page=15)
+    page = Page(items, page=0, items_per_page=15, url=my_url_generator)
     assert page.page == 1
     assert page.first_item == 1
     assert page.last_item == 15
             'wsgi.url_scheme': 'http'
             }
 
-    def setUp(self):
-        map = routes.Mapper()
-        map.connect('test')
-        map.connect(':controller/:action/:id')
-
-        self.routes_config = routes.request_config()
-        self.routes_config.mapper = map
-        self.routes_config.environ = self.test_environ()
-        assert self.routes_config.mapper_dict

webhelpers/paginate.py

     >> my_page.pager('$link_previous ~3~ $link_next (Page $page of $page_count)')
     1 2 [3] 4 5 6 .. 50 > (Page 3 of 50)
 
-Please see the documentation on *Page* and *Page.pager()*. There are many
+Please see the documentation on ``Page`` and ``Page.pager()``. There are many
 parameters that customize the Page's behavior.
 
+URL generator
+-------------
+
+The ``Page.pager()`` method requires a URL generator to create the links to the other
+pages. You can supply a callback function to the constructor, or let it
+fall back to ``pylons.url.current`` or ``routes.url_for`` (in that order). If
+none of these are available, you'll get a ``NotImplementedError``.
+
+Pylons applications can simply let it default to ``pylons.url.current``, which
+is available in Pylons 0.9.7 and later.
+
+Older versions of Paginate (up to 1.0b5) used ``routes.url_for`` in all cases.
+This caused an unnecessary dependency on Routes, and was untenable when
+``url_for`` was deprecated and Pylons 1.x no longer supported it. Nevertheless
+it remains for backward compatibility.
+
+To provide your own callback, create a function that takes a *page* argument
+and optional *partial* argument, and returns the URL to that page. Pass this
+function as the ``Page`` constructor's *url* argument.
+
+Note that the *page* and *partial* arguments may be called something else! You
+can rename these by specifying *page_param* and/or *partial_param* in
+``Page.pager()``. Just make sure to be consistent between the pager args and
+callback args.
+
+Also note that any extra keyword args passed to the ``Page`` constructor or
+``Page.pager()`` will be passed to the callback, so it should expect them.
+
+A typical callback will return the current page's URL, converting the keyword
+args to query parameters.  This is what ``pylons.url.current`` and
+``routes.url_for``, except that these also use keyword args to override path
+variables.
+
+The *partial* arg will be unspecified for a normal URL. It will have the value
+1 (int) for a partial URL.
+
+Examples::
+
+    # Example 1: explicitly use pylons.url.current
+    page = Page(MY_COLLECTION, url=pylons.url.current)
+
+    # Example 2: implicitly use pylons.url.current.
+    page = Page(MY_COLLECTION)
+
+    # Example 3: a dumb callback that uses string interpolation.
+    def get_page_url(page, partial=None):
+        url = "%s?page=%s" % (THE_URL, page)
+        if partial:
+            url += "&partial=1"
+        return url
+    page = Page(MY_COLLECTION, url=get_page_url) 
+
+    # Example 4: a smarter callback that uses ``update_params``, which converts
+    # keyword args to query parameters.
+    from webhelpers.tools import update_params
+    def get_page_url(**kw):
+        return update_params("/content", **kw)
+    page = Page(MY_COLLECTION, url=get_page_url)
+
+
+
 Can I use AJAX / AJAH?
 ------------------------
 
-Yes. See *partial_param* and *onclick* in *Page.pager()*.
+Yes. See *partial_param* and *onclick* in ``Page.pager()``.
 
 Notes
 -------
 
 This version of paginate is based on the code from
 http://workaround.org/cgi-bin/hg-paginate that is known at the
-"Paginate" module on PyPi.
+"Paginate" module on PyPI.
 """
 
 __version__ = '0.3.7'
     """
     def __init__(self, collection, page=1, items_per_page=20,
         item_count=None, sqlalchemy_session=None, presliced_list=False,
-        **kwargs):
+        url=None, **kwargs):
         """Create a "Page" instance.
 
         Parameters:
             Select objects do not have a database connection attached so it
             would not be able to execute the SELECT query.
 
+        url (optional)
+            A URL generator function. See module docstring for details.
+            This is used only by ``.pager()``.
+
         Further keyword arguments are used as link arguments in the pager().
         """
+        self._url_generator = url
+
         # 'page_nr' is deprecated.
         if 'page_nr' in kwargs:
             warnings.warn("'page_nr' is deprecated. Please use 'page' instead.")
 
         return literal(result)
 
+    def get_url_generator(self):
+        """Return a URL generator. See module docstring for details."""
+        if self._url_generator is None:
+            try:
+                import pylons
+                self._url_generator = pylons.url.current
+            except (ImportError, AttributeError):
+                try:
+                    import routes
+                    self._url_generator = routes.url_for
+                except (ImportError, AttributeError):
+                    raise NotImplementedError("no URL generator available")
+        return self._url_generator
+
+
+    #### Private methods ####
     def _range(self, regexp_match):
         """
         Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
 
         return self.separator.join(nav_items)
 
-    def _pagerlink(self, pagenr, text):
+    def _pagerlink(self, page, text):
         """
         Create a URL that links to another page using url_for().
 
         Parameters:
             
-        pagenr
+        page
             Number of the page that the link points to
 
         text
         # Let the url_for() from webhelpers create a new link and set
         # the variable called 'page_param'. Example:
         # You are in '/foo/bar' (controller='foo', action='bar')
-        # and you want to add a parameter 'pagenr'. Then you
-        # call the navigator method with page_param='pagenr' and
-        # the url_for() call will create a link '/foo/bar?pagenr=...'
+        # and you want to add a parameter 'page'. Then you
+        # call the navigator method with page_param='page' and
+        # the url_for() call will create a link '/foo/bar?page=...'
         # with the respective page number added.
         link_params = {}
         # Use the instance kwargs from Page.__init__ as URL parameters
         link_params.update(self.kwargs)
         # Add keyword arguments from pager() to the link as parameters
         link_params.update(self.pager_kwargs)
-        link_params[self.page_param] = pagenr
+        link_params[self.page_param] = page
 
-        # get the configuration for the current request
-        config = request_config()
-        # if the Mapper is configured with explicit=True we have to fetch
-        # the controller and action manually
-        if config.mapper.explicit:
-            if hasattr(config, 'mapper_dict'):
-                for k, v in config.mapper_dict.items():
-                    link_params[k] = v
+        # Get the URL generator
+        if self._url_generator is not None:
+            url_generator = self._url_generator
+        else:
+            try:
+                import pylons
+                url_generator = pylons.url.current
+            except (ImportError, AttributeError):
+                try:
+                    import routes
+                    url_generator = routes.url_for
+                    config = routes.request_config()
+                except (ImportError, AttributeError):
+                    raise NotImplementedError("no URL generator available")
+                else:
+                    # if the Mapper is configured with explicit=True we have to fetch
+                    # the controller and action manually
+                    if config.mapper.explicit:
+                        if hasattr(config, 'mapper_dict'):
+                            for k, v in config.mapper_dict.items():
+                                link_params[k] = v
 
         # Create the URL to load a certain page
-        link_url = url_for(**link_params)
-        # Create the URL to load the page area part of a certain page (AJAX updates)
-        link_params[self.partial_param] = 1
-        partial_url = url_for(**link_params)
+        link_url = url_generator(**link_params)
 
         if self.onclick: # create link with onclick action for AJAX
+            # Create the URL to load the page area part of a certain page (AJAX
+            # updates)
+            link_params[self.partial_param] = 1
+            partial_url = url_generator(**link_params)
             try: # if '%s' is used in the 'onclick' parameter (backwards compatibility)
                 onclick_action = self.onclick % (partial_url,)
             except TypeError:
                 onclick_action = Template(self.onclick).safe_substitute({
                   "partial_url": partial_url,
-                  "page": pagenr
+                  "page": page
                 })
             return HTML.a(text, href=link_url, onclick=onclick_action, **self.link_attr)
         else: # return static link
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.