【Django】ページネーション(ページング):記事一覧を複数ページに分ける方法

2020.08.03 /

【Django】ページネーション(ページング):記事一覧を複数ページに分ける方法

大量のデータ一覧を表示する場合、複数のページに分けて表示したいときがあります。そういった場合は、ページネーション(ページング)と呼ばれる技術を使います。

例えば、Webアプリケーションで自社のユーザー一覧を表示する場合、10人程度であれば1つのページに表示するのは問題ないです。
しかし何百人もいる場合は、ページを分けた方が使い勝手やページの読みやすさはいいですよね。

また、ブログで記事数が100以上ある場合、1つのページに記事一覧すべてを表示なんかしないですよね。

このような場合に、ページを複数に分けるページネーション(ページング)を使用して、1画面に表示する件数を制限してコンパクトにします。

今回は、このページネーションをDjangoでどのように実装するのか解説していきます。

環境はPython 3.7.5、Django3.0.0です。

完成ページ

最終的に完成させるページネーションの表示は以下のようになります。

Django:ページネーション

どのようにして、このページネーションをDjangoで作成するか解説していきます。

Djangoにおけるページネーション(ページング)

Djangoにはページネーションをサポートする機能がデフォルトで実装されています。

ページネーション機能を使うために、view.pyではDjango.core.paginatorモジュール内のPaginator、EmptyPage、PageNotAnIntegerを使用します。

Paginatorクラス

Paginatorオブジェクトの生成

Paginatorクラスの構文は次のようになります。

class Paginator(object_list, per_page, orphans=0, allow_empty_first_page=True)

引数 説明
object_list ページ表示したいリストまたはタプル、QuerySetを指定
per_page 1ページに表示する項目数を指定
orphans 最終ページの項目数が指定値以下の場合、前ページに項目を含める
allow_empty_first_page 最初のページが空でも許容するか

このようにPaginatorに、分割するオブジェクトリスト、1ページ当たりに表示するデータの個数を指定して、インスタンスを作成します。

例えば、Articleモデルからリストを全件取得して、1ページ当たり20件データを表示させる場合は以下のようになります。

articles = Article.objects.all()
paginator = Paginator(articles, 20)

Djangoに標準で用意されているページネーション機能を利用するためには、上記のようにしてPaginatorオブジェクトを生成する必要があります。

Paginatorオブジェクトのメソッド

生成されたPaginatorオブジェクトにはget_pageメソッドpage()メソッドの2つが用意されています。

get_page(number)

Django2.0から追加されたメソッドです。

Paginatorクラスに次のように定義されています。

def get_page(self, number):
    try:
        number = self.validate_number(number)
    except PageNotAnInteger:
         number = 1
    except EmptyPage:
        number = self.num_pages
    return self.page(number)

引数numberには開きたいページ番号を指定します。
もし無効なページ番号や数字でない文字を指定された場合でも、例外処理で最初のページまたは最終ページを返します。

Pagenatorオブジェクトにget_pageメソッドを使うことにより、Pageオブジェクトを生成します。

page(number)

page()メソッドはPageオブジェクトを生成するために使用します。

もし指定されたページが存在しないページだった場合は、InvalidPageエラーが発生します。

Paginatorオブジェクトの属性

count

Paginatorオブジェクトにcount属性を使用することで、全ページのオブジェクト総数を取得できます。

Paginator.count

num_pages

Paginatorオブジェクトにnum_pages属性を使用することで、ページの総数を取得できます。

Paginator.num_pages

page_range

Paginatorオブジェクトにpage_range属性を使用することで、ページをイデレーターで取得できます。

Paginator.page_range

ページ番号をURLから取得する

get_page()メソッドとpage()メソッドの引数には、ページ番号を指定します。

このページ番号は、リクエストされたページのリストを表示する場合、URLから取得します。
GET送信されたoffice54.net/article/?page=4 のようなURLから、ページ番号を以下で取得できます。

page = request.GET.get('page')

この取得したページ番号をget_page()またはpage()の引数に指定して、リクエストされたページのリストを取得します。

pages = paginator.page(page)

リクエストされたページのリストを受け取ったpagesはPageオブジェクトと呼びます。

Pageオブジェクトのメソッドは後述いたします。

