Python Django入門 初めの1歩からWEBアプリ作成までの流れ その4 フォームの基礎1

今回の記事からから下記のようディレクトリ名を変更しています。

プロジェクトフォルダ名:(旧)django_project => (新)pj

アプリフォルダ名:(旧)django_application => (新)app

DjangoでFormを作成する方法は主に2種類あります。

DjangoでFormを作成する方法

①forms.Formを継承して、自分で一からFormを作成する。

②forms.ModelFormを継承して、モデルが用意してくれているテンプレートからFormを作成する。

①の方法は、コード量が多くなってしまいますが、比較的自由にカスタマイズすることが出来るので柔軟に変更を加えることが出来ます。

②の方法は、既に用意されたテンプレートを流用するのでコード量を減らすことが出来ますが、テンプレートに沿って作成していくことになるため、①の方法と比較すると若干自由度は劣ります。

今回は、①form.Formを継承して、自分で一からFormを作成する方法について解説していきます。

今回作成するアプリの完成イメージは下記のようになります。

①フォーム入力 → ②フォーム送信 → ③フォーム入力内容表示

フォームアプリ完成イメージ
フォームアプリのディレクトリ構成
<主なディレクトリ名>
プロジェクト名:pj
アプリ名:app
<主に編集するファイル>
①forms.py
②テンプレート(base.html, index.html)
③views.py

forms.pyの作成

applicationディレクトリに「forms.py」というファイル名で新規ファイルを作成します。

forms.pyの保存場所

forms.pyにフォームクラスを定義し、このフォームクラスをviews.pyでインスタンス化してテンプレートに渡すという流れになります。

今回は、forms.py内に「CustomerForm」というフォームクラスを定義します。

forms.pyコード
from django import forms
from django.core.exceptions import ValidationError
today = datetime.date.today().strftime('yyyy-mm-dd')

class CustomerForm(forms.Form):
    date = forms.DateField(label='入力日付', initial=today, input_formats=['%Y-%m-%d'])
    last_name = forms.CharField(label="姓")
    first_name = forms.CharField(label="名")
    age = forms.CharField(label="ご年齢")
    gender = forms.fields.ChoiceField(label="性別", choices=(("男性", "男性"),("女性","女性")),required=True,widget=forms.widgets.Select)
    address = forms.CharField(label="住所")
    mail = forms.EmailField(label="メールアドレス")
    title = forms.CharField(label="件名", initial="初回問い合わせ")
    content = forms.CharField(label="お問い合わせ内容", widget=forms.Textarea())

ChiceFieldでは、choices属性にタプル型で選択肢を定義する必要があります。通常は、choicesの中身は外出しで定義しますが、今回は選択肢が少なかったため、コード内にベタ打ちしています。

views.pyでテンプレートにフォームクラスを渡す仕組みを作る

forms.pyで作成したフォームクラス(「CustomerForm」)をテンプレートに渡すための仕組みをviews.pyに作成します。

今回は、データベースへの保存は不要のため、models.pyとの連携を必要としない(=モデルクラスとの連携を必要としない。)「FormView」をクラスベースビューとして使用します。

逆に、データベースへの保存が必要な場合は、models.py内に「ModelForm」を継承したモデルクラスを定義し、それをviews.pyで処理していくことなります。こちらの方法は別途解説記事を作成致します。

(用語解説)モデルクラス

(用語解説)モデルクラス

モデルクラスとは、models.pyに定義されたクラスのことを指します。

(用語解説)クラスベースビュー

(用語解説)クラスベースビュー

クラスベースビューとは、views.pyにおいて関数(def)ではなくクラス(class)でテンプレートに渡すデータを定義する方法です。

クラスベースビューは、Djangoに用意されたViewクラスを継承して作成することになります。

(用語解説)FormView

FormViewとは?

・FormViewとは、models.pyとの連携を必要としない(=モデルクラスとの連携を必要としない。)クラスベースビューである。

・form_class変数に、forms.pyに作成したフォームクラスを設定することでテンプレートへ渡す仕組みが用意されている。

・POSTリクエストを受信した際、自動的にバリデーションが実行される。

・バリデーション成功時、success_url変数に設定したテンプレートに自動的にリダイレクトされる。

