These days the majority of websites contain the pagination system. Pagination is the fact of splitting data across several pages in order to speed up the loading of the page and improve the user experience. In this tutorial, we will see in detail how pagination works in the Django framework.
To follow this guide, you need to create a new project and do some configurations. So, first, open the terminal and run these commands:
mkdir dj-pagination
cd dj-pagination
python3 -m venv venv
source venv/bin/activate
pip install django
django-admin startproject app .
django-admin startapp core
python manage.py migrate
python manage.py createsuperuser
Username: rouizi
Email address:
Password: # 1234
Password (again): # 1234
Now, open settings.py and add the following:
# app/settings.py
# ...
INSTALLED_APPS = [
# ...
'core', # Add this line
]
# ...
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # Add this line
# ...
},
]
# ...
and, finally, create the templates directory:
mkdir templates
Django uses the Paginator class to split a Queryset object (or other objects with a count()
or __len__()
method) into Page objects.
The paginator class takes two parameters (actually, it accepts 4 but 2 are optional): the first one is the list of objects that we want to split, and the second one is the number of objects to display per page.
Let's open the shell and see how this class works:
$ python manage.py shell
# import the Paginator class and the User class
>>> from django.core.paginator import Paginator
>>> from django.contrib.auth.models import User
# let's first create some users to play with
>>> for i in range(27):
... User.objects.create(username='test_{}'.format(i))
...
<User: test_0>
<User: test_1>
<User: test_2>
# ...
>>> users = User.objects.all()
>>> users[:5]
<QuerySet [<User: rouizi>, <User: test_0>, <User: test_1>, <User: test_2>, <User: test_3>]>
>>> p = Paginator(users, 5) # 5 users per page
>>> p.count # Total number of objects
28
>>> p.num_pages # Number of pages needed to split all the users
6 # We will have 5 pages with 5 users each and 1 page with 3 users
>>> p.page_range # Range of the pages
range(1, 7)
>>> p.page(1) # Page object for the first page
<Page 1 of 6>
>>> p.page(1).object_list # Returns the content of the first page
[<User: rouizi>, <User: test_0>, <User: test_1>, <User: test_2>, <User: test_3>]
>>> p.page(6).object_list # Content for the last page
[<User: test_24>, <User: test_25>, <User: test_26>]
>>> p.page(6).has_next()
False # There is no page after the 6th page
>>> p.page(6).has_previous()
True # The 6th page has a previous page
>>> p.page(6).next_page_number() # The number of the page after the 6th page
Traceback (most recent call last):
# ...
django.core.paginator.EmptyPage: That page contains no results
>>> p.page(6).previous_page_number() # The number of the page before the 6th page
5
The Paginator.page() method throws two types of exceptions:
Both classes are subclasses of InvalidPage, so if we want to return the same behavior for the two exceptions we can simply use the InvalidPage exception.
Here is a quick example:
def list_users(request):
page = request.GET.get('page', 1)
users = User.objects.all()
paginator = Paginator(users, 5) # 5 users per page
try:
users = paginator.page(page)
except InvalidPage:
# if the page contains no results (EmptyPage exception) or
# the page number is not an integer (PageNotAnInteger exception)
# return the first page
users = paginator.page(1)
Before finishing this section, let's see some examples for the EmptyPage and PageNotAnInteger exceptions:
>>> p.page(0)
Traceback (most recent call last):
# ...
django.core.paginator.EmptyPage: That page number is less than 1
>>> p.page(999)
Traceback (most recent call last):
# ...
django.core.paginator.EmptyPage: That page contains no results
>>> p.page('zed')
Traceback (most recent call last):
# ...
django.core.paginator.PageNotAnInteger: That page number is not an integer
Now that you have a better understanding of how the Pagination class works, let's see now how to use it in a real project.
First, edit the urls.py file like this:
# app/urls.py
from django.contrib import admin
from django.urls import path
from core.views import list_users
urlpatterns = [
path('admin/', admin.site.urls),
path('list_users/<int:page>/', list_users, name='list_users')
]
To be able to know what page to show, our view needs a page
parameter. We have 2 ways to do this:
page = request.GET.get('page')
.page
as an extra parameter (like we did above).Now let's create the view function:
# core/views.py
from django.shortcuts import render
from django.contrib.auth.models import User
from django.core.paginator import Paginator, EmptyPage
def list_users(request, page=1):
users = User.objects.all()
paginator = Paginator(users, 5) # 5 users per page
# We don't need to handle the case where the `page` parameter
# is not an integer because our URL only accepts integers
try:
users = paginator.page(page)
except EmptyPage:
# if we exceed the page limit we return the last page
users = paginator.page(paginator.num_pages)
return render(request, 'home.html', {'users': users})
Create a file home.html inside the templates directory and add this code:
<!-- templates/home.html -->
<!DOCTYPE html>
<html>
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
<title>Pagination system</title>
</head>
<body>
<div class='container'>
<div class='mt-5 pt-5 ml-5 pl-5'>
<p><strong>Username:</strong></p>
{% for user in users %}
<p>{{ user.username }}</p>
{% endfor %}
</div>
</body>
</html>
Run the server and let's see how this works:
In this last section, we will add a way to navigate between pages without the need to play with the URL.
So as not to get lost in the details, let's create a super simple navigation with Previous and Next links:
<!-- templates/home.html -->
<div class='container'>
<div class='mt-5 pt-5 ml-5 pl-5'>
<p><strong>Username:</strong></p>
{% for user in users %}
<p>{{ user.username }}</p>
{% endfor %}
</div>
<!-- Pagination -->
<div class='pagination justify-content-center'>
{% if users.has_previous %}
<a href='{% url "list_users" users.previous_page_number %}'>Previous </a>
{% endif %}
<span class='mx-4'>
Page {{ users.number }} of {{ users.paginator.num_pages }}
</span>
{% if users.has_next %}
<a href='{% url "list_users" users.next_page_number %}' > Next</a>
{% endif %}
</div>
<!-- END Pagination -->
</div>
We can customize it to display the number for all the pages:
<!-- templates/home.html -->
<!-- Pagination -->
<ul class='pagination justify-content-center'>
{% if users.has_previous %}
<li class="mx-1"><a href='{% url "list_users" users.previous_page_number %}'>Previous</a></li>
{% else %}
<li class='mx-1 disabled'><span >Previous</span></li>
{% endif %}
{% for i in users.paginator.page_range %}
{% if users.number == i %}
<li class='active mx-1'><span>{{ i }}</span></li>
{% else %}
<li class="mx-1"><a href='{% url "list_users" i %}'>{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if users.has_next %}
<li class="mx-1"><a href='{% url "list_users" users.next_page_number %}' >Next</a></li>
{% else %}
<li class="mx-1"><span class='disabled'>Next</span></li>
{% endif %}
</ul>
<!-- END Pagination -->
The above implementation is pretty good, but if we had a lot of users, the whole number of pages will be displayed. So what we need to do is display only a few pages after and before the current page.
Here is how to do it:
<!-- templates/home.html -->
<!-- Pagination -->
<nav>
<ul class="pagination justify-content-center">
<li class="page-item {% if not users.has_previous %} disabled {% endif %}">
<a class="page-link" href="{% if users.has_previous %} {% url 'list_users' users.previous_page_number %}{% endif %} ">Previous</a>
</li>
{% if users.number|add:'-1' > 1 %}
<li class="page-item disabled"><a class="page-link">…</a></li>
{% endif %}
{% for i in users.paginator.page_range %}
{% if users.number == i %}
<li class="active page-item disabled"><a class="page-link" href="#">{{ i }}</a></li>
{% elif i > users.number|add:'-2' and i < users.number|add:'2' %}
<li class="page-item"><a class="page-link" href="{% url 'list_users' i %}">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if users.paginator.num_pages > users.number|add:'1' %}
<li class="page-item disabled"><a class="page-link" href="#">…</a></li>
{% endif %}
<li class="page-item {% if not users.has_next %} disabled {% endif %}">
<a class="page-link" href="{% if users.has_next %} {% url 'list_users' users.next_page_number %} {% endif %}">Next</a>
</li>
</ul>
</nav>
<!-- END Pagination -->
In this tutorial I showed you how Django's Pagination class works and how to use its methods in a view and templates to have a fully working pagination system. We also saw how to handle the edge cases with exceptions.
The final code is available on GitHub at: https://github.com/Rouizi/django-pagination-with-fbc/
Hi @Dane Miller, I'm glad you found my post useful, and thanks for the compliment. I am not quite sure where the problem is coming from, but I think why the filters comes off when you click on a new page because you are filtering using a POST request instead of a GET request: # ... csv_export = request.POST.get('csv_export') Here, for example, you should use request.GET.get('csv_export'). Because when you click on a new page, you are sending a GET request to the server, not a POST request. Actually, when the user click on a filter, he is making a POST request to the server but when he clicks on a new page, he is making a GET request. So what you need to do is: the first time the user click on a filter you get that filter on the server from a POST request (if request.method == 'POST'), and create a new variable ('filter' for example), assign to it the filter the user chosen, and send it back to the template. So the next time the user use the pagination to change a page (GET request this time) you'll have the 'filter' in the GET request. I faced this problem once in my project, which you can check how I did it here: https://github.com/Rouizi/OC_project11/blob/4f8e5b4765eaab9470906c14ae5e8528ceafa610/catalog/views.py#L118 Also try to see the effect on the live site on Heroku here: https://purbeurre-oc11.herokuapp.com/catalog/products/
June 14, 2021, 5:36 p.m.
Hi Yacine thanks for all your help and assistance. Once again I am indicating that you have listed a wonderful post that can be easily implemented. Cheers. But as I developed my app I needed another Use Case where I needed pagination with a filter. In that regard I found this post which gives good example. I am just posting it to help others. https://www.youtube.com/watch?v=dkJ3uqkdCcY
June 20, 2021, 5:02 p.m.
Hi @Dane, it's always a pleasure for me to help, and I am glad that you found my post useful. Thank you. Also, thank you for sharing with us what you found, it will surely be useful for someone.
June 20, 2021, 7:41 p.m.
This was helpful
Nov. 23, 2021, 3:02 p.m.
June 14, 2021, 3:37 p.m.