TestDriven.io: Pagination in Django

Pagination is the process of breaking large chunks of data up across multiple, discrete web pages. Rather than dumping all the data to the user, you can define the number of individual records you want to be displayed per page and then send back the data that corresponds to the page requested by the user.

The advantage of using this type of technique is that it improves the user experience, especially when there are thousands of records to be retrieved. Implementing pagination in Django is fairly easy as Django provides a Paginator class from which you can use to group content onto different pages.

Pagination can come in different flavors depending on how it is configured by the developer. That said, in this article, we’ll look at how to include pagination with function and class-based views using three different UI flavors.

The example project can be found on the django-pagination-example repo on GitHub.

Objectives

By the end of this article, you will be able to:

  1. Explain what pagination is and why you may want to use it.
  2. Work with Django’s Paginator class and Page objects.
  3. Implement pagination in Django with function and class-based views.

Django Constructs

When implementing pagination in Django, rather than re-inventing the logic required for pagination, you’ll work with the following constructs:

  1. Paginator – splits a Django QuerySet or list into chunks of Page objects.
  2. Page – holds the actual paginated data along with pagination metadata.

Let’s look at some quick examples.

Paginator

from django.contrib.auth.models import User


for num in range(43):
    User.objects.create(username=f"{num}")

Here, we created 43 User objects.

Next, we’ll import the Paginator class and create a new instance:

from django.contrib.auth.models import User
from django.core.paginator import Paginator

users = User.objects.all()

paginator = Paginator(users, 10)

print(paginator.num_pages)  # => 5

The Paginator class takes four parameters:

  1. object_list – any object with a count() or __len__() method, like a list, tuple, or QuerySet
  2. per_page – maximum number of items to include on a page
  3. orphans (optional) – used to prevent the last page from having very few items, defaults to 0
  4. allow_empty_first_page (optional) – as implied by the name, you can raise an EmtpyPage error if you disallow the first page from being empty by setting the argument to False, defaults to True

So, in the above example, we sliced the users into pages (or chunks) of ten. The first four pages will have ten users while the last page will have three.

The Paginator class has the following attributes:

  1. count – total number of objects
  2. num_pages – total number of pages
  3. page_range – range iterator of page numbers

For consistent paginated results, the QuerySet or the model should be ordered.

If you’d prefer to not have just three users on the final page, you can use the orphans argument like so to add the final three users to the previous page:

from django.contrib.auth.models import User
from django.core.paginator import Paginator

users = User.objects.all()

paginator = Paginator(users, 10, orphans=3)

print(paginator.num_pages)  # => 4

So, when the number of remaining objects for the last page is less than or equal to the value of orphans, those objects will be added to the previous page.

Page

After the Django QuerySet has been broken up into Page objects. We can then use the page() method to access the data for each page by passing the page number to it:

from django.contrib.auth.models import User
from django.core.paginator import Paginator

users = User.objects.all()

paginator = Paginator(users, 10)

page_obj = paginator.page(1)

print(page_obj)  # => <Page 1 of 5>

Here, page_obj gives us a page object that represents the first page of results. This can then be used in your templates.

Note that we didn’t literally create a Page instance. Instead, we obtained the instance from the Paginator class.

What happens if the page doesn’t exist?

from django.contrib.auth.models import User
from django.core.paginator import Paginator

users = User.objects.all()

paginator = Paginator(users, 10)

page_obj = paginator.page(99)

You should see:

    raise EmptyPage(_('That page contains no results'))
django.core.paginator.EmptyPage: That page contains no results

Thus, it’s a good idea to catch an EmptyPage exception like so:

from django.contrib.auth.models import User
from django.core.paginator import EmptyPage, Paginator

users = User.objects.all()

paginator = Paginator(users, 10)

try:
    page_obj = paginator.page(99)
except EmptyPage:
    # Do something
    pass

You may want to catch a PageNotAnInteger exception as well.

For more on this, review the Exceptions section from the Paginator documentation.

That said, if you’d prefer not to deal with the EmptyPage or PageNotAnInteger exceptions explicitly, you could use get_page() method instead of page():

from django.contrib.auth.models import User
from django.core.paginator import Paginator

users = User.objects.all()

paginator = Paginator(users, 10)

page_obj = paginator.get_page(99)

print(page_obj)  # => <Page 5 of 5>

So, even though the number 99 is beyond the range, the last page will be returned.

Also, if the page isn’t a valid number get_page() will return, by default, the first page:

from django.contrib.auth.models import User
from django.core.paginator import Paginator

users = User.objects.all()

paginator = Paginator(users, 10)

page_obj = paginator.get_page('foo')

print(page_obj)  # => <Page 1 of 5>

Therefore, both methods — page() or get_page() — can be used depending on your preferences. The examples shown in this article will use page().

The Page object has several attributes and methods that can be used while constructing your template:

  1. number – shows the page number for a given page
  2. paginator – displays the associated Paginator object
  3. has_next() – returns True if there’s a next page
  4. has_previous() – – returns True if there’s a previous page
  5. next_page_number() – returns the number of the next page
  6. previous_page_number() – returns the number of the previous page

