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