Pagination in Django

As situation stands, In every page of our blog, we are displaying all the posts at once. We don't have much posts at this point, but if we had hundreds or thousands of posts then loading them all at once, could take a while. Implementing pagination not only makes site load faster, it is also great in terms of site usability.

Django provides a class named Paginator which allows us to create paginated records. To use it, import it from django.core.paginator module.

from django.core.paginator import Paginator

To use pagination we, have to create a Paginator object first. Paginator constructor has the following syntax:

paginator_object = Paginator(object_list, records_per_page)

The object_list can be tuple, list, Queryset, etc.
The records_per_page is number of records you want to show in each page.

Here is the code to create a Paginator object to show 5 post per page:

paginator = Paginator(Post.objects.all(), 5)

Paginator object has follows attributes and methods, which are frequently used while creating paginated records.

Attribute/Method What it does ?
count count attribute returns the total number of records, across all the pages.
num_pages num_pages attribute returns total number of pages.
page(number) It accepts 1-based page number and returns a Page() object for the given number. If the argument is not a number, then it throws PageNotAnInteger exception. However, If the the page() object for the given number doesn't exists, it throws a EmptyPage exception.

The Page object in turns provide some other useful attributes/methods.

Attribute/Method What it does ?
object_list It returns an iterable object, which contains the records for the page number which we had passed to the Page() method.
number 1 based numeric count of the current page.
has_next() returns True if there is next page. Otherwise False.
has_previous() returns True if there is previous page. Otherwise False.
next_page_number() returns next page number. If next page doesn't exists it throws a django.core.paginator.InvalidPage exception
previous_page_number() returns previous page number. If previous page doesn't exists it throws a django.core.paginator.InvalidPage exception
paginator This attribute returns the Paginated object attached to the Page object.

Open Django shell and let's try some of the things we have learned so far:

>>>
>>> from django.core.paginator import Paginator
>>> from blog.models import Post
>>>
>>> p = Paginator(Post.objects.all(), 3)   # Creating a Paginator object to show 3 posts per page
>>>
>>> type(p)
<class 'django.core.paginator.Paginator'>
>>>
>>> p.count      # total number of records across all the pages
10
>>>
>>> p.num_pages    # total number of pages
4

Get the records for the first page:

>>>
>>> page1 = p.page(1)   # creating Page object for the first page.
>>> page1
<Page 1 of 4>
>>>
>>> type(page1)
<class 'django.core.paginator.Page'>
>>>
>>> page1.object_list   # get a list of posts for the first page.
<QuerySet [<Post: this is title>, <Post: this is title 2>, <Post: Post created f
rom category>]>
>>>
>>> page1.number
1
>>>
>>> page1.has_previous()
False
>>>
>>> page1.has_next()
True
>>>
>>> page1.next_page_number()
2
>>>
>>> page1.previous_page_number()
Traceback (most recent call last):  
...
    raise EmptyPage('That page number is less than 1')
django.core.paginator.EmptyPage: That page number is less than 1
>>>

We can also access the Paginator object which created page1 object in the first place.

>>>
>>> page1.paginator
<django.core.paginator.Paginator object at 0x0000000004331550>
>>>

Infact, object pointed to by page1.paginator and p are same. We can verify this fact by executing the following code.

>>>
>>> page1.paginator == p
True
>>>

Once we have access to Paginator object we can access it's attributes and methods too.

>>>
>>> page1.paginator.num_pages
4
>>>

Get the records for the second page:

>>>
>>> page2 = p.page('2')      # creating Page object for the second page.
>>>                          # notice that we are passing a string with integer value
>>>
>>> page2.object_list
<QuerySet [<Post: Post from tag object>, <Post: POST TITLE>, <Post: The Great Dj
ango Blog>]>
>>>
>>> page2.number
2
>>>
>>> page2.has_previous()
True
>>>
>>> page2.has_next()
True
>>>
>>> page2.previous_page_number()
1
>>> page2.next_page_number()
3
>>>
>>> pagea = p.page('a')     # Here we are passing string with a non integer value.
....
    raise PageNotAnInteger('That page number is not an integer')
django.core.paginator.PageNotAnInteger: That page number is not an integer
>>>
>>> page100 = p.page(100)    # Trying to access records for 100th page
...
    raise EmptyPage('That page contains no results')
django.core.paginator.EmptyPage: That page contains no results
>>>

I hope you get the idea here.

Once we have access to the Page object, we can use it in our template to loop over each item in the page. Consider the following code:

>>>
>>> from blog.models import Post
>>>
>>> p = Paginator(Post.objects.all(), 3)
>>> page1 = p.page(1)
>>>
>>>
>>> from django import template
>>>
>>> t = template.Template("{% for post in posts %}<p>{{post.title}}</p>{% endfor %}")
>>>
>>> c = template.Context({'posts': page1 })
>>>
>>> t.render(c)
'<p>Post 1</p><p>Post 2</p><p>Post from Java Category</p>'
>>>

So all we need to do to use pagination is to pass Page object as context variable to template. That's it. We don't need to modify our for tag to use Page object in anyway.

In the following few sections we will implement pagination in post list, post by category, post by tag and post by author page.

Implementing pagination in post_list() view #

Our goal in this section is to build implement pagination which allows user to access posts as follows:

