fix
[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('page_suffix'):
146             page_suffix = context['page_suffix']
147         elif 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, window=None, margin=None, ignored_vars=None):
191         self.template = template
192         self.window = window
193         self.margin = margin
194         self.ignored_vars = ignored_vars
195
196     def render(self, context):
197         template_list = ['pagination/pagination.html']
198         new_context = paginate(context, window=self.window, margin=self.margin, ignored_vars=self.ignored_vars)
199         if self.template:
200             template_list.insert(0, self.template)
201         return loader.render_to_string(template_list, new_context)
202
203
204 def do_paginate(parser, token):
205     """
206     Emits the pagination control for the most recent autopaginate list
207
208     Syntax is:
209
210         paginate [using "TEMPLATE"] [window N] [margin N]
211
212     Where TEMPLATE is a quoted template name. If missing the default template
213     is used (paginate/pagination.html).
214     """
215     argv = token.split_contents()
216     argc = len(argv)
217     template = None
218     window = DEFAULT_WINDOW
219     margin = DEFAULT_MARGIN
220     ignored_vars = []
221     i = 1
222     while i < argc:
223         if argv[i] == 'using':
224             template = unescape_string_literal(argv[i + 1])
225             i += 2
226         elif argv[i] == 'window':
227             window = argv[i + 1]
228             i += 2
229         elif argv[i] == 'margin':
230             margin = argv[i + 1]
231             i += 2
232         elif argv[i] == 'ignore':
233             ignored_vars.append(argv[i + 1])
234             i += 2
235         else:
236             raise TemplateSyntaxError(
237                 "Invalid syntax. Proper usage of this tag is: "
238                 "{% paginate [using \"TEMPLATE\"] %}")
239     return PaginateNode(template, window, margin, ignored_vars)
240
241
242 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN, ignored_vars=None):
243     """
244     Renders the ``pagination/pagination.html`` template, resulting in a
245     Digg-like display of the available pages, given the current page.  If there
246     are too many pages to be displayed before and after the current page, then
247     elipses will be used to indicate the undisplayed gap between page numbers.
248
249     Requires one argument, ``context``, which should be a dictionary-like data
250     structure and must contain the following keys:
251
252     ``paginator``
253         A ``Paginator`` or ``QuerySetPaginator`` object.
254
255     ``page_obj``
256         This should be the result of calling the page method on the
257         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
258         the current page.
259
260     This same ``context`` dictionary-like data structure may also include:
261
262     ``getvars``
263         A dictionary of all of the **GET** parameters in the current request.
264         This is useful to maintain certain types of state, even when requesting
265         a different page.
266
267     Argument ``window`` is number to pages before/after current page. If window
268     exceeds pagination border (1 and end), window is moved to left or right.
269
270     Argument ``margin``` is number of pages on start/end of pagination.
271     Example:
272         window=2, margin=1, current=6     1 ... 4 5 [6] 7 8 ... 11
273         window=2, margin=0, current=1     [1] 2 3 4 5 ...
274         window=2, margin=0, current=5     ... 3 4 [5] 6 7 ...
275         window=2, margin=0, current=11     ... 7 8 9 10 [11]
276         """
277
278     try:
279         window = int(window)
280     except ValueError:
281         window = Variable(window).resolve(context)
282     try:
283         margin = int(margin)
284     except ValueError:
285         margin = Variable(margin).resolve(context)
286
287     if window < 0:
288         raise ValueError('Parameter "window" cannot be less than zero')
289     if margin < 0:
290         raise ValueError('Parameter "margin" cannot be less than zero')
291     try:
292         paginator = context['paginator']
293         page_obj = context['page_obj']
294         page_suffix = context.get('page_suffix', '')
295         page_range = list(paginator.page_range)
296         # Calculate the record range in the current page for display.
297         records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
298         records['last'] = records['first'] + paginator.per_page - 1
299         if records['last'] + paginator.orphans >= paginator.count:
300             records['last'] = paginator.count
301
302         # figure window
303         window_start = page_obj.number - window - 1
304         window_end = page_obj.number + window
305
306         # solve if window exceeded page range
307         if window_start < 0:
308             window_end = window_end - window_start
309             window_start = 0
310         if window_end > paginator.num_pages:
311             window_start = max(0, window_start - (window_end - paginator.num_pages))
312             window_end = paginator.num_pages
313         pages = page_range[window_start:window_end]
314
315         # figure margin and add elipses
316         if margin > 0:
317             # figure margin
318             tmp_pages = set(pages)
319             tmp_pages = tmp_pages.union(page_range[:margin])
320             tmp_pages = tmp_pages.union(page_range[-margin:])
321             tmp_pages = list(tmp_pages)
322             tmp_pages.sort()
323             pages = []
324             pages.append(tmp_pages[0])
325             for i in range(1, len(tmp_pages)):
326                 # figure gap size => add elipses or fill in gap
327                 gap = tmp_pages[i] - tmp_pages[i - 1]
328                 if gap >= 3:
329                     pages.append(None)
330                 elif gap == 2:
331                     pages.append(tmp_pages[i] - 1)
332                 pages.append(tmp_pages[i])
333         else:
334             if pages[0] != 1:
335                 pages.insert(0, None)
336             if pages[-1] != paginator.num_pages:
337                 pages.append(None)
338
339         new_context = {
340             'MEDIA_URL': settings.MEDIA_URL,
341             'STATIC_URL': getattr(settings, "STATIC_URL", None),
342             'disable_link_for_first_page': DISABLE_LINK_FOR_FIRST_PAGE,
343             'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
344             'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
345             'display_page_links': DISPLAY_PAGE_LINKS,
346             'is_paginated': paginator.count > paginator.per_page,
347             'next_link_decorator': NEXT_LINK_DECORATOR,
348             'page_obj': page_obj,
349             'page_suffix': page_suffix,
350             'pages': pages,
351             'paginator': paginator,
352             'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
353             'records': records,
354         }
355         if 'request' in context:
356             getvars = context['request'].GET.copy()
357             if ignored_vars:
358                 for v in ignored_vars:
359                     if v in getvars:
360                         del getvars[v]
361             if 'page%s' % page_suffix in getvars:
362                 del getvars['page%s' % page_suffix]
363             if len(getvars.keys()) > 0:
364                 new_context['getvars'] = "&%s" % getvars.urlencode()
365             else:
366                 new_context['getvars'] = ''
367         return new_context
368     except (KeyError, AttributeError):
369         return {}
370
371
372 register = Library()
373 register.tag('paginate', do_paginate)
374 register.tag('autopaginate', do_autopaginate)