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