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


【解説】Django2.2でModelFormSetの使い方

Django2 web開発 Django サーバー開発 jinja html 複数フォーム ModelFormSet

投稿日:2020年10月10日

このエントリーをはてなブックマークに追加
ブログの様なWebサイトでは、同時に複数のレコードを編集したい場面がよくあります。DjangoのModelFormSetはそのような機能を簡単に作ることのできる機能です。この記事ではModelFormSetの使い方の説明とその解説をしています。

はじめに

この記事について

DjangoのModelFormは便利なのですが、そのままでは同時に複数のModelを作成・編集することはできません。Djangoはそれを解決するModelFormSetというものを用意しています。

この記事ではModelFormSetの使い方について詳しく解説しています。

環境

この記事は以下の環境で試されました。

  • Python 3.6.7
  • Django==2.2.1

使い方

前提

Djangoのアプリケーションで以下のようなModelFormがあるとします。

blog/models.py
from django.db import models

class Article(models.Model):
    """記事を管理するModel.

    Args:
        title(str) : 記事のタイトル。ユーザーに表示される。
        created(date) : 記事の作成日時。自動管理。
        edited(date) : 記事の最終更新日時。自動管理。
        body(str) : 記事の本文。
    Returns:
        [type]: [description]
    """
    title = models.CharField("title", max_length=50,null=False,blank=False)
    created = models.DateField("created", auto_now_add=True)
    edited = models.DateField("edited", auto_now=True)
    body = models.TextField("body",null=False, blank=False)

    class Meta:
        verbose_name = "Article"
        verbose_name_plural = "Articles"

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("Article_detail", kwargs={"pk": self.pk})
blog/forms.py
from .models import Article
from django import forms

class ArticleForm(forms.ModelForm):
    """記事モデルを作成・編集するform
    """
    class Meta:
        model = Article
        fields = ("title","body")

複数の空のformを作成

ModelFormSetdjango.forms.modelformset_factory()という関数を使って作成します。ただしこの関数はModelFormSetのインスタンスを返すわけではなく、ModelFormSetのクラスを返すファクトリーメソッドであることに注意してください。

実際に使ってみましょう。

blog/views.py
from django.shortcuts import render, redirect
from django.urls import reverse
from django.forms import formset_factory, modelformset_factory
from .forms import ArticleForm
from .models import Article

def formset_article(request):
    """
    Getリクエストに対しては単純なformsetを作成する
    Postリクエストに対してはformを編集する
    """
    ArticleFormSet = modelformset_factory(Article, form=ArticleForm, extra=3)
    formset = ArticleFormSet(request.POST or None, queryset=Article.objects.none())
    if request.method == "POST" and formset.is_valid():
        formset.save()
        return redirect("blog:list_article")
    else:
        context = {
            "formset": formset
        }
        return render(request, template_name="article_formset.html", context=context)

def list_article(request):
    """Article一覧のリスト
    """
    article_list = Article.objects.all()
    context = {
        "article_list": article_list,
        "formset_article_url": reverse("blog:formset_article")
    }

    return render(request, context=context, template_name="article_list.html")

注目するのはformset_article関数です。modelformset_factory()関数の引数はそれぞれ以下の様になっています。

  • 第1引数:対象Modelクラス
  • form引数:formsetとして作成したいform
  • extra:formsetで新規作成するためのformの数

この関数で作成したArticleFormSet()でformsetを作成しています。このクラスに与えているquerysetと言う引数に注目してみてください。この引数に渡したQuerySetに合わせて、通常の空欄のformに加えてそのQuerySetに含まれるレコードの変更formをユーザーに表示できます。今回はわかりやすさのため、とりあえず変更formは表示しない様にArticle.object.none()、つまり空のQuerySetを渡しています。

▲modelformset_factory()のイメージ

生成したModelFormSetに対応するtemplateは以下の様になります。

blog/article_formset.html
<!DOCTYPE html>
<html>
  <head> </head>
  <body>
    <h2>記事フォーム</h2>
    <form action="" method="POST">
      {% csrf_token %} {{formset}}
      <button>追加</button>
    </form>
  </body>
</html>

すると以下のようなフォームが作成されます。

▲ブラウザに表示されるフォーム

複数の編集フォームを作成

既存のレコードの編集フォームはmodelformset_factory()で生成したFormSetクラスに渡すquerysetに編集したいQuerySetを渡すことで作成できます。

例えば先程のformsetに全てのArticleレコードの編集フォームも追加するコードは以下のようになります。

blog/views.py
[...]

def formset_article(request):
    """
    Getリクエストに対しては単純なformsetを作成する
    Postリクエストに対してはformを編集する
    """
    ArticleFormSet = modelformset_factory(
        Article, 
        form=ArticleForm, 
        extra=3)
    formset = ArticleFormSet(
        request.POST or None, 
        queryset=Article.objects.all())   # 全てのArticleも編集
    if request.method == "POST" and formset.is_valid():
        formset.save()
        return redirect("blog:list_article")
    else:
        context = {
            "formset": formset
        }
        return render(request, template_name="article_formset.html", context=context)


