【Django入門12】ページネーションの基本

Djangoにおけるページネーションの解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、Django におけるページネーションについて解説していきます。

ページネーションとは

ウェブアプリにおけるページネーションとは、一覧表・一覧リストなどで表示するオブジェクトが多い場合、それを複数のページに分割して表示し、さらに各ページへのリンクを設置することになります。

例えば、Google の検索結果はページネーションの例の1つとなります。Google 検索でキーワードを入力すれば大量の検索結果が得られることになります。ですが、1つのページに全ての結果が表示されるのではなく、複数のページに分割して検索結果が表示されるようになっています。また、検索結果の他のページに簡単に遷移できるよう、他のページへのリンクも設置されています。これにより、検索結果が見やすくなってユーザーにとって使いやすいウェブアプリとなっています。

このように、量の多いオブジェクトを複数のページに分けて表示し、さらに他のページへのリンクを設置することをページネーションと呼びます。

ページネーションの説明図

特に、Django ではデータベースに保存されているレコードの一覧を表示するような際にページネーションが利用されることが多いです。Django においてレコードとはモデルのインスタンスのことになりますので、つまりは Django におけるページネーションとはモデルのインスタンスの一覧表・一覧リストを分割して表示する機能と考えて良いと思います。

このページネーションを実現する上では、まずページの分割が必要になります。また、複数のページに分割されるため、前後のページへの遷移を行うためのリンクの設置なども必要になります。そして、これらを実現するのが Django フレームワーク用意された PaginatorPage クラスになります。

ページネーションの実現方法

次は、ページネーションの実現方法・実現手順について解説していきます。

Django においては、前述の通りページネーションは Paginator というクラスと Page というクラスの2つを利用して実現することになります。

MEMO

Paginator のスペルに注意してください

4文字目は e ではなく i となっています

Page の4文字目は e となりますが、PaginatorPagination の場合は4文字目が i となります

基本的には Paginator に関してはビューから、Page に関してはテンプレートから利用することになります。

前回と前々回の連載の中で管理画面について説明しましたが、実はこの管理画面でも PaginatorPage が利用されてページネーションが実現されています。

簡単に言えば、Paginator はページの分割や分割後の全ページを管理するクラスになります。そして、Page は分割後の1つのページを管理するためのクラスになります。Page からは前後のページ等の情報が取得できるため、この取得した情報を利用すれば前後のページへのリンクの設置なども簡単に実現できます。

スポンサーリンク

ページネーションを実現する流れ

続いて、ページネーションを実現する流れを解説しておきます。

ページネーションを利用しない場合の処理の流れ

まず、おさらいの意味でページネーションを利用しない場合の処理の流れについて説明しておきます。この場合の処理の流れは下の図の通りとなります。

ページネーションを利用しなかった場合のアプリの処理の流れ

ページネーションを利用しない場合、モデル(データベース)から取得したモデルクラスのインスタンスの集合を全てテンプレートに渡し、テンプレートに渡されたインスタンスの情報を全て表示することになります。そのため、基本的にはモデルから取得されたインスタンスの数が多くても、1つのページに全て表示されることになります。

ページネーションを利用する場合の処理の流れ

それに対し、ページネーションを利用する場合の処理の流れは下の図のようになります。

ページネーションを利用する場合のアプリの処理の流れ

モデル(データベース)からモデルクラスのインスタンスの集合を取得するという点は同じですが、その集合がビューから Paginator に渡されることで、それらのインスタンスの情報を表示するページの分割が行われ、各ページ(Page)に表示するインスタンスの割り付けが行われます。

Paginatorでページの分割が行われる様子

さらに、ビューから分割後の特定のページを取得し、そのページ(Page)をテンプレートに渡すことで、そのページに割り付けられたインスタンスの情報のみが1つのページに表示されることになります。

Paginatorから特定のページを取得する様子

また、前後のページへの遷移を実現するためにページ内へのリンクの設置が必要となりますが、その前後のページのページ番号等の情報はビューより受け取った Page からテンプレートが取得し、テンプレートが取得したページ番号に基づいてリンクとして表示を行うことになります。Page からは前後のページのページ番号だけでなく、様々な情報をが取得できるため、それを利用すればいろんなページ表示を実現することも可能です。

Pageから前ページのページ番号を取得する様子

特に、今までのようにページネーションを利用していなかったときに比べて、ビューやテンプレートから PaginatorPage を利用して処理を行うところが新たに実装が必要になる部分となりますので、ここからはこれらの処理に焦点を当てて解説を行なっていきます。具体的には下記の3つについて説明していきます。

  • ページを分割する
  • 特定のページを取得する
  • ページの情報を取得する

このページでは、上記で示した流れに基づいて解説を行なっていきますが、もっと別の実装でもページネーションを実現することは可能です。例えば、ビューから Page の情報を取得し、その取得したデータをテンプレートに渡してやるような実装もあり得ます。

ページを分割する

まずは、ページを分割する処理について解説していきます。前述の通り、このページの分割はビューから Paginator を利用して実現していくことになります。

Paginator のコンストラクタの実行

Paginator を利用する際には、まず Paginator のコンストラクタを実行してインスタンスの生成を行います。Paginator のコンストラクタには、下記のように4つの引数を指定することが可能であり、object_listper_page の引数指定は必須となります。

コンストラクタの実行
from django.core.paginator import Paginator
paginator = Paginator(object_list, per_page, orphans, allow_empty_first_page)

object_list 引数には表示したい全てのモデルのインスタンスの集合を指定します。基本的には、データベースから取得したクエリーセットを指定することになると思います。

さらに、per_page 引数には1つのページに表示したいインスタンスの個数を整数で指定します。

これらを指定して Paginator のコンストラクタを実行すれば、object_list で与えられたクエリーセットの要素が per_page 個ごとに分割され、別々のページに割り付けられることになります。

例えば、object_list 引数に指定したクエリーセットのインスタンスの数が 32per_page10 を指定した場合、下記のように各ページにインスタンスが分割されて割り付けられることになります。

  • 1ページ目:object_list[0:10] (09 個目のインスタンス)
  • 2ページ目:object_list[10:20] (1019 個目のインスタンス)
  • 3ページ目:object_list[20:30] (2029 個目のインスタンス)
  • 4ページ目:object_list[30:32] (3031 個目のインスタンス)

このように、Paginator のコンストラクタを実行すれば、本来1ページに全て表示されていた object_list が複数のページに割り付けられることになります。つまり、Paginator のコンストラクタの実行によりページの分割が行われることになります。そして、返却値として得られる Paginator のインスタンスが、その分割後のページの情報を持っていることになります。

orphans 引数

さて、先ほど Paginator のコンストラクタの引数には object_listper_page のみを指定しましたが、他にも orphans 引数と allow_empty_first_page 引数も指定可能ですので、これらの引数について説明をしておきます。

