Pagination: it's not pretty, but it works

Django    2008-07-30

I wanted a simpler solution than django-pagination. Django-pagination looks easy to implement, particularly thanks to Eric Florenzano's stellar screencast, but the actual installation process isn't very well-documented.

Besides, I wanted something that didn't require installing a lot of outside code, and I sure don't want to have to rely on symlinks. Django's documentation of the Paginator class makes it look so simple, after all.

In the urls.py

It's a little messy, but here are all of the possible urlpatterns I have that could lead to a categorized (or not) list of articles, all calling the list_all() method in my view:

urlpatterns = patterns('',
    url(r'^category/(?P\w+)/$', views.list_all, name='articles_list_all'),
    url(r'^category/(?P\w+)/page/(?P\w+)/$', views.list_all, name='articles_list_all'),
    url(r'^page/(?P\w+)/$', views.list_all, name='articles_list_all'),
    url(r'', views.list_all, name='articles_list_all'),
)

In the view

Import Django's Paginator, along with the models you'll need to populate the paginator class:

from django.core.paginator import Paginator
from articles.models import Article, Category

In that list method, I'm setting the category and page_id defaults as additional arguments:

def list_all(request, category=None, page_id=1):
    template_name = 'list_all.html'
    category_list = Category.objects.all().order_by('title')
    article_list = article.objects.all().order_by('-created_at')

The 'category_list' you're seeing here is incidental to the pagination. It's used to populate a select in the template that allows users to filter the article list further.

If the user has selected a category to filter on, we set the category_id to that POST value, and reset the page_id to '1' (for now, that's the value of a hidden field in the template form). It probably makes just as much sense to set 'page_id = 1' here in the view, but I want the flexibility of being able to pass in a value from the form for now:

    if request.method == 'POST':
        category_id = request.POST['category']
        page_id = request.POST['page_id']
    else:
        category_id = category

Filter the list of articles based on the category_id:

    if category_id:
        try:
            article_list = article.objects.filter(articlecategory__category=category_id)
        except ObjectDoesNotExist:
            article_list = None
    else:
        category_id = 0
    category_id = INT(category_id)

And then back to the pagination. Create a paginator object out of the article_list we've already defined, where '3' is the number of objects to display per page. The current page is the page attribute for the page_id we've passed in or set above. And the page range is a list of ... well, the range of pages (something like [1, 2, 3, 4] that we can iterate over in the template):

    paginator = Paginator(article_list, 3)
    page = paginator.page(page_id)
    page_range = paginator.page_range

Then return all that stuff back to the template:

    return render_to_response(template_name, { 'page_range': page_range, 'page': page, 
		'article_list': article_list, 'category_list': category_list, 'category_id': category_id, },
                context_instance=RequestContext(request))

In the template

The form that lets a user select a category to filter by:

{% block content %}

{% if category_list %} <select name="category""> <option value="">-- Select a Category --</option> {% for category in category_list %} <option value="{{ category.id }}"{% ifequal category.id category_id %} selected{% endifequal %}>{{ category.title }}</option> {% endfor %} </select> {% endif %} <input type="submit" value="Go" /> <input type="hidden" name="page_id" value="1"> </form>

Note the default page_id passed in with the POST.

If list_all() returns a page object, we loop over each object in that list and create a link to the detail view for each one:

{% if page.object_list %}
    <ul>
    {% for article in page.object_list %}
    <li><a href="/articles/view/{{ article.id }}/">{{ article.title }}</a></li>
    {% endfor %}
    </ul>
{% else %}
     <p>No articles available </p>
{% endif %}

Then here we're looping over the page_range to create a link for each available page with results:

Page:
{% for page_number in page_range %}
<a href="/articles/{% if category_id %}category/{{ category_id }}/{% endif %}page/{{ page_number }}/">{{ page_number }}</a>
{% endfor %}

{% endblock %}

Varying the number of items displayed

One more thing - if you wanted the option to show more than 3 articles at a time, instead of doing this in the view:

    paginator = Paginator(article_list, 3)

You might do something like this instead:

    paginator = Paginator(article_list, items_per_page)

Where 'items_per_page' is a value passed in from the template, allowing a user to select the number of items they want to see.