ModelFormのような内部のMetaを見るFormを自分で作る

ModelFormのように、内部のMetaを見るクラスを自分で作って見たいと思った。

概要

djangoでは、以下のようにMetaを使ってmetaclassに情報を渡して、Formクラスを作れる。

class ArticleForm(ModelForm):
    class Meta:
        model = Article

また、以下のようにfieldを直接書けるのもdjangoが用意しているmetaclassの機能*1

class ContactForm(forms.Form):
    topic = forms.ChoiceField(choices=TOPIC_CHOICES)
    message = forms.CharField(widget=forms.Textarea())
    sender = forms.EmailField(required=False)

同じようにMetaをみるクラスを作ってみたいと思った。

今回作る機能

フォームの以下の機能を分けてみる。

  • 人間が見やすいようなhtmlのレイアウトをする機能
  • 入力(POST)されたデータを検証する機能

それぞれ「Layoutフォーム」、「Validateフォーム」と呼ぶことにする。
この時のValidateフォームを作ってみることにする。

注意

入力値の検証に失敗した時、再入力を促したい。
なので、ValidateフォームからLayoutフォームへ変換できる必要がある。

使い方

以下のようにして使う。

formの定義

class LayoutForm(forms.Form):
    ## どういう風に表示するかなどたくさん書く。

    item = forms.ChoiceField(choices=[(i, i) for i in xrange(3)])
    date = forms.DateTimeField(required=False)


class ValidateForm(ValidateForm):
	## 値の検証方法をたくさん書く

    item = forms.IntegerField()
    date = forms.DateTimeField()

    class Meta(object):
        layout_form = LayoutForm #form.layout_form()が呼べるようになる.

定義しようとしているFormクラスが大きすぎない限り煩雑になるだけであまり意味がないもだけれど。

viewでの利用

#...
   form = ValidateForm(dict(item=3), prefix="foo")
   if not form.is_valid():
	  lform = form.layout_form() #formで生成されたerror.listを持っている
#...
ValidateFormが行っている作業
  • form.layout_form()の生成
  • layoutフォームとValidateフォームのフィールドの差分を検証
    • Validateフォームで定義された全てのフィールドをLayoutフォームが持っていないとエラー

作り方

  1. Metaに含まれた情報を格納するValidateFormOptionsというクラスを作る
  2. ValidateFormMetaclassというmetaclassを作る
  3. __metaclass__に定義したmetaclassを渡したクラスを作る
import django.forms.forms as forms

class ValidateFormOptions(object):
    def __init__(self, options=None):
        self.layout_form = getattr(options, "layout_form", None)
        self.translate = getattr(options, "translate", None)

def layout_form_function_fuctory(formclass):
    def layout_form(self):
        form = formclass(auto_id = self.auto_id, 
                          prefix = self.prefix, 
                          error_class = self.error_class, 
                          label_suffix = self.label_suffix, 
                          empty_permitted = self.empty_permitted)
        form._changed_data = self._changed_data
        form.data = self.data
        form._errors = self._errors
        return form
    return layout_form
        
class ValidateFormMetaclass(forms.DeclarativeFieldsMetaclass):
    def __new__(cls, name, bases, attrs):
        new_class = super(ValidateFormMetaclass, cls).__new__(cls, name, bases, attrs)
        opts = new_class._meta = ValidateFormOptions(getattr(new_class, "Meta", None))

        if opts.layout_form:
            ## validate
            layout_form_fields = opts.layout_form.base_fields
            for k in new_class.base_fields.keys():
                if layout_form_fields.get(k) is None:
                    raise AssertionError("%s is not found in %s class" % (k, opts.layout_form.__name__))

            if opts.translate:
                new_class.layout_form = opts.translate
            else:
                new_class.layout_form = layout_form_function_fuctory(opts.layout_form)

class ValidateForm(forms.BaseForm):
    __metaclass__ = ValidateFormMetaclass

*1:django.forms.forms.DeclarativeFieldsMetaclass