[Django] Class Based View を使った入力フォームの作り方

##この記事について

Djangoにはモデル定義から動的に画面(HTML)を生成するClass Based Viewという仕組みがあります。

Class Based View で入力フォームを出力すると、モデルのフィールドに対応するバリデーション機能とエラー表示（日本語対応）が暗黙的に追加されます。さらに表示をBootstrapに対応させる追加パッケージ Django-Crispy-Forms を組み合わせることでフォーム作成の作業が大幅に簡略化されます。

他のフレームワークのスキャフォールディングに近い役割ですが、オプション設定を行うフォームクラスを追加して細かい調節ができるので自由度は高いです。

この記事では、入力フォームの作り方の要点をまとめました。
本当に要点のみなので、詳しい点についてはリンク先の参考サイトを見てください。

##サンプルイメージ

```py:models.py

class Item(models.Model):

    name = models.CharField(max_length=50, verbose_name="名前")
    age = models.IntegerField(verbose_name="年齢")
    memo = models.TextField(max_length=200, verbose_name="備考")
    is_checked = models.BooleanField(verbose_name="確認済み")
    check_date = models.DateField(verbose_name="確認日")

```

上記のコードで以下の赤線部分が生成される。
バリデーションとエラーメッセージは日付フィールドに合わせて自動的に設定されたものが使われる。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/239d9cd9-e2a9-55ce-5171-4ee5db1bb6fe.png)


##作成方法
###１．Modelを定義する


####１－１．フィールド

まずDjangoの一般的な方法で、models.pyにModelを定義しフィールドを追加します。
フィールドの型に合わせて出力されるHTMLのinputタグのtype属性が決まります。

**フィールドの種類の例**

|扱う型|フィールド|input type|
|:--|:--|:--|:--|
|文字列|CharField|text|
|長い文字列|TextField|textareaタグ|
|数値|IntegerField|number|
|ブール値|BooleanField|checkbox|

Djangoにはこの他にも大量のフィールドクラスが用意されています。
フィールドによってはmin・max属性などが追加されるものもあります。
IntegerRangeFieldなど、Postgres専用の型もあります。
詳しくは公式サイトを参照してください。

https://docs.djangoproject.com/ja/2.0/ref/models/fields/
https://docs.djangoproject.com/ja/2.0/ref/contrib/postgres/fields/


**選択リストの作り方**
フィールドの「choice」オプションを設定することでinput typeがSelectになります。

```py:models.py

class Item(models.Model):

    YEAR_CHOICES = (
        (10, '10代'),
        (20, '20代'),
        (30, '30代'),
        (40, '40代'),
    )

    age = models.IntegerField(verbose_name="年齢", choices=YEAR_CHOICES)

```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/6085dd16-43de-5826-f1c9-7a8b550daf35.png)


後述するModelFormクラスでwidgetオプションを設定することにより、ラジオボタンに変更することも可能です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/c3a56741-249a-f227-d0ef-ec4fb8017277.png)

####１－２．バリデーション

バリデーションは、まずフィールドに合わせた型チェックが行われ、blankオプションの有無にもとづいて必須チェックが適用されます。そのほか以下のようなバリデーションクラスを追加することができます。

バリデーションクラスの例：

|検証内容|バリデーションクラス|
|:--|:--|
|正規表現によるチェック|RegexValidator 
|最小文字数|MinLengthValidator|
|最大文字数|MaxLengthValidator|
|最小値|MinValueValidator 
|最大値|MaxValueValidator 

サンプルコード

```py:models.py

class Item(models.Model):

    name = models.CharField(
        max_length=50,
        verbose_name="名前",
        validators=[validators.RegexValidator(
            regex=u'^[ぁ-んァ-ヶー一-龠]+\u3000[ぁ-んァ-ヶー一-龠]+$',
            message='氏名は漢字・ひらがな・カタカナのみとし、氏と名の間に全角スペースを入れてください',
        )]
    )

    age = models.IntegerField(
        verbose_name="年齢",
        validators=[validators.MinValueValidator(1)])

    check_date = models.DateField(
        verbose_name="確認日",
        validators=[validators.MaxValueValidator(
            date.today(),
            message='本日以前の日付を入力してください',
        )],    
    )
```

結果

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/33e37186-d968-5c37-7701-148e17badeba.png)


###２．ModelFormを定義する

以下のようにViewを記述することでModelクラスだけでClass Based Viewによる入力フォームが利用できます。

```py:views.py
from django.views.generic.edit import CreateView

class ItemCreateView(CreateView):
    model = Item
    fields = '__all__' #実際は必要なフィールドのみを選択する

```

しかし、Modelの定義のみでは以下のような細かい要件に対応できません。

・Selectリストではなくradioボタンを使う。
・入力画面と変更画面で編集可項目を変える。
・placeholderやclassなどの属性を追加する

そのような場合はModelFormクラスを別途定義します。
以下の例ではプレースホルダの追加と、選択リストからラジオボタンへの変更をしています。

**サンプルコード**

```py:forms.py
class ItemForm(forms.ModelForm):


    class Meta:
        model = Item
        fields = ("__all__")
        widgets = {
                    "name": forms.TextInput(attrs={'placeholder':'紀伊　太郎'}),
                    "age": forms.RadioSelect(),
                    
                  }
```


```py:views.py

class ItemCreateView(CreateView):
    model = Item
    form_class = ItemForm    

```

**結果**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/7dc67a81-8a35-5897-adc2-1fe715b08230.png)

フォームのフィールドも多くの種類が用意されています。公式のページを参考にしてください。
https://docs.djangoproject.com/ja/2.0/ref/forms/fields/


### ３．templeteに埋め込む

あとはテンプレートにフォームタグを埋めこむだけです。
Django-Crispy-Formsを使うとcontrol-group まわりのタグが自動的に付与されるので、Bootstrapでの利用が簡単になります。
Django-Crispy-Forms 設定等は以下のエントリが参考になります。

[Django Formの落穂拾い](https://qiita.com/felyce/items/cc356e836162113aeadd)

**サンプル**

```html:item_form.html
<!--  -->
{% extends "./base.html" %}
<!--  -->
{% load crispy_forms_tags %}
<!--  -->
{% block content %}
{{ form.certifications.errors }}
<div class="container">
    <div class="row">
        <div class="col-12">
            <h1>入力画面</h1>
        </div>
    </div>
    <div class="row">
        <div class="col-12">
            <div class="float-right">
                <a class="btn btn-outline-secondary" href="{% url 'index' %}">戻る</a>
                <button type="submit" class="btn btn-outline-secondary" form="myform">保存</button>
            </div>
        </div>
    </div>
    <!--  -->
    <div class="row">
        <div class="col-12">
            <form method="post" id="myform">
                {%crispy form%}
            </form>
        </div>
    </div>
    <div class="row">
        <div class="col-12">
            <div class="float-right">
                <a class="btn btn-outline-secondary" href="{% url 'index' %}">戻る</a>
                <button type="submit" class="btn btn-outline-secondary" form="myform">保存</button>
            </div>
        </div>
    </div>
</div>
<!--  -->
{% endblock %}
```

Crispy-Formsの利用には、別途事前にBootstrapを読み込む必要があります。

```html:base.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <!-- Required meta tags always come first -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <title>サンプルアプリ</title>
    <!-- Bootstrap CSS -->
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
        crossorigin="anonymous">
</head>

<body>
    <!--  -->
    {% block content %}
    <!--  -->
    {% endblock %}

    <!-- jQuery first, then Tether, then Bootstrap JS. -->

    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
        crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
</body>

</html>
```