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 (
44 from django.template.loader import select_template
45 from django.utils.text import unescape_string_literal
47 # TODO, import this normally later on
48 from linaro_django_pagination.settings import *
51 def do_autopaginate(parser, token):
53 Splits the arguments to the autopaginate tag and formats them correctly.
57 autopaginate QUERYSET [PAGINATE_BY] [ORPHANS] [as NAME]
59 # Check whether there are any other autopaginations are later in this template
60 expr = lambda obj: (obj.token_type == TOKEN_BLOCK and \
61 len(obj.split_contents()) > 0 and obj.split_contents()[0] == "autopaginate")
62 multiple_paginations = len(filter(expr, parser.tokens)) > 0
64 i = iter(token.split_contents())
72 assert word == "autopaginate"
73 queryset_var = i.next()
78 paginate_by = int(paginate_by)
85 orphans = int(orphans)
90 context_var = i.next()
93 if queryset_var is None:
94 raise TemplateSyntaxError(
95 "Invalid syntax. Proper usage of this tag is: "
96 "{% autopaginate QUERYSET [PAGINATE_BY] [ORPHANS]"
97 " [as CONTEXT_VAR_NAME] %}")
98 return AutoPaginateNode(queryset_var, multiple_paginations, paginate_by, orphans, context_var)
101 class AutoPaginateNode(Node):
103 Emits the required objects to allow for Digg-style pagination.
105 First, it looks in the current context for the variable specified, and using
106 that object, it emits a simple ``Paginator`` and the current page object
107 into the context names ``paginator`` and ``page_obj``, respectively.
109 It will then replace the variable specified with only the objects for the
114 It is recommended to use *{% paginate %}* after using the autopaginate
115 tag. If you choose not to use *{% paginate %}*, make sure to display the
116 list of available pages, or else the application may seem to be buggy.
118 def __init__(self, queryset_var, multiple_paginations, paginate_by=None,
119 orphans=None, context_var=None):
120 if paginate_by is None:
121 paginate_by = DEFAULT_PAGINATION
123 orphans = DEFAULT_ORPHANS
124 self.queryset_var = Variable(queryset_var)
125 if isinstance(paginate_by, int):
126 self.paginate_by = paginate_by
128 self.paginate_by = Variable(paginate_by)
129 if isinstance(orphans, int):
130 self.orphans = orphans
132 self.orphans = Variable(orphans)
133 self.context_var = context_var
134 self.multiple_paginations = multiple_paginations
136 def render(self, context):
137 if self.multiple_paginations or "paginator" in context:
138 page_suffix = '_%s' % self.queryset_var
142 key = self.queryset_var.var
143 value = self.queryset_var.resolve(context)
144 if isinstance(self.paginate_by, int):
145 paginate_by = self.paginate_by
147 paginate_by = self.paginate_by.resolve(context)
148 if isinstance(self.orphans, int):
149 orphans = self.orphans
151 orphans = self.orphans.resolve(context)
152 paginator = Paginator(value, paginate_by, orphans)
154 request = context['request']
156 raise ImproperlyConfigured(
157 "You need to enable 'django.core.context_processors.request'."
158 " See linaro-django-pagination/README file for TEMPLATE_CONTEXT_PROCESSORS details")
160 page_obj = paginator.page(request.page(page_suffix))
162 if INVALID_PAGE_RAISES_404:
163 raise Http404('Invalid page requested. If DEBUG were set to ' +
164 'False, an HTTP 404 page would have been shown instead.')
166 context['invalid_page'] = True
168 if self.context_var is not None:
169 context[self.context_var] = page_obj.object_list
171 context[key] = page_obj.object_list
172 context['paginator'] = paginator
173 context['page_obj'] = page_obj
174 context['page_suffix'] = page_suffix
178 class PaginateNode(Node):
180 def __init__(self, template=None):
181 self.template = template
183 def render(self, context):
184 template_list = ['pagination/pagination.html']
185 to_return = paginate(context)
187 template_list.insert(0, self.template)
188 t = select_template(template_list)
191 context = Context(to_return)
192 return t.render(context)
195 def do_paginate(parser, token):
197 Emits the pagination control for the most recent autopaginate list
201 paginate [using "TEMPLATE"]
203 Where TEMPLATE is a quoted template name. If missing the default template
204 is used (paginate/paginate.html).
206 argv = token.split_contents()
210 elif argc == 3 and argv[1] == 'using':
211 template = unescape_string_literal(argv[2])
213 raise TemplateSyntaxError(
214 "Invalid syntax. Proper usage of this tag is: "
215 "{% paginate [using \"TEMPLATE\"] %}")
216 return PaginateNode(template)
219 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
221 Renders the ``pagination/pagination.html`` template, resulting in a
222 Digg-like display of the available pages, given the current page. If there
223 are too many pages to be displayed before and after the current page, then
224 elipses will be used to indicate the undisplayed gap between page numbers.
226 Requires one argument, ``context``, which should be a dictionary-like data
227 structure and must contain the following keys:
230 A ``Paginator`` or ``QuerySetPaginator`` object.
233 This should be the result of calling the page method on the
234 aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
237 This same ``context`` dictionary-like data structure may also include:
240 A dictionary of all of the **GET** parameters in the current request.
241 This is useful to maintain certain types of state, even when requesting
244 Argument ``window`` is number to pages before/after current page. If window
245 exceeds pagination border (1 and end), window is moved to left or right.
247 Argument ``margin``` is number of pages on start/end of pagination.
249 window=2, margin=1, current=6 1 ... 4 5 [6] 7 8 ... 11
250 window=2, margin=0, current=1 [1] 2 3 4 5 ...
251 window=2, margin=0, current=5 ... 3 4 [5] 6 7 ...
252 window=2, margin=0, current=11 ... 7 8 9 10 [11]
256 raise ValueError('Parameter "window" cannot be less than zero')
258 raise ValueError('Parameter "margin" cannot be less than zero')
260 paginator = context['paginator']
261 page_obj = context['page_obj']
262 page_suffix = context.get('page_suffix', '')
263 page_range = paginator.page_range
264 # Calculate the record range in the current page for display.
265 records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
266 records['last'] = records['first'] + paginator.per_page - 1
267 if records['last'] + paginator.orphans >= paginator.count:
268 records['last'] = paginator.count
271 window_start = page_obj.number - window - 1
272 window_end = page_obj.number + window
274 # solve if window exceeded page range
276 window_end = window_end - window_start
278 if window_end > paginator.num_pages:
279 window_start = window_start - (window_end - paginator.num_pages)
280 window_end = paginator.num_pages
281 pages = page_range[window_start:window_end]
283 # figure margin and add elipses
286 tmp_pages = set(pages)
287 tmp_pages = tmp_pages.union(page_range[:margin])
288 tmp_pages = tmp_pages.union(page_range[-margin:])
289 tmp_pages = list(tmp_pages)
292 pages.append(tmp_pages[0])
293 for i in range(1, len(tmp_pages)):
294 # figure gap size => add elipses or fill in gap
295 gap = tmp_pages[i] - tmp_pages[i - 1]
299 pages.append(tmp_pages[i] - 1)
300 pages.append(tmp_pages[i])
303 pages.insert(0, None)
304 if pages[-1] != paginator.num_pages:
308 'MEDIA_URL': settings.MEDIA_URL,
309 'STATIC_URL': getattr(settings, "STATIC_URL", None),
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 to_return['getvars'] = "&%s" % getvars.urlencode()
329 to_return['getvars'] = ''
331 except (KeyError, AttributeError):
336 register.tag('paginate', do_paginate)
337 register.tag('autopaginate', do_autopaginate)