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

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

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

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

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

フォームの例

Django では、『フォームクラス』を定義することでフォームを簡単に扱うことが出来ます。このページでは、このフォームクラスを利用したフォームの扱い方について解説していきます。

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

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

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

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

これらのページでは実装例も紹介していますが、どちらにおいても受け付けるリクエストはページ表示に対するものだけでした。つまり、リクエストのメソッドが 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】テンプレートの基本

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

この辺りの解説は、後述の フォームクラス で解説していきます。

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

続いて、フォームとメソッドの関係性について解説していきます。

ウェブアプリは、主にクライアントからの操作をリクエストによって受け付けることになります。例えばウェブブラウザでユーザーがリンクをクリックしたり、ボタンを押したりした際にはウェブアプリに対してリクエストが送信され、そのリクエストに応じてウェブアプリが処理を行なったりレスポンスの返却を行なったりします。

そして、このリクエストの内容は、主に URL とメソッドから構成されます。URL ではリクエスト先を指定し、メソッドではリクエストの目的を指定します。

さらに、このメソッドには GETPOST と呼ばれるものが存在し、GET は主にデータの取得を目的とするリクエストに設定され、POST は主にデータの送信を目的とするリクエストに設定されます。

例えば、ログインページの URL が /login/ であれば、ログインページを表示するための HTML を取得する際には .URL に /login/、さらにメソッドに GET が設定されたリクエストがウェブアプリに送信されることになります。

それに対し、ログインページに対してデータを送信したい場合は、URL に /login/、さらにメソッドに POST を設定されたリクエストがウェブアプリに送信されることになります。この際には何かしらのデータがリクエストと一緒に送信されることになります。

メソッドの説明図

メソッドが異なるということは、リクエストの目的が異なるということなので、その目的が達成できるようウェブアプリはメソッドに応じた処理やレスポンスの返却を行う必要があります。

より具体的には、受け取ったリクエストのメソッドが GET の場合は指定された URL のページを表示するための HTML を返却する必要がありますし、POST の場合は送信されてきたデータに応じた処理を実行したり、送信されてきたデータを保存して後からユーザーが利用できるようにしたりする必要があります。

例えば、先程の URL が /login/ の例であれば、リクエスト先が同じ  /login/ であったとしてもメソッドに応じて処理を切り替える必要があります。メソッドが GET の場合は、/login/ に対応したデータ(ログインページ表示用の HTML)を返却する必要があります。

それに対し、メソッドが POST の場合は、送信されてきたデータを利用して /login/ に対応した処理(ログイン処理)を実行する必要があります。

メソッドに応じた処理をウェブアプリが実行する様子

このように、複数のメソッドのリクエストを受け付けるウェブアプリの場合、同じ URL へのリクエストであったとしてもメソッドに応じて処理を切り替えられるようにウェブアプリを開発する必要があります。

前述の通り、フォームからはデータを送信することが可能です。データが送信されるわけですから、フォームから送信されるリクエストは POST メソッドとなります。

そのため、ページを表示するだけのウェブアプリであればリクエストのメソッドが GET であることを前提に開発すれば良かったのですが、フォームを扱ってデータが送信可能なウェブアプリを開発したい場合、リクエストのメソッドが POST である可能性を考慮して開発を行う必要があります。

フォームを扱う流れ

フォームを扱う際に POST メソッドのリクエストを扱う必要がある点については理解していただいたと思いますので、次はフォームを扱う流れについて解説していきたいと思います。

フォームを表示する

フォームを扱うためには POST メソッドだけでなく、GET メソッドのリクエストも扱う必要があります。これは、ユーザーがフォームからデータを送信するためには、事前にフォームを表示する必要があるからになります。

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

フォームを表示するためにまずはGETメソッドのリクエストが送信されてくる様子

また、下記ページでも解説していますが、クライアントからのリクエストを受け取り、さらにレスポンスを返却するのはビューとなります。実際にはウェブサーバー等がリクエストの受け取りとレスポンスの返却を行うのですが、プロジェクト内で考えれば、最初にリクエストを受け取るのはビューとなります。

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

したがって、このリクエストを受け取るビューの関数がフォームを扱う場合、このビューの関数は、まずメソッドが GET のリクエストを受け取り、フォームを表示するための HTML を生成し、そしてそれをボディとするレスポンスを返却する必要があります。

