Skip to main content

Django Pagination

When displaying large sets of data in a web application, it's important to break them into smaller chunks for better user experience and performance. Django provides built-in pagination functionality that allows you to divide your data into pages, making your application more usable and efficient.

Introduction to Pagination

Pagination is the process of dividing content into discrete pages and providing navigation to access these pages. Instead of loading hundreds or thousands of database records at once, pagination allows your application to fetch and display only a subset of records at a time.

Benefits of pagination include:

  • Improved page load times: Loading fewer items means faster rendering
  • Reduced server load: Processing fewer queries and less data
  • Better user experience: Easier navigation through manageable chunks of content
  • Reduced bandwidth usage: Less data transferred between server and client

Django's Pagination Classes

Django provides pagination through the django.core.paginator module, primarily with two classes:

  • Paginator: Handles the pagination logic
  • Page: Represents a single page of objects

Let's see how to implement pagination in Django step by step.

Basic Pagination Implementation

Step 1: Import the Paginator Class

First, import the necessary class in your view:

python
from django.core.paginator import Paginator

Step 2: Create a Paginator Instance

In your function-based view, create a Paginator instance with your queryset:

python
def article_list(request):
article_list = Article.objects.all()
paginator = Paginator(article_list, 10) # Show 10 articles per page

page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)

return render(request, 'blog/article_list.html', {'page_obj': page_obj})

Step 3: Update Your Template

Now, update your template to display the paginated objects and navigation:

html
<!-- Display the articles from the current page -->
{% for article in page_obj %}
<h2>{{ article.title }}</h2>
<p>{{ article.content|truncatewords:30 }}</p>
{% endfor %}

<!-- Pagination navigation -->
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">&laquo; first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}

<span class="current">
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>
<a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</span>
</div>

Pagination in Class-Based Views

Django makes pagination even easier in class-based views with the ListView class:

python
from django.views.generic import ListView
from .models import Article

class ArticleListView(ListView):
model = Article
template_name = 'blog/article_list.html'
context_object_name = 'articles'
paginate_by = 10 # Show 10 articles per page

In your template, you can access the paginated data as follows:

html
<!-- Display the articles from the current page -->
{% for article in articles %}
<h2>{{ article.title }}</h2>
<p>{{ article.content|truncatewords:30 }}</p>
{% endfor %}

<!-- Pagination navigation -->
{% if is_paginated %}
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}

<span class="current">
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 %}
</div>
{% endif %}

Handling Edge Cases

Empty Pages and Invalid Page Numbers

Django's get_page() method handles invalid page numbers gracefully:

  • If the page number is not an integer, it returns the first page
  • If the page number is out of range, it returns the last page

For more control, you can use page() method with try/except:

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

def article_list(request):
article_list = Article.objects.all()
paginator = Paginator(article_list, 10)
page_number = request.GET.get('page')

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

return render(request, 'blog/article_list.html', {'page_obj': page_obj})

Advanced Pagination Features

Customizing Page Range Display

For a large number of pages, you might want to show only a limited range of page numbers:

python
def article_list(request):
article_list = Article.objects.all()
paginator = Paginator(article_list, 10)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)

# Get the index of the current page
index = page_obj.number - 1

# This value will display a range of 7 pages (3 before + current + 3 after)
max_index = len(paginator.page_range)
start_index = index - 3 if index >= 3 else 0
end_index = index + 3 if index <= max_index - 3 else max_index

# Get the page range to display
page_range = paginator.page_range[start_index:end_index]

context = {
'page_obj': page_obj,
'page_range': page_range,
}

return render(request, 'blog/article_list.html', context)

Update your template to use the custom page range:

html
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page=1">First</a>
<a href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}

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

{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">Last</a>
{% endif %}
</div>

Maintaining Filters with Pagination

When using filters along with pagination, you'll want to maintain the filter parameters across pages:

html
<!-- In your template -->
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page=1{% if request.GET.category %}&category={{ request.GET.category }}{% endif %}">First</a>
<a href="?page={{ page_obj.previous_page_number }}{% if request.GET.category %}&category={{ request.GET.category }}{% endif %}">Previous</a>
{% endif %}

<!-- Page numbers -->

{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}{% if request.GET.category %}&category={{ request.GET.category }}{% endif %}">Next</a>
<a href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.category %}&category={{ request.GET.category }}{% endif %}">Last</a>
{% endif %}
</div>

