Be smarter about missing request object.
[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 import template
33 from django.conf import settings
34 from django.core.exceptions import ImproperlyConfigured
35 from django.core.paginator import Paginator, InvalidPage
36 from django.http import Http404
37 from django.template import TOKEN_BLOCK
38
39 # TODO, import this normally later on
40 from linaro_django_pagination.settings import *
41
42
43 def do_autopaginate(parser, token):
44     """
45     Splits the arguments to the autopaginate tag and formats them correctly.
46
47     Syntax is:
48
49         autopaginate QUERYSET [PAGINATE_BY] [ORPHANS] [as NAME]
50     """
51     # Check whether there are any other autopaginations are later in this template
52     expr = lambda obj: (obj.token_type == TOKEN_BLOCK and \
53         len(obj.split_contents()) > 0 and obj.split_contents()[0] == "autopaginate")
54     multiple_paginations = len(filter(expr, parser.tokens)) > 0
55
56     i = iter(token.split_contents())
57     paginate_by = None
58     queryset_var = None
59     context_var = None
60     orphans = None
61     word = None
62     try:
63         word = i.next()
64         assert word == "autopaginate"
65         queryset_var = i.next()
66         word = i.next()
67         if word != "as":
68             paginate_by = word
69             try:
70                 paginate_by = int(paginate_by)
71             except ValueError:
72                 pass
73             word = i.next()
74         if word != "as":
75             orphans = word
76             try:
77                 orphans = int(orphans)
78             except ValueError:
79                 pass
80             word = i.next()
81         assert word == "as"
82         context_var = i.next()
83     except StopIteration:
84         pass
85     if queryset_var is None:
86         raise template.TemplateSyntaxError(
87             "Invalid syntax. Proper usage of this tag is: "
88             "{%% autopaginate QUERYSET [PAGINATE_BY] [ORPHANS]"
89             " [as CONTEXT_VAR_NAME] %%}"
90         )
91     return AutoPaginateNode(queryset_var, multiple_paginations, paginate_by, orphans, context_var)
92
93
94 class AutoPaginateNode(template.Node):
95     """
96     Emits the required objects to allow for Digg-style pagination.
97     
98     First, it looks in the current context for the variable specified, and using
99     that object, it emits a simple ``Paginator`` and the current page object 
100     into the context names ``paginator`` and ``page_obj``, respectively.
101     
102     It will then replace the variable specified with only the objects for the
103     current page.
104     
105     .. note::
106         
107         It is recommended to use *{% paginate %}* after using the autopaginate
108         tag.  If you choose not to use *{% paginate %}*, make sure to display the
109         list of available pages, or else the application may seem to be buggy.
110     """
111     def __init__(self, queryset_var,  multiple_paginations, paginate_by=None,
112                  orphans=None, context_var=None):
113         if paginate_by is None:
114             paginate_by = DEFAULT_PAGINATION
115         if orphans is None:
116             orphans = DEFAULT_ORPHANS
117         self.queryset_var = template.Variable(queryset_var)
118         if isinstance(paginate_by, int):
119             self.paginate_by = paginate_by
120         else:
121             self.paginate_by = template.Variable(paginate_by)
122         if isinstance(orphans, int):
123             self.orphans = orphans
124         else:
125             self.orphans = template.Variable(orphans)
126         self.context_var = context_var
127         self.multiple_paginations = multiple_paginations
128
129     def render(self, context):
130         if self.multiple_paginations or "paginator" in context:
131             page_suffix = '_%s' % self.queryset_var
132         else:
133             page_suffix = ''
134         
135         key = self.queryset_var.var
136         value = self.queryset_var.resolve(context)
137         if isinstance(self.paginate_by, int):
138             paginate_by = self.paginate_by
139         else:
140             paginate_by = self.paginate_by.resolve(context)
141         if isinstance(self.orphans, int):
142             orphans = self.orphans
143         else:
144             orphans = self.orphans.resolve(context)
145         paginator = Paginator(value, paginate_by, orphans)
146         try:
147             request = context['request']
148         except KeyError:
149             raise ImproperlyConfigured(
150                 "You need to enable 'django.core.context_processors.request'."
151                 " See linaro-django-pagination/README file for TEMPLATE_CONTEXT_PROCESSORS details")
152         try:
153             page_obj = paginator.page(request.page(page_suffix))
154         except InvalidPage:
155             if INVALID_PAGE_RAISES_404:
156                 raise Http404('Invalid page requested.  If DEBUG were set to ' +
157                     'False, an HTTP 404 page would have been shown instead.')
158             context[key] = []
159             context['invalid_page'] = True
160             return u''
161         if self.context_var is not None:
162             context[self.context_var] = page_obj.object_list
163         else:
164             context[key] = page_obj.object_list
165         context['paginator'] = paginator
166         context['page_obj'] = page_obj
167         context['page_suffix'] = page_suffix
168         return u''
169
170
171 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
172     """
173     Renders the ``pagination/pagination.html`` template, resulting in a
174     Digg-like display of the available pages, given the current page.  If there
175     are too many pages to be displayed before and after the current page, then
176     elipses will be used to indicate the undisplayed gap between page numbers.
177     
178     Requires one argument, ``context``, which should be a dictionary-like data
179     structure and must contain the following keys:
180     
181     ``paginator``
182         A ``Paginator`` or ``QuerySetPaginator`` object.
183     
184     ``page_obj``
185         This should be the result of calling the page method on the 
186         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
187         the current page.
188     
189     This same ``context`` dictionary-like data structure may also include:
190     
191     ``getvars``
192         A dictionary of all of the **GET** parameters in the current request.
193         This is useful to maintain certain types of state, even when requesting
194         a different page.
195     
196     ``pagination_template``
197         A custom template to include in place of the default ``pagination/default.html`` 
198         contents.
199         
200     Argument ``window`` is number to pages before/after current page. If window
201     exceeds pagination border (1 and end), window is moved to left or right.
202
203     Argument ``margin``` is number of pages on start/end of pagination. 
204     Example:
205         window=2, margin=1, current=6     1 ... 4 5 [6] 7 8 ... 11 
206         window=2, margin=0, current=1     [1] 2 3 4 5 ...
207         window=2, margin=0, current=5     ... 3 4 [5] 6 7 ...
208         window=2, margin=0, current=11     ... 7 8 9 10 [11]
209         """
210
211     if window < 0:
212         raise ValueError('Parameter "window" cannot be less than zero')
213     if margin < 0:
214         raise ValueError('Parameter "margin" cannot be less than zero')
215     try:
216         paginator = context['paginator']
217         page_obj = context['page_obj']
218         page_suffix = context.get('page_suffix', '')
219         page_range = paginator.page_range
220         pagination_template = context.get('pagination_template', 'pagination/default.html')
221         # Calculate the record range in the current page for display.
222         records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
223         records['last'] = records['first'] + paginator.per_page - 1
224         if records['last'] + paginator.orphans >= paginator.count:
225             records['last'] = paginator.count
226
227         # figure window
228         window_start = page_obj.number - window - 1
229         window_end = page_obj.number + window
230
231         # solve if window exceeded page range
232         if window_start < 0:
233             window_end = window_end - window_start
234             window_start = 0
235         if window_end > paginator.num_pages:
236             window_start = window_start - (window_end - paginator.num_pages)
237             window_end = paginator.num_pages
238         pages = page_range[window_start:window_end]
239
240         # figure margin and add elipses
241         if margin > 0:
242             # figure margin
243             tmp_pages = set(pages)
244             tmp_pages = tmp_pages.union(page_range[:margin])
245             tmp_pages = tmp_pages.union(page_range[-margin:])
246             tmp_pages = list(tmp_pages)
247             tmp_pages.sort()
248             pages = []
249             pages.append(tmp_pages[0])
250             for i in range(1, len(tmp_pages)):
251                 # figure gap size => add elipses or fill in gap
252                 gap = tmp_pages[i] - tmp_pages[i - 1]
253                 if gap >= 3:
254                     pages.append(None)
255                 elif gap == 2:
256                     pages.append(tmp_pages[i] - 1)
257                 pages.append(tmp_pages[i])
258         else:
259             if pages[0] != 1:
260                 pages.insert(0, None)
261             if pages[-1] != paginator.num_pages:
262                 pages.append(None)
263
264         to_return = {
265             'MEDIA_URL': settings.MEDIA_URL,
266             'STATIC_URL': getattr(settings, "STATIC_URL", None),
267             'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
268             'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
269             'display_page_links': DISPLAY_PAGE_LINKS,
270             'is_paginated': paginator.count > paginator.per_page,
271             'next_link_decorator': NEXT_LINK_DECORATOR,
272             'page_obj': page_obj,
273             'page_suffix': page_suffix,
274             'pages': pages,
275             'pagination_template': pagination_template,
276             'paginator': paginator,
277             'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
278             'records': records,
279         }
280         if 'request' in context:
281             getvars = context['request'].GET.copy()
282             if 'page%s' % page_suffix in getvars:
283                 del getvars['page%s' % page_suffix]
284             if len(getvars.keys()) > 0:
285                 to_return['getvars'] = "&%s" % getvars.urlencode()
286             else:
287                 to_return['getvars'] = ''
288         return to_return
289     except (KeyError, AttributeError):
290         return {}
291
292
293 register = template.Library()
294 register.inclusion_tag(
295     'pagination/pagination.html', takes_context=True)(paginate)
296 register.tag('autopaginate', do_autopaginate)