DjangoのForm(CreateView、UpdateViewなど)について

  • 18
    いいね
  • 2
    コメント

DjangoのFormについて

この記事は Django Advent Calender 2016の 6日目の記事です。

Djangoにおけるクラスベース汎用ビューの入門と使い方サンプル の続きみたいなものです。

はじめに

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となります。
つまり、保存すればそのままモデルに反映されるということです。

まずは、モデルとフォームの例を書いてみます。

models.py
from django.db import models


class Person(models.Model):

    name = models.CharField(max_length=255)
    age = models.IntegerField(default=25) # 25歳に戻りたい

私はいつも、モデル名+Formというクラス名にしています。

forms.py
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のような機能で、フォームから送信されたデータで受け入れるフィールドを設定します。
今回はnameage以外は送信されても無視します。
これにより、悪意あるデータが送信されても無視するはずです。

class Metaには、受け入れるフィールドではなく、無視するフィールドを設定することもできます。

class Meta:
    exclude = ("name", )

のようにします。

だがやるな

ドキュメントにもありますが、fields = "__all__"とすると全部受け入れます。
フィールドが多いモデルだとすごく楽ですよね。
だがやるな
これが鉄則です。

普通のForm

ModelFormはモデルに紐付いています。
多くの場合はModelFormを使うのではないかと思います。

しかし、検索など、特定のモデルに紐付かないフォームもあります。
そういった場合は普通のフォームを使います。

form.py
from django import forms

class SearchForm(forms.Form):
    word = forms.CharField(max_length=250)

モデルの定義と似ていますので、迷うことはないはずです。
ここで設定したmax_lengthなどがバリデーションで使用されるのは言うまでもありません。

CreateView を使ってテンプレートへフォームを表示する

ModelFormFormを継承して、自分のフォームクラスを作成しました。

次に、views.pyにクラスベース汎用ビューを使ってテンプレートへフォームを表示させてみます。
今回はCreateViewにしてみました。

views.py
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を登録します。

urls.py
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がよしなに処理してくれます。

テンプレートへの記述

さて、テンプレートへ記述します。

base.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Person</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
form.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>タグを使用することもあるかもしれません。
その時は、

form.html
{% 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

※ テーブルで表示しています。
ss1.png

冬の寒空のようなページが表示されたと思います。
しかし、成功時にリダイレクトするURLは存在しないのでエラーになります。
ちゃんと一覧ページを表示したいと思います。

余談:リストビュー

Formと直接関係ないので余談にしてみました。
トップページにリスト表示させるだけです。

コードだけ載せていきます。(一応上で書いている内容もすべて入っています)

views.py
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"

urls.py
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),
]
list.html
{% 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に書いていきたいと思います。

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.pyurl関数にname="hogehoge"を追加しました。
urlにつける名前です。テンプレートで使おうと思っています。

urls.py
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にアクセスできるように修正してみます。

list.html
{% 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です。

urls.py
url(r'^update/(?P<pk>\d+)$', PersonUpdateView.as_view(), name="update"),

先程、name="update"と追記しました。
updateという名前を使って、urlを生成することができるようになります。

list.html
{% 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メソッドが呼ばれます。

つまり、バリデーションが通ったときとそうでないときで処理を分岐することができます。

具体的にはメソッドをオーバーライドします。
また、汎用ビューの機能を実行するため、スーパークラスのメソッドも呼び出します。

views.py
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にはフォーム周りも結構便利に使えるようになっています。

余談

建設業の事務員やってますが、ウェブ業界に転職したいなぁ。。。

この投稿は Django Advent Calendar 20166日目の記事です。