1つ目の orphans 引数はちょっとややこしいです。まず orphans は孤児という意味の単語であり、orphans 引数で指定した整数以下の個数のインスタンスは孤児であるとみなされ、これらのインスタンスは他のページに合流させて表示されることになります。

もう少し具体的に説明すると、ページの分割を行うと最後のページだけ表示されるインスタンスの数が中途半端になることがあります。例えば、前述の例では最後のページに 2 つのインスタンスのみが表示されることになります。こういった最後のページに表示されるインスタンスの数が少ない場合、つまり孤児であるとみなされる場合に、それらのインスタンスを前のページにまとめて表示させるというのが orphans 引数の効果となります。

orphans引数の説明図

前述の例の場合、orphans 引数に 2 を指定しておけば、ページに表示されるインスタンスの数が 2 以下の場合にそれらのインスタンスは前のページに含めて表示されることになります。つまり、下記のようにページの表示が行われることになります。

  • 1ページ目:object_list[0:10] (09 個目のインスタンス)
  • 2ページ目:object_list[10:20] (1019 個目のインスタンス)
  • 3ページ目:object_list[20:32] (2031 個目のインスタンス)

orphans 引数を指定しなかった場合、orphans にはデフォルト値の 0 が設定されることになります。つまり、最後のページに表示されるインスタンスが 1 であったとしても、そのインスタンスは個別に最後のページに表示されることになります。

allow_empty_first_page 引数

allow_empty_first_page 引数は、その名の通り、最初のページに表示するインスタンスの数が 0 個であることを許可する(True)or 許可しない(False)を指定する引数になります。デフォルトは True です。

False を指定した場合、最初のページに表示するインスタンスの数が 0 個である場合に下記のような例外が発生することになります。

That page contains no results

ウェブアプリの公開初期や開発段階などの場合、表示するインスタンスの数が 0 であることも多いと思いますので、何らかの理由があって例外を発生させたい場合以外は、基本的には True を指定しておけば良いと思います。

ページを取得する

前述の通り、Paginator のコンストラクタを実行することでページの分割が行われることになります。

続いて行うことは、分割後のページの取得になります。このページの取得もビューから Paginator を利用して実現していくことになります。

ページの取得

このページの取得は Paginator のインスタンスに page メソッドを実行させることで実現できます。page メソッドの引数 num_page には、取得したいページのページ番号を整数で指定します。ページ番号は 1 から始まる点に注意してください。

ページの取得
page_obj = paginator.page(num_page)

page メソッドの返却値は Page というクラスのインスタンスとなり、このインスタンスは num_page で指定されたページの情報をデータ属性で持っていたり、ページの情報を取得するメソッドを持っていたりします。

ページ番号の取得

上記のように、ページを取得すること自体はページ番号を指定して page メソッドを実行すれば良いだけなので簡単に思えます。

ですが、実はページ番号を取得するのに少し工夫が必要です。例えば、下の図のようなページ表示の場合、現在2ページ目を表示しているのですが、次へ リンクがクリックされた際には3ページ目を表示する必要があります。

クリックされた際に次のページが表示する必要があるケース

つまり、次へ リンクがクリックされた際には、次のページの表示を行うために page メソッドの引数にページ番号として 3 を指定する必要があります。このページ番号はどのようにして取得すれば良いでしょうか?

やり方はいろいろあって、URL パターンの仕組みを利用することもできるのですが、このページネーションを実現する際にはクエリパラメーターが利用されることが多いです。クエリパラメーターとは、URL の最後部分に下記の形式で指定されるパラメーターになります。

クエリパラメーター
?変数名 = 値

そして、このクエリパラメーターはビューから取得することが可能です。具体的には、下記のように request のデータ属性から取得することが可能です。

クエリパラメーターの取得
number = int(request.GET.get('変数名', 1))

get の第2引数には 変数名 のクエリパラメーターが指定されていない場合のデフォルト値を指定します。つまり、上記の場合、クエリパラメーターが指定されなかった場合は1ページ目を表示することになります。クエリパラメーターが必ず指定されるとは限らないため、このデフォルト値は指定するようにしたほうが良いです。

また、上記の通り、ビューからはクエリパラメーターに指定された値を取得可能ですので、先ほど示した 次へ リンクがクリックされた際に送信されるリクエストの URL に下記のようなクエリパラーメータが含まれるようにしてやれば、ビューから次に表示すべきページ番号を取得することができ、page メソッドにそれを指定することで表示すべきページの Page のインスタンスを取得することができることになります。

ページ番号を表すクエリパラメーター
?p = ページ番号

上記では変数名を p にしていますが、この部分はページ番号を表すことが分かりやすければ何でも良いです。要は、クエリパラメーターを取得する際に、その変数名の値を取得するように変数名を指定してやれば良いだけです。例えばクエリパラメーターに指定される変数名が p であるのであれば、下記のように変数名 p を指定してビューからページ番号を取得すれば良いことになります。

ページ番号の取得
number = int(request.GET.get('p', 1))

じゃあ、このクエリパラメーターが URL に指定されるようにするためにはどうすれば良いでしょうか?単純ですが、そのようにテンプレートファイルを作れば良いことになります。

上の図のようなページのリンクはテンプレートファイルの記述によって表示されることになります。そして、このリンクには、クリックされた際に送信するリクエストの URL を指定することができます。ですので、その指定する URL にクエリパラメーターが含まれるようにしてやれば良いことになります。

HTML ではリンクの要素は a タグによって表示することができ、さらにリンク先の URL は href で指定できるため、下記のようなタグをテンプレートファイルに記述しておけば、リンククリック時に ?p=3 をクエリパラメーターとしたリクエストを送信できるようになります。

クエリパラメーターの付加
<a href="?p=3">next

ここで、3 は次のページ番号となるのですが、この「次のページ番号」は次に説明する Page から取得してクエリパラメーターに指定することになります。

MEMO

href="?p=3"?p=3 は相対パス指定となるため、今表示しているページの URL に対してクエリパラメーターが追加されたリクエストが送信されることになります

href="/?p=3" と最初に / を付けるとルートパス指定になり、URL 自体が変化してしまうことになるので注意してください

前者の場合、例えば表示しているページの URL が http://localhost:8000/appli/ である場合、リンクのクリックによって送信されるリクエストの URL はクエリパラメーターを含めて http://localhost:8000/appli/?p=3 となります

それに対し、後者の場合は、http://localhost:8000/?p=3 となり、元々 URL に含まれていた /appli の部分が消えてしまうことになります

スポンサーリンク

ページの情報を取得する

Page のインスタンスが取得できれば、あとは、そのインスタンスからページの情報を取得してあなたの行いたい処理や表示を行なっていけば良いだけになります。特にページネーションで必要になるのが他のページへのリンクの設置になります。

ページに割り付けられたインスタンスの取得