Real-World Example: Blog with Categories and Pagination

Let's create a more comprehensive example of a blog with categories and pagination:

Models

python
# blog/models.py
from django.db import models

class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)

def __str__(self):
return self.name

class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
category = models.ForeignKey(Category, on_delete=models.CASCADE)
published_date = models.DateTimeField(auto_now_add=True)

def __str__(self):
return self.title

class Meta:
ordering = ['-published_date']

Views

python
# blog/views.py
from django.shortcuts import render
from django.views.generic import ListView
from .models import Post, Category

class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 5

def get_queryset(self):
queryset = Post.objects.all()

# Filter by category if provided
category_slug = self.request.GET.get('category')
if category_slug:
queryset = queryset.filter(category__slug=category_slug)

return queryset

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.all()

# Add current category to context if filtering
category_slug = self.request.GET.get('category')
if category_slug:
context['current_category'] = category_slug

return context

URLs

python
# blog/urls.py
from django.urls import path
from .views import PostListView

urlpatterns = [
path('', PostListView.as_view(), name='post_list'),
]

Template

html
<!-- blog/templates/blog/post_list.html -->
{% extends 'base.html' %}

{% block content %}
<div class="blog-container">
<div class="category-sidebar">
<h3>Categories</h3>
<ul>
<li><a href="{% url 'post_list' %}" {% if not current_category %}class="active"{% endif %}>All Posts</a></li>
{% for category in categories %}
<li>
<a href="?category={{ category.slug }}" {% if current_category == category.slug %}class="active"{% endif %}>
{{ category.name }}
</a>
</li>
{% endfor %}
</ul>
</div>

<div class="post-list">
<h1>Blog Posts</h1>

{% if current_category %}
<p>Filtering by: {{ current_category }}</p>
<a href="{% url 'post_list' %}">Clear filter</a>
{% endif %}

{% for post in posts %}
<div class="post">
<h2>{{ post.title }}</h2>
<p class="meta">
Published on: {{ post.published_date|date:"F j, Y" }} |
Category: {{ post.category.name }}
</p>
<div class="content">
{{ post.content|truncatewords:50 }}
</div>
<a href="#">Read more</a>
</div>
{% empty %}
<p>No posts found.</p>
{% endfor %}

<!-- Pagination -->
{% if is_paginated %}
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page=1{% if current_category %}&category={{ current_category }}{% endif %}">First</a>
<a href="?page={{ page_obj.previous_page_number }}{% if current_category %}&category={{ current_category }}{% endif %}">Previous</a>
{% endif %}

<span class="current-page">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>

{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}{% if current_category %}&category={{ current_category }}{% endif %}">Next</a>
<a href="?page={{ page_obj.paginator.num_pages }}{% if current_category %}&category={{ current_category }}{% endif %}">Last</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endblock %}

Styling

You could add some CSS to make the pagination look more appealing:

html
<style>
.pagination {
margin: 20px 0;
text-align: center;
}

.pagination a, .pagination span {
display: inline-block;
padding: 8px 16px;
margin: 0 5px;
border: 1px solid #ddd;
border-radius: 4px;
text-decoration: none;
color: #2c3e50;
}

.pagination a:hover {
background-color: #f1f1f1;
}

.pagination .current-page {
background-color: #2c3e50;
color: white;
}
</style>

Summary

In this tutorial, you've learned:

  1. The basics of pagination in Django
  2. How to implement pagination in function-based views
  3. How to use Django's built-in pagination in class-based views
  4. How to handle edge cases like invalid page numbers
  5. How to customize page range display
  6. How to maintain filters when navigating through pages
  7. How to implement a real-world blog application with categories and pagination

Pagination is an essential feature for any Django application that displays large datasets. It improves user experience, enhances performance, and reduces server load. By implementing pagination correctly, you ensure your application remains responsive even as your data grows.

Additional Resources

Exercises

  1. Basic Implementation: Create a simple Django application that displays a list of books with pagination (10 books per page).

  2. Filter Integration: Extend the book application to include filters for genres and publication years while maintaining pagination.

  3. AJAX Pagination: Implement pagination that loads new content without refreshing the page using AJAX and Django's pagination.

  4. Custom Pagination Template: Create a reusable pagination template tag that can be included in multiple templates with customizable styling.

  5. Infinite Scroll: Implement an "infinite scroll" feature that loads more content as the user scrolls down the page, using Django's pagination API on the backend.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)