ここまでがview.pyでのPaginatorクラスの基本的な使用方法です。

Pageオブジェクト

すでに上述していますが、PageオブジェクトはPaginator.page()で生成できます。

Pageオブジェクトはテンプレート(htmlファイル)内で使用します。

Pageオブジェクトを使用して、現在のページを起点として次ページの有無、前ページの有無、他のページの有無などを取得することができます。

pageオブジェクトをテンプレート(htmlファイル)でそのまま入れ込むと、全ページ数と現在のページを表示します(例:Page 2 of 4)。

<p>{{ pages }}</p>
ページネーションの表示

pageオブジェクトのメソッド

以下にメソッドおよび属性一覧を記します。

メソッド 説明
has_next() 次のページがある場合:True
has_previous() 前のページがある場合:True
has_other_pages() 前後どちらかにページがある場合:True
next_page_number() 次ページのページ番号を返す(次ページが存在していなくても)
previous_page_number() 前ページのページ番号を返す(前ページが存在していなくても)
start_index() ページの先頭ページ(1)を返す
end_index() ページの最終ページを返す

pageオブジェクトの属性

object_list属性

Pageオブジェクトのリストを返します。

paginator属性

Pageオブジェクトに関連付けられたPaginatorオブジェクトを返します。

number属性

現在のページ番号を返します。

例外処理

リクエストされたページ番号が有効でない場合に備えて、PageNotAnIntegerとEmptyPageを使います。

PageNotAnIntegerは、整数でない値がリクエストされた際に呼び出されます。
EmptyPageは、有効な値がリクエストされているが、そのページが存在していない際に呼び出されます。

サンプルプログラム

では実際にWebアプリケーションを作成してみましょう。

プロジェクト名はmyproject、アプリケーション名はblogとします。
myprojectのurls.py、blogアプリのurls.py、models.py、views.py、index.htmlは以下のようになっております。

# myproject/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('blog.urls')),
]
# blog/urls.py
from django.urls import path
from . import views
urlpatterns = [
    path('', views.index, name='index'),
]
# blog/models.py
from django.db import models
from django.utils import timezone

class Post(models.Model):
    title = models.CharField(max_length=200)
    text = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return self.title
# blog/views.py
from django.shortcuts import render
from .models import Post

def index(request):
    return render(request, 'index.html', {})
# blog/template/index.html
<html>
<head>
</head>
<body style="margin:20px;">
  <h1>ページネーション確認ページ</h1>
</body>
</html>

views.pyの変更

それではviews.pyを編集し、テンプレートにPageオブジェクトを渡すようにします。

# blog/views.py
from django.shortcuts import render
from .models import Post
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

def index(request):
    Posts = Post.objects.order_by('created_date').reverse()
    paginator = Paginator(Posts, 3)
    page = request.GET.get('page', 1)
    try:
    	pages = paginator.page(page)
    except PageNotAnInteger:
    	pages = paginator.page(1)
    except EmptyPage:
    	pages = paginator.page(1)
    context = {'pages': pages}
    return render(request, 'index.html', context)

3行目では、django.core.paginatorモジュールからPaginator、EmptyPage、PageNotAnIntegerをインポートしています。

6行目では、Postモデルからオブジェクト全件を取得、created_dateで順番を整列しています。

7行目のpaginator = Paginator(Posts, 3)では、取得したオブジェクトのリストを1ページ当たり3件ずつ表示するようにしています。

8行目のrequest.GET.get(‘page’, 1)では、現在のページ番号をURLから取得しています。

10行目のpages = paginator.page(page)では、リクエストされたページ番号のリストを取得し、pagesに格納しています。

次に画面表示されるテンプレートindex.htmlを編集していきます。

表示するhtmlファイルの設定

テンプレートのindex.htmlを編集し、ページネーションが表示されるようにします。
BOOTSTRAP4を使って見栄えもよくしますので、Bootstrap4が使用できるようにスタイルシートをlinkタグでダウンロードしています。

