フォームセット (formsets)

revision-up-to:17812 (1.4)

フォームセットとは、同じページで複数のフォームを扱うための抽象化レイヤで、 いわばデータグリッドのようなものです。フォームセットを説明するために、まず 以下のようなフォームを考えましょう:

>>> from django import forms
>>> class ArticleForm(forms.Form):
...     title = forms.CharField()
...     pub_date = forms.DateField()

このフォームを使って、ユーザが一度に複数の記事を作成できるようにしたい場合 があったとします。そのために、 ArticleForm からフォームセットを生成しま す:

>>> from django.forms.formsets import formset_factory
>>> ArticleFormSet = formset_factory(ArticleForm)

ArticleFormSet という名前のフォームセットクラスができました。このフォー ムセットには、フォームセットに入っているフォームを一つ一つ取り出して、それ ぞれを普通のフォームとして表示する機能があります:

>>> formset = ArticleFormSet()
>>> for form in formset:
...     print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>

出力を見て分かる通り、空のフォームが一つだけ表示されています。 今表示されている空っぽのフォームの合計は、 extra パラメータ によってコントロールされます。 formset_factory のデフォルトの設定で、「追加のフォーム表示数 (extra)」 を 1 に設定しているからです。二つの空っぽのフォームを出す例は:

>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
Django 1.3 で変更されました: リリースノートを参照してください

Django 1.3 で重要視されるのは、 フォームセットのインスタンスがイテレート (iterate)できないということです。フォームセットの表示をするために、 forms アトリビュート(attribute)をイテレートします。:

>>> formset = ArticleFormSet()
>>> for form in formset.forms:
...    print form.as_table()

formset.forms をイテレートすることは、フォームが作成された順番で フォームを並べるのと同じことです。通常、フォームセットのイテレータは この順番でフォームを表示します、しかしこの順番を __iter__() メソッドを使って別の順番へと変更することができます。

フォームセットは、インデックスを入れこむこともできます。フォームセットは 組み込まれたフォームを返します。もし、 __iter__ を上書きしたならば、 __getitem__ も、フォームとのマッチングのために上書きする必要があります。

フォームセットに初期データを指定する

初期データは、フォームセットのユーザビリティに影響する大きな要素です。上に 示したように、 formset_factory には追加のフォーム表示数を指定できます。 この「追加」とは、初期データを渡したときに表示されるフォームの中で、追加で 表示されている空のフォーム数という意味です。以下の例をよく見てください:

>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
>>> formset = ArticleFormSet(initial=[
...     {'title': u'Django is now open source',
...      'pub_date': datetime.date.today(),}
... ])

>>> for form in formset:
...     print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Django is now open source" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-12" id="id_form-0-pub_date" /></td></tr>
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" id="id_form-1-title" /></td></tr>
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" id="id_form-1-pub_date" /></td></tr>
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>

上の例では、今度は 3 つのフォームが表示されました。初期データとして渡した 1 つと、 2 つの追加フォームです。初期データとして、辞書のリストを渡しているこ とにも注意してください。

フォームの最大表示数を制限する

formset_factorymax_num パラメタを指定すると、フォームセット中に 表示されるフォームの最大数を制御できます:

>>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1)
>>> formset = ArticleFormset()
>>> for form in formset:
...     print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
Django 1.2 で変更されました: リリースノートを参照してください

もし、 max_num の値が存在するオブジェクトの合計より大きい場合、 extra 次第で、空のフォームがフォームセットに加えられます。 フォームの合計の長さは max_num を超えることはできません。

max_num の値が None (通常です)であった場合、表示されるフォームの数には 制限がありません。 max_num の値が 0 から None になったことを 覚えておいてください。 version 1.2 から 0 は有効な値となりました。

フォームセットのバリデーション

フォームセットのバリデーションは、普通の Form とほぼ同じです。フォーム セットにも is_valid メソッドがあり、フォームセット中の全てのフォームを 簡単に検証できます:

