【Django入門5】フォームの基本

Djangoのフォームの解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、Django におけるフォームについて解説していきます。

フォームというのは、下の図のようなユーザーから入力を受け付けるような画面のことになります。下の図はユーザー登録時に利用するフォームの例となりますが、他にもログインや掲示板へのコメントの投稿等にもフォームが利用されることになります。

フォームの例

このようなフォームは、Django においては『フォームクラス』を定義することで簡単に扱うことができます。このページでは、まずフォームやフォームの扱いについて簡単に解説し、その後、このフォームクラスについて解説していきます。

また、この Django 入門 は連載形式となっており、下記ページでは Django におけるビューについて、

Djangoのビューの機能についての解説ページアイキャッチ 【Django入門3】ビュー(View)の基本とURLのマッピング

さらに、下記ページでは Django におけるテンプレートについて解説を行いました。

Djangoのテンプレートの解説ページアイキャッチ 【Django入門4】テンプレート(Template)の基本

これらのページではウェブアプリの実装例も紹介していますが、どちらにおいても受け付けるリクエストはページ表示に対するものだけでした。つまり、リクエストのメソッドが GET であることを前提とした実装例になっています。

今回紹介するフォームクラスを利用すれば、データ送信のリクエスト(メソッドが POST のリクエスト)をウェブアプリで扱えるようになります。厳密に言えば、フォームクラスを利用しなくてもデータ送信のリクエストをウェブアプリで扱うことは可能です。が、フォームクラスを利用した方が様々な面でフォームの扱いが楽になります。

つまり、フォームクラスは、フォームを扱うウェブアプリの開発効率を上げるための Django に用意された仕組みになります。さらに、フォームを利用することで、ユーザーとデータのやり取りが可能なウェブアプリを実現できるようになり、開発できるウェブアプリの幅も広げることができますので、是非フォームおよびフォームクラスについては理解していってください!

フォームの基本

まずは、フォームに関する基本的な内容の解説を行なっていきます。

フォームとは

ウェブアプリにおいて、フォームとはユーザーからウェブアプリに対して「データを送信する」ための画面(ユーザーインターフェース)になります。

ユーザーがウェブアプリに対してデータを送信する様子

フォームの例は下の図のようなものになります。フォームは入力欄となる複数の「フィールド」と、データの送信を行う「ボタン」から構成されることが多いです。下の図の例は、「ユーザー名入力用のフィールド」と「パスワード入力用のフィールド」、さらには「送信ボタン」から構成されるフォームとなります。フィールドに関しては「入力欄」「入力フォーム」など、いろんな呼び方があるのですが、Django においてはフィールドという呼び方が実装と合っていて分かりやすいかと思います。

フォームの構成を示す図

このようなフォームを利用すれば、ユーザーは各フィールドにデータ(文字列)を入力し、フィールドに入力したデータをウェブアプリに対して送信することができるようになります。

フィールドに入力したデータがウェブアプリに送信される様子

さらに、ウェブアプリはフォームから送信されたデータを受信し、それを利用して様々な処理を行うことが可能となります。そして、これによって様々な機能を実現することができます。

例えば、上の図のフォームで送信されてきた username フィールドと password フィールドへの入力データを利用してユーザー認証を行い、認証 OK の場合にユーザーのログイン処理を行なうようにすれば、ウェブアプリでログイン機能が実現できることになります。

フォームから送信されたデータを利用して機能を実現する様子

ログインだけでなく、ユーザー登録や掲示板へのコメントの投稿など、ウェブアプリに対してデータの送信を行うことで実現される機能は、こういったフォームを利用するのが一般的な方法となります。

スポンサーリンク

フォームの HTML 構成

このフォームは Django 専用のものではなく、一般的な HTML で実現可能な要素となっています。

具体的には、フォームは下記のような HTML により実現可能です。

フォームのタグ
<form action="/login/" method="post">
  <p>
      <label for="id_username">ユーザー名:</label>
      <input type="text" name="username" required id="id_username">
  </p>
  <p>
      <label for="id_password">パスワード:</label>
      <input type="password" name="password" required id="id_password">
  </p>
  <p><input type="submit" value="送信"></p>
</form>

ウェブブラウザで上記の HTML を表示すれば、下の図のようなフォームが表示されます。

HTMLによってフォームが表示される様子

このように、フォームは一般的な HTML で実現可能ですので、下記のページで解説しているテンプレートファイルに上記のようなタグを直接記述することでもフォームは実現可能です。

Djangoのテンプレートの解説ページアイキャッチ 【Django入門4】テンプレート(Template)の基本

ですが、Django には別途フォームを扱うために便利な仕組みが用意されており、それを利用する方が楽にフォームは実現することが出来ます。

このフォームクラスの詳細については、後述の フォームクラス の章で解説していきます。

フォームとリクエストの関係

続いて、フォームとリクエストの関係性について解説していきます。

下記のビューの解説ページの 役割2:リクエストに応じた処理を行うリクエストに応じた処理を実行する でも説明している通り、ウェブアプリでは、クライアントから送信されてきたリクエスト、より具体的にはリクエストの URL とメソッドに応じた機能を実行することが必要となります。

Djangoのビューの機能についての解説ページアイキャッチ 【Django入門3】ビュー(View)の基本とURLのマッピング

フォームに対する2つの機能

ウェブアプリでフォームを扱う場合、各フォームに対し、”フォームを表示するための HTML を取得する機能” と、”フォームからのデータの送信を行う機能” を提供する必要があります。ここでは、前者の機能を「フォームの取得機能」、後者の機能を「フォームの送信機能」と呼ばせていただきます。

フォームからのデータの送信を行う際には、まずフォームを表示してユーザーがフォームの操作が行えるようにする必要があります。なので「フォームの取得機能」が必要です。また、フォームはデータを送信するためのインターフェースなので、フォームからのデータの送信を行う「フォームの送信機能」も必要となります。

フォームを扱うウェブアプリに必要となる2つの機能の説明図

フォームの送信機能は、どちらかと言うとユーザー視点での名前で、ウェブアプリの視点ではデータの受信機能と考えても良いと思います。

フォームと URL の関係

また、前述のとおり、リクエストに応じた機能は URL とメソッドによって決まります。

特定の同一のフォームに対するリクエストでは、基本的にはフォームの取得を要求する場合もフォームの送信を要求する場合も同じ URL のリクエストがウェブアプリに送信されてくることになります。なので、リクエストの URL だけでは、クライアントが何を要求しているのかが判断できないことになります。

同一のフォームに対するリクエストが同じURLとなる様子

フォームとメソッドの関係

ですが、これらの場合でリクエストのメソッドが異なることになります。

このメソッドには GETPOST と呼ばれるものが存在し、GET は主にデータの取得を目的とするリクエストに設定され、POST は主にデータの送信を目的とするリクエストに設定されます。また、メソッドが POST の場合は、リクエストのボディという部分でデータが送信されてくることになります。

したがって、フォームを扱う URL へのリクエストを受け取った場合、メソッドが GET であればフォームの取得が要求されていることになるため「フォームの取得機能」を実行するように、さらにフォームを扱う URL へのメソッドが POST であればフォームからのデータの送信が要求されていることになるため「フォームの送信機能」を実行するようにウェブアプリを開発する必要があります。

フォーム取得字とフォーム送信時とで同一のフォームに対するリクエストのメソッドが異なる様子

「フォームの取得機能」は、基本的には URL に対応するフォームを表示するための HTML をボディとしたレスポンスの返却になります。「フォームの送信機能」は、その URL に対応するフォームから送信されてきたデータを受け取り、そのデータを利用して “何かしらの処理” を実行することで実現されます。”何かしらの処理” とは、送信されてきたデータをデータベースやレスポンス等に反映する処理であり、具体的な処理に関してはリクエストの URL 等によって異なることになります。

例えば、コメント投稿フォームを利用するための URL が /comment/ であれば、GET /comment/ のリクエストを受け取った時には、 コメント投稿フォームを表示するための HTML をボディとしたレスポンスを返却し、さらに POST /comment/ のリクエストを受け取った時には、コメント投稿フォームから送信されてきたデータを受け取り、受け取ったデータをデータベースに保存するような処理を行うウェブアプリを開発する必要があります。

GETメソッドのリクエストやPOSTメソッドのリクエストを受け取った時に実行する処理の例

ここまでの説明のように、フォームを扱う上ではリクエストが GET  の場合だけでなく、POST の場合も考慮してウェブアプリを開発していく必要があります。 今までの Django 入門 の連載の中では特にメソッドに関しては考慮せずにウェブアプリを開発してきましたので、この点が今までと異なるところになります。

MEMO

データの送信時のメソッドには POST だけでなく、PUTPATCH 等が指定されることもあります

このページではデータ送信時のメソッドが POST であることを前提に解説していきますが、 PUTPATCH 等も指定可能であることは覚えておいてください

フォームを扱う流れ

少し、先ほどの説明と重複する部分もありますが、次はフォームを扱うときの操作や処理の流れについて解説していきたいと思います。

フォームを表示する

ユーザーがフォームを扱うためには、まずはフォームを表示してやる必要があります。この表示されたフォームの各フィールドにデータ・値を入力し、送信ボタンをクリックすることで、ユーザーはデータをウェブアプリに対して送信することができることになります。なので、フォームを扱うためには、まずフォームの表示が必要となります。

つまり、フォームを扱う際には、まずフォームの表示のための HTML の取得を行うために、クライアントからウェブアプリに対して GET メソッドのリクエストが送信されることになります。そして、そのレスポンスで受け取った HTML をクライアントがページとして描画することでフォームが表示され、そのフォームをクライアントのユーザーが利用できるようになります。

最初にメソッドがGETのリクエストがクライアントから送信される様子

ウェブアプリ側の視点で考えると、クライアントからフォームを扱う URL に対する GET メソッドのリクエストを受け取った際には、まず、その URL にマッピングされたビューの関数が実行されることになり、そのビューの関数は、フォームの取得機能、すなわちフォームを表示するための HTML をボディとするレスポンスの生成および、その返却を行うような処理を実行する必要があります。

GETリクエストを受け取ったウェブアプリでビューの関数が実行され、フォームの取得機能が動作する様子

そして、この HTML は、下記ページでも説明しているテンプレートを利用して生成されることになります。

Djangoのテンプレートの解説ページアイキャッチ 【Django入門4】テンプレート(Template)の基本

ちなみに、上記で触れた “マッピング” に関しては下記ページの ビューと URL とのマッピング で解説していますので、詳しく知りたい方は下記ページをご参照ください。

Djangoのビューの機能についての解説ページアイキャッチ 【Django入門3】ビュー(View)の基本とURLのマッピング

ただし、ここで返却する HTML にも一工夫が必要となります。フォームにはデータ送信用のボタンを設ける必要がありますが、このボタンは、押下された時にウェブアプリに対して POST メソッドのリクエストが送信されるように作成しておく必要があります。また、この POST メソッドのリクエストが送信される際には、各種フィールドに入力されたデータが送信されるようにもしておく必要があります。

ボタンを押した際にPOSTメソッドのリクエストが送信されるようにしておく必要があることを示す図

このようにボタンを作成しておかないと、ユーザーはフォームのフィールドにデータを入力できたとしても、それらのデータをウェブアプリに送信することができません。

フォームを表示するHTMLに不備があってデータが送信できない様子

また、この時に送信されるリクエストの URL には、基本的にはフォーム表示時のリクエストに指定した URL と同じ URL が指定されます(というか、そうなるようにウェブアプリを開発します)。

このような HTML の作り方に関しては後述の フォームの表示 の節で詳細を解説しますが、フォームを表示するのは単にフィールドを表示することが目的ではなく、ユーザーがデータを送信する手段を提供することが目的となります。なので、その目的を果たせるように HTML を生成する必要があることは覚えておきましょう。

フォームから送信されてきたデータを利用して機能を実行する

ウェブブラウザにフォームが表示されれば、ユーザーはフォームの各種フィールドに値を入力し、送信ボタン等でフォームからのデータの送信を行うことになります。送信ボタンが押下された時には POST メソッドのリクエストが送信されることになります。そして、フォームの各種フィールドに入力された値は、このリクエストのボディにセットされた状態で送信されます。

