【Django】1ページの中に複数のフォームを作る

# まず結論
クラスベース汎用ビューを使った方法を紹介します。長々と読みたくない方のために結論を言うと、やり方は

- それぞれのフォームのためのフォームクラスを作成する（ `forms.Form` や `forms.ModelForm` クラスを継承したクラスの作成）
- ビュー関数ないで、 `contect` にそれぞれのフォームクラスを `'myform': MyForm(**self.kwargs)` のように追加し、htmlで表示できるようにする
- htmlでは上記の `myform` を使って `{{ myform }}` としてしまえばフォームが自動で作られる（カスタムしたい方はどうぞ！）。また、postするためのボタンに名前をつけておく
- postを受け取る関数で、postのボタンの名前で条件分岐させ、そのとき、form=MyForm()とする。

# 背景
pollsアプリケーションである投票項目についてのページを作った。しかし、あるとき、それぞれの投票について質問や選択肢、図を追加したり変更したり削除する必要が出てきた…

# 環境
- Python 3.7.0
- Django 2.1.4

また、今回はクラスベースビュー（`django.views.generic.なんとか`）を用いることにします。

# ディレクトリ構成
ディレクトリは以下のように構成されています。ここでは、必要な最低限のものを書きます。

mysite/
　├ manage.py
　├ mysite/
　│　　├ settings.py
　│　　└ urls.py
　└ myapp/
　　　　├ urls.py
　　　　├ views.py
　　　　├ forms.py
　　　　├ models.py
　　　　└ templates/
　　　　　　├ base.html
　　　　　　└ myapp/
　　　　　　　　├ mymodel_detail.html
　　　　　　　　└ などいろいろなhtml

# 具体的な状況設定

抽象的にいえば、1つの詳細ページ（`django.views.generic.DetailView`を用いたもの）で複数のフォームがある場合を考えます。

例えばpollアプリケーションの1つの質問に対する（管理者が見れる）詳細ページがあるとしましょう。このとき、モデルは以下のように、質問文、選択肢1、選択肢2からなる簡単なものを考えます。

```Python:models.py
from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200, verbose_name='質問文')
    answer_text_1 = models.CharField(max_length=200, verbose_name='選択肢1')
    answer_text_2 = models.CharField(max_length=200, verbose_name='選択肢2')
    def __str__(self):
        return self.question_text
```

公式のチュートリアルでは、URLが`'<int:pk>/', views.DetailView.as_view()`のページでは

```Python:views.py
from django.views import generic

class DetailView(generic.DetailView):
    model = Question
    template_name = 'myapp/detail.html'
```
とだけ定義されているので、データベースを変更するフォームがないのはもちろん、Questionモデルに紐づいたものしか表示できません。

# フォームクラスの作成
フォームを使うので、基本にならって`forms.py`でフォームを作成します。ここでは、モデルに紐づくので、`forms.ModelForm`を継承しますが、特定のモデルに紐づかないならば`forms.Form`を継承してももちろん問題ないです。

```Python:forms.py
from django import forms
from . import models

class QuestionTextChangeForm(forms.ModelForm):
    """
    質問文変更用フォーム
    """
    class Meta:
        model = models.Quesion
        fields = ('question_text',)

class ChoiceTextChangeForm(forms.ModelForm):
    """
    選択肢の文章変更用フォーム
    """
    class Meta:
        model = models.Question
        fields = ('choice_text1', 'choice_text2',)
```

今回は`models.py`で`verbose_name`をちゃんと指定しているので、ラベルは明示的には指定していません。

# 変更フォームの作成
## DetailViewとフォームを共存させる方法
ここからは汎用ビュー（クラスベースビュー）に関する知識も入ってきます。

