勉強会用資料です
今回は本家のチュートリアルから少し脇道にそれてFormについて説明していきます.
Formに入る前にdjangoの更新,urlのnamespaceについて少し紹介します.
チュートリアルのチュートリアル
チュートリアル1
チュートリアル2
チュートリアル3
チュートリアル4
また,前回まではdjango1.8の説明でしたが,更新をさぼってる間にdjango1.9がリリースされてしまったので
以降のチュートリアルではdjango1.9について説明していきます.
なお,djangoのバージョンを1.8から1.9に変えてもチュートリアル1〜4までの内容に特別な違いはありません.
django1.9への移行とrequirements.txtの紹介
ソース→ee195d1
djangoの更新
コマンドラインで以下のコマンドを打つと最新のdjango (2016/2/20時点では1.9.2) にアップデートできます.
仮想環境を使用している人は workon
で仮想環境をactivateするのを忘れないように.
(tutorial)$ pip install --upgrade django
(tutorial)$ pip install --upgrade django
Collecting django
Using cached Django-1.9.2-py2.py3-none-any.whl
Installing collected packages: django
Found existing installation: Django 1.8.6
Uninstalling Django-1.8.6:
Successfully uninstalled Django-1.8.6
Successfully installed django-1.9.2
正常に終了した場合は上記のように既にインスール済みのdjango(この場合は1.8.6)をアンインストールし,最新のdjangoをインストールしてくれます.
requirements.txt
本チュートリアルでは今のところ使用している外部ライブラリはdjangoだけですが,今後使用する関連ライブラリはどんどん増えてきます.
そんな時に,どのライブラリを使用しているか?,そのライブラリのバージョンは同じか? をいちいちチェックするのは大変です.
幸いにも,pythonには pip
というパッケージ管理システムと, virtualenv
という仮想環境があるため,
この問題に悩まされることは多くないでしょう.
現在の環境で使用しているライブラリ一覧を出すにはシェルで以下のコマンドを打ちます.
(tutorial)$ pip freeze
Django==1.9.2
wheel==0.24.0
pythonでは慣例的に,この内容を requirements.txt
という名前で出力しておきます.
別の人はこのファイルを取り込むことで,必要なライブラリをインストールすることができます.
ご覧のように出力内容には1.9.2のようなバージョン番号もついているため,バージョンの違いによる動作不良が出る心配もありません.
チュートリアルではすでに置いてたつもりでしたが,作成してなかったので commit:
ee195d1
で追加しています.
使用しているライブラリの出力 (requirements.txtの作成)
(tutorial)$ pip freeze > requirements.txt
pip freeze
の内容をリダイレクトするだけです.
djangoの場合は manage.py
と同じ階層に置くことを推奨します.
ライブラリの取込
取込には pip install -r
コマンドを使用します.
取り込みたい環境に workon
されていることを確認した上で, -r
の後ろに
取り込みたいファイル (requirements.txt
) へのパスを記述しましょう.
(tutorial)$ pip install -r requirements.txt
既にinstall済みのライブラリがあり,そのバージョンを更新したい場合は
(tutorial)$ pip install -U -r requirements.txt
のように-U
オプションが必要です.
ただ,手元で試したところ,pipのバージョンが8.0.2
の場合は付けなくても更新してくれるようです.
urlにnamespaceの追加
ソース→bba5e4f
チュートリアル3ではurlのnamespace化の説明をスキップしてましたが,
のちのちの事を考えると分離しといたほうが都合がいいのでpollsのurlにnamespaceを設定します.
付け方は include
関数の引数に namespace
を追加するだけです.
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^polls/', include('polls.urls')),
]
↓ pollsのincludeの引数に namespace
を追加
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^polls/', include('polls.urls', namespace='polls')),
]
閉じカッコの位置を間違えてurl関数の引数にしないように注意
namespaceを設定すると,namespace:hoge
の形でurlを引くことができるようになります.
namespaceは api:polls:create
のようにさらに階層化して書くこともできます.
include先の polls/urls.py
の名前には poll_
というprefixを付けてましたが,
namespaceを使うと不要になるので削除しておきましょう.
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'(?P<pk>\d+)/$', views.detail, name='poll_detail'),
url(r'(?P<pk>\d+)/vote$', views.vote, name='poll_vote'),
url(r'(?P<pk>\d+)/results$', views.results, name='poll_results'),
]
↓ namespaceを切ったので poll_
という名前のprefixが不要になる.
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'(?P<pk>\d+)/$', views.detail, name='detail'),
url(r'(?P<pk>\d+)/vote$', views.vote, name='vote'),
url(r'(?P<pk>\d+)/results$', views.results, name='results'),
]
django1.9では
tutorial/urls.py
で includeの引数にnamespace名を書く代わりに,
polls/urls.py
にapp_name='polls'
を書くことでも設定できます.
https://docs.djangoproject.com/en/1.9/releases/1.9/#passing-a-3-tuple-or-an-app-name-to-include
この変更により,名前からのURLの引き方が poll_detail
などの代わりに polls:detail
になりました.
修正箇所についてはソースの差分を確認してみてください.
名前空間を切ることで,polls
アプリが tutorial
プロジェクトからより分離され,他プロジェクトで使いやすくなります.
前回までのやり方では,同プロジェクト内の別アプリが poll_detail
などの名前をURLにつけていた場合はエラーになるため,
プロジェクトが管理している全てのアプリケーションのURL名を気にする必要があります.
一方,namespaceを使うと,rootのurls.py (tutorial/urls.py
) で namespace
を衝突しないように
気をつけるだけでよくなります.
Formとは
さて,やっとFormの話に入っていきます.
Formとは英訳の「書式,申込用紙」などの意味の通り,ある形式に合わせたデータをクライアントからサーバへ渡すのに使用します.
チュートリアル4ではテンプレート内で直にFormを記述し,view側で受け取り後の処理を書いていました.
しかし,テンプレート,viewはMVCモデルでいうView, Controllerに相当し,ここにロジックが入るのはよろしくありません.
また,今の形式ではラジオボタンの表示がテンプレートに記述されており,そのバリデーション(選択されたデータが正しいかの判定),
さらにその入力値を用いた処理(投票処理)がviewに記述されています.
しかし,入力項目とそれに対するバリデーション,そのデータを使用した処理は密接に関わっているため,
これらをまとめる扱いたいです.
djangoではFormクラスが用意されており,一般的な入力(テキスト,セレクトリスト,ラジオボタンなど)と,
そのバリデーター(入力チェック)を提供しています.
Formクラスを使えば本家チュートリアル5で行うテストも書きやすくなりますし,
入力項目も簡単に増やせるようなりますし,別箇所での使い回しも簡単になります.
その上,クラスベース汎用Viewと連携することでviewの記述をさらに少なく,わかりやすくすることができます.
Formクラス
テキストフィールドの出力
ソース→34698a1
まずはともあれ,試しにFormを作ってみましょう.
アプリケーションフォルダ内に forms.py
というファイルを作り,その中にFormクラスを定義していきます.
Formクラスにはfieldクラスをメンバに設定していきます.
設定できるfieldクラスは公式ドキュメントを参考にしてください.
https://docs.djangoproject.com/en/1.9/ref/forms/fields/
とりあえず手始めに文字入力用のフィールドであるCharFieldを設定してみます.
CharFieldは必須引数として最長文字数(max_length)を指定する必要があるので,とりあえず100文字を設定します.
from django import forms
class MyForm(forms.Form):
text = forms.CharField(max_length=100)
こう書くとどうなるのか,出力を確認してみましょう.
./manage.py shell
でpythonのシェルを起動させ,先ほど作ったFormクラスのインスタンスを作成し,
それをprintしてみます.
$ ./manage.py shell
(InteractiveConsole)
>>> from polls.forms import MyForm
>>> f = MyForm()
>>> print(f)
<tr><th><label for="id_text">Text:</label></th><td><input id="id_text" maxlength="100" name="text" type="text" /></td></tr>
出力としてtext型のinputタグが出力されているのがわかります.
フィールド名を text
にしたため,labelとして Text
という文字列が出ていることも確認できます.
templateへの出力,ブラウザでの確認
ソース→34f4914
では次に,作ったFormクラスをhtmlさせましょう.
まずはviews.pyにFormの作成と,テンプレートへの受け渡しを書きます.
from .forms import MyForm
def form_test(request):
form = MyForm()
return render(request, 'polls/form.html', {
'form': form,
})
次にテンプレートを用意しましょう.
polls/form.html
というテンプレートpathを指定したので,ファイルの場所は
polls/templates/polls/form.html
です.
<html>
<body>
<form>
{{ form }}
</form>
</body>
</html>
これで {{ form }}
の中に先ほどシェルで確認した以下の文字列が入るはずです.
<tr><th><label for="id_text">Text:</label></th><td><input id="id_text" maxlength="100" name="text" type="text" /></td></tr>
最後に,form_test関数とurlを接続します.
polls/urls.py
にurlを追加しましょう.
urlpatterns = [
...
url(r'^form$', views.form_test),
...
]
ここまで書いたら ./manage.py runserver
でテストサーバを起動させ,ブラウザで確認してみましょう.
urlは http://localhost:8000/polls/form
です.
殺風景ですが,とりあえず入力用のボックスが1つ出ていることが確認できます.
htmlのソースはこんな感じです.
目論見通り,{{ form }}
の部分が置き換わってます.
POST送信処理
ソース→06c8422
さて,入力ボックスはできましたが,サーバ側での受け取り処理がまだありません.
htmlでサーバへデータを送信する場合,<form>
タグに どこに どうやって データを送るかを記述します.
それぞれ action
と method
というattributeを書きます.
また,送信するには <form>
タグ内に submit
ボタンを配置する必要があります.
同じviewで受け取る場合はaction
指定しなくても大丈夫です.
今回は空文字で記述だけしてます.
ここで注意して欲しいのですが,POSTでデータ送信をする場合,csrf_token
が必要になります.
csrfの説明 → みんな大好きwikipedia
POSTの通信はhtml的にはサーバへのデータ送信(=サーバの情報変更)となるため,
どうやって入力されたかが重要になります.
djangoではcsrf_token
タグを付けることで,そのデータが自分が用意したページで入力されたことを保証します.
修正したhtmlは以下のようになります.
<html>
<body>
<form action="" method="POST">
{% csrf_token %}
{{ form }}
<input type="submit" value="送信"/>
</form>
</body>
</html>
ボタンを押してもその後の処理を何も書いてないので何も起こりませんが,
runsever
を実行している画面のログを見るとPOST通信がされていることが確認できます.
(tutorial)$ ./manage.py runserver 127.0.0.1:13000
Performing system checks...
System check identified no issues (0 silenced).
February 21, 2016 - 16:56:38
Django version 1.9.2, using settings 'tutorial.settings'
Starting development server at http://127.0.0.1:13000/
Quit the server with CONTROL-C.
[21/Feb/2016 17:34:28] "GET /polls/form HTTP/1.1" 200 343 # ← 画面の表示
[21/Feb/2016 17:34:30] "POST /polls/form HTTP/1.1" 200 343 # ←「送信」ボタンクリック
ちなみに,{% csrf_token %}
タグをhtmlに書き忘れてるとこうなります.
このチェック処理はMIDDLEWAREで行われており,settings.pyを見るとデフォルトで設定されていることが確認できます.
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', # ←←←←←←←← csrfチェック処理
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
)
データ受信処理
ソース→8ea545ce
受け取ったPOSTデータはrequestクラスがPOST
という名前で持っています.
このデータをFormクラスに渡し,データの正当性を確認し,正しければそのデータを使ってなにか処理を行います.
修正したviewはこんな感じになります.
def form_test(request):
if request.method == "POST":
form = MyForm(data=request.POST) # ← 受け取ったPOSTデータを渡す
if form.is_valid(): # ← 受け取ったデータの正当性確認
pass # ← 正しいデータを受け取った場合の処理
else: # ← methodが'POST'ではない = 最初のページ表示時の処理
form = MyForm()
return render(request, 'polls/form.html', {
'form': form,
})
正しいデータを受け取った後はDBなどにデータを登録し,別のページにリダイレクトするなどの処理を行いますが,
今回はひとまず何もしていません.
この状態でブラウザから動作を確認してみましょう.
Text
に何も入力せずに送信ボタンを押すとエラーメッセージが表示されることが確認できるはずです.
フィールドのカスタマイズ
ソース→5637b54
フィールドは作成する時に引数を渡すことで動作や出力を変更することができます.
例えば,required=False
と渡すとそのフィールドに入力値がなくてもエラーにならなくなります.
また,label
引数を追加することでText
の文字を変更できるようになります.
指定できる引数は以下を参考にしてください.
https://docs.djangoproject.com/en/1.9/ref/forms/fields/#core-field-arguments
今回は先に説明したrequired
とlabel
を設定してみます.
class MyForm(forms.Form):
text = forms.CharField(max_length=100, required=False, label='テキスト')
Text
がテキスト
になりました.
また,何も入力せずに送信を押してもエラーメッセージが出なくなったのが確認できたかと思います.
投票用Form作成
VoteFormの作成
ソース→9077adee
チュートリアル4で作った投票フォームをFormクラスを使って書き換えてみましょう.
必要な入力は question
モデルに紐付いた選択肢一覧を選択するためのラジオボタンです.
モデルの選択にはModelChoiceFieldが使えます.
このfieldにはモデルを選択するためのquerysetが必要ですが,fieldの定義時にはどのquestion
に対する
フォームなのか確定しないため,__init__
メソッドをオーバーライドし,引数で受け取ることにします.
class VoteForm(forms.Form):
choice = forms.ModelChoiceField(
queryset=None,
label='選択',
widget=forms.RadioSelect(),
empty_label=None,
error_messages={
'required': "You didn't select a choice.",
'invalid_choice': "invalid choice.",
},
)
def __init__(self, question, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['choice'].queryset = question.choice_set.all()
ModelChoiceFieldのデフォルトwidgetはセレクトリストなので,widgetにRadioSelectを指定してあげます.
querysetはひとまずNone
にしておき,__init__
内の引数で上書きします.
error_messagesを設定すると様々な不正値が入力された際のエラーメッセージを指定できます.
ここでは指定されなかった場合(required)と,選択肢にない値が入力された場合(invalid_choice)の
エラーメッセージを設定しています.
続いてviews.py,templatesを書き換えて作成したFormを出してみましょう.
from .forms import VoteForm
def detail(request, pk):
obj = get_object_or_404(Question, pk=pk)
form = VoteForm(question=obj)
return render(request, 'polls/detail.html', {
'form': form,
'question': obj,
})
<h1>{{ question.question_text }}</h1>
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Vote" />
</form>
選択肢がChoice object
になってます.
チュートリアル2で少し触れましたが,インスタンスの文字列表現はデフォルトでModelName object
のような形になっています.
models.pyを開き __str__
メソッドをオーバーライドしましょう.
class Choice(models.Model):
...
def __str__(self):
return self.choice_text
ちゃんと変わりました.
ちなみに,最初の画面と違って各選択肢の前に余計な黒ポチがついていますが,
これはデフォルトで各選択肢を<li>
タグで区切るようになっているためです.
cssで黒ポチが出ないようにするほうが好ましいですが,
htmlをどうしても変えたい場合はwidgetが握っているrendererのinner_htmlを書き換えるとできます.
class VoteForm(forms.Form):
def __init__(self, question, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['choice'].queryset = question.choice_set.all()
self.fields['choice'].widget.renderer.inner_html = '{choice_value}{sub_widgets}<br>'
受け取り処理の記述
ソース→56f2b498
チュートリアル4では vote
という投票受け取り用の関数を用意していましたが,分離させる必要はありません.
form_test
で書いた時にように,POST
された場合の分岐処理を記述しましょう.
def detail(request, pk):
obj = get_object_or_404(Question, pk=pk)
if request.method == "POST":
form = VoteForm(question=obj, data=request.POST)
if form.is_valid():
# TODO: 投票処理
return redirect('polls:results', pk)
else:
form = VoteForm(question=obj)
return render(request, 'polls/detail.html', {
'form': form,
'question': obj,
})
html側もvoteではなくdetailに飛ぶように直すのをお忘れなく.
<h1>{{ question.question_text }}</h1>
<form action="" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Vote" />
</form>
action内を消去.
ちなみにerror_message
もFormクラスが自動で出してくれるので消してます.
投稿処理の記述
ソース→38eb2ec47
いよいよ最後の作業です.
views.vote
でやっていた投票処理(選択したchoiceのvotesを+1してsaveする処理)をFormクラスに移しましょう.
やりたい処理
def vote(request, pk):
question = get_object_or_404(Question, pk=pk)
try:
selected_choice = question.choice_set.get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
return render(request, 'polls/detail.html', {
'question': question,
'error_message': "You didn't select a choice.",
})
else:
selected_choice.votes += 1
selected_choice.save()
return redirect('polls:results', pk)
ということで,Formクラスに選択されたChoiceインスタンスのvotesをインクリメントする処理を書きます.
Formクラスはis_valid()
を実行するとcleaned_data
の中に,入力値を適切な形式に変換したデータが入ります.
少しわかりにくい言い回しですが,htmlからラジオボタンで選択し,
サーバに渡されるデータは"2"
や"3"
など,Choiceインスタンスのpkの文字表現です.
is_valid()
を実行すると,cleaned_dataの中に Choiceインスタンスに変換して 入れてくれます.
cleaned_data
は各field名をkeyとする辞書形式になっているので,choiceインスタンスを取るには
self.cleaned_data['choice']
のように書きます.
以上を踏まえた上で,選択されたChoiceインスタンスのvotesをインクリメントするvote
メソッドを書くと以下のようになります.
class VoteForm(forms.Form):
...
def vote(self):
assert(self.is_valid())
choice = self.cleaned_data['choice']
choice.votes += 1
choice.save()
assertは,is_valid()を実行済みで,かつ入力値が正常である必要があることを明確にするために書いてます.
voteメソッドができたら,view側から呼び出して上げましょう.
def detail(request, pk):
obj = get_object_or_404(Question, pk=pk)
if request.method == "POST":
form = VoteForm(question=obj, data=request.POST)
if form.is_valid():
form.vote() # ←←←←←←←←←←←← これを追記
return redirect('polls:results', pk)
else:
form = VoteForm(question=obj)
return render(request, 'polls/detail.html', {
'form': form,
'question': obj,
})
これで無事完成です.
views.voteは必要なくなったので消しときましょう.
urls.pyからも消去するのをお忘れなく.
fieldとwidget
fieldは内部で持つデータに相当します.
例えばCharField
の場合は内部データ(cleaned_data)はテキスト型,
IntegerField
なら数字型,今回のようにModelChoiceField
なら選択したモデルのインスタンスになります.
一方widgetはGUIのことで,ブラウザにどういうパーツを出すかを指定します.
ModelChoiceField
の場合,デフォルトではセレクトリストですが,今回のようにラジオボタンに変更することもできますし,
(入力としては最悪ですが,)テキストボックスにしてChoiceのpkを直に入力させることも可能です.
また,SplitDateTimeWidget
のように,日付と時間をわけで入力できるようなwidgetもあります.
始めのうちは混乱しがちですが,内部で持つデータを変えたいのか,ブラウザ上の表示を変えたいのかを意識してどちらを変更すべきかを判断しましょう.
クラスベース汎用Viewとの連携
test_form関数の書き換え
ソース→7d19e395
djangoでは汎用ViewとしてFormViewが提供されています.
このクラスを用いて,まずはform_testから書き換えてみます.
さて,書き換え対象となるform_test関数ですが,短いながらもいくつかの処理にわかれています.
def form_test(request):
if request.method == "POST": # POST メソッドで受け取った場合の処理
form = MyForm(data=request.POST) # Form作成処理( + 受け取ったデータをFormへ渡す)
if form.is_valid():
pass # formの入力値が正しい場合の処理
else: # GET メソッドで受け取った場合の処理
form = MyForm() # Form作成処理(データはなし)
return render(request, 'polls/form.html', { # templateのレンダリング
'form': form,
})
コメントでごちゃごちゃしちゃいましたが,まず大雑把にわけてGET
で呼びだされたかPOST
で呼びだされたかに別れます.
GET
の場合は最初にページを表示する場合の処理ですので,Formを作る場合の引数はありませんでした.
一方,POST
の場合は一度ページを表示した後に,入力されたデータを受け取る処理があるため,
Formの引数に data=request.POST
が渡されています.
さらに,このデータの正当性判定(form.is_valid()
)を行い,正しければ何らかの処理を行うことになります.
djangoが提供するFormViewクラスはこれら一個一個の処理が全てメソッドになっており,
特定のメソッドをオーバーライドすることで該当箇所の処理を変えることができます.
以下に一例として動作変更をする際にオーバーライドするメソッドの一覧を示します.
(この他にベースのViewが持っているメソッド(GETのときgetを呼び出すメソッド,など)があります).
def get(self, request, *args, **kwargs): # GETメソッドでアクセス時に呼ばれる
def post(self, request, *args, **kwargs): # POSTメソッドでアクセス時に呼ばれる
def put(self, *args, **kwargs): # PUTメソッドでアクセス時に呼ばれる
def get_initial(self): # Formの初期値を設定する
def get_prefix(self): # Formのprefixを設定する
def get_form_class(self): # 使用するFormクラスを取得する
def get_form_kwargs(self): # Formクラスに渡す引数を取得する
def get_form(self, form_class=None): # Formクラスのインスタンスを取得する
def get_success_url(self): # 正常終了時にリダイレクトするURL
def form_valid(self, form): # 正常時の処理
def form_invalid(self, form): # データが不正な場合の処理
def get_context_data(self, **kwargs): # templateに渡すcontextを取得する
def render_to_response(self, context, **response_kwargs): # responseを作成する
def get_template_names(self): # レンダリングに使用するテンプレート名を取得する
使用するFormクラスやテンプレートの指定用に,
get_form_class
やget_template_names
というメソッドがありますが,
このメソッドをオーバーライドする代わりにform_class
,template_name
などのフィールド値を指定するだけでも動作します.
単純なFormページの場合,指定するものは
form_class
:使用するFormクラス
template_name
:レンダリングに使用するテンプレート
success_url
:成功時の飛び先URL
の3つだけです.
#これに加えて,成功時になんらかの処理をしたくなると思うのでform_valid
をオーバーライドすることになります.
success_urlは文字列の指定になります.urlで設定した名前を使いたい場合はresolverを使いましょう.
class定義時にresolve_urlでフィールドをセットすることはできないので,reverse_lazy
を使うか,
get_success_url
をオーバーライドすることになります.
書き換え後のform_testhは以下のようになります.
from django.views.generic import FormView
from django.core.urlresolvers import reverse_lazy
from .forms import MyForm
...
class FormTest(FormView):
form_class = MyForm
template_name = 'polls/form.html'
success_url = reverse_lazy('polls:index')
form_test = FormTest.as_view()
最初に作ってた
def form_test(request):
は不要なので削除してください.
名前が衝突してエラーになります.
クラスベースViewを使う場合は,上記ソースのように
view名 = クラス名.as_view()
をクラス定義後に実行します.
最初書いていたform_test
関数はメソッドの判定,データの判定のif文が入っていましたが,
FormViewを使うとその判定はSuperクラスの中に押し込められるため,
if文(=ロジック)の全く無いviewが作成できました.
detail関数の書き換え
ソース→1efe74c527
最後にdetail関数をクラスベースViewに書き換えてみます.
最初に言い訳しておきますが,チュートリアルには向いてない程度には複雑です.
理由が2つあって,
- templateの中で
Question
インスタンスが必要 - Formクラスの引数に
Question
インスタンスが必要
1だけなら話は簡単で,あるインスタンスの詳細ページを表示するためのDetailView
というものが用意されています.
このクラスにFormView
と同じような感じで,使用するModel,テンプレートを指定するだけで表示が完成します.
# viewで受け取る引数の名前(今はpk)やテンプレートに渡すオブジェクトの名前ももちろん変更できます.
# また,テンプレートを指定しなくてもデフォルトで(app_name / model_name _detail.html)を使うようになっています.
また,2番も,Formクラスの引数を増やす自体は簡単(get_form_kwargs
をオーバーライドするだけ)なのですが,
Question
インスタンスが必要,という点が話をややこしくしています.
あるモデルのインスタンスを取得するViewクラスのmixinとして,SingleObjectMixin
が用意されているので,
今回はこのMixinとFormViewを組み合わせて使用します.
polls/views.py
def detail(request, pk):
obj = get_object_or_404(Question, pk=pk)
if request.method == "POST":
form = VoteForm(question=obj, data=request.POST)
if form.is_valid():
form.vote()
return redirect('polls:results', pk)
else:
form = VoteForm(question=obj)
return render(request, 'polls/detail.html', {
'form': form,
'question': obj,
})
書き換え前のdetail
↓ クラスを使った書き換え
```python:polls/views.py
from django.shortcuts import resolve_url
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin
from .forms import VoteForm
from .models import Question
...
class Detail(SingleObjectMixin, FormView):
model = Question
form_class = VoteForm
context_object_name = 'question'
template_name = 'polls/detail.html'
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return super().post(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['question'] = self.object
return kwargs
def form_valid(self, form):
form.vote()
return super().form_valid(form)
def get_success_url(self):
return resolve_url('polls:results', self.kwargs['pk'])
detail = Detail.as_view()
まず,GET
の場合もPOST
の場合もQuestion
のインスタンスが必要なので
self.object = self.get_object()
を呼び出すようにしています.
このメソッドで取得するモデルクラスを model = Question
で指定しています.
テンプレート内でquestion
という名前でQuestion
インスタンスを使っているので,
context_object_name = 'question'
で名前を指定しています.
VoteForm
への引数としてこのインスタンスが必要になるので
get_form_kwargs
をオーバーライドし,question
引数を追加しています.
データが正しい場合に投票処理を行いたいので,form_valid
をオーバーライドし,
form.vote
を呼び出すようにしています.
最後に,飛び先のURLを書き換えるためにget_success_url
をオーバーライドしています.
viewが受け取った引数はself.kwargs
に入るため,self.kwargs['pk']
でURLから受け取ったpkを結果URLに渡しています.
”最初のdetail
関数に比べて余りシンプルになってない,むしろ複雑になってる感じがするけどクラスにしてよかったの?”
と思っている方は,大事なことを忘れています.
クラスは 継承して使いまわせる のです.
FormViewを継承してDetailクラスを作ったように,このクラスをスーパークラスとして別のViewクラスを作成すれば
同じような処理をするViewを書く際に,model
などのフィールド4つを書き換えるだけで同じ動作をするviewが簡単に作れます.
viewを関数で書いてる人はこの機会にクラスベースViewへの乗り換えをぜひ検討してみてください.
次はbootstrapを使ってデザインを綺麗にしていきます.