05c44f381e3bfedc1aec40536c08a38c96561f22
[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             page_obj = paginator.page(context['request'].page(page_suffix))
148         except InvalidPage:
149             if INVALID_PAGE_RAISES_404:
150                 raise Http404('Invalid page requested.  If DEBUG were set to ' +
151                     'False, an HTTP 404 page would have been shown instead.')
152             context[key] = []
153             context['invalid_page'] = True
154             return u''
155         if self.context_var is not None:
156             context[self.context_var] = page_obj.object_list
157         else:
158             context[key] = page_obj.object_list
159         context['paginator'] = paginator
160         context['page_obj'] = page_obj
161         context['page_suffix'] = page_suffix
162         return u''
163
164
165 def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
166     """
167     Renders the ``pagination/pagination.html`` template, resulting in a
168     Digg-like display of the available pages, given the current page.  If there
169     are too many pages to be displayed before and after the current page, then
170     elipses will be used to indicate the undisplayed gap between page numbers.
171     
172     Requires one argument, ``context``, which should be a dictionary-like data
173     structure and must contain the following keys:
174     
175     ``paginator``
176         A ``Paginator`` or ``QuerySetPaginator`` object.
177     
178     ``page_obj``
179         This should be the result of calling the page method on the 
180         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
181         the current page.
182     
183     This same ``context`` dictionary-like data structure may also include:
184     
185     ``getvars``
186         A dictionary of all of the **GET** parameters in the current request.
187         This is useful to maintain certain types of state, even when requesting
188         a different page.
189     
190     ``pagination_template``
191         A custom template to include in place of the default ``pagination/default.html`` 
192         contents.
193         
194     Argument ``window`` is number to pages before/after current page. If window
195     exceeds pagination border (1 and end), window is moved to left or right.
196
197     Argument ``margin``` is number of pages on start/end of pagination. 
198     Example:
199         window=2, margin=1, current=6     1 ... 4 5 [6] 7 8 ... 11 
200         window=2, margin=0, current=1     [1] 2 3 4 5 ...
201         window=2, margin=0, current=5     ... 3 4 [5] 6 7 ...
202         window=2, margin=0, current=11     ... 7 8 9 10 [11]
203         """
204
205     if window < 0:
206         raise ValueError('Parameter "window" cannot be less than zero')
207     if margin < 0:
208         raise ValueError('Parameter "margin" cannot be less than zero')
209     try:
210         paginator = context['paginator']
211         page_obj = context['page_obj']
212         page_suffix = context.get('page_suffix', '')
213         page_range = paginator.page_range
214         pagination_template = context.get('pagination_template', 'pagination/default.html')
215         # Calculate the record range in the current page for display.
216         records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
217         records['last'] = records['first'] + paginator.per_page - 1
218         if records['last'] + paginator.orphans >= paginator.count:
219             records['last'] = paginator.count
220
221         # figure window
222         window_start = page_obj.number - window - 1
223         window_end = page_obj.number + window
224
225         # solve if window exceeded page range
226         if window_start < 0:
227             window_end = window_end - window_start
228             window_start = 0
229         if window_end > paginator.num_pages:
230             window_start = window_start - (window_end - paginator.num_pages)
231             window_end = paginator.num_pages
232         pages = page_range[window_start:window_end]
233
234         # figure margin and add elipses
235         if margin > 0:
236             # figure margin
237             tmp_pages = set(pages)
238             tmp_pages = tmp_pages.union(page_range[:margin])
239             tmp_pages = tmp_pages.union(page_range[-margin:])
240             tmp_pages = list(tmp_pages)
241             tmp_pages.sort()
242             pages = []
243             pages.append(tmp_pages[0])
244             for i in range(1, len(tmp_pages)):
245                 # figure gap size => add elipses or fill in gap
246                 gap = tmp_pages[i] - tmp_pages[i - 1]
247                 if gap >= 3:
248                     pages.append(None)
249                 elif gap == 2:
250                     pages.append(tmp_pages[i] - 1)
251                 pages.append(tmp_pages[i])
252         else:
253             if pages[0] != 1:
254                 pages.insert(0, None)
255             if pages[-1] != paginator.num_pages:
256                 pages.append(None)
257
258         to_return = {
259             'MEDIA_URL': settings.MEDIA_URL,
260             'STATIC_URL': getattr(settings, "STATIC_URL", None),
261             'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
262             'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
263             'display_page_links': DISPLAY_PAGE_LINKS,
264             'is_paginated': paginator.count > paginator.per_page,
265             'next_link_decorator': NEXT_LINK_DECORATOR,
266             'page_obj': page_obj,
267             'page_suffix': page_suffix,
268             'pages': pages,
269             'pagination_template': pagination_template,
270             'paginator': paginator,
271             'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
272             'records': records,
273         }
274         if 'request' in context:
275             getvars = context['request'].GET.copy()
276             if 'page%s' % page_suffix in getvars:
277                 del getvars['page%s' % page_suffix]
278             if len(getvars.keys()) > 0:
279                 to_return['getvars'] = "&%s" % getvars.urlencode()
280             else:
281                 to_return['getvars'] = ''
282         return to_return
283     except (KeyError, AttributeError):
284         return {}
285
286
287 register = template.Library()
288 register.inclusion_tag(
289     'pagination/pagination.html', takes_context=True)(paginate)
290 register.tag('autopaginate', do_autopaginate)