.. _topics-forms-formsets: .. _formsets: フォームセット (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() 出力を見て分かる通り、空のフォームが一つだけ表示されています。 今表示されている空っぽのフォームの合計は、 ``extra`` パラメータ によってコントロールされます。 ``formset_factory`` のデフォルトの設定で、「追加のフォーム表示数 (extra)」 を 1 に設定しているからです。二つの空っぽのフォームを出す例は:: >>> ArticleFormSet = formset_factory(ArticleForm, extra=2) .. versionchanged:: 1.3 Django 1.3 で重要視されるのは、 フォームセットのインスタンスがイテレート (iterate)できないということです。フォームセットの表示をするために、 ``forms`` アトリビュート(attribute)をイテレートします。:: >>> formset = ArticleFormSet() >>> for form in formset.forms: ... print form.as_table() ``formset.forms`` をイテレートすることは、フォームが作成された順番で フォームを並べるのと同じことです。通常、フォームセットのイテレータは この順番でフォームを表示します、しかしこの順番を :meth:`__iter__()` メソッドを使って別の順番へと変更することができます。 フォームセットは、インデックスを入れこむこともできます。フォームセットは 組み込まれたフォームを返します。もし、 ``__iter__`` を上書きしたならば、 ``__getitem__`` も、フォームとのマッチングのために上書きする必要があります。 .. _formsets-initial-data: フォームセットに初期データを指定する ------------------------------------- 初期データは、フォームセットのユーザビリティに影響する大きな要素です。上に 示したように、 ``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() 上の例では、今度は 3 つのフォームが表示されました。初期データとして渡した 1 つと、 2 つの追加フォームです。初期データとして、辞書のリストを渡しているこ とにも注意してください。 .. seealso:: :ref:`Creating formsets from models with model formsets `. .. _formsets-max-num: フォームの最大表示数を制限する ------------------------------------ ``formset_factory`` に ``max_num`` パラメタを指定すると、フォームセット中に 表示されるフォームの最大数を制御できます:: >>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1) >>> formset = ArticleFormset() >>> for form in formset: ... print form.as_table() .. versionchanged:: 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 .. _understanding-the-managementform: ``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_count`` と ``initial_form_count`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``BaseFormSet`` は二つの ``ManagementForm`` に類似したメソッドを持っています、 ``total_form_count`` と ``initial_form_count`` です。 ``total_form_count`` は、フォームセットの中のフォームの合計の数を返します。 ``initial_form_count`` はフォームセットの中のフォームのうち、データが 入力されているものだけを返します、また幾つのフォームが必要かを判断するのに 使えます。おそらくこれらのメソッドを上書きする必要に迫られることはないでしょ う。まずは、実行する前に何を実行するのかを理解しておいたほうがよいでしょう。 .. versionadded:: 1.2 ``empty_form`` ~~~~~~~~~~~~~~ ``BaseFormSet`` は ``empty_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() このオプションを指定すると、各フォームにフィールドが追加されます。フィール ドの名前は ``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() ``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() ビューやテンプレートでフォームセットを使う ---------------------------------------------- ビュー内でのフォームセットの扱いは簡単で、普通の ``Form`` クラスと同じよう に使うだけです。気をつけておかねばならないのは、テンプレート内で ``management_form`` を使うという点です。ビューの例を以下に示します: .. code-block:: python 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`` テンプレートは以下のようになります: .. code-block:: html+django
{{ formset.management_form }} {% for form in formset %} {{ form }} {% endfor %}
ただし、下記のようなショートカットを使えば、フォームセット自体に管理フォー ムを扱わせられます: .. code-block:: html+django
{{ formset }}
このショートカットは、フォームセットの ``as_table`` を呼び出した時と同じ内 容を出力します。 .. _manually-rendered-can-delete-and-can-order: ``can_delete`` と ``can_order`` を手動で出力する。 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ もし、テンプレートの中で手動でフィールドを出力するなら、 ``can_delete`` パラメータは ``{{ form.DELETE }}`` で出力できます: .. code-block:: html+django
{{ formset.management_form }} {% for form in formset %} {{ form.id }} {% endfor %}
同じように、もしフォームセットが並べ替え機能を所持するなら( ``can_order=True`` )、これもまた ``{{ form.ORDER }}`` で呼び出すことができます。 複数のフォームセットを一つのビュー内で使う ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ お望みなら、複数のフォームセットを一つのビューで扱えます。フォームセットは フォームとほとんど同じく動作します。つまり、 ``prefix`` を使ってフォームセッ トのフォームフィールド名にプレフィクスをつければ、複数のフォームセットを 一つのビューに入れても問題なく動作するのです。複数のフォームセットがどのよ うに動作するか、以下の例で示しましょう: .. code-block:: python 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 リクエストの場合とそうでない場合の両方に指定して、正しくレンダさせるこ とです。