Django、ページング処理まとめ

Python Django Bulma Bootstrap4


概要

データの一覧が多すぎるとき、一度に100件表示するよりも、10件などの単位で表示することで読みやすいページになります。これが所謂ページング(ページネーション)というもので、Webアプリケーションにおいては頻出する処理です。

今回はDjangoでページングを実装にするにあたっての基本的な使い方や、いくつかの見た目のサンプル、他GETパラメータとの共存等についてを紹介します。

クラスベースビュー

まず、クラスベースビューで使う方法です。ソースコードではPostというモデルを利用していますが、お好きなモデルを利用してください。

from django.views import generic
from .models import Post


class PostIndex(generic.ListView):
    model = Post
    paginate_by = 1

データを一覧表示したいとき、クラスベースビューではgeneric.ListViewを使うのが一般的です。このビューは一覧表示だけでなく、ページング処理も行ってくれます。

1ページに何件表示するかの指定が、paginate_byです。今回は試しやすいように1ページに1件という贅沢な指定にしました。

post_listには、1ページに表示する件数分のデータ...上の例なら1件...が入っており、ページ情報に関してはpage_objという名前でテンプレートへ渡されます。

関数ビュー

少し長いですが、ビューをお見せします。

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import render
from .models import Post


def paginate_queryset(request, queryset, count):
    """Pageオブジェクトを返す。

    ページングしたい場合に利用してください。

    countは、1ページに表示する件数です。
    返却するPgaeオブジェクトは、以下のような感じで使えます。

        {% if page_obj.has_previous %}
          <a href="?page={{ page_obj.previous_page_number }}">Prev</a>
        {% endif %}

    また、page_obj.object_list で、count件数分の絞り込まれたquerysetが取得できます。

    """
    paginator = Paginator(queryset, count)
    page = request.GET.get('page')
    try:
        page_obj = paginator.page(page)
    except PageNotAnInteger:
        page_obj = paginator.page(1)
    except EmptyPage:
        page_obj = paginator.page(paginator.num_pages)
    return page_obj


def post_index(request):
    post_list = Post.objects.all()
    page_obj = paginate_queryset(request, post_list, 1)
    context = {
        'post_list': page_obj.object_list,
        'page_obj': page_obj,
    }
    return render(request, 'app/post_list.html', context)

関数ビューでは、そのページに合わせて件数を調整する処理であったり、ページ情報のオブジェクト作成処理を自分でやる必要があります。とはいえ、基本的には上にあるpaginate_queryset()関数のような形になります。

post_listは件数調整済みのクエリセット、page_objとしてページ情報オブジェクトをテンプレートへ渡すようにしています。本来はpage_objを{% for %}に渡せばページ件数分のデータが取得できる...つまり、辞書内の'post_list'要素は必要ないのですが、クラスベースビューと似た挙動にするためにあえて作成しています。

Djangoで、モデルフォームセット+ページングではモデルフォームセットにページング機能をつけましたが、こういった場合はListViewを使うと逆にややこしくもなります。関数ベースでの使い方を覚えておくと、そのようなとき役立ちます。

いくつかの見た目サンプル

ページネーション部分をどうやって見せるかは自由です。シンプルなものから、オシャレなものまで自由に作れます。

前へ 1/3 次へ

