Ryan Kanno: The diary of an Enginerd in Hawaii

Everything you've ever thought, but never had the balls to say.

My LinkedIn Profile
Follow @ryankanno on Twitter
My Feed

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.

Different paginators

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:

Explanation of pagination 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">&laquo; Previous</a></span>{% else %}<span class="prev-na">&laquo; 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 &raquo;</a></span>{% else %}<span class="next-na">Next &raquo;</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. :)

Enjoy!

36 responses to “Digg-style pagination in Django
  1. 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 :)

  2. 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.

  3. 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).

  4. [...] 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) [...]

  5. [...] Everything you’ve ever thought, but never had the balls to say. « Digg-style pagination in Django [...]

  6. 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"]

  7. @Jon –

    If you post the fix, I’d love to integrate it. :)

  8. 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.

  9. [...] 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) [...]

  10. 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

  11. [...] 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. [...]

  12. 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…

  13. @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. :)

  14. KeyError for ‘base_url.’ It seems that it can’t find base_url in context.

  15. @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.

  16. 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… :(

  17. @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!

  18. 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.

  19. 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!

  20. @Brasilone – Sorry about that, that endif shouldn’t be there.

  21. I love this tag. Can I use this with google app engine?

  22. @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! :)

  23. 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.

  24. 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.

  25. Thanks guy! Genious… genious…

  26. [...] This image was Google Image borrowed from Ryan Kanno [...]

  27. 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

  28. @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 :)

  29. 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))

  30. Good “snippet” xDD

  31. Wow! Why do you delete my comment? I said ironically “snippet” …btw , good job! (and now i hope not delete the comment..) xD

  32. great work Ryan!
    your code works great, thank you.

  33. Thanks for sharing. I wasn’t able to get it to work. Using Django v1.3. I get

    In the URL Conf using the format you described I get:
    TypeError at /
    url() takes at least 2 arguments (2 given)

  34. @Carlos

    Thanks for letting me know! I haven’t used this in a bit, but I’ll update it for 1.3/1.4. My apologies. :)

  35. Based on your great work, I wrote a new one at [http://djangosnippets.org/snippets/2763/](http://djangosnippets.org/snippets/2763/). The value-added part is merely to retain GET params across different requests.

  36. Based on your great work, I wrote a new one at http://djangosnippets.org/snippets/2763/ . The value-added part is merely to retain GET params across different requests.

Please leave a reply »

Powered by Wordpress. Stalk me.