フォームからデータの送信を行うことでPOSTメソッドのリクエストが送信される様子

したがって、ウェブアプリは POST メソッドのリクエストを受け取り、そのリクエストの URL にマッピングされたビューの関数が実行されることになります。そして、リクエストが POST メソッドの場合は、リクエストのボディとしてデータも一緒に送信されてくるはずです。そのため、この時に実行されるビューの関数においては、送信されてきたデータを受け取り、そのデータを利用した処理を実行する必要があります。これらの一連の処理が「フォームの送信機能」となります。

データを受け取った際に、どのような処理を行うかはウェブアプリによって異なりますし、フォームの役割によっても異なることになります。例えばログイン用のフォームからデータが送信されてきたのであれば、受け取ったデータを利用して認証を実施し、認証 OK であればログイン処理を実施することになると思います。また、掲示板におけるコメント投稿用のフォームであれば、受け取ったデータを掲示板の新たな投稿としてデータベースに保存することになると思います。

POSTメソッドのリクエストを受け取ったウェブアプリでビューの関数が実行され、フォーム送信機能が動作する様子

いずれにせよ、POST メソッドのリクエストを受け取った時には、単に機能・処理を実行するのではなく、“送信されてきたデータを利用して” 機能・処理を実行することが重要となります。また、POST メソッドの場合でもレスポンスの返却は必要になります。例えば、コメント投稿用のフォームからのリクエストの場合は、投稿に成功 or 失敗したことを示すメッセージを含む HTML をボディとするレスポンスであったり、他のページ、例えばコメント一覧のページへリダイレクトを指示するリダイレクトレスポンスを返却したりする感じになります。

データが妥当でない場合の処理も必要

また、基本的には POST メソッドのリクエストを受け取った時に実行する処理は上記のような流れになるのですが、加えて1つ考慮しなければならないのが「妥当でないデータ」を受け取った時に実行する処理となります。例えば、ウェブアプリでは整数を扱いたいのに、フォームからはアルファベットの文字列などが送信されてくる可能性もあります。このような、ウェブアプリで扱うデータとして妥当でないものを受け取った時に、どういった処理を行うのかについても検討し、それを実装しておく必要があります。

「妥当でないデータ」を受け取った時によくやるのがフォームの再表示になります。要は、ユーザーにデータを修正してもらうことを目的に再度フォームの表示を行います。この場合は、POST メソッドのリクエストの場合でも、フォーム表示用の HTML を生成して返却するような処理が実行されるようにする必要があります。

受け取ったデータが妥当でない場合にフォームの表示用HTMLを返却する様子

POST メソッドでどのようなデータが送信されてくるかが分からないため、ウェブアプリではデータの妥当性を検証することが重要になります。このデータの妥当性の検証に関しては、後述の 妥当性の検証 で詳しく説明したいと思います。

ビューでのメソッドに応じた機能の切り替え

ここまで、フォームを扱う際のウェブアプリの動作を中心に解説してきましたが、上記で説明したような動作はビューによって実現していくことになります。

その上で重要になるのが、ビューの関数での「メソッドに応じた実行する機能・処理」の切り替えになります。

フォームとリクエストの関係 でも説明したように、特定の同一のフォームに対するリクエストでは、フォームの表示時もフォームの送信時も同じ URL となります。そして、リクエストを受け取った時に実行されるビューの関数は URL で決まります。なので、同じ URL に対するリクエストを受け取った時には同じビューの関数が実行されることになります。

そのため、ビューの関数では、リクエストのメソッドを確認し、それがフォームの表示に対するリクエストであるか、フォームの送信に対するリクエストであるかを判断し、前者の場合は フォームを表示する で説明した流れを実現するような機能・処理を実行し、後者の場合は フォームから送信されてきたデータを利用して機能を実行する で説明した流れを実現するような機能・処理を実行する必要があります。

つまり、ビューの関数ではリクエストのメソッドを確認し、そのメソッドに応じて実行する機能・処理を切り替えることが必要となります。具体的には、ビューの関数には HttpRequest のサブクラス のインスタンスを引数で受け取ることになり、このインスタンスのデータ属性 method にメソッドを示す文字列がセットされていますので、この method による条件分岐を行って、上記のような、実行する機能・処理の切り替えを実現すればよいことになります。

メソッドに応じた機能の分岐
def ビューの関数(request):
    if request.method == 'POST':
        # メソッドがPOSTの場合の機能を実装

    else:
        # メソッドがPOST以外(GET)の場合の機能を実装

スポンサーリンク

フォームクラス

大体フォームについては理解していただけたでしょうか?

前述の通り、Django で開発するウェブアプリでフォームを扱いたい場合はフォームクラスを利用するのが便利です。次は、このフォームクラスについて解説していきます。

フォームクラスとは

フォームクラスとは、Django フレームワークで定義される Form というクラスを継承するクラスのことになります。要は、Form のサブクラス のことを、便宜上フォームクラスと呼んでいます。

詳細は後述で解説していきますが、このフォームクラスのインスタンスをテンプレートファイルに埋め込むだけで、フォームの各種フィールド(とラベル)の表示、つまり HTML へのフィールド要素の追加を実現することができます。なので、フォームクラスを利用すれば、フィールド要素をテンプレートファイルに直接記述する作業は不要になります。

フォームクラスのインスタンスをテンプレートファイルに埋め込む様子

また、先ほど『妥当性の検証』について簡単に説明しましたが、この妥当性の検証の実施や妥当性の検証後のデータの取得もフォームラスのインスタンスによって実現できます。

要は、Django でフォームを “簡単&効率的&安全” に扱うためのクラスがフォームクラスになります。

フォームクラスの定義先ファイル

このフォームクラスの定義先のファイルはアプリフォルダの forms.py となります。アプリフォルダとは startapp コマンドで実行されるフォルダになります。このコマンドの実行によって views.pymodels.py も自動的に生成されることになるのですが、forms.py は自動的に生成されないので注意してください。

フォームクラスの定義先を示す図

スポンサーリンク

フォームクラスの定義方法

続いて、このフォームクラスの定義の仕方について解説していきます。

フォームクラスの定義

前述のとおり、フォームクラスとは、Django フレームワークで定義される Form のサブクラスのことになります。

フォームクラスをFormを継承して定義する様子

この Form クラスは、具体的には django.forms で定義されるクラスになります。なので、下記のように django から forms を import し、さらに、forms.Form を継承するようにクラスを定義すれば、そのクラスがフォームクラスとなります。

Formの継承
from django import forms

class UserForm(forms.Form):
    pass

フィールドの追加

ただし、フォームクラスは単に定義するだけではあまり意味がありません。

このフォームクラスを定義する上で重要なのは、そのフォームに必要なフィールドをクラス変数として定義することになります。フォームクラスに定義したクラス変数はフォームの「フィールド」として扱われることになります。さらに、このクラス変数の変数名が「フィールド名」として扱われることになります。

そして、後述する方法でフォームクラスのインスタンスを HTML に埋め込めば、下の図のようにフォームクラスのクラス変数がフィールド要素として表示されることになります。また、そのフィールドの左側のラベルにはクラス変数名の頭文字を大文字にしたものが表示されます。

フォームクラスのクラス変数がフォームのフィールドとして表示される様子

ただし、どんなクラス変数でもフィールドとして扱われるというわけではないので注意してください。フィールドとして扱われるようにするためには、クラス変数を Field のサブクラスのインスタンスとして定義する必要があります。要は、クラス変数の定義式の右辺で Field のサブクラスのコンストラクタを実行するようにします。

フィールドとしてクラス変数を定義する方法の説明図

この Fielddjang.forms で定義されるクラスであり、用途に応じて様々なサブクラスが用意されています。

例えば、フォームで文字列データの入力を受け付けたいような場合、CharField というクラスのインスタンスとしてクラス変数を定義すれば、フォーム表示時に文字列の入力フィールドが表示されるようになります。そして、その入力フィールドでユーザーからの文字列入力を受け付けることができるようになります。

他にも整数データの入力を受け付ける IntegerField、メールアドレスの入力を受け付ける EmailField などが存在します。

Fieldのサブクラスに応じてフィールドに入力可能なデータが変化する様子

下記はクラスフォームの定義例となります。この UserForm を定義した場合、このフォームは文字列の入力フィールド、整数の入力フィールド、メールアドレスの入力フィールド、の3つのフィールドを持つことになります。

クラス変数の定義
from django import forms

class UserForm(forms.Form):
    name = forms.CharField()
    age = forms.IntegerField()
    email = forms.EmailField()

そして、後で表示方法については解説しますが、この UserForm のインスタンスを表示した場合、下図の右側のような表示結果が得られることになります。

フォームクラスのクラス変数がフォームのフィールドとして表示される様子

このように、フォームクラスの定義時は Form クラスを継承し、さらに必要な分だけクラス変数をフィールドとして定義する必要があります。

このページでは詳しくは説明しませんが、Field のサブクラスには様々なクラスが用意されており、コンストラクタへの引数指定によってラジオボタンやチェックボックスを実現することもできます。また、各フィールドの初期値等もコンストラクタへの引数の指定により設定可能です。この辺りも別途ページを公開して解説していきたいと思っています。

フォームクラスのインタスタンスの生成

また、一般的なクラス同様に、フォームクラスにおいてもコンストラクタを実行することでインスタンスを生成することができます。そして、そのインスタンスをフォームとして扱っていくことになります。

このインスタンスの生成やインスタンスの制御に関しては、基本的にはビューが行うことになります。また、render 関数実行時にテンプレートファイルにフォームクラスのインスタンスを埋め込むことで、フィールドの表示を行うことも可能です。この辺りは、次の フォームの表示 で解説します。

ここでは、まず、フォームクラスのインスタンスの生成について解説します。

“引数なし” でインスタンスを生成

インスタンスの生成については、先ほど定義した下記の UserForm クラスを例に挙げて解説していきたいと思います。

クラス変数の定義
from django import forms

class UserForm(forms.Form):
    name = forms.CharField()
    age = forms.IntegerField()
    email = forms.EmailField()

フォームクラスのコンストラクタは引数なしで実行することが可能です。例えば、下記のように引数なしで UserForm のコンストラクタを実行してインスタンスを生成することができます(この場合、form 変数が生成されたインスタンスを参照することになります)。

引数なしでのインスタンス生成
from .forms import UserForm

def user(request):
    form = UserForm()

上記のようにフォームクラスのコンストラクタに引数を指定しなかった場合、生成されるインスタンスのフォームの各フィールドには初期値が設定されることになります。この初期値は、フォームクラスのクラス変数定義時に設定可能です。ただ、上記の UserForm クラスでは初期値を設定していないため、各種フィールドの値は “なし” となります。この状態で、このインスタンスを表示すると、下の図のようにフィールドには何も値が設定されていない状態で表示されることになります。

クラスフォームのコンストラクを引数なしで実行した際のインスタンスの状態を示す図

なので、上図のように、何も値が設定されていない状態でフィールドを表示したいのであれば、フォームクラスのインスタンスは引数なしで生成してやると良いです。

引数に辞書データを指定してインスタンスを生成

また、コンストラクタの第1引数には辞書(or 辞書のサブクラスのインスタンス)を指定することが可能です(キーワード引数 data で指定しても良いです)。辞書を引数に指定してインスタンスを生成した場合、その引数に指定したデータに対応した値が各種フィールドにセットされた状態のインスタンスが生成されることになります。

例えば、UserForm のコンストラクタに下記のように辞書データを引数に指定して実行した場合、name フィールドには YamadaTaroage フィールドには 18email フィールドに taro@yamada.jp が設定されたフォーム(フォームクラスのインスタンス)が生成されることになります。

引数ありでのインスタンス生成
from .forms import UserForm

def user(request):
    data = {
        'name' : 'YamadaTaro',
        'age' : 18,
        'email' : 'taro@yamada.jp'
    }

    form = UserForm(data)

そのため、このインスタンスを表示すれば、下の図のように値が設定された状態の各種フィールドが表示されることになります。

クラスフォームのコンストラクを引数ありで実行した際のインスタンスの状態を示す図