GETメソッドのリクエストに対しHTMLの返却を行う様子

ただし、この生成する HTML にも一工夫が必要です。フォームにはデータ送信用のボタンを設ける必要がありますが、このボタンが押された時に POST メソッドでウェブアプリに対してリクエストを送信するようにしておく必要があります。このようなボタンを作成しておかないと、ユーザーはフォームのフィールドにデータを入力できたとしても、それらのデータをウェブアプリに送信することが出来ません。

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

ボタンを押した際に各種フィールドのデータが POST メソッドでウェブアプリに対して送信できるようにしておけば、ユーザーは表示されたフォームに入力を行ってからボタンを押すだけでリクエストをウェブアプリに対して送信することができるようになります。リクエストのメソッドは POST ですので、各種フィールドのデータも一緒に送信することが出来ます。

フォームから送信されてきたデータを扱う

そして、このボタンが押された際には、フォームから送信されてきたリクエストをウェブアプリが受け取ることになります。この時に受け取るリクエストは POST メソッドとなります。

POSTメソッドのリクエストが送信されてくる様子

リクエストが POST メソッドなのでデータも一緒に送信されてくるはずですので、フォームを扱うビューの関数においては、リクエストが POST メソッドの場合は送信されてきたデータを受け取り、そのデータを利用して処理を行なう必要があります。

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

POSTメソッドのリクエストを受け取った際に受け取ったデータを利用して処理を行う必要があることを示す図

どんな処理を行うにせよ、ここで重要なのは、フォームを扱うビューの関数はメソッドに応じて処理を切り替えられるようにしておく必要がある点になります。ビューの関数では Django フレームワークからリクエストのデータを引数で受け取るようになっており、そのリクエストのデータからメソッドの種類を判断することが出来ますので、この種類によって条件分岐するような処理が必要となります。

また、POST メソッドの場合にもレスポンスの返却は必要なので、この点も忘れないようにしてください。

ここで、フォームから送信されるデータについて一点補足しておくと、送信されるデータはウェブアプリで扱うデータとして妥当でない場合があるので注意してください。例えば、ウェブアプリでは整数を扱いたいのにフォームからはアルファベットの文字列などが送信されてくる可能性もあります。これは可愛い例で、フォームや POST メソッドを扱う怖さは、ユーザーからどんなデータが送信されてくるか分からない点にあります。

そのため、送信されてきたデータを扱う際には、まずデータの妥当性の検証を行うことをオススメします。そして、この検証結果が OK の場合のみデータを保存したり利用したりするようにした方が安全です。

この妥当性の検証については、後述の 妥当性の検証 で詳しく説明したいと思います。

スポンサーリンク

フォームクラス

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

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

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

フォームクラスは基本的に自身で定義して使用することになります。Django に標準で用意されているフォームクラスを利用することも可能ですが、このページでは自身で定義することを前提に解説を進めていきます。

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

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

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

フォームクラスの定義の仕方はいくつかありますが、ここでは一番基本的な Form クラスを継承して定義する方法について解説していきます。

フォームクラスの定義

この Form クラスは djang.forms で定義されるクラスであり、Form のサブクラスとして定義したクラスはフォームとして扱うことができるようになります。ここまで「フォームクラス」という言葉を何回か利用してきましたが、本サイトでは、この Form のサブクラスのことをフォームラスと呼んでいます。

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

下記はフォームクラスの定義例となります。前述の通り、ポイントは Form を継承してクラスを定義する点になります。

Formの継承
from django import forms

class UserForm(forms.Form):
    pass

フィールドの追加

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

このフォームクラスを定義する上で重要なのは、必要なフィールドとしてクラス変数を定義することになります。フォームクラスにフィールドとして定義したクラス変数は、ページ表示時のフォームにおける入力フィールド要素となります。

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

ただし、どんなクラス変数もフィールドとして扱われるわけではないです。フィールドとして扱われるようにするためには、クラス変数を 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 クラスを定義している場合、下記のようにしてインスタンスを生成することができます。そして、この場合、form 変数が生成されたインスタンスを参照することになります。

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

def user(request):
    form = UserForm()

上記のようにフォームクラスのコンストラクタに引数を指定しなかった場合、生成されるインスタンスの各入力フィールドには初期値が設定されることになります。ただし、上記の UserForm クラスでは初期値を設定していないため、各種フィールドには空白が設定されることになります。

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