<html>
<head>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="… " crossorigin="anonymous">
</head>
<body style="margin:20px;">
  <h1>ページネーション確認ページ</h1>
  {% for page in pages %}
  <p>{{ page.title }} : {{ page.text }}</p>
  {% endfor %}
  {% if pages.has_other_pages %}
    <nav aria-label="Page navigation example">
        <ul class="pagination">
            {% if pages.has_previous %}
                <li><a class="page-link text-primary d-inline-block" href="?page={{ pages.previous_page_number }}"><<</a></li>
            {% else %}
                <li class="disabled"><div class="page-link text-secondary d-inline-block disabled" href="#"><<</div></li>
            {% endif %}

            {% for page_num in pages.paginator.page_range %}
                {% if page_num %}
                    {% if page_num == pages.number %}
                        <li class="disabled"><div class="page-link text-secondary d-inline-block disabled" href="#">{{ page_num }}</div></li>
                    {% else %}
                        <li><a class="page-link text-primary d-inline-block" href="?page={{ page_num }}">{{ page_num }}</a></li>
                    {% endif %}
                {% else %}
                    <li class="disabled"><a class="page-link text-secondary d-inline-block text-muted" href="#">・・・</a></li>
                {% endif %}
            {% endfor %}

            {% if pages.has_next %}
                <li><a class="page-link text-primary d-inline-block" href="?page={{ pages.next_page_number }}">>></a></li>
            {% else %}
                <li class="disabled"><div class="page-link text-secondary d-inline-block disabled" href="#">>></div></li>
            {% endif %}
        </ul>
    </nav>
  {% endif %}
</body>
</html>

上記index.htmlにより、以下のようにページネーションが表示されます。

Django:ページネーションの確認

サンプルプログラム2

上記サンプルでは、ページ数が増えるとその分ページ番号が横並びになるため、ページ数が多い場合は使用できません。

そういった場合は次のようにページネーションを表示することができます。

Django:ページ数が多いときのページネーション
Django:ページ数が多いときのページネーション
Django:ページ数が多いときのページネーション

上記のようにページネーションを表示するために、テンプレートでは次のように記述しています。

<html>
<head>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="… " crossorigin="anonymous">
</head>
<body style="margin:20px;">
  <h1>ページネーション確認ページ</h1>
  {% for page in pages %}
  <p>{{ page.title }} : {{ page.text }}</p>
  {% endfor %}
  {% if pages.has_other_pages %}
    <nav aria-label="Page navigation example">
        <ul class="pagination">
            {% if pages.has_previous %}
                <li><a class="page-link text-primary d-inline-block" href="?page={{ pages.previous_page_number }}"><<</a></li>
            {% else %}
                <li class="disabled"><div class="page-link text-secondary d-inline-block disabled" href="#"><<</div></li>
            {% endif %}

            {% if pages.has_previous %}
                {% if pages.previous_page_number != 1 %}
                    <li><a class="page-link text-primary d-inline-block" href="?page=1">1</a></li>
                    <li>…</li>
                {% endif %}
                <li><a class="page-link text-primary d-inline-block" href="?page={{ pages.previous_page_number }}">{{ pages.previous_page_number }}</a></li>
            {% endif %}
            <li class="disabled"><div class="page-link text-secondary d-inline-block disabled" href="#">{{ pages.number }}</div></li>
            {% if pages.has_next %}
                <li><a class="page-link text-primary d-inline-block" href="?page={{ pages.next_page_number }}">{{ pages.next_page_number }}</a></li>
                {% if pages.next_page_number != pages.paginator.num_pages %}
                    <li>…</li>
                    <li><a class="page-link text-primary d-inline-block" href="?page={{ pages.paginator.num_pages }}">{{ pages.paginator.num_pages }}</a></li>
                {% endif %}
            {% endif %}
            {% if pages.has_next %}
                <li><a class="page-link text-primary d-inline-block" href="?page={{ pages.next_page_number }}">>></a></li>
            {% else %}
                <li class="disabled"><div class="page-link text-secondary d-inline-block disabled" href="#">>></div></li>
            {% endif %}
        </ul>
    </nav>
  {% endif %}
</body>
</html>

PaginatorオブジェクトとPageオブジェクトのメソッド・属性を巧みに使うことで、このように簡単に使いやすいページネーションを実現できます。

まとめ

Djangoにおけるページネーションの使い方の解説でしたが、いかがでしたか?

ぜひWebアプリケーションに取り入れていただき、使いやすいアプリ作成を実現させてください。