まず、Page のインスタンスからは、そのページに割り付けられたモデルクラスのインスタンスを取得することが可能です。Page のインスタンスはイテラブルなオブジェクトであり、下記のように for ループでページに割り付けられたモデルクラスのインスタンスを1つ1つ取得することが可能となります。

モデルクラスのインスタンスの取得
for obj in page_obj:
        print(obj)

Page のデータ属性

また、前述の通り、Page のインスタンスからはデータ属性やメソッドによってページの情報を取得することができます。例えばデータ属性 number からは、そのインスタンスに対応するページ番号を取得することができます。

また、Page はデータ属性として Paginator のインスタンスを持っており、Paginator のデータ属性やメソッドを利用することも可能です。よく使うのが num_pages で、これにより分割後のページのページ数を取得することができます。

ページ数の取得
print(page_obj.paginator.num_pages)

Page のメソッド

さらに、Page には下記のようなメソッドが存在し、これらのメソッドによって様々なページの情報を取得することができます。下記で示したメソッドは全て「引数なし」で実行することができます。

  • start_index:そのページで表示する最初のインスタンスのインデックスを整数で取得
  • end_index:そのページで表示する最後のインスタンスのインデックスを整数で取得
  • has_other_pages:そのページ以外にページが存在するかどうかをブールで取得
  • has_previous:そのページの前のページが存在するかどうかをブールで取得
  • has_next:そのページの次のページが存在するかどうかをブールで取得
  • previous_page_number:そのページの前のページのページ番号を整数で取得
  • next_page_number:そのページの次のページのページ番号を整数で取得

例えば下記を実行すれば、前のページが存在する場合のみ前ページのページ番号が表示され、次のページが存在する場合のみ次ページのページ番号が表示されることになります。

前・次のページの番号の表示
if page_obj.has_previous():
    print(page_obj.previous_page_number())
if page_obj.has_next():
    print(page_obj.next_page_number())

テンプレートから Page を利用する

上記でいくつか Page のインスタンスを利用する例を示しましたが、どちらかというとこれらの例はビューから利用される例を示したものになります。ですが、実際には Page はテンプレートファイルから利用されることの方が多いです。そして、テンプレートファイルから Page に割り付けられているモデルのインスタンスの表示や、Page のデータ属性やメソッドを利用して前後のページへのリンクの表示やページ番号の表示等を行います。

例えば、下の図のようなページを表示することを考えてみましょう。このページでは、モデルのインスタンスのリスト・表示されているページのページ番号(2)、全ページ数(5)、前のページのページ番号(1)、次のページのページ番号(3)が表示されています。

表示するページの例

このような表示であれば、テンプレートに渡すコンテキストのデータは Page のインスタンスのみでも十分実現可能です。前述の通り、Page のインスタンスからは、そのページに割り付けられているモデルのインスタンスを取得することができますし、前のページや次のページが存在すること、それらのページ番号もメソッドから取得可能です。

例えば、テンプレートファイルが受け取る Page のインスタンスを page_obj とすれば、各項目は下の図のようなテンプレートのタグや変数表示の仕組みを利用して表示を行うことができます。全ての情報が page_obj から取得できていることが確認できると思います。

テンプレートからPageを利用してページの情報を表示する様子

このように、テンプレートファイルで Page のインスタンスの情報を表示することで、モデルのインスタンスの一覧だけでなく、前後のページへ誘導するためのリンク等も設置することができるようになります。

ページネーションの利用例

続いて、ここまで説明してきた内容を踏まえながらページネーションの利用例を紹介していきます。

まずは、ページネーションを利用しない例を示し、それをページネーションを利用するアプリに変更していきたいと思います。

ページネーションを利用しないアプリ

まずはページネーションを利用しないアプリを開発しますので、いつも通りの手順でプロジェクトやアプリの作成等を行なっていきます。

プロジェクトの作成

まずは、適当な作業フォルダに移動し、その後下記コマンドを実行してプロジェクトを作成します。今回はプロジェクト名は paginationtest としたいと思います。

% django-admin startproject paginationtest

上記コマンドの実行によって paginationtest フォルダが作成されますので、下記コマンドで paginationtest フォルダ内に移動します。

% cd paginationtest

アプリの作成

続いて、下記コマンドを実行してアプリの作成を行います。今回はアプリ名は appli としたいと思います。

% python manage.py startapp appli

さらに、今いるフォルダの中に paginationtest フォルダが存在するはずですので、そのフォルダの下にある settings.py を開き、INSTALLED_APPS の定義を下記のように変更します。1行目に 'appli', を追加すれば良いだけです。

settings.pyの変更
INSTALLED_APPS = [
    'appli',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

これにより、アプリ appli がプロジェクト paginationtest に登録されることになります。

ビューの作成

次はビューを作成していきます。今いるフォルダの中に appli フォルダが存在し、その中に views.py が存在するはずですので、この views.py を下記のように変更してください。

views.py
from django.shortcuts import render
from django.contrib.auth.models import User

def index(request):
    users = User.objects.all()

    context = {
        'users' : users
    }

    return render(request, 'appli/users.html', context)

テンプレートの作成

次はテンプレートを作成していきます。まず、appli フォルダの中に templates フォルダ、さらに templates フォルダの中に appli フォルダを作成してください。

そして、最後の作成した appli フォルダの中に users.html を新規作成し、下記のように中身を変更してください。

appli/users.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>ユーザー一覧</title>
</head>
<body>
    <main class="container my-5 bg-light">
        <h2>ユーザー一覧</h2>
        <table class="table table-hover">
            <thead>
                <tr><th>ユーザー名</th><th>登録日</th></tr>
            </thead>
            <tbody>
                {% for user in users %}
                <tr>
                    <td>{{ user.username }}</td>
                    <td>{{ user.date_joined|date }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </main>
</body>
</html>

ちょっとソースコードが長いので複雑にも思えますが、単にテーブルを作成し、ビューから渡された users に含まれる各 User のインスタンスの情報を users に対する for ループで1つ1つテーブルのセルの中に出力しているだけになります。ただ、見た目を整えるために、bootstrap を読み込んだり class の設定なども行っています。

User のインスタンスの情報としては usernamedate_joined フィールドの値を出力するようにしており、date_joinedUser のインスタンスが作成された日時となります。

URL のマッピング

次は先ほど作成したビューと URL のマッピングを行います。まず、paginationtest フォルダの下にある urls.py を下記のように変更してください。

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('appli/', include('appli.urls'))
]

これにより、リクエストされた URL がルートパス指定で /appli/ から始まる場合に appli フォルダ内の urls.py が読み込まれるようになります。が、現状 appli フォルダ内に urls.py は存在しないため、appli フォルダ内に urls.py を新規作成し、その urls.py の中身を下記のように変更してください。

appli/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index')
]

マイグレーションの実行

以上で、ページネーションを “利用しない” 例としてのソースコードの変更は完了となります。

ただし、ページネーションを “利用しない” 場合と “利用する” 場合の差を確認するためには、モデルのインスタンスが複数用意されている必要があります。その準備をここから行なっていきます。