例えば、ユーザー操作によってフォームから送信されてきたデータが妥当でない場合、入力値の修正後をしてもらうためにフォームの再表示を行うようなことがあります。このような場合に、全フィールドが空の状態だと、一からデータの入力が必要になってしまってユーザーに不便さを感じさせることになります。

各種フィールドが空の状態でフォームが再表示される様子

なので、フォームの再表示時には、ユーザーが送信してきたデータに基づいて各種フィールドに値を設定した状態でフォームを表示してあげた方が良いです。そして、これは、上記のように引数へ辞書を指定してフォームクラスのインスタンスを生成することによって実現できます。

これに関しては後述の フォームからのデータの受け取り で詳細を解説しますが、フォームから送信されてきたデータはビューの関数の中で「辞書のサブクラスのインスタンス」として受け取ることができます。なので、それをそのまま引数に指定してフォームクラスのコンストラクタを実行してやればフォームの再表示を簡単に実現できることになります。

各種フィールドに前回入力した値が設定された態でフォームが再表示される様子

また、上記でフォームから送信されてきたデータが妥当でない場合の説明をしましたが、フォームから送信されてきたデータの妥当性の検証を行うためには、フォームから送信されてきたデータを引数に指定してインスタンスを生成する必要があります。このあたりに関しては 妥当性の検証 の章で詳しく説明します。

メソッドによるインスタンスの生成の仕方の違い

先ほど説明した、フォームの再表示に関しても、妥当性の検証に関しても、フォームからデータが送信されてきたときに行う処理となります。つまり、辞書を引数に指定してインスタンスを生成するのは、基本的にはフォームを扱う URL に対して POST メソッドのリクエストが送信されてきた場合となります。それに対し、フィールドに何も値が設定されていない状態(もしくは初期値が設定された状態)で表示するのが望ましいのが最初にフォームを表示する時で、つまりはフォームを扱う URL に対して GET メソッドのリクエストが送信されてきた場合となります。

したがって、フォームを扱う URL に対するリクエストのメソッドが GET の場合は、フォームクラスのコンストラクタを引数なしで実行してインスタンスを生成し、メソッドが POST の場合はフォームクラスのコンストラクタに辞書を指定してインスタンスを生成してやれば良いことになります。もちろん、このパターンに当てはまらないこともあるかもしれませんが、まずは、このルールに従ってフォームクラスのインスタンスの生成を行うようにすれば良いと思います。

フォームの表示

ここまでの説明により、フォームクラスを定義し、そのフォームクラスのインスタンスを生成することができるようになったことになります。次は、このクラスフォームのインスタンスを利用して フォームを扱う流れ で説明したようなフォームの表示やデータの受け取りを実現する方法について説明していきたいと思います。

各種フィールドを表示する

下記ページでも解説しているように、Django では HTML の生成をテンプレートファイルを利用して実現することができます。

Djangoのテンプレートの解説ページアイキャッチ 【Django入門4】テンプレート(Template)の基本

また、上記ページでも解説していますが、テンプレートファイルでは変数を扱うが可能です。そして、その変数部分は、render 関数実行時にコンテキストで定義される変数の値に置き換えられることになります。

テンプレートが変数を参照し、render関数実行時にコンテキストの値に置き換えられる様子

このコンテキストで定義される変数の値としてはクラスのインスタンスを指定することが可能で、変数の値にフォームクラスのインスタンスを指定することも可能となっています。

コンテキストで定義される変数の値がフォームクラスのインスタンスである場合、render 関数実行時にはテンプレートファイルの変数部分が単にフォームクラスのインスタンスに置き換えられるのではなく、フォームクラスの持つ各種フィールドを表示するためのタグに置き換えられることになります。

テンプレートファイルの変数参照部がformのインスタンスに置き換えれらる様子

つまり、render 関数の実行により生成される HTML をページとして表示してやれば、フォームクラスで定義されているクラス変数がフィールドとして表示されることになります。

次は、具体例を使って、このフィールドの表示の様子を確認してみましょう!

まず、今回は下記のような form_view 関数を views.py に用意したいと思います。2行目では forms.py から前述で紹介した UserForm を import しています。また、アプリ名は app であることを前提とした関数となっています。

form関数
from django.shortcuts import render
from .forms import UserForm

def form_view(request):
    form = UserForm()
    context = {
        'form' : form
    }
    return render(request, 'app/form.html', context)

この関数では、UserForm のインスタンスを生成し、そのインスタンスをコンテキストにおける 'form' キーの値に設定しています。さらに、render 関数を実行してテンプレートファイルとコンテキストから HTML の生成を行い、この生成された HTML をボディとするレスポンスを返却しています。

今回は、この render 関数の引数に指定しているテンプレートファイル form.html は下記であることを想定して説明していきたいと思います。

form.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>フォーム</title>
</head>
<body>
    {{ form }}
</body>
</html>

ポイントは {{ form }} の部分で、ここで変数 form を扱うようにしています。views.py の form 関数で用意するコンテキストの 'form' キーには UserForm のインスタンスを設定しているため、この {{ form }} 部分は render 実行時に UserForm の各種フィールドを表示するためのタグに置き換えられることになります。

具体的には、{{ form }} 部分は下記のような HTML タグに置き換えられることになります(見やすくなるように少し整形しています)。

フォームクラスのインスタンスの埋め込み
<tr>
  <th><label for="id_name">Name:</label></th>
  <td>
    <input type="text" name="name" maxlength="20" required id="id_name">
  </td>
</tr>

<tr>
  <th><label for="id_age">Age:</label></th>
  <td>
    <input type="number" name="age" required id="id_age">
  </td>
</tr>

<tr>
  <th><label for="id_comment">Comment:</label></th>
  <td>
    <input type="text" name="comment" required id="id_comment">
  </td>
</tr>

<tr><th> タグ等が存在している点が気になるかもしれませんが、この理由については後述で解説します。

実際に、ウェブブラウザ等からリクエストを送信して form 関数を実行すると、下の図のようなページが表示されることになります。

フォームクラスのインスタンスをテンプレートファイルに埋め込んだときのページを示す図

UserForm クラスに定義したクラス変数に対するフィールドが表示されていることを確認できると思います。また、各種フィールドにはラベルが設けられており、このラベルの文字列にはクラス変数の変数名の最初の文字を大文字にしたものが設定されます。

こんな感じで、フォームクラスのインスタンスをテンプレートファイルに埋め込んでやれば、それが各種フィールドを表示するためのタグに変化することになります。なので、フォームクラスを利用することで、HTML やテンプレートファイルにフィールドを表示するためのタグを記述する手間が省けます。

ただ、各種フィールドが縦方向ではなく横方向に並んで表示されているのが気になりますね…。これに関しては理由があって、その理由については後述の フィールドの表示形式をカスタマイズする で解説します。

フォーム要素としてフィールドを表示する

先程の例のように、テンプレートファイルにフォームクラスのインスタンスを埋め込むことで、フォームクラスに定義したフィールドが表示されるようになります。ですが、実はこれだけではフォームとして不十分です。

だってボタンが無いですからね…。ボタンが無いので、フィールドにデータを入力してもデータを送信することができません。つまり、テンプレートファイルには、フォームクラスのインスタンスを埋め込むための実装だけでなく、各種フィールドに入力されたデータを送信するための実装が別途必要になります。

各種フィールドに入力されたデータを送信できるようにするためには、テンプレートファイルには下記の記述が必要となります。{{ form }}form 部分はテンプレートタグから参照する変数名ですので、コンテキストに用意するキーに合わせて変更しても OK です。また、2. に関しては後述で説明しますので、まずは気にせず解説を読み進めていただければと思います。

  1. <form></form> を記述する(フォームの定義)
    • action 属性にリクエスト先の URL をルートパス形式で指定する
    • method 属性に "POST" を指定する
  2. <form></form> の間に {% csrf_token %} を記述する
  3. <form></form> の間に {{ form }} を記述する(各種フィールドの表示)
  4. <form></form> の間に <input> タグを記述する(送信ボタンの表示)
    • type 属性に "submit" を指定する

具体的には、テンプレートファイルのフォームを表示したい部分に下記を記述してやれば良いことになります。URLは /login/ としていますが、ここは実際の URL に合わせて変更してやる必要があります。

フォームの表示
<form action="/login/" method="POST">
{% csrf_token %}
{{ form }}
    <input type="submit" value="送信">
</form>

このようにテンプレートファイルを作成しておけば、<form></form> が1つのフォーム要素となり、このフォーム内で送信ボタンを押すと、action 属性に指定された URL に対して method 属性に指定されたメソッドのリクエストがフォームの送信元であるウェブアプリに送信されることになります。そして、このフォーム内のフィールドは、前述のとおり{{ form }} がフォームクラスのインスタンスに置き換えられることで表示され、リクエストの送信時にはフォーム内のフィールドに入力されているデータも一緒に送信されることになります。

ボタンを押した際に送信されるリクエストの説明図

ここまでの説明のとおり、単に {{ form }} と記述した場合はフォームクラスで定義したフィールドが表示されるようになるだけですが、上記のように <form></form> を記述し、その中に {{ form }} とボタン要素のタグを記述するようにすれば、ボタン押下によってフィールドに入力されたデータを送信可能なフォームを実現することができます。

ただし、Django のフォームによってウェブアプリに情報を送信するようにするためには、上記の 2. のように必ず<form></form> の間に {% csrf_token %} を記述する必要がある点に注意してください。

この {% csrf_token %} はテンプレートタグの1つで、これを記述しないと、別途設定を行わない限り、フォームからの情報の送信に必ず失敗するようになります。

理由は下記ページで解説しているので詳しく知りたい方はこちらをご参照ください。まずは <form></form> と {% csrf_token %} はセットで記述する必要があることを覚えておくと良いと思います。

DjangoにおけるCSRFトークンやCSRF検証についての説明ページアイキャッチ 【Python/Django】CSRF対策について解説

ここまでの解説に基づくと、フィールドに入力されたデータを送信可能なフォームを実現するためには、前述で紹介した form.html を下記のように変更する必要があることになります。

form.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>フォーム</title>
</head>
<body>
    <form action="{% url 'form' %}" method="POST">
    {% csrf_token %}
    {{ form }}
        <input type="submit" value="送信">
    </form>
</body>
</html>

この form.html から render 関数で HTML を生成した場合、表示されるページは下図のようなものになります。先程の例に比べると、見た目的にはボタンが追加されただけですが、これらは1つのフォーム要素として構成されるようになっており、送信 ボタンを押せば、各種フィールドに入力されたデータが送信されるようになっています。

フォームが表示される様子

上記の form.html に対して1つ補足しておくと、{% url 'form' %} は URL の名前から URL 自体を逆引きし、その URL に置き換えるためのテンプレートタグになります。つまり、render 関数実行時に、この部分は「名前が 'form' である URL」に置き換えられることになります。そして、送信 ボタンが押された際には、その URL に対してフィールドに入力された情報を含むリクエストが送信されることになります。

名前からの URL の逆引きを行うためには URL に名前を設定してく必要があります。URL の名前は path 関数の引数によって設定することが可能です。詳しくは下記ページの ビューと URL とのマッピング を参考にしていただければと思います。

Djangoのビューの機能についての解説ページアイキャッチ 【Django入門3】ビュー(View)の基本とURLのマッピング

フィールドの表示形式をカスタマイズする

さて、ここまでフォームクラスのインスタンスを利用したフォームの表示方法について解説してきましたが、ここまで示したテンプレートファイルの場合、各種フィールドはテーブル形式で表示されるようになっています。

もっと具体的に言えば、{{ form }} のように単にテンプレートファイルに {{ 変数名 }} を記載し、さらに、この変数の参照する値がフォームクラスのインスタンスである場合、各種フィールドは td タグとして出力されるようになっています。また、各種フィールドのラベルは th タグとして出力されます。さらに、テーブル内の各行に1つのフィールドのラベルとフィールドが表示されるよう、フィールドごとに tr タグで囲まれるようになっています。

フォームクラスのインスタンスを埋め込んだ結果出力されるタグ

