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.base import TokenType
46 TOKEN_BLOCK = TokenType.BLOCK
48 from django.template.loader import select_template
49 from django.utils.text import unescape_string_literal
51 # TODO, import this normally later on
52 from fnp_django_pagination.settings import *
55 def do_autopaginate(parser, token):
57 Splits the arguments to the autopaginate tag and formats them correctly.
61 autopaginate QUERYSET [PAGINATE_BY] [ORPHANS] [as NAME]
63 # Check whether there are any other autopaginations are later in this template
64 expr = lambda obj: (obj.token_type == TOKEN_BLOCK and \
65 len(obj.split_contents()) > 0 and obj.split_contents()[0] == "autopaginate")
66 multiple_paginations = len([tok for tok in parser.tokens if expr(tok)]) > 0
68 i = iter(token.split_contents())
76 assert word == "autopaginate"
77 queryset_var = next(i)
82 paginate_by = int(paginate_by)
89 orphans = int(orphans)
97 if queryset_var is None:
98 raise TemplateSyntaxError(
99 "Invalid syntax. Proper usage of this tag is: "
100 "{% autopaginate QUERYSET [PAGINATE_BY] [ORPHANS]"
101 " [as CONTEXT_VAR_NAME] %}")
102 return AutoPaginateNode(queryset_var, multiple_paginations, paginate_by, orphans, context_var)
105 class AutoPaginateNode(Node):
107 Emits the required objects to allow for Digg-style pagination.
109 First, it looks in the current context for the variable specified, and using
110 that object, it emits a simple ``Paginator`` and the current page object
111 into the context names ``paginator`` and ``page_obj``, respectively.
113 It will then replace the variable specified with only the objects for the
118 It is recommended to use *{% paginate %}* after using the autopaginate
119 tag. If you choose not to use *{% paginate %}*, make sure to display the
120 list of available pages, or else the application may seem to be buggy.
122 def __init__(self, queryset_var, multiple_paginations, paginate_by=None,
123 orphans=None, context_var=None):
124 if paginate_by is None:
125 paginate_by = DEFAULT_PAGINATION
127 orphans = DEFAULT_ORPHANS
128 self.queryset_var = Variable(queryset_var)
129 if isinstance(paginate_by, int):
130 self.paginate_by = paginate_by
132 self.paginate_by = Variable(paginate_by)
133 if isinstance(orphans, int):
134 self.orphans = orphans
136 self.orphans = Variable(orphans)
137 self.context_var = context_var
138 self.multiple_paginations = multiple_paginations
140 def render(self, context):
141 # Save multiple_paginations state in context
142 if self.multiple_paginations and 'multiple_paginations' not in context:
143 context['multiple_paginations'] = True
145 if context.get('page_suffix'):
146 page_suffix = context['page_suffix']
147 elif context.get('multiple_paginations') or getattr(context, "paginator", None):
148 page_suffix = '_%s' % self.queryset_var
152 key = self.queryset_var.var
153 value = self.queryset_var.resolve(context)
154 if isinstance(self.paginate_by, int):
155 paginate_by = self.paginate_by
157 paginate_by = self.paginate_by.resolve(context)
158 if isinstance(self.orphans, int):
159 orphans = self.orphans
161 orphans = self.orphans.resolve(context)
162 paginator = Paginator(value, paginate_by, orphans)
164 request = context['request']
166 raise ImproperlyConfigured(
167 "You need to enable 'django.core.context_processors.request'."
168 " See linaro-django-pagination/README file for TEMPLATE_CONTEXT_PROCESSORS details")
170 page_obj = paginator.page(request.page(page_suffix))
172 if INVALID_PAGE_RAISES_404:
173 raise Http404('Invalid page requested. If DEBUG were set to ' +
174 'False, an HTTP 404 page would have been shown instead.')
176 context['invalid_page'] = True
178 if self.context_var is not None:
179 context[self.context_var] = page_obj.object_list
181 context[key] = page_obj.object_list
182 context['paginator'] = paginator
183 context['page_obj'] = page_obj
184 context['page_suffix'] = page_suffix
188 class PaginateNode(Node):
190 def __init__(self, template=None, window=None, margin=None, ignored_vars=None):
191 self.template = template
194 self.ignored_vars = ignored_vars
196 def render(self, context):
197 template_list = ['pagination/pagination.html']
198 new_context = paginate(context, window=self.window, margin=self.margin, ignored_vars=ignored_vars)
200 template_list.insert(0, self.template)
201 return loader.render_to_string(template_list, new_context)
204 def do_paginate(parser, token):
206 Emits the pagination control for the most recent autopaginate list
210 paginate [using "TEMPLATE"] [window N] [margin N]
212 Where TEMPLATE is a quoted template name. If missing the default template
213 is used (paginate/pagination.html).
215 argv = token.split_contents()
218 window = DEFAULT_WINDOW
219 margin = DEFAULT_MARGIN
223 if argv[i] == 'using':
224 template = unescape_string_literal(argv[i + 1])
226 elif argv[i] == 'window':
229 elif argv[i] == 'margin':
232 elif argv[i] == 'ignore':
233 ignored_vars.append(argv[i + 1])
236 raise TemplateSyntaxError(
237 "Invalid syntax. Proper usage of this tag is: "
238 "{% paginate [using \"TEMPLATE\"] %}")
239 return PaginateNode(template, window, margin, ignored_vars)
242 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN, ignored_vars=None):
244 Renders the ``pagination/pagination.html`` template, resulting in a
245 Digg-like display of the available pages, given the current page. If there
246 are too many pages to be displayed before and after the current page, then
247 elipses will be used to indicate the undisplayed gap between page numbers.
249 Requires one argument, ``context``, which should be a dictionary-like data
250 structure and must contain the following keys:
253 A ``Paginator`` or ``QuerySetPaginator`` object.
256 This should be the result of calling the page method on the
257 aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
260 This same ``context`` dictionary-like data structure may also include:
263 A dictionary of all of the **GET** parameters in the current request.
264 This is useful to maintain certain types of state, even when requesting
267 Argument ``window`` is number to pages before/after current page. If window
268 exceeds pagination border (1 and end), window is moved to left or right.
270 Argument ``margin``` is number of pages on start/end of pagination.
272 window=2, margin=1, current=6 1 ... 4 5 [6] 7 8 ... 11
273 window=2, margin=0, current=1 [1] 2 3 4 5 ...
274 window=2, margin=0, current=5 ... 3 4 [5] 6 7 ...
275 window=2, margin=0, current=11 ... 7 8 9 10 [11]
281 window = Variable(window).resolve(context)
285 margin = Variable(margin).resolve(context)
288 raise ValueError('Parameter "window" cannot be less than zero')
290 raise ValueError('Parameter "margin" cannot be less than zero')
292 paginator = context['paginator']
293 page_obj = context['page_obj']
294 page_suffix = context.get('page_suffix', '')
295 page_range = list(paginator.page_range)
296 # Calculate the record range in the current page for display.
297 records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
298 records['last'] = records['first'] + paginator.per_page - 1
299 if records['last'] + paginator.orphans >= paginator.count:
300 records['last'] = paginator.count
303 window_start = page_obj.number - window - 1
304 window_end = page_obj.number + window
306 # solve if window exceeded page range
308 window_end = window_end - window_start
310 if window_end > paginator.num_pages:
311 window_start = max(0, window_start - (window_end - paginator.num_pages))
312 window_end = paginator.num_pages
313 pages = page_range[window_start:window_end]
315 # figure margin and add elipses
318 tmp_pages = set(pages)
319 tmp_pages = tmp_pages.union(page_range[:margin])
320 tmp_pages = tmp_pages.union(page_range[-margin:])
321 tmp_pages = list(tmp_pages)
324 pages.append(tmp_pages[0])
325 for i in range(1, len(tmp_pages)):
326 # figure gap size => add elipses or fill in gap
327 gap = tmp_pages[i] - tmp_pages[i - 1]
331 pages.append(tmp_pages[i] - 1)
332 pages.append(tmp_pages[i])
335 pages.insert(0, None)
336 if pages[-1] != paginator.num_pages:
340 'MEDIA_URL': settings.MEDIA_URL,
341 'STATIC_URL': getattr(settings, "STATIC_URL", None),
342 'disable_link_for_first_page': DISABLE_LINK_FOR_FIRST_PAGE,
343 'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
344 'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
345 'display_page_links': DISPLAY_PAGE_LINKS,
346 'is_paginated': paginator.count > paginator.per_page,
347 'next_link_decorator': NEXT_LINK_DECORATOR,
348 'page_obj': page_obj,
349 'page_suffix': page_suffix,
351 'paginator': paginator,
352 'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
355 if 'request' in context:
356 getvars = context['request'].GET.copy()
358 for v in ignored_vars:
361 if 'page%s' % page_suffix in getvars:
362 del getvars['page%s' % page_suffix]
363 if len(getvars.keys()) > 0:
364 new_context['getvars'] = "&%s" % getvars.urlencode()
366 new_context['getvars'] = ''
368 except (KeyError, AttributeError):
373 register.tag('paginate', do_paginate)
374 register.tag('autopaginate', do_autopaginate)