PEP8 fixes
[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     TOKEN_BLOCK,
41     TemplateSyntaxError,
42     Variable,
43 )
44 from django.template.loader import select_template
45
46 # TODO, import this normally later on
47 from linaro_django_pagination.settings import *
48
49
50 def do_autopaginate(parser, token):
51     """
52     Splits the arguments to the autopaginate tag and formats them correctly.
53
54     Syntax is:
55
56         autopaginate QUERYSET [PAGINATE_BY] [ORPHANS] [as NAME]
57     """
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
62
63     i = iter(token.split_contents())
64     paginate_by = None
65     queryset_var = None
66     context_var = None
67     orphans = None
68     word = None
69     try:
70         word = i.next()
71         assert word == "autopaginate"
72         queryset_var = i.next()
73         word = i.next()
74         if word != "as":
75             paginate_by = word
76             try:
77                 paginate_by = int(paginate_by)
78             except ValueError:
79                 pass
80             word = i.next()
81         if word != "as":
82             orphans = word
83             try:
84                 orphans = int(orphans)
85             except ValueError:
86                 pass
87             word = i.next()
88         assert word == "as"
89         context_var = i.next()
90     except StopIteration:
91         pass
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)
98
99
100 class AutoPaginateNode(Node):
101     """
102     Emits the required objects to allow for Digg-style pagination.
103
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.
107
108     It will then replace the variable specified with only the objects for the
109     current page.
110
111     .. note::
112
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.
116     """
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
121         if orphans is None:
122             orphans = DEFAULT_ORPHANS
123         self.queryset_var = Variable(queryset_var)
124         if isinstance(paginate_by, int):
125             self.paginate_by = paginate_by
126         else:
127             self.paginate_by = Variable(paginate_by)
128         if isinstance(orphans, int):
129             self.orphans = orphans
130         else:
131             self.orphans = Variable(orphans)
132         self.context_var = context_var
133         self.multiple_paginations = multiple_paginations
134
135     def render(self, context):
136         if self.multiple_paginations or "paginator" in context:
137             page_suffix = '_%s' % self.queryset_var
138         else:
139             page_suffix = ''
140
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
145         else:
146             paginate_by = self.paginate_by.resolve(context)
147         if isinstance(self.orphans, int):
148             orphans = self.orphans
149         else:
150             orphans = self.orphans.resolve(context)
151         paginator = Paginator(value, paginate_by, orphans)
152         try:
153             request = context['request']
154         except KeyError:
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")
158         try:
159             page_obj = paginator.page(request.page(page_suffix))
160         except InvalidPage:
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.')
164             context[key] = []
165             context['invalid_page'] = True
166             return u''
167         if self.context_var is not None:
168             context[self.context_var] = page_obj.object_list
169         else:
170             context[key] = page_obj.object_list
171         context['paginator'] = paginator
172         context['page_obj'] = page_obj
173         context['page_suffix'] = page_suffix
174         return u''
175
176
177 class PaginateNode(Node):
178
179     def __init__(self, template=None):
180         self.template = template
181
182     def render(self, context):
183         template_list = ['pagination/pagination.html']
184         to_return = paginate(context)
185         if self.template:
186             template_list.insert(0, self.template)
187         t = select_template(template_list)
188         if not t:
189             return None
190         context = Context(to_return)
191         return t.render(context)
192
193
194 def do_paginate(parser, token):
195     """
196     {% paginate [using] [template] %}
197
198     {% paginate %}
199     {% paginate using paginations/custom_pagination.html %}
200     """
201     argv = token.contents.split()
202     argc = len(argv)
203     if argc > 3:
204         raise TemplateSyntaxError("Tag %s takes at most 2 argument." % argv[0])
205     if argc == 1:
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])
210
211
212 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
213     """
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.
218
219     Requires one argument, ``context``, which should be a dictionary-like data
220     structure and must contain the following keys:
221
222     ``paginator``
223         A ``Paginator`` or ``QuerySetPaginator`` object.
224
225     ``page_obj``
226         This should be the result of calling the page method on the
227         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
228         the current page.
229
230     This same ``context`` dictionary-like data structure may also include:
231
232     ``getvars``
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
235         a different page.
236
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.
239
240     Argument ``margin``` is number of pages on start/end of pagination.
241     Example:
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]
246         """
247
248     if window < 0:
249         raise ValueError('Parameter "window" cannot be less than zero')
250     if margin < 0:
251         raise ValueError('Parameter "margin" cannot be less than zero')
252     try:
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
262
263         # figure window
264         window_start = page_obj.number - window - 1
265         window_end = page_obj.number + window
266
267         # solve if window exceeded page range
268         if window_start < 0:
269             window_end = window_end - window_start
270             window_start = 0
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]
275
276         # figure margin and add elipses
277         if margin > 0:
278             # figure margin
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)
283             tmp_pages.sort()
284             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]
289                 if gap >= 3:
290                     pages.append(None)
291                 elif gap == 2:
292                     pages.append(tmp_pages[i] - 1)
293                 pages.append(tmp_pages[i])
294         else:
295             if pages[0] != 1:
296                 pages.insert(0, None)
297             if pages[-1] != paginator.num_pages:
298                 pages.append(None)
299
300         to_return = {
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,
310             'pages': pages,
311             'paginator': paginator,
312             'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
313             'records': records,
314         }
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()
321             else:
322                 to_return['getvars'] = ''
323         return to_return
324     except (KeyError, AttributeError):
325         return {}
326
327
328 register = Library()
329 register.tag('paginate', do_paginate)
330 register.tag('autopaginate', do_autopaginate)