※このブログではサーバー運用、技術の検証等の費用のため広告をいれています。
記事が見づらいなどの問題がありましたらContactからお知らせください。

<前のページ
【初心者チュートリアル】Django2でブログ作成(Part14)〜ページのデザイン~

【初心者チュートリアル】Django2でブログ作成(Part15)〜Pagination~

Django2 python3 Djangoチュートリアル Pagination

投稿日:2019年12月7日

このエントリーをはてなブックマークに追加

はじめに

この記事について

ブログの記事が10,20,...100,200,と増えて行くことを考えるとブログのリストは複数のページに分けるべきであることがわかりますね。この記事ではDjangoのPaginatorを使ってこれを実装してみたいと思います。


Paginator

Paginationとは

Webサイトではブログ記事一覧やユーザー一覧などの様に、特定のデータの一覧を実装する場面が多々あります。しかし、一覧だからといって単純にデータを全て取得し、すべて表示していてはユーザビリティ的にもパフォーマンス的にも問題があります。
そこで用いられるのがPaginationという方法です。これはデータの一覧を一定間隔で区切り、ユーザーに順番にみていってもらう方式です。

▲Paginationのイメージ図

最も身近な例で言うとGoogleでの検索がそれですね。
もしGoogleが、数千、数万にわたる検索結果を一つのページに表示していたら、情報を探すことよりも米粒のようなスクロールバーをどう扱うかにばかり頭を悩ませていたかも知れませんね。

▲Google検索で利用されているPagination

viewでpaginatorを使ってみる

まずはviews.pyを編集して、article_list関数からtemplateに渡されるcontextのデータをPaginatorを使って分割してみます。

sample_blog/blog/views.py
[...]
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_pageobject_listが空の状態を許容するかどうか)を受け取ります。

Paginatorインスタンスはpage()メソッドで引数で渡した値のページ目のObjectper_page分だけ返します。このメソッドの使用をする際には2つのExceptionについて考慮しておく必要があります。一つはpage()に渡される値が整数でないときに投げられるPageNotAnIntegerで、もう一つは空のページ(最大ページ数を超えたときには空のページになる)を指定したときに投げられるEmptyPageです。
今回はtry-exceptできちんとそれらをキャッチできる様にしています。これをしないと、Exceptionが発生する様なページを指定するたびに500(InternalServerError)が発生するサイトになってしまうので気をつけましょう。

リクエストのGETパラメータはviewに渡した第一引数を使ってrequest.GET.get("パラメータのキー")のように取得できます。

sample_blog/blog/templates/pagination.html
<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>
sample_blog/blog/templates/blog/article_list.html
[...]

{% block content %}

[...]

  {% include "pagination.html" with page=articles %}
{% endblock %}

ここで新しく{% include %}タグが登場しています。ここまで使用してきた{% extends %}タグで予め作成しておいた枠となるテンプレートを呼び出して、コンテンツに当たる部分を{% block %} {%endblock %}タグで補完するという作成方法でした。それに対して{% include %}タグは予め作成しておいたパーツを呼び出すタグです。withで引数の様に値を定義することで、呼び出したテンプレート内で使う変数を渡すことができます。
以下の様に使い分けましょう。

  • {% include %}:Paginationや特定のリンクなど、置く場所の決まっていない汎用的なパーツとなる部分
  • {% extends %}:サイト全体やサイト内の特定の機能全体などサイト全体で同じ場所、同じスタイルで提供することで統一感を持たせられそうな枠となる部分。ヘッダやフッタ、ナビゲーションがこれに当たる。

サーバーを起動させてhttp://localhost:8000/blog/の様子を確認してみましょう。

ターミナル
$ python manage.py runserver
▲前後のページのリンクが自動的に生成される

リファクタリング

ここではコードを管理しやすくするために少しだけリファクタリングをします。

sample_blog/blog/views.py
[...]

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パラメータとして

  • pageキーで整数が送られた時
  • pageキーで送られたページがない時
  • pageキーで値が送られない、またはおくられた値が整数ではない時

と、それぞれの場合に対応できる様に、PageNotAnIntegerEmptyPageのを受け取る例外処理をしていました。このアプローチこれとして後々自分で何かのページを作る時GETパラメータとして値を受け取る際に使うことになると思うので覚えておいてください。Paginatorget_page()と言うメソッドにはすでにこの例外処理が実装されています。

もうひとつがGETパラメータの値を取得する際のデフォルト値です。request.GET.get()の第2引数に値を渡すことでそのキーで値がセットされていなかった時のデフォルト値として利用できます。

Bootstrap4で使いやすいPaginationを作成

最後にChapterで使用したBootstrap4にはPaginationのデザインが用意されています。これを利用して検索エンジンにあるようなPaginationを作成してみましょう。これをDjangoTemplateでレンダリングする際には以下の情報が必要になります。

  • 現在のページナンバー
  • 全ページのiteration
  • 前後にページがあるかどうか

sample_blog/blog/views.py
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 %}文を駆使して場合分けをしていきます。

sample_blog/blog/templates/pagination.html
<ul class="pagination">
    {% if object_page.has_previous %}
    <li class="page-item">
    <a href="?page={{ object_page.previous_page_number }}" class="page-link">&laquo;</a>
    </li>
    {% else %}
    <li class="page-item disabled">
    <a href="" class="page-link">&laquo;</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">&raquo;</a>
    </li>
    {% else %}
    <li class="page-item disabled">
    <a href="" class="page-link disabled">&raquo;</a>
    </li>
    {% endif %}
    </li>
  </ul>

ここで注目してほしいのがpage_rangeのforループです。Djangoテンプレート言語にはpythonのrange()の様な指定回数ループする機能がありません。そういう時は今回の様にview側でrangeの役割を果たす変数を生成してそれをcontextとして渡すというアプローチを取ります。
呼び出す側の変更は{% include %}withで渡す変数が増えるだけです。

sample_blog/blog/templates/blog/article_list.html
{% extends "base.html" %}
{% block title %}ブログ一覧|Sampleブログ{% endblock %}
{% block content %}

  {% include "pagination.html" with object_page=articles page=page page_range=page_range %}
{% endblock %}

さいごに

まとめ

  • ページの分割にはdjango.core.paginator.Paginatorを使う。
  • HttpリクエストのGETパラメータはrequest.GET.get()で取得できる。
  • Djangoのテンプレート言語では{% include %}で他のテンプレートを呼び出せる。
  • Djangoのテンプレート言語では{% if %}{% else %}{% endif %}で場合分けができる。
このエントリーをはてなブックマークに追加

<前のページ
【初心者チュートリアル】Django2でブログ作成(Part14)〜ページのデザイン~

関連記事

記事へのコメント