今回は、views.py の変更からも分かるように、モデルクラスとしては User を利用します。この Userauth アプリにあらかじめ用意されたモデルクラスであり、別途 models.py で定義する必要はありません。そして、データベースに保存された User のインスタンスの一覧が表示されるように views.pyusers.html が作成されています。なので、User のインスタンスを複数作成しておけば、その一覧がウェブアプリから確認できることになります。

ただし、この User のインスタンスの作成先は User に対応するテーブルであるため、このテーブルを事前に作成しておく必要があります。そして、このテーブルの作成はマイグレーションによって実現することができ、マイグレーションは下記のコマンドで実行することができます。

% python manage.py migrate

スーパーユーザーの作成

続いてユーザーの作成、すなわち User のインスタンスの作成を行なっていきます。

この作成はどんな手段で行っても良いのですが、ここでは管理画面からユーザーの作成を行なっていきたいと思っています。この管理画面でのユーザーの作成に関しては詳細を下記ページで解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。

Djangoの管理画面の使い方の解説ページアイキャッチ 【Django入門10】管理画面(admin)の使い方の基本

まず、管理画面にログインするためにはスーパーユーザーが必要となりますので、下記のコマンドでスーパーユーザーの作成を行います。

% python manage.py createsuperuser

コマンドを実行すればユーザー名とメールアドレスとパスワード(確認用も含めて2回)の入力が促されますので、適当なものを入力してください。最後に Superuser created successfully. と表示されればスーパーユーザーの作成に成功したことになります。メールアドレスやパスワードは出鱈目なものを入力するとエラーになる可能性があるので注意してください。メールアドレスやパスワードとして妥当なものを入力する必要があります。

これにより、スーパーユーザーとして管理画面にログインできるようになったことになります。

開発用サーバーの起動

次は、管理画面にログインするために下記コマンドで開発用サーバーを起動しておきたいと思います。開発用サーバーが起動することで、サーバーがリクエストを受信することが可能となり、受信したリクエストに応じてアプリが動作することになります。管理画面も利用可能となります。

% python manage.py runserver

ユーザーの作成

次は、管理画面でユーザーの作成を行なっていきます。

まず、ウェブブラウザから下記 URL を開いてください。

http://localhost:8000/admin/

管理画面へのログインフォームが表示されるはずですので、UsernamePassword に先ほど作成したスーパーユーザーのユーザー名とパスワードをそれぞれ入力して Log in ボタンをクリックしてください。

ログインフォーム

ログインすれば下の図のような管理画面のトップページが表示されると思いますので、Users の右側にある Add リンクをクリックしてください。

管理画面のトップページ

そうすると、下の図のようなユーザー追加フォームが表示されます。このフォームでユーザーの追加を行うことができますので、適当なユーザー名とパスワードを入力して SAVE ボタンのクリックを行ってください。

ユーザーの追加フォーム

ポイントは、ユーザー名は重複は許されないという点と、パスワードは重複しても良いという点になります。なので、ユーザー名は個別に考える必要がありますが、パスワードに関しては毎回同じもので良いです。ですが、フォームに記載されている通り、ユーザー名と似ているパスワードや数字だけのパスワード等は許可されていないので注意してください。

SAVE ボタンをクリックしてユーザーの追加に成功すれば、下の図のような画面が表示されます。ここではユーザーの詳細情報の変更を行うこともできるのですが、今回はこの変更は不要なので、画面左側にある Users の右の Add リンクをクリックしてください。

ユーザー追加後に遷移するページ

Add リンクをクリックすれば再びユーザー追加フォームが表示されることになります。あとは、これらを繰り返してユーザーを複数作成しておいてください。最低でも、合計で5人程度のユーザーを作成しておいていただければと思います。ちなみに、最初にコマンドで作成したスーパーユーザーもユーザーの一人となるため、スーパーユーザーを含めて5人程度のユーザーを作成しておけばオーケーです。

動作確認

ちょっと準備に時間がかかりましたが、これで一旦ページネーションを “利用しない” 場合の動作確認が可能となったことになります。ということで、ウェブブラウザから下記 URL を開いてください。

http://localhost:8000/appli/

これにより、ウェブラウザから開発サーバーにリクエストが送信され、上記 URL にマッピングされた views.pyindex 関数が動作します。そして、その中で Users のインスタンスが全て取得され、それがテンプレートファイル users.html に埋め込まれてページとして表示されることになります。

その結果は、下の図のようなものになると思います。

ページネーションを利用しない場合に表示されるページ

今回の例ではページネーションを利用していないため、全インスタンスが1つのページに表示されることになります。これは、インスタンスの数が増えたとしても同様になります。

次は、ページネーションを利用し、インスタンスが複数のページに分割されて表示されるようにしていきたいと思います。

一旦ここで動作確認は終了となるため、開発用サーバーは終了しておいてください。開発用サーバーを起動したターミナルやコマンドプロンプト等で ctrl + c を入力すれば開発用サーバーを強制終了できるはずです。

スポンサーリンク

ページネーションを利用するアプリ(ページの分割)

ということで、ここからは先ほど作成したページネーションを “利用しない” アプリを、ページネーションを “利用する” アプリに変更していきたいと思います。

ページネーションを利用するアプリに変更するため、先ほど作成した views.pyusers.html の変更を行います。

views.py の変更

まずは views.py の変更を行なっていきます。結論として、ページネーションによりページの分割を行うためには下記のように views.py を変更してやれば良いです。

変更後のviews.py
from django.shortcuts import render
from django.contrib.auth.models import User
from django.core.paginator import Paginator

def index(request):
    users = User.objects.all()

    # ページの分割
    paginator = Paginator(users, 2)

    # クエリパラメーターからページ番号取得
    number = int(request.GET.get('p', 1))

    # 取得したページ番号のページを取得
    page_obj = paginator.page(number)

    context = {
        'page_obj' : page_obj
    }

    return render(request, 'appli/users.html', context)

ページネーションを利用しない場合に比べて、ページの分割、ページ番号の取得、ページの取得の処理を追加しています。また、テンプレートに渡すコンテキストで、変数名を page_obj に変更しているので、その点にも注意してください。

まず、ページの分割は Paginator() の実行、すなわち Paginator のコンストラクタの実行によって行なっています。第1引数にはデータベースから取得したクエリセット users を指定しています。今回の場合は、User のインスタンス全てがこのクエリセット users に含まれていることになります。

さらに、第2引数には 2 を指定しているため、users の各インスタンスが 2 つずつ各ページに割り付けられるようにページの分割が行われることになります。

次に行っているのがページ番号の取得で、このページ番号は変数名が p のクエリパラメーターから取得するようにしています。変数名が p のクエリパラメーターが指定されていない場合は、取得されるページ番号はデフォルトの 1 となります。

