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