DjangoのFormについて
この記事は Django Advent Calender 2016の 6日目の記事です。
Djangoにおけるクラスベース汎用ビューの入門と使い方サンプル の続きみたいなものです。
お知らせ
(ちょっと古いですが)Udemyで入門講座作っています。
興味ある方はこちらから
お知らせここまで
はじめに
Djangoは強力なウェブアプリケーションフレームワークです。
その中でもよく使うと思われるForm周りについて自分が知っていることを書きたいと思います。
初めてのアドベントカレンダーです。
環境
Django 1.10.4
Python 3.5.2
Formとは
Djangoでは、ユーザーからの入力を受け取る機能です。
ただ、それだけではなく
- フォームを表示する(エラーがあればエラー表示)
- ユーザーからフォームから送られたデータがモデルの方などに合致しているかチェック(要はバリデーション)
などの機能を提供しています。
また、一つの画面に複数のformを設置したりすることもできます。
今回は、クラスベースビューを使って楽をしながらformについて考えていみたいと思います。
Formクラス
まずはなにはともあれ、Formクラスが必要です。
Formクラスは、その名の通りForm
を表します。
Form
にはいくつか種類があり、Form
クラスを作る際に継承するベースクラスによって若干違います。
forms.py
をアプリケーション内作成します。
startapp
ではデフォルトで作成しないので、自分で作ればOKです。
基本的にただのモジュールなので、自分でガシガシファイル追加して便利にするのが私のDjangoの使い方です。
django-admin startapp hoge
cd hoge
touch forms.py
ModelForm
Formクラスを作る際に、ModelForm
を継承すると、すなわちモデルに関係したFormとなります。
つまり、保存すればそのままモデルに反映されるということです。
まずは、モデルとフォームの例を書いてみます。
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=255)
age = models.IntegerField(default=25) # 25歳に戻りたい
私はいつも、モデル名+Formというクラス名にしています。
from django import forms
from .models import Person
class PersonForm(forms.ModelForm):
class Meta:
model = Person
fields = ("name", "age")
ModelFormの特徴は、class Meta
にモデルを設定します。
これでモデルと紐つけるわけです。
使用するフィールドの選択
class Meta:
fields = ("name", "age")
としています。
これは、RailsのStrongParamter
のような機能で、フォームから送信されたデータで受け入れるフィールドを設定します。
今回はname
とage
以外は送信されても無視します。
これにより、悪意あるデータが送信されても無視するはずです。
class Meta
には、受け入れるフィールドではなく、無視するフィールドを設定することもできます。
class Meta:
exclude = ("name", )
のようにします。
だがやるな
ドキュメントにもありますが、fields = "__all__"
とすると全部受け入れます。
フィールドが多いモデルだとすごく楽ですよね。
だがやるな
これが鉄則です。
普通のForm
ModelFormはモデルに紐付いています。
多くの場合はModelFormを使うのではないかと思います。
しかし、検索など、特定のモデルに紐付かないフォームもあります。
そういった場合は普通のフォームを使います。
from django import forms
class SearchForm(forms.Form):
word = forms.CharField(max_length=250)
モデルの定義と似ていますので、迷うことはないはずです。
ここで設定したmax_length
などがバリデーションで使用されるのは言うまでもありません。
CreateView を使ってテンプレートへフォームを表示する
ModelForm
やForm
を継承して、自分のフォームクラスを作成しました。
次に、views.py
にクラスベース汎用ビューを使ってテンプレートへフォームを表示させてみます。
今回はCreateView
にしてみました。
from django.views.generic import CreateView, UpdateView
from .models import Person
from .forms import PersonForm
class PersonCreateView(CreateView):
model = Person
form_class = PersonForm
template_name = "form.html"
success_url = "/" # 成功時にリダイレクトするURL
次に、urls.py
にURLを登録します。
from django.conf.urls import url
from django.contrib import admin
from hoge.views import PersonCreateView
urlpatterns = [
url(r'^create$', PersonCreateView.as_view()),
]
いつもの通り、as_view()
とすればCreateViewがよしなに処理してくれます。
テンプレートへの記述
さて、テンプレートへ記述します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Person</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
{% extends "base.html" %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form }}
<button type="submit">save</button>
</form>
{% endblock %}
こんな感じです。
注目してほしいのは、{{ form }}
です。
フォームを表示するだけでしたら、これでOKです。
ものすごく楽ですね。
もし、モデルやフォームが変更されても、テンプレートでは自動的に変更が反映されます。
{% csrf_token %}
ドキュメントが詳しいです。
クロスサイトリクエストフォージェリ (CSRF) 対策 — Django 1.4 documentation
POSTするフォームにはつけておきましょう。
tableで表示したい
きれいに入力欄を表示するため、<table>
タグを使用することもあるかもしれません。
その時は、
{% extends "base.html" %}
{% block content %}
<form method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<button type="submit">save</button>
</form>
{% endblock %}
とします。
表示
さて、フォームを表示するための準備は全て整いましたので、runserver
して、フォームにアクセスしてみましょう。
http://localhost:8000/create
冬の寒空のようなページが表示されたと思います。
しかし、成功時にリダイレクトするURLは存在しないのでエラーになります。
ちゃんと一覧ページを表示したいと思います。
余談:リストビュー
Formと直接関係ないので余談にしてみました。
トップページにリスト表示させるだけです。
コードだけ載せていきます。(一応上で書いている内容もすべて入っています)
from django.views.generic import CreateView, ListView # ListView 追加
from .models import Person
from .forms import PersonForm
class PersonCreateView(CreateView):
model = Person
form_class = PersonForm
template_name = "form.html"
success_url = "/"
# 以下追加
class PersonListView(ListView):
model = Person
template_name = "list.html"
from django.conf.urls import url
from django.contrib import admin
from hoge.views import PersonFormView, PersonCreateView, PersonListView
urlpatterns = [
url(r'^$', PersonListView.as_view()),
url(r'^create$', PersonCreateView.as_view()),
url(r'^admin/', admin.site.urls),
]
{% extends "base.html" %}
{% block content %}
<h1>Person List</h1>
<a href="/create">create person</a>
<ul>
{% for person in object_list %}
<li>名前:{{ person.name }}、年齢:{{ person.age }}</li>
{% endfor %}
</ul>
{% endblock %}
トップページにアクセスすると、現在保存されているPerson
がリスト表示なっていると思います。
動作チェックできそうですね。
UpdateView
CreateView
では基本的なFormの動作を見ていきました。
次はUpdateView
でもう少し詳しく見ていきたいと思います。
UpdateView
はお察しの通り、既存のデータを更新するためのものです。ただし、削除は別にDeleteView
があるので、削除はできません。
いつもの通り、views.py
に書いていきたいと思います。
from django.views.generic import FormView, CreateView, ListView, UpdateView
from .models import Person
from .forms import PersonForm
# 追加
class PersonUpdateView(UpdateView):
model = Person
form_class = PersonForm
template_name = "form.html"
success_url = "/"
# 既存
class PersonCreateView(CreateView):
model = Person
form_class = PersonForm
template_name = "form.html"
success_url = "/"
class PersonListView(ListView):
model = Person
template_name = "list.html"
次に、urls.py
でURLに紐つけます。
今回から、urls.py
のurl
関数にname="hogehoge"
を追加しました。
urlにつける名前です。テンプレートで使おうと思っています。
from django.conf.urls import url
from django.contrib import admin
from hoge.views import PersonFormView, PersonCreateView, PersonListView, PersonUpdateView
urlpatterns = [
url(r'^$', PersonListView.as_view(), name="index"),
url(r'^create$', PersonCreateView.as_view(), name="create"),
url(r'^update/(?P<pk>\d+)$', PersonUpdateView.as_view(), name="update"), # 追加
url(r'^admin/', admin.site.urls),
]
今回追加した部分はこれだけです。
トップページのリストから、ダイレクトにUpdateView
にアクセスできるように修正してみます。
{% extends "base.html" %}
{% block content %}
<h1>Person List</h1>
<a href="/create">create person</a>
<ul>
{% for person in object_list %}
<li><a href="{% url "update" person.id %}">名前:{{ person.name }}</a>、年齢:{{ person.age }}</li>
{% endfor %}
</ul>
{% endblock %}
変更したのは
<li><a href="{% url "update" person.id %}">名前:{{ person.name }}</a>、年齢:{{ person.age }}</li>
です。
urlの動的生成
url関数はリンクを生成する関数です。
Rails でいう link_to
と 〜_path
です。
url(r'^update/(?P<pk>\d+)$', PersonUpdateView.as_view(), name="update"),
先程、name="update"
と追記しました。
updateという名前を使って、urlを生成することができるようになります。
{% url "update" person.id %}
"update"
はurls.py
で指定した名前です。
person.id
はこの場合Person
オブジェクトのIDですので、DB上のプライマリーキーです。
こうすると、"update"
というURLにつけた名前からupdate/
が導かれ、person.id
からプライマリーキーがわかるので、それらを合成してupdate/3
のようなURLを生成します。
Update Viewの動作
UpdateView
は与えられたURLから、自動的にDBに保存してあるデータをロードし、テンプレートに渡します。
※ url(r'^update/(?P<pk>\d+)$', PersonUpdateView.as_view(), name="update"),
の<pk>
がポイントです。
これで、準備はできましたので、トップページのリストから名前をクリックしてアクセスしてみましょう。
また、名前、年齢を変更して実際に変更されるか確認してみてください。
CreateView, UpdateView でのバリデーション
この2つのフォームはバリデーション機能があります。
と言うのは嘘で、バリデーション自体はモデルにあるのですが、バリデーションをviews内で実行しています。
バリデーションの実行は自動で実行されますが、その結果がvalidであればform_valid
メソッドが呼ばれ、invalidであればform_invalid
メソッドが呼ばれます。
つまり、バリデーションが通ったときとそうでないときで処理を分岐することができます。
具体的にはメソッドをオーバーライドします。
また、汎用ビューの機能を実行するため、スーパークラスのメソッドも呼び出します。
class PersonCreateView(CreateView):
model = Person
form_class = PersonForm
template_name = "form.html"
success_url = "/"
def form_valid(self, form):
# do something...
return super().form_valid(form)
do something部分に自分の処理を差し込みます。
たとえば、次のページに「保存しました」などの通知を送ったり、メールを送信したり、slackに通知したりです。
フォームからのデータでゴニョゴニョしたいときは、仮引数のform
を使います。
私は成功・失敗をメッセージフレームワークで通知するぐらいしか使っていません。
逆に、デフォルトでは成功失敗を通知しないので、通知したいときはオーバーライド必須です。
django-bracesというモジュールを使うと、その辺の処理なしにメッセージを追加できます。
必須モジュールだと思います。
まとめ
このようにDjangoにはフォーム周りも結構便利に使えるようになっています。
余談
建設業の事務員やってますが、ウェブ業界に転職したいなぁ。。。