.. _topics-forms-formsets: フォームセット (formsets) ============================= :revision-up-to: 9084 (1.0) フォームセットとは、同じページで複数のフォームを扱うための抽象化レイヤで、 いわばデータグリッドのようなものです。フォームセットを説明するために、まず 以下のようなフォームを考えましょう:: >>> 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.forms: ... print form.as_table() 出力を見て分かる通り、フォームは一つだけ表示されています。これは、 ``formset_factory`` のデフォルトの設定で、「追加のフォーム表示数 (extra)」 を 1 に設定しているからです。表示数は ``extra`` パラメタで制御できます:: >>> ArticleFormSet = formset_factory(ArticleForm, extra=2) .. _Using initial data with a formset: フォームセットに初期データを指定する ------------------------------------- 初期データは、フォームセットのユーザビリティに影響する大きな要素です。上に 示したように、 ``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.forms: ... print form.as_table() 上の例では、今度は 3 つのフォームが表示されました。初期データとして渡した 1 つと、 2 つの追加フォームです。初期データとして、辞書のリストを渡しているこ とにも注意してください。 .. _Limiting the maximum number of forms: フォームの最大表示数を制限する ------------------------------------ ``formset_factory`` に ``max_num`` パラメタを指定すると、フォームセット中に 表示されるフォームの最大数を制御できます:: >>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1) >>> formset = ArticleFormset() >>> for form in formset.forms: ... print form.as_table() ``max_num`` のデフォルト値は表示数に制限がないことを示す ``0`` に設定されて います。 .. _Formset validation: フォームセットのバリデーション ------------------------------------ フォームセットのバリデーションは、普通の ``Form`` とほぼ同じです。フォーム セットにも ``is_valid`` メソッドがあり、フォームセット中の全てのフォームを 簡単に検証できます:: >>> ArticleFormSet = formset_factory(ArticleForm) >>> formset = ArticleFormSet({}) >>> formset.is_valid() True この例では、フォームセットにデータを渡さなかったので、有効なフォームを返し ています。フォームセットは賢くて、データの変更されなかったフォームを無視し てくれます。あるフォーム上の記事を変更しようとして、失敗した場合の挙動を以 下に示します:: >>> data = { ... 'form-TOTAL_FORMS': u'1', ... 'form-INITIAL_FORMS': u'1', ... 'form-0-title': u'Test', ... 'form-0-pub_date': u'', ... } >>> formset = ArticleFormSet(data) >>> formset.is_valid() False >>> formset.errors [{'pub_date': [u'This field is required.']}] このように、フォームセットはバリデーションを正しく実行して、期待通りのエラー を表示します。 .. _Understanding the ManagementForm: ``ManagementForm`` を理解する ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 上の例でフォームセットに与えた初期値には、追加のデータが入っていたことに気 付いたでしょうか。これはフォームセットで内部的に処理されているフォーム、 ``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`` データのカウントも増やさねばなりません。 .. _Custom formset validation: カスタムのフォームセットバリデーション ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``Form`` クラスと同様、フォームセットには ``clean`` メソッドがあります。こ のメソッドには、フォームセットレベルで扱う独自のバリデーションを定義します:: >>> from django.forms.formsets import BaseFormSet >>> class BaseArticleFormSet(BaseFormSet): ... def clean(self): ... raise forms.ValidationError, u'An error occured.' >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet) >>> formset = ArticleFormSet({}) >>> formset.is_valid() False >>> formset.non_form_errors() [u'An error occured.'] フォームセットの ``clean`` メソッドは、全てのフォームに対して ``Form.clean`` メソッドが呼出された後に実行されます。フォームセットに関する エラーは、フォームセットの ``non_form_errors()`` メソッドで取り出せます。 .. _Dealing with ordering and deletion of forms: フォームの並び順や削除の扱い ------------------------------ フォームセットを扱う上でよくあるユースケースは、フォームインスタンスの並び 順や削除の処理です。フォームセットはこれらの処理を実行してくれます。 ``formset_factory`` には ``can_order`` および ``can_delete`` という二つの パラメタがあり、指定するとフォームにフィールドを追加して、並び順や削除フラ グを操作する簡単な手段を提供します。 ``can_order`` ~~~~~~~~~~~~~ デフォルト値: ``False`` ``True`` にすると、フォームセットは順番を指定できるフォームを生成します:: >>> 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.forms: ... print form.as_table() このオプションを指定すると、各フォームにフィールドが追加されます。フィール ドの名前は ``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`` ``True`` にすると、フォームセットは削除を実行できるフォームを生成します:: >>> 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.forms: .... print form.as_table() ``can_order`` と同様、 ``DELETE`` という名前のついたフィールドが追加されま す。このフィールドは ``forms.BooleanField`` です。削除フラグフィールドをマー クしたデータを投入すると、削除フラグの立っているフォームに ``deleted_forms`` でアクセスできます:: >>> 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-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'}] .. _Adding additional fields to a formset: フォームセットにフィールドを追加する ------------------------------------- フォームセットには、追加のフィールドを簡単に追加できます。フォームセットの ベースクラスは ``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.forms: ... print form.as_table() .. _Using a formset in views and templates: ビューやテンプレートでフォームセットを使う ---------------------------------------------- ビュー内でのフォームセットの扱いは簡単で、普通の ``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(): # formset.cleaned_data を使った処理をここに else: formset = ArticleFormSet() return render_to_response('manage_articles.html', {'formset': formset}) ``manage_articles.html`` テンプレートは以下のようになります: .. code-block:: html+django
{{ formset.management_form }} {% for form in formset.forms %} {{ form }} {% endfor %}
ただし、下記のようなショートカットを使えば、フォームセット自体に管理フォー ムを扱わせられます: .. code-block:: html+django
{{ formset }}
このショートカットは、フォームセットの ``as_table`` を呼び出した時と同じ内 容を出力します。 複数のフォームセットを一つのビュー内で使う ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ お望みなら、複数のフォームセットを一つのビューで扱えます。フォームセットは フォームとほとんど同じく動作します。つまり、 ``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 リクエストの場合とそうでない場合の両方に指定して、正しくレンダさせるこ とです。