まず結論
クラスベース汎用ビューを使った方法を紹介します。長々と読みたくない方のために結論を言うと、やり方は
- それぞれのフォームのためのフォームクラスを作成する(
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からなる簡単なものを考えます。
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()
のページでは
from django.views import generic
class DetailView(generic.DetailView):
model = Question
template_name = 'myapp/detail.html'
とだけ定義されているので、データベースを変更するフォームがないのはもちろん、Questionモデルに紐づいたものしか表示できません。
フォームクラスの作成
フォームを使うので、基本にならってforms.py
でフォームを作成します。ここでは、モデルに紐づくので、forms.ModelForm
を継承しますが、特定のモデルに紐づかないならばforms.Form
を継承してももちろん問題ないです。
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とフォームを共存させる方法
ここからは汎用ビュー(クラスベースビュー)に関する知識も入ってきます。
クラスベースビューの基本
クラスベースビューに関することはこちらのページなどが参考になると思います。バージョンが1.9と少し古いですが問題ないと思います。
ここで必要なことのみ説明すると、generic.DetailView
というクラスは、1つのモデル(データベース上のテーブル)に紐づいた1つの項目(データベース上のフィールド)を表示するのに便利なクラスです。
クラスベースビューの正体
まず、基本的な知識です。
クラスベースビューに関するクラスは、すべてdjango.views.generic
内で定義されています。この中で定義されているクラスには、実際にviews.py
で用いるためのDetailView
などはもちろん、共通化できるところはすべて共通化されており、クラスを継承し、多重継承することを繰り返すことで構成されています(DjangoのGitHubを一度は見てみることをおすすめします)。
変更するフォームのクラス
クラスベースビューを用いたデータを変更するためのページは例えばgeneric.UpdateView
などを普通用います。
しかし、前項目のような経緯から、django.views.generic
内で定義されているクラスをうまく継承することで複数の役割を楽にもたせることができます。
そして、モデルに紐づいたフォームを作りたいならば、generic.edit.ModelFormMixin
を継承するとよいです。
ここまでのソース
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
関数をオーバーライドすればよいです。これを用いると、フォームを複数表示できます。
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では、
<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関数を書いてみます。
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)
やるべきことは、
-
qform = forms.QuestionTextChangeForm(**self.get_form_kwargs())
などとしてフォームに書かれている内容を取得する -
qform.is_valid
で入力内容が不正でないかを検証する。-
is_valid
関数はFormクラスにある関数で、フィールドの種類に反する入力がないかを検証してくれます。
-
-
qform_query = qform.save(commit=False)
で、データベースに保存せずに情報をメモリ上に保存します。 -
qform_query.pk=...
としているのは、データベースでこの質問と特定するためのもので、これはself.kwargs['pk']
から取得できます。 - 最後に
qform_query.save()
で実際にデータベース上に保存できます。 - `return self.form_valid(qform)では検証通過時の最後の処理をしてくれます。
という感じです。
最後にまとめ
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)