4 from sets import Set as set
5 from django import template
6 from django.db.models.query import QuerySet
7 from django.core.paginator import Paginator, QuerySetPaginator, InvalidPage
9 register = template.Library()
11 DEFAULT_PAGINATION = 20
14 def do_autopaginate(parser, token):
16 Splits the arguments to the autopaginate tag and formats them correctly.
18 split = token.split_contents()
20 return AutoPaginateNode(split[1])
23 paginate_by = int(split[2])
25 raise template.TemplateSyntaxError(u'Got %s, but expected integer.' % split[2])
26 return AutoPaginateNode(split[1], paginate_by=paginate_by)
28 raise template.TemplateSyntaxError('%r tag takes one required argument and one optional argument' % split[0])
30 class AutoPaginateNode(template.Node):
32 Emits the required objects to allow for Digg-style pagination.
34 First, it looks in the current context for the variable specified. This
35 should be either a QuerySet or a list.
37 1. If it is a QuerySet, this ``AutoPaginateNode`` will emit a
38 ``QuerySetPaginator`` and the current page object into the context names
39 ``paginator`` and ``page_obj``, respectively.
41 2. If it is a list, this ``AutoPaginateNode`` will emit a simple
42 ``Paginator`` and the current page object into the context names
43 ``paginator`` and ``page_obj``, respectively.
45 It will then replace the variable specified with only the objects for the
50 It is recommended to use *{% paginate %}* after using the autopaginate
51 tag. If you choose not to use *{% paginate %}*, make sure to display the
52 list of availabale pages, or else the application may seem to be buggy.
54 def __init__(self, queryset_var, paginate_by=DEFAULT_PAGINATION):
55 self.queryset_var = template.Variable(queryset_var)
56 self.paginate_by = paginate_by
58 def render(self, context):
59 key = self.queryset_var.var
60 value = self.queryset_var.resolve(context)
61 if issubclass(value.__class__, QuerySet):
63 paginator_class = QuerySetPaginator
67 model = value[0].__class__
70 paginator_class = Paginator
71 paginator = paginator_class(value, self.paginate_by)
73 page_obj = paginator.page(context['request'].page)
76 context['invalid_page'] = True
78 context[key] = page_obj.object_list
79 context['paginator'] = paginator
80 context['page_obj'] = page_obj
83 def paginate(context, window=DEFAULT_WINDOW):
85 Renders the ``pagination/pagination.html`` template, resulting in a
86 Digg-like display of the available pages, given the current page. If there
87 are too many pages to be displayed before and after the current page, then
88 elipses will be used to indicate the undisplayed gap between page numbers.
90 Requires one argument, ``context``, which should be a dictionary-like data
91 structure and must contain the following keys:
94 A ``Paginator`` or ``QuerySetPaginator`` object.
97 This should be the result of calling the page method on the
98 aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
101 This same ``context`` dictionary-like data structure may also include:
104 A dictionary of all of the **GET** parameters in the current request.
105 This is useful to maintain certain types of state, even when requesting
109 paginator = context['paginator']
110 page_obj = context['page_obj']
111 page_range = paginator.page_range
112 # First and last are simply the first *n* pages and the last *n* pages,
113 # where *n* is the current window size.
114 first = set(page_range[:window])
115 last = set(page_range[-window:])
116 # Now we look around our current page, making sure that we don't wrap
118 current_start = page_obj.number-1-window
119 if current_start < 0:
121 current_end = page_obj.number-1+window
124 current = set(page_range[current_start:current_end])
126 # If there's no overlap between the first set of pages and the current
127 # set of pages, then there's a possible need for elusion.
128 if len(first.intersection(current)) == 0:
129 first_list = sorted(list(first))
130 second_list = sorted(list(current))
131 pages.extend(first_list)
132 diff = second_list[0] - first_list[-1]
133 # If there is a gap of two, between the last page of the first
134 # set and the first page of the current set, then we're missing a
137 pages.append(second_list[0] - 1)
138 # If the difference is just one, then there's nothing to be done,
139 # as the pages need no elusion and are correct.
142 # Otherwise, there's a bigger gap which needs to be signaled for
143 # elusion, by pushing a None value to the page list.
146 pages.extend(second_list)
148 pages.extend(sorted(list(first.union(current))))
149 # If there's no overlap between the current set of pages and the last
150 # set of pages, then there's a possible need for elusion.
151 if len(current.intersection(last)) == 0:
152 second_list = sorted(list(last))
153 diff = second_list[0] - pages[-1]
154 # If there is a gap of two, between the last page of the current
155 # set and the first page of the last set, then we're missing a
158 pages.append(second_list[0] - 1)
159 # If the difference is just one, then there's nothing to be done,
160 # as the pages need no elusion and are correct.
163 # Otherwise, there's a bigger gap which needs to be signaled for
164 # elusion, by pushing a None value to the page list.
167 pages.extend(second_list)
169 pages.extend(sorted(list(last.difference(current))))
172 'page_obj': page_obj,
173 'paginator': paginator,
174 'is_paginated': paginator.count > paginator.per_page,
176 if 'request' in context:
177 getvars = context['request'].GET.copy()
178 if 'page' in getvars:
180 if len(getvars.keys()) > 0:
181 to_return['getvars'] = "&%s" % getvars.urlencode()
183 to_return['getvars'] = ''
187 register.inclusion_tag('pagination/pagination.html', takes_context=True)(paginate)
188 register.tag('autopaginate', do_autopaginate)