Fixes for Django up to 2.2
[django-pagination.git] / fnp_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 TokenType
47     TOKEN_BLOCK = TokenType.BLOCK
48 except ImportError:
49     try:
50         # Django 1.8-1.11
51         from django.template.base import TOKEN_BLOCK
52     except ImportError:     # Django < 1.8
53         from django.template import TOKEN_BLOCK
54
55 from django.template.loader import select_template
56 from django.utils.text import unescape_string_literal
57
58 # TODO, import this normally later on
59 from fnp_django_pagination.settings import *
60
61
62 def do_autopaginate(parser, token):
63     """
64     Splits the arguments to the autopaginate tag and formats them correctly.
65
66     Syntax is:
67
68         autopaginate QUERYSET [PAGINATE_BY] [ORPHANS] [as NAME]
69     """
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
74
75     i = iter(token.split_contents())
76     paginate_by = None
77     queryset_var = None
78     context_var = None
79     orphans = None
80     word = None
81     try:
82         word = next(i)
83         assert word == "autopaginate"
84         queryset_var = next(i)
85         word = next(i)
86         if word != "as":
87             paginate_by = word
88             try:
89                 paginate_by = int(paginate_by)
90             except ValueError:
91                 pass
92             word = next(i)
93         if word != "as":
94             orphans = word
95             try:
96                 orphans = int(orphans)
97             except ValueError:
98                 pass
99             word = next(i)
100         assert word == "as"
101         context_var = next(i)
102     except StopIteration:
103         pass
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)
110
111
112 class AutoPaginateNode(Node):
113     """
114     Emits the required objects to allow for Digg-style pagination.
115
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.
119
120     It will then replace the variable specified with only the objects for the
121     current page.
122
123     .. note::
124
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.
128     """
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
133         if orphans is None:
134             orphans = DEFAULT_ORPHANS
135         self.queryset_var = Variable(queryset_var)
136         if isinstance(paginate_by, int):
137             self.paginate_by = paginate_by
138         else:
139             self.paginate_by = Variable(paginate_by)
140         if isinstance(orphans, int):
141             self.orphans = orphans
142         else:
143             self.orphans = Variable(orphans)
144         self.context_var = context_var
145         self.multiple_paginations = multiple_paginations
146
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
151
152         if context.get('multiple_paginations') or getattr(context, "paginator", None):
153             page_suffix = '_%s' % self.queryset_var
154         else:
155             page_suffix = ''
156
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
161         else:
162             paginate_by = self.paginate_by.resolve(context)
163         if isinstance(self.orphans, int):
164             orphans = self.orphans
165         else:
166             orphans = self.orphans.resolve(context)
167         paginator = Paginator(value, paginate_by, orphans)
168         try:
169             request = context['request']
170         except KeyError:
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")
174         try:
175             page_obj = paginator.page(request.page(page_suffix))
176         except InvalidPage:
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.')
180             context[key] = []
181             context['invalid_page'] = True
182             return ''
183         if self.context_var is not None:
184             context[self.context_var] = page_obj.object_list
185         else:
186             context[key] = page_obj.object_list
187         context['paginator'] = paginator
188         context['page_obj'] = page_obj
189         context['page_suffix'] = page_suffix
190         return ''
191
192
193 class PaginateNode(Node):
194
195     def __init__(self, template=None):
196         self.template = template
197
198     def render(self, context):
199         template_list = ['pagination/pagination.html']
200         new_context = paginate(context)
201         if self.template:
202             template_list.insert(0, self.template)
203         return loader.render_to_string(template_list, new_context)
204
205
206 def do_paginate(parser, token):
207     """
208     Emits the pagination control for the most recent autopaginate list
209
210     Syntax is:
211
212         paginate [using "TEMPLATE"]
213
214     Where TEMPLATE is a quoted template name. If missing the default template
215     is used (paginate/pagination.html).
216     """
217     argv = token.split_contents()
218     argc = len(argv)
219     if argc == 1:
220         template = None
221     elif argc == 3 and argv[1] == 'using':
222         template = unescape_string_literal(argv[2])
223     else:
224         raise TemplateSyntaxError(
225             "Invalid syntax. Proper usage of this tag is: "
226             "{% paginate [using \"TEMPLATE\"] %}")
227     return PaginateNode(template)
228
229
230 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
231     """
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.
236
237     Requires one argument, ``context``, which should be a dictionary-like data
238     structure and must contain the following keys:
239
240     ``paginator``
241         A ``Paginator`` or ``QuerySetPaginator`` object.
242
243     ``page_obj``
244         This should be the result of calling the page method on the
245         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
246         the current page.
247
248     This same ``context`` dictionary-like data structure may also include:
249
250     ``getvars``
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
253         a different page.
254
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.
257
258     Argument ``margin``` is number of pages on start/end of pagination.
259     Example:
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]
264         """
265
266     if window < 0:
267         raise ValueError('Parameter "window" cannot be less than zero')
268     if margin < 0:
269         raise ValueError('Parameter "margin" cannot be less than zero')
270     try:
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
280
281         # figure window
282         window_start = page_obj.number - window - 1
283         window_end = page_obj.number + window
284
285         # solve if window exceeded page range
286         if window_start < 0:
287             window_end = window_end - window_start
288             window_start = 0
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]
293
294         # figure margin and add elipses
295         if margin > 0:
296             # figure margin
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)
301             tmp_pages.sort()
302             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]
307                 if gap >= 3:
308                     pages.append(None)
309                 elif gap == 2:
310                     pages.append(tmp_pages[i] - 1)
311                 pages.append(tmp_pages[i])
312         else:
313             if pages[0] != 1:
314                 pages.insert(0, None)
315             if pages[-1] != paginator.num_pages:
316                 pages.append(None)
317
318         new_context = {
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,
329             'pages': pages,
330             'paginator': paginator,
331             'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
332             'records': records,
333         }
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()
340             else:
341                 new_context['getvars'] = ''
342         return new_context
343     except (KeyError, AttributeError):
344         return {}
345
346
347 register = Library()
348 register.tag('paginate', do_paginate)
349 register.tag('autopaginate', do_autopaginate)