Function-based Views

Next, let’s look at how to work with pagination in function-based views:

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

from . models import Employee


def index(request):
    object_list = Employee.objects.all()
    page_num = request.GET.get('page', 1)

    paginator = Paginator(object_list, 6) # 6 employees per page


    try:
        page_obj = paginator.page(page_num)
    except PageNotAnInteger:
        # if page is not an integer, deliver the first page
        page_obj = paginator.page(1)
    except EmptyPage:
        # if the page is out of range, deliver the last page
        page_obj = paginator.page(paginator.num_pages)

    return render(request, 'index.html', {'page_obj': page_obj})

Here, we:

  1. Defined a page_num variable from the URL.
  2. Instantiated the Paginator class passing it the required parameters, the employees QuerySet and the number of employees to be included on each page.
  3. Generated a page object called page_obj, which contains the paginated employee data along with metadata for navigating to the previous and next pages.

https://github.com/testdrivenio/django-pagination-example/blob/main/employees/views.py

Class-based Views

Example of implementing pagination in a class-based view:

from django.views.generic import ListView

from . models import Employee


class Index(ListView):
    model = Employee
    context_object_name = 'employees'
    paginate_by = 6
    template_name = 'index.html'

https://github.com/testdrivenio/django-pagination-example/blob/main/employees/views.py

Templates

Working with pagination in the template is where things start to get interesting, as there are several different implementations. In this article, we’ll look at three different implementations, each showing a different way in which to navigate to the previous and next pages.

You can find the code for each example in the templates folder on the on the django-pagination-example repo on GitHub.

Flavor 1

This is the first flavor implementing the pagination UI.

Pagination UI - first flavor

So, in this example, we have “Previous” and “Next” links that the end-user can click to move from page to page.

index.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
    <title>Pagination in Django</title>
  </head>
  <body>
    <div class="container">
      <h1 class="text-center">List of Employees</h1>
      <hr>

      <ul class="list-group list-group-flush">
        {% for employee in page_obj %}
          <li class="list-group-item">{{ employee }}</li>
        {% endfor %}
      </ul>

      <br><hr>

     {% include "pagination.html" %}
    </div>
  </body>
</html>

pagination.html:

<div>
  <span>
    {% if page_obj.has_previous %}
      <a href="?page={{ page_obj.previous_page_number }}">Previous</a>
    {% endif %}
    <span>
      Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
    </span>
    {% if page_obj.has_next %}
      <a href="?page={{ page_obj.next_page_number }}">Next</a>
    {% endif %}
  </span>
</div>

Keep in mind that the pagination.html template can be reused across many templates.

Flavor 2

Pagination UI - second flavor

pagination.html:

{% if page_obj.has_previous %}
 <a href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% else %}
  <a>Previous</a>
{% endif %}

{% for i in page_obj.paginator.page_range %}
  {% if page_obj.number == i %}
    <a href="#">{{ i }} </a>
  {% else %}
    <a href="?page={{ i }}">{{ i }}</a>
  {% endif %}
{% endfor %}

{% if page_obj.has_next %}
  <a href="?page={{ page_obj.next_page_number }}">Next</a>
{% else %}
  <a>Next</a>
{% endif %}

This flavor presents all the page numbers in the UI, making it easier to navigate to different pages.

Flavor 3

Pagination UI - third flavor

pagination.html:

{% if page_obj.has_previous %}
  <a href="?page={{ page_obj.previous_page_number }}">« Previous page</a>

  {% if page_obj.number > 3 %}
    <a href="?page=1">1</a>
    {% if page_obj.number > 4 %}
      <span>...</span>
    {% endif %}
  {% endif %}
{% endif %}

{% for num in page_obj.paginator.page_range %}
  {% if page_obj.number == num %}
    <a href="?page={{ num }}">{{ num }}</a>
  {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
    <a href="?page={{ num }}">{{ num }}</a>
  {% endif %}
{% endfor %}

{% if page_obj.has_next %}
  {% if page_obj.number < page_obj.paginator.num_pages|add:'-3' %}
    <span>...</span>
    <a href="?page={{ page_obj.paginator.num_pages }}">{{ page_obj.paginator.num_pages }}</a>
  {% elif page_obj.number < page_obj.paginator.num_pages|add:'-2' %}
    <a href="?page={{ page_obj.paginator.num_pages }}">{{ page_obj.paginator.num_pages }}</a>
  {% endif %}

  <a href="?page={{ page_obj.next_page_number }}">Next Page »</a>
{% endif %}

If you have a large number of pages, you may want to look at this third and final flavor.

Conclusion

This concludes the article about implementing pagination in Django. Here are the key takeaways to remember:

  1. Implementing Pagination in Django is quite easy due to the Paginator and Page helper classes provided out the box.
  2. Once the view is created, you simply pass back the page object, with the paginated data, to be used in the template.

Author: Shantun Parmar

Leave a Reply

Your email address will not be published.