2d15e720bd71194804bed4dd7ff04d65e3e0c14a
[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 from django.utils.text import unescape_string_literal
46
47 # TODO, import this normally later on
48 from linaro_django_pagination.settings import *
49
50
51 def do_autopaginate(parser, token):
52     """
53     Splits the arguments to the autopaginate tag and formats them correctly.
54
55     Syntax is:
56
57         autopaginate QUERYSET [PAGINATE_BY] [ORPHANS] [as NAME]
58     """
59     # Check whether there are any other autopaginations are later in this template
60     expr = lambda obj: (obj.token_type == TOKEN_BLOCK and \
61         len(obj.split_contents()) > 0 and obj.split_contents()[0] == "autopaginate")
62     multiple_paginations = len(filter(expr, parser.tokens)) > 0
63
64     i = iter(token.split_contents())
65     paginate_by = None
66     queryset_var = None
67     context_var = None
68     orphans = None
69     word = None
70     try:
71         word = i.next()
72         assert word == "autopaginate"
73         queryset_var = i.next()
74         word = i.next()
75         if word != "as":
76             paginate_by = word
77             try:
78                 paginate_by = int(paginate_by)
79             except ValueError:
80                 pass
81             word = i.next()
82         if word != "as":
83             orphans = word
84             try:
85                 orphans = int(orphans)
86             except ValueError:
87                 pass
88             word = i.next()
89         assert word == "as"
90         context_var = i.next()
91     except StopIteration:
92         pass
93     if queryset_var is None:
94         raise TemplateSyntaxError(
95             "Invalid syntax. Proper usage of this tag is: "
96             "{%% autopaginate QUERYSET [PAGINATE_BY] [ORPHANS]"
97             " [as CONTEXT_VAR_NAME] %%}")
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     Emits the pagination control for the most recent autopaginate list
198
199     Syntax is:
200
201         paginate [using "TEMPLATE"]
202
203     Where TEMPLATE is a quoted template name. If missing the default template
204     is used (paginate/paginate.html).
205     """
206     argv = token.split_contents()
207     argc = len(argv)
208     if argc == 1:
209         template = None
210     elif argc == 3 and argv[1] == 'using':
211         template = unescape_string_literal(argv[2])
212     else:
213         raise TemplateSyntaxError(
214             "Invalid syntax. Proper usage of this tag is: "
215             "{% paginate [using \"TEMPLATE\"] %}")
216     return PaginateNode(template)
217
218
219 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
220     """
221     Renders the ``pagination/pagination.html`` template, resulting in a
222     Digg-like display of the available pages, given the current page.  If there
223     are too many pages to be displayed before and after the current page, then
224     elipses will be used to indicate the undisplayed gap between page numbers.
225
226     Requires one argument, ``context``, which should be a dictionary-like data
227     structure and must contain the following keys:
228
229     ``paginator``
230         A ``Paginator`` or ``QuerySetPaginator`` object.
231
232     ``page_obj``
233         This should be the result of calling the page method on the
234         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
235         the current page.
236
237     This same ``context`` dictionary-like data structure may also include:
238
239     ``getvars``
240         A dictionary of all of the **GET** parameters in the current request.
241         This is useful to maintain certain types of state, even when requesting
242         a different page.
243
244     Argument ``window`` is number to pages before/after current page. If window
245     exceeds pagination border (1 and end), window is moved to left or right.
246
247     Argument ``margin``` is number of pages on start/end of pagination.
248     Example:
249         window=2, margin=1, current=6     1 ... 4 5 [6] 7 8 ... 11
250         window=2, margin=0, current=1     [1] 2 3 4 5 ...
251         window=2, margin=0, current=5     ... 3 4 [5] 6 7 ...
252         window=2, margin=0, current=11     ... 7 8 9 10 [11]
253         """
254
255     if window < 0:
256         raise ValueError('Parameter "window" cannot be less than zero')
257     if margin < 0:
258         raise ValueError('Parameter "margin" cannot be less than zero')
259     try:
260         paginator = context['paginator']
261         page_obj = context['page_obj']
262         page_suffix = context.get('page_suffix', '')
263         page_range = paginator.page_range
264         # Calculate the record range in the current page for display.
265         records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
266         records['last'] = records['first'] + paginator.per_page - 1
267         if records['last'] + paginator.orphans >= paginator.count:
268             records['last'] = paginator.count
269
270         # figure window
271         window_start = page_obj.number - window - 1
272         window_end = page_obj.number + window
273
274         # solve if window exceeded page range
275         if window_start < 0:
276             window_end = window_end - window_start
277             window_start = 0
278         if window_end > paginator.num_pages:
279             window_start = window_start - (window_end - paginator.num_pages)
280             window_end = paginator.num_pages
281         pages = page_range[window_start:window_end]
282
283         # figure margin and add elipses
284         if margin > 0:
285             # figure margin
286             tmp_pages = set(pages)
287             tmp_pages = tmp_pages.union(page_range[:margin])
288             tmp_pages = tmp_pages.union(page_range[-margin:])
289             tmp_pages = list(tmp_pages)
290             tmp_pages.sort()
291             pages = []
292             pages.append(tmp_pages[0])
293             for i in range(1, len(tmp_pages)):
294                 # figure gap size => add elipses or fill in gap
295                 gap = tmp_pages[i] - tmp_pages[i - 1]
296                 if gap >= 3:
297                     pages.append(None)
298                 elif gap == 2:
299                     pages.append(tmp_pages[i] - 1)
300                 pages.append(tmp_pages[i])
301         else:
302             if pages[0] != 1:
303                 pages.insert(0, None)
304             if pages[-1] != paginator.num_pages:
305                 pages.append(None)
306
307         to_return = {
308             'MEDIA_URL': settings.MEDIA_URL,
309             'STATIC_URL': getattr(settings, "STATIC_URL", None),
310             'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
311             'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
312             'display_page_links': DISPLAY_PAGE_LINKS,
313             'is_paginated': paginator.count > paginator.per_page,
314             'next_link_decorator': NEXT_LINK_DECORATOR,
315             'page_obj': page_obj,
316             'page_suffix': page_suffix,
317             'pages': pages,
318             'paginator': paginator,
319             'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
320             'records': records,
321         }
322         if 'request' in context:
323             getvars = context['request'].GET.copy()
324             if 'page%s' % page_suffix in getvars:
325                 del getvars['page%s' % page_suffix]
326             if len(getvars.keys()) > 0:
327                 to_return['getvars'] = "&%s" % getvars.urlencode()
328             else:
329                 to_return['getvars'] = ''
330         return to_return
331     except (KeyError, AttributeError):
332         return {}
333
334
335 register = Library()
336 register.tag('paginate', do_paginate)
337 register.tag('autopaginate', do_autopaginate)