Monday 25 May 2009

Pagination: Splitting Your Data Into Bite-Size Chunks For Easier Digestion

This week it's time for something nice and simple, Django has some very useful tools for handling pagination and that is what will be covered.

If you have a site that is constantly having to return long lists of results/data, then sooner or later you are probably going to want to look for a way to easily split that data over several pages so that the user can view it in easier to read chunks. Django's pagination framework is one quick and easy answer to such a problem. Take the example given below:

#File: views.py
from django.shortcuts import render_to_response
from django.template import RequestContext

def pagination_test(request, word_string):
word_chars = list(word_string)
anagrams = []
for anagram in _generate_anagrams(word_chars):
anag = "".join(anagram)
if anag not in anagrams:
anagrams.append(anag)
response_dict = {'anagrams':anagrams,
'original':word_string,
}
context_instance = RequestContext(request, response_dict)
return render_to_response('pagination_test.html',
context_instance=context_instance)

def _generate_anagrams(word_chars):
# Warning - This function is horrendously inefficient and should
# not really be used for words longer than 6 characters.
char_count = len(word_chars)
if char_count == 0:
yield []
elif char_count == 1:
yield [word_chars[0]]
else:
for char_index in range(len(word_chars)):
lone_char = [word_chars[char_index]]
other_chars=word_chars[:char_index]+word_chars[char_index+1:]
for others in _generate_anagrams(other_chars):
yield lone_char + others
{# File: pagination_test.html #}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Anagrams of {{ original }}</title>
</head>
<body>
<h1>Anagrams of {{ original }}</h1>
<ul>
{% for anagram in anagrams %}
<li>{{ anagram }}</li>
{% endfor %}
</ul>
</body>
</html>
#File: urls.py
from django.conf.urls.defaults import *

urlpatterns = patterns('views',
# Other urls ...
url(r'^test/pagination/(?P<word_string>.+)/$,
'pagination_test', name='pagination_test')
)

Here we take a string as an input from the user and return all the possible unique combinations that can be achieved by rearranging its characters. For short strings this produces manageable numbers of results but even getting up to five character strings the list starts to increase in size dramatically (something like order n^2). What we can do here though is put it through Django's pagination framework like so:

#File: views.py
def pagination_test(request, word_string, page_index=1):
word_chars = list(word_string)
anagrams = []
for anagram in _generate_anagrams(word_chars):
anag = "".join(anagram)
if anag not in anagrams:
anagrams.append(anag)
paginator = Paginator(anagrams, 10, 5)
try:
page = paginator.page(page_index)
except (EmptyPage, InvalidPage), e:
page = paginator.page(paginator.num_pages)

response_dict = {'anagrams':page,
'original':word_string,
}
context_instance = RequestContext(request, response_dict)
return render_to_response('pagination_test.html',
context_instance=context_instance)

def _generate_anagrams(word_chars):
# Same function as before

In the code above, paginator = Paginator(anagrams, 10, 5) sets up a paginator on our results list. The first argument to Paginator says that we want the items in the anagrams list to be split into pages. The second argument says that each page should contain ten results, whilst the third argument says that if there would not be at least five results on the final page they should be added to the one before (meaning the last page will contain anything from five to fourteen results). The other addition is responsible for saying which page of results should be displayed, first of all trying the value passed in and if it's a non-existent page then using the last available page.

{# File: pagination_test.html #}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Anagrams of {{ original }}</title>
</head>
<body>
<h1>Anagrams of {{ original }}</h1>
<div>
Results {{ anagrams.start_index }}
to {{ anagrams.end_index }}
of {{ anagrams.paginator.count }}
</div>

<ul>
{% for anagram in anagrams.object_list %}
<li>{{ anagram }}</li>
{% endfor %}
</ul>
<div>
{% if anagrams.has_previous %}
<a href="{% url paged_pagination_test original anagrams.previous_page_number %}">Previous</a>
{% endif %}
{% if anagrams.has_next %}
<a href="{% url paged_pagination_test original anagrams.next_page_number %}">Next</a>
{% endif %}
</div>

</body>
</html>

In the template the main additions are for saying which results we're viewing through the use of start_index and end_index which are functions that belong to pagination page objects representing the first and last indices of the results on display and count which is a function on the paginator itself representing the total number of results.

In the loop that iterates over the results, it needs to be told to iterate over the object_list attribute of the pagination page as this represents the actual results to be shown on this page.

Finally the section at the bottom of the page is responsible for adding Next and Prev links. has_previous and has_next are pagination page functions that do exactly what you would expect and tell you if there is a page of results available either before or after the current one. Meanwhile previous_page_number and next_page_number return the indices of the page before and page after.

#File: urls.py
from django.conf.urls.defaults import *

urlpatterns = patterns('views',
# Other urls ...
url(r'^test/pagination/(?P<word_string>[^/]+)/$',
'pagination_test', name='pagination_test'),
url(r'^test/pagination/(?P<word_string>.+)/(?P<page_index>\d+)/$',
'pagination_test', name='paged_pagination_test'),

)

An extra line in the urls file picks up any visits to our test url that has a page index included on the end.

If you now try visiting this with a shortish word like "hop" you should be presented with a single page containing all six possible anagrams. Try again with "shop" and you should get two pages worth of results with ten on the first page and fourteen on the second. Finally try with "shops" and you should get six pages of ten results.

That's it for this weeks tutorial, tune in again next week where I will be starting to look at Django's middleware system.

1 comment:

  1. You just saved my day with your code example. Thanks!

    ReplyDelete