1 # Copyright (c) 2008, Eric Florenzano
2 # Copyright (C) 2010, 2011 Linaro Limited
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are
9 # * Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 # * Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following
13 # disclaimer in the documentation and/or other materials provided
14 # with the distribution.
15 # * Neither the name of the author nor the names of other
16 # contributors may be used to endorse or promote products derived
17 # from this software without specific prior written permission.
19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 from django import template
33 from django.conf import settings
34 from django.core.exceptions import ImproperlyConfigured
35 from django.core.paginator import Paginator, InvalidPage
36 from django.http import Http404
37 from django.template import TOKEN_BLOCK
39 # TODO, import this normally later on
40 from linaro_django_pagination.settings import *
43 def do_autopaginate(parser, token):
45 Splits the arguments to the autopaginate tag and formats them correctly.
49 autopaginate QUERYSET [PAGINATE_BY] [ORPHANS] [as NAME]
51 # Check whether there are any other autopaginations are later in this template
52 expr = lambda obj: (obj.token_type == TOKEN_BLOCK and \
53 len(obj.split_contents()) > 0 and obj.split_contents()[0] == "autopaginate")
54 multiple_paginations = len(filter(expr, parser.tokens)) > 0
56 i = iter(token.split_contents())
64 assert word == "autopaginate"
65 queryset_var = i.next()
70 paginate_by = int(paginate_by)
77 orphans = int(orphans)
82 context_var = i.next()
85 if queryset_var is None:
86 raise template.TemplateSyntaxError(
87 "Invalid syntax. Proper usage of this tag is: "
88 "{%% autopaginate QUERYSET [PAGINATE_BY] [ORPHANS]"
89 " [as CONTEXT_VAR_NAME] %%}"
91 return AutoPaginateNode(queryset_var, multiple_paginations, paginate_by, orphans, context_var)
94 class AutoPaginateNode(template.Node):
96 Emits the required objects to allow for Digg-style pagination.
98 First, it looks in the current context for the variable specified, and using
99 that object, it emits a simple ``Paginator`` and the current page object
100 into the context names ``paginator`` and ``page_obj``, respectively.
102 It will then replace the variable specified with only the objects for the
107 It is recommended to use *{% paginate %}* after using the autopaginate
108 tag. If you choose not to use *{% paginate %}*, make sure to display the
109 list of available pages, or else the application may seem to be buggy.
111 def __init__(self, queryset_var, multiple_paginations, paginate_by=None,
112 orphans=None, context_var=None):
113 if paginate_by is None:
114 paginate_by = DEFAULT_PAGINATION
116 orphans = DEFAULT_ORPHANS
117 self.queryset_var = template.Variable(queryset_var)
118 if isinstance(paginate_by, int):
119 self.paginate_by = paginate_by
121 self.paginate_by = template.Variable(paginate_by)
122 if isinstance(orphans, int):
123 self.orphans = orphans
125 self.orphans = template.Variable(orphans)
126 self.context_var = context_var
127 self.multiple_paginations = multiple_paginations
129 def render(self, context):
130 if self.multiple_paginations or "paginator" in context:
131 page_suffix = '_%s' % self.queryset_var
135 key = self.queryset_var.var
136 value = self.queryset_var.resolve(context)
137 if isinstance(self.paginate_by, int):
138 paginate_by = self.paginate_by
140 paginate_by = self.paginate_by.resolve(context)
141 if isinstance(self.orphans, int):
142 orphans = self.orphans
144 orphans = self.orphans.resolve(context)
145 paginator = Paginator(value, paginate_by, orphans)
147 request = context['request']
149 raise ImproperlyConfigured(
150 "You need to enable 'django.core.context_processors.request'."
151 " See linaro-django-pagination/README file for TEMPLATE_CONTEXT_PROCESSORS details")
153 page_obj = paginator.page(request.page(page_suffix))
155 if INVALID_PAGE_RAISES_404:
156 raise Http404('Invalid page requested. If DEBUG were set to ' +
157 'False, an HTTP 404 page would have been shown instead.')
159 context['invalid_page'] = True
161 if self.context_var is not None:
162 context[self.context_var] = page_obj.object_list
164 context[key] = page_obj.object_list
165 context['paginator'] = paginator
166 context['page_obj'] = page_obj
167 context['page_suffix'] = page_suffix
171 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
173 Renders the ``pagination/pagination.html`` template, resulting in a
174 Digg-like display of the available pages, given the current page. If there
175 are too many pages to be displayed before and after the current page, then
176 elipses will be used to indicate the undisplayed gap between page numbers.
178 Requires one argument, ``context``, which should be a dictionary-like data
179 structure and must contain the following keys:
182 A ``Paginator`` or ``QuerySetPaginator`` object.
185 This should be the result of calling the page method on the
186 aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
189 This same ``context`` dictionary-like data structure may also include:
192 A dictionary of all of the **GET** parameters in the current request.
193 This is useful to maintain certain types of state, even when requesting
196 ``pagination_template``
197 A custom template to include in place of the default ``pagination/default.html``
200 Argument ``window`` is number to pages before/after current page. If window
201 exceeds pagination border (1 and end), window is moved to left or right.
203 Argument ``margin``` is number of pages on start/end of pagination.
205 window=2, margin=1, current=6 1 ... 4 5 [6] 7 8 ... 11
206 window=2, margin=0, current=1 [1] 2 3 4 5 ...
207 window=2, margin=0, current=5 ... 3 4 [5] 6 7 ...
208 window=2, margin=0, current=11 ... 7 8 9 10 [11]
212 raise ValueError('Parameter "window" cannot be less than zero')
214 raise ValueError('Parameter "margin" cannot be less than zero')
216 paginator = context['paginator']
217 page_obj = context['page_obj']
218 page_suffix = context.get('page_suffix', '')
219 page_range = paginator.page_range
220 pagination_template = context.get('pagination_template', 'pagination/default.html')
221 # Calculate the record range in the current page for display.
222 records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
223 records['last'] = records['first'] + paginator.per_page - 1
224 if records['last'] + paginator.orphans >= paginator.count:
225 records['last'] = paginator.count
228 window_start = page_obj.number - window - 1
229 window_end = page_obj.number + window
231 # solve if window exceeded page range
233 window_end = window_end - window_start
235 if window_end > paginator.num_pages:
236 window_start = window_start - (window_end - paginator.num_pages)
237 window_end = paginator.num_pages
238 pages = page_range[window_start:window_end]
240 # figure margin and add elipses
243 tmp_pages = set(pages)
244 tmp_pages = tmp_pages.union(page_range[:margin])
245 tmp_pages = tmp_pages.union(page_range[-margin:])
246 tmp_pages = list(tmp_pages)
249 pages.append(tmp_pages[0])
250 for i in range(1, len(tmp_pages)):
251 # figure gap size => add elipses or fill in gap
252 gap = tmp_pages[i] - tmp_pages[i - 1]
256 pages.append(tmp_pages[i] - 1)
257 pages.append(tmp_pages[i])
260 pages.insert(0, None)
261 if pages[-1] != paginator.num_pages:
265 'MEDIA_URL': settings.MEDIA_URL,
266 'STATIC_URL': getattr(settings, "STATIC_URL", None),
267 'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
268 'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
269 'display_page_links': DISPLAY_PAGE_LINKS,
270 'is_paginated': paginator.count > paginator.per_page,
271 'next_link_decorator': NEXT_LINK_DECORATOR,
272 'page_obj': page_obj,
273 'page_suffix': page_suffix,
275 'pagination_template': pagination_template,
276 'paginator': paginator,
277 'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
280 if 'request' in context:
281 getvars = context['request'].GET.copy()
282 if 'page%s' % page_suffix in getvars:
283 del getvars['page%s' % page_suffix]
284 if len(getvars.keys()) > 0:
285 to_return['getvars'] = "&%s" % getvars.urlencode()
287 to_return['getvars'] = ''
289 except (KeyError, AttributeError):
293 register = template.Library()
294 register.inclusion_tag(
295 'pagination/pagination.html', takes_context=True)(paginate)
296 register.tag('autopaginate', do_autopaginate)