・models.pyの使用が必要ないため、データベースへ自動的にデータが保存されることはない。(データベースへの保存が必要な場合は、別途models.pyの設定をするかまたは、ModelFormを継承した方法を検討する。

views.pyコード
from django.views.generic.edit import FormView
from . import forms

# Create your views here.
class Index(FormView):
    form_class = forms.CustomerForm
    template_name = "index.html"
  # success_url = "#" }<= 

テンプレート(htmlファイル)にフォームを表示させる

テンプレートにviews.pyから渡されたフォームを表示させます。

pj/app/templates/base.html
{% load static %}
{% load boost %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    <link rel="stylesheet" href="{% static 'style.css' %}">
    <title>Document</title>
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <a class="navbar-brand" href="/">お問い合わせフォーム</a>
    </nav>
    <div class="container mt-4">
        {% block main %}
        <p>※コンテンツがありません。</p>
        {% endblock %}
    </div>
</body>
</html>
pj/app/templates/index.html
{% extends "base.html" %}
{% block main %}
<h2>フォーム記入欄</h2>
<form method="post">
    {% csrf_token %}
    
    {{ form.as_p }}
    
    <input type="submit" value="変換" class="btn btn-info" />
</form>
{% endblock %}
(用語解説)

{% csrf_token %}

CSRF (クロスサイト・リクエスト・フォージェリ)です。なりすまし送信防止用の記述です。Djangoのフォーム送信ではこの記述が無ければエラーが発生しますので、記述が必須となります。

{{ form.as_p }}

{{ form.as_p }}は、views.pyからテンプレートに渡されたformを表示するための変数タグです。

後程の解説でviews.pyからテンプレートにformを引き渡すコードについては解説致します。

as_pは、formの各要素をpタグで囲むことを指します。

urls.pyの編集

いつもと同様に下記のようにurls.pyを編集します。

pj/pj/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('', include('app.urls')),
    path('admin/', admin.site.urls),
]
pj/app/urls.py
from django.urls import path
from app import views

urlpatterns = [
    path('', views.Index.as_view(), name="index"),
]

ここで、python manage.py runserverでフォームが正しく表示されるか確認しましょう。

下記のように表示されていれば成功です。

views.pyにフォーム送信データの処理方法を記述

pj/app/views.pyのIndexクラスに下記コードを追記します。

FormViewでは、POSTリクエストが送信された際、バリデーションに問題が無ければ最終的にform_valid()メソッドが呼び出されます。但し、form_valid()メソッド自体はクラス変数successful_urlに設定したURLにリダイレクトするだけのメソッドですので、このform_valid()メソッドをオーバーライド(上書き)することで、virews.py内での処理結果をテンプレートに引き渡す仕組みをとることが一般的です。

views.pyコード
    def form_valid(self, form):
        data = form.cleaned_data
        last_name = data["last_name"]
        first_name = data["first_name"]
        age = data["age"]
        gender = data["gender"]
        address = data["address"]
        mail = data["mail"]
        title = data["title"]
        content = data["content"]

        form_data = [last_name, first_name, age, gender, address, mail, title, content]

        ctxt = self.get_context_data(form_data=form_data, form=form)
        return self.render_to_response(ctxt)
(メソッド解説)form_valid()メソッド

form_valid()メソッド

form_valid()メソッドは、FormViewクラスを継承したクラスベースビューが生成するフォームに対して、POSTリクエストが送信された際、バリデーションに問題が無い時に最終的に実行されるメソッドです。

POSTリクエストが送信された際、バリデーションに問題が無ければform_valid()メソッドがコールされます。

form_valid()メソッド自体は、クラス変数successful_urlにリダイレクトするだけのメソッドです。(HttpResponceRedirect)

(クラス変数解説)cleaned_data

cleaned_data

フォームからPOST送信されてきたデータのバリデーションに問題が無い場合、POST送信されたデータがこのcleaned_dataに辞書型で入ります。

逆に、バリデーションに問題があったデータ(例、字数オーバー等)はcleaned_dataに入りません。

バリデーションに問題があったデータも受け取りたい場合は、”form.data”を使用します。

(メソッド解説)get_context_data

get_context_data

クラスベースビューにおいてcontextデータを取得するためのメソッドです。

urls.pyでas_view()メソッドが実行された際に呼び出されます。

取得できる情報は以下3点です。

  1. Pagenation情報(ページ設定している場合)
  2. Quesryset関連(モデルに登録されているデータ)
  3. View情報(views.pyでテンプレートに指定したhtmlの情報が格納されています。)

また、get_context_dataに引数を渡すことでデータを追加することでき、追加されたデータをテンプレートに表示することが出来ます。

追加方法は下記の通りです。

selfでインスタンス化されたクラスを呼び出し、その中にget_context_dataメソッドを使って下記の通り変数を代入します。

self.get_context_data("1.テンプレート側で呼び出す際に使用する変数名"="2.views.py内で定義した変数名")
(メソッド解説)render_to_response

render_to_response

テンプレートをHttpレスポンスで返すメソッドです。

引数にcontextデータを引き渡すことで、テンプレートにcontextデータを反映できます。

テンプレートにviews.pyを通じて送信されてきたフォーム送信データを表示

pj/app/templates/index.htmlにフォームからPOST送信されてきたデータを表示するコードを記述します。

ポイントは、views.py内でオーバーライドしたform_valid()メソッド内で定義したcontext情報(“form_data”)をfor文を用いて下記出している部分です。

また、エラー発生時の表示方法をhtmlの上部に追記しています。

pj/app/templates/index.html
{% extends "base.html" %}
{% block main %}
{% for error in form.non_field_errors %}
<div class="alert alert-danger" role="alert">
  {{ error }}
</div>
{% endfor %}
<div class="row">
    <div class="col-sm">
        <h2>フォーム記入欄</h2>
        <form method="post" action="#">
        {% csrf_token %}
        
        {% for field in form %}
        <p>
            <label>{{ field.label }}</label>
            {{ field }}
        </p>
        {% endfor %}
        
            <input type="submit" value="確認" class="btn btn-info" />
        </form>
    </div>
    <div class="col-sm">
        <h2>フォーム記入内容確認欄</h2>
        {% for data in form_data %}
        <p>
            {{ data }}
        </p>
        {% endfor %}
    </div>
</div>
{% endblock %}

フォームにバリデーションチェックを追加

フォームの件名が30文字よりも多かった場合にエラーが表示されるようにします。

forms.pyコード
    def clean(self):
        data = super().clean()
        title = data["title"]
        if len(title) >= 30:
            raise ValidationError("件名は30文字以内でお願い致します。")
        return data
      # 以下のコードはBootstrapを反映させるためにクラスを設定するコードです。無くても動作しますので追加は任意です。
        def __init__(self, *args, **kwargs):
            for field in self.base_fields.values():
                field.widget.attrs["class"] = "form-control"
            super().__init__(*args, **kwargs)

コメント

タイトルとURLをコピーしました