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
46 # TODO, import this normally later on
47 from linaro_django_pagination.settings import *
50 def do_autopaginate(parser, token):
52 Splits the arguments to the autopaginate tag and formats them correctly.
56 autopaginate QUERYSET [PAGINATE_BY] [ORPHANS] [as NAME]
58 # Check whether there are any other autopaginations are later in this template
59 expr = lambda obj: (obj.token_type == TOKEN_BLOCK and \
60 len(obj.split_contents()) > 0 and obj.split_contents()[0] == "autopaginate")
61 multiple_paginations = len(filter(expr, parser.tokens)) > 0
63 i = iter(token.split_contents())
71 assert word == "autopaginate"
72 queryset_var = i.next()
77 paginate_by = int(paginate_by)
84 orphans = int(orphans)
89 context_var = i.next()
92 if queryset_var is None:
93 raise TemplateSyntaxError(
94 "Invalid syntax. Proper usage of this tag is: "
95 "{%% autopaginate QUERYSET [PAGINATE_BY] [ORPHANS]"
96 " [as CONTEXT_VAR_NAME] %%}")
97 return AutoPaginateNode(queryset_var, multiple_paginations, paginate_by, orphans, context_var)
100 class AutoPaginateNode(Node):
102 Emits the required objects to allow for Digg-style pagination.
104 First, it looks in the current context for the variable specified, and using
105 that object, it emits a simple ``Paginator`` and the current page object
106 into the context names ``paginator`` and ``page_obj``, respectively.
108 It will then replace the variable specified with only the objects for the
113 It is recommended to use *{% paginate %}* after using the autopaginate
114 tag. If you choose not to use *{% paginate %}*, make sure to display the
115 list of available pages, or else the application may seem to be buggy.
117 def __init__(self, queryset_var, multiple_paginations, paginate_by=None,
118 orphans=None, context_var=None):
119 if paginate_by is None:
120 paginate_by = DEFAULT_PAGINATION
122 orphans = DEFAULT_ORPHANS
123 self.queryset_var = Variable(queryset_var)
124 if isinstance(paginate_by, int):
125 self.paginate_by = paginate_by
127 self.paginate_by = Variable(paginate_by)
128 if isinstance(orphans, int):
129 self.orphans = orphans
131 self.orphans = Variable(orphans)
132 self.context_var = context_var
133 self.multiple_paginations = multiple_paginations
135 def render(self, context):
136 if self.multiple_paginations or "paginator" in context:
137 page_suffix = '_%s' % self.queryset_var
141 key = self.queryset_var.var
142 value = self.queryset_var.resolve(context)
143 if isinstance(self.paginate_by, int):
144 paginate_by = self.paginate_by
146 paginate_by = self.paginate_by.resolve(context)
147 if isinstance(self.orphans, int):
148 orphans = self.orphans
150 orphans = self.orphans.resolve(context)
151 paginator = Paginator(value, paginate_by, orphans)
153 request = context['request']
155 raise ImproperlyConfigured(
156 "You need to enable 'django.core.context_processors.request'."
157 " See linaro-django-pagination/README file for TEMPLATE_CONTEXT_PROCESSORS details")
159 page_obj = paginator.page(request.page(page_suffix))
161 if INVALID_PAGE_RAISES_404:
162 raise Http404('Invalid page requested. If DEBUG were set to ' +
163 'False, an HTTP 404 page would have been shown instead.')
165 context['invalid_page'] = True
167 if self.context_var is not None:
168 context[self.context_var] = page_obj.object_list
170 context[key] = page_obj.object_list
171 context['paginator'] = paginator
172 context['page_obj'] = page_obj
173 context['page_suffix'] = page_suffix
177 class PaginateNode(Node):
179 def __init__(self, template=None):
180 self.template = template
182 def render(self, context):
183 template_list = ['pagination/pagination.html']
184 to_return = paginate(context)
186 template_list.insert(0, self.template)
187 t = select_template(template_list)
190 context = Context(to_return)
191 return t.render(context)
194 def do_paginate(parser, token):
196 {% paginate [using] [template] %}
199 {% paginate using paginations/custom_pagination.html %}
201 argv = token.contents.split()
204 raise TemplateSyntaxError("Tag %s takes at most 2 argument." % argv[0])
206 return PaginateNode()
207 if argc == 3 and argv[1] == 'using':
208 return PaginateNode(template=argv[2])
209 raise TemplateSyntaxError("Tag %s is invalid. Please check the syntax" % argv[0])
212 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
214 Renders the ``pagination/pagination.html`` template, resulting in a
215 Digg-like display of the available pages, given the current page. If there
216 are too many pages to be displayed before and after the current page, then
217 elipses will be used to indicate the undisplayed gap between page numbers.
219 Requires one argument, ``context``, which should be a dictionary-like data
220 structure and must contain the following keys:
223 A ``Paginator`` or ``QuerySetPaginator`` object.
226 This should be the result of calling the page method on the
227 aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
230 This same ``context`` dictionary-like data structure may also include:
233 A dictionary of all of the **GET** parameters in the current request.
234 This is useful to maintain certain types of state, even when requesting
237 Argument ``window`` is number to pages before/after current page. If window
238 exceeds pagination border (1 and end), window is moved to left or right.
240 Argument ``margin``` is number of pages on start/end of pagination.
242 window=2, margin=1, current=6 1 ... 4 5 [6] 7 8 ... 11
243 window=2, margin=0, current=1 [1] 2 3 4 5 ...
244 window=2, margin=0, current=5 ... 3 4 [5] 6 7 ...
245 window=2, margin=0, current=11 ... 7 8 9 10 [11]
249 raise ValueError('Parameter "window" cannot be less than zero')
251 raise ValueError('Parameter "margin" cannot be less than zero')
253 paginator = context['paginator']
254 page_obj = context['page_obj']
255 page_suffix = context.get('page_suffix', '')
256 page_range = paginator.page_range
257 # Calculate the record range in the current page for display.
258 records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
259 records['last'] = records['first'] + paginator.per_page - 1
260 if records['last'] + paginator.orphans >= paginator.count:
261 records['last'] = paginator.count
264 window_start = page_obj.number - window - 1
265 window_end = page_obj.number + window
267 # solve if window exceeded page range
269 window_end = window_end - window_start
271 if window_end > paginator.num_pages:
272 window_start = window_start - (window_end - paginator.num_pages)
273 window_end = paginator.num_pages
274 pages = page_range[window_start:window_end]
276 # figure margin and add elipses
279 tmp_pages = set(pages)
280 tmp_pages = tmp_pages.union(page_range[:margin])
281 tmp_pages = tmp_pages.union(page_range[-margin:])
282 tmp_pages = list(tmp_pages)
285 pages.append(tmp_pages[0])
286 for i in range(1, len(tmp_pages)):
287 # figure gap size => add elipses or fill in gap
288 gap = tmp_pages[i] - tmp_pages[i - 1]
292 pages.append(tmp_pages[i] - 1)
293 pages.append(tmp_pages[i])
296 pages.insert(0, None)
297 if pages[-1] != paginator.num_pages:
301 'MEDIA_URL': settings.MEDIA_URL,
302 'STATIC_URL': getattr(settings, "STATIC_URL", None),
303 'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
304 'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
305 'display_page_links': DISPLAY_PAGE_LINKS,
306 'is_paginated': paginator.count > paginator.per_page,
307 'next_link_decorator': NEXT_LINK_DECORATOR,
308 'page_obj': page_obj,
309 'page_suffix': page_suffix,
311 'paginator': paginator,
312 'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
315 if 'request' in context:
316 getvars = context['request'].GET.copy()
317 if 'page%s' % page_suffix in getvars:
318 del getvars['page%s' % page_suffix]
319 if len(getvars.keys()) > 0:
320 to_return['getvars'] = "&%s" % getvars.urlencode()
322 to_return['getvars'] = ''
324 except (KeyError, AttributeError):
329 register.tag('paginate', do_paginate)
330 register.tag('autopaginate', do_autopaginate)