From 598ff7844c5acbf477bbf28d63044730bc813640 Mon Sep 17 00:00:00 2001 From: floguy Date: Tue, 1 Jul 2008 06:40:52 +0000 Subject: [PATCH] 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 --- INSTALL.txt | 18 +++ README.txt | 59 +++++++ pagination/middleware.py | 4 + pagination/registration.py | 27 ---- pagination/templatetags/pagination_tags.py | 178 +++++++++++++-------- pagination/tests.py | 22 +++ 6 files changed, 218 insertions(+), 90 deletions(-) delete mode 100644 pagination/registration.py diff --git a/INSTALL.txt b/INSTALL.txt index 8b13789..b370b5d 100644 --- a/INSTALL.txt +++ b/INSTALL.txt @@ -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 diff --git a/README.txt b/README.txt index 8b13789..0834d6c 100644 --- a/README.txt +++ b/README.txt @@ -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 diff --git a/pagination/middleware.py b/pagination/middleware.py index 8a541fb..dd69377 100644 --- a/pagination/middleware.py +++ b/pagination/middleware.py @@ -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']) diff --git a/pagination/registration.py b/pagination/registration.py deleted file mode 100644 index 29edbbe..0000000 --- a/pagination/registration.py +++ /dev/null @@ -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 diff --git a/pagination/templatetags/pagination_tags.py b/pagination/templatetags/pagination_tags.py index 6db06ba..8a88815 100644 --- a/pagination/templatetags/pagination_tags.py +++ b/pagination/templatetags/pagination_tags.py @@ -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 diff --git a/pagination/tests.py b/pagination/tests.py index 21aa350..a1d23f0 100644 --- a/pagination/tests.py +++ b/pagination/tests.py @@ -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