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


勉強会用資料です．
django1.8のチュートリアルを辿りつつ説明していきます．
https://docs.djangoproject.com/en/1.8/intro/tutorial04/

日本語の公式ドキュメントはバージョン1.4が最新なので若干違いが有りますが大まかな流れは一緒なので一読するのもいいと思います．
http://django-docs-ja.readthedocs.org/en/latest/intro/tutorial04.html


[チュートリアル1](http://qiita.com/maisuto/items/d01e2cca63c8e5b2c18c)
[チュートリアル2](http://qiita.com/maisuto/items/357d13027e4fe5e10618)
[チュートリアル3](http://qiita.com/maisuto/items/eece9d880d94fd241a0d)

→ [チュートリアルまとめ](http://qiita.com/maisuto/items/17653f344d1f64afa019)
--

# 投票フォームの作成

ソース：`99b01e3`→`49d2e35`

まずは本家チュートリアル通りに`detail.html`を投票用に書き換え，投票できるようにしてみましょう．

```html:polls/templates/polls/detail.html
<h1>{{ question.question_text }}</h1>

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="{% url 'poll_vote' question.id %}" method="post">
  {% csrf_token %}
  {% for choice in question.choice_set.all %}
  <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
  <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
  {% endfor %}
  <input type="submit" value="Vote" />
</form>
```

> URLのネームスペース化はしてないので `polls:vote`を`poll_vote`に書き換えています．

polls_voteという名前のurlはまだ定義してないのでこのままブラウザを開いてもエラーがでます．
とりあえずurls.pyとviews.pyを書き換えて皮だけ用意しましょう．

```python:polls/views.py
...
def vote(request, pk):
    pass
```

```python: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'),
]
```

ブラウザで詳細画面を確認．

![Kobito.eiUclF.png](https://qiita-image-store.s3.amazonaws.com/0/58048/0a310452-4d5d-359f-480f-0f9ea39a9ce1.png "Kobito.eiUclF.png")

いい感じですね．
ちなみにvote関数を実装していないのでボタンを押すとエラーになります．

#### html説明
少し戻ってhtmlにコードを解説していきます．
`{% if %}`タグはまだ出ていませんでしたが，そのままの意味でerror_messageが渡されており，値があれば `{% endif %}`までのブロックを出力します．
```
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
```

`{% csrf_token %}`タグは名前の通りcsrfトークンをセットするためのタグです．
`<form method="post">`タグの中に必ずセットする必要があります．
このタグをセットし忘れるとcsrf exceptionが発生します．

> [wikipedia](https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%82%B9%E3%82%B5%E3%82%A4%E3%83%88%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%AA) より
>>クロスサイトリクエストフォージェリ（Cross site request forgeries、略記：CSRF、またはXSRF）は、WWW における攻撃手法のひとつである。
>>
具体的な被害としては、掲示板に意図しない書き込みをさせられたり、オンラインショップで買い物をさせられたりするなどが挙げられる。また、ルーターや無線LAN等の情報機器のWebインタフェースが攻撃対象となれば、それらの機器の設定を勝手に変更される懸念もある。

> 要するに．正規の入力ページからデータが送信されたことを保証するためのものです．
他サイトからPOSTで勝手にデータを投げられないようにするために，tokenを入力ページに埋め込むようになっています．

## 投稿用viewの編集

ソース：`49d2e35`→`274bd82 `

vote関数を編集していきましょう．
と言ってもほぼ本家チュートリアルのコピペです．
リダイレクト先が本家では結果ページになっていますが，今回は一覧画面に飛ばすようにしています．

```python:polls/views.py
from django.shortcuts import redirect

from .models import Choice
...

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('index')
```

少しコードの解説を．
`selected_choice = question.choice_set.get(pk=request.POST['choice'])`
`choice_set`という記述がありますが，これはquestionオブジェクトに対してリレーションを張っているChoiceモデルを操作するためのmanagerです．
`question.choice_set`と`Choice.objects.filter(question=question)`が同じ意味です．
> よく意味がわからない方は軽く流しといてください．

postされた値は`request.POST`に辞書のような形式で入っています．
inputタグのname属性値が`choice`なのでrequest.POST['choice']で選択肢が取れます．

次の行の
`except (KeyError, Choice.DoesNotExist):`
ここでchoiceの入力がない場合(KeyError)，選択肢外の値を投げられた場合(DoesNotExist)の処理を書いています．
今回の例ではerror_messageを入れ，再度入力ページを表示しています．

`else:`句は無事投稿用のchoiceが取れた場合入ってきます．
選択されたchoiceのvotes（投稿数）をインクリメントして保存し，一覧ページへジャンプしています．

本家チュートリアルでは
`return HttpResponseRedirect(reverse('polls:results', args=(p.id,)))`
のように書いてますが，ここでは`redirect`関数を使用しています．やってることは全く同じです．

## 結果ページ追加

ソース：`274bd82`→`4ecb885`

一覧画面じゃなくてちゃんとした結果ページを作りましょう．
htmlは例によって本家チュートリアルからのコピペです．

```html:polls/templates/polls/results.html
<html>
  <body>
    <h1>{{ question.question_text }}</h1>

    <ul>
      {% for choice in question.choice_set.all %}
      <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
      {% endfor %}
    </ul>
    <a href="{% url 'poll_detail' question.id %}">Vote again?</a>
  </body>
</html>
```

続いてviews.pyにresults関数を追加します．
内容はdetailとほぼ一緒で，htmlのパスだけ変えています．

```python:polls/views.py
def results(request, pk):
    obj = get_object_or_404(Question, pk=pk)
    return render(request, 'polls/results.html', {
        'question': obj,
    })
```

最後にurlを編集してviewを繋いで完成です．

```python: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'),
]
```

![Kobito.utItOF.png](https://qiita-image-store.s3.amazonaws.com/0/58048/68296861-c493-4d12-1590-d3e392078493.png "Kobito.utItOF.png")


結果ページができました．

## リダイレクト先変更

投稿後に一覧ページに飛ぶようになっていたので結果ページに飛ぶように直しましょう．
一覧ページと違って`poll_results`は引数が必要なので注意しましょう．

```python:polls/views.py
def vote(request, pk):
...
        return redirect('poll_results', pk)
```

これで投稿後は結果ページへジャンプするようになりました．

# クラスベース汎用View

ドキュメント→https://docs.djangoproject.com/en/1.8/intro/tutorial04/#use-generic-views-less-code-is-better
クラスベースViewのドキュメント→https://docs.djangoproject.com/en/1.8/topics/class-based-views/


モデルの一覧表示，詳細表示，テンプレート指定など，よく使われるviewについて，
djangoではクラスベース汎用Viewを提供しています．

今書いているviews.pyの中にある`index`, `detail`, `results`はすでに行数が少ないので恩恵が感じにくいかもしれませんが，voteなどのようにFormが絡み，少し複雑になってくると非常に強力な道具になります．

Formクラスについてはまだチュートリアルで出てないので今回はソースの変更はせず，
本家チュートリアルを見つつ紹介に留めます．

クラスベースView化は次（かその次）のチュートリアルでやるので興味のない方は読み飛ばしてください．

## TemplateView

本家チュートリアルにはありませんが，使用するテンプレートを設定するだけのシンプルなViewです．
どのテンプレートを使用するか設定するだけで使用できます．
templateに渡す辞書を設定する場合は`get_context`メソッドをオーバーライドします．

urlにはview関数を渡す必要が有るため，全てのクラスベースViewは`as_view()`メソッドを使用して関数化する必要があります．

```python:app/views.py
from django.views.generic import TemplateView


class Index(TemplateView):
    template_name = 'app/index.html'

index = Index.as_view()
```

こんな感じです．

template_nameなど，クラスのプロパティはas_viewの時に渡すこともできます．
つまりviews.pyを変更せずに，urls.pyの中でこんなふうにかけます．

```python:app/urls.py
from django.conf.urls import url
from django.views.generic import TemplateView


urlpatterns = [
    url(r'^$', TemplateView.as_view(template_name='app/index.html'), name='app_index'),
]
```


## DetailView

使用するテンプレートに加えて，どのモデルを使うかを指定します．

```python:app/views.py
from django.views.generic import DetailView

from .model import AppModel


class Detail(DetailView):
    template_name = 'app/detail.html'
    model = AppModel


detail = Detail.as_view()
```

もちろんこのクラスもurls.pyに直書きできます．
templateには`object`という名前（と，`appmodel`のようにモデル名を小文字にした名前）で
取得したオブジェクトが渡されます．
デフォルトでは`pk`という名前でurls.pyから呼び出されるようになっており，
オブジェクトが見つからないとHttp404がraiseされます．


## ListView

Detailはあるモデルの単一オブジェクトだったのに対し，ListViewは複数オブジェクトを返します．
どのオブジェクトを返すかは`queryset`プロパティを渡すか，`get_queryset`メソッドをオーバーライドすることで指定します．

```app/views.py
from django.views.generic import ListView

from .model import AppModel


class Index(ListView):
    template_name = 'app/index.html'
    queryset = AppModel.objects.all()
```


このクラスにはページングや並び順など，多彩なカスタマイズ項目を持っています．

--

本家チュートリアルではこの次にテストについて説明がありますが，
次回チュートリアルでは少し脇道にそれてFormクラス，ModelFormクラスの説明をしていきます．

[次のチュートリアルへ](http://qiita.com/maisuto/items/33dfeb58f5953d1c5fdf)

→ [チュートリアルまとめ](http://qiita.com/maisuto/items/17653f344d1f64afa019)