Django、モデルフォームセット+ページング機能

Python Django

概要

Djangoでフォームセットを使うシリーズ。モデルフォームセットは、すでに作成済みデータの更新用フォームも表示されます。1000件作成していれば、1000件がすべて表示されます。そこで、1ページ内に10件だけ表示されるようにページング機能を実装していきます。

このように、次ページへのリンクが表示され... リンクが表示される

きちんと次ページに移動します。
次ページに移動する

Django、モデルフォームセットの基本的な使い方のモデルやフォームを元に作成していきます。ソースコードのダウンロードは、Githubからクローンしてください。

フォーム

新規作成用フォームがあるとちょっと見づらいので、extra=0としておきます。

PostCreateFormSet = forms.modelformset_factory(
    Post, form=PostCreateForm,
    extra=0, can_delete=True,
)

ビュー

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import render, redirect
from .forms import PostCreateFormSet
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 index(request):
    post_list = Post.objects.order_by('-date')
    page_obj = paginate_queryset(request, post_list, 3)
    formset = PostCreateFormSet(request.POST or None, queryset=page_obj.object_list)

    if request.method == 'POST' and formset.is_valid():
        formset.save()
        return redirect('app:index')

    context = {
        'formset': formset,
        'page_obj': page_obj,
    }

    return render(request, 'app/post_formset.html', context)

paginate_queryset関数は、ページング機能のための関数です。関数ビューでページング機能を付ける場合は、こんな感じの関数をよく定義します。

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

index関数は、Django、モデルフォームセットの基本的な使い方 のadd関数とほぼ同じです。関数名をindexとし、フォームセットクラスのqueryset引数に、そのページ分のquerysetを渡しつつ、Pageオブジェクトもテンプレートへ渡します。

def index(request):
    post_list = Post.objects.order_by('-date')
    page_obj = paginate_queryset(request, post_list, 3)
    formset = PostCreateFormSet(request.POST or None, queryset=page_obj.object_list)

    if request.method == 'POST' and formset.is_valid():
        formset.save()
        return redirect('app:index')

    context = {
        'formset': formset,
        'page_obj': page_obj,
    }

    return render(request, 'app/post_formset.html', context)

テンプレートファイル

post_formset.htmlを変更します。以前との違いは、ページリンク部分ができたことです。よくあるリンク部分です。

{% extends 'app/base.html' %}

{% block content %}
<form action="" method="post">
    <div class="row">
        {% for form in formset %}
            <div class="col-sm-4">
                {{ form.as_p }}
            </div>
        {% endfor %}
    </div>
    {{ formset.management_form }}
    {% csrf_token %}
    <button type="submit" class="btn btn-primary">送信</button>
</form>

<hr>

<!-- 前へ の部分 -->
{% if page_obj.has_previous %}
  <a href="?page={{ page_obj.previous_page_number }}">Prev</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 }}">Next</a>
{% endif %}

{% endblock %}

絞り込みに対応する

検索して表示データを絞り込めるようにしましょう。まずですが、post_formset.htmlやbase.html に以下のような検索欄を付けます。今回は直接書いていますが、フォームとしてちゃんと定義するのがよりベターです。

<form action="" method="GET">
    <input type="text" placeholder="検索ワード" name="keyword">
    <button type="submit" class="btn btn-primary">検索</button>
</form>

views.pyのindex関数の先頭に、この検索キーワードの取得とfilter処理を書きます。

def index(request):
    post_list = Post.objects.order_by('-date')
    search_keyword = request.GET.get('keyword')
    if search_keyword:
        post_list = post_list.filter(text__icontains=search_keyword)

    (以降は同じ)

今のところ、次ページなどのリンクを、href="?page=...と直接書いています。このように書いてしまうと、他のGETパラメータ...今回でいえば、?keyword=検索キー といったものと衝突し、うまく動作しません。この問題は、Django、ページング処理まとめurl_replace関数のようなテンプレートタグを利用すると解決します。

新規フォームと更新フォームを別々に取り出す

新規作成用フォームを素直に表示すると、以下のようになります。 新規フォームが表示される

更新用フォームのあとに新規フォームが表示されました。どのページに移動してもこのように表示され、見た目的に気になる人もいるかもしれません。

上側に更新フォームとページング下側に新規フォームと送信ボタンを配置してみます。これならばまだ、見やすいです。 別々にハイチした

post_formset.htmlを変更します。ページ部分は、page.htmlに分割しました。

{% extends 'app/base.html' %}

{% block content %}
<form action="" method="post">
    <h2>更新</h2>
    <div class="row">
        {% for form in formset.initial_forms %}
            <div class="col-sm-4">
                {{ form.as_p }}
            </div>
        {% endfor %}
    </div>

    {% include 'app/page.html' %}
    <hr>

    <h2>新規作成</h2>
    <div class="row">
        {% for form in formset.extra_forms %}
            <div class="col-sm-4">
                {{ form.as_p }}
            </div>
        {% endfor %}
    </div>

    {{ formset.management_form }}
    {% csrf_token %}
    <button type="submit" class="btn btn-primary">送信</button>

</form>
{% endblock %}

フォームセットは、initial_forms属性に更新用フォームが、extra_forms属性に新規作成用フォームが格納されていますので、それらを別々に取り出すことができます。

特定ページでだけ新規フォームを置く

最初のページか、又は最後のページにだけ新規フォームを置くと直感的でわかりやすいですね。 最初のページにだけ新規フォームを置く例です。index関数を変更します。

def index(request):
    post_list = Post.objects.order_by('-date')
    page_obj = paginate_queryset(request, post_list, 3)

    # 今が1ページ目ならば、extraに3を入れて新規作成用フォームを作る。
    if page_obj.number == 1:
        PostCreateFormSet = forms.modelformset_factory(
            Post, form=PostCreateForm,
            extra=3, can_delete=True,
        )
    # それ以外は、新規作成用フォームなし。
    else:
        PostCreateFormSet = forms.modelformset_factory(
            Post, form=PostCreateForm,
            extra=0, can_delete=True,
        )

    formset = PostCreateFormSet(request.POST or None, queryset=page_obj.object_list)

    if request.method == 'POST' and formset.is_valid():
        formset.save()
        return redirect('app:index')

    context = {
        'formset': formset,
        'page_obj': page_obj,
    }

    return render(request, 'app/post_formset.html', context)

この例では、PostCreateFormSetクラスをビューの中で動的に定義しています。このようにビューの中で定義するケースもよくあります。 最後のページにだけ新規フォームを配置したい場合は、page_obj.paginator.num_pages で最後のページ数が取得できます。

if page_obj.number == page_obj.paginator.num_pages:

Relation Posts

Djangoでフォームセットを使うシリーズ

Djangoにはフォームセットという、複数のフォームを一括で扱うための機能があります。いくつか種類があり、様々な機能があるので、それらを紹介していきます。

Python Django シリーズ・まとめ

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

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

Python Django Bulma Bootstrap4

Comment

記事にコメントする

まだコメントはありません。