【Django】formsetのmanagement_formの正体を説明【Python】

当ページのリンクには広告が含まれています。
management_formの使い方

最近独学でPythonのDjangoを勉強しています。

Fomsetというformを複数扱える機能でいろいろ試していたとき、実装は正しいはずなのにis_valid()がFalseとなりうまくいかない事象が発生しました。

結論から言うと、テンプレートファイルにmanagement_formがなかったことが原因でした。

そこでmanagement_formについていろいろ調べてみましたが、おまじないだからとりあえず書いておけ、とお茶をにごした書き方が多かったです…。

なので、この記事ではmanagement_formの必要性や役割について具体的な実装例を交えて説明することにしました

エンジュニア

management_formのことがよくわからずにモヤモヤしたまま使っている方は、是非ご覧ください!

また、以下の記事ではDjangoが学べるおすすめのプログラミングスクールをDjangoを学ぶ目的別に紹介しています

Djangoの習得には幅広い知識が必要で、独学では大変な部分も多いと思いますので、気になる方はよければ覗いてみてください。

目次

management_formとは

結論から記載すると、management_formとは、formsetを使用する際に必要な管理データを自動付与してくれる機能です。

一応、公式サイトにも記載があります。
https://docs.djangoproject.com/en/4.0/topics/forms/formsets/#understanding-the-managementform-1

まずは「formsetを使用する際に必要な管理データ」について説明します。

formsetをHTMLにレンダリングする際に、form-TOTAL_FORMSと、form-INITIAL_FORMSというパラメータが必要のようです。

名前から想像できますが、form-TOTAL_FORMSはフォームの合計数で、form-INITIAL_FORMSは、initialで初期値が入っていたフォームの数を表します。

この2つのパラメータは、明示的に設定しないとそのままでは設定されないようです。

そこでmanagement_formの出番です。

テンプレートファイルにmanagement_formを記載することで、自動的に上記2つのパラメータが設定されます。

以降、具体的な実装例で説明していきましょう。

urls.pyなど今回の話と関係ない部分は省略します。

forms.py

今回の例では、以下のFormを使います。

from django import forms


class MenberForm(forms.Form):
    name = forms.CharField(max_length=100, label = '名前')
    age = forms.IntegerField(label='年齢')

名前用のnameフィールドと年齢用のageフィールドを持ったフォームクラスです。

views.py

views.pyは以下のようにViewクラスを継承したIndexViewクラスを定義します。

postメソッドでフォーム内容の検証としてform.is_valid()メソッドを呼び出しています。

is_valid()がTrueの場合は’OK’と表示しますが、Falseの場合はnon_form_error()メソッドでエラー文を表示するような構成です。

from django.shortcuts import render
from django.views.generic import View
from django import forms
from .forms import MenberForm

# formsetの定義
class IndexView(View):
    MemberFormSet = forms.formset_factory(
        form=MenberForm,
        extra=3,
        max_num=10
    )
    
    # render関数に渡すcontext
    context = {'formset': MemberFormSet(), 'message': ''}

    # GETメソッド
    def get(self, request, *args, **kwargs):
        return render(request, 'app/index.html', self.context)

    # POSTメソッド
    def post(self, request, *args, **kwargs):
        formset = self.MemberFormSet(request.POST)
        
        # formsetのバリデーション
        if formset.is_valid():
            # is_validがTrueの場合はOKと表示
            self.context['message'] = 'OK'
        else:
            # is_validがFalseの場合はエラー文を表示
            self.context['message'] = formset.non_form_errors()

        self.context['formset'] = formset
        return render(request, 'app/index.html', self.context)

テンプレートHTML

index.htmlは以下のように、ビューから渡されたformsetをforで1つずつ取り出して、formを並べています。

formの取り出し後、{{ formset.management_form }}という部分が今回の肝となるmanagement_formです。

あとで、これがない場合にどうなるのか見てみましょう。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Formsetのテスト</title>
</head>
<body>
    <form method='POST'>
        {% csrf_token %}
        {% for form in formset %}
          {{ form }}
          <br>
        {% endfor %}
        {{ formset.management_form }}
        <button type="submit">
          送信する
        </button>
    </form>
    <h3>{{ message }}</h3>
</body>
</html>

ブラウザ確認(UI)

では、サーバーを起動して動きを見てみましょう。

フォームに値を打ち込んで送信ボタンを押すと、無事にOKと表示されました!

management_formあり

management_formがないとどうなるのか?

