merged patch from flosch for multiple paginators on one page
[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
122 def paginate(context, window=DEFAULT_WINDOW, hashtag=''):
123     """
124     Renders the ``pagination/pagination.html`` template, resulting in a
125     Digg-like display of the available pages, given the current page.  If there
126     are too many pages to be displayed before and after the current page, then
127     elipses will be used to indicate the undisplayed gap between page numbers.
128     
129     Requires one argument, ``context``, which should be a dictionary-like data
130     structure and must contain the following keys:
131     
132     ``paginator``
133         A ``Paginator`` or ``QuerySetPaginator`` object.
134     
135     ``page_obj``
136         This should be the result of calling the page method on the 
137         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
138         the current page.
139     
140     This same ``context`` dictionary-like data structure may also include:
141     
142     ``getvars``
143         A dictionary of all of the **GET** parameters in the current request.
144         This is useful to maintain certain types of state, even when requesting
145         a different page.
146         """
147     try:
148         paginator = context['paginator']
149         page_obj = context['page_obj']
150         page_suffix = context.get('page_suffix', '')
151         page_range = paginator.page_range
152         # Calculate the record range in the current page for display.
153         records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
154         records['last'] = records['first'] + paginator.per_page - 1
155         if records['last'] + paginator.orphans >= paginator.count:
156             records['last'] = paginator.count
157         # First and last are simply the first *n* pages and the last *n* pages,
158         # where *n* is the current window size.
159         first = set(page_range[:window])
160         last = set(page_range[-window:])
161         # Now we look around our current page, making sure that we don't wrap
162         # around.
163         current_start = page_obj.number-1-window
164         if current_start < 0:
165             current_start = 0
166         current_end = page_obj.number-1+window
167         if current_end < 0:
168             current_end = 0
169         current = set(page_range[current_start:current_end])
170         pages = []
171         # If there's no overlap between the first set of pages and the current
172         # set of pages, then there's a possible need for elusion.
173         if len(first.intersection(current)) == 0:
174             first_list = list(first)
175             first_list.sort()
176             second_list = list(current)
177             second_list.sort()
178             pages.extend(first_list)
179             diff = second_list[0] - first_list[-1]
180             # If there is a gap of two, between the last page of the first
181             # set and the first page of the current set, then we're missing a
182             # page.
183             if diff == 2:
184                 pages.append(second_list[0] - 1)
185             # If the difference is just one, then there's nothing to be done,
186             # as the pages need no elusion and are correct.
187             elif diff == 1:
188                 pass
189             # Otherwise, there's a bigger gap which needs to be signaled for
190             # elusion, by pushing a None value to the page list.
191             else:
192                 pages.append(None)
193             pages.extend(second_list)
194         else:
195             unioned = list(first.union(current))
196             unioned.sort()
197             pages.extend(unioned)
198         # If there's no overlap between the current set of pages and the last
199         # set of pages, then there's a possible need for elusion.
200         if len(current.intersection(last)) == 0:
201             second_list = list(last)
202             second_list.sort()
203             diff = second_list[0] - pages[-1]
204             # If there is a gap of two, between the last page of the current
205             # set and the first page of the last set, then we're missing a 
206             # page.
207             if diff == 2:
208                 pages.append(second_list[0] - 1)
209             # If the difference is just one, then there's nothing to be done,
210             # as the pages need no elusion and are correct.
211             elif diff == 1:
212                 pass
213             # Otherwise, there's a bigger gap which needs to be signaled for
214             # elusion, by pushing a None value to the page list.
215             else:
216                 pages.append(None)
217             pages.extend(second_list)
218         else:
219             differenced = list(last.difference(current))
220             differenced.sort()
221             pages.extend(differenced)
222         to_return = {
223             'MEDIA_URL': settings.MEDIA_URL,
224             'pages': pages,
225             'records': records,
226             'page_obj': page_obj,
227             'paginator': paginator,
228             'hashtag': hashtag,
229             'is_paginated': paginator.count > paginator.per_page,
230             'page_suffix': page_suffix,
231         }
232         if 'request' in context:
233             getvars = context['request'].GET.copy()
234             if 'page%s' % page_suffix in getvars:
235                 del getvars['page%s' % page_suffix]
236             if len(getvars.keys()) > 0:
237                 to_return['getvars'] = "&%s" % getvars.urlencode()
238             else:
239                 to_return['getvars'] = ''
240         return to_return
241     except KeyError, AttributeError:
242         return {}
243
244 register.inclusion_tag('pagination/pagination.html', takes_context=True)(
245     paginate)
246 register.tag('autopaginate', do_autopaginate)