これによって、各種フィールドがテーブル形式で整形されて表示されるようにはなっているのですが、うまく整形して表示するためには、これらのフィールドのタグを <table></table> で囲う必要があります

ですが、ここまで紹介してきたテンプレートファイルでは {{ form }}<table></table> で囲っていなかったため、うまくテーブル形式に整形されずに表示され、横方向に並べて表示されていました。

フォームが表示される様子

うまくテーブル形式に整形して表示するためには、{{ 変数名 }} が参照する値がフォームクラスのインスタンスである場合は <table></table> で囲う形式で記述する必要があります。先程紹介した form.html においても、{{ form }} 部分を <table>{{ form }}</table> に置き換えれば、ページとして表示した場合、下の図のように表示されるようになります。

フォームがテーブル形式で表示される様子

また、{{ 変数名 }} における 変数名 部分の指定方法を変更することにより、テーブル形式以外の形式で表示することも可能です。具体的な指定方法とその場合の表示形式は下記のようになります。

  • {{ 変数名.as_p }}:段落形式(<p> タグが出力される)
  • {{ 変数名.as_table }}:テーブル形式(<tr><th><td>タグが出力される)
    • <table></table> で囲う必要あり
  • {{ 変数名.as_ul }}:箇条書き形式(<li> タグが出力される)
    • <ul></ul> or <ol></ol> で囲う必要あり

単に {{ 変数名 }} と記述した場合は、デフォルト設定であるテーブル形式の表示となります。また、as_tableas_ul を指定した場合は他のタグで囲う必要があるので注意してください。

スポンサーリンク

フォームからのデータの受け取り

前述の フォームの表示 で説明したようにテンプレートファイルを作成すれば、フォームからフィールドに入力されたデータが送信可能となります。

では、ウェブアプリはどうやってそのデータを受け取れば良いでしょうか?

続いては、その点について解説していきます。

リクエストのデータ属性 POST から取得する

1つ目の方法は、リクエストのデータ属性 POST から取得する方法となります。

下記ページでも解説していますが、ウェブアプリがリクエストを受け取った際、Django フレームワークを介してビューの関数が実行されることになります。そして、ビューの関数が実行される際に、リクエストは HttpRequest のサブクラスのインスタンスとしてビューの関数に引数で渡されることになります。ここからは、この HttpRequest のサブクラスのインスタンスを受け取る引数の引数名を request として説明していきます。

Djangoのビューの機能についての解説ページアイキャッチ 【Django入門3】ビュー(View)の基本とURLのマッピング

フォームから送信されてきたデータは、この request のデータ属性から取得することが可能です。

リクエストのメソッドが POST である場合、フォームから送信されてきたデータが request のデータ属性 POST にセットされた状態でビューの関数が実行されることになります。そのため、フォームから送信されてきたデータは request.POST から取得することが可能となります。

この request.POSTQueryDict という “辞書のサブクラスのインスタンス” となります。なので、request.POST.get('フィールド名') や request.POST['フィールド名'] からフォームの フィールド名 に対応するフィールドに入力されたデータを取得することが可能です。要は辞書と同じようにデータを取得することが可能です。

request.POSTから各種フィールドのデータを取得する様子

つまり、前述で紹介した UserForm の場合、各フィールドに入力されて送信されてきたデータは、下記のような処理によって取得することができることになります。

フォームから送信されたデータの取得例1
name = request.POST.get('name') # or request.POST['name']
age = request.POST.get('age') # or request.POST['age']
comment = request.POST.get('comment') # or request.POST['comment']

cleaned_data から取得する

もう1つの方法は、フォームクラスのインスタンスのデータ属性 cleaned_data から取得する方法になります。ただし、特定の手順を踏まないとフォームクラスのインスタンスにはデータ属性 cleaned_data が追加されません。この特定の手順とは「フォームクラスのコンストラクタに request.POST を引数指定してインスタンスを生成する」と「そのインスタンスに is_valid メソッドを実行させる」になります。

まず、フォームクラスのインタスタンスの生成 で説明したように、フォームクラスのコンストラクタには引数として辞書や辞書のサブクラスのインスタンスを指定することが可能です。そして、その場合、引数に指定したデータの各キーにセットされている値が各種フィールドに設定された状態のフォームクラスのインスタンスを生成することができます。

前述のとおり、フォームから送信されてきたデータは request.POST から取得することができ、request.POST は辞書のサブクラスのインスタンスです。したがって、request.POST を引数に指定してフォームクラスのコンストラクタを実行すれば、ユーザーが各種フィールドに入力して送信してきた情報をセットした状態のインスタンスを生成することができます。そして、このインスタンスに is_valid メソッドを実行させれば、このインスタンスにデータ属性 cleaned_data が追加されることになります。

インスタンスにデータ属性 cleaned_data が追加されれば、後はこの cleaned_data から各種フィールドに入力されたデータを取得すればよいだけです。このデータ属性 cleaned_data は辞書のデータとなりますので、辞書と同じ扱いで各種フィールドに入力されたデータを取得することが可能です。

ということで、前述で紹介した UserForm の場合、このクラスの各フィールドに入力されて送信されてきたデータは、下記のようにしても取得することができることになります。

フォームから送信されたデータの取得例2
form = UserForm(request.POST)

if form.is_valid():
    name = form.cleaned_data.get('name')
    age = form.cleaned_data.get('age')
    comment = form.cleaned_data.get('comment')

この例や、ここまでの説明からも分かる通り、cleaned_data からのデータの取得方法ではインスタンスの生成および is_valid メソッドを行う必要があって若干面倒です。request.POST から直接取得した方が楽ですね。

では、どちらの方法がオススメかというと、それは cleaned_data からの取得の方になります。なぜなら、cleaned_data は妥当性の検証で OK と判断されたデータだからです。そして、この妥当性の検証を行うのが is_valid メソッドとなります。

この妥当性の検証に関しては、次の 妥当性の検証 の章で解説を行いますが、まずは、フォームから送信されてきたデータは cleaned_data から取得するのが望ましいことを覚えておいてください。

妥当性の検証

では、上記で挙げた妥当性の検証とはどのような機能になるのでしょうか?

この点について解説していきます。

妥当性の検証とは

フォームクラスにおける妥当性の検証とは、そのフォームの各種フィールドに入力されたデータに対し、「そのフィールドのデータとして妥当であるかどうかを判断すること」になります。

例えば「消費税込の価格」を算出するウェブアプリの例で妥当性の検証について考えてみましょう。このウェブアプリは、税抜価格入力用のフィールドを持つフォームを表示し、このフィールドで整数を入力してフォームからデータを送信してもらい、その価格に 1.1 を掛けた値をレスポンスとして返却するものとしたいと思います(つまり消費税は 10 % です)。

妥当性の検証の例1

で、この場合、ウェブアプリとしては、税抜価格入力用のフィールドに入力されるデータは整数であって欲しいはずです。価格は基本的に整数ですし、万が一入力されたデータが文字列であると、1.1 を掛ける時に例外が発生することになります。これは避けたい…。つまり、この税抜価格入力のフィールドに入力されるデータとして妥当なのは「整数」ということになります。

なんですが、ユーザーがフィールドに整数を入力してくれるとは限りません。例えばてきとうに abcde のような文字列が入力されるかもしれません。ユーザーは、必ずしもウェブアプリで扱うデータとして妥当なものをフィールドに入力してくれるとは限らないのです…。

そして、ユーザーが、税抜価格入力用のフィールドに文字列を入力してフォームからデータを送信した場合、前述の通り、ウェブアプリでは例外が発生することになります。

妥当性の検証の例2

このようなことを防ぐためには、そのデータを扱う前に、まず、そのフィールドに入力されたデータが整数であるかどうかを判断する必要があります。そして、整数でない場合は、送信されてきたデータは使用せず、例えばユーザーに再度フォームを送信してもらうためにフォームの再表示を行うようなことが必要となります。

妥当性の検証の例3

こんな感じで、各種フィールドに入力されたデータが、そのフィールドのデータとして妥当であるかどうかを判断することが妥当性の検証となります。

もし、この妥当性の検証を実施せずに送信されてきたデータを使用すると、そのデータがウェブアプリが扱うデータとして妥当なものではなく、そのデータ利用時に例外が発生するようなこともあり得ます。また、悪意あるユーザーがウェブアプリを攻撃するようなデータを送信してくるかもしれません。そういったデータをウェブアプリで扱わないようにするためにも、フォームから送信されてきたデータは、必ず妥当性の検証を行ってから扱うようにする必要があります。

スポンサーリンク

is_valid メソッドと cleaned_data

そして、この妥当性の検証を行うメソッドが、フォームクラスの is_valid メソッドになります。

前述のように、request.POST を引数に指定してフォームクラスのコンストラクタを実行すれば、ユーザーが各種フィールドに入力して送信してきたデータをセットした状態のインスタンスを生成することができます。そして、このインスタンスに is_valid メソッドを実行させれば、その各種フィールドに入力されたデータの妥当性の検証が実施されることになります。そして、妥当であると判断された場合には True が、妥当でないと判断された場合には False が返却されます。

さらに、is_valid メソッドを実行したインスタンスにはデータ属性 cleaned_data が追加され、この cleaned_data には妥当であると判断されたフィールドのデータのみがセットされることになります(cleaned_data は辞書であり、妥当であると判断されたフィールドのフィールド名をキーとする要素が追加される)。

そのため、is_validTrue を返却した場合のみ cleaned_data からデータを取得するようにすれば、妥当であると判断されたフィールドのデータのみを取得でき、安全にウェブアプリでデータを扱うことができることになります。

前述のとおり、request.POST から取得できるデータは、ユーザーがフィールドに入力したデータそのものとなり、そのデータがウェブアプリで扱うのに妥当かどうかは検証されていません。なので、is_valid メソッドを実行せずに request.POST から取得したデータをウェブアプリで扱うと例外が発生する可能性もあり危険です。

request.POSTとcleaned_dataの違いを表す説明図

とにかく、データの送信を受け付けるようなウェブアプリでは、クライアントからどんなデータが送信されてくるかわからないことを前提にして動作すべきで、送信されてきたデータを安全に扱うためには、is_valid での妥当性の検証と cleaned_data からのデータの取得が必要となります。

「妥当である」の判断基準

前述の通り、is_valid メソッドを利用することでフィールドへの入力データの妥当性の検証を行うことが可能です。でも、「妥当である」とは具体的にどういうことを指すのでしょうか?

Field のサブクラスの種類によって決まる判断基準

まず、この「妥当である」と判断するための基準の1つはフィールドの種類によって決まります。

フォームクラスの定義方法 で解説したように、フォームクラスにはフィールドを持たせることができ、このフィールドを持たせるためにクラス変数の定義を行いました。そして、クラス変数をフィールドとして扱うためには、Field のサブクラスのインスタンスとしてクラス変数を定義する必要がありました。

フィールドとしてクラス変数を定義する方法の説明図

フィールドに入力されたデータが妥当であるかどうかの判断基準は、この Field のサブクラスの種類によって異なることになります。

IntergerField の場合は整数を扱うフィールドですので、整数以外が入力されている場合、is_valid メソッドでは False が返却されることになります。また、EmailField の場合はメールアドレスを扱うフィールドですので、入力された文字列に @ が存在しない場合や、ドメイン名として不適切なものに対しては is_valid メソッドで False が返却されることになります。例えば、test@abc であれば、@ は存在しているものの、@ の後ろ側がドメイン名として不適切(. がない)ので is_valid メソッドで False が返却されることになります。

このように、is_valid メソッドで行われる妥当性の検証では、フィールドごとに対して検証が行われ、さらに妥当であるかどうかの判断基準は Field のサブクラスの種類によって異なります。

コンストラクタに指定する引数によって決まる判断基準

また、フィールドは “Field のサブクラスのインスタンスとしてクラス変数を定義する” ことでフォームクラスに追加することができますが、このインスタンス生成時のコンストラクタへの引数指定によって、妥当であるかの判断基準を開発者自身で決めることもできます。

