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