では、本題のmanagement_formがないとどうなるのか?について確認します。

index.htmlの{{ formset.management_form }}を削除、またはコメントアウトして先ほどのようにフォームを送信してみます。

すると、以下のような表示になりました。

management_formなし

OKという表示ではなく、以下のエラー文が出ているのでis_valid()がFalseになっていることがわかります。

ManagementForm data is missing or has been tampered with. Missing fields: form-TOTAL_FORMS, form-INITIAL_FORMS. You may need to file a bug report if the issue persists.

エラー文の内容は「form-TOTAL_FORMSとform-INITIAL_FORMSのフィールドがないよ」ということが書かれているようです。

実際に、management_formがある場合とない場合でrequest.POSTの内容を確認したところ、以下のような違いがありました。

・management_formあり

{
  'csrfmiddlewaretoken': ['トークン文字列'],
  'form-0-name': ['taro'], 'form-0-age': ['12'],
  'form-1-name': ['hanako'], 'form-1-age': ['24'],
  'form-2-name': ['ojichan'], 'form-2-age': ['80'],
  'form-TOTAL_FORMS': ['3'], 'form-INITIAL_FORMS': ['0'],
  'form-MIN_NUM_FORMS': ['0'], 'form-MAX_NUM_FORMS': ['10']
}

・management_formなし

{
  'csrfmiddlewaretoken': ['トークン文字列'],
  'form-0-name': ['taro'], 'form-0-age': ['12'],
  'form-1-name': ['hanako'], 'form-1-age': ['24'],
  'form-2-name': ['ojichan'], 'form-2-age': ['80']
}

このように、management_formsがない場合は、たしかにform-TOTAL_FORMSとform-INITIAL_FORMSのフィールドがなくなっていることがわかります。
(そのほか、form-MIN_NUM_FORMSとform-MAX_NUM_FORMSというフィールドもなくなっています)

つまり、managent_formsはこれらのフィールドを付与するための制御を行なっていることがわかりました。

それぞれの隠れフィールドの意味は以下のような感じかと思います。

form-TOTAL_FORMS送信したフォームの合計数
form-INITIAL_FORMS初期値が設定されたフォームの合計数
form-MIN_NUM_FORMSフォームの最小数
form-MAX_NUM_FORMSフォームの最大数(max_numで指定した数字)

まとめ

今回は、おまじないとして書かれているmanagement_formについて実験を通して意味を理解しました。

management_formがないと必要なフィールドが設定されないため、フォームセットを扱う際はテンプレートHTMLに必ず記載する必要があります。

views.pyで自前で設定することもできますが、特にメリットもないので素直にmanagement_formを使いましょう。

Djangoはまだまだたくさんの機能が盛り沢山です。

このサイトでは、他にもさまざまな解説記事を載せているので、ぜひ参考にしてください!

また、独学だけでなく、人から教えてもらうというのも大切です。

以下の記事ではDjangoが学べるプログラミングスクールを、目的別におすすめを紹介していますので、こちらもぜひご検討ください。

おまけ:management_formを使わずにviews.pyでなんとかするやり方

一応、views.pyで無理矢理設定する例を載せておきます。

request.POSTに、form-TOTAL_FORMSとform-INITIAL_FORMSのフィールドを追加することでバリエーションに成功するようになります。

from django.shortcuts import render
from django.views.generic import View
from django import forms
from .forms import MenberForm
from django.http import QueryDict

class IndexView(View):
    # formsetの定義
    MemberFormSet = forms.formset_factory(
        form=MenberForm,
        extra=3,
        max_num=10
    )

    # render関数に渡すcontext
    context = {'formset': MemberFormSet(), 'message': ''}

    # GETメソッド
    def get(self, request, *args, **kwargs):
        return render(request, 'app/index.html', self.context)

    # POSTメソッド
    def post(self, request, *args, **kwargs):
        # management_form利用時と同じようなQueryDictを自前で作成する
        my_querydict = QueryDict(mutable=True)
        my_querydict.update(request.POST)
        my_querydict.update({'form-TOTAL_FORMS': 3, 'form-INITIAL_FORMS': 0})

        formset = self.MemberFormSet(my_querydict)

        # formsetのバリデーション
        if formset.is_valid():
            self.context['message'] = 'OK'
        else:
            self.context['message'] = formset.non_form_errors()

        self.context['formset'] = formset
        return render(request, 'app/index.html', self.context)
よかったらシェアしてね!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次