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