まず、最もシンプルな作りのやつです。前と次へのリンク、今のページ番号と最大ページが確認できます。
前へ 1/3 次へ のように表示

    {% for post in post_list %}
        <p>{{ post.title }}</p>
    {% endfor %}

    <!-- 前へ の部分 -->
    {% if page_obj.has_previous %}
        <a href="?page={{ page_obj.previous_page_number }}">前へ</a>
    {% endif %}

    <!-- 1/3 の部分 -->
   {{ page_obj.number }}/{{ page_obj.paginator.num_pages }}

    <!-- 次へ の部分 -->
    {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">次へ</a>
    {% endif %}

前へ 1 2 3 次へ

前、ページ番号、次のリンクが表示されるシンプルな例です。各ページ番号クリックでそのページに飛べるので、割と便利です。
前、ページ番号、次

    {% for post in post_list %}
        <p>{{ post.title }}</p>
    {% endfor %}

    <!-- 前へ の部分 -->
    {% if page_obj.has_previous %}
        <a href="?page={{ page_obj.previous_page_number }}">前へ</a>
    {% endif %}

    <!-- 数字の部分 -->
    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <span>{{ num }}</span>
        {% else %}
            <a href="?page={{ num }}">{{ num }}</a>
        {% endif %}
    {% endfor %}

    <!-- 次へ の部分 -->
    {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">次へ</a>
    {% endif %}

Bootstrap4

Bootstrap4にはページネーションを作るためのCSS設定が用意されています。
Bootstrap4のページネーション

これぐらいになると、割とオシャレで、どこに出しても恥ずかしくなさそうですね。

{% for post in post_list %}
    <p>{{ post.title }}</p>
{% endfor %}

<ul class="pagination">
    <!-- 前へ の部分 -->
    {% if page_obj.has_previous %}
        <li class="page-item">
            <a class="page-link" href="?page={{ page_obj.previous_page_number }}">
                <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
    {% endif %}

    <!-- 数字の部分 -->
    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <li class="page-item active"><a class="page-link" href="#!">{{ num }}</a></li>
        {% else %}
            <li class="page-item"><a class="page-link" href="?page={{ num }}">{{ num }}</a></li>
        {% endif %}
    {% endfor %}

    <!-- 次へ の部分 -->
    {% if page_obj.has_next %}
        <li class="page-item">
            <a class="page-link" href="?page={{ page_obj.next_page_number }}">
                <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
    {% endif %}
</ul>

Bulma

Bulmaにも専用のものがあります。

<nav class="pagination is-centered" role="navigation" aria-label="pagination">
    <!-- 前へ の部分 -->
    {% if page_obj.has_previous %}
        <a class="pagination-previous" href="?page={{ page_obj.previous_page_number }}">前へ</a>
    {% endif %}

    <!-- 次へ の部分 -->
    {% if page_obj.has_next %}
        <a class="pagination-next" href="?page={{ page_obj.next_page_number }}">次へ</a>
    {% endif %}

  <ul class="pagination-list">
    <!-- 数字の部分 -->
    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <a class="pagination-link is-current" href="#!">{{ num }}</a>
        {% else %}
            <a class="pagination-link" href="?page={{ num }}">{{ num }}</a>
        {% endif %}
    {% endfor %}
  </ul>
</nav>

こちらも良い見た目ですね。 Bulmaのページネーション

再利用可能な部品にする

テンプレートでのページネーション部分は割と記述が長くなりますし、そもそも汎用的に使えるものなので、独立したテンプレートにするのがオススメです。

page.htmlのようにして、ページネーション部分を抜き出しておきます。includeされる部品であることをわかりやすくするために、includesのようなディレクトリに置いておくと良いです。

プロジェクト直下にテンプレートディレクトリを作っている場合で、全てのアプリケーションに共通して使えそうならばtemplates直下にincludesを作ったり、そのまま置いても良いでしょう。

<ul class="pagination">
    <!-- 前へ の部分 -->
    {% if page_obj.has_previous %}
        <li class="page-item">
            <a class="page-link" href="?page={{ page_obj.previous_page_number }}">
                <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
    {% endif %}

    <!-- 数字の部分 -->
    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <li class="page-item active"><a class="page-link" href="#!">{{ num }}</a></li>
        {% else %}
            <li class="page-item"><a class="page-link" href="?page={{ num }}">{{ num }}</a></li>
        {% endif %}
    {% endfor %}

    <!-- 次へ の部分 -->
    {% if page_obj.has_next %}
        <li class="page-item">
            <a class="page-link" href="?page={{ page_obj.next_page_number }}">
                <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
    {% endif %}
</ul>

他のテンプレートからは、テンプレートタグ{% include %}を使って読み込みます。

    {% for post in post_list %}
        <p>{{ post.title }}</p>
    {% endfor %}

    {% include 'app/includes/page.html' %}

他GETパラメータと共存させる

例えばですが、テンプレートの内容が以下のようになったとしましょう。検索フォームがつきました。

    {% for post in post_list %}
        <p>{{ post.title }}</p>
    {% endfor %}

    {% include 'app/page.html' %}

    <hr>
    <form action="" method="GET">
        {{ search_form.as_p }}
        <button type="submit">検索</button>
    </form>

「こん」というキーワードで検索すると、キーワードが含まれた記事だけ表示されます。
こん、で検索してみる

「こんにちは」と「こんばんは」という記事の2つがマッチし、1ページ1件なので、2ページまで表示されるのは正しい動作です。

ふと次ページへのリンクを押してみました。すると、なぜか以下のような状態になります。
なぜかページ数が急に増える

全部で2ページのはずですが、何故か4ページに増えています。現在4記事なので、全ての記事が取得されているということになります。検索結果が維持されていないというわけです。

ページリンクのhrefを思い出しましょう、以下のようになっていました。

<a class="page-link" href="?page={{ page_obj.previous_page_number }}">

GETパラメータでデータを送信するとき、それはURLで管理されます。?keyword=こんのようなURLになり、このURLからDjangoはキーワードを取得するのですが、href="?page=のようなリンクの書き方をすると、元のGETパラメータ...つまり?keyword=こんは消えてしまいます。なので、検索結果が維持できなくなるのです。

検索結果を維持しつつページングもさせるには、結果的に以下のようなURLになる必要があります。

?keyword=こん&page=1

これを解決する最もスマートな方法は、GETパラメータをうまい具合に結合するテンプレートタグを作ることです。

汎用的な解決策

テンプレートフィルタ・タグを作るには、アプリケーション内にtemplatetagsディレクトリを作り、任意のpythonファイルを作ります。このpythonファイル名が、テンプレートでの{% load ファイル名 %}に対応するのでわかりやすいのが良いです。アプリケーション名とかをよく使います。今回はapp.pyとしました。

そして、以下のようなコードにします。

from django import template
register = template.Library()


@register.simple_tag
def url_replace(request, field, value):
    """GETパラメータを一部を置き換える"""

    url_dict = request.GET.copy()
    url_dict[field] = str(value)  # Django2.1の一部対策。通常はvalueだけでOK
    return url_dict.urlencode()

テンプレートのページネーション部分も、このタグを使うように書き換えます。Bootstrap4の例。

{% load app %}<!-- app部分は、ファイル名です。 -->

<ul class="pagination">
    <!-- 前へ の部分 -->
    {% if page_obj.has_previous %}
        <li class="page-item">
            <a class="page-link" href="?{%  url_replace request 'page' page_obj.previous_page_number %}">
                <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
    {% endif %}

    <!-- 数字の部分 -->
    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <li class="page-item active"><a class="page-link" href="#!">{{ num }}</a></li>
        {% else %}
            <li class="page-item"><a class="page-link" href="?{%  url_replace request 'page' num %}">{{ num }}</a></li>
        {% endif %}
    {% endfor %}

    <!-- 次へ の部分 -->
    {% if page_obj.has_next %}
        <li class="page-item">
            <a class="page-link" href="?{%  url_replace request 'page' page_obj.next_page_number %}">
                <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
    {% endif %}
</ul>

Bulmaの例

{% load app %}<!-- app部分は、ファイル名です。 -->

<nav class="pagination is-centered" role="navigation" aria-label="pagination">
    <!-- 前へ の部分 -->
    {% if page_obj.has_previous %}
        <a class="pagination-previous" href="?{% url_replace request 'page' page_obj.previous_page_number %}">前へ</a>
    {% endif %}

    <!-- 次へ の部分 -->
    {% if page_obj.has_next %}
        <a class="pagination-next" href="?{% url_replace request 'page' page_obj.next_page_number %}">次へ</a>
    {% endif %}

  <ul class="pagination-list">
    <!-- 数字の部分 -->
    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <a class="pagination-link is-current" href="#!">{{ num }}</a>
        {% else %}
            <a class="pagination-link" href="?{% url_replace request 'page' num %}">{{ num }}</a>
        {% endif %}
    {% endfor %}
  </ul>
</nav>

コード的には長くなりましたが、実際かなり汎用的です。他のGETパラメータがいくつあろうがなかろうが、安定して動作します。私は、ページネーションには無条件にこのテンプレートタグを利用しています。


記事にコメントする