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