Updated Jun 26, 2020

Django - Pagination Tutorial

How to split content across pages.

Introduction

We use Django's pagination tools to spread content across multiple pages.

Quickstart (with a class-based view)

Edit the blog app views.py file and add these lines to it:

from django.shortcuts import render
from django.views.generic import ListView

from .models import Post


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

Edit the blog/index.html template file and add these lines to it:

<ul>
    {% for post in posts %}
    <li>
        {{ post }}
    </li>
    {% endfor %}
</ul>

{% if page_obj.paginator.num_pages > 1 %}

    {% 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 %}

{% endif %}

Full tutorial

Setup

Run these commands to setup a new project:

mkdir mysite && cd mysite
python3 -m venv venv
source venv/bin/activate
pip install django
django-admin startproject mysite .
python manage.py startapp blog

Add blog configuration class to the INSTALLED_APPS list:

INSTALLED_APPS = [
    # HERE
    'blog.apps.BlogConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

blog/models.py

Add a class called Post in the blog app models.py file:

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=255)
    
    def __str__(self):
        return self.title

Run migrations:

python manage.py makemigrations
python manage.py migrate

Create posts

Create some content:

python manage.py shell
>>> from blog.models import Post
>>> for i in range(10):
...     Post.objects.create(title="Lorem ipsum %s" % i)

mysite/urls.py

Edit the project urls.py file and add the following path to it:

from django.contrib import admin
from django.urls import path

from blog import views

urlpatterns = [
    # HERE
    path('', views.HomepageView.as_view(), name='home'),
    path('admin/', admin.site.urls),
]

blog/views.py

Edit the blog app views.py file and add these lines to it:

from django.shortcuts import render
from django.views.generic import ListView

from .models import Post


class HomepageView(ListView):
    model = Post
    paginate_by = 5
    template_name = 'blog/index.html'
    context_object_name = 'posts'
  • The paginate_by attribute specifies how many objects we want to display on each page.

blog/templates/blog/index.html

Create a file called index.html in the blog app templates/blog directory and add these lines to it:

<ul>
    {% for post in posts %}
    <li>
        {{ post }}
    </li>
    {% endfor %}
</ul>

{% if page_obj.paginator.num_pages > 1 %}

    {% include 'blog/_pagination.html' with page_obj=page_obj %}

{% endif %}
  • The page_obj template variable contains everything we need to build the pagination markup.
  • page_obj.paginator.num_pages tells us the total number of pages.
  • The include tag loads and renders the markup from a separate template.

blog/templates/_pagination.html

Create a file called _pagination.html in the blog app templates directory and add these lines to it:

{% 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 %}
  • We pass the page number to the view using the ?page query string.
  • page_obj.has_previous returns True if there is a previous page.
  • page_obj.previous_page_number returns the previous page number.
  • page_obj.number is the current page number.
  • page_obj.has_next returns True if there is a next page.
  • page_obj.next_page_number returns the next page number.

Underscore(_) indicates that this template is meant to be included within other templates. We can now re-use this template across the site when we need to paginate content.

Function-based views

blog/views.py

With function-based views you can use the Paginator class directly. Edit the blog app views.py file and add these lines to it:

from django.core.paginator import Paginator


def function_based(request):

    posts = Post.objects.all()
    paginator = Paginator(posts, 5)
    page = request.GET.get('page')
    posts = paginator.get_page(page)

    return render(request,
                  'blog/function_based.html',
                  {'posts': posts})
  • We pass the objects we want to paginate and the number of objects per page to the Paginator class.
  • The page query string value is fetched with the request.GET.get() function and passed to the paginator.get_page() function. This gets us the posts object that contains post objects for a specific page and access to the pagination data.

blog/templates/blog/function_based.html

Create a file called function_based.html in the blog app templates directory and add these lines to it:

<ul>
    {% for post in posts %}
    <li>
        {{ post }}
    </li>
    {% endfor %}
</ul>

{% if posts.paginator.num_pages > 1 %}

    {% if posts.has_previous %}

        <a href="?page={{ posts.previous_page_number }}">previous</a>

    {% endif %}

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

    {% if posts.has_next %}

        <a href="?page={{ posts.next_page_number }}">next</a>

    {% endif %}

{% endif %}
Note: with this function-based example we use the "posts" object to access the pagination data, not "page_obj".

mysite/urls.py

Edit the project urls.py file and add the following path to it:

from blog import views

urlpatterns = [
    path('', views.HomepageView.as_view(), name='home'),
    # HERE
    path('function-based/', views.function_based, name='function_based'),
    path('admin/', admin.site.urls),
]

Visit /function-based/.

Alternative Themes

Blue Theme

Create a file called theme_blue.html in the blog/templates/blog directory and add these lines to it:

<!doctype html>
{% load static %}
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
        integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
    <link rel="stylesheet" href="{% static 'blog/css/pagination_blue.css' %}">
    <title>Document</title>
</head>

<body>

    <ul>
        {% for post in posts %}
            <li>
                {{ post }}
            </li>
        {% endfor %}
    </ul>
    
    {% if page_obj.paginator.num_pages > 1 %}
    
        {% include 'blog/_pagination_blue.html' with page_obj=page_obj %}
    
    {% endif %}