例えば、フォームクラスの定義方法 で紹介した UserForm には age フィールドが存在しています。この age フィールドは IntegerField クラスのインスタンスなので、そもそも整数以外の入力は妥当でないと判断されます。ただ、この age フィールドは年齢の入力を受け付けるものなので、そもそも負の値は年齢としては妥当ではありません。また、これは断言できない話かもしれませんが、現在では年齢が 200 歳を超える人は存在しないと思われます。

そのため、age フィールドでは入力されたデータが整数であっても、0 未満の整数や 200 を超える整数は妥当でないと判断して良いでしょう。このような場合、IntergerField のコンストラクタに max_value=200min_value=0 を指定することで、妥当であるという判断基準に「200 以下であること」と「0 以上であること」を追加するようなことが出来ます。

他にも CharField のコンストラクタに max_length と min_length 引数を指定することで、妥当であるという判断基準に入力文字列の最大長と最小長を加えることも可能です。

バリデーターによる判断基準の定義

ここまでの妥当であることの判断基準は Django フレームワークで定められたものになります。つまり、一般的なウェブアプリに適用される判断基準となります。

ただ、ウェブアプリで扱うデータの妥当性は当然ウェブアプリによって様々です。そのため、ウェブアプリ独自の判断基準で妥当性の検証を行いたくなることも多いです。例えば、ユーザー名に対して「頭文字が大文字の半角アルファベット」を満たさないものは妥当でないと判断したい場合や、パスワードに対して「3文字以上の記号が使用されている」を満たすものだけ妥当であると判断したいような場合もあります。

こういった、ウェブアプリ独自の判断基準を設けるために Django では『バリデーター』を定義することができ、このバリデーターによってウェブアプリ独自の判断基準を妥当性の検証に追加することができます。要は、バリデーターとは妥当性検証時に実行される関数です(関数だけでなく、Django フレームワークに用意されたクラスを継承して定義することも可能です)。

例えば、ユーザー名のフィールドへの入力データが半角アルファベットと半角数字のみ、かつ、最初の文字がアルファベットである文字列のみを妥当であると判断したい場合、下記のようなバリデーターを定義してやれば良いです。

バリデーターの例
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

def check_name(name):
    if not name.isalnum() or not name.isascii():
        raise ValidationError(_('nameにはアルファベットと数字のみ入力可能です'))
    if not name[0].isalpha():
        raise ValidationError(_('nameの最初の文字はアルファベットにしてください'))

コードの詳細な説明は省略しますが、ポイントは妥当でないと判断した際には ValidationError 例外を発生させることになります。バリデーターは is_valid メソッドの中から実行され、ValidationError の例外が発生した場合に妥当でないと判断して is_valid から False が返却されることになります。

また、ValidationError の引数にはユーザーに示す注意文を指定します。この注意文の意味合いについては、次の フォームの再表示と注意文の表示 で解説します。

さらに、Field のサブクラスのコンストラクタの引数に validators=[バリデーターの関数名] を指定すれば、is_valid メソッド実行時に validators 引数に指定された関数での妥当性の検証が追加されることになります。

例えば、UserFormname フィールドに対し、上記のバリデーター check_name での妥当性の検証を追加したい場合は、下記のように UserForm の定義を変更を変更してやれば良いことになります。

クラス変数の定義
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

def check_name(name):
    if not name.isalnum() or not name.isascii():
        raise ValidationError(_('nameにはアルファベットと数字のみ入力可能です'))
    if not name[0].isalpha():
        raise ValidationError(_('nameの最初の文字はアルファベットにしてください'))

class UserForm(forms.Form):
    name = forms.CharField(validators=[check_name])
    age = forms.IntegerField()
    email = forms.EmailField()

validators 引数に指定するオブジェクトはリスト形式やタプル形式である必要があります。また、複数指定することも可能です。

このように validators 引数を指定すれば、is_valid メソッドを実行した際に、name フィールドにセットされているデータを引数として上記の check_name 関数が実行されるようになります。ValidationError の例外が発生した場合は、is_valid メソッドが False を返却することになりますし、例外が発生しなかった場合は True が返却され、さらに cleaned_data のキー 'name' の値として name フィールドに入力されたデータにセットされます。

ここまでの説明のように、「妥当である」の判断基準は、フォームクラスのクラス変数の定義の仕方、具体的には Field のサブクラスやコンストラクタへの引数の指定の仕方で変わります。したがって、開発者は、ウェブアプリに適した妥当性の検証が実施されるようにクラス変数を定義する必要があります。

フォームの再表示と注意文の表示

さて、フィールドに入力されたデータが妥当である場合、ビューとしてはそのデータを利用して何かしらの処理を行ってレスポンスを返却すれば良いことになります。

では、フィールドに入力されたデータが妥当でない場合、ビューはどのような動作を行えば良いでしょうか?

これも結局ウェブアプリ次第ではあるのですが、再度ユーザーからの入力を受け付けるため、フォームを再表示するための HTML をレスポンスとして返却するケースが一番多いと思います。

つまり、フィールドに入力されたデータが妥当でない場合は、基本的には フォームの表示 で示した手順で HTML を生成し、それをボディとするレスポンスを返却することになります(ビューの関数から render 関数の返却値を返却する)。

妥当性の検証の例3

ただし、妥当でないデータが入力されたからといって、フィールドを全て空にしたフォームが再度表示されるのはユーザーに対して不親切です。ユーザーは再度全てのフィールドを入力する必要があって面倒です。

また、再表示されるフォームに「妥当でないと判断された理由」を示してあげないと、もしかしたらユーザーは妥当でないと判断された理由が理解できないかもしれません。

上記の理由から、各種フィールドにはユーザーが事前に入力したデータをセットしておき、さらに入力したデータが妥当でないと判断された理由を示すための注意文を表示するような HTML を生成して返却したあげた方が親切です。

なんだか面倒そうですが、実はこれらは非常に簡単に実現することが可能です。

結論としては、ビューが render 関数の引数に指定するコンテキストに対し、テンプレートファイルがフォームとして参照するキーの値として「is_valid メソッドを実行したフォームクラスのインスタンス」を指定すれば良いだけになります。要は、テンプレートファイルに is_valid メソッドを実行したインスタンスを埋め込めば良いです。

前回ユーザーが入力したデータをセットした状態のフィールド&注意文を表示する方法の説明図

まず、そもそも is_valid メソッドを実行するためにはユーザーから入力されたデータを各種フィールドにセットしたフォームクラスのインスタンスを生成する必要があります。これは、具体的にはフォームクラスのコンストラクタを引数に request.POST を指定して実行してやれば良いだけになります。

フォームからのデータの受け取り で説明したように、このようにして生成したフォームクラスのインスタンスは、各種フィールドに送信されてきたデータがセットされた状態になっている、すなわち、ユーザーがフィールドに入力したデータがセットされた状態になっています。そのため、これをテンプレートファイルに埋め込んで HTML を生成すれば、ページとして表示した際に各種フィールドにユーザーが入力したデータがセットされた状態で表示されることになります。

フォームを再表示する際に前回ユーザーが入力したデータがセットされた状態になっていることを示す図

さらに、このインスタンスに is_valid メソッドを実行させ、さらに is_valid メソッドの中で妥当でないと判断された際には、is_valid メソッドを実行したインスタンスに自動的に「妥当でないと判断した理由を示す注意文」がセットされることになります。

そして、この状態のインスタンスをテンプレートファイルに埋め込めば、ページとして表示した際に、その注意文も一緒に表示されることになります。

フォームを再表示する際に注意文が表示される様子

このように、is_valid メソッドが False を返却した際に「is_valid メソッドを実行したフォームクラスのインスタンス」を HTML に埋め込んでやれば、ユーザーが事前に入力したデータが各種フィールドに反映され、さらに入力したデータが妥当でないと判断された理由を示すための注意文が表示されるような HTML を生成することが可能となります。

また、注意文としては、基本的には Django フレームワークで定義されている文章が表示されることになります。ただし、バリデーターによる判断基準の定義 で紹介したバリデーターによって妥当でないと判断された場合は、注意文として ValidationError の引数に指定した文字列が表示されることになります。したがって、バリデーターを定義する場合は、ユーザーに「妥当でないと判断された理由」を理解してもらえるよう、分かりやすい文字列を ValidationError の引数に指定しておく必要があります。

スポンサーリンク

クライアント側とサーバー側での検証

ここまでは is_valid メソッドで実施する妥当性の検証について解説してきました。is_valid メソッドはウェブアプリ内で実行されることになりますが、実は、ウェブアプリ内だけでなくウェブブラウザでも妥当性の検証が実施されることになります。次は、このウェブブラウザで実施される妥当性の検証について説明します。

クライアント側での妥当性の検証

前述の通り、妥当性の検証に関してはウェブブラウザでも実施されることになります。

ウェブアプリはサーバー上で動作するため、ウェブアプリで行われる妥当性の検証はサーバー側での検証と言えます。それに対し、ウェブブラウザはサーバーに対するクライアントですので、ウェブブラウザで行われる妥当性の検証はクライアント側の検証と言えます。

クライアント側とサーバー側とで妥当性の検証が行われる様子

実際にウェブブラウザでフォームのフィールドにデータを入力してみれば分かると思いますが、フォーム内のフィールドでは入力可能なデータが決められており、これに反するデータは入力できない or 送信できないようになっています。つまり、ウェブブラウザでも妥当性の検証が実施され、妥当でないデータが送信できないようになっています。

例えば、Chrome の場合は入力不可なデータを送信しようとすると下の図のようなメッセージが表示されて送信できません。

ウェブブラウザで行われる妥当性の検証

このウェブブラウザでの妥当性の検証は HTML に従って実施されます。より具体的には、入力フィールド要素となる HTML のタグには type 属性が設定され、この type 属性に従ってウェブブラウザでの妥当性の検証が行われることになります。

MEMO

ウェブブラウザでの妥当性の検証は JavaScript で行うことも多いです

例えば、type 属性に "email" が指定されている場合は入力されたデータに @ が含まれていないとメールアドレスとして妥当でないと判断されるようになっていますし、type 属性に "number" が設定されている場合は数字以外は妥当でないと判断されるようになっています。そして、妥当でないと判断された場合は送信ボタンを押してもリクエストが送信されないようになっています。

また、フォームの表示 で説明したように、フォームクラスのインスタンスを render 関数を利用して HTML に埋め込んだ場合、入力フィールド要素となる HTML タグはフォームクラスの定義に従って生成されることになります。そして、前述の type 属性は、そのフォームクラスに持たせたフィールドの種類、すなわち Field のサブクラスの種類によって自動的に決定されて HTML に埋め込まれることになります。

したがって、フォームクラスの定義に従った妥当性の検証は is_valid メソッドだけでなく、ウェブブラウザでも実施されることになります。つまり、妥当性の検証はサーバー側とクライアント側の2段階で実施されるようになっています。

サーバー側での妥当性の検証は不要?

では、妥当性の検証をわざわざクライアント側とサーバー側の両方で行う必要はあるのでしょうか?

クライアント側で妥当性の検証が行われるのであれば、サーバー側には妥当でないデータは送信されてこないようにも思えます。そう考えると、ウェブアプリでの検証、つまりサーバー側の検証は不要なようにも思えるかもしれません。ですが、結論としてはサーバー側でも検証は行うようにしたほうが良いです。

その理由の1つ目は、ウェブブラウザでの妥当性の検証に用いられる判断基準と is_valid メソッドでの判断基準が異なるからになります。例えば、メールアドレスとして妥当であるかどうかの判断基準は、少なくとも現在 Chrome と is_valid メソッドで異なります。また、バリデーターを利用して細かな判断基準を設ける場合、この判断基準は is_valid メソッドにのみ適用されることになり、ウェブブラウザではその判断基準での妥当性の検証が行われません。

理由の2つ目は、そもそも「クライアント側で妥当性の検証が行われる保証がない」からになります。

ウェブアプリに対してデータが送信可能なのはウェブブラウザだけではありません。例えば、下記のページで紹介している curl コマンド等を利用すればウェブブラウザ等を利用せずにウェブアプリにデータを送信することができます。そして、このような場合はウェブブラウザを使用しないため、当然ながらウェブラウザでの妥当性の検証なしにデータが送信されてくることになります。

curlコマンドの紹介ページアイキャッチ curlコマンドの紹介