### クラスベースビューの基本
クラスベースビューに関することは[こちらのページ](https://qiita.com/felyce/items/7d0187485cad4418c073)などが参考になると思います。バージョンが1.9と少し古いですが問題ないと思います。

ここで必要なことのみ説明すると、`generic.DetailView`というクラスは、**1つのモデル（データベース上のテーブル）に紐づいた1つの項目（データベース上のフィールド）を表示するのに便利なクラス**です。

### クラスベースビューの正体
まず、基本的な知識です。
クラスベースビューに関するクラスは、すべて`django.views.generic`内で定義されています。この中で定義されているクラスには、実際に`views.py`で用いるための`DetailView`などはもちろん、共通化できるところはすべて共通化されており、クラスを継承し、多重継承することを繰り返すことで構成されています（[DjangoのGitHub](https://github.com/django/django/tree/master/django/views/generic)を一度は見てみることをおすすめします）。

### 変更するフォームのクラス
クラスベースビューを用いたデータを変更するためのページは例えば`generic.UpdateView`などを普通用います。
しかし、前項目のような経緯から、**`django.views.generic`内で定義されているクラスをうまく継承することで複数の役割を楽にもたせることができます**。

そして、**モデルに紐づいた**フォームを作りたいならば、**`generic.edit.ModelFormMixin`を継承するとよいです**。

### ここまでのソース

```Python:views.py
from django.views import generic
from . import models # models.pyをインポート

class DetailView(generic.DetailView, generic.edit.ModelFormMixin):
    model = models.Question # Questionモデルに関する詳細ページを作成
```

## 複数のフォームを表示させる方法
クラスベース汎用ビューで変数をhtml上でレンダリングするためには、`get_context_data`関数をオーバーライドすればよいです。これを用いると、フォームを複数表示できます。

```Python:views.py
from django.views import generic
from . import models # models.pyをインポート

class DetailView(generic.DetailView, generic.edit.ModelFormMixin):
    model = models.Question # Questionモデルに関する詳細ページを作成
    template_name = 'myapp/detail.html'
    fields = ()

    def get_context_data(self, **kwargs):
        # スーパークラスのget_context_dataを使うとobject_listに
        # 表示中のモデルの情報が入るのでそれを利用
        context = super().get_context_data(**kwargs)
        # contextは辞書型
        context.update({
            'qustion_text_form': forms.QuestionTextChangeForm(**self.get_form_kwargs()),
            'choice_text_form': forms.ChoiceTextChangeForm(**self.get_form_kwargs()),
        })
        return context
```

`get_context_data`に入れるのがミソです。

さて、ここで`template_name = 'myapp/detail.html'`はhtmlのありかを示すために必要なものですが、`fields=()`はなんなのか、という話になります。

`ModelFormMixin`では、`model=`でモデルを指定した場合、かつ`form_class=`でフォームのクラスを指定していない場合（フォームクラスを1つしか使わないならばこれで指定でき、`get_context_data`関数をオーバーライドしなくても、htmlでは`form.~~~`でレンダリングできます））には、**`fields=(タプル)`でmodel内のどのフィールドを変更するのかを書かなくてはいけません**。
しかし、今回は自作のフォームクラスを使うので`fields=~~`の指定は役割をもちませんが、書かないとエラーになるのでこのような書き方をしています。

ここまでくれば、htmlでは、

```html:myapp/detail.html
<form action="" method="POST" name="form_change_question_text" id="form_change_question_text">
            {% csrf_token %}
            {{ qustion_text_form.non_field_errors }}
            <div class="form-group">
              {{ qustion_text_form.as_p }}
              <button type="submit" class="btn btn-primary" name="button_change_question_text"> 変更 </button>
            </div>
          </form>

<form action="" method="POST" name="form_change_choice_text" id="form_change_choice_text">
            {% csrf_token %}
            {{ choice_text_form.non_field_errors }}
            <div class="form-group">
              {{ choice_text_form.as_p }}
              <button type="submit" class="btn btn-primary" name="button_change_choice_text"> 変更 </button>
            </div>
          </form>
```

として表示できます。

# 情報の入力
クラスベース汎用ビューでは、`post`関数をオーバーライドすることでPOSTすることができます。複数のPOSTするためのボタンがある場合、htmlで名前を付けておいて、その名前があるかどうかで判別することが多いようです。

post関数では、`request`を引数にとることができます。この中には、`requst.POST`があり、これは辞書型でPOSTデータがすべて入っています。これを用いて、質問文変更の場合のpost関数を書いてみます。

```Python:views.py
    def post(self, request, *args, **kwargs):
        # 質問文変更ボタンがPOSTにあるとき
        if 'button_change_question_text' in request.POST:
            qform = forms.QuestionTextChangeForm(**self.get_form_kwargs())
            # バリデーション
            if qform.is_valid():
                # フォームに書き込んだ部分を取得する(保存しない)
                qform_query = qform.save(commit=False)
                qform_query.pk=models.Question.objects.get(pk=self.kwargs['pk'])
                # 保存
                qform_query.save()
                return self.form_valid(qform)
            else:
                self.object = self.get_object()
                return self.form_invalid(qform)
```
やるべきことは、

1. `qform = forms.QuestionTextChangeForm(**self.get_form_kwargs())`などとしてフォームに書かれている内容を取得する
1. `qform.is_valid`で入力内容が不正でないかを検証する。
    1. `is_valid`関数はFormクラスにある関数で、フィールドの種類に反する入力がないかを検証してくれます。
1. `qform_query = qform.save(commit=False)`で、**データベースに保存せずに**情報をメモリ上に保存します。
1. `qform_query.pk=...`としているのは、データベースでこの質問と特定するためのもので、これは`self.kwargs['pk']`から取得できます。
1. 最後に`qform_query.save()`で実際にデータベース上に保存できます。
1. `return self.form_valid(qform)では検証通過時の最後の処理をしてくれます。

という感じです。

# 最後にまとめ

```Python:views.py
from django.views import generic
from . import models # models.pyをインポート

class DetailView(generic.DetailView, generic.edit.ModelFormMixin):
    model = models.Question # Questionモデルに関する詳細ページを作成
    template_name = 'myapp/detail.html'
    fields = ()

    def get_context_data(self, **kwargs):
        # スーパークラスのget_context_dataを使うとobject_listに
        # 表示中のモデルの情報が入るのでそれを利用
        context = super().get_context_data(**kwargs)
        # contextは辞書型
        context.update({
            'qustion_text_form': forms.QuestionTextChangeForm(**self.get_form_kwargs()),
            'choice_text_form': forms.ChoiceTextChangeForm(**self.get_form_kwargs()),
        })
        return context

    def post(self, request, *args, **kwargs):
        # 質問文変更ボタンがPOSTにあるとき
        if 'button_change_question_text' in request.POST:
            qform = forms.QuestionTextChangeForm(**self.get_form_kwargs())
            # バリデーション
            if qform.is_valid():
                # フォームに書き込んだ部分を取得する(保存しない)
                qform_query = qform.save(commit=False)
                qform_query.pk=models.Question.objects.get(pk=self.kwargs['pk'])
                # 保存
                qform_query.save()
                return self.form_valid(qform)
            else:
                self.object = self.get_object()
                return self.form_invalid(qform)
        # 選択肢の文変更ボタンがPOSTにあるとき
        elif 'button_change_choice_text' in request.POST:
            cform = forms.ChoiceTextChangeForm(**self.get_form_kwargs())
            # バリデーション
            if cform.is_valid():
                # フォームに書き込んだ部分を取得する(保存しない)
                cform_query = cform.save(commit=False)
                cform_query.pk=models.Question.objects.get(pk=self.kwargs['pk'])
                # 保存
                cform_query.save()
                return self.form_valid(cform)
            else:
                self.object = self.get_object()
                return self.form_invalid(cform)
```