Django勉強会04 newforms

  • 8月 26, 2007 06:48

先日、Django勉強会04がサイボウズラボにて行われました。発表者のみなさん、参加者のみなさんおつかれさまでした。

ymasudaさんによるnewformsのプレゼンがあったのですが、実際のコードによる検証もあった方がより理解できると思いましたので基本的な部分だけipyhonで試してみました。

まずnewformsをインポートします。newformsは今後formsという名前に変更になるのでasをつかってformsという名前でインポートします。

In [1]: from django import newforms as forms

モデルからフォームを生成する方法もありましたが、今回はfroms.Formを継承する方法でフォームをつくります。django.db.modelsのFieldと若干違うのが迷うポイントかもしれませんが、違うものだと思ってください。

とくに違うのがtextareaでdjango.db.modelsだとTextFieldを使うのですが、フォームを直接使うとforms.CharFieldにforms.Textarea widgetを使います。

In [2]: class TestForm(forms.Form):
   ...:     integer = forms.IntegerField()
   ...:     char = forms.CharField()
   ...:     text = forms.CharField(widget=forms.Textarea())
   ...:     datetime = forms.DateTimeField()
   ...:
   ...:

フォームは

  1. ユーザーがGETでアクセス
  2. フォームを生成して表示
  3. ユーザーがフォームに入力してSubmit(POSTで送信)
  4. POSTのデータを検証して正しければDBに保存など、間違ってたらエラーメッセージつきのフォームを生成

というパターンが多いです。

まず1の最初にGETでアクセスされたときは空データでフォームを生成します

In [3]: testform_instance = TestForm()

空データで生成したフォームは非束縛です。is_boundがFalseなのを確認してください。ダータの検証は

testform_instance.is_valid()

で行うのですが、is_boundがFalseなので常にFalseを返します。

In [4]: testform_instance.__dict__
Out[4]:
 {'_errors': None,
 'auto_id': 'id_%s',
 'data': {},
 'fields': {'integer': <django.newforms.fields.IntegerField object at 0x15a3890>,
         'char': <django.newforms.fields.CharField object at 0x15a3cf0>,
         'text': <django.newforms.fields.CharField object at 0x15a38b0>,
         'datetime': <django.newforms.fields.DateTimeField object at 0x15a3c30>},
 'files': {},
 'initial': {},
 'is_bound': False,
 'prefix': None}

比束縛フォームはエラーメッセージを持たずにそのままフォームを表示することができます。

In [5]: print testform_instance
<tr><th><label for="id_integer">Integer:</label></th><td><input type="text" name="integer" id="id_integer" /></td></tr>
<tr><th><label for="id_char">Char:</label></th><td><input type="text" name="char" id="id_char" /></td></tr>
<tr><th><label for="id_text">Text:</label></th><td><input id="id_text" type="text" name="text" maxlength="&lt;django.newforms.widgets.Textarea object at 0x15a3730&gt;" /></td></tr>
<tr><th><label for="id_datetime">Datetime:</label></th><td><input type="text" name="datetime" id="id_datetime" /></td></tr>

ちなみにformのインスタンスの属性、メソッド

In [6]: testform_instance.
testform_instance.__class__         testform_instance._get_errors
testform_instance.__delattr__       testform_instance._html_output
testform_instance.__dict__          testform_instance.add_prefix
testform_instance.__doc__           testform_instance.as_p
testform_instance.__getattribute__  testform_instance.as_table
testform_instance.__getitem__       testform_instance.as_ul
testform_instance.__hash__          testform_instance.auto_id
testform_instance.__init__          testform_instance.base_fields
testform_instance.__iter__          testform_instance.clean
testform_instance.__metaclass__     testform_instance.data
testform_instance.__module__        testform_instance.errors
testform_instance.__new__           testform_instance.fields
testform_instance.__reduce__        testform_instance.files
testform_instance.__reduce_ex__     testform_instance.full_clean
testform_instance.__repr__          testform_instance.initial
testform_instance.__setattr__       testform_instance.is_bound
testform_instance.__str__           testform_instance.is_valid
testform_instance.__unicode__       testform_instance.non_field_errors
testform_instance.__weakref__       testform_instance.prefix
testform_instance._errors

エラーはありません

