【Django】ページネーション(ページング):記事一覧を複数ページに分ける方法
2020.08.03 /
大量のデータ一覧を表示する場合、複数のページに分けて表示したいときがあります。そういった場合は、ページネーション(ページング)と呼ばれる技術を使います。
例えば、Webアプリケーションで自社のユーザー一覧を表示する場合、10人程度であれば1つのページに表示するのは問題ないです。
しかし何百人もいる場合は、ページを分けた方が使い勝手やページの読みやすさはいいですよね。
また、ブログで記事数が100以上ある場合、1つのページに記事一覧すべてを表示なんかしないですよね。
このような場合に、ページを複数に分けるページネーション(ページング)を使用して、1画面に表示する件数を制限してコンパクトにします。
今回は、このページネーションをDjangoでどのように実装するのか解説していきます。
環境はPython 3.7.5、Django3.0.0です。
完成ページ
最終的に完成させるページネーションの表示は以下のようになります。
どのようにして、このページネーションを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により、以下のようにページネーションが表示されます。
サンプルプログラム2
上記サンプルでは、ページ数が増えるとその分ページ番号が横並びになるため、ページ数が多い場合は使用できません。
そういった場合は次のようにページネーションを表示することができます。
上記のようにページネーションを表示するために、テンプレートでは次のように記述しています。
<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アプリケーションに取り入れていただき、使いやすいアプリ作成を実現させてください。