Support Django 4.0, drop Django < 2.1, Python < 3.6.
[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 from django.template.base import TokenType
46 TOKEN_BLOCK = TokenType.BLOCK
47
48 from django.template.loader import select_template
49 from django.utils.text import unescape_string_literal
50
51 # TODO, import this normally later on
52 from fnp_django_pagination.settings import *
53
54
55 def do_autopaginate(parser, token):
56     """
57     Splits the arguments to the autopaginate tag and formats them correctly.
58
59     Syntax is:
60
61         autopaginate QUERYSET [PAGINATE_BY] [ORPHANS] [as NAME]
62     """
63     # Check whether there are any other autopaginations are later in this template
64     expr = lambda obj: (obj.token_type == TOKEN_BLOCK and \
65         len(obj.split_contents()) > 0 and obj.split_contents()[0] == "autopaginate")
66     multiple_paginations = len([tok for tok in parser.tokens if expr(tok)]) > 0
67
68     i = iter(token.split_contents())
69     paginate_by = None
70     queryset_var = None
71     context_var = None
72     orphans = None
73     word = None
74     try:
75         word = next(i)
76         assert word == "autopaginate"
77         queryset_var = next(i)
78         word = next(i)
79         if word != "as":
80             paginate_by = word
81             try:
82                 paginate_by = int(paginate_by)
83             except ValueError:
84                 pass
85             word = next(i)
86         if word != "as":
87             orphans = word
88             try:
89                 orphans = int(orphans)
90             except ValueError:
91                 pass
92             word = next(i)
93         assert word == "as"
94         context_var = next(i)
95     except StopIteration:
96         pass
97     if queryset_var is None:
98         raise TemplateSyntaxError(
99             "Invalid syntax. Proper usage of this tag is: "
100             "{% autopaginate QUERYSET [PAGINATE_BY] [ORPHANS]"
101             " [as CONTEXT_VAR_NAME] %}")
102     return AutoPaginateNode(queryset_var, multiple_paginations, paginate_by, orphans, context_var)
103
104
105 class AutoPaginateNode(Node):
106     """
107     Emits the required objects to allow for Digg-style pagination.
108
109     First, it looks in the current context for the variable specified, and using
110     that object, it emits a simple ``Paginator`` and the current page object
111     into the context names ``paginator`` and ``page_obj``, respectively.
112
113     It will then replace the variable specified with only the objects for the
114     current page.
115
116     .. note::
117
118         It is recommended to use *{% paginate %}* after using the autopaginate
119         tag.  If you choose not to use *{% paginate %}*, make sure to display the
120         list of available pages, or else the application may seem to be buggy.
121     """
122     def __init__(self, queryset_var,  multiple_paginations, paginate_by=None,
123                  orphans=None, context_var=None):
124         if paginate_by is None:
125             paginate_by = DEFAULT_PAGINATION
126         if orphans is None:
127             orphans = DEFAULT_ORPHANS
128         self.queryset_var = Variable(queryset_var)
129         if isinstance(paginate_by, int):
130             self.paginate_by = paginate_by
131         else:
132             self.paginate_by = Variable(paginate_by)
133         if isinstance(orphans, int):
134             self.orphans = orphans
135         else:
136             self.orphans = Variable(orphans)
137         self.context_var = context_var
138         self.multiple_paginations = multiple_paginations
139
140     def render(self, context):
141         # Save multiple_paginations state in context
142         if self.multiple_paginations and 'multiple_paginations' not in context:
143             context['multiple_paginations'] = True
144
145         if context.get('multiple_paginations') or getattr(context, "paginator", None):
146             page_suffix = '_%s' % self.queryset_var
147         else:
148             page_suffix = ''
149
150         key = self.queryset_var.var
151         value = self.queryset_var.resolve(context)
152         if isinstance(self.paginate_by, int):
153             paginate_by = self.paginate_by
154         else:
155             paginate_by = self.paginate_by.resolve(context)
156         if isinstance(self.orphans, int):
157             orphans = self.orphans
158         else:
159             orphans = self.orphans.resolve(context)
160         paginator = Paginator(value, paginate_by, orphans)
161         try:
162             request = context['request']
163         except KeyError:
164             raise ImproperlyConfigured(
165                 "You need to enable 'django.core.context_processors.request'."
166                 " See linaro-django-pagination/README file for TEMPLATE_CONTEXT_PROCESSORS details")
167         try:
168             page_obj = paginator.page(request.page(page_suffix))
169         except InvalidPage:
170             if INVALID_PAGE_RAISES_404:
171                 raise Http404('Invalid page requested.  If DEBUG were set to ' +
172                     'False, an HTTP 404 page would have been shown instead.')
173             context[key] = []
174             context['invalid_page'] = True
175             return ''
176         if self.context_var is not None:
177             context[self.context_var] = page_obj.object_list
178         else:
179             context[key] = page_obj.object_list
180         context['paginator'] = paginator
181         context['page_obj'] = page_obj
182         context['page_suffix'] = page_suffix
183         return ''
184
185
186 class PaginateNode(Node):
187
188     def __init__(self, template=None):
189         self.template = template
190
191     def render(self, context):
192         template_list = ['pagination/pagination.html']
193         new_context = paginate(context)
194         if self.template:
195             template_list.insert(0, self.template)
196         return loader.render_to_string(template_list, new_context)
197
198
199 def do_paginate(parser, token):
200     """
201     Emits the pagination control for the most recent autopaginate list
202
203     Syntax is:
204
205         paginate [using "TEMPLATE"]
206
207     Where TEMPLATE is a quoted template name. If missing the default template
208     is used (paginate/pagination.html).
209     """
210     argv = token.split_contents()
211     argc = len(argv)
212     if argc == 1:
213         template = None
214     elif argc == 3 and argv[1] == 'using':
215         template = unescape_string_literal(argv[2])
216     else:
217         raise TemplateSyntaxError(
218             "Invalid syntax. Proper usage of this tag is: "
219             "{% paginate [using \"TEMPLATE\"] %}")
220     return PaginateNode(template)
221
222
223 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
224     """
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.
229
230     Requires one argument, ``context``, which should be a dictionary-like data
231     structure and must contain the following keys:
232
233     ``paginator``
234         A ``Paginator`` or ``QuerySetPaginator`` object.
235
236     ``page_obj``
237         This should be the result of calling the page method on the
238         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
239         the current page.
240
241     This same ``context`` dictionary-like data structure may also include:
242
243     ``getvars``
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
246         a different page.
247
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.
250
251     Argument ``margin``` is number of pages on start/end of pagination.
252     Example:
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]
257         """
258
259     if window < 0:
260         raise ValueError('Parameter "window" cannot be less than zero')
261     if margin < 0:
262         raise ValueError('Parameter "margin" cannot be less than zero')
263     try:
264         paginator = context['paginator']
265         page_obj = context['page_obj']
266         page_suffix = context.get('page_suffix', '')
267         page_range = list(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
273
274         # figure window
275         window_start = page_obj.number - window - 1
276         window_end = page_obj.number + window
277
278         # solve if window exceeded page range
279         if window_start < 0:
280             window_end = window_end - window_start
281             window_start = 0
282         if window_end > paginator.num_pages:
283             window_start = max(0, window_start - (window_end - paginator.num_pages))
284             window_end = paginator.num_pages
285         pages = page_range[window_start:window_end]
286
287         # figure margin and add elipses
288         if margin > 0:
289             # figure margin
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)
294             tmp_pages.sort()
295             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]
300                 if gap >= 3:
301                     pages.append(None)
302                 elif gap == 2:
303                     pages.append(tmp_pages[i] - 1)
304                 pages.append(tmp_pages[i])
305         else:
306             if pages[0] != 1:
307                 pages.insert(0, None)
308             if pages[-1] != paginator.num_pages:
309                 pages.append(None)
310
311         new_context = {
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,
322             'pages': pages,
323             'paginator': paginator,
324             'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
325             'records': records,
326         }
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()
333             else:
334                 new_context['getvars'] = ''
335         return new_context
336     except (KeyError, AttributeError):
337         return {}
338
339
340 register = Library()
341 register.tag('paginate', do_paginate)
342 register.tag('autopaginate', do_autopaginate)