~netlandish/django-pagination

598ff7844c5acbf477bbf28d63044730bc813640 — floguy 16 years ago dcbff86
Updated documentation and reduced overall complexity by stripping away the registration, which nobody used anyway.

git-svn-id: https://django-pagination.googlecode.com/svn/trunk@15 7f1efe38-554e-0410-b69d-834cb44da2d5
6 files changed, 218 insertions(+), 90 deletions(-)

M INSTALL.txt
M README.txt
M pagination/middleware.py
D pagination/registration.py
M pagination/templatetags/pagination_tags.py
M pagination/tests.py
M INSTALL.txt => INSTALL.txt +18 -0
@@ 1,1 1,19 @@
Installing django-pagination
----------------------------

To install, first check out the latest version of the application from
subversion:

    svn co http://django-pagination.googlecode.com/svn/trunk django-pagination

Now, link the inner ``pagination`` project to your Python path:

    sudo ln -s `pwd`/pagination SITE_PACKAGES_DIR/pagination

If you don't know the location of your site packages directory, this hack might
do the trick for you:

    sudo ln -s `pwd`/pagination `python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()"`/pagination
    
Now it's installed.  Please see README.txt for information on how to use this
application in your projects.
\ No newline at end of file

M README.txt => README.txt +59 -0
@@ 1,1 1,60 @@
How to use django-pagination
----------------------------

``django-pagination`` allows for easy Digg-style pagination without modifying
your views.

There are really 5 steps to setting it up with your projects (not including 
installation, which is covered in INSTALL.txt in this same directory.)

1. List this application in the ``INSTALLED_APPS`` portion of your settings
   file.  Your settings file might look something like:
   
       INSTALLED_APPS = (
           # ...
           'pagination',
       )


2. Install the pagination middleware.  Your settings file might look something
   like:
   
       MIDDLEWARE_CLASSES = (
           # ...
           'pagination.middleware.PaginationMiddleware',
       )


3. Add this line at the top of your template to load the pagination tags:

       {% load pagination_tags %}


4. Decide on a variable that you would like to paginate, and use the
   autopaginate tag on that variable before iterating over it.  This could 
   take one of two forms (using the canonical ``object_list`` as an example
   variable):
   
       {% autopaginate object_list %}
       
   This assumes that you would like to have the default 20 results per page.
   If you would like to specify your own amount of results per page, you can
   specify that like so:
   
       {% autopaginate object_list 10 %}
   
   Note that this replaces ``object_list`` with the list for the current page, so
   you can iterate over the ``object_list`` like you normally would.
   

5. Now you want to display the current page and the available pages, so
   somewhere after having used autopaginate, use the paginate inclusion tag:
   
       {% paginate %}
   
   This does not take any arguments, but does assume that you have already
   called autopaginate, so make sure to do so first.


That's it!  You have now paginated ``object_list`` and given users of the site
a way to navigate between the different pages--all without touching your views.
\ No newline at end of file

M pagination/middleware.py => pagination/middleware.py +4 -0
@@ 1,4 1,8 @@
class PaginationMiddleware(object):
    """
    Inserts a variable representing the current page onto the request object if
    it exists in either **GET** or **POST** portions of the request.
    """
    def process_request(self, request):
        try:
            request.page = int(request['page'])

D pagination/registration.py => pagination/registration.py +0 -27
@@ 1,27 0,0 @@
from django.conf import settings

default_pagination = getattr(settings, 'DEFAULT_PAGINATION', 20)

class PaginationRegistrar(object):
    _registry = {}
    
    def register(self, model, pagination=None):
        self._registry[model] = pagination or default_pagination
    
    def unregister(self, model):
        try:
            del self._registry[model]
        except KeyError:
            return
    
    def get_for_model(self, model):
        if model in self._registry:
            return self._registry[model]
        return None

def get_registry():
    registry = getattr(settings, '_pagination_registry', None)
    if registry is None:
        registry = PaginationRegistrar()
        setattr(settings, '_pagination_registry', registry)
    return registry
\ No newline at end of file

M pagination/templatetags/pagination_tags.py => pagination/templatetags/pagination_tags.py +115 -63
@@ 3,83 3,116 @@ try:
except NameError:
    from sets import Set as set
from django import template
from pagination.registration import get_registry, default_pagination
registry = get_registry()
from django.db.models.query import QuerySet
from django.core.paginator import Paginator, QuerySetPaginator, InvalidPage
#from django.template.loader import render_to_string

register = template.Library()

