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)
19 DISPLAY_PAGE_LINKS = getattr(settings, 'PAGINATION_DISPLAY_PAGE_LINKS', True)
20 PREVIOUS_LINK_DECORATOR = getattr(settings, 'PAGINATION_PREVIOUS_LINK_DECORATOR', "‹‹ ")
21 NEXT_LINK_DECORATOR = getattr(settings, 'PAGINATION_NEXT_LINK_DECORATOR', " ››")
22 DISPLAY_DISABLED_PREVIOUS_LINK = getattr(settings, 'PAGINATION_DISPLAY_DISABLED_PREVIOUS_LINK', False)
23 DISPLAY_DISABLED_NEXT_LINK = getattr(settings, 'PAGINATION_DISPLAY_DISABLED_NEXT_LINK', False)
25 def do_autopaginate(parser, token):
27 Splits the arguments to the autopaginate tag and formats them correctly.
30 # Check whether there are any other autopaginations are later in this template
31 expr = lambda obj: (obj.token_type == TOKEN_BLOCK and \
32 len(obj.split_contents()) > 0 and obj.split_contents()[0] == "autopaginate")
33 multiple_paginations = len(filter(expr, parser.tokens)) > 0
35 split = token.split_contents()
38 for i, bit in enumerate(split):
42 if as_index is not None:
44 context_var = split[as_index + 1]
46 raise template.TemplateSyntaxError("Context variable assignment " +
47 "must take the form of {%% %r object.example_set.all ... as " +
48 "context_var_name %%}" % split[0])
49 del split[as_index:as_index + 2]
51 return AutoPaginateNode(split[1], multiple_paginations=multiple_paginations)
53 return AutoPaginateNode(split[1], paginate_by=split[2],
54 context_var=context_var, multiple_paginations=multiple_paginations)
57 orphans = int(split[3])
59 raise template.TemplateSyntaxError(u'Got %s, but expected integer.'
61 return AutoPaginateNode(split[1], paginate_by=split[2], orphans=orphans,
62 context_var=context_var, multiple_paginations=multiple_paginations)
64 raise template.TemplateSyntaxError('%r tag takes one required ' +
65 'argument and one optional argument' % split[0])
67 class AutoPaginateNode(template.Node):
69 Emits the required objects to allow for Digg-style pagination.
71 First, it looks in the current context for the variable specified, and using
72 that object, it emits a simple ``Paginator`` and the current page object
73 into the context names ``paginator`` and ``page_obj``, respectively.
75 It will then replace the variable specified with only the objects for the
80 It is recommended to use *{% paginate %}* after using the autopaginate
81 tag. If you choose not to use *{% paginate %}*, make sure to display the
82 list of available pages, or else the application may seem to be buggy.
84 def __init__(self, queryset_var, multiple_paginations, paginate_by=DEFAULT_PAGINATION,
85 orphans=DEFAULT_ORPHANS, context_var=None):
86 self.queryset_var = template.Variable(queryset_var)
87 if isinstance(paginate_by, int):
88 self.paginate_by = paginate_by
90 self.paginate_by = template.Variable(paginate_by)
91 self.orphans = orphans
92 self.context_var = context_var
93 self.multiple_paginations = multiple_paginations
95 def render(self, context):
96 if self.multiple_paginations or context.has_key('paginator'):
97 page_suffix = '_%s' % self.queryset_var
101 key = self.queryset_var.var
102 value = self.queryset_var.resolve(context)
103 if isinstance(self.paginate_by, int):
104 paginate_by = self.paginate_by
106 paginate_by = self.paginate_by.resolve(context)
107 paginator = Paginator(value, paginate_by, self.orphans)
109 page_obj = paginator.page(context['request'].page(page_suffix))
111 if INVALID_PAGE_RAISES_404:
112 raise Http404('Invalid page requested. If DEBUG were set to ' +
113 'False, an HTTP 404 page would have been shown instead.')
115 context['invalid_page'] = True
117 if self.context_var is not None:
118 context[self.context_var] = page_obj.object_list
120 context[key] = page_obj.object_list
121 context['paginator'] = paginator
122 context['page_obj'] = page_obj
123 context['page_suffix'] = page_suffix
127 def paginate(context, window=DEFAULT_WINDOW, hashtag=''):
129 Renders the ``pagination/pagination.html`` template, resulting in a
130 Digg-like display of the available pages, given the current page. If there
131 are too many pages to be displayed before and after the current page, then
132 elipses will be used to indicate the undisplayed gap between page numbers.
134 Requires one argument, ``context``, which should be a dictionary-like data
135 structure and must contain the following keys:
138 A ``Paginator`` or ``QuerySetPaginator`` object.
141 This should be the result of calling the page method on the
142 aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
145 This same ``context`` dictionary-like data structure may also include:
148 A dictionary of all of the **GET** parameters in the current request.
149 This is useful to maintain certain types of state, even when requesting
152 ``pagination_template``
153 A custom template to include in place of the default ``pagination.html``
158 paginator = context['paginator']
159 page_obj = context['page_obj']
160 page_suffix = context.get('page_suffix', '')
161 page_range = paginator.page_range
162 pagination_template = context.get('pagination_template', 'pagination/default.html')
163 # Calculate the record range in the current page for display.
164 records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
165 records['last'] = records['first'] + paginator.per_page - 1
166 if records['last'] + paginator.orphans >= paginator.count:
167 records['last'] = paginator.count
168 # First and last are simply the first *n* pages and the last *n* pages,
169 # where *n* is the current window size.
170 first = set(page_range[:window])
171 last = set(page_range[-window:])
172 # Now we look around our current page, making sure that we don't wrap
174 current_start = page_obj.number-1-window
175 if current_start < 0:
177 current_end = page_obj.number-1+window
180 current = set(page_range[current_start:current_end])
182 # If there's no overlap between the first set of pages and the current
183 # set of pages, then there's a possible need for elusion.
184 if len(first.intersection(current)) == 0:
185 first_list = list(first)
187 second_list = list(current)
189 pages.extend(first_list)
190 diff = second_list[0] - first_list[-1]
191 # If there is a gap of two, between the last page of the first
192 # set and the first page of the current set, then we're missing a
195 pages.append(second_list[0] - 1)
196 # If the difference is just one, then there's nothing to be done,
197 # as the pages need no elusion and are correct.
200 # Otherwise, there's a bigger gap which needs to be signaled for
201 # elusion, by pushing a None value to the page list.
204 pages.extend(second_list)
206 unioned = list(first.union(current))
208 pages.extend(unioned)
209 # If there's no overlap between the current set of pages and the last
210 # set of pages, then there's a possible need for elusion.
211 if len(current.intersection(last)) == 0:
212 second_list = list(last)
214 diff = second_list[0] - pages[-1]
215 # If there is a gap of two, between the last page of the current
216 # set and the first page of the last set, then we're missing a
219 pages.append(second_list[0] - 1)
220 # If the difference is just one, then there's nothing to be done,
221 # as the pages need no elusion and are correct.
224 # Otherwise, there's a bigger gap which needs to be signaled for
225 # elusion, by pushing a None value to the page list.
228 pages.extend(second_list)
230 differenced = list(last.difference(current))
232 pages.extend(differenced)
234 'MEDIA_URL': settings.MEDIA_URL,
237 'page_obj': page_obj,
238 'paginator': paginator,
240 'is_paginated': paginator.count > paginator.per_page,
241 'page_suffix': page_suffix,
242 'display_page_links': DISPLAY_PAGE_LINKS,
243 'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
244 'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
245 'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
246 'next_link_decorator': NEXT_LINK_DECORATOR,
247 'pagination_template': pagination_template,
249 if 'request' in context:
250 getvars = context['request'].GET.copy()
251 if 'page%s' % page_suffix in getvars:
252 del getvars['page%s' % page_suffix]
253 if len(getvars.keys()) > 0:
254 to_return['getvars'] = "&%s" % getvars.urlencode()
256 to_return['getvars'] = ''
258 except KeyError, AttributeError:
261 register.inclusion_tag('pagination/pagination.html', takes_context=True)(
263 register.tag('autopaginate', do_autopaginate)