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