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