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