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

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

Python - Django
2018年11月23日21:45に更新(約20日前)
2018年10月17日22:32に作成(約57日前)

旧ブログ移行記事です。

概要

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

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

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

Django、モデルフォームセットを使うのモデルやフォームを元に作成していきます。

forms.py

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

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

views.py

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

以前の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:
Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする