Ryan Kanno: The diary of an Enginerd in Hawaii

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

Tag Archive » ‘generic-list_object-view’

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

Enjoy!

Tagged: , , , , , , .


Powered by Wordpress. Stalk me.