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