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