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