Merge branch 'master' of git://github.com/callowayproject/django-pagination
[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         )
98     return AutoPaginateNode(queryset_var, multiple_paginations, paginate_by, orphans, context_var)
99
100
101 class AutoPaginateNode(Node):
102     """
103     Emits the required objects to allow for Digg-style pagination.
104     
105     First, it looks in the current context for the variable specified, and using
106     that object, it emits a simple ``Paginator`` and the current page object 
107     into the context names ``paginator`` and ``page_obj``, respectively.
108     
109     It will then replace the variable specified with only the objects for the
110     current page.
111     
112     .. note::
113         
114         It is recommended to use *{% paginate %}* after using the autopaginate
115         tag.  If you choose not to use *{% paginate %}*, make sure to display the
116         list of available pages, or else the application may seem to be buggy.
117     """
118     def __init__(self, queryset_var,  multiple_paginations, paginate_by=None,
119                  orphans=None, context_var=None):
120         if paginate_by is None:
121             paginate_by = DEFAULT_PAGINATION
122         if orphans is None:
123             orphans = DEFAULT_ORPHANS
124         self.queryset_var = Variable(queryset_var)
125         if isinstance(paginate_by, int):
126             self.paginate_by = paginate_by
127         else:
128             self.paginate_by = Variable(paginate_by)
129         if isinstance(orphans, int):
130             self.orphans = orphans
131         else:
132             self.orphans = Variable(orphans)
133         self.context_var = context_var
134         self.multiple_paginations = multiple_paginations
135
136     def render(self, context):
137         if self.multiple_paginations or "paginator" in context:
138             page_suffix = '_%s' % self.queryset_var
139         else:
140             page_suffix = ''
141         
142         key = self.queryset_var.var
143         value = self.queryset_var.resolve(context)
144         if isinstance(self.paginate_by, int):
145             paginate_by = self.paginate_by
146         else:
147             paginate_by = self.paginate_by.resolve(context)
148         if isinstance(self.orphans, int):
149             orphans = self.orphans
150         else:
151             orphans = self.orphans.resolve(context)
152         paginator = Paginator(value, paginate_by, orphans)
153         try:
154             request = context['request']
155         except KeyError:
156             raise ImproperlyConfigured(
157                 "You need to enable 'django.core.context_processors.request'."
158                 " See linaro-django-pagination/README file for TEMPLATE_CONTEXT_PROCESSORS details")
159         try:
160             page_obj = paginator.page(request.page(page_suffix))
161         except InvalidPage:
162             if INVALID_PAGE_RAISES_404:
163                 raise Http404('Invalid page requested.  If DEBUG were set to ' +
164                     'False, an HTTP 404 page would have been shown instead.')
165             context[key] = []
166             context['invalid_page'] = True
167             return u''
168         if self.context_var is not None:
169             context[self.context_var] = page_obj.object_list
170         else:
171             context[key] = page_obj.object_list
172         context['paginator'] = paginator
173         context['page_obj'] = page_obj
174         context['page_suffix'] = page_suffix
175         return u''
176
177
178 class PaginateNode(Node):
179
180     def __init__(self, template=None):
181         self.template = template
182         
183     def render(self, context):
184         template_list = ['pagination/pagination.html']
185         to_return = paginate(context)
186         if self.template:
187             template_list.insert(0, self.template)
188         t = select_template(template_list)
189         if not t: 
190             return None
191         context = Context(to_return)
192         return t.render(context)
193         
194
195 def do_paginate(parser, token):
196     """
197     {% paginate [using] [template] %}
198     
199     {% paginate %}
200     {% paginate using paginations/custom_pagination.html %}
201     """
202     argv = token.contents.split()
203     argc = len(argv)
204     if argc > 3:
205         raise TemplateSyntaxError("Tag %s takes at most 2 argument." % argv[0])
206     if argc == 1:
207         return PaginateNode()
208     if argc == 3 and argv[1] == 'using':
209         return PaginateNode(template=argv[2])
210     raise TemplateSyntaxError("Tag %s is invalid. Please check the syntax" % argv[0])
211
212
213 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
214     """
215     Renders the ``pagination/pagination.html`` template, resulting in a
216     Digg-like display of the available pages, given the current page.  If there
217     are too many pages to be displayed before and after the current page, then
218     elipses will be used to indicate the undisplayed gap between page numbers.
219     
220     Requires one argument, ``context``, which should be a dictionary-like data
221     structure and must contain the following keys:
222     
223     ``paginator``
224         A ``Paginator`` or ``QuerySetPaginator`` object.
225     
226     ``page_obj``
227         This should be the result of calling the page method on the 
228         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
229         the current page.
230     
231     This same ``context`` dictionary-like data structure may also include:
232     
233     ``getvars``
234         A dictionary of all of the **GET** parameters in the current request.
235         This is useful to maintain certain types of state, even when requesting
236         a different page.
237         
238     Argument ``window`` is number to pages before/after current page. If window
239     exceeds pagination border (1 and end), window is moved to left or right.
240
241     Argument ``margin``` is number of pages on start/end of pagination. 
242     Example:
243         window=2, margin=1, current=6     1 ... 4 5 [6] 7 8 ... 11 
244         window=2, margin=0, current=1     [1] 2 3 4 5 ...
245         window=2, margin=0, current=5     ... 3 4 [5] 6 7 ...
246         window=2, margin=0, current=11     ... 7 8 9 10 [11]
247         """
248
249     if window < 0:
250         raise ValueError('Parameter "window" cannot be less than zero')
251     if margin < 0:
252         raise ValueError('Parameter "margin" cannot be less than zero')
253     try:
254         paginator = context['paginator']
255         page_obj = context['page_obj']
256         page_suffix = context.get('page_suffix', '')
257         page_range = paginator.page_range
258         # Calculate the record range in the current page for display.
259         records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
260         records['last'] = records['first'] + paginator.per_page - 1
261         if records['last'] + paginator.orphans >= paginator.count:
262             records['last'] = paginator.count
263
264         # figure window
265         window_start = page_obj.number - window - 1
266         window_end = page_obj.number + window
267
268         # solve if window exceeded page range
269         if window_start < 0:
270             window_end = window_end - window_start
271             window_start = 0
272         if window_end > paginator.num_pages:
273             window_start = window_start - (window_end - paginator.num_pages)
274             window_end = paginator.num_pages
275         pages = page_range[window_start:window_end]
276
277         # figure margin and add elipses
278         if margin > 0:
279             # figure margin
280             tmp_pages = set(pages)
281             tmp_pages = tmp_pages.union(page_range[:margin])
282             tmp_pages = tmp_pages.union(page_range[-margin:])
283             tmp_pages = list(tmp_pages)
284             tmp_pages.sort()
285             pages = []
286             pages.append(tmp_pages[0])
287             for i in range(1, len(tmp_pages)):
288                 # figure gap size => add elipses or fill in gap
289                 gap = tmp_pages[i] - tmp_pages[i - 1]
290                 if gap >= 3:
291                     pages.append(None)
292                 elif gap == 2:
293                     pages.append(tmp_pages[i] - 1)
294                 pages.append(tmp_pages[i])
295         else:
296             if pages[0] != 1:
297                 pages.insert(0, None)
298             if pages[-1] != paginator.num_pages:
299                 pages.append(None)
300
301         to_return = {
302             'MEDIA_URL': settings.MEDIA_URL,
303             'STATIC_URL': getattr(settings, "STATIC_URL", None),
304             'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
305             'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
306             'display_page_links': DISPLAY_PAGE_LINKS,
307             'is_paginated': paginator.count > paginator.per_page,
308             'next_link_decorator': NEXT_LINK_DECORATOR,
309             'page_obj': page_obj,
310             'page_suffix': page_suffix,
311             'pages': pages,
312             'paginator': paginator,
313             'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
314             'records': records,
315         }
316         if 'request' in context:
317             getvars = context['request'].GET.copy()
318             if 'page%s' % page_suffix in getvars:
319                 del getvars['page%s' % page_suffix]
320             if len(getvars.keys()) > 0:
321                 to_return['getvars'] = "&%s" % getvars.urlencode()
322             else:
323                 to_return['getvars'] = ''
324         return to_return
325     except (KeyError, AttributeError):
326         return {}
327
328
329 register = Library()
330 register.tag('paginate', do_paginate)
331 register.tag('autopaginate', do_autopaginate)