また、コンストラクタの引数に辞書データ等を指定することが可能です。引数に辞書データ等を指定した場合、引数で指定した辞書データの各キーに対応するクラス変数のフィールドに、それらのキーの値が設定されたインスタンスが生成されることになります。

MEMO

フォームクラスのコンストラクタの引数には QueryDict 型のデータを指定することが多いです

これは、フォームから送信されてきたデータは QueryDict 型のデータとしてビューに渡されることになるからです

QueryDict 型のデータをコンストラクの引数に指定する例に関しては、後述の フォームからのデータの受け取り で紹介します

例えば、UserForm のコンストラクタに下記のように辞書データを引数指定して実行した場合、

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

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

    form = UserForm(data)

下の図のように各フィールドに辞書の値が設定されたインスタンスが生成されることになります。

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

フォームを最初に表示する際には各フィールドは初期値で表示するのが望ましいため、この場合に用意するインスタンスは引数なしでコンストラクタを実行して生成することが多いです。

それに対し、フォームから送信されたデータを受け取った際には、そのデータを引数に設定してコンストラクタを実行してインスタンスを生成することが多いです。この理由については、後述で詳細を解説します。

フォームの表示

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

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

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

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

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

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

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

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

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

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

次は、具体例を考えながら、このフィールドの表示の様子を確認してみましょう!

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

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