</body>

</html>

Create a file called _pagination_blue.html in the blog/templates/blog directory and these lines to it:

<div class="pagination">
    {% if page_obj.has_previous %}
    <a class="pagination-action" href="?page=1">
        <i class="fa fa-angle-double-left" aria-hidden="true"></i> </a>
    <a class="pagination-action" href="?page={{ page_obj.previous_page_number }}">
        <i class="fa fa-angle-left" aria-hidden="true"></i>
    </a>
    {% endif %}
    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <span class="pagination-number pagination-current">{{ num }}</span>
        {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
            <a class="pagination-number" href="?page={{ num }}">{{ num }}</a>
        {% endif %}
    {% endfor %}
    {% if page_obj.has_next %}
        <a class="pagination-action" href="?page={{ page_obj.next_page_number }}">
            <i class="fa fa-angle-right" aria-hidden="true"></i>
        </a>
        <a class="pagination-action" href="?page={{ page_obj.paginator.num_pages }}">
            <i class="fa fa-angle-double-right" aria-hidden="true"></i>
        </a>
    {% endif %}
</div>

Create a file called pagination_blue.css file in the blog/static/blog/css directory and add these lines to it:

/* BLUE THEME */

.pagination {
    margin-top: 1em;
}

.pagination a {
    text-decoration: none;
}

.pagination-number {
    padding: 0.5em 0.8em;
    border-radius: 2px;
    color: #fff;
    background-color: #6D85C7;
}

.pagination-number:hover,
.pagination-current {
    background-color: #3354AA;
}

.pagination-action {
    margin: 0 0.1em;
    display: inline-block;
    padding: 0.5em 0.5em;
    color: #B9B9B9;
    font-size: 1.3em;
}

.pagination-action:hover,
.pagination-previous,
.pagination-next {
    color: #3354AA;
}

Add this view to the blog app views.py file:

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

Add this path to the project urls.py file:

urlpatterns = [
    path('', views.HomepageView.as_view(), name='home'),
    path('function-based/', views.function_based, name='function_based'),
    # HERE
    path('blue-theme/', views.BlueThemeView.as_view(), name='blue_theme'),
    path('admin/', admin.site.urls),
]

Visit /blue-theme/.

Green Theme

Create a file called theme_green.html in the blog/templates/blog directory and add these lines to it:

<!doctype html>
{% load static %}
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
        integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
    <link rel="stylesheet" href="{% static 'blog/css/pagination_green.css' %}">
    <title>Document</title>
</head>

<body>

    <ul>
        {% for post in posts %}
            <li>
                {{ post }}
            </li>
        {% endfor %}
    </ul>

    {% if page_obj.paginator.num_pages > 1 %}

        {% include 'blog/_pagination_green.html' with page_obj=page_obj %}

    {% endif %}

</body>

</html>

Create a file called _pagination_green.html in the blog/templates/blog directory and add these lines to it:

<div class="pagination">
    {% if page_obj.has_previous %}
        <a class="pagination-action" href="?page=1">
            <i class="fa fa-angle-double-left" aria-hidden="true"></i>
        </a>
        <a class="pagination-action" href="?page={{ page_obj.previous_page_number }}">
            <i class="fa fa-angle-left" aria-hidden="true"></i>
        </a>
    {% endif %}
    <span class="pagination-current">{{ page_obj.number }}</span>
    <span class="pagination-of">of</span>
    <span class="pagination-total">{{ page_obj.paginator.num_pages }}</span>
    {% if page_obj.has_next %}
        <a class="pagination-action" href="?page={{ page_obj.next_page_number }}">
            <i class="fa fa-angle-right" aria-hidden="true"></i> </a>
        <a class="pagination-action" href="?page={{ page_obj.paginator.num_pages }}">
            <i class="fa fa-angle-double-right" aria-hidden="true"></i>
        </a>
    {% endif %}
</div>

Create a file called pagination_green.css file in the blog/static/blog/css directory and add these lines to it:

/* GREEN THEME */

.pagination {
    margin-top: 1em;
}

.pagination a {
    text-decoration: none;
}

.pagination-current, 
.pagination-total {
    padding: 0.5em 0.8em;
    border-radius: 2px;
    color: #fff;
    background-color: #30BF61;
}

.pagination-total {
    background-color: #B9B9B9;
}

.pagination-action {
    margin: 0 0.1em;
    display: inline-block;
    padding: 0.5em 0.5em;
    color: #B9B9B9;
    font-size: 1.3em;
}

.pagination-of {
    color: #B9B9B9;
    padding: 0 1em;
}

.pagination-action:hover {
    color: #3354AA;
}

Add this view to the blog app views.py file:

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

Add this path to the project urls.py file:

urlpatterns = [
    path('', views.HomepageView.as_view(), name='home'),
    path('function-based/', views.function_based, name='function_based'),
    path('blue-theme/', views.BlueThemeView.as_view(), name='blue_theme'),
    # HERE
    path('green-theme/', views.GreenThemeView.as_view(), name='green_theme'),
    path('admin/', admin.site.urls),
]

Visit /green-theme/.