f1069426d850e5c65ce6d0806472e4225d37999d
[django-pagination.git] / linaro_django_pagination / templatetags / pagination_tags.py
1 # Copyright (c) 2008, Eric Florenzano
2 # Copyright (c) 2010, 2011 Linaro Limited
3 # All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are
7 # met:
8 #
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.
18 #
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.
30
31
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 (
37     Context,
38     Library,
39     Node,
40     TemplateSyntaxError,
41     Variable,
42     loader,
43 )
44
45 try:
46     from django.template.base import TOKEN_BLOCK
47 except ImportError:     # Django < 1.8
48     from django.template import TOKEN_BLOCK
49
50 from django.template.loader import select_template
51 from django.utils.text import unescape_string_literal
52
53 # TODO, import this normally later on
54 from linaro_django_pagination.settings import *
55
56
57 def do_autopaginate(parser, token):
58     """
59     Splits the arguments to the autopaginate tag and formats them correctly.
60
61     Syntax is:
62
63         autopaginate QUERYSET [PAGINATE_BY] [ORPHANS] [as NAME]
64     """
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
69
70     i = iter(token.split_contents())
71     paginate_by = None
72     queryset_var = None
73     context_var = None
74     orphans = None
75     word = None
76     try:
77         word = next(i)
78         assert word == "autopaginate"
79         queryset_var = next(i)
80         word = next(i)
81         if word != "as":
82             paginate_by = word
83             try:
84                 paginate_by = int(paginate_by)
85             except ValueError:
86                 pass
87             word = next(i)
88         if word != "as":
89             orphans = word
90             try:
91                 orphans = int(orphans)
92             except ValueError:
93                 pass
94             word = next(i)
95         assert word == "as"
96         context_var = next(i)
97     except StopIteration:
98         pass
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)
105
106
107 class AutoPaginateNode(Node):
108     """
109     Emits the required objects to allow for Digg-style pagination.
110
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.
114
115     It will then replace the variable specified with only the objects for the
116     current page.
117
118     .. note::
119
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.
123     """
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
128         if orphans is None:
129             orphans = DEFAULT_ORPHANS
130         self.queryset_var = Variable(queryset_var)
131         if isinstance(paginate_by, int):
132             self.paginate_by = paginate_by
133         else:
134             self.paginate_by = Variable(paginate_by)
135         if isinstance(orphans, int):
136             self.orphans = orphans
137         else:
138             self.orphans = Variable(orphans)
139         self.context_var = context_var
140         self.multiple_paginations = multiple_paginations
141
142     def render(self, context):
143         # Save multiple_paginations state in context
144         if self.multiple_paginations and 'multiple_paginations' not in context:
145             context['multiple_paginations'] = True
146
147         if context.get('multiple_paginations') or getattr(context, "paginator", None):
148             page_suffix = '_%s' % self.queryset_var
149         else:
150             page_suffix = ''
151
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
156         else:
157             paginate_by = self.paginate_by.resolve(context)
158         if isinstance(self.orphans, int):
159             orphans = self.orphans
160         else:
161             orphans = self.orphans.resolve(context)
162         paginator = Paginator(value, paginate_by, orphans)
163         try:
164             request = context['request']
165         except KeyError:
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")
169         try:
170             page_obj = paginator.page(request.page(page_suffix))
171         except InvalidPage:
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.')
175             context[key] = []
176             context['invalid_page'] = True
177             return ''
178         if self.context_var is not None:
179             context[self.context_var] = page_obj.object_list
180         else:
181             context[key] = page_obj.object_list
182         context['paginator'] = paginator
183         context['page_obj'] = page_obj
184         context['page_suffix'] = page_suffix
185         return ''
186
187
188 class PaginateNode(Node):
189
190     def __init__(self, template=None):
191         self.template = template
192
193     def render(self, context):
194         template_list = ['pagination/pagination.html']
195         new_context = paginate(context)
196         if self.template:
197             template_list.insert(0, self.template)
198         return loader.render_to_string(template_list, new_context,
199             context_instance = context)
200
201
202
203 def do_paginate(parser, token):
204     """
205     Emits the pagination control for the most recent autopaginate list
206
207     Syntax is:
208
209         paginate [using "TEMPLATE"]
210
211     Where TEMPLATE is a quoted template name. If missing the default template
212     is used (paginate/pagination.html).
213     """
214     argv = token.split_contents()
215     argc = len(argv)
216     if argc == 1:
217         template = None
218     elif argc == 3 and argv[1] == 'using':
219         template = unescape_string_literal(argv[2])
220     else:
221         raise TemplateSyntaxError(
222             "Invalid syntax. Proper usage of this tag is: "
223             "{% paginate [using \"TEMPLATE\"] %}")
224     return PaginateNode(template)
225
226
227 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
228     """
229     Renders the ``pagination/pagination.html`` template, resulting in a
230     Digg-like display of the available pages, given the current page.  If there
231     are too many pages to be displayed before and after the current page, then
232     elipses will be used to indicate the undisplayed gap between page numbers.
233
234     Requires one argument, ``context``, which should be a dictionary-like data
235     structure and must contain the following keys:
236
237     ``paginator``
238         A ``Paginator`` or ``QuerySetPaginator`` object.
239
240     ``page_obj``
241         This should be the result of calling the page method on the
242         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
243         the current page.
244
245     This same ``context`` dictionary-like data structure may also include:
246
247     ``getvars``
248         A dictionary of all of the **GET** parameters in the current request.
249         This is useful to maintain certain types of state, even when requesting
250         a different page.
251
252     Argument ``window`` is number to pages before/after current page. If window
253     exceeds pagination border (1 and end), window is moved to left or right.
254
255     Argument ``margin``` is number of pages on start/end of pagination.
256     Example:
257         window=2, margin=1, current=6     1 ... 4 5 [6] 7 8 ... 11
258         window=2, margin=0, current=1     [1] 2 3 4 5 ...
259         window=2, margin=0, current=5     ... 3 4 [5] 6 7 ...
260         window=2, margin=0, current=11     ... 7 8 9 10 [11]
261         """
262
263     if window < 0:
264         raise ValueError('Parameter "window" cannot be less than zero')
265     if margin < 0:
266         raise ValueError('Parameter "margin" cannot be less than zero')
267     try:
268         paginator = context['paginator']
269         page_obj = context['page_obj']
270         page_suffix = context.get('page_suffix', '')
271         page_range = list(paginator.page_range)
272         # Calculate the record range in the current page for display.
273         records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
274         records['last'] = records['first'] + paginator.per_page - 1
275         if records['last'] + paginator.orphans >= paginator.count:
276             records['last'] = paginator.count
277
278         # figure window
279         window_start = page_obj.number - window - 1
280         window_end = page_obj.number + window
281
282         # solve if window exceeded page range
283         if window_start < 0:
284             window_end = window_end - window_start
285             window_start = 0
286         if window_end > paginator.num_pages:
287             window_start = max(0, window_start - (window_end - paginator.num_pages))
288             window_end = paginator.num_pages
289         pages = page_range[window_start:window_end]
290
291         # figure margin and add elipses
292         if margin > 0:
293             # figure margin
294             tmp_pages = set(pages)
295             tmp_pages = tmp_pages.union(page_range[:margin])
296             tmp_pages = tmp_pages.union(page_range[-margin:])
297             tmp_pages = list(tmp_pages)
298             tmp_pages.sort()
299             pages = []
300             pages.append(tmp_pages[0])
301             for i in range(1, len(tmp_pages)):
302                 # figure gap size => add elipses or fill in gap
303                 gap = tmp_pages[i] - tmp_pages[i - 1]
304                 if gap >= 3:
305                     pages.append(None)
306                 elif gap == 2:
307                     pages.append(tmp_pages[i] - 1)
308                 pages.append(tmp_pages[i])
309         else:
310             if pages[0] != 1:
311                 pages.insert(0, None)
312             if pages[-1] != paginator.num_pages:
313                 pages.append(None)
314
315         new_context = {
316             'MEDIA_URL': settings.MEDIA_URL,
317             'STATIC_URL': getattr(settings, "STATIC_URL", None),
318             'disable_link_for_first_page': DISABLE_LINK_FOR_FIRST_PAGE,
319             'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
320             'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
321             'display_page_links': DISPLAY_PAGE_LINKS,
322             'is_paginated': paginator.count > paginator.per_page,
323             'next_link_decorator': NEXT_LINK_DECORATOR,
324             'page_obj': page_obj,
325             'page_suffix': page_suffix,
326             'pages': pages,
327             'paginator': paginator,
328             'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
329             'records': records,
330         }
331         if 'request' in context:
332             getvars = context['request'].GET.copy()
333             if 'page%s' % page_suffix in getvars:
334                 del getvars['page%s' % page_suffix]
335             if len(getvars.keys()) > 0:
336                 new_context['getvars'] = "&%s" % getvars.urlencode()
337             else:
338                 new_context['getvars'] = ''
339         return new_context
340     except (KeyError, AttributeError):
341         return {}
342
343
344 register = Library()
345 register.tag('paginate', do_paginate)
346 register.tag('autopaginate', do_autopaginate)