また、ウェブブラウザが利用される場合であっても、表示されるページの基になっている HTML を変更すれば妥当でないデータを送信することが出来てしまいます。例えば input タグの type 属性を "email" から "text" に変更してしまえば、@ の存在しない文字列も送信できるようになります。

このように、クライアント側の妥当性の検証は必ず行われるとは限りません。そして、この場合、妥当性の検証が行われていないデータがウェブアプリに送信されてくることになります。もしかしたら、悪意あるユーザーがウェブアプリを攻撃するような悪意あるデータを送信してくるかもしれないです。

悪意あるユーザーがウェブアプリを攻撃する様子

そういったクライアント側の妥当性の検証が行われずにデータが送信されてくる可能性がある以上、ウェブアプリでも妥当性の検証を行うことは必須と考えた方が良いです。なので、フォームをウェブアプリで扱うのであれば、フォームから送信されてきたデータに対しては必ず is_valid メソッドによる妥当性の検証を実施し、さらに送信されてきたデータは cleaned_data から取得するようにしましょう!

お試しでウェブアプリを開発するだけであればそこまで神経質になる必要もありませんが、世界中に対してウェブアプリを公開するような場合、自身のウェブアプリを守るためにも妥当性の検証は必須となります。

ビューでフォームクラスを扱う

ここまでフォームクラスや、フォームクラスを利用したフォームの表示の仕方・送信されてきたデータの受け取り方に、さらには妥当性の検証について解説してきました。

ただ、解説が断片的になっているため、ここでフォームクラスを扱う流れをビューの関数の処理の流れについて説明し、ここまで解説してきた内容の復習を行いたいと思います。

フォームクラスを扱うビュー

下記にフォームクラスを扱うビューの関数の例を示します。このビューの関数は、フォームクラスを扱う関数の典型的な処理の流れを踏襲したものになります。つまり、フォームクラスを扱うビューの関数は、基本的には下記のような処理の流れが実現できるように実装すればよいことになります。

フォームクラスを扱うビュー
from django.shortcuts import render
from django.http.response import HttpResponse
from .forms import UserForm

def form_view(request):
    if request.method == 'POST':
        form = UserForm(request.POST)
        if form.is_valid():
            name = form.cleaned_data.get('name')
            age = form.cleaned_data.get('age')
            email = form.cleaned_data.get('email')
            return HttpResponse('Name : ' + name + '<br>Age : ' + str(age) + '<br>Email : ' + email)
    else:
        form = UserForm()

    context = {
        'form' : form
    }
    return render(request, 'app1/form.html', context)

この form_view 関数が利用している UserForm は、フォームクラスの定義方法 で定義した UserForm クラスであることを想定としています。また、アプリ名は app1 で、form.htmlフォームの表示 で最後に示した form.html を利用することを想定した実装例となっています。

フォームクラスのインポート

まず最初のポイントとなるのがフォームクラスのインポートになります。上記では同じアプリ内の forms.py で定義された UserForm をインポートするため、from .forms import UserForm を行うようになっています。

メソッド及び妥当性の検証結果による処理の分岐

また、form_view 関数では、下記の3つのケースを考慮し、それぞれで異なる処理が行われるように実装しています。このように、フォームクラスを扱う関数では、少なくとも下記の3つのケースを考慮し、それぞれのケースで適切な処理が行われるように実装する必要があります。

  • メソッドが POST かつ is_valid() の結果が True
  • メソッドが POST かつ is_valid() の結果が False
  • メソッドが GET

この “メソッド” に関しては、request.method から取得することが可能です。

ここからは、上記の3つのパターンの処理の流れについて解説していきます。

メソッドが POST かつ is_valid() の結果が True の場合の処理

form_view 関数において、メソッドが POST かつ is_valid() の結果が True の場合に実行される処理は、下図の青字の部分となります。

メソッドがPOSTかつis_validの結果がTrueの場合に実行される処理

基本的に、フォームクラスを扱うビューの関数で最初に実施するのはフォームクラスのインスタンスの生成になると考えて良いです。そして、フォームクラスのインタスタンスの生成 や フォームからのデータの受け取り で解説したように、request.method'POST' の場合は、request.POST を引数として UserForm のコンストラクタを実行してインスタンスを生成することになります。

さらに、request.method'POST' の場合は、生成したインスタンスに対して is_valid メソッドを実行させて妥当性の検証を行う必要があります。ここまでの処理は、フォームクラスを扱うビューの関数で request.method'POST' の場合に実施する共通的な処理と考えて良いと思います。

そして、is_valid メソッドの結果が True ということは、フォームから送信されてきた各種フィールドのデータが妥当であると判断されたことになり、インスタンスのデータ属性 cleaned_data から送信されてきたデータを取得することができるようになります。後は、この cleaned_data から各種フィールドのデータを取得し、これらのデータを利用して実現したい処理を実行すれば良いことになります。

上記の form_view 関数では、説明を簡単にするために、これらのフィールドのデータを結合した文字列をボディとする HttpResponse のインスタンスを返却しているだけになりますが、本来であれば、ここで、データベースに各種フィールドのデータを保存するような処理を実行することになります。データベースへのデータの保存等に関してはモデルを利用することになり、このモデルに関しては次の連載である下記ページで詳しく解説していきます。

モデルの解説ページアイキャッチ 【Django入門6】モデルの基本

また、メソッドが POST かつ is_valid() の結果が True の場合でもビューの関数からはレスポンスの返却が必要であることを忘れないように注意してください。私は結構忘れます…。

メソッドが POST かつ is_valid() の結果が False の場合の処理

ただし、送信されてきたデータがウェブアプリで扱うものとして妥当でない場合もあります。その場合は、is_valid メソッドが False を返却することになります。そして、この場合は、下図のオレンジ字で示す処理が実行されるようになっています。

メソッドがPOSTかつis_validの結果がFalseの場合に実行される処理

is_valid メソッドを実行するまでの流れは、先ほど説明した処理の流れと同じです。is_valid メソッドが False を返却するということは、フォームから送信されてきたデータが妥当でないということなので、データの再送信をしてもらうためにフォームを再表示することが多いと思います。上図のオレンジ字の後半部分では、このフォームの再表示を行っています。

このフォームの再表示用の HTML の生成は、フォームの表示時に利用するテンプレートファイルに is_valid メソッドを実行したフォームクラスのインスタンス(request.POST を引数に指定して生成したフォームクラスのインスタンス)を埋め込むことで実現できます。で、このインスタンスは form となるため、コンテキストの 'form' キーの要素の値に form をセットし、このインスタンスがテンプレートファイルの {{ form }} 部分に埋め込まれるようにしています。

これにより、フォームクラスのインタスタンスの生成妥当性の検証 で説明したように、render 関数によって前回入力されたデータがフィールドにセットされた状態&注意文が表示される状態のフォームが埋め込まれた HTML が生成されることになり、それをボディとするレスポンスが返却されることになります。

メソッドが GET の場合の処理

最後にメソッドが GET の場合の処理について説明します。

この場合は、フォームクラスのインタスタンスの生成 や フォームの表示 で説明したように、ビューとしては UserForm のコンストラクタを “引数なし” で実行してインスタンスを生成し、それをコンテキストにセットして render 関数を実行すれば良いだけになります。

これらを行っているのは、下の図における緑字の部分になります。

メソッドがGETの場合に実行される処理

フォームクラスを扱うビューの関数の処理の流れの説明は以上となります。

少し条件分岐が増えてビューの関数が複雑になりますので、下記の3つのケースの場合の処理が意図したものになっているのかをしっかり確認しながら実装することをオススメします。

  • メソッドが POST かつ is_valid() の結果が True
  • メソッドが POST かつ is_valid() の結果が False
  • メソッドが GET

スポンサーリンク

掲示板アプリでフォームを利用してみる

では、ここまで説明してきた内容を踏まえて、実際にフォームの利用例を示していきたいと思います。

この Django 入門 に関しては連載形式となっており、ここでは前回のテンプレートの解説(下記ページ)で 掲示板アプリでテンプレートを利用してみる で作成したウェブアプリに対してフォームを導入する形で、フォームの利用例を示していきたいと思います。

Djangoのテンプレートの解説ページアイキャッチ 【Django入門4】テンプレート(Template)の基本

より具体的には、「ユーザー登録フォーム」と「コメント投稿フォーム」の2つのフォームを用意し、ユーザーによってユーザー登録とコメント投稿を行うことが可能なウェブアプリに仕立てていきます。

ウェブアプリの例で作成するフォーム

今までは、単純に元々リストに存在していたデータを表示していただけでしたが、これらのフォームを追加することで、ユーザーからデータを受け付け、それをウェブアプリに反映するような動作が可能となります。

掲示板アプリのプロジェクト一式の公開先

この Django 入門 の連載を通して開発している掲示板アプリのプロジェクトは GitHub の下記レポジトリで公開しています。

https://github.com/da-eu/django-introduction

また、前述のとおり、ここでは前回の連載の 掲示板アプリでテンプレートを利用してみる で作成したプロジェクトをベースに変更を加えていきます。このベースとなるプロジェクトは下記のリリースで公開していますので、必要に応じてこちらからプロジェクト一式を取得してください。

https://github.com/da-eu/django-introduction/releases/tag/django-template

さらに、ここから説明していく内容の変更を加えたプロジェクトも下記のリリースで公開しています。ソースコードの変更等を行うのが面倒な場合など、必要に応じて下記からプロジェクト一式を取得してください。

https://github.com/da-eu/django-introduction/releases/tag/django-form

ユーザー登録フォーム

まずはユーザー登録フォームを定義していきます。

現状、views.py では User クラスを定義し、このクラスのインスタンスで各ユーザーを管理するようにしています。そして、このインスタンスのリスト users によって全ユーザーを管理するようにしています。

ユーザーの管理
class User:
    def __init__(self, id, username, email, age):
        self.id = id
        self.username = username
        self.email = email
        self.age = age

users = [
    User(1, 'Yamada Taro', 'taro@yamada.jp', 18),
    User(2, 'Yamada Hanako', 'hanako@yamada.jp', 22),
    User(3, 'Sato Saburo', 'saburo@sato.jp', 53),
    User(4, 'Takahashi Shiro', 'shiro@takahashi.jp', 64)
]

ですので、現状の本ウェブアプリにおいて、ユーザー登録とは User クラスのインスタンスを作成し、さらにそのインスタンスを users に追加することであると言えます。つまり、下記を実行すればユーザー登録が実現できることになります。

ユーザー登録
id = len(users) + 1
user = User(id=id, username=???, email=???, age=???)
users.append(user)

これだけの話ではあるのですが、今回はユーザーがユーザー登録を行えるようにするのですから、ユーザーが User クラスのインスタンスのデータ属性となる「名前(name)」・「メールアドレス(email)」・「年齢(age)」を指定できるようにする必要があります。つまり、上記における ??? の部分に関しては、ユーザーからの入力によって指定できるようにする必要があります(ID に関しては自動採番できるので指定不要です)。

これを実現するため、ユーザー登録フォームを追加し、このフォームでユーザーが名前・メールアドレス・年齢の3つのデータの入力が行えるようにしていきます。そして、これらの入力されたデータを User クラスのインスタンスのデータ属性に指定するよう実装していきます。

forms.py でのフォームの定義

ということで、名前・メールアドレス・年齢の3つのデータの入力受付が可能なユーザー登録フォームを定義していきたいと思います。

フォームの定義先のファイルはforum フォルダの中にある forms.py になります。が、このファイルは自動生成されないため、自身で作成する必要があります。なので、まずは forms.py を作成してください。

さらに、この作成した forms.py に下記のような定義を行います。RegisterForm がユーザー登録フォームを実現するフォームクラスとなります。

ユーザー登録フォームの定義
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

def check_username(username):
    if not username.isalnum() or not username.isascii():
        raise ValidationError(_('usernameにはアルファベットと数字のみ入力可能です'))
    if not username[0].isalpha():
        raise ValidationError(_('usernameの最初の文字はアルファベットにしてください'))

class RegisterForm(forms.Form):
    username = forms.CharField(validators=[check_username])
    email = forms.EmailField()
    age = forms.IntegerField(min_value=0, max_value=200)

