44
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-04-09

まず結論

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

  • それぞれのフォームのためのフォームクラスを作成する( forms.Formforms.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からなる簡単なものを考えます。

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()のページでは

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を継承してももちろん問題ないです。

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.pyverbose_nameをちゃんと指定しているので、ラベルは明示的には指定していません。

変更フォームの作成

DetailViewとフォームを共存させる方法

ここからは汎用ビュー(クラスベースビュー)に関する知識も入ってきます。

クラスベースビューの基本

クラスベースビューに関することはこちらのページなどが参考になると思います。バージョンが1.9と少し古いですが問題ないと思います。

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

クラスベースビューの正体

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

変更するフォームのクラス

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

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

ここまでのソース

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関数をオーバーライドすればよいです。これを用いると、フォームを複数表示できます。

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では、

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関数を書いてみます。

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

という感じです。

最後にまとめ

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)
44
51
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?