Visit http://127.0.0.1:8000/ or http://127.0.0.1:8000/?page=1 to view posts in the first page
Visit http://127.0.0.1:8000/?page=2 to view posts in the second page
Visit http://127.0.0.1:8000/?page=3 to view posts in the third page
and so on.

?page=3 part of the URL is called query string.

Open views.py and modify post_list() as follows:

TGDB/django_project/blog/views.py

...
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage

...
# view function to display a list of posts
def post_list(request):
    post_list = Post.objects.order_by("-id").all()
    paginator = Paginator(post_list, 5)

    # get the page parameter from the query string
    # if page parameter is available get() method will return empty string ''
    page = request.GET.get('page')

    try:
        # create Page object for the given page
        posts = paginator.page(page)
    except PageNotAnInteger:
        # if page parameter in the query string is not available, return the first page
        posts = paginator.page(1)
    except EmptyPage:
        # if the value of the page parameter exceeds num_pages then return the last page
        posts = paginator.page(paginator.num_pages)

    return render(request, 'blog/post_list.html', {'posts': posts})

The code for creating paginated records (from line 8 to 22) will be same, all across the views. As a result instead of copying and pasting the same code multiple times, create a new file named helpers.py in Django project configuration directory i.e TGDB/django_project/django_project (same place where settings.py is located) and put the pagination code in a function named pg_records(), which is defined as follows.

TGDB/django_project/django_project/helpers.py

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

def pg_records(request, list, num):
    print(request)
    paginator = Paginator(list, num)

    # get the page parameter from the query string
    # if page parameter is available get() method will return empty string ''
    page = request.GET.get('page')

    try:
        # create Page object for the given page
        page_object = paginator.page(page)
    except PageNotAnInteger:
        # if page parameter in the query string is not available, return the first page
        page_object = paginator.page(1)
    except EmptyPage:
        # if the value of the page parameter exceeds num_pages then return the last page
        page_object = paginator.page(paginator.num_pages)

    return page_object

To use helper function inside our views add the following code to the end of the import list in views.py.

TGDB/django_project/blog/views.py

...
from django_project import helpers

Replace the code for pagination in post_list() view with the following code.

posts = helpers.pg_records(request, post_list, 5)

At this stage, post_list() view should like this:

TGDB/django_project/blog/views.py

...
def post_list(request):
    post_list = Post.objects.order_by("-id").all()
    posts = helpers.pg_records(request, post_list, 5)
    return render(request, 'blog/post_list.html', {'posts': posts})
...

To add pagination links to our template, open post_list.html and add the following code just below the closing <div class="content"> tag.

TGDB/django_project/blog/templates/blog/post_list.html

<div class="pagination">
    <div class="section-inner clearfix">
        <p>
            {% if posts.has_previous %}
                <a href="?page={{ posts.previous_page_number }}">&lt; Prev</a> |
            {% endif %}

            {% if posts.has_next %}
                <a href="?page={{ posts.next_page_number }}">Next &gt;</a>
            {% endif %}

            <span>Page {{ posts.number }} of {{ posts.paginator.num_pages }}</span>
        </p>
    </div>
</div>

Save the file and visit http://127.0.0.1:8000/ to see the pagination in action.

pagination-in-home-page-QWBJKI.png

Implementing pagination in other pages #

To add pagination to Post by category, Post by author and Post by tag page first modify
post_by_category(), post_by_tag() and post_by_author() respectively in views.py file as follows:

TGDB/django_project/blog/views.py

...

# view function to display post by category
def post_by_category(request, category_slug):
    category = get_object_or_404(Category, slug=category_slug)
    posts = get_list_or_404(Post.objects.order_by("-id"), category=category)
    posts = helpers.pg_records(request, posts, 5)
    context = {
        'category': category,
        'posts': posts
    }
    return render(request, 'blog/post_by_category.html', context)


# view function to display post by tag
def post_by_tag(request, tag_slug):
    tag = get_object_or_404(Tag, slug=tag_slug)
    posts = get_list_or_404(Post.objects.order_by("-id"), tags=tag)
    posts = helpers.pg_records(request, posts, 5)
    context = {
        'tag': tag,
        'posts': posts
    }
    return render(request, 'blog/post_by_tag.html', context )


# view function to display post by author
def post_by_author(request, name):
    author = get_object_or_404(Author, name=name)
    posts = get_list_or_404(Post.objects.order_by("-id"), author=author)
    posts = helpers.pg_records(request, posts, 5)
    context = {
        'author': author,
        'posts': posts
    }
    return render(request, 'blog/post_by_author.html', context )

Finally, add the following code to show pagination links to post_by_category.html, post_by_tag.html and post_by_author.html just after the closing <div class="content"> tag in the content block.

<div class="pagination">
    <div class="section-inner clearfix">
        <p>
            {% if posts.has_previous %}
                <a href="?page={{ posts.previous_page_number }}">&lt; Prev</a> |
            {% endif %}

            {% if posts.has_next %}
                <a href="?page={{ posts.next_page_number }}">Next &gt;</a>
            {% endif %}

            <span>Page {{ posts.number }} of {{ posts.paginator.num_pages }}</span>
        </p>
    </div>
</div>

Now all the four pages in our blog have pagination.