def form(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 のインスタンスを設定しているため、この部分は 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 に埋め込むだけでは無く、ボタン等の要素のタグを別途テンプレートファイルに記述する必要があります。また、ボタンが押された際に、各種フィールドに入力されたデータが全て送信されるように HTML を記述する必要があります。

具体的には、フォームクラスを利用してフォームを表示する際には、下記のようにテンプレートファイルを記述する必要があります。

  1. <form></form> を記述する
    • action 属性にリクエスト先の URL をルートパス形式で指定する
    • method 属性に "POST" を指定する
  2. <form></form> の間に {% csrf_token %} を記述する
  3. <form></form> の間に {{ form }} を記述する
  4. <form></form> の間に <input> タグを記述する
    • type 属性に "submit" を指定する

{{ form }}form 部分はテンプレートタグから参照する変数名ですので、コンテキストに用意するキーに合わせて変更しても OK です。

ポイントだけ説明しておくと、まず 1. の <form></form> が1つのフォーム要素となり、このフォーム内で送信ボタンを押すと、action 属性に指定された URL に対して method 属性に指定されたメソッドのリクエストが送信されることになります。そして、この際にはフォーム内のフィールドに入力されているデータが一緒に送信されることになります。

このデータの入力を行うフィールドに関しては 3. によって出力されることになります。

また、4. の <input> タグには type 属性に "submit" が指定されているため、このタグが送信ボタン要素となります。

フォームを扱う流れ でも説明しましたが、フォームのボタンが押された際には POST メソッドのリクエストがウェブアプリに送信されるようにする必要があります。

これを実現するのが、<form> における action 属性と method 属性になります。action 属性にルートパス形式の URL を指定しておけば、フォームの送信元であるウェブアプリにリクエストが送信されることになります。そして、method 属性に "POST" を指定しておけば、<form></form> 内に存在するボタンが押された際に POST メソッドのリクエストが送信されるようになります。

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

つまり、単に {{ 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】ビューの基本と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> タグで囲う必要あり
  • {{ 変数名.as_ul }}:箇条書き形式(<li> タグが出力される)
    • <ul> タグ or <ol> タグで囲う必要あり

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

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

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

では、ウェブアプリはどうやってその情報を受け取れば良いでしょうか?続いては、その点について解説していきます。

reqest.POST から取得する

1つ目の方法は、request.POST から取得する方法となります。

下記ページでも解説していますが、サーバーがリクエストを受け取った際、Django フレームワークを介してビューの関数が実行されることになります。そして、ビューの関数が実行される際に、リクエスト関連のデータは引数 request によってビューの関数に渡されることになります。

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

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

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

さらに、フォームクラスを利用して表示したフォームの場合、フォームに存在する各フィールドの フィールド名 は基本的にフォームクラスに持たせたフィールド(クラス変数)の名前と一致するはずなので、request.POST.get('フィールド名') or 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 から取得する方法になります。フォームクラスのコンストラクタでは引数に QueryDict を指定可能であり、これを指定した場合、その QueryDict の各キーにセットされている値が各種フィールドに設定された状態のインスタンスを生成することができます。

したがって、request.POST をフォームクラスのコンストラクタの引数に指定すれば、ユーザーが各種フィールドに入力して送信してきた情報をセットした状態のインスタンスを生成することができるようになります。

ただし、生成直後のインスタンスはデータ属性 cleaned_data は持っていません。インスタンスにデータ属性 cleaned_data を持たせるためには、インスタンスに is_valid メソッドを実行させる必要があります。

そして、この追加されたデータ属性 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 から取得できるデータは妥当性の検証が行われたデータとなっているからです。この妥当性の検証を行うのが is_valid メソッドとなります。

この妥当性の検証に関しては、次の 妥当性の検証 の章で解説を行いたいと思います。

スポンサーリンク

妥当性の検証

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

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

is_valid メソッドと cleaned_data

前述の通り、ビューの関数では request.POST からフィールドに入力されたデータを取得することは可能です。ですが、この request.POST から取得できるデータはユーザーがフィールドに入力したデータそのものとなります。そのデータがウェブアプリで扱うのに妥当かどうかは検証されていません。

それに対し、cleaned_data から取得できるデータは is_valid メソッドによって妥当性の検証が完了したデータとなります。is_valid メソッドは実行したインスタンスの各種フィールドにセットされたデータが妥当であるかを判断するメソッドで、妥当である場合は True を返却し、妥当でない場合は False を返却します。また、妥当であると判断したデータのみを cleaned_data に格納し、インスタンスのデータ属性に追加します。

妥当性の検証の例として分かりやすいのがメールアドレスになると思います。

例えば、ウェブアプリでユーザー登録のためにメールアドレス入力用のフィールドを持つフォームを表示したとします。このフィールドはメールアドレスの入力を受け付けるためのものですから、このフィールドに入力されたデータはウェブアプリ内でメールアドレスとして扱いたいはずです。

ですが、ユーザーがフィールドに適切なメールアドレスを入力してくれるとは限りません。例えば入力が面倒なので、abcde のような、メールアドレスとして不適切な文字列が入力されるかもしれませんし、数値などを入力されるかもしれません。

ウェブアプリで扱うデータとして妥当でないデータが送信される様子

つまり、フィールドにはメールアドレスとして妥当でないものが入力され、それがウェブアプリに送信されてくる可能性があることになります。

前述の通り、request.POST から取得できるデータはフィールドに入力されたデータそのものになります。したがって、そのデータがメールアドレスとして妥当でない場合があります。

それに対し、フォームクラスのインスタンスのデータ属性 cleaned_datais_valid メソッドの実行によって追加されるものとなります。is_valid メソッドでは、フィールドに入力されたデータとして妥当かどうかの検証が行われ、検証 OK のものだけが cleaned_data にセットされることになります。

したがって、cleaned_data から取得できるデータはフィールドに入力されたデータとして妥当であると判断されたデータとなります。

例えば、先程のメールアドレスの例であれば、メールアドレスとして妥当でないデータが送信されてきた場合、is_valid メソッド実行時に cleaned_data にはそのメールアドレスはセットされず、is_valid メソッドの返却値も False となります。

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

ここまでの説明のように、フィールドにデータを入力するのはユーザーであり、間違ってデータが入力される可能性もありますし、故意に不適切なデータが入力される可能性もあります。

そのため、フォームから送信されてきたデータを扱うウェブアプリでは、送信されてきたデータが扱うデータとして妥当かどうかを判断する必要があります。この判断が妥当性の検証となります。

request.POST からデータを取得する場合は、別途自身で処理を記述して取得したデータに対して妥当性の検証を行う必要がありますし、検証を行わなかった場合は不適当なデータを扱うことになって他の処理でエラーや例外が発生する可能性もあります。

ですが、is_valid メソッドを利用することでフィールドへの入力データの妥当性の検証を行うことができ、さらに cleaned_data からフィールドへの入力データを取得することで、妥当性の検証が OK のデータのみを取得することができるようになります。

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

前述の通り、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 引数を指定することで、妥当であるという判断基準に入力文字列の最大長と最小長を加えることも可能です。

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

先ほど紹介した IntergerField への max_valuemin_value の引数指定の例は非常に単純で分かりやすい例になるかと思います。

ただ、ウェブアプリで扱うデータの妥当性は当然ウェブアプリによって異なります。そのため、ウェブアプリ独自の判断基準で妥当性の検証を行いたくなることも多いです。例えば、ユーザー名の最初の文字をアルファベットに統一したい場合もありますし、ユーザー名は半角文字で統一したい場合もあると思います。

こういった、ウェブアプリ独自の判断基準を設けるために 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=[バリデーターの関数名] を指定するようにします。これにより、妥当性の検証を行う際に利用するバリデーターとして指定した関数等がセットされることになります。上記のバリデーターを利用したい場合は validators=[check_name] を指定することになります。

例えば、UserFormname フィールドで上記のバリデーターを利用する場合は下記のように定義を変更することになります。

クラス変数の定義
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 フィールドに入力されたデータにセットされます。 

スポンサーリンク

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

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

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

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

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

妥当性の検証でNGと判断した場合の再度フォームを表示するためのHTMLを返却する様子

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

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

上記の理由から、各種フィールドにはユーザーが事前に入力したデータをセットしておき、さらに入力したデータが妥当でないと判断された理由を示すための注意文を表示するような 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 に従って行われます(別途 JavaScript を用意して検証するようなことも可能です)。

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

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 メソッドにより実現することが出来ます。

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

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

ここまでフォームクラスやフォームの表示・フォームから送信されたデータの扱い、さらには妥当性の検証について解説してきました。

ここで、ここまでの解説をまとめる意味も込めて、フォームクラスを扱う側となるビューの関数の実装例を紹介しておきます。

スポンサーリンク

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

下記はフォームクラスを扱うビューの関数の一例になります。

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

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

def form(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)

簡単な例にはなりますが、この関数はこのページで解説してきた内容の多くを網羅するものとなっています。

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

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

メソッドに応じた処理の切り替え

また、リクエストのメソッドに応じて処理を切り替えるようになっている点もポイントとなります。リクエストのメソッドは request.method から取得できますので、この request.method に応じて処理を切り替えるようにしています。

POST メソッド時の処理(妥当性の検証 OK の場合)

そして、request.method'POST' の場合は フォームからのデータの受け取り で解説したように request.POST を引数として UserForm のコンストラクタを実行し、生成したインスタンスに is_valid メソッドを実行させています。

is_valid メソッドでは妥当性の検証が行われ、返却値が True の場合は、インスタンスのデータ属性 cleaned_data から送信されてきたデータを取得し、それを利用した処理を行なっています。といっても、上記では単に取得したデータを表示する文字列をボディとする HttpResponse のインスタンスを返却しているだけです。

本来であれば、取得したデータを加工して新たなデータを生成したり、取得したデータをデータベースに保存したりするようなことが多いです。また、HttpResponse のインスタンスも render 関数や redirect 関数を実行して生成する方が良いのですが、フォームの扱いに関係ない部分である&テンプレートファイルを用意するのが面倒なので、今回は楽をして単に HttpResponse のインスタンスの返却を行うようにしています。

上記のような、request.method'POST' かつ is_valid メソッドの返却値が Trueの場合は、下図の青字で示す処理が実行されることになります。

メソッドがPOST・is_validの返却値がTrueの場合に実行される処理

POST メソッド時の処理(妥当性の検証 NG の場合)

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

メソッドがPOST・is_validの返却値がFalseの場合に実行される処理

is_valid メソッドを実行した後はコンテキストを用意して render 関数を実行するだけになりますが、form は request.POST を引数として実行した UserForm のコンストラクタから生成されるインスタンスであり、さらにこのインスタンスは is_valid メソッドを実行しているため、妥当性の検証 で説明したように、render 関数によって前回入力されたデータがフィールドにセットされた状態&注意文が表示される状態のフォームが埋め込まれた HTML が生成されることになります。

GET メソッド時の処理

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

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

他にも、バリデータを用意して更に詳細な妥当性の検証を行うようなことも可能ですが、フォームクラスを扱う際には、まずは上記のような流れを実現するようなビュー関数を作成することを心がけるのが良いと思います!

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

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

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

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

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

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

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

ユーザー登録フォーム

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

現状、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 に下記のような定義を行います。

ユーザー登録フォームの定義
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)

ポイントは、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】テンプレートの基本

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

testproject/forum/templates/forum/

また、フォームの表示 で説明したように、フォームの HTML の埋め込みはテンプレートファイルから変数を参照することで実現可能です。今回は、フォームはテーブル形式で表示を行うようにしたいと思います。

以上を踏まえ、上記フォルダにユーザー登録フォーム表示用のテンプレートファイル 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 でのフォームの定義

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

先ほど forums フォルダの中にある forms.pyRegisterForm を定義しましたが、次は下記のように、RegisterForm の下側に PostForm を定義します。

コメント登録フォームの定義
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つのテンプレートファイルのみの用意で様々なページ表示が実現可能であることは覚えておくと良いと思います。

ビューの変更

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

フォームを扱うビューの関数においては、基本的にはリクエストのメソッドに応じて下記を行うことになります。

  • メソッドが GET:フォームを表示する
  • メソッドが POST:フォームから送信されてきたデータを利用して処理を行う

メソッドが GET である場合は、フォームの表示 で解説したように、ビューの関数からフォーム表示用のテンプレートのパスを指定して render 関数を実行することになります。

また、今回用意したユーザー登録フォームとコメント投稿フォームは両方ともフォームクラスのインスタンスを form で参照することを想定したものになっているため、前者の場合は RegisterForm のインスタンス、後者の場合は PostForm のインスタンスをコンテキストにセットし、そのコンテキストも render 関数の引数に指定する必要があります。

メソッドが POST である場合は、フォームからのデータの受け取り で解説したように、まず妥当性の検証を行い、検証 OK の場合のみデータを取得して “ユーザーの追加” や “コメントの追加” を行うことになります。また、検証 NG の場合は、再度フォームを表示する必要があります。

上記の “ユーザーの追加” とは、具体的には、受け取ったデータをデータ属性にセットした User クラスのインスタンスの生成、および、そのインスタンスのリスト users への追加となります。

また、”コメントの追加” とは、受け取ったデータをデータ属性にセットした Comment クラスのインスタンスの生成、および、そのインスタンスのリスト comments への追加となります。

この辺りのポイントを踏まえると、views.py は下記のように変更を行うことになります。定義したフォームクラスを利用するため、ファイルの先頭部分で RegisterFormPostFormforms.py から import する必要がある点に注意してください。

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)

下記ページの 掲示板アプリでテンプレートを利用してみる で作成した views.py との差異は、前述の import を追加したことと、関数 register_view と関数 post_view を追加した点のみになります。

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

関数 register_view はユーザー登録フォームの表示および、ユーザーの追加を行うビューの関数となります。また、関数 post_view はコメント投稿フォームの表示および、コメントの追加を行うビューの関数となります。

これらの関数は役割は違うものの、作りとしてはほとんど同じです。そして、両方の関数は ビューでフォームクラスを扱う で示したビューの処理の流れを踏襲したものとなっています。

まず、リクエストのメソッドが “POST 以外” の場合は RegisterForm or PostForm のインスタンスを生成し、さらに render を実行して、フォームの表示を行うようにしています。

リクエストのメソッドが POST の場合は送信されてきたデータ(request.POST)からフォームクラスのインスタンスを生成し、is_valid メソッドにより妥当性の検証を行うようにしています。

is_valid メソッドが True を返却した場合は、受け取ったデータが妥当であることになるため、cleaned_data から各種フィールドに入力されたデータを取得し、それに基づいて User or Comment のインスタンスの生成、および、そのインスタンスのリストへの追加を行なっています。

ここでも重要になるのが、ビューの関数は必ずレスポンス(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 に追加されることになります。

さらに、リストへの追加処理後に /forum/comments/ にリダイレクトされ、その結果コメント一覧ページが表示されています。このコメント一覧ページではリスト comments の各要素の text が表示されるようになっており、先程投稿したコメントが comments に追加されたため、先ほど投稿したコメントの text も一緒に表示されるようになっています。

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

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

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

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

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

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

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

妥当性の検証の動作確認

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

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

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

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

これは、メールアドレス入力フィールドに入力された文字列がメールアドレスとして妥当でないためです。つまり、これは is_valid での妥当性検証の結果が NG であったからで、この結果より、フォームの再表示と注意文の表示 で説明したように、フォームの再表示と注意文の表示が行われるようになっていることが確認できます。

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

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

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

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

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

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

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

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

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

まとめ

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

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

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

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

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

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