投稿日:2019年12月7日
ブログの記事が10,20,...100,200,と増えて行くことを考えるとブログのリストは複数のページに分けるべきであることがわかりますね。この記事ではDjangoのPaginatorを使ってこれを実装してみたいと思います。
Webサイトではブログ記事一覧やユーザー一覧などの様に、特定のデータの一覧を実装する場面が多々あります。しかし、一覧だからといって単純にデータを全て取得し、すべて表示していてはユーザビリティ的にもパフォーマンス的にも問題があります。
そこで用いられるのがPaginationという方法です。これはデータの一覧を一定間隔で区切り、ユーザーに順番にみていってもらう方式です。
最も身近な例で言うとGoogleでの検索がそれですね。
もしGoogleが、数千、数万にわたる検索結果を一つのページに表示していたら、情報を探すことよりも米粒のようなスクロールバーをどう扱うかにばかり頭を悩ませていたかも知れませんね。
まずはviews.pyを編集して、article_list関数からtemplateに渡されるcontextのデータをPaginatorを使って分割してみます。
[...]
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
[...]
def article_list(request):
"""
記事の一覧を表示する
"""
object_list = Article.published.all()
paginator = Paginator(object_list, 3)
page = request.GET.get('page')
try:
articles = paginator.page(page)
except PageNotAnInteger:
articles = paginator.page(1)
except EmptyPage:
articles = paginator.page(paginator.num_pages)
return render(
request,
"blog/article_list.html",
{"articles":articles,}
)
[...]
Paginatorはインスタンス化する際に、ページングする対象のQuerySetを第一引数object_listに、各ページに含めるQuerySetの数を第2引数per_pageに受け取ります。第3引数、第4引数にはそれぞれオプション引数としてorphans(リストにおけるstepの様なもの)と、Bool型でallow_empty_first_page(object_listが空の状態を許容するかどうか)を受け取ります。
Paginatorインスタンスはpage()メソッドで引数で渡した値のページ目のObjectをper_page分だけ返します。このメソッドの使用をする際には2つのExceptionについて考慮しておく必要があります。一つはpage()に渡される値が整数でないときに投げられるPageNotAnIntegerで、もう一つは空のページ(最大ページ数を超えたときには空のページになる)を指定したときに投げられるEmptyPageです。
今回はtry-exceptできちんとそれらをキャッチできる様にしています。これをしないと、Exceptionが発生する様なページを指定するたびに500(InternalServerError)が発生するサイトになってしまうので気をつけましょう。
リクエストのGETパラメータはviewに渡した第一引数を使ってrequest.GET.get("パラメータのキー")のように取得できます。
<div class="pagination">
<span class="step-links">
{% if page.has_previous %}
<a href="?page={{ page.previous_page_number }}">前のページ</a>
{% endif %}
<span class="current">
Page {{ page.number }} / {{ page.paginator.num_pages }}.
</span>
{% if page.has_next %}
<a href="?page={{ page.next_page_number }}">次のページ</a>
{% endif %}
</span>
</div>
[...]
{% block content %}
[...]
{% include "pagination.html" with page=articles %}
{% endblock %}
ここで新しく{% include %}タグが登場しています。ここまで使用してきた{% extends %}タグで予め作成しておいた枠となるテンプレートを呼び出して、コンテンツに当たる部分を{% block %} {%endblock %}タグで補完するという作成方法でした。それに対して{% include %}タグは予め作成しておいたパーツを呼び出すタグです。withで引数の様に値を定義することで、呼び出したテンプレート内で使う変数を渡すことができます。
以下の様に使い分けましょう。
サーバーを起動させてhttp://localhost:8000/blog/の様子を確認してみましょう。
$ python manage.py runserver
ここではコードを管理しやすくするために少しだけリファクタリングをします。
[...]
def article_list(request):
"""
記事の一覧を表示する
"""
object_list = Article.published.all()
paginator = Paginator(object_list, 3)
page = request.GET.get('page',1) # default値を設定
articles = paginator.get_page(page) # get_page()に変更
return render(
request,
"blog/article_list.html",
{"articles":articles,}
)
まずはget_page()メソッドについてです。先程のpage()メソッドのときにはHTTPリクエストのGETパラメータとして
と、それぞれの場合に対応できる様に、PageNotAnInteger、EmptyPageのを受け取る例外処理をしていました。このアプローチこれとして後々自分で何かのページを作る時GETパラメータとして値を受け取る際に使うことになると思うので覚えておいてください。Paginatorのget_page()と言うメソッドにはすでにこの例外処理が実装されています。
もうひとつがGETパラメータの値を取得する際のデフォルト値です。request.GET.get()の第2引数に値を渡すことでそのキーで値がセットされていなかった時のデフォルト値として利用できます。
最後にChapterで使用したBootstrap4にはPaginationのデザインが用意されています。これを利用して検索エンジンにあるようなPaginationを作成してみましょう。これをDjangoTemplateでレンダリングする際には以下の情報が必要になります。
from django.shortcuts import render, get_object_or_404, Http404
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from .models import Article
def article_list(request):
"""
記事の一覧を表示する
"""
articles_list = Article.published.all()
paginator = Paginator( articles_list, 3) # paginatorオブジェクトを生成
page = request.GET.get("page",1) # GETパラメータのpageキーの値を取得
page_range = paginator.page_range # ページのレンジを取得
articles = paginator.get_page(page)
return render(
request,
"blog/article_list.html",
{
'page': int(page),
'page_range':page_range,
"articles":articles,}
)
[...]
テンプレート側ではDjangoテンプレート言語の{% if %} {% else %} {% endif %}文を駆使して場合分けをしていきます。
<ul class="pagination">
{% if object_page.has_previous %}
<li class="page-item">
<a href="?page={{ object_page.previous_page_number }}" class="page-link">«</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="" class="page-link">«</a>
</li>
{% endif %}
{% for i in page_range %}
<li class="page-item {% if forloop.counter == page %}active{% endif %}"><a class="page-link" href="?page={{ forloop.counter }}">{{ forloop.counter }}</a></li>
{% endfor %}
{% if object_page.has_next %}
<li class="page-item">
<a href="?page={{ object_page.next_page_number }}" class="page-link">»</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="" class="page-link disabled">»</a>
</li>
{% endif %}
</li>
</ul>
ここで注目してほしいのがpage_rangeのforループです。Djangoテンプレート言語にはpythonのrange()の様な指定回数ループする機能がありません。そういう時は今回の様にview側でrangeの役割を果たす変数を生成してそれをcontextとして渡すというアプローチを取ります。
呼び出す側の変更は{% include %}のwithで渡す変数が増えるだけです。
{% extends "base.html" %}
{% block title %}ブログ一覧|Sampleブログ{% endblock %}
{% block content %}
{% include "pagination.html" with object_page=articles page=page page_range=page_range %}
{% endblock %}