DjangoのFormSetで各フォームをまたがったバリデーションを行う

どうやるのかなと思って調べたときのメモ


例えば複数のブックマークを一括で入力できる画面を想定して、こんなFormとFormSetがある場合

from django.forms.formsets import formset_factory
from django import forms

class BookmarkForm(forms.Form):
    title = forms.CharField(max_length=100)
    url = forms.CharField(max_length=100)


BookmarkFormSet = formset_factory(BookmarkForm, extra=1, max_num=100)

同じurlのブックマークが入力された場合にエラーにする、というのは各FormのバリデーションではできないのでFormSetの仕事になる。

FormSetにバリデーションの処理をさせるにはBaseFormSetを継承したクラスにcleanメソッドを実装し、formset_factoryの引数として渡す。

from django.forms.formsets import formset_factory
from django import forms

class BookmarkForm(forms.Form):
    title = forms.CharField(max_length=100)
    url = forms.CharField(max_length=100)

class BaseBookmarkFormSet(BaseFormSet):
    def clean(self):
        url_list = [form['url'].value() for form in self.forms]

        if len(url_list) > len(set(url_list)):
            raise forms.ValidationError, u'duplicate url'

BookmarkFormSet = formset_factory(BookmarkForm, formset=BaseBookmarkFormSet, extra=1, max_num=100)


Formsetのcleanメソッドは、各Formのバリデーションが全て実行されたあとに実行される。
エラーはnon_form_errorsメソッドで取得できる。

>>> data = {'form-TOTAL_FORMS': u'2',
           'form-INITIAL_FORMS': u'2',
           'form-0-title': '',
           'form-0-url': 'http://www.yahoo.co.jp',
           'form-1-title': u'Yahoo! Japan',
           'form-1-url': 'http://www.yahoo.co.jp',
           }
   
>>> fs = BookmarkFormSet(data)
>>> fs.is_valid()
False

>>> fs.errors
[{'title': [u'This field is required.']}, {}]

>>> fs.non_form_errors()
[u'duplicate url']