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.conf import settings
33 from django.core.exceptions import ImproperlyConfigured
34 from django.core.paginator import Paginator, InvalidPage
35 from django.http import Http404
36 from django.template import (
45 from django.template.loader import select_template
46 from django.utils.text import unescape_string_literal
48 # TODO, import this normally later on
49 from linaro_django_pagination.settings import *
52 def do_autopaginate(parser, token):
54 Splits the arguments to the autopaginate tag and formats them correctly.
58 autopaginate QUERYSET [PAGINATE_BY] [ORPHANS] [as NAME]
60 # Check whether there are any other autopaginations are later in this template
61 expr = lambda obj: (obj.token_type == TOKEN_BLOCK and \
62 len(obj.split_contents()) > 0 and obj.split_contents()[0] == "autopaginate")
63 multiple_paginations = len([tok for tok in parser.tokens if expr(tok)]) > 0
65 i = iter(token.split_contents())
73 assert word == "autopaginate"
74 queryset_var = next(i)
79 paginate_by = int(paginate_by)
86 orphans = int(orphans)
94 if queryset_var is None:
95 raise TemplateSyntaxError(
96 "Invalid syntax. Proper usage of this tag is: "
97 "{% autopaginate QUERYSET [PAGINATE_BY] [ORPHANS]"
98 " [as CONTEXT_VAR_NAME] %}")
99 return AutoPaginateNode(queryset_var, multiple_paginations, paginate_by, orphans, context_var)
102 class AutoPaginateNode(Node):
104 Emits the required objects to allow for Digg-style pagination.
106 First, it looks in the current context for the variable specified, and using
107 that object, it emits a simple ``Paginator`` and the current page object
108 into the context names ``paginator`` and ``page_obj``, respectively.
110 It will then replace the variable specified with only the objects for the
115 It is recommended to use *{% paginate %}* after using the autopaginate
116 tag. If you choose not to use *{% paginate %}*, make sure to display the
117 list of available pages, or else the application may seem to be buggy.
119 def __init__(self, queryset_var, multiple_paginations, paginate_by=None,
120 orphans=None, context_var=None):
121 if paginate_by is None:
122 paginate_by = DEFAULT_PAGINATION
124 orphans = DEFAULT_ORPHANS
125 self.queryset_var = Variable(queryset_var)
126 if isinstance(paginate_by, int):
127 self.paginate_by = paginate_by
129 self.paginate_by = Variable(paginate_by)
130 if isinstance(orphans, int):
131 self.orphans = orphans
133 self.orphans = Variable(orphans)
134 self.context_var = context_var
135 self.multiple_paginations = multiple_paginations
137 def render(self, context):
138 if self.multiple_paginations or getattr(context, "paginator", None):
139 page_suffix = '_%s' % self.queryset_var
143 key = self.queryset_var.var
144 value = self.queryset_var.resolve(context)
145 if isinstance(self.paginate_by, int):
146 paginate_by = self.paginate_by
148 paginate_by = self.paginate_by.resolve(context)
149 if isinstance(self.orphans, int):
150 orphans = self.orphans
152 orphans = self.orphans.resolve(context)
153 paginator = Paginator(value, paginate_by, orphans)
155 request = context['request']
157 raise ImproperlyConfigured(
158 "You need to enable 'django.core.context_processors.request'."
159 " See linaro-django-pagination/README file for TEMPLATE_CONTEXT_PROCESSORS details")
161 page_obj = paginator.page(request.page(page_suffix))
163 if INVALID_PAGE_RAISES_404:
164 raise Http404('Invalid page requested. If DEBUG were set to ' +
165 'False, an HTTP 404 page would have been shown instead.')
167 context['invalid_page'] = True
169 if self.context_var is not None:
170 context[self.context_var] = page_obj.object_list
172 context[key] = page_obj.object_list
173 context['paginator'] = paginator
174 context['page_obj'] = page_obj
175 context['page_suffix'] = page_suffix
179 class PaginateNode(Node):
181 def __init__(self, template=None):
182 self.template = template
184 def render(self, context):
185 template_list = ['pagination/pagination.html']
186 new_context = paginate(context)
188 template_list.insert(0, self.template)
189 return loader.render_to_string(template_list, new_context,
190 context_instance = context)
194 def do_paginate(parser, token):
196 Emits the pagination control for the most recent autopaginate list
200 paginate [using "TEMPLATE"]
202 Where TEMPLATE is a quoted template name. If missing the default template
203 is used (paginate/pagination.html).
205 argv = token.split_contents()
209 elif argc == 3 and argv[1] == 'using':
210 template = unescape_string_literal(argv[2])
212 raise TemplateSyntaxError(
213 "Invalid syntax. Proper usage of this tag is: "
214 "{% paginate [using \"TEMPLATE\"] %}")
215 return PaginateNode(template)
218 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
220 Renders the ``pagination/pagination.html`` template, resulting in a
221 Digg-like display of the available pages, given the current page. If there
222 are too many pages to be displayed before and after the current page, then
223 elipses will be used to indicate the undisplayed gap between page numbers.
225 Requires one argument, ``context``, which should be a dictionary-like data
226 structure and must contain the following keys:
229 A ``Paginator`` or ``QuerySetPaginator`` object.
232 This should be the result of calling the page method on the
233 aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
236 This same ``context`` dictionary-like data structure may also include:
239 A dictionary of all of the **GET** parameters in the current request.
240 This is useful to maintain certain types of state, even when requesting
243 Argument ``window`` is number to pages before/after current page. If window
244 exceeds pagination border (1 and end), window is moved to left or right.
246 Argument ``margin``` is number of pages on start/end of pagination.
248 window=2, margin=1, current=6 1 ... 4 5 [6] 7 8 ... 11
249 window=2, margin=0, current=1 [1] 2 3 4 5 ...
250 window=2, margin=0, current=5 ... 3 4 [5] 6 7 ...
251 window=2, margin=0, current=11 ... 7 8 9 10 [11]
255 raise ValueError('Parameter "window" cannot be less than zero')
257 raise ValueError('Parameter "margin" cannot be less than zero')
259 paginator = context['paginator']
260 page_obj = context['page_obj']
261 page_suffix = context.get('page_suffix', '')
262 page_range = paginator.page_range
263 # Calculate the record range in the current page for display.
264 records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
265 records['last'] = records['first'] + paginator.per_page - 1
266 if records['last'] + paginator.orphans >= paginator.count:
267 records['last'] = paginator.count
270 window_start = page_obj.number - window - 1
271 window_end = page_obj.number + window
273 # solve if window exceeded page range
275 window_end = window_end - window_start
277 if window_end > paginator.num_pages:
278 window_start = window_start - (window_end - paginator.num_pages)
279 window_end = paginator.num_pages
280 pages = page_range[window_start:window_end]
282 # figure margin and add elipses
285 tmp_pages = set(pages)
286 tmp_pages = tmp_pages.union(page_range[:margin])
287 tmp_pages = tmp_pages.union(page_range[-margin:])
288 tmp_pages = list(tmp_pages)
291 pages.append(tmp_pages[0])
292 for i in range(1, len(tmp_pages)):
293 # figure gap size => add elipses or fill in gap
294 gap = tmp_pages[i] - tmp_pages[i - 1]
298 pages.append(tmp_pages[i] - 1)
299 pages.append(tmp_pages[i])
302 pages.insert(0, None)
303 if pages[-1] != paginator.num_pages:
307 'MEDIA_URL': settings.MEDIA_URL,
308 'STATIC_URL': getattr(settings, "STATIC_URL", None),
309 'disable_link_for_first_page': DISABLE_LINK_FOR_FIRST_PAGE,
310 'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
311 'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
312 'display_page_links': DISPLAY_PAGE_LINKS,
313 'is_paginated': paginator.count > paginator.per_page,
314 'next_link_decorator': NEXT_LINK_DECORATOR,
315 'page_obj': page_obj,
316 'page_suffix': page_suffix,
318 'paginator': paginator,
319 'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
322 if 'request' in context:
323 getvars = context['request'].GET.copy()
324 if 'page%s' % page_suffix in getvars:
325 del getvars['page%s' % page_suffix]
326 if len(getvars.keys()) > 0:
327 new_context['getvars'] = "&%s" % getvars.urlencode()
329 new_context['getvars'] = ''
331 except (KeyError, AttributeError):
338 register.tag('paginate', do_paginate)
339 register.tag('autopaginate', do_autopaginate)