LoginSignup
50
55

More than 5 years have passed since last update.

Python Django チュートリアル(5)

Last updated at Posted at 2016-02-23

勉強会用資料です
今回は本家のチュートリアルから少し脇道にそれてFormについて説明していきます.
Formに入る前にdjangoの更新,urlのnamespaceについて少し紹介します.

チュートリアルのチュートリアル
チュートリアル1
チュートリアル2
チュートリアル3
チュートリアル4

他のチュートリアル

ソース(github)

また,前回までは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を追加するだけです.

tutorial/urls.py
urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^polls/', include('polls.urls')),
]

↓ pollsのincludeの引数に namespace を追加

tutorial/urls.py
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を使うと不要になるので削除しておきましょう.

polls/urls.py
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が不要になる.

polls/urls.py
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.pyapp_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文字を設定します.

polls/forms.py
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の作成と,テンプレートへの受け渡しを書きます.

polls/views.py
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 です.

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を追加しましょう.

polls/urls.py
urlpatterns = [
...
    url(r'^form$', views.form_test),
...
]

ここまで書いたら ./manage.py runserver でテストサーバを起動させ,ブラウザで確認してみましょう.
urlは http://localhost:8000/polls/form です.

Kobito.HyYPSO.png
画面

殺風景ですが,とりあえず入力用のボックスが1つ出ていることが確認できます.

Kobito.JxJa67.png
htmlソース

htmlのソースはこんな感じです.
目論見通り,{{ form }} の部分が置き換わってます.

POST送信処理

ソース→06c8422

さて,入力ボックスはできましたが,サーバ側での受け取り処理がまだありません.
htmlでサーバへデータを送信する場合,<form>タグに どこに どうやって データを送るかを記述します.
それぞれ actionmethod というattributeを書きます.
また,送信するには <form> タグ内に submitボタンを配置する必要があります.
同じviewで受け取る場合はaction指定しなくても大丈夫です.
今回は空文字で記述だけしてます.

ここで注意して欲しいのですが,POSTでデータ送信をする場合,csrf_token が必要になります.

csrfの説明 → みんな大好きwikipedia

POSTの通信はhtml的にはサーバへのデータ送信(=サーバの情報変更)となるため,
どうやって入力されたかが重要になります.
djangoではcsrf_tokenタグを付けることで,そのデータが自分が用意したページで入力されたことを保証します.

修正したhtmlは以下のようになります.

polls/templates/polls/form.html
<html>
  <body>
    <form action="" method="POST">
      {% csrf_token %}
      {{ form }}
      <input type="submit" value="送信"/>
    </form>
  </body>
</html>

Kobito.1nDDdU.png
ブラウザでボタンが出ていることを確認

ボタンを押してもその後の処理を何も書いてないので何も起こりませんが,
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に書き忘れてるとこうなります.

Kobito.Ffr1Uf.png

このチェック処理はMIDDLEWAREで行われており,settings.pyを見るとデフォルトで設定されていることが確認できます.

tutorial/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はこんな感じになります.

polls/views.py
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に何も入力せずに送信ボタンを押すとエラーメッセージが表示されることが確認できるはずです.

Kobito.Bgm3aT.png

フィールドのカスタマイズ

ソース→5637b54

フィールドは作成する時に引数を渡すことで動作や出力を変更することができます.
例えば,required=False と渡すとそのフィールドに入力値がなくてもエラーにならなくなります.
また,label引数を追加することでTextの文字を変更できるようになります.
指定できる引数は以下を参考にしてください.
https://docs.djangoproject.com/en/1.9/ref/forms/fields/#core-field-arguments

今回は先に説明したrequiredlabelを設定してみます.

polls/forms.py
class MyForm(forms.Form):
    text = forms.CharField(max_length=100, required=False, label='テキスト')

Kobito.WXYnel.png

Textテキストになりました.
また,何も入力せずに送信を押してもエラーメッセージが出なくなったのが確認できたかと思います.

投票用Form作成

VoteFormの作成

ソース→9077adee

チュートリアル4で作った投票フォームをFormクラスを使って書き換えてみましょう.
必要な入力は question モデルに紐付いた選択肢一覧を選択するためのラジオボタンです.
モデルの選択にはModelChoiceFieldが使えます.
このfieldにはモデルを選択するためのquerysetが必要ですが,fieldの定義時にはどのquestionに対する
フォームなのか確定しないため,__init__メソッドをオーバーライドし,引数で受け取ることにします.

polls/forms.py
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を出してみましょう.

polls/views.py
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,
    })
polls/templates/polls/detail.html
<h1>{{ question.question_text }}</h1>

<form action="{% url 'polls:vote' question.id %}" method="post">
    {% csrf_token %}
    {{ form }}
    <input type="submit" value="Vote" />
</form>

Kobito.5NwxDg.png
ブラウザで確認

選択肢がChoice objectになってます.
チュートリアル2で少し触れましたが,インスタンスの文字列表現はデフォルトでModelName objectのような形になっています.
models.pyを開き __str__ メソッドをオーバーライドしましょう.

polls/models.py
class Choice(models.Model):
...
    def __str__(self):
        return self.choice_text

Kobito.cya0WW.png

ちゃんと変わりました.

ちなみに,最初の画面と違って各選択肢の前に余計な黒ポチがついていますが,
これはデフォルトで各選択肢を<li>タグで区切るようになっているためです.
cssで黒ポチが出ないようにするほうが好ましいですが,
htmlをどうしても変えたい場合はwidgetが握っているrendererのinner_htmlを書き換えるとできます.

polls/forms.py
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>'

Kobito.j0CZTQ.png

受け取り処理の記述

ソース→56f2b498

チュートリアル4では vote という投票受け取り用の関数を用意していましたが,分離させる必要はありません.
form_test で書いた時にように,POSTされた場合の分岐処理を記述しましょう.

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():
            # TODO: 投票処理
            return redirect('polls:results', pk)
    else:
        form = VoteForm(question=obj)
    return render(request, 'polls/detail.html', {
        'form': form,
        'question': obj,
    })

html側もvoteではなくdetailに飛ぶように直すのをお忘れなく.

polls/templates/polls/detail.html
<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クラスに移しましょう.

やりたい処理

polls/views.py
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メソッドを書くと以下のようになります.

polls/forms.py
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側から呼び出して上げましょう.

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,
    })

これで無事完成です.
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関数ですが,短いながらもいくつかの処理にわかれています.

polls.py
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を呼び出すメソッド,など)があります).

FormViewでオーバーライドできるメソッド
    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_classget_template_namesというメソッドがありますが,
このメソッドをオーバーライドする代わりにform_classtemplate_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は以下のようになります.

polls/views.py
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つあって,
1. templateの中でQuestionインスタンスが必要
2. 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

↓ クラスを使った書き換え

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を使ってデザインを綺麗にしていきます.

次のチュートリアルへ

他のチュートリアル

50
55
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
50
55