そして、その後に取得したページ番号を引数に指定して Paginatorpage メソッドを実行して指定したページ番号に対応する Page のインスタンス page_obj の取得を行なっています。

そして、この page_obj をコンテキストに設定してテンプレートに渡すようにしています。テンプレートには変数名 'page_obj' として page_obj が渡されることになります。

users.html の変更

次にテンプレートファイルの変更を行なっていきます。今までテンプレートファイルには 'users' という変数名で User のインスタンスの集合が渡されていました。そして、この users に対して for ループを行うことで User のインスタンスを1つ1つ取得して情報を表示していました。

forループによりusersのインスタンスが1つ1つ表示される様子

ですが、先ほどのビューの変更により 'page_obj' という変数名で Page のインスタンスが渡されるようになったことになります。前述の通り、この Page のインスタンスからは、その Page のページに割り付けられたモデルクラスのインスタンスが取得可能です。今回の場合、User のインスタンスの集合を Paginator で分割して Page に割り付けたのですから、Page からは User のインスタンスが取得可能であることになります。また、この User のインスタンスは、Page のインスタンスに対して for ループを行うことで1つ1つ取得することが可能です。

forループによりpage_objに割り付けられたインスタンスが1つ1つ表示される様子

つまり、users に対して for ループを行なっている部分を page_obj に対して for ループを行うようにすれば、ページネーションを利用しなかった場合と同様に page_obj に割り付けられた User のインスタンスの情報の表示が行えることになります。ただし、users にはビューで取得された User のインスタンス全てが含まれていたのに対し、page_obj には page_obj に割り付けられた User のインスタンスのみが含まれることになります。そのため、users に対する for ループを page_obj に対する for ループに変更してやれば、自動的に page_obj に割り付けられた User のインスタンスの情報のみが表示されるようになります。

ということで、テンプレートファイル users.html を下記のように変更したいと思います。

