Digg-style pagination in Django
Since I’ve finally been picked up by the Django community aggregator (Thanks Jacob!), I figured I’d put out a little snippet for people to use/critique; hopefully more use than critique.
I really, really liked the PaginatorTag on the Django Wiki, but I’ve always wanted my sites to have configurable Digg-like behavior; if you wanna know what I’m talking about, just check out how pagination works on Digg.
Here’s how the PaginatorTag looks compared to the Digg-style tags.

Like the PaginatorTag, this tag is a very basic inclusion tag that builds on the variables already set on the context when paginating with the generic object_list view. There are a few additional context variables created:
- page_numbers – a list of page numbers to display
- in_leading_range – boolean if the page is within the leading range
- in_trailing_range – boolean if the page is within the trailing range
- pages_outside_leading_range – a list of page numbers outside the leading range
- pages_outside_trailing_range – a list of page numbers outside the trailing range
If you don’t understand what these are, don’t worry – I don’t remember either.
I could’ve just appended what needed to be displayed in page_numbers, but instead, I broke it out into what needed to be displayed before and after the actual pages so one could customize the code a little more. In any case, without further adieu, here’s the code snippet:
The first thing I did was create a file called digg_paginator.py.
from django import template register = template.Library() LEADING_PAGE_RANGE_DISPLAYED = TRAILING_PAGE_RANGE_DISPLAYED = 10 LEADING_PAGE_RANGE = TRAILING_PAGE_RANGE = 8 NUM_PAGES_OUTSIDE_RANGE = 2 ADJACENT_PAGES = 4 def digg_paginator(context): if (context["is_paginated"]): " Initialize variables " in_leading_range = in_trailing_range = False pages_outside_leading_range = pages_outside_trailing_range = range(0) if (context["pages"] <= LEADING_PAGE_RANGE_DISPLAYED): in_leading_range = in_trailing_range = True page_numbers = [n for n in range(1, context["pages"] + 1) if n > 0 and n <= context["pages"]] elif (context["page"] <= LEADING_PAGE_RANGE): in_leading_range = True page_numbers = [n for n in range(1, LEADING_PAGE_RANGE_DISPLAYED + 1) if n > 0 and n <= context["pages"]] pages_outside_leading_range = [n + context["pages"] for n in range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)] elif (context["page"] > context["pages"] - TRAILING_PAGE_RANGE): in_trailing_range = True page_numbers = [n for n in range(context["pages"] - TRAILING_PAGE_RANGE_DISPLAYED + 1, context["pages"] + 1) if n > 0 and n <= context["pages"]] pages_outside_trailing_range = [n + 1 for n in range(0, NUM_PAGES_OUTSIDE_RANGE)] else: page_numbers = [n for n in range(context["page"] - ADJACENT_PAGES, context["page"] + ADJACENT_PAGES + 1) if n > 0 and n <= context["pages"]] pages_outside_leading_range = [n + context["pages"] for n in range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)] pages_outside_trailing_range = [n + 1 for n in range(0, NUM_PAGES_OUTSIDE_RANGE)] return { "base_url": context["base_url"], "is_paginated": context["is_paginated"], "previous": context["previous"], "has_previous": context["has_previous"], "next": context["next"], "has_next": context["has_next"], "results_per_page": context["results_per_page"], "page": context["page"], "pages": context["pages"], "page_numbers": page_numbers, "in_leading_range" : in_leading_range, "in_trailing_range" : in_trailing_range, "pages_outside_leading_range": pages_outside_leading_range, "pages_outside_trailing_range": pages_outside_trailing_range } register.inclusion_tag("digg_paginator.html", takes_context=True)(digg_paginator)
To give a brief explanation of the file:

