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 %} {% endif %}
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.