変更後のusers.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>ユーザー一覧</title>
</head>
<body>
    <main class="container my-5 bg-light">
        <h2>ユーザー一覧</h2>
        <table class="table table-hover">
            <thead>
                <tr><th>ユーザー名</th><th>登録日</th></tr>
            </thead>
            <tbody>
                {% for user in page_obj %}
                <tr>
                    <td>{{ user.username }}</td>
                    <td>{{ user.date_joined|date }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </main>
</body>
</html>

ページネーションを利用しない場合に比べて変更しているのは下記部分のみです。元々 users に対して for ループを行なっていたところを、page_obj に対する for ループに変更しています。

users.htmlの変更部分
{% for user in page_obj %}

動作確認

一旦ここで動作確認を行なっておきましょう!

先ほどと同様の手順で開発用サーバーを起動し、ウェブアプリから下記 URL を開いてください。

http://localhost:8000/appli/

すると、下の図のようなページが表示されると思います。先程は全ての User のインスタンスが表示されていたのに対し、今回は 2 つ分のインスタンスのみが表示されていることが確認できます。これは、Paginator() 実行時に第2引数(per_page 引数)に 2 を指定しているためになります。

1ページ目の表示結果

さて、先程ウェブブラウザに指定した URL にはクエリパラメータが指定されていません。そのため、views.pyindex 関数ではページ番号としてデフォルトの 1 が表示されるようになっています。

次は、クエリパラメーターを追加して再度ページの表示を行なってみましょう!ということで、次は下記の URL をウェブブラウザに指定して開いてください。

http://localhost:8000/appli/?p=2

すると、表示されるインタンスの情報が変化することを確認できると思います。これは、クエリパラーメーターの指定によって、2 ページ目に割り付けられたインスタンスが表示されるようになったためです。

2ページ目の表示結果

ユーザーを5人以上作成している場合、クエリパラメーター部分を ?p=3 に変更した場合も、また今回とは異なるインスタンスが表示されることを確認することができます。

このように、Paginator を利用することでページが分割され、各ページに自動的にモデルクラスのインスタンスが割り付けられます。また、クエリパラメーターを利用して表示するページ番号を取得し、ユーザーが表示したいページを表示することができます。

これでページの分割が実現できたことになるのですが、ユーザーが毎回 URL のクエリパラメーターを直接変更する必要があり、ユーザーにはちょっと不親切です。次のページや前のページへのリンクを表示し、リンクのクリックでユーザーが表示したいページに遷移するようにしてあげたほうが良いです。

また、今表示しているのが何ページ目なのか、全部で何ページあるのか、といった情報も表示した挙げたほうが親切です。

ページ分割を行う場合、こういったページの情報やリンクの設置はセットで行なったほうが良いです。

ページネーションを利用するアプリ(リンクの設置)

ということで、次はページの情報の表示や前後のページへのリンク等の設置の例を示していきたいと思います。

ここで変更するのは users.html のみで、views.py からはコンテキストの 'page_obj' という変数名で Page のインスタンスが渡されるようになっているため、この Page のインスタンスから今表示しているページの情報や前後のページのページ番号等を取得し、それらを利用してよりユーザーに使いやすいページに変更していきます。

具体的には、下記のように users.html を変更します。

ページの情報の表示
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>ユーザー一覧</title>
</head>
<body>
    <main class="container my-5 bg-light">
        <h2>ユーザー一覧</h2>
        <table class="table table-hover">
            <thead>
                <tr><th>ユーザー名</th><th>登録日</th></tr>
            </thead>
            <tbody>
                {% for user in page_obj %}
                <tr>
                    <td>{{ user.username }}</td>
                    <td>{{ user.date_joined|date }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
        <div class="pagination">
            <span class="step-links">
                {% if page_obj.has_previous %}
                    <a href="?p=1">« first</a>
                    <a href="?p={{ 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="?p={{ page_obj.next_page_number }}">next</a>
                    <a href="?p={{ page_obj.paginator.num_pages }}">last »</a>
                {% endif %}
            </span>
        </div>
    </main>
</body>
</html>

前述の users.html から <table></table> の後ろ側にコードを追加し、今表示しているページの情報や前後のページへのリンクの表示を行うように変更しています。

で、この追加したコードは基本的に下記ページの Django 公式で紹介されているものの引用となります。

https://docs.djangoproject.com/en/4.2/topics/pagination/

また、この追加したコードで何をしているかは、コードと ページの情報を取得する の内容を確認していただければ大体わかるのではないかと思います。

例えば下記であれば、has_previous で前のページが存在するかどうかを確認し、存在する場合は 1 ページ目と前のページへのリンクの設置を行なっています。存在しない場合、これらのリンクは表示されません。

前ページへのリンクの設置
{% if page_obj.has_previous %}
    <a href="?p=1">« first</a>
    <a href="?p={{ page_obj.previous_page_number }}">previous</a>
{% endif %}

ポイントは、a タグの href?p=ページ番号 を指定するところになります。1 ページ目へのリンクを追加する場合は、単純に ページ番号1 を指定してやれば良いことになります。

それに対し、前のページへのリンクを追加する場合、ページ番号 に指定する値は今表示しているページによって異なることになります。例えば、今 3 ページ目を表示しているのであれば、ページ番号 には 2 を指定する必要があります。そのため、このようなページ番号は今表示しているページに合わせて動的に変化するよう previous_page_number を利用し、前のページを page_obj から取得するようにしています。

ここでは前のページへのリンクについて説明しましたが、次のページへのリンクについても同様の仕組みで追加を行なっています。

また、page_objpaginator をデータ属性として持っており、この paginator からも情報の取得が可能となります。例えば、分割後の全ページ数を page_obj は知りませんが、paginator は知っているため、この情報は paginator から取得することが可能です。上記においては、分割後の全ページ数を paginator.num_pages から取得して表示するようにしており、この全ページ数の情報から最後のページへのリンクの設置を実現しています。

動作確認

これでコードの変更は完了したので動作確認を行なっていきます。

まず開発用サーバーを起動させ、続いて下記の URL をウェブブラウザで表示してください。

http://localhost:8000/appli/

すると、下の図のようなページが表示され、インスタンスの一覧の下側に次のページへのリンク(next)と最後のページへのリンク(last)が表示されていることが確認できると思います。また、全体のページ数と今表示しているページのページ番号(1 of 3)も表示されていることが確認できると思います。

インスタンス一覧の下にリンクが表示されている様子1

前のページへのリンクが表示されていないのは、今表示しているのが 1 ページ目になるからになります。

続いて next リンクをクリックしてください。これにより、表示されるインスタンスが変化することが確認できるはずです。また、ユーザが5名以上登録されている場合、2 ページ目を表示すれば前のページへのリンク(previous)と次のページへのリンク(next)の両方が表示されます。

インスタンス一覧の下にリンクが表示されている様子2

さらに、ウェブブラウザの URL バーを確認すると、下記のようにクエリパラメーター ?p=2 が表示されているはずです。このように、リンクがクリックされた際にクエリパラメーターを設定し、ビュー側でクエリパラメータからページ番号を取得するようにすることで、分割後のページの遷移が実現されることになります。ページの分割だけでなく、ページ番号を取得するという点も重要になるので覚えておいてください!

http://localhost:8000/appli/?p=2

掲示板アプリでページネーションを利用してみる

最後に、いつも通りの流れで、この Django 入門の連載の中で開発してきている掲示板アプリに対し、ページネーションを適用していきたいと思います。

この Django 入門に関しては連載形式となっており、ここでは以前に下記ページの 掲示板アプリで管理画面をカスタマイズしてみる で作成したウェブアプリに対してページネーションを実装していきたいと思います。

【Django入門11】管理画面のカスタマイズ(ModelAdmin)

現状、このアプリにはモデルクラスのインスタンスの一覧を示すページとして「ユーザー一覧」「コメント一覧」があります。また、各ユーザーのコメント履歴が「ユーザーの詳細」ページで表示されるようになっています。

各インスタンスの一覧が表示されるページ

これらは1ページで全てのインスタンスが表示されるようになっているため、このページで紹介したページネーションを導入して複数のページの分割されるようにしていきたいと思います。

スポンサーリンク

コメント一覧の変更

アプリとしては既に動作するようになっているため、今回変更が必要なのはビューとテンプレートのみとなります。

ビューの変更

まず、コメント一覧の表示を実現しているのは view.py における下記の comments_view 関数になります。

まずは、この comments_view を例にページネーションを利用するように変更していきたいと思います。

変更前のcomments_view
from django.shortcuts import redirect, render, get_object_or_404
from .forms import RegisterForm, PostForm, LoginForm
from .models import Comment
from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, logout
from django.contrib.auth import get_user_model

# 略

@login_required
def comments_view(request):
    comments = Comment.objects.all()

    context = {
        'comments' : comments
    }

    return render(request, 'forum/comments.html', context)

といっても、変更内容は ページネーションを利用するアプリ(ページの分割) で示した通りで、インスタンスの集合を引数に指定して Paginator() を実行することでページの分割を行い、クエリパラメーターからページ番号を取得、さらに page メソッドで取得したページ番号のページの Page インスタンスを取得し、その  Page インスタンスをコンテキストにセットしてやれば良いだけです。

ということで、変更後の comments_view は下記のようなものになります。

変更後のcomments_view
from django.shortcuts import redirect, render, get_object_or_404
from .forms import RegisterForm, PostForm, LoginForm
from .models import Comment
from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, logout
from django.contrib.auth import get_user_model
from django.core.paginator import Paginator

# 略

@login_required
def comments_view(request):
    comments = Comment.objects.all()

    paginator = Paginator(comments, 3)
    number = int(request.GET.get('p', 1))
    page_obj = paginator.page(number)

    context = {
        'page_obj' : page_obj
    }

    return render(request, 'forum/comments.html', context)

今回は、各ページに表示されるインスタンスの数(per_page)を 3 に指定しています。

また、ページ番号の取得を行う request.GET.get('p', 1) の第2引数には重要な意味合いがあるので指定を忘れないよう注意してください。この第2引数は、URL にクエリパラメーター p が存在しなかった場合のデフォルト値として利用されます。第2引数が指定されなかった場合、URL にクエリパラメーター p が存在しないと None が返却されることになります。そして、Nonepage メソッドに指定して実行すると例外が発生します(そもそも上記の場合は int() 実行時に例外が発生します)。

掲示板アプリでは各ページにナビゲーションバーを表示してリンクからコメント一覧ページを表示されるようになっていますが、このリンククリック時にはクエリパラメーターは指定されません。そのため、第2引数を指定せずに request.GET.get('p') を実行するようにしてしまうと、このリンククリック時に毎回例外が発生することになってしまいますので注意してください。

テンプレートの変更

続いて、テンプレートファイル側の変更を行っていきます。

コメント一覧ページの基になっているテンプレートファイルは comments.html になっています。

変更前の comments.html は下記になります。

変更前のcomments.html
{% extends "forum/base.html" %}

{% block title %}
コメント一覧
{% endblock %}

{% block main %}
<h2>コメント一覧(全{{ comments|length }}件)</h2>
<table class="table table-hover">
    <thead>
        <tr>
            <th>本文</th><th>投稿者</th>
        </tr>
    </thead>
    <tbody>
        {% for comment in comments %}
        <tr>
            <td><a href="{% url 'comment' comment.id %}">{{ comment.text|truncatechars:20 }}</a></td>
            <td>{{ comment.user.username }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}

テンプレートファイルの変更に関しても、ページネーションを利用するアプリ(ページの分割) と ページネーションを利用するアプリ(リンクの設置) で説明した内容に基づいて変更を行えば良いです。特に、前後のページへのリンクの設置に関しては ページネーションを利用するアプリ(リンクの設置) で紹介したものをそのままコピペして利用できます。

ただし、テンプレートファイルからは comments という名前で Comment のインスタンスの集合が渡されるのではなく、page_obj という名前で Page のインスタンスが渡されるようになっていますので、この変化には注意が必要です。

特に、コメント一覧(全{{ comments|length }}件) の部分の comments|length は全てのコメントの件数を表示するための記述となっています。この部分を page_obj|length にそのまま変更してしまうと、その page_obj に割り付けられているインスタンスの数が表示されてしまうことになるため、全てのコメントの件数が表示されなくなってしまいます。全てのインスタンスの数は page_obj.paginator.count によって取得できるため、comments|lengthpage_obj.paginator.count に書き換える必要があります。

これらを考慮すると、comments.html を下記のように変更すればページネーションが実現できることになります。

変更後のcomments.html
{% extends "forum/base.html" %}

{% block title %}
コメント一覧
{% endblock %}

{% block main %}
<h2>コメント一覧(全{{ page_obj.paginator.count }}件)</h2>
<table class="table table-hover">
    <thead>
        <tr>
            <th>本文</th><th>投稿者</th>
        </tr>
    </thead>
    <tbody>
        {% for comment in page_obj %}
        <tr>
            <td><a href="{% url 'comment' comment.id %}">{{ comment.text|truncatechars:20 }}</a></td>
            <td>{{ comment.user.username }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>
<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?p=1">« first</a>
            <a href="?p={{ 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="?p={{ page_obj.next_page_number }}">next</a>
            <a href="?p={{ page_obj.paginator.num_pages }}">last »</a>
        {% endif %}
    </span>
</div>
{% endblock %}

ユーザー一覧の変更

続いてユーザー一覧の変更を行っていきますが、変更内容はコメント一覧の時と同様となりますので、ここでは変更後のビューの関数 users_view と テンプレートファイル users.html のみを示しておきます。

まず、変更後の users_view の関数は下記のようになります。

変更後のusers_view
@login_required
def users_view(request):
    users = User.objects.all()

    paginator = Paginator(users, 3)
    number = int(request.GET.get('p', 1))
    page_obj = paginator.page(number)

    context = {
        'page_obj' : page_obj
    }

    return render(request, 'forum/users.html', context)

続いて、変更後の users.html は下記のようになります。

変更後のusers.html
{% extends "forum/base.html" %}

{% block title %}
ユーザー一覧
{% endblock %}

{% block main %}
<h2>ユーザー一覧(全{{ page_obj.paginator.count }}人)</h2>
<table class="table table-hover">
   
    <thead>
        <tr>
            <th>ユーザー</th><th>コメント数</th>
        </tr>
    </thead>
    <tbody>
        {% for user in page_obj %}
        <tr>
            <td><a href="{% url 'user' user.id %}">{{ user.username }}</a></td>
            <td>{{ user.comments.all|length }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>
<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?p=1">« first</a>
            <a href="?p={{ 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="?p={{ page_obj.next_page_number }}">next</a>
            <a href="?p={{ page_obj.paginator.num_pages }}">last »</a>
        {% endif %}
    </span>
</div>
{% endblock %}

ユーザー詳細の変更

次はユーザー詳細ページの変更を行っていきます。

ユーザー詳細ページではユーザーの情報の表示とユーザーのコメント履歴を表示するようにしています。このコメント履歴部分のみをページ分割していきたいと思います。

ページ分割を行う部分を示す図

ユーザーの詳細を表示するビューの関数は user_view であり、変更前の user_vie は下記のようになっています。

user_view.py
from django.shortcuts import redirect, render, get_object_or_404
from .forms import RegisterForm, PostForm, LoginForm
from .models import Comment
from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, logout
from django.contrib.auth import get_user_model

User = get_user_model()

@login_required
def user_view(request, user_id):
    user = get_object_or_404(User, id=user_id)

    context = {
        'user' : user
    }

    return render(request, 'forum/user.html', context)

ページネーションの利用手順は基本的には comments_view の時と同じです。ですが、上記の通り、現在テンプレートに渡しているのは User のインスタンスのみで(より正確に言えば User ではなく CustomUser ですが、説明を簡単にするため、ここでは User と記します)、この User のインスタンスからリレーションが構築されているコメントをテンプレートで取得するようにしています。そして、このコメントは Comment のインスタンスであり、各ユーザーと複数のコメントとの間にリレーションが構築されていることになります。

このリレーションが構築されているコメントを全て表示するのであれば、現状のようにテンプレートに User のインスタンスのみを渡すのでも良いのですが、ページ分割を行う場合、事前にビューでページを分割し、分割後のページ、つまり Page のインスタンスをテンプレートに渡すようにする必要があります。

なので、まずはビューでユーザーの情報を表示する対象となる User のインスタンスを取得し、次に、その User とリレーションが構築されている Comment のインスタンスの集合、すなわち、user フィールドに「取得した User のインスタンス」がセットされている Comment のインスタンスの集合を取得するように変更します。さらに、その取得した Comment インスタンスの集合に対してページ分割を行うようにします。そして、comments_view 等と同様に Page のインスタンスを取得した後に、それをテンプレートに渡すように変更します。

この考え方に基づいて変更を行った user_view 関数は下記のようになります。

変更後のuser_view
@login_required
def user_view(request, user_id):
    user = get_object_or_404(User, id=user_id)
    comments = Comment.objects.filter(user=user)

    paginator = Paginator(comments, 3)
    number = int(request.GET.get('p', 1))
    page_obj = paginator.page(number)

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

    return render(request, 'forum/user.html', context)

comments を取得した後は、基本的には comments_view と同じような流れの処理になっています。ただし、テンプレートに渡すコンテキストには User のインスタンスである userPage のインスタンスである page_obj の2つがセットされるようになっています。

User のインスタンスと Page のインスタンスの2つをコンテキストにセットしているのは、テンプレート側でユーザーの情報を表示するのに User のインスタンスを利用し、ページに割り付けられたコメントの情報を表示するのに Page のインスタンスを利用するようにしたいからになります。

テンプレートの変更

ということで、テンプレート側では、ユーザーの情報を表示する部分は User のインスタンスを利用し、コメントの情報を表示するのに Page のインスタンスを利用すれば良いことになります。

ユーザーの詳細ページのテンプレートファイルは user.html であり、変更前の user.html は下記のようになっています。

変更前のuser.html
{% extends "forum/base.html" %}

{% block title %}
{{ user.username }}
{% endblock %}

{% block main %}
<h1>ユーザー({{ user.id }})</h1>
<h2>{{ user.username }}の情報</h2>
<table class="table table-hover">
    <tbody>
        <tr><th>名前</th><td>{{ user.username }}</td></tr>
        <tr><th>連絡先</th><td>{{ user.email|urlize }}</td></tr>
        <tr><th>年齢</th><td>{{ user.age }}</td></tr>

    </tbody>
</table>
<h2>{{ user.username }}のコメント履歴</h2>
<table class="table table-hover">
    <tbody>
        {% for comment in user.comments.all %}
        <tr>
            <td><a href="{% url 'comment' comment.id %}">{{ comment.text|truncatechars:20 }}</a></td>
        </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}

前半部分ではユーザーの情報の表示を行なっており、変更前でも User のインスタンスを利用しているので、特に変更は必要ありません。それに対し、後半部分ではコメントの情報を表示するために、user.comments.all に対する for ループを行なっているため、この部分を page_obj に対するループに変更する必要があります。この変更のみを行えば、今まで通りループの中で Comment のインスタンスが comment として取得されるようになるため、for ループの中身の変更は不要となります。

また、前後のページへのリンクを追加するため、これに関しては comments.html の変更時と同様に ページネーションを利用するアプリ(リンクの設置) で示したものをそのままコピペします。

これらを踏まえて変更を行った user.html は下記のようになります。

変更後のuser.html
{% extends "forum/base.html" %}

{% block title %}
{{ user.username }}
{% endblock %}

{% block main %}
<h1>ユーザー({{ user.id }})</h1>
<h2>{{ user.username }}の情報</h2>
<table class="table table-hover">
    <tbody>
        <tr><th>名前</th><td>{{ user.username }}</td></tr>
        <tr><th>連絡先</th><td>{{ user.email|urlize }}</td></tr>
        <tr><th>年齢</th><td>{{ user.age }}</td></tr>

    </tbody>
</table>
<h2>{{ user.username }}のコメント履歴</h2>
<table class="table table-hover">
    <tbody>
        {% for comment in page_obj %}
        <tr>
            <td><a href="{% url 'comment' comment.id %}">{{ comment.text|truncatechars:20 }}</a></td>
        </tr>
        {% endfor %}
    </tbody>
</table>
<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?p=1">« first</a>
            <a href="?p={{ 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="?p={{ page_obj.next_page_number }}">next</a>
            <a href="?p={{ page_obj.paginator.num_pages }}">last »</a>
        {% endif %}
    </span>
</div>
{% endblock %}

スポンサーリンク

動作確認

以上で、ページネーションを利用するための実装は完了したことになります。

最後に動作確認を行なっていきましょう!

開発用サーバーの起動

まずは前準備として、いつも通り開発用サーバーの起動を行います。manage.py が存在するフォルダ(testproject フォルダ)に移動し、下記コマンドを実行して開発用サーバーを起動してください。

python manage.py runserver

models.py の変更は行なっていないため、マイグレーション等の実施は不要となります。

続いて、ウェブブラウザを起動し、下記 URL を開きます。

http://localhost:8000/forum/

上記 URL を開くとコメント一覧ページもしくはログインフォームが表示されることになると思います。

ログインフォームが表示された方はログアウトされている状態であるため、ログインフォームからまずはログインを行なってください。ログインすれば、ログインしたユーザーの詳細情報のページが表示されることになりますが、このページについては後から確認しますので、まずはナビゲーションバーの コメント一覧 をクリックしてコメント一覧ページを表示してください。

また、ログイン可能なユーザー名・パスワードを忘れてしまった方は、ユーザー登録 リンクをクリックしてユーザー登録を行なっていただければログインすることが可能です。この場合も、登録したユーザーの詳細情報のページが表示されることになりますが、ナビゲーションバーの コメント一覧 をクリックしてください。

ということで、これで皆さんがコメント一覧ページを見ている状態になっていると思いますので、まずはコメント一覧ページを確認してみましょう!

コメント一覧ページでは、今まで投稿済みのコメントが全てページ内に表示されるようになっていたのですが、今回の変更によって表示されるコメントの数は各ページで 3 以下となるようになっています。そして、投稿済みのコメントの数が 4 以上の場合、ページの分割が行われ、コメント一覧の下の方に次のページへのリンク(next)や最後のページへのリンク(last)が表示されるようになっています。

表示されるコメント一覧ページ

コメントの数が 4 未満の場合は次のページへのリンク(next)や最後のページへのリンク(last)が表示されないため、ナビゲーションバーの コメント投稿 をクリックし、コメントを投稿してコメント数を増やしてみてください。コメントの数が 4 以上になれば、コメント一覧の下にリンクが表示されるようになるはずです。

そして、これらのリンクをクリックすれば、次のページや最後のページを表示できることが確認できると思います。また、例えば次のページのリンクをクリックすれば、今度は前のページへのリンク(previous)や最初のページへのリンク(first)も表示されることが確認でき、これらをクリックすれば、前のページや最初のページに遷移することも確認できると思います。

次へをクリックした時に表示されるページ

このように、Paginator を利用することでページの分割を実現することができ、さらにリンクを設置することで分割後の各ページへの遷移をマウス操作で実現することができるようになります。

また、上記ではコメント一覧を例に説明を行いましたが、ユーザー一覧やユーザーの詳細ページに表示されるコメント履歴に関しても同様のことが確認できると思います。

ナビゲーションバーの ユーザー一覧 をクリックすればユーザー一覧が表示され、ここでもページ分割が行われていることが確認できると思います。この場合もユーザー数が 4 未満の場合は1ページで全てのユーザーが表示されることになりますが、ナビゲーションバーの ユーザー登録 からユーザーの登録を行えばユーザー数を増やすことができ、ユーザー数が 4 以上になればページの分割が行われることが確認できると思います。

ユーザー一覧がページ分割されている様子

また、このユーザー一覧から特定のユーザーの名前をクリックすることで、そのユーザーの詳細ページが表示され、そこでコメント履歴も確認することができます。このコメント履歴に関しても同様のことが確認できると思います。

コメント履歴がページ分割されている様子

以上で動作確認は完了です!

まとめ

このページでは、Django のページネーションについて解説をしました!

Django では Paginator が用意されており、これを利用することで簡単にページネーションを実現することができます。特にページ分割に関しては簡単に実現できることが、このページの解説からも理解していただけたのではないかと思います。また、分割後のページへの遷移をマウス操作で実現するためにはリンクの設置も必要で、これに関してはややこしいようにも思えますが、基本的に一度作ってしまえば、あとは基本的にはコピペだけで実装できると思います。

アプリの開発入門者にとってはあまり魅力的な機能に思えないかもしれないですが、ウェブアプリの利用者が増えると大量のレコードがデータベースに保存されることになり、それを一度に表示すると閲覧性の悪いページになってしまいます。そういったことを改善するのにページネーションは有効ですので、是非この機能についても覚えておいてください!

次の連載では、Django でのウェブアプリ開発時に陥りやすい N + 1 問題と、その解決方法について解説します。ちょっと地味な話題になりますが、ウェブアプリ開発者には是非知っておいてもらいたい内容の解説になりますので、是非次の連載も読んでみてください!

DjangoにおけるN+1問題の解説ページアイキャッチ 【Django入門13】N+1問題とselect_related・prefetch_relatedでの解決

同じカテゴリのページ一覧を表示

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です