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 TOKEN_BLOCK
47 except ImportError: # Django < 1.8
48 from django.template import TOKEN_BLOCK
50 from django.template.loader import select_template
51 from django.utils.text import unescape_string_literal
53 # TODO, import this normally later on
54 from linaro_django_pagination.settings import *
57 def do_autopaginate(parser, token):
59 Splits the arguments to the autopaginate tag and formats them correctly.
63 autopaginate QUERYSET [PAGINATE_BY] [ORPHANS] [as NAME]
65 # Check whether there are any other autopaginations are later in this template
66 expr = lambda obj: (obj.token_type == TOKEN_BLOCK and \
67 len(obj.split_contents()) > 0 and obj.split_contents()[0] == "autopaginate")
68 multiple_paginations = len([tok for tok in parser.tokens if expr(tok)]) > 0
70 i = iter(token.split_contents())
78 assert word == "autopaginate"
79 queryset_var = next(i)
84 paginate_by = int(paginate_by)
91 orphans = int(orphans)
99 if queryset_var is None:
100 raise TemplateSyntaxError(
101 "Invalid syntax. Proper usage of this tag is: "
102 "{% autopaginate QUERYSET [PAGINATE_BY] [ORPHANS]"
103 " [as CONTEXT_VAR_NAME] %}")
104 return AutoPaginateNode(queryset_var, multiple_paginations, paginate_by, orphans, context_var)
107 class AutoPaginateNode(Node):
109 Emits the required objects to allow for Digg-style pagination.
111 First, it looks in the current context for the variable specified, and using
112 that object, it emits a simple ``Paginator`` and the current page object
113 into the context names ``paginator`` and ``page_obj``, respectively.
115 It will then replace the variable specified with only the objects for the
120 It is recommended to use *{% paginate %}* after using the autopaginate
121 tag. If you choose not to use *{% paginate %}*, make sure to display the
122 list of available pages, or else the application may seem to be buggy.
124 def __init__(self, queryset_var, multiple_paginations, paginate_by=None,
125 orphans=None, context_var=None):
126 if paginate_by is None:
127 paginate_by = DEFAULT_PAGINATION
129 orphans = DEFAULT_ORPHANS
130 self.queryset_var = Variable(queryset_var)
131 if isinstance(paginate_by, int):
132 self.paginate_by = paginate_by
134 self.paginate_by = Variable(paginate_by)
135 if isinstance(orphans, int):
136 self.orphans = orphans
138 self.orphans = Variable(orphans)
139 self.context_var = context_var
140 self.multiple_paginations = multiple_paginations
142 def render(self, context):
143 if self.multiple_paginations or getattr(context, "paginator", None):
144 page_suffix = '_%s' % self.queryset_var
148 key = self.queryset_var.var
149 value = self.queryset_var.resolve(context)
150 if isinstance(self.paginate_by, int):
151 paginate_by = self.paginate_by
153 paginate_by = self.paginate_by.resolve(context)
154 if isinstance(self.orphans, int):
155 orphans = self.orphans
157 orphans = self.orphans.resolve(context)
158 paginator = Paginator(value, paginate_by, orphans)
160 request = context['request']
162 raise ImproperlyConfigured(
163 "You need to enable 'django.core.context_processors.request'."
164 " See linaro-django-pagination/README file for TEMPLATE_CONTEXT_PROCESSORS details")
166 page_obj = paginator.page(request.page(page_suffix))
168 if INVALID_PAGE_RAISES_404:
169 raise Http404('Invalid page requested. If DEBUG were set to ' +
170 'False, an HTTP 404 page would have been shown instead.')
172 context['invalid_page'] = True
174 if self.context_var is not None:
175 context[self.context_var] = page_obj.object_list
177 context[key] = page_obj.object_list
178 context['paginator'] = paginator
179 context['page_obj'] = page_obj
180 context['page_suffix'] = page_suffix
184 class PaginateNode(Node):
186 def __init__(self, template=None):
187 self.template = template
189 def render(self, context):
190 template_list = ['pagination/pagination.html']
191 new_context = paginate(context)
193 template_list.insert(0, self.template)
194 return loader.render_to_string(template_list, new_context,
195 context_instance = context)
199 def do_paginate(parser, token):
201 Emits the pagination control for the most recent autopaginate list
205 paginate [using "TEMPLATE"]
207 Where TEMPLATE is a quoted template name. If missing the default template
208 is used (paginate/pagination.html).
210 argv = token.split_contents()
214 elif argc == 3 and argv[1] == 'using':
215 template = unescape_string_literal(argv[2])
217 raise TemplateSyntaxError(
218 "Invalid syntax. Proper usage of this tag is: "
219 "{% paginate [using \"TEMPLATE\"] %}")
220 return PaginateNode(template)
223 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
225 Renders the ``pagination/pagination.html`` template, resulting in a
226 Digg-like display of the available pages, given the current page. If there
227 are too many pages to be displayed before and after the current page, then
228 elipses will be used to indicate the undisplayed gap between page numbers.
230 Requires one argument, ``context``, which should be a dictionary-like data
231 structure and must contain the following keys:
234 A ``Paginator`` or ``QuerySetPaginator`` object.
237 This should be the result of calling the page method on the
238 aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
241 This same ``context`` dictionary-like data structure may also include:
244 A dictionary of all of the **GET** parameters in the current request.
245 This is useful to maintain certain types of state, even when requesting
248 Argument ``window`` is number to pages before/after current page. If window
249 exceeds pagination border (1 and end), window is moved to left or right.
251 Argument ``margin``` is number of pages on start/end of pagination.
253 window=2, margin=1, current=6 1 ... 4 5 [6] 7 8 ... 11
254 window=2, margin=0, current=1 [1] 2 3 4 5 ...
255 window=2, margin=0, current=5 ... 3 4 [5] 6 7 ...
256 window=2, margin=0, current=11 ... 7 8 9 10 [11]
260 raise ValueError('Parameter "window" cannot be less than zero')
262 raise ValueError('Parameter "margin" cannot be less than zero')
264 paginator = context['paginator']
265 page_obj = context['page_obj']
266 page_suffix = context.get('page_suffix', '')
267 page_range = paginator.page_range
268 # Calculate the record range in the current page for display.
269 records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
270 records['last'] = records['first'] + paginator.per_page - 1
271 if records['last'] + paginator.orphans >= paginator.count:
272 records['last'] = paginator.count
275 window_start = page_obj.number - window - 1
276 window_end = page_obj.number + window
278 # solve if window exceeded page range
280 window_end = window_end - window_start
282 if window_end > paginator.num_pages:
283 window_start = window_start - (window_end - paginator.num_pages)
284 window_end = paginator.num_pages
285 pages = page_range[window_start:window_end]
287 # figure margin and add elipses
290 tmp_pages = set(pages)
291 tmp_pages = tmp_pages.union(page_range[:margin])
292 tmp_pages = tmp_pages.union(page_range[-margin:])
293 tmp_pages = list(tmp_pages)
296 pages.append(tmp_pages[0])
297 for i in range(1, len(tmp_pages)):
298 # figure gap size => add elipses or fill in gap
299 gap = tmp_pages[i] - tmp_pages[i - 1]
303 pages.append(tmp_pages[i] - 1)
304 pages.append(tmp_pages[i])
307 pages.insert(0, None)
308 if pages[-1] != paginator.num_pages:
312 'MEDIA_URL': settings.MEDIA_URL,
313 'STATIC_URL': getattr(settings, "STATIC_URL", None),
314 'disable_link_for_first_page': DISABLE_LINK_FOR_FIRST_PAGE,
315 'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
316 'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
317 'display_page_links': DISPLAY_PAGE_LINKS,
318 'is_paginated': paginator.count > paginator.per_page,
319 'next_link_decorator': NEXT_LINK_DECORATOR,
320 'page_obj': page_obj,
321 'page_suffix': page_suffix,
323 'paginator': paginator,
324 'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
327 if 'request' in context:
328 getvars = context['request'].GET.copy()
329 if 'page%s' % page_suffix in getvars:
330 del getvars['page%s' % page_suffix]
331 if len(getvars.keys()) > 0:
332 new_context['getvars'] = "&%s" % getvars.urlencode()
334 new_context['getvars'] = ''
336 except (KeyError, AttributeError):
341 register.tag('paginate', do_paginate)
342 register.tag('autopaginate', do_autopaginate)