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 (
46 from django.template.base import TokenType
47 TOKEN_BLOCK = TokenType.BLOCK
51 from django.template.base import TOKEN_BLOCK
52 except ImportError: # Django < 1.8
53 from django.template import TOKEN_BLOCK
55 from django.template.loader import select_template
56 from django.utils.text import unescape_string_literal
58 # TODO, import this normally later on
59 from fnp_django_pagination.settings import *
62 def do_autopaginate(parser, token):
64 Splits the arguments to the autopaginate tag and formats them correctly.
68 autopaginate QUERYSET [PAGINATE_BY] [ORPHANS] [as NAME]
70 # Check whether there are any other autopaginations are later in this template
71 expr = lambda obj: (obj.token_type == TOKEN_BLOCK and \
72 len(obj.split_contents()) > 0 and obj.split_contents()[0] == "autopaginate")
73 multiple_paginations = len([tok for tok in parser.tokens if expr(tok)]) > 0
75 i = iter(token.split_contents())
83 assert word == "autopaginate"
84 queryset_var = next(i)
89 paginate_by = int(paginate_by)
96 orphans = int(orphans)
101 context_var = next(i)
102 except StopIteration:
104 if queryset_var is None:
105 raise TemplateSyntaxError(
106 "Invalid syntax. Proper usage of this tag is: "
107 "{% autopaginate QUERYSET [PAGINATE_BY] [ORPHANS]"
108 " [as CONTEXT_VAR_NAME] %}")
109 return AutoPaginateNode(queryset_var, multiple_paginations, paginate_by, orphans, context_var)
112 class AutoPaginateNode(Node):
114 Emits the required objects to allow for Digg-style pagination.
116 First, it looks in the current context for the variable specified, and using
117 that object, it emits a simple ``Paginator`` and the current page object
118 into the context names ``paginator`` and ``page_obj``, respectively.
120 It will then replace the variable specified with only the objects for the
125 It is recommended to use *{% paginate %}* after using the autopaginate
126 tag. If you choose not to use *{% paginate %}*, make sure to display the
127 list of available pages, or else the application may seem to be buggy.
129 def __init__(self, queryset_var, multiple_paginations, paginate_by=None,
130 orphans=None, context_var=None):
131 if paginate_by is None:
132 paginate_by = DEFAULT_PAGINATION
134 orphans = DEFAULT_ORPHANS
135 self.queryset_var = Variable(queryset_var)
136 if isinstance(paginate_by, int):
137 self.paginate_by = paginate_by
139 self.paginate_by = Variable(paginate_by)
140 if isinstance(orphans, int):
141 self.orphans = orphans
143 self.orphans = Variable(orphans)
144 self.context_var = context_var
145 self.multiple_paginations = multiple_paginations
147 def render(self, context):
148 # Save multiple_paginations state in context
149 if self.multiple_paginations and 'multiple_paginations' not in context:
150 context['multiple_paginations'] = True
152 if context.get('multiple_paginations') or getattr(context, "paginator", None):
153 page_suffix = '_%s' % self.queryset_var
157 key = self.queryset_var.var
158 value = self.queryset_var.resolve(context)
159 if isinstance(self.paginate_by, int):
160 paginate_by = self.paginate_by
162 paginate_by = self.paginate_by.resolve(context)
163 if isinstance(self.orphans, int):
164 orphans = self.orphans
166 orphans = self.orphans.resolve(context)
167 paginator = Paginator(value, paginate_by, orphans)
169 request = context['request']
171 raise ImproperlyConfigured(
172 "You need to enable 'django.core.context_processors.request'."
173 " See linaro-django-pagination/README file for TEMPLATE_CONTEXT_PROCESSORS details")
175 page_obj = paginator.page(request.page(page_suffix))
177 if INVALID_PAGE_RAISES_404:
178 raise Http404('Invalid page requested. If DEBUG were set to ' +
179 'False, an HTTP 404 page would have been shown instead.')
181 context['invalid_page'] = True
183 if self.context_var is not None:
184 context[self.context_var] = page_obj.object_list
186 context[key] = page_obj.object_list
187 context['paginator'] = paginator
188 context['page_obj'] = page_obj
189 context['page_suffix'] = page_suffix
193 class PaginateNode(Node):
195 def __init__(self, template=None):
196 self.template = template
198 def render(self, context):
199 template_list = ['pagination/pagination.html']
200 new_context = paginate(context)
202 template_list.insert(0, self.template)
203 return loader.render_to_string(template_list, new_context)
206 def do_paginate(parser, token):
208 Emits the pagination control for the most recent autopaginate list
212 paginate [using "TEMPLATE"]
214 Where TEMPLATE is a quoted template name. If missing the default template
215 is used (paginate/pagination.html).
217 argv = token.split_contents()
221 elif argc == 3 and argv[1] == 'using':
222 template = unescape_string_literal(argv[2])
224 raise TemplateSyntaxError(
225 "Invalid syntax. Proper usage of this tag is: "
226 "{% paginate [using \"TEMPLATE\"] %}")
227 return PaginateNode(template)
230 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
232 Renders the ``pagination/pagination.html`` template, resulting in a
233 Digg-like display of the available pages, given the current page. If there
234 are too many pages to be displayed before and after the current page, then
235 elipses will be used to indicate the undisplayed gap between page numbers.
237 Requires one argument, ``context``, which should be a dictionary-like data
238 structure and must contain the following keys:
241 A ``Paginator`` or ``QuerySetPaginator`` object.
244 This should be the result of calling the page method on the
245 aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
248 This same ``context`` dictionary-like data structure may also include:
251 A dictionary of all of the **GET** parameters in the current request.
252 This is useful to maintain certain types of state, even when requesting
255 Argument ``window`` is number to pages before/after current page. If window
256 exceeds pagination border (1 and end), window is moved to left or right.
258 Argument ``margin``` is number of pages on start/end of pagination.
260 window=2, margin=1, current=6 1 ... 4 5 [6] 7 8 ... 11
261 window=2, margin=0, current=1 [1] 2 3 4 5 ...
262 window=2, margin=0, current=5 ... 3 4 [5] 6 7 ...
263 window=2, margin=0, current=11 ... 7 8 9 10 [11]
267 raise ValueError('Parameter "window" cannot be less than zero')
269 raise ValueError('Parameter "margin" cannot be less than zero')
271 paginator = context['paginator']
272 page_obj = context['page_obj']
273 page_suffix = context.get('page_suffix', '')
274 page_range = list(paginator.page_range)
275 # Calculate the record range in the current page for display.
276 records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
277 records['last'] = records['first'] + paginator.per_page - 1
278 if records['last'] + paginator.orphans >= paginator.count:
279 records['last'] = paginator.count
282 window_start = page_obj.number - window - 1
283 window_end = page_obj.number + window
285 # solve if window exceeded page range
287 window_end = window_end - window_start
289 if window_end > paginator.num_pages:
290 window_start = max(0, window_start - (window_end - paginator.num_pages))
291 window_end = paginator.num_pages
292 pages = page_range[window_start:window_end]
294 # figure margin and add elipses
297 tmp_pages = set(pages)
298 tmp_pages = tmp_pages.union(page_range[:margin])
299 tmp_pages = tmp_pages.union(page_range[-margin:])
300 tmp_pages = list(tmp_pages)
303 pages.append(tmp_pages[0])
304 for i in range(1, len(tmp_pages)):
305 # figure gap size => add elipses or fill in gap
306 gap = tmp_pages[i] - tmp_pages[i - 1]
310 pages.append(tmp_pages[i] - 1)
311 pages.append(tmp_pages[i])
314 pages.insert(0, None)
315 if pages[-1] != paginator.num_pages:
319 'MEDIA_URL': settings.MEDIA_URL,
320 'STATIC_URL': getattr(settings, "STATIC_URL", None),
321 'disable_link_for_first_page': DISABLE_LINK_FOR_FIRST_PAGE,
322 'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
323 'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
324 'display_page_links': DISPLAY_PAGE_LINKS,
325 'is_paginated': paginator.count > paginator.per_page,
326 'next_link_decorator': NEXT_LINK_DECORATOR,
327 'page_obj': page_obj,
328 'page_suffix': page_suffix,
330 'paginator': paginator,
331 'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
334 if 'request' in context:
335 getvars = context['request'].GET.copy()
336 if 'page%s' % page_suffix in getvars:
337 del getvars['page%s' % page_suffix]
338 if len(getvars.keys()) > 0:
339 new_context['getvars'] = "&%s" % getvars.urlencode()
341 new_context['getvars'] = ''
343 except (KeyError, AttributeError):
348 register.tag('paginate', do_paginate)
349 register.tag('autopaginate', do_autopaginate)