(LEADING_PAGE_RANGE and TRAILING_PAGE_RANGE is the number of pages before the views switch from the top view to the bottom view).
Also, there is a variable passed in from the view function called base_url which is basically the portion of the url before /page/. Don’t worry, you’ll see what I mean after the template file. Basically, I needed this url because I reconstruct the pagination links to:
http://yoursite.com/model/page/page_number/records/num_records
Next, I created a file called digg_paginator.html.
{% spaceless %}
{% if is_paginated %}
<div class="paginator">
{% if has_previous %}<span class="prev"><a href="{{base_url}}/page/{{ previous }}/records/{{results_per_page}}" title="Previous Page">« Previous</a></span>{% else %}<span class="prev-na">« Previous</span>{% endif %}
{% if not in_leading_range %}
{% for num in pages_outside_trailing_range %}
<span class="page"><a href="{{base_url}}/page/{{ num }}/records/{{results_per_page}}" >{{ num }}</a></span>
{% endfor %}
...
{% endif %}
{% for num in page_numbers %}
{% ifequal num page %}
<span class="curr" title="Current Page">{{ num }}</span>
{% else %}
<span class="page"><a href="{{base_url}}/page/{{ num }}/records/{{results_per_page}}" title="Page {{ num }}">{{ num }}</a></span>
{% endifequal %}
{% endfor %}
{% if not in_trailing_range %}
...
{% for num in pages_outside_leading_range reversed %}
<span class="page"><a href="{{base_url}}/page/{{ num }}/records/{{results_per_page}}" >{{ num }}</a></span>
{% endfor %}
{% endif %}
{% if has_next %}<span class="next"><a href="{{base_url}}/page/{{ next }}/records/{{results_per_page}}" title="Next Page">Next »</a></span>{% else %}<span class="next-na">Next »</span>{% endif %}
</div>
{% endif %}
{% endspaceless %}Notice what the next, previous, and page urls look like. You’ll have to make sure your urls.py file reflects this. Here’s a snippet of what my urls.py looks like:
url(r'^browse/all/restaurants/page/(?P<page>[\d]+)/records/(?P<records>[\d]+)/$', restaurant_views.browse_all),
Finally, just so you can have the same Django-like color-scheme, here’s the relevant css.
/** PAGINATOR **/ .paginator { padding:.5em .75em; float:left; font:normal .8em arial; } .paginator .prev-na, .paginator .next-na { padding:.3em; font:bold .875em arial; } .paginator .prev-na, .paginator .next-na { border:1px solid #ccc; background-color:#f9f9f9; color:#aaa; font-weight:normal; } .paginator .prev a, .paginator .prev a:visited, .paginator .next a, .paginator .next a:visited { border:1px solid #c2ee62; background-color:#edfdd0; color:#234f32; padding:.3em; font:bold .875em arial; } .paginator .prev, .paginator .prev-na { margin-right:.5em; } .paginator .next, .paginator .next-na { margin-left:.5em; } .paginator .page a, .paginator .page a:visited, .paginator .curr { padding:.25em; font:normal .875em verdana; border:1px solid #C2EE62; background-color:#EDFDD0; margin:0em .25em; color:#006000; } .paginator .curr { background-color:#234f32; color:#fff; border:1px solid #234f32; font-weight:bold; font-size:1em; } .paginator .page a:hover, .paginator .curr a:hover, .paginator .prev a:hover, .paginator .next a:hover { color:#fff; background-color:#234f32; border:1px solid #234f32; }
Btw, I do realize that I didn’t do any checking of the variables in the digg_paginator.py. I’m assuming that the developer won’t put in strange values. (I know, I know – bad assumption). If you wanna see what I’m talking about, try these values:
LEADING_PAGE_RANGE_DISPLAYED = TRAILING_PAGE_RANGE_DISPLAYED = 10 LEADING_PAGE_RANGE = TRAILING_PAGE_RANGE = 4 NUM_PAGES_OUTSIDE_RANGE = 6 ADJACENT_PAGES = 6
Oh, one last thing. To use the tag, it’ll look something like this in your templates:
{% load utils.paginator.digg_paginator %} {% digg_paginator %}
Oh, and since this is on my blog, feel free to take/use/steal/distribute/copy/modify any code you see fit, but since I threw this together in a few hours, if you find any bugs, have any comments, or think the code can be cleaner, I’d love to hear from you.






September 6th, 2007 at 7:54 pm
PaginatorTag was originally based on Invision Power Board’s topic pagination; the most recent paginator inclusion tag I’ve written was based on PHPBB3’s topic pagination:
http://www.jonathanbuchanan.plus.com/repos/forum/templatetags/forum_tags.py
I wonder if there’d be any value in pulling these different pagination schemes into a standalone pagination application (which could consist solely of templatetags, like django.contrib.humanize). The hardest part might be thinking up a suitable name for each pagination scheme, assuming you wouldn’t want too call them things like “digg_paginator” or “phpbb3_paginator” in such a project!
Or how about defining different possible components of pagination schemes like you’ve done for the Digg one here (in terms of ranges, adjacent pages, intelligent separators to plug gaps, next/first/last links, etc. etc.) and allowing the user to specify *any* combination of these components, providing some pre-built pagination schemes using this method in place of hard-coded ones. Overkill? Perhaps
September 7th, 2007 at 8:18 am
Jonathan – I’m totally down.
I’ll send you an email about it later, but I was looking at http://code.djangoproject.com/browser/django/trunk/django/templatetags/comments.py?rev=3 and I figure I should wrap the paginator template into something similar.
September 8th, 2007 at 7:53 am
Hi Ryan,
thanks for sharing this code, I just integrated it into an application I’m currently working on and it fits perfectly (just had to edit the css).
September 11th, 2007 at 4:24 pm
[...] Ryan Kanno: The diary of an Enginerd in Hawaii » Digg-style pagination in Django Since I’ve finally been picked up by the Django community aggregator (Thanks Jacob!), I figured I’d put out a little snippet for people to use/critique; hopefully more use than critique. I really, really liked the PaginatorTag on the Django Wiki, b (tags: django template tags pagination) [...]
October 2nd, 2007 at 10:15 pm
[...] Everything you’ve ever thought, but never had the balls to say. « Digg-style pagination in Django [...]
December 22nd, 2007 at 5:55 pm
Excellent work. Thanks a ton.
I found one minor bug.. the first page count condition should be changed so you don’t get a situation like 1 2 3 4 5 6 7 8 9 10 .. 10 11. A 12 page result is odd too. Instead I suggest:
if (context["pages"]
December 30th, 2007 at 12:53 am
@Jon -
If you post the fix, I’d love to integrate it.
January 2nd, 2008 at 7:37 pm
Argh. Yeah, sorry.. somehow my comment was truncated before. Anyway it’s a simple adjustment — just change the first condition to:
if (context["pages"] <= LEADING_PAGE_RANGE_DISPLAYED + NUM_PAGES_OUTSIDE_RANGE + 1):
So if the page count is <= 13 (when using the values you’ve defined) then it will simply show all the pages. Without this change, 11, 12 and 13-page results are somewhat strange.
February 24th, 2008 at 9:22 pm
[...] Digg-style pagination in Django | Ryan Kanno: The diary of an Enginerd in Hawaii Since I’ve finally been picked up by the Django community aggregator (Thanks Jacob!), I figured I’d put out a little snippet for people to use/critique; (tags: Django pagination) [...]
March 5th, 2008 at 5:27 am
Excellent work!
I only make my own css, using only arrows for previous, next and … in between. All items in paginator have same width. So I did little calculation for global numbers to get always the same width for paginator:
ADJACENT_PAGES = 3
NUM_PAGES_OUTSIDE_RANGE = 3
WIDTH = 2*ADJACENT_PAGES + NUM_PAGES_OUTSIDE_RANGE
LEADING_PAGE_RANGE_DISPLAYED = TRAILING_PAGE_RANGE_DISPLAYED = WIDTH + 2
LEADING_PAGE_RANGE = TRAILING_PAGE_RANGE = WIDTH
March 6th, 2008 at 12:35 am
[...] is at least one such paginator for Django around, but it’s based on an inclusion tag – a concept I am not too fond of. [...]
April 10th, 2008 at 9:26 pm
hi everybody..
i need to do the paging part for my list of records…
i try to integrate with ur code but i can’t do it..
could anybody help me please…
April 14th, 2008 at 12:44 pm
@Sally -
As an FYI, I haven’t updated this snippet for Changeset 7306 that Adrian committed implementing a “new and improved Paginator class”. I’m not sure if you’re running off of trunk, but could this possibly be a source of the problem? As an added bonus, what’s the error message you’re receiving.
April 25th, 2008 at 3:01 pm
KeyError for ‘base_url.’ It seems that it can’t find base_url in context.
April 30th, 2008 at 6:40 pm
@Huy –
Oh, base_url is a context variable that I set. Basically, it’s just the base URL because I don’t use a key/value pair (ie, ?page=4)… instead, I decided to use the URL http://some_url.com/page/4, so to create the page links, I need http://some_url.com/, and that’s what base_url is equal to.
May 21st, 2008 at 8:09 am
hmm.. and could you provide some info, on how to set that context variable of base_url ? i’m running into the same problem as Huy T. does…
May 22nd, 2008 at 2:16 am
@Huy, @Sudoku -
I set the context variable called base_url in my view as such:
import re
base_url = re.split(’/page/’, request.path)[0]
or if you like to precompile:
import re
regex = re.compile(’/page/’)
base_url = regex.split(request.path)[0]
Hope that helps!
November 26th, 2008 at 9:10 pm
Doesn’t contrib/admin have a paginator similar to this already? I’ve seen it when I have a ton of objects in a model.
December 29th, 2008 at 12:11 pm
Looks really cool. I’m trying to figure out how to put this into an app and would hugely appreciate a little help as can’t get it to work so far! This is what ?i did: Created folder called templatetags inside app folder, created __init__.py and digg_paginator.py. Then put digg_paginator.html inside project templates folder. Lastly I put this in the appropriate template file:
{% load digg_paginator %}
{% digg_paginator %}
{% endif %}
When I try loading the appropriate template I get the following TemplateSyntaxError message: Invalid block tag: ‘endif’
How can I fix the problem? Am I on the right track at least? Many thanks in advance!
December 29th, 2008 at 3:52 pm
@Brasilone – Sorry about that, that endif shouldn’t be there.
January 18th, 2009 at 10:52 am
I love this tag. Can I use this with google app engine?
January 19th, 2009 at 3:43 pm
@Manisha – I’m actually in the process of migrating a ton of things to GAE. There are other tags out there that are probably doing things in a better fashion since I just threw this up. I’ll probably work on the migration into GAE and post any updates to this code that I might have. Or… if you’d like to, feel free to link to it from here!
January 20th, 2009 at 10:37 am
Thanks Ryan for the reply.
Your basic code works fine with GAE too, except I haven’t tried to make it a tag. Here is my code (a simple port of your code).
def get(self, page=1): try: page = int(page) except: page = 1 data = MyData.all().order('keyname') paginator = ObjectPaginator(data,10) if page>=paginator.pages: page = paginator.pages in_leading_range = in_trailing_range = False pages_outside_leading_range = pages_outside_trailing_range = range(0) context_pages = paginator.pages if (context_pages 0 and n <= context_pages] elif (page 0 and n context_pages - TRAILING_PAGE_RANGE): in_trailing_range = True page_numbers = [n for n in range(context_pages - TRAILING_PAGE_RANGE_DISPLAYED + 1, context_pages + 1) if n > 0 and n 0 and n <= context_pages] pages_outside_leading_range = [n + context_pages for n in range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)] pages_outside_trailing_range = [n + 1 for n in range(0, NUM_PAGES_OUTSIDE_RANGE)] template_values = { 'data':paginator.get_page(page-1), "base_url": "/BrowsemyData", "previous": page-1, "has_previous": paginator.has_previous_page(page-1), "next": page+1, "has_next": paginator.has_next_page(page-1), "results_per_page": 10, "page": page, "pages": context_pages, "page_numbers": page_numbers, "in_leading_range" : in_leading_range, "in_trailing_range" : in_trailing_range, "pages_outside_leading_range": pages_outside_leading_range, "pages_outside_trailing_range": pages_outside_trailing_range } path = os.path.join(os.path.dirname(__file__), 'templates/sample.html') self.response.out.write(template.render(path,template_values))I used your digg_paginator.html and css with minor modifications. It works like a charm, you can check it out at my website Indian Baby Names.
Thanks for the code.
February 23rd, 2009 at 1:27 pm
Useful Tag. I started building the paginator in my own way till I stumbled upon this.
My question is if my base_url contains other get params like http://www.someurl.com/page?type=‘x’
how can i add the page=’x’ to this. base_url does not seem to give me the entire url, it gives it only till http://www.someurl.com/page.
February 27th, 2009 at 5:14 am
Thanks guy! Genious… genious…
March 19th, 2009 at 5:12 pm
[...] This image was Google Image borrowed from Ryan Kanno [...]
March 19th, 2009 at 5:51 pm
Nice approach, I’ve aways wondered why Django doesn’t have a generic built in tag for this. I implemented a similar solution but I didn’t use any context variables. Basically the tag takes an object query and re-casts it into a new object. The upside is that there is no query overhead as Django is lazy with queries. It also makes for a simple integration, just one line of code and no need to re-code the handler (except current page).
Digg Style Pagination In Django Revisited
I focused on doing a single pass for loop, the idea was to save loop time. Anyway I just thought I’d let you know I used one of your images on my re-post regarding pagination in Django.
-Paul
March 19th, 2009 at 9:17 pm
@Paul
I’ve always wondered the same. There have been better OO approaches to the same problem (including yours), but the implementation was so simple that I didn’t mind it being dirty at the time.
I took a look at your solution, and I love it. +1
June 21st, 2009 at 6:44 am
Hi. great work man!!!.
why do I have to modify my view, if you said “you only need to modify your template”?
my original view:
def index(request):
productos = Producto.objects.all()
return render_to_response(’productos/index.html’,
{’productos’: productos
})
view modified to:
from django.template import RequestContext
def index(request):
productos = Producto.objects.all()
return render_to_response(’productos/index.html’,
{’productos’: productos
}, context_instance=RequestContext(request))
Please leave a reply »