4 from sets import Set as set
6 from django import template
7 from django.template import TOKEN_BLOCK
8 from django.http import Http404
9 from django.core.paginator import Paginator, InvalidPage
10 from django.conf import settings
12 register = template.Library()
14 DEFAULT_PAGINATION = getattr(settings, 'PAGINATION_DEFAULT_PAGINATION', 20)
15 DEFAULT_WINDOW = getattr(settings, 'PAGINATION_DEFAULT_WINDOW', 4)
16 DEFAULT_ORPHANS = getattr(settings, 'PAGINATION_DEFAULT_ORPHANS', 0)
17 INVALID_PAGE_RAISES_404 = getattr(settings,
18 'PAGINATION_INVALID_PAGE_RAISES_404', False)
20 def do_autopaginate(parser, token):
22 Splits the arguments to the autopaginate tag and formats them correctly.
25 # Check whether there are any other autopaginations are later in this template
26 expr = lambda obj: (obj.token_type == TOKEN_BLOCK and \
27 len(obj.split_contents()) > 0 and obj.split_contents()[0] == "autopaginate")
28 multiple_paginations = len(filter(expr, parser.tokens)) > 0
30 split = token.split_contents()
33 for i, bit in enumerate(split):
37 if as_index is not None:
39 context_var = split[as_index + 1]
41 raise template.TemplateSyntaxError("Context variable assignment " +
42 "must take the form of {%% %r object.example_set.all ... as " +
43 "context_var_name %%}" % split[0])
44 del split[as_index:as_index + 2]
46 return AutoPaginateNode(split[1], multiple_paginations=multiple_paginations)
48 return AutoPaginateNode(split[1], paginate_by=split[2],
49 context_var=context_var, multiple_paginations=multiple_paginations)
52 orphans = int(split[3])
54 raise template.TemplateSyntaxError(u'Got %s, but expected integer.'
56 return AutoPaginateNode(split[1], paginate_by=split[2], orphans=orphans,
57 context_var=context_var, multiple_paginations=multiple_paginations)
59 raise template.TemplateSyntaxError('%r tag takes one required ' +
60 'argument and one optional argument' % split[0])
62 class AutoPaginateNode(template.Node):
64 Emits the required objects to allow for Digg-style pagination.
66 First, it looks in the current context for the variable specified, and using
67 that object, it emits a simple ``Paginator`` and the current page object
68 into the context names ``paginator`` and ``page_obj``, respectively.
70 It will then replace the variable specified with only the objects for the
75 It is recommended to use *{% paginate %}* after using the autopaginate
76 tag. If you choose not to use *{% paginate %}*, make sure to display the
77 list of available pages, or else the application may seem to be buggy.
79 def __init__(self, queryset_var, multiple_paginations, paginate_by=DEFAULT_PAGINATION,
80 orphans=DEFAULT_ORPHANS, context_var=None):
81 self.queryset_var = template.Variable(queryset_var)
82 if isinstance(paginate_by, int):
83 self.paginate_by = paginate_by
85 self.paginate_by = template.Variable(paginate_by)
86 self.orphans = orphans
87 self.context_var = context_var
88 self.multiple_paginations = multiple_paginations
90 def render(self, context):
91 if self.multiple_paginations or context.has_key('paginator'):
92 page_suffix = '_%s' % self.queryset_var
96 key = self.queryset_var.var
97 value = self.queryset_var.resolve(context)
98 if isinstance(self.paginate_by, int):
99 paginate_by = self.paginate_by
101 paginate_by = self.paginate_by.resolve(context)
102 paginator = Paginator(value, paginate_by, self.orphans)
104 page_obj = paginator.page(context['request'].page(page_suffix))
106 if INVALID_PAGE_RAISES_404:
107 raise Http404('Invalid page requested. If DEBUG were set to ' +
108 'False, an HTTP 404 page would have been shown instead.')
110 context['invalid_page'] = True
112 if self.context_var is not None:
113 context[self.context_var] = page_obj.object_list
115 context[key] = page_obj.object_list
116 context['paginator'] = paginator
117 context['page_obj'] = page_obj
118 context['page_suffix'] = page_suffix
122 def paginate(context, window=DEFAULT_WINDOW, hashtag=''):
124 Renders the ``pagination/pagination.html`` template, resulting in a
125 Digg-like display of the available pages, given the current page. If there
126 are too many pages to be displayed before and after the current page, then
127 elipses will be used to indicate the undisplayed gap between page numbers.
129 Requires one argument, ``context``, which should be a dictionary-like data
130 structure and must contain the following keys:
133 A ``Paginator`` or ``QuerySetPaginator`` object.
136 This should be the result of calling the page method on the
137 aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
140 This same ``context`` dictionary-like data structure may also include:
143 A dictionary of all of the **GET** parameters in the current request.
144 This is useful to maintain certain types of state, even when requesting
148 paginator = context['paginator']
149 page_obj = context['page_obj']
150 page_suffix = context.get('page_suffix', '')
151 page_range = paginator.page_range
152 # Calculate the record range in the current page for display.
153 records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
154 records['last'] = records['first'] + paginator.per_page - 1
155 if records['last'] + paginator.orphans >= paginator.count:
156 records['last'] = paginator.count
157 # First and last are simply the first *n* pages and the last *n* pages,
158 # where *n* is the current window size.
159 first = set(page_range[:window])
160 last = set(page_range[-window:])
161 # Now we look around our current page, making sure that we don't wrap
163 current_start = page_obj.number-1-window
164 if current_start < 0:
166 current_end = page_obj.number-1+window
169 current = set(page_range[current_start:current_end])
171 # If there's no overlap between the first set of pages and the current
172 # set of pages, then there's a possible need for elusion.
173 if len(first.intersection(current)) == 0:
174 first_list = list(first)
176 second_list = list(current)
178 pages.extend(first_list)
179 diff = second_list[0] - first_list[-1]
180 # If there is a gap of two, between the last page of the first
181 # set and the first page of the current set, then we're missing a
184 pages.append(second_list[0] - 1)
185 # If the difference is just one, then there's nothing to be done,
186 # as the pages need no elusion and are correct.
189 # Otherwise, there's a bigger gap which needs to be signaled for
190 # elusion, by pushing a None value to the page list.
193 pages.extend(second_list)
195 unioned = list(first.union(current))
197 pages.extend(unioned)
198 # If there's no overlap between the current set of pages and the last
199 # set of pages, then there's a possible need for elusion.
200 if len(current.intersection(last)) == 0:
201 second_list = list(last)
203 diff = second_list[0] - pages[-1]
204 # If there is a gap of two, between the last page of the current
205 # set and the first page of the last set, then we're missing a
208 pages.append(second_list[0] - 1)
209 # If the difference is just one, then there's nothing to be done,
210 # as the pages need no elusion and are correct.
213 # Otherwise, there's a bigger gap which needs to be signaled for
214 # elusion, by pushing a None value to the page list.
217 pages.extend(second_list)
219 differenced = list(last.difference(current))
221 pages.extend(differenced)
223 'MEDIA_URL': settings.MEDIA_URL,
226 'page_obj': page_obj,
227 'paginator': paginator,
229 'is_paginated': paginator.count > paginator.per_page,
230 'page_suffix': page_suffix,
232 if 'request' in context:
233 getvars = context['request'].GET.copy()
234 if 'page%s' % page_suffix in getvars:
235 del getvars['page%s' % page_suffix]
236 if len(getvars.keys()) > 0:
237 to_return['getvars'] = "&%s" % getvars.urlencode()
239 to_return['getvars'] = ''
241 except KeyError, AttributeError:
244 register.inclusion_tag('pagination/pagination.html', takes_context=True)(
246 register.tag('autopaginate', do_autopaginate)