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 logicPage
: 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:
from django.core.paginator import Paginator
Step 2: Create a Paginator Instance
In your function-based view, create a Paginator instance with your queryset:
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:
<!-- 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">« 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 »</a>
{% endif %}
</span>
</div>
Pagination in Class-Based Views
Django makes pagination even easier in class-based views with the ListView
class:
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:
<!-- 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:
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:
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:
<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:
<!-- 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
# 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
# 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
# blog/urls.py
from django.urls import path
from .views import PostListView
urlpatterns = [
path('', PostListView.as_view(), name='post_list'),
]
Template
<!-- 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:
<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:
- The basics of pagination in Django
- How to implement pagination in function-based views
- How to use Django's built-in pagination in class-based views
- How to handle edge cases like invalid page numbers
- How to customize page range display
- How to maintain filters when navigating through pages
- 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
-
Basic Implementation: Create a simple Django application that displays a list of books with pagination (10 books per page).
-
Filter Integration: Extend the book application to include filters for genres and publication years while maintaining pagination.
-
AJAX Pagination: Implement pagination that loads new content without refreshing the page using AJAX and Django's pagination.
-
Custom Pagination Template: Create a reusable pagination template tag that can be included in multiple templates with customizable styling.
-
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! :)