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

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

Python - Django
2018年12月3日6:14に更新(約10日前)
2018年11月23日20:32に作成(約20日前)

概要

データの一覧が多すぎるとき、一度に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>

再利用可能な部品にする

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

page.htmlのようにして、ページネーション部分を抜き出しておきます。includeされる部品であることをわかりやすくするために、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()

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

{% 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>

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

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする