Added support for orphans. Thanks, Martin Mahner.
[django-pagination.git] / pagination / templatetags / pagination_tags.py
1 try:
2     set
3 except NameError:
4     from sets import Set as set
5 from django import template
6 from django.db.models.query import QuerySet
7 from django.core.paginator import Paginator, QuerySetPaginator, InvalidPage
8
9 register = template.Library()
10
11 DEFAULT_PAGINATION = 20
12 DEFAULT_WINDOW = 4
13 DEFAULT_ORPHANS = 0
14
15 def do_autopaginate(parser, token):
16     """
17     Splits the arguments to the autopaginate tag and formats them correctly.
18     """
19     split = token.split_contents()
20     if len(split) == 2:
21         return AutoPaginateNode(split[1])
22     elif len(split) == 3:
23         try:
24             paginate_by = int(split[2])
25         except ValueError:
26             raise template.TemplateSyntaxError(u'Got %s, but expected integer.' % split[2])
27         return AutoPaginateNode(split[1], paginate_by=paginate_by)
28     elif len(split) == 4:
29         try:
30             paginate_by = int(split[2])
31         except ValueError:
32             raise template.TemplateSyntaxError(u'Got %s, but expected integer.' % split[2])
33         try:
34             orphans = int(split[3])
35         except ValueError:
36             raise template.TemplateSyntaxError(u'Got %s, but expected integer.' % split[3])           
37         return AutoPaginateNode(split[1], paginate_by=paginate_by, orphans=orphans)
38     else:
39         raise template.TemplateSyntaxError('%r tag takes one required argument and one optional argument' % split[0])
40
41 class AutoPaginateNode(template.Node):
42     """
43     Emits the required objects to allow for Digg-style pagination.
44     
45     First, it looks in the current context for the variable specified.  This
46     should be either a QuerySet or a list.
47     
48     1. If it is a QuerySet, this ``AutoPaginateNode`` will emit a 
49        ``QuerySetPaginator`` and the current page object into the context names
50        ``paginator`` and ``page_obj``, respectively.
51     
52     2. If it is a list, this ``AutoPaginateNode`` will emit a simple
53        ``Paginator`` and the current page object into the context names 
54        ``paginator`` and ``page_obj``, respectively.
55     
56     It will then replace the variable specified with only the objects for the
57     current page.
58     
59     .. note::
60         
61         It is recommended to use *{% paginate %}* after using the autopaginate
62         tag.  If you choose not to use *{% paginate %}*, make sure to display the
63         list of availabale pages, or else the application may seem to be buggy.
64     """
65     def __init__(self, queryset_var, paginate_by=DEFAULT_PAGINATION, orphans=DEFAULT_ORPHANS):
66         self.queryset_var = template.Variable(queryset_var)
67         self.paginate_by = paginate_by
68         self.orphans = orphans
69
70     def render(self, context):
71         key = self.queryset_var.var
72         value = self.queryset_var.resolve(context)
73         if issubclass(value.__class__, QuerySet):
74             model = value.model
75             paginator_class = QuerySetPaginator
76         else:
77             value = list(value)
78             try:
79                 model = value[0].__class__
80             except IndexError:
81                 return u''
82             paginator_class = Paginator
83         paginator = paginator_class(value, self.paginate_by, self.orphans)
84         try:
85             page_obj = paginator.page(context['request'].page)
86         except InvalidPage:
87             context[key] = []
88             context['invalid_page'] = True
89             return u''
90         context[key] = page_obj.object_list
91         context['paginator'] = paginator
92         context['page_obj'] = page_obj
93         return u''
94
95 def paginate(context, window=DEFAULT_WINDOW):
96     """
97     Renders the ``pagination/pagination.html`` template, resulting in a
98     Digg-like display of the available pages, given the current page.  If there
99     are too many pages to be displayed before and after the current page, then
100     elipses will be used to indicate the undisplayed gap between page numbers.
101     
102     Requires one argument, ``context``, which should be a dictionary-like data
103     structure and must contain the following keys:
104     
105     ``paginator``
106         A ``Paginator`` or ``QuerySetPaginator`` object.
107     
108     ``page_obj``
109         This should be the result of calling the page method on the 
110         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
111         the current page.
112     
113     This same ``context`` dictionary-like data structure may also include:
114     
115     ``getvars``
116         A dictionary of all of the **GET** parameters in the current request.
117         This is useful to maintain certain types of state, even when requesting
118         a different page.
119         """
120     try:
121         paginator = context['paginator']
122         page_obj = context['page_obj']
123         page_range = paginator.page_range
124         # First and last are simply the first *n* pages and the last *n* pages,
125         # where *n* is the current window size.
126         first = set(page_range[:window])
127         last = set(page_range[-window:])
128         # Now we look around our current page, making sure that we don't wrap
129         # around.
130         current_start = page_obj.number-1-window
131         if current_start < 0:
132             current_start = 0
133         current_end = page_obj.number-1+window
134         if current_end < 0:
135             current_end = 0
136         current = set(page_range[current_start:current_end])
137         pages = []
138         # If there's no overlap between the first set of pages and the current
139         # set of pages, then there's a possible need for elusion.
140         if len(first.intersection(current)) == 0:
141             first_list = sorted(list(first))
142             second_list = sorted(list(current))
143             pages.extend(first_list)
144             diff = second_list[0] - first_list[-1]
145             # If there is a gap of two, between the last page of the first
146             # set and the first page of the current set, then we're missing a
147             # page.
148             if diff == 2:
149                 pages.append(second_list[0] - 1)
150             # If the difference is just one, then there's nothing to be done,
151             # as the pages need no elusion and are correct.
152             elif diff == 1:
153                 pass
154             # Otherwise, there's a bigger gap which needs to be signaled for
155             # elusion, by pushing a None value to the page list.
156             else:
157                 pages.append(None)
158             pages.extend(second_list)
159         else:
160             pages.extend(sorted(list(first.union(current))))
161         # If there's no overlap between the current set of pages and the last
162         # set of pages, then there's a possible need for elusion.
163         if len(current.intersection(last)) == 0:
164             second_list = sorted(list(last))
165             diff = second_list[0] - pages[-1]
166             # If there is a gap of two, between the last page of the current
167             # set and the first page of the last set, then we're missing a 
168             # page.
169             if diff == 2:
170                 pages.append(second_list[0] - 1)
171             # If the difference is just one, then there's nothing to be done,
172             # as the pages need no elusion and are correct.
173             elif diff == 1:
174                 pass
175             # Otherwise, there's a bigger gap which needs to be signaled for
176             # elusion, by pushing a None value to the page list.
177             else:
178                 pages.append(None)
179             pages.extend(second_list)
180         else:
181             pages.extend(sorted(list(last.difference(current))))
182         to_return = {
183             'pages': pages,
184             'page_obj': page_obj,
185             'paginator': paginator,
186             'is_paginated': paginator.count > paginator.per_page,
187         }
188         if 'request' in context:
189             getvars = context['request'].GET.copy()
190             if 'page' in getvars:
191                 del getvars['page']
192             if len(getvars.keys()) > 0:
193                 to_return['getvars'] = "&%s" % getvars.urlencode()
194             else:
195                 to_return['getvars'] = ''
196         return to_return
197     except KeyError:
198         return {}
199 register.inclusion_tag('pagination/pagination.html', takes_context=True)(paginate)
200 register.tag('autopaginate', do_autopaginate)