[...]

解説

templateで作成されるform要素について

DjangoTemplateに{{ formset }}を渡した際に生成されるhtmlは少し特徴があります。

通常のFormやModelFormの場合、生成されるformのinputはname属性としてfield名が指定されます。

しかし、その指定方法だと複数のFormが生成された際、同じname属性のinputが複数できてしまうため、Postデータを受け取るサーバー側はどのデータがどのinputの値であるかわからなくなってしまいます。

その解決策としてModelFormSetで生成されるinputはname属性がform-{number}-{field}という値になっています。numberは何番目のinput要素であるかという値です。

ManagementFormについて

FormSetは通常のformに加えてManagementFormも追加で作成されます。ManagementFormにはformsetの内容がhidden要素で格納されています。

このManagementFormはそのまま{{ formset }}で渡した場合には自動的に追加されます。

生成されたmanagementformの例
<input type="hidden" name="form-TOTAL_FORMS" value="3" id="id_form-TOTAL_FORMS">
<input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS">
<input type="hidden" name="form-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS">
<input type="hidden" name="form-MAX_NUM_FORMS" value="1000" id="id_form-MAX_NUM_FORMS">

何らかの理由でformをカスタマイズしたい場合などにはManagementFormを生成する必要があります。ManagementFormはformsetのmanagement_form()で生成できます。

{{ formset }}タグを手動で書き換える場合には以下の様になります。

blog/article_formset.html
<!DOCTYPE html>
<html>

<head> </head>

<body>
    <h2>記事フォーム</h2>
    <form action="" method="POST">
        {% csrf_token %}
        {{formset.management_form}}
        {% for form in formset %}
        {% for field in form %}
        {{field}}
        {% endfor %}
        {% endfor %}
        <button>追加</button>
    </form>
</body>

</html>

発展

Javascriptでフロントでformを複製した場合

ブログなどのフォームをフロント側でjavascriptで追加、削除などをする場合などにはManagementFormなどの関係で少し工夫が必要になります。

今回は簡単なサンプルの例として、ModelFormをフロント側のjavascriptで受け取り、受け取ったフォームのHTMLから3つのフォームを生成する場合のコードを載せておきます。

blog/article_form_mutable.html
<!DOCTYPE html>
  <html>
    <body>
      <h2>記事フォーム</h2>
      <form action="" method="POST">
        {% csrf_token %}
        <div id="box-form"></div>
        <input type="submit"></input>
      </form>
      <script>
        var formBox = document.querySelector("#box-form");
        var formListStr = "";
        for (var i = 0; i < 3; i++) {
          formListStr += `{{ form }}`;
        }
        formBox.innerHTML = formListStr;
      </script>
    </body>
  </html>
blog/views.py
from django.shortcuts import render, redirect
from django.urls import reverse
from django.forms import modelformset_factory
from .forms import ArticleForm
from .models import Article

def form_article_mutable(request):
    """

    Args:
        request ([type]): [description]
    """
    if request.method == "GET":
        form = ArticleForm()
        context = {
            "form": form
        }
        return render(request, context=context, template_name="article_form.html")
    elif request.method == "POST":
        post_data = request.POST
        # データを取得する
        post_data_title_list = post_data.getlist("title")
        post_data_body_list = post_data.getlist("body")
        data_len = len(post_data_body_list)
        # マネジメントフォームのデータを作成
        insert_data = {
            'form-TOTAL_FORMS': str(data_len),
            'form-INITIAL_FORMS': '0',
            'form-MAX_NUM_FORMS': '',
        }
        # フォームのデータを取得する
        for i, post_data_title in enumerate(post_data_title_list):
            insert_data[f"form-{i}-title"] = post_data_title
            insert_data[f"form-{i}-body"] = post_data_body_list[i]
        ArticleFormSet = modelformset_factory(Article, form=ArticleForm, extra=1)
        formset = ArticleFormSet(insert_data)
        if formset.is_valid():
            formset.save()
            return redirect("blog:list_article")

[...]

上のコードはPOSTでデータを受け取った際にはFormSetでまとめてデータをバリデーションして保存します。フロント側ではjavascriptで複製したフォームのipnut要素のname属性は同じ値になります。

この時、サーバー側でそれらの値を受け取るにはrequest.POST.get_list()関数を使います。

FormSetに渡すデータはマネジメントフォームが必要になるためそれの作成もしています。また、POSTされたデータについてもキーに順番を表す値を入れています。

(正直、ここまでするのであればFormをforループで回してそれをtransactionで囲んだほうが良い気もする…)

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


関連記事

記事へのコメント