DEFAULT_PAGINATION = 20
DEFAULT_WINDOW = 4

def do_autopaginate(parser, token):
    """
    Splits the arguments to the autopaginate tag and formats them correctly.
    """
    split = token.split_contents()
    if len(split) == 1:
        return AutoPaginateNode()
    elif len(split) == 2:
        return AutoPaginateNode(queryset_var=split[1])
    if len(split) == 2:
        return AutoPaginateNode(split[1])
    elif len(split) == 3:
        try:
            paginate_by = int(split[2])
        except ValueError:
            raise template.TemplateSyntaxError(u'Got %s, but expected integer.' % split[2])
        return AutoPaginateNode(split[1], paginate_by=paginate_by)
    else:
        raise template.TemplateSyntaxError('%r tag takes only one optional argument.' % split[0])
        raise template.TemplateSyntaxError('%r tag takes one required argument and one optional argument' % split[0])

class AutoPaginateNode(template.Node):
    def __init__(self, queryset_var=None):
        if queryset_var:
            self.queryset_var = template.Variable(queryset_var)
        else:
            self.queryset_var = None
    """
    Emits the required objects to allow for Digg-style pagination.
    
    First, it looks in the current context for the variable specified.  This
    should be either a QuerySet or a list.
    
    1. If it is a QuerySet, this ``AutoPaginateNode`` will emit a 
       ``QuerySetPaginator`` and the current page object into the context names
       ``paginator`` and ``page_obj``, respectively.
    
    2. If it is a list, this ``AutoPaginateNode`` will emit a simple
       ``Paginator`` and the current page object into the context names 
       ``paginator`` and ``page_obj``, respectively.
    
    It will then replace the variable specified with only the objects for the
    current page.
    
    .. note::
        
        It is recommended to use *{% paginate %}* after using the autopaginate
        tag.  If you choose not to use *{% paginate %}*, make sure to display the
        list of availabale pages, or else the application may seem to be buggy.
    """
    def __init__(self, queryset_var, paginate_by=DEFAULT_PAGINATION):
        self.queryset_var = template.Variable(queryset_var)
        self.paginate_by = paginate_by

    def render(self, context):
        if self.queryset_var is not None:
        key = self.queryset_var.var
        value = self.queryset_var.resolve(context)
        if issubclass(value.__class__, QuerySet):
            model = value.model
            paginator_class = QuerySetPaginator
        else:
            value = list(value)
            try:
                key = self.queryset_var.var
                value = self.queryset_var.resolve(context)
                if issubclass(value.__class__, QuerySet):
                    model = value.model
                    paginator_class = QuerySetPaginator
                else:
                    value = list(value)
                    try:
                        model = value[0].__class__
                    except IndexError:
                        return u''
                    paginator_class = Paginator
                pagination = registry.get_for_model(model)
                if pagination is None:
                    pagination = default_pagination
                paginator = paginator_class(value, pagination)
                try:
                    page_obj = paginator.page(context['request'].page)
                except:
                    return u''
                context[key] = page_obj.object_list
                context['paginator'] = paginator
                context['page_obj'] = page_obj
                model = value[0].__class__
            except IndexError:
                return u''
            except template.VariableDoesNotExist:
                pass
        for d in context:
            for key, value in d.iteritems():
                if issubclass(value.__class__, QuerySet):
                    model = value.model
                    pagination = registry.get_for_model(model)
                    if pagination is not None:
                        paginator = QuerySetPaginator(value, pagination)
                        try:
                            page_obj = paginator.page(context['request'].page)
                        except:
                            return u''
                        context[key] = page_obj.object_list
                        context['paginator'] = paginator
                        context['page_obj'] = page_obj
                        return u''
            paginator_class = Paginator
        paginator = paginator_class(value, self.paginate_by)
        try:
            page_obj = paginator.page(context['request'].page)
        except:
            return u''
        context[key] = page_obj.object_list
        context['paginator'] = paginator
        context['page_obj'] = page_obj
        return u''