ポイントは、RegisterForm をフォームクラスとして定義する点、すなわち、forms.Form を継承したクラスとしてフォームを定義する点と、ユーザーからの入力受付を行いたいデータに合わせてフィールドを設ける点になります。

上記の場合、usernameemailage の3つに対するフィールドが設けられ、それぞれ文字列、メールアドレス、整数の入力を受け付けるフィールドとなります。この入力を受け付けるデータの型に関しては、Field クラスのどのサブクラスを利用するかによって決まります。

また、age に関しては forms.IntegerField() の引数によって、受付可能な整数の値が 0200 に限定されるようになり、仮にウェブアプリが 0200 の整数を受け取った場合、is_valid メソッド実行時に行われる妥当性の検証で NG と判断されるようになります。

さらに、バリデーターによる判断基準の定義 で説明したバリデーターを利用し、username に半角アルファベット・数字以外が含まれる場合と username の頭文字が半角アルファベット以外の場合に、is_valid メソッド実行時に username フィールドへの入力値が妥当でないと判断されるようにしています。

ユーザー登録フォーム表示用のテンプレートの作成

次は、先ほど定義した RegisterForm を表示するためのテンプレートを作成していきたいと思います。

今回も、下記ページの 掲示板アプリでテンプレートを利用してみる で作成した users.htmlcomment.html 同様に、base.hml を継承する形でテンプレートファイルを作成していきます。つまり、今回作成するテンプレートファイルでは、base.hml の継承および、title ブロックと main ブロックのコンテンツの記述を行う必要があります。

Djangoのテンプレートの解説ページアイキャッチ 【Django入門4】テンプレート(Template)の基本

今回は、ユーザー登録フォーム表示用のテンプレートファイルを register.html という名前のファイルで作成していきたいと思います。これも上記ページで解説しているように、テンプレートファイルを設置するフォルダは下記となります。

testproject/forum/templates/forum/

また、フォームの表示 で説明したように、フォームの各種フィールドのタグの生成は、テンプレートファイルにフォームのインスタンスを埋め込むことで実現できます。今回は、各種フィールドをテーブル形式で表示を行うようにしたいと思います。

以上を踏まえ、上記フォルダにユーザー登録フォーム表示用のテンプレートファイル register.html を新規作成し、下記のように実装してください。このテンプレートファイルは、コンテキストの 'form' キーの値としてフォームクラスのインスタンスを form として受け取ることを想定したものになっています。

ユーザー登録フォーム表示用テンプレート
{% extends "forum/base.html" %}

{% block title %}
ユーザー登録
{% endblock %}

{% block main %}
<h1>ユーザー登録</h1>
<form action="{% url 'register' %}" method="post">
{% csrf_token %}
    <table class="table table-hover">{{ form.as_table }}</table>
    <p><input type="submit" class="btn btn-primary" value="送信"></p>
</form>
{% endblock %}

このテンプレートファイルから HTML が生成された場合、表示されるページには上記で紹介した3つのフィールドとボタンが表示されることになります。そして、ボタンが押下された際には、フィールドに入力されたデータが POST メソッドのリクエストとして送信されることになります。リクエスト先は 'register' の名前が付けられた URL となります(URL の名前の設定は urls.py で行う必要がありますが、これに関しては後ほど説明します)。

したがって、ボタン押下によって送信されるリクエストを受け取るビューの関数は、メソッドが POST であることを考慮し、POST の場合は受け取ったデータを利用するように作成しておく必要があります。このユーザー登録フォームの場合は、受け取ったデータを User クラスのインスタンスのデータ属性に設定するために利用することになります。ビューの関数の実装例については、後述の ビューの変更 で紹介します。

スポンサーリンク

コメント投稿フォーム

続いてコメント投稿フォームを定義していきます。

先ほどとは役割の違うフォームを定義することになりますが、やることはほとんどユーザー登録フォームの時と同じです。

まず、現状、views.py では Comment クラスを定義し、このクラスのインスタンスで各コメントを管理するようにしています。そして、このインスタンスのリスト comments によって全コメントを管理するようにしています。

コメントの管理
class Comment:
    def __init__(self, id, text, date):
        self.id = id
        self.text = text
        self.date = date

comments = [
    Comment(1, 'おはようございます', datetime.datetime(2023, 3, 4, 12, 4, 0)),
    Comment(2, 'いい天気ですねー', datetime.datetime(2023, 4, 5, 16, 21, 0)),
    Comment(3, '明日もよろしくお願いします', datetime.datetime(2000, 12, 25, 1, 55, 0)),
    Comment(4, 'おやすみなさい', datetime.datetime(2024, 1, 1, 1, 37, 0)),
    Comment(5, '山路を登りながら、こう考えた。智に働けば角が立つ。情に棹させば流される。意地を通とおせば窮屈だ。とかくに人の世は住みにくい。', datetime.datetime(2012, 10, 8, 3, 49, 0)),
]

ですので、現状のウェブアプリにおいて、コメント投稿とは、下記のように Comment クラスのインスタンスを作成し、さらにそのインスタンスを comments に追加することであると言えます。

コメント投稿
id = len(comments) + 1
date = datetime.datetime.now()  
comment = Comment(id=id, text=???, date=date)
comments.append(comment)

この Comment クラスのインスタンスのデータ属性となる text (本文) をユーザーから入力受付できるようにコメント投稿フォームを追加していきます。その他の id (ID) や date (投稿日) に関しては、ユーザーからの入力受付によってではなく、上記のように自動で設定するようにしたいと思います。

forms.py でのフォームの定義

ということで、コメント本文のみのデータの入力受付が可能なコメント投稿フォームを定義していきたいと思います。

先ほど forum フォルダの中にある forms.pyRegisterForm を定義しましたが、次は下記のように RegisterForm の下側に PostForm の定義を追加します。コメント本文のみをフィールドとして持つフォームを定義するため、PostForm のクラス変数は text = forms.CharField() のみで良いです。

コメント登録フォームの定義
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

def check_username(username):
    if not username.isalnum() or not username.isascii():
        raise ValidationError(_('usernameにはアルファベットと数字のみ入力可能です'))
    if not username[0].isalpha():
        raise ValidationError(_('usernameの最初の文字はアルファベットにしてください'))

class RegisterForm(forms.Form):
    username = forms.CharField(validators=[check_username])
    email = forms.EmailField()
    age = forms.IntegerField(min_value=0, max_value=200)

class PostForm(forms.Form):
    text = forms.CharField()

コメント投稿フォーム表示用のテンプレートの作成

次は、先ほど定義した PostForm を表示するためのテンプレートを作成していきたいと思います。このテンプレートファイルの名前は post.html としたいと思います。

このテンプレートに関しても、作り方はユーザー登録フォームの時とほぼ同じですので、詳細な説明は省略します。

コメント登録フォーム表示用のテンプレートファイル post.html の実装例は下記のようになります。このテンプレートファイルに関しても、フォームクラスのインスタンスを form として受け取ることを想定したものになっています。

ユーザー登録フォーム表示用テンプレート
{% extends "forum/base.html" %}

{% block title %}
コメント投稿
{% endblock %}

{% block main %}
<h1>コメント投稿</h1>
<form action="{% url 'post' %}" method="post">
    {% csrf_token %}
    <table class="table table-hover">{{ form.as_table }}</table>
    <p><input type="submit" class="btn btn-primary" value="送信"></p>
</form>
{% endblock %}

1点補足をしておくと、比較してみれば分かる通り、register.htmlpost.html の作りはほぼ同じです。そのため、タイトルや見出し、リクエストの送信先(form タグの action 属性への指定値)等を変数として参照するようにしてやれば、用意が必要なテンプレートファイルは1つのみとなります。

今回は2つのテンプレートファイルを用意していますが、テンプレートファイルの作りを工夫すれば1つのファイルのみで様々なページ表示が実現可能であることは覚えておくと良いと思います。

ビューの変更

テンプレートファイルも完成したので、次はビューの変更を行なっていきます。

ここでは、ユーザー登録フォームを扱う register_view 関数と、コメント投稿フォームを扱う post_view 関数の定義の追加を行います。そして、これらの関数の処理の流れは ビューでフォームクラスを扱う で説明したとおりになります。ただし、それぞれの関数で扱うフォームクラス、フォーム表示時に使用するテンプレートファイル、送信されてきたデータが妥当であると判断された時の処理が下記のように異なります。

関数 フォームクラス テンプレートファイル データが妥当な場合の処理
register_view RegisterForm register.html ユーザーの追加
ユーザー一覧へのリダイレクト
post_view PostForm post.html コメントの追加
コメント一覧へのリダイレクト

逆に言えば、「扱うフォームクラス」「フォーム表示時に使用するテンプレートファイル」「送信されてきたデータが妥当であると判断された時の処理」以外に関してはフォームクラスを扱う関数の処理は同じになるということになります。なので、フォームクラスを扱う関数を作成する場合は、ビューでフォームクラスを扱う で示した form_view 関数や、次に紹介する register_viewpost_view を参考にしていただければ簡単に作成することが出来ると思います。

実際に、上記で説明した register_viewpost_view を追加した views.py は下記のようになります。

views.py
from django.http import Http404
from django.shortcuts import redirect, render
from .forms import RegisterForm, PostForm

class User:
    def __init__(self, id, username, email, age):
        self.id = id
        self.username = username
        self.email = email
        self.age = age

import datetime
class Comment:
    def __init__(self, id, text, date):
        self.id = id
        self.text = text
        self.date = date

users = [
    User(1, 'Yamada Taro', 'taro@yamada.jp', 18),
    User(2, 'Yamada Hanako', 'hanako@yamada.jp', 22),
    User(3, 'Sato Saburo', 'saburo@sato.jp', 53),
    User(4, 'Takahashi Shiro', 'shiro@takahashi.jp', 64)
]

comments = [
    Comment(1, 'おはようございます', datetime.datetime(2023, 3, 4, 12, 4, 0)),
    Comment(2, 'いい天気ですねー', datetime.datetime(2023, 4, 5, 16, 21, 0)),
    Comment(3, '明日もよろしくお願いします', datetime.datetime(2000, 12, 25, 1, 55, 0)),
    Comment(4, 'おやすみなさい', datetime.datetime(2024, 1, 1, 1, 37, 0)),
    Comment(5, '山路を登りながら、こう考えた。智に働けば角が立つ。情に棹させば流される。意地を通とおせば窮屈だ。とかくに人の世は住みにくい。', datetime.datetime(2012, 10, 8, 3, 49, 0)),
]

def index_view(request):
    return redirect(to='comments')

def users_view(request):
    context = {
        'users' : users
    }

    return render(request, 'forum/users.html', context)

def user_view(request, user_id):
    if user_id > len(users) or user_id < 1:
        raise Http404('Not found user')

    user = users[user_id - 1]

    context = {
        'user' : user
    }

    return render(request, 'forum/user.html', context)

def comments_view(request):
    context = {
        'comments' : comments
    }

    return render(request, 'forum/comments.html', context)

def comment_view(request, comment_id):
    if comment_id > len(comments) and comment_id < 1:
        raise Http404('Not found comment')

    comment = comments[comment_id - 1]

    context = {
        'comment' : comment
    }

    return render(request, 'forum/comment.html', context)

def register_view(request):

    if request.method == 'POST':

        form = RegisterForm(request.POST)
        if form.is_valid():
            id = len(users) + 1
            username = form.cleaned_data.get('username')
            email = form.cleaned_data.get('email')
            age = form.cleaned_data.get('age')
            
            user = User(id=id, username=username, email=email, age=age)
            users.append(user)

            return redirect('users')

    else:
        form = RegisterForm()
    
    context = {
        'form': form
    }

    return render(request, 'forum/register.html', context)

def post_view(request):
    if request.method == 'POST':
        form = PostForm(request.POST)

        if form.is_valid():
            id = len(comments) + 1
            text = form.cleaned_data.get('text')
            date = datetime.datetime.now()   

            comment = Comment(id, text, date)
            comments.append(comment)

            return redirect('comments')
    else:
        form = PostForm()

    context = {
        'form': form,
    }

    return render(request, 'forum/post.html', context)

