4 from sets import Set as set
6 from django import template
7 from django.http import Http404
8 from django.core.paginator import Paginator, InvalidPage
9 from django.conf import settings
11 register = template.Library()
13 DEFAULT_PAGINATION = getattr(settings, 'PAGINATION_DEFAULT_PAGINATION', 20)
14 DEFAULT_WINDOW = getattr(settings, 'PAGINATION_DEFAULT_WINDOW', 4)
15 DEFAULT_ORPHANS = getattr(settings, 'PAGINATION_DEFAULT_ORPHANS', 0)
16 INVALID_PAGE_RAISES_404 = getattr(settings,
17 'PAGINATION_INVALID_PAGE_RAISES_404', False)
19 def do_autopaginate(parser, token):
21 Splits the arguments to the autopaginate tag and formats them correctly.
23 split = token.split_contents()
26 for i, bit in enumerate(split):
30 if as_index is not None:
32 context_var = split[as_index + 1]
34 raise template.TemplateSyntaxError("Context variable assignment " +
35 "must take the form of {%% %r object.example_set.all ... as " +
36 "context_var_name %%}" % split[0])
37 del split[as_index:as_index + 2]
39 return AutoPaginateNode(split[1])
41 return AutoPaginateNode(split[1], paginate_by=split[2],
42 context_var=context_var)
45 orphans = int(split[3])
47 raise template.TemplateSyntaxError(u'Got %s, but expected integer.'
49 return AutoPaginateNode(split[1], paginate_by=split[2], orphans=orphans,
50 context_var=context_var)
52 raise template.TemplateSyntaxError('%r tag takes one required ' +
53 'argument and one optional argument' % split[0])
55 class AutoPaginateNode(template.Node):
57 Emits the required objects to allow for Digg-style pagination.
59 First, it looks in the current context for the variable specified, and using
60 that object, it emits a simple ``Paginator`` and the current page object
61 into the context names ``paginator`` and ``page_obj``, respectively.
63 It will then replace the variable specified with only the objects for the
68 It is recommended to use *{% paginate %}* after using the autopaginate
69 tag. If you choose not to use *{% paginate %}*, make sure to display the
70 list of available pages, or else the application may seem to be buggy.
72 def __init__(self, queryset_var, paginate_by=DEFAULT_PAGINATION,
73 orphans=DEFAULT_ORPHANS, context_var=None):
74 self.queryset_var = template.Variable(queryset_var)
75 if isinstance(paginate_by, int):
76 self.paginate_by = paginate_by
78 self.paginate_by = template.Variable(paginate_by)
79 self.orphans = orphans
80 self.context_var = context_var
82 def render(self, context):
83 key = self.queryset_var.var
84 value = self.queryset_var.resolve(context)
85 if isinstance(self.paginate_by, int):
86 paginate_by = self.paginate_by
88 paginate_by = self.paginate_by.resolve(context)
89 paginator = Paginator(value, paginate_by, self.orphans)
91 page_obj = paginator.page(context['request'].page)
93 if INVALID_PAGE_RAISES_404:
94 raise Http404('Invalid page requested. If DEBUG were set to ' +
95 'False, an HTTP 404 page would have been shown instead.')
97 context['invalid_page'] = True
99 if self.context_var is not None:
100 context[self.context_var] = page_obj.object_list
102 context[key] = page_obj.object_list
103 context['paginator'] = paginator
104 context['page_obj'] = page_obj
108 def paginate(context, window=DEFAULT_WINDOW, hashtag=''):
110 Renders the ``pagination/pagination.html`` template, resulting in a
111 Digg-like display of the available pages, given the current page. If there
112 are too many pages to be displayed before and after the current page, then
113 elipses will be used to indicate the undisplayed gap between page numbers.
115 Requires one argument, ``context``, which should be a dictionary-like data
116 structure and must contain the following keys:
119 A ``Paginator`` or ``QuerySetPaginator`` object.
122 This should be the result of calling the page method on the
123 aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
126 This same ``context`` dictionary-like data structure may also include:
129 A dictionary of all of the **GET** parameters in the current request.
130 This is useful to maintain certain types of state, even when requesting
134 paginator = context['paginator']
135 page_obj = context['page_obj']
136 page_range = paginator.page_range
137 # Calculate the record range in the current page for display.
138 records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
139 records['last'] = records['first'] + paginator.per_page - 1
140 if records['last'] + paginator.orphans >= paginator.count:
141 records['last'] = paginator.count
142 # First and last are simply the first *n* pages and the last *n* pages,
143 # where *n* is the current window size.
144 first = set(page_range[:window])
145 last = set(page_range[-window:])
146 # Now we look around our current page, making sure that we don't wrap
148 current_start = page_obj.number-1-window
149 if current_start < 0:
151 current_end = page_obj.number-1+window
154 current = set(page_range[current_start:current_end])
156 # If there's no overlap between the first set of pages and the current
157 # set of pages, then there's a possible need for elusion.
158 if len(first.intersection(current)) == 0:
159 first_list = list(first)
161 second_list = list(current)
163 pages.extend(first_list)
164 diff = second_list[0] - first_list[-1]
165 # If there is a gap of two, between the last page of the first
166 # set and the first page of the current set, then we're missing a
169 pages.append(second_list[0] - 1)
170 # If the difference is just one, then there's nothing to be done,
171 # as the pages need no elusion and are correct.
174 # Otherwise, there's a bigger gap which needs to be signaled for
175 # elusion, by pushing a None value to the page list.
178 pages.extend(second_list)
180 unioned = list(first.union(current))
182 pages.extend(unioned)
183 # If there's no overlap between the current set of pages and the last
184 # set of pages, then there's a possible need for elusion.
185 if len(current.intersection(last)) == 0:
186 second_list = list(last)
188 diff = second_list[0] - pages[-1]
189 # If there is a gap of two, between the last page of the current
190 # set and the first page of the last set, then we're missing a
193 pages.append(second_list[0] - 1)
194 # If the difference is just one, then there's nothing to be done,
195 # as the pages need no elusion and are correct.
198 # Otherwise, there's a bigger gap which needs to be signaled for
199 # elusion, by pushing a None value to the page list.
202 pages.extend(second_list)
204 differenced = list(last.difference(current))
206 pages.extend(differenced)
208 'MEDIA_URL': settings.MEDIA_URL,
211 'page_obj': page_obj,
212 'paginator': paginator,
214 'is_paginated': paginator.count > paginator.per_page,
216 if 'request' in context:
217 to_return['request'] = context['request']
218 getvars = context['request'].GET.copy()
219 if 'page' in getvars:
221 if len(getvars.keys()) > 0:
222 to_return['getvars'] = "&%s" % getvars.urlencode()
224 to_return['getvars'] = ''
226 except (KeyError, AttributeError):
229 register.inclusion_tag('pagination/pagination.html', takes_context=True)(
231 register.tag('autopaginate', do_autopaginate)