def paginate(context, window=4):
def paginate(context, window=DEFAULT_WINDOW):
    """
    Renders the ``pagination/pagination.html`` template, resulting in a
    Digg-like display of the available pages, given the current page.  If there
    are too many pages to be displayed before and after the current page, then
    elipses will be used to indicate the undisplayed gap between page numbers.
    
    Requires one argument, ``context``, which should be a dictionary-like data
    structure and must contain the following keys:
    
    ``paginator``
        A ``Paginator`` or ``QuerySetPaginator`` object.
    
    ``page_obj``
        This should be the result of calling the page method on the 
        aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
        the current page.
    
    This same ``context`` dictionary-like data structure may also include:
    
    ``getvars``
        A dictionary of all of the **GET** parameters in the current request.
        This is useful to maintain certain types of state, even when requesting
        a different page.
        """
    try:
        paginator = context['paginator']
        page_obj = context['page_obj']
        page_range = paginator.page_range
        # First and last are simply the first *n* pages and the last *n* pages,
        # where *n* is the current window size.
        first = set(page_range[:window])
        last = set(page_range[-window:])
        # Now we look around our current page, making sure that we don't wrap
        # around.
        current_start = page_obj.number-1-window
        if current_start < 0:
            current_start = 0


@@ 88,44 121,63 @@ def paginate(context, window=4):
            current_end = 0
        current = set(page_range[current_start:current_end])
        pages = []
        # If there's no overlap between the first set of pages and the current
        # set of pages, then there's a possible need for elusion.
        if len(first.intersection(current)) == 0:
            first_list = sorted(list(first))
            second_list = sorted(list(current))
            pages.extend(first_list)
            diff = second_list[0] - first_list[-1] 
            diff = second_list[0] - first_list[-1]
            # If there is a gap of two, between the last page of the first
            # set and the first page of the current set, then we're missing a
            # page.
            if diff == 2:
                pages.append(second_list[0] - 1)
            # If the difference is just one, then there's nothing to be done,
            # as the pages need no elusion and are correct.
            elif diff == 1:
                pass
            # Otherwise, there's a bigger gap which needs to be signaled for
            # elusion, by pushing a None value to the page list.
            else:
                pages.append(None)
            pages.extend(second_list)
        else:
            pages.extend(sorted(list(first.union(current))))
        # If there's no overlap between the current set of pages and the last
        # set of pages, then there's a possible need for elusion.
        if len(current.intersection(last)) == 0:
            second_list = sorted(list(last))
            diff = second_list[0] - pages[-1]
            # If there is a gap of two, between the last page of the current
            # set and the first page of the last set, then we're missing a 
            # page.
            if diff == 2:
                pages.append(second_list[0] - 1)
            # If the difference is just one, then there's nothing to be done,
            # as the pages need no elusion and are correct.
            elif diff == 1:
                pass
            # Otherwise, there's a bigger gap which needs to be signaled for
            # elusion, by pushing a None value to the page list.
            else:
                pages.append(None)
            pages.extend(second_list)
        else:
            pages.extend(sorted(list(last.difference(current))))
        
        getvars = context['request'].GET.copy()
        if 'page' in getvars:
            del getvars['page']
        return {
        to_return = {
            'pages': pages,
            'page_obj': page_obj,
            'paginator': paginator,
            'is_paginated': paginator.count > paginator.per_page,
            'getvars': "&%s" % getvars.urlencode()
        }
        if 'request' in context:
            getvars = context['request'].GET.copy()
            if 'page' in getvars:
                del getvars['page']
            to_return['getvars'] = "&%s" % getvars.urlencode()
        return to_return
    except KeyError:
        return u''
        return {}
register.inclusion_tag('pagination/pagination.html', takes_context=True)(paginate)
register.tag('autopaginate', do_autopaginate)
\ No newline at end of file

M pagination/tests.py => pagination/tests.py +22 -0
@@ 1,6 1,7 @@
"""
>>> from django.core.paginator import Paginator
>>> from pagination.templatetags.pagination_tags import paginate
>>> from django.template import Template, Context

>>> p = Paginator(range(15), 2)
>>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']


@@ 17,4 18,25 @@
>>> p = Paginator(range(21), 2)
>>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
[1, 2, 3, 4, None, 8, 9, 10, 11]

>>> t = Template("{% load pagination_tags %}{% autopaginate var 2 %}{% paginate %}")

# WARNING: Please, please nobody read this portion of the code!
>>> class GetProxy(object):
...     def __iter__(self): yield self.__dict__.__iter__
...     def copy(self): return self
...     def urlencode(self): return u''
>>> class RequestProxy(object):
...     page = 1
...     GET = GetProxy()
>>>
# ENDWARNING

>>> t.render(Context({'var': range(21), 'request': RequestProxy()}))
u'\\n<div class="pagination">...
>>>
>>> t = Template("{% load pagination_tags %}{% autopaginate var %}{% paginate %}")
>>> t.render(Context({'var': range(21), 'request': RequestProxy()}))
u'\\n<div class="pagination">...
>>>
"""
\ No newline at end of file