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