In [6]: testform_instance.errors
Out[6]: {}

次に4つあるフィールドのうち1つだけデータを受け取ったときの状況をためします。フォームは辞書型(もしくは辞書型として扱えるもの)でデータをもらいます。

In [7]: testform_instance = TestForm({"integer":"9"})

この時点で

  • is_valid()をしてなのでエラーがない
  • is_boundがTrueになったこと(束縛フォームになった)

ことに注目してください。

In [8]: testform_instance.__dict__
Out[8]:
{'_errors': None,
 'auto_id': 'id_%s',
 'data': {'integer': '9'},
 'fields': {'integer': <django.newforms.fields.IntegerField object at 0x15a39b0>,
         'char': <django.newforms.fields.CharField object at 0x15a3e50>,
         'text': <django.newforms.fields.CharField object at 0x15a3d90>,
         'datetime': <django.newforms.fields.DateTimeField object at 0x15a3950>},
 'files': {},
 'initial': {},
 'is_bound': True,
 'prefix': None}

is_valid()でデータを検証してみます。is_boundがTrueになったので、引数でうけたデータの検証をDjangoが行います。

引数のデータが足りないのでエラーになります。

In [9]: testform_instance.is_valid()
Out[9]: False

インスタンスの中身をみてみましょう。

  • _errorsにエラーメッセージが入った

ことに注意してください。

In [10]: testform_instance.__dict__
Out[10]:
 {'_errors': {'char': [u'This field is required.'],
                                 'datetime': [u'This field is required.'],
                                 'text': [u'This field is required.']},
 'auto_id': 'id_%s',
 'data': {'integer': '9'},
 'fields': {'integer': <django.newforms.fields.IntegerField object at 0x15a39b0>,
         'char': <django.newforms.fields.CharField object at 0x15a3e50>,
         'text': <django.newforms.fields.CharField object at 0x15a3d90>,
         'datetime': <django.newforms.fields.DateTimeField object at 0x15a3950>},
 'files': {},
 'initial': {},
 'is_bound': True,
 'prefix': None}

この時点でフォームを表示させてみましょう。

エラーメッセージつきのフォームが表示されます。

In [11]: print testform_instance.as_p()
<p><label for="id_integer">Integer:</label> <input type="text" name="integer" value="9" id="id_integer" /></p>
<ul class="errorlist"><li>This field is required.</li></ul>
<p><label for="id_char">Char:</label> <input type="text" name="char" id="id_char" /></p>
<ul class="errorlist"><li>This field is required.</li></ul>
<p><label for="id_text">Text:</label> <input id="id_text" type="text" name="text" maxlength="&lt;django.newforms.widgets.Textarea object at 0x15a3730&gt;" /></p>
<ul class="errorlist"><li>This field is required.</li></ul>
<p><label for="id_datetime">Datetime:</label> <input type="text" name="datetime" id="id_datetime" /></p>

次にエラーにならないデータの作成。ブラウザのフォームからPythonの型でデータをもらうことはできないので文字列でデータを作っています。

In [12]: data = dict(integer="9",
   ....: char="spam",
   ....: text="hum\negg\n",
   ....: datetime = "2007-10-01 10:00:00")

In [13]: data
Out[13]:
{'char': 'spam',
 'datetime': '2007-10-01 10:00:00',
 'integer': '9',
 'text': 'hum\negg\n'}

作ったデータを引数にフォームをインスタンス化します。

In [14]: testform_instance = TestForm(data)

検証(is_valid())する前と後でどのように中身が変わったか確認しましょう。

まずis_valid()前

In [15]: testform_instance.__dict__
Out[15]:
{'_errors': None,
 'auto_id': 'id_%s',
 'data': {'char': 'spam',
         'datetime': '2007-10-01 10:00:00',
         'integer': '9',
         'text': 'hum\negg\n'},
 'fields': {'integer': <django.newforms.fields.IntegerField object at 0x15ae430>,
         'char': <django.newforms.fields.CharField object at 0x15ae4d0>,
         'text': <django.newforms.fields.CharField object at 0x15ae5b0>,
         'datetime': <django.newforms.fields.DateTimeField object at 0x15c8c90>},
 'files': {},
 'initial': {},
 'is_bound': True,
 'prefix': None}