>>> ArticleFormSet = formset_factory(ArticleForm)
>>> data = {
...     'form-TOTAL_FORMS': u'1',
...     'form-INITIAL_FORMS': u'0',
...     'form-MAX_NUM_FORMS': u'',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
True

この例では、フォームセットにデータを渡さなかったので、有効なフォームを返し ています。フォームセットは賢くて、データの変更されなかったフォームを無視し てくれます。あるフォーム上の記事を変更しようとして、失敗した場合の挙動を以 下に示します:

>>> data = {
...     'form-TOTAL_FORMS': u'2',
...     'form-INITIAL_FORMS': u'0',
...     'form-MAX_NUM_FORMS': u'',
...     'form-0-title': u'Test',
...     'form-0-pub_date': u'1904-06-16',
...     'form-1-title': u'Test',
...     'form-1-pub_date': u'', # <-- this date is missing but required
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {'pub_date': [u'This field is required.']}]

確認できるように、 formset.errors はリストで、 そのエントリーは、フォームセットの中のフォームのものです。 バリデーションは、二つのフォームそれぞれに働いて、 リストの二つ目のアイテムにエラーメッセージが表示されています。

また、初めのデータと入力されたデータが異なっているかどうかも チェックできます。(すなわち、フォームは何のデータもなしに送信されない) ということです。

>>> data = {
...     'form-TOTAL_FORMS': u'1',
...     'form-INITIAL_FORMS': u'0',
...     'form-MAX_NUM_FORMS': u'',
...     'form-0-title': u'',
...     'form-0-pub_date': u'',
... }
>>> formset = ArticleFormSet(data)
>>> formset.has_changed()
False

ManagementForm を理解する

上の例でフォームセットに与えた初期値には、追加のデータ( form-TOTAL_FORMS , form-INITIAL_FORMS , form-MAX_NUM_FORMS )が入っていたことに気 付いたでしょうか。これはフォームセットで内部的に処理されているフォーム、 ManagementForm で扱うためのデータです。追加のデータ抜きでフォームセット を使おうとすると、例外が送出されます:

>>> data = {
...     'form-0-title': u'Test',
...     'form-0-pub_date': u'',
... }
>>> formset = ArticleFormSet(data)
Traceback (most recent call last):
...
django.forms.util.ValidationError: [u'ManagementForm data is missing or has been tampered with']

ManagementForm のデータは、表示するフォームインスタンスの数を追跡するた めに使われます。 JavaScript でフォームを動的に追加する場合、 ManagementForm データのカウントも増やさねばなりません。

マネジメントフォームはフォームセット自身のアトリビュートとして使用可能です。 テンプレートにフォームセットをレンダリングする際、 {{ my_formset.management_form }} をレンダリングすることでマネジメント データを含むことができます。(フォームセットのところは適切な名前に変えて 使ってください)

total_form_countinitial_form_count

BaseFormSet は二つの ManagementForm に類似したメソッドを持っています、 total_form_countinitial_form_count です。

total_form_count は、フォームセットの中のフォームの合計の数を返します。 initial_form_count はフォームセットの中のフォームのうち、データが 入力されているものだけを返します、また幾つのフォームが必要かを判断するのに 使えます。おそらくこれらのメソッドを上書きする必要に迫られることはないでしょ う。まずは、実行する前に何を実行するのかを理解しておいたほうがよいでしょう。

Django 1.2 で新たに登場しました: リリースノートを参照してください

empty_form

BaseFormSetempty_form という追加のアトリビュートを提供します。 これは、 JavaScript を使った動的なフォーム生成を簡単にするために __prefix__ のプレフィクスをフォームインスタンスとともに返します。

カスタムのフォームセットバリデーション

Form クラスと同様、フォームセットには clean メソッドがあります。こ のメソッドには、フォームセットレベルで扱う独自のバリデーションを定義します:

>>> from django.forms.formsets import BaseFormSet

>>> class BaseArticleFormSet(BaseFormSet):
...     def clean(self):
...         """Checks that no two articles have the same title."""
...         if any(self.errors):
...             # Don't bother validating the formset unless each form is valid on its own
...             return
...         titles = []
...         for i in range(0, self.total_form_count()):
...             form = self.forms[i]
...             title = form.cleaned_data['title']
...             if title in titles:
...                 raise forms.ValidationError("Articles in a set musthave distinct titles.")
...             titles.append(title)

>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> data = {
...     'form-TOTAL_FORMS': u'2',
...     'form-INITIAL_FORMS': u'0',
...     'form-MAX_NUM_FORMS': u'',
...     'form-0-title': u'Test',
...     'form-0-pub_date': u'1904-06-16',
...     'form-1-title': u'Test',
...     'form-1-pub_date': u'1912-06-23',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
[u'Articles in a set must have distinct titles.']

フォームセットの clean メソッドは、全てのフォームに対して Form.clean メソッドが呼出された後に実行されます。フォームセットに関する エラーは、フォームセットの non_form_errors() メソッドで取り出せます。

フォームの並び順や削除の扱い

フォームセットを扱う上でよくあるユースケースは、フォームインスタンスの並び 順や削除の処理です。フォームセットはこれらの処理を実行してくれます。 formset_factory には can_order および can_delete という二つの パラメタがあり、指定するとフォームにフィールドを追加して、並び順や削除フラ グを操作する簡単な手段を提供します。

can_order

デフォルト値: False

並べ替え機能とともに、フォームセットを作ってみましょう:

>>> ArticleFormSet = formset_factory(ArticleForm, can_order=True)
>>> formset = ArticleFormSet(initial=[
...     {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
...     {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ])
>>> for form in formset:
...     print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date" /></td></tr>
<tr><th><label for="id_form-0-ORDER">Order:</label></th><td><input type="text" name="form-0-ORDER" value="1" id="id_form-0-ORDER" /></td></tr>
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title" /></td></tr>
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date" /></td></tr>
<tr><th><label for="id_form-1-ORDER">Order:</label></th><td><input type="text" name="form-1-ORDER" value="2" id="id_form-1-ORDER" /></td></tr>
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
<tr><th><label for="id_form-2-ORDER">Order:</label></th><td><input type="text" name="form-2-ORDER" id="id_form-2-ORDER" /></td></tr>

このオプションを指定すると、各フォームにフィールドが追加されます。フィール ドの名前は ORDER で、型は forms.IntegerField です。初期データを使っ てフォームを生成した場合、 ORDER フィールドには自動的に数値が割り当てら れます。ユーザがこの値を変更するとどうなるか見てみましょう:

>>> data = {
...     'form-TOTAL_FORMS': u'3',
...     'form-INITIAL_FORMS': u'2',
...     'form-0-title': u'Article #1',
...     'form-0-pub_date': u'2008-05-10',
...     'form-0-ORDER': u'2',
...     'form-1-title': u'Article #2',
...     'form-1-pub_date': u'2008-05-11',
...     'form-1-ORDER': u'1',
...     'form-2-title': u'Article #3',
...     'form-2-pub_date': u'2008-05-01',
...     'form-2-ORDER': u'0',
... }

>>> formset = ArticleFormSet(data, initial=[
...     {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
...     {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ])
>>> formset.is_valid()
True
>>> for form in formset.ordered_forms:
...     print form.cleaned_data
{'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0, 'title': u'Article #3'}
{'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1, 'title': u'Article #2'}
{'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2, 'title': u'Article #1'}

can_delete

デフォルト値: False

削除機能とともに、フォームセットを作ってみましょう:

>>> ArticleFormSet = formset_factory(ArticleForm, can_delete=True)
>>> formset = ArticleFormSet(initial=[
...     {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
...     {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ])
>>> for form in formset:
....    print form.as_table()
<input type="hidden" name="form-TOTAL_FORMS" value="3" id="id_form-TOTAL_FORMS" />
<input type="hidden" name="form-INITIAL_FORMS" value="2" id="id_form-INITIAL_FORMS" />
<input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS" />
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date" /></td></tr>
<tr><th><label for="id_form-0-DELETE">Delete:</label></th><td><input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE" /></td></tr>
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title" /></td></tr>
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date" /></td></tr>
<tr><th><label for="id_form-1-DELETE">Delete:</label></th><td><input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE" /></td></tr>
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
<tr><th><label for="id_form-2-DELETE">Delete:</label></th><td><input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE" /></td></tr>

can_order と同様、 DELETE という名前のついたフィールドが追加されま す。このフィールドは forms.BooleanField です。削除フラグフィールドをマー クしたデータを投入すると、削除フラグの立っているフォームに deleted_forms でアクセスできます:

>>> data = {
...     'form-TOTAL_FORMS': u'3',
...     'form-INITIAL_FORMS': u'2',
...     'form-MAX_NUM_FORMS': u'',
...     'form-0-title': u'Article #1',
...     'form-0-pub_date': u'2008-05-10',
...     'form-0-DELETE': u'on',
...     'form-1-title': u'Article #2',
...     'form-1-pub_date': u'2008-05-11',
...     'form-1-DELETE': u'',
...     'form-2-title': u'',
...     'form-2-pub_date': u'',
...     'form-2-DELETE': u'',
... }

>>> formset = ArticleFormSet(data, initial=[
...     {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
...     {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ])
>>> [form.cleaned_data for form in formset.deleted_forms]
[{'DELETE': True, 'pub_date': datetime.date(2008, 5, 10), 'title': u'Article #1'}]

フォームセットにフィールドを追加する

フォームセットには、追加のフィールドを簡単に追加できます。フォームセットの ベースクラスは add_fields メソッドを提供しています。このメソッドをオー バライドして、独自にフィールドを追加したり、デフォルトのフィールドや属性を 再定義したり、フィールドを削除したりできます:

>>> class BaseArticleFormSet(BaseFormSet):
...     def add_fields(self, form, index):
...         super(BaseArticleFormSet, self).add_fields(form, index)
...         form.fields["my_field"] = forms.CharField()

>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> formset = ArticleFormSet()
>>> for form in formset:
...     print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
<tr><th><label for="id_form-0-my_field">My field:</label></th><td><input type="text" name="form-0-my_field" id="id_form-0-my_field" /></td></tr>

ビューやテンプレートでフォームセットを使う

ビュー内でのフォームセットの扱いは簡単で、普通の Form クラスと同じよう に使うだけです。気をつけておかねばならないのは、テンプレート内で management_form を使うという点です。ビューの例を以下に示します:

def manage_articles(request):
    ArticleFormSet = formset_factory(ArticleForm)
    if request.method == 'POST':
        formset = ArticleFormSet(request.POST, request.FILES)
        if formset.is_valid():
            # do something with the formset.cleaned_data
    else:
        formset = ArticleFormSet()
    return render_to_response('manage_articles.html', {'formset': formset})

manage_articles.html テンプレートは以下のようになります:

<form method="post" action="">
    {{ formset.management_form }}
    <table>
        {% for form in formset %}
        {{ form }}
        {% endfor %}
    </table>
</form>

ただし、下記のようなショートカットを使えば、フォームセット自体に管理フォー ムを扱わせられます:

<form method="post" action="">
    <table>
        {{ formset }}
    </table>
</form>

このショートカットは、フォームセットの as_table を呼び出した時と同じ内 容を出力します。

can_deletecan_order を手動で出力する。

もし、テンプレートの中で手動でフィールドを出力するなら、 can_delete パラメータは {{ form.DELETE }} で出力できます:

<form method="post" action="">
    {{ formset.management_form }}
    {% for form in formset %}
        {{ form.id }}
        <ul>
            <li>{{ form.title }}</li>
            {% if formset.can_delete %}
                <li>{{ form.DELETE }}</li>
            {% endif %}
        </ul>
    {% endfor %}
</form>

同じように、もしフォームセットが並べ替え機能を所持するなら( can_order=True )、これもまた {{ form.ORDER }} で呼び出すことができます。

複数のフォームセットを一つのビュー内で使う

お望みなら、複数のフォームセットを一つのビューで扱えます。フォームセットは フォームとほとんど同じく動作します。つまり、 prefix を使ってフォームセッ トのフォームフィールド名にプレフィクスをつければ、複数のフォームセットを 一つのビューに入れても問題なく動作するのです。複数のフォームセットがどのよ うに動作するか、以下の例で示しましょう:

def manage_articles(request):
    ArticleFormSet = formset_factory(ArticleForm)
    BookFormSet = formset_factory(BookForm)
    if request.method == 'POST':
        article_formset = ArticleFormSet(request.POST, request.FILES, prefix='articles')
        book_formset = BookFormSet(request.POST, request.FILES, prefix='books')
        if article_formset.is_valid() and book_formset.is_valid():
            # do something with the cleaned_data on the formsets.
    else:
        article_formset = ArticleFormSet(prefix='articles')
        book_formset = BookFormSet(prefix='books')
    return render_to_response('manage_articles.html', {
        'article_formset': article_formset,
        'book_formset': book_formset,
    })

これで、フォームセットは通常通りレンダできます。重要なのは、 prefix を POST リクエストの場合とそうでない場合の両方に指定して、正しくレンダさせるこ とです。