前述のとおり、追加した register_viewpost_view の処理の流れは ビューでフォームクラスを扱う で説明したものと同じなので、不明な点があれば ビューでフォームクラスを扱う を参照していただければと思います。

一点ポイントを挙げると、それは、ビューの関数は必ずレスポンス(HttpResponse のインスタンス、もしくは HttpResponse のサブクラスのインスタンス)の返却 or 例外の発生を行う必要があるという点になります。これは、リクエストのメソッドが POST である場合も同様です。

上記の関数 register_view・関数 post_view では、リストへの追加を行なった後に redirect の返却値を return するようにしており、redirect の返却値は HttpResponse のサブクラスのインスタンスとなるため、上記を満たすことができるようになっています。

別に redirect を利用してリダイレクトをしなくても、例えば「ユーザーを登録しました」や「コメントの投稿に完了しました」などのデータの送信の受付が完了した旨を伝える HttpResponse のインスタンスを返却するのでも良いです。重要なことは、ビューの関数は必ずレスポンスの返却 or 例外の発生を行う必要がある点になります。

その他の変更

ビューの関数が新たに追加されたので、この関数が実行されるようにするために、次は urls.py を変更して URL のマッピングを行いたいと思います。

urls.py の変更

今回は、下記のように forum フォルダの urls.py を変更したいと思います。urlpatterns の後ろ側に2つ要素を追加しています。

forum/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index_view, name='index'),
    path('comments/', views.comments_view, name='comments'),
    path('comment/<int:comment_id>/', views.comment_view, name='comment'),
    path('users/', views.users_view, name='users'),
    path('user/<int:user_id>/', views.user_view, name='user'),
    path('register/', views.register_view, name='register'),
    path('post/', views.post_view, name='post'),
]

この forum フォルダの ursl.py はリクエスト先の URL が /forum/ から始まる場合に参照される URL マッピングの設定となりますので、上記のように変更を行うことで、URL が /forum/register/ の場合は views.pyregister_view が実行され、URL が /forum/post/ の場合は views.pypost_view が実行されるようになります。

base.html の変更

あとは、ナビゲーションバーからユーザー登録フォーム表示用のページとコメント投稿フォーム表示用のページに遷移できるよう、これらのページへのリンクをナビゲーションバーに追加しておきたいと思います。

ナビゲーションバーは base.html で作成していますので、base.html の変更を行います。

といっても、実は base.html には既に上記の2つのページへのリンクが追加されています。ただし、現状では、これらのリンク部分はコメントアウトで無効化されているため、これらのコメントアウト部分を有効化してやる必要があります。

ということで、現状の base.html からコメントアウト部分を有効にするよう、下記の2行を削除してください。

削除する行1
{% comment 'フォーム説明後に追加' %}
削除する行2
{% endcomment %}

削除すれば、base.html は下記のようなものになると思います。そして、これによりナビゲーションバーに項目が追加され、その項目をクリックすることでユーザー登録ページやコメント投稿ページに遷移できるようになります。

base.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
    <header>
        <nav class="navbar navbar-expand navbar-dark bg-primary">
            <ul class="navbar-nav mr-auto">
                <li class="nav-item">
                    <a class="nav-link navbar-brand" href="{% url 'index'%}">掲示板</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'users'%}">ユーザー一覧</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'comments'%}">コメント一覧</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'post'%}">コメント投稿</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'register'%}">ユーザー登録</a>
                </li>
            </ul>
        </nav>
    </header>
    <main class="container my-5 bg-light">
        {% block main %}{% endblock %}
    </main>
</body>
</html>

ここまで説明してきたように、表示するページの追加を行うためには、テンプレートファイルの追加・ビューの関数の追加・urls.py の変更が必要となります。また、テンプレートファイルから追加したページへの遷移を行うためには、他のテンプレートファイルにリンク等を追加する必要があります。この辺りの一連の流れも覚えておいておくと良いと思います!

スポンサーリンク

動作確認

最後に動作確認を行なっておきましょう!

開発用ウェブサーバーの起動

まずは、ウェブアプリにアクセスできるよう、Django 開発用ウェブサーバーを起動してください。この開発用ウェブサーバーの起動は、manage.py が存在するフォルダ(プロジェクトの testproject フォルダの中)で下記コマンドを実行することで実現できます。

% python manage.py runserver

これにより、ウェブブラウザ等のクライアントからリクエストを受け取るウェブサーバーが起動することになります。

ページ表示の確認

次はウェブブラウザを開き、アドレスバーに下記 URL を指定します。

http://localhost:8000/forum/comments/

そうすると、下の図のようなページが表示されると思います。注目していただきたいのがナビゲーションバーで、先程の base.html の変更により、ナビゲーションバーに コメント投稿 リンクと ユーザー登録 リンクが追加されていることが確認できると思います。

ナビゲーションバーにリンクが追加されている様子

まず、コメント投稿 リンクをクリックしてみましょう!そうすると、下図のようにコメント投稿フォームが表示されることになります。

表示されるコメント投稿フォーム

これは、リンクのクリックによって /forum/post/ に対するリクエストが送信され、さらにその結果 post_view が実行されたからになります。post_view ではリクエストのメソッドが “POST 以外” の場合の処理が実行されることになります。

続いて、text フィールドに適当な文字列を入力して 送信 ボタンをクリックしてみてください。次は、下図のようにコメント一覧ページが表示されると思います。注目すべきはコメント一覧の最後のコメントで、ここに先ほど投稿したコメントが表示されているはずです。

コメント一覧に投稿したコメントが表示されるようになった様子

この場合も、先ほど コメント投稿 リンクをクリックした時と同様に 送信 ボタンのクリックによって /forum/post/ に対するリクエストが送信され、さらにその結果 post_view が実行されることになります。ただし、送信 ボタンクリック時に送信されるリクエストのメソッドは POST に設定されているため、今回の場合は post_view ではリクエストのメソッドが POST の場合の処理が実行され、投稿されたコメントが新たにリスト comments に追加されることになります。

さらに、リストへの追加処理後に redirect('comments') が実行されて /forum/comments/ へのリダイレクトが行われます('comments' は、urs.py で定義された /forum/comments/ の URL の名前となります)。そして、その結果、コメント一覧ページが表示されることになります。このコメント一覧ページでは、リスト comments の各要素の text が表示されるようになっていますので、先程投稿したコメントが表示されていることは、フォームからのデータの送信によってコメントが comments に追加されたことを意味し、これによってフォームからのデータの送信、および、そのデータに基づいたコメントの追加が実現できていることが確認できたことになります。

また、投稿したコメントのリンクをクリックすれば、そのコメントの詳細も表示することができます。投稿日には、投稿した日が表示されていることも確認できるはずです。

投稿したコメントの詳細が表示される様子

同様に、ナビゲーションバーの ユーザー登録 リンクをクリックすれば、ユーザー登録フォームが表示されます。

表示されるユーザー登録フォーム

ここで、登録したいユーザーの情報を各種フィールドに入力し、さらに 送信 ボタンをクリックすれば、今度はユーザー一覧ページが表示され、一番下の項目として、先ほど登録したユーザーが表示されるようになっていることが確認できるはずです。

ユーザー一覧に登録したユーザーが表示されるようになった様子

この時のウェブアプリ内の動作については、コメントの投稿時と同様なので説明は省略します。

妥当性の検証の動作確認

ここまでの例のように、コメントの投稿やユーザーの登録に成功しているのは、フィールドに入力したデータが、フィールドに入力するデータとして妥当であると判断されているからになります。

次は、妥当性の検証で NG と判断されるような例を紹介します。

再度 ユーザー登録 リンクをクリックし、今度はユーザー登録フォームのメールアドレス入力フィールドに abc@def と入力して 送信 ボタンをクリックしてみてください(他のフィールドには適当なものを入力しておいてください)。

そうすると、再度ユーザー登録フォームが表示されるはずです。そして、表示されたフォームのメールアドレス入力フィールドには注意文が表示されているはずです。

妥当性の検証でNGになった場合に再表示されるフォーム

このようなフォームが表示されるのは、メールアドレスの入力フィールドに入力された文字列がメールアドレスとして妥当でないため、つまり、register_view での is_valid メソッド実行の結果が False となったためです。register_view では、is_valid メソッド実行の結果が False であった場合は、その is_valid メソッドを実行したインスタンスをテンプレートファイルに埋め込んでフォームを再表示するような処理を行うようになっています。そのため、フォームの再表示と注意文の表示 で説明したように、前回入力されたフィールドの値が設定された状態のフォームと注意文の表示が行われるようになっています。

ちなみに、メールアドレスとして妥当であるかどうかの検証が行われるのは、RegisterForm のクラス変数 email を EmailField のインスタンスとして定義しているからです。例えば CharField のインスタンスとして定義した場合は、単に文字列として妥当かどうかの検証のみが行われ、メールアドレスとして妥当であるかどうかの検証までは行われません。

次は、再度ユーザー登録フォームを表示し、username フィールドに 7Yamada と入力して 送信 ボタンをクリックしてみてください(他のフィールドには適当なものを入力しておいてください)。

この場合は、送信 ボタンクリック後に下図のようなフォームが表示されると思います。username フィールドに注意文が表示されている点がポイントになります。

usernameフィールドにバリデーターから出力される注意文が表示される様子

今回定義した RegisterFormusername フィールドには自前のバリデーター関数 check_username を設定しており、この check_username では入力された文字列の先頭が半角アルファベットでない場合に妥当でないと判断するようになっています。入力した 7Yamada は先頭が数字になっているため、妥当でないと判断され、その際に ValidationError の引数に指定した注意文の文字列がフォームに埋め込まれて表示されることになります。この結果より、バリデーターの効果や動作についても理解していただけるのではないかと思います。

このように、フォームを利用することでコメントの投稿やユーザーの登録等をユーザーから受け付けることができるようになります。今まで示してきた例では、ユーザーができることはページを表示すること、つまりウェブアプリから情報を受け取るだけでしたが、フォームを利用することでユーザーからウェブアプリにデータを送信し、そのデータをウェブアプリに反映することができるようになります。

ユーザーがウェブアプリにデータを送信する様子

もちろん、コメントの投稿やユーザーの登録だけでなく、画像を投稿したり、ツイートしたり、ツイートに「いいね!」をしたりするような操作も、フォームによって実現することが可能です。

ただし、ここで作成したウェブアプリでは、単にユーザーから送信されてきたデータをリストに追加しているだけなので、ウェブアプリを再起動したりすると追加したデータが消えてしまいます…。

これを解決するためには、例えばファイルにデータを保存しておくような対策も考えられますが、ウェブアプリではデータベースを利用し、データベースにデータを保存することが多いです。そして、このデータベースを利用する際に活躍するのが、次に説明するモデルとなります。モデルを利用することで、様々なデータを保存したり、その保存したデータを取得したりすることができるようになり、さらに開発できるウェブアプリの幅が広がります。是非次の連載の解説も読んでみてください!

まとめ

このページでは、Django におけるフォームについて解説を行いました!

フォームを利用することで、ユーザーからのデータの送信を受け付け、それをウェブアプリに反映することができるようになります。そして、それにより、より面白みのある&価値のあるウェブアプリに仕上げることが可能です。

ただし、その反面、悪意あるユーザーから悪意あるデータが送信されるなど、フォームを設けることでウェブアプリへの攻撃を受ける可能性も出てきます。そのため、ウェブアプリでは受け取ったデータをそのまま扱うのではなく、しっかり妥当性の検証を行なってからデータを扱うことが重要となります。また、このページでは詳細は解説しませんでしたが、Django では CSRF 検証を行うことで攻撃を防ぐような対策も取られています。この辺りの仕組みが利用できる点もフレームワークの強みと考えられると思います。

次の連載では、MTV の M にあたるモデルについて説明を行います。モデルを利用することでデータベース管理が簡単に行うことができるようになり、ウェブアプリからのデータの保存や取得も実現できるようになります!下記から次の連載ページに遷移できますので、是非次のページも読んでみてください!

モデルの解説ページアイキャッチ 【Django入門6】モデルの基本

同じカテゴリのページ一覧を表示