検証(is_valid())をします。今回はTrueになりました。

In [16]: testform_instance.is_valid()
Out[16]: True

中身を確認すると

  • cleaned_dataができてる。しかもintegerとdatetimeがPythonの型に変換されてる。
  • _errorsに値なし

になっています。

In [17]: testform_instance.__dict__
Out[17]:
{'_errors': {},
 'auto_id': 'id_%s',
 'cleaned_data': {'char': u'spam',
         'datetime': datetime.datetime(2007, 10, 1, 10, 0),
         'integer': 9,
         'text': u'hum\negg\n'},
 'data': {'char': 'spam',
         'datetime': '2007-10-01 10:00:00',
         'integer': '9',
         'text': 'hum\negg\n'},
 'fields': {'integer': <django.newforms.fields.IntegerField object at 0x15ae430>, 'char': <django.newforms.fields.CharField object at 0x15ae4d0>, 'text': <django.newforms.fields.CharField object at 0x15ae5b0>, 'datetime': <django.newforms.fields.DateTimeField object at 0x15c8c90>},
 'files': {},
 'initial': {},
 'is_bound': True,
 'prefix': None}

cleaned_dataを確認、これを使ってモデルインスタンスを作って保存などの処理をします。

In [18]: testform_instance.cleaned_data
Out[18]:
{'char': u'spam',
 'datetime': datetime.datetime(2007, 10, 1, 10, 0),
 'integer': 9,
 'text': u'hum\negg\n'}

views関数を簡単に書くと以下のようになります。

01: def test_views(request):
02:     if request.POST:
03:         form = TestForm(request.POST)
04:         if forms.is_valid():
05:             m = TestModel(from.cleaned_data)
06:                     m.save()
07:             # リダイレクトなどの処理
08:     else:
09:         form = TestForm()
10:    return render_to_response("test.html", {"form": form })
  • ユーザーがGETでアクセスした場合は、2行目でFalseなので9行目で空フォームを生成してテンプレートでレンダリング、
  • ユーザーがPOSTで誤った値を入力した場合は、2行目はTrueでPOSTのデータをもとにformインスタンスを作るけど、4行目のis_valid()がFalseなのでエラーメッセージをもったフォームをテンプレートでレンダリング
  • ユーザーがPOSTで正しい値を入力した場合は、2行目、4行目がそれぞれTrueなのでcleaded_dataをもとに処理をしてリダイレクト

さて、最後にあった質問の件ですが、

「初期値を入れると最初にintegerだけデータをいれたときに束縛フォーム(is_boundがTrue)になってフォームにエラーが入ってしまうのでは?」ということでした。

この場合はinitial引数にデータを渡します。

In [21]: testform_instance = TestForm(initial={"integer":"88"})

こうすれば検証(is_valid())してもエラーに値が入りません

In [22]: testform_instance.is_valid()
Out[22]: False

中身を確認。is_boundがFalse(非束縛フォーム)になっていることを確認してください。

In [23]: testform_instance.__dict__
Out[23]:
{'_errors': None,
 'auto_id': 'id_%s',
 'data': {},
 'fields': {'integer': <django.newforms.fields.IntegerField object at 0x15dd930>,
     'char': <django.newforms.fields.CharField object at 0x15dda90>,
         'text': <django.newforms.fields.CharField object at 0x15ddad0>,
         'datetime': <django.newforms.fields.DateTimeField object at 0x15ddaf0>},
 'files': {},
 'initial': {'integer': '88'},
 'is_bound': False,
 'prefix': None}

フォームを表示させてもエラーはありません

In [24]: print testform_instance
<tr><th><label for="id_integer">Integer:</label></th><td><input type="text" name="integer" value="88" id="id_integer" /></td></tr>
<tr><th><label for="id_char">Char:</label></th><td><input type="text" name="char" id="id_char" /></td></tr>
<tr><th><label for="id_text">Text:</label></th><td><input id="id_text" type="text" name="text" maxlength="&lt;django.newforms.widgets.Textarea object at 0x15a3730&gt;" /></td></tr>
<tr><th><label for="id_datetime">Datetime:</label></th><td><input type="text" name="datetime" id="id_datetime" /></td></tr>