昼飯データベースを作りたい【EP1-3】はじめてのDjango勉強編

#本稿は続きものです。
前回⇒[昼飯データベースを作りたい　【EP1-2】はじめてのDjango勉強編](https://qiita.com/EconUC/items/408ae1a7118fe8078369)
前々回⇒[昼飯データベースを作りたい　【EP1-1】はじめてのDjango勉強編](https://qiita.com/EconUC/items/96fc1cd2e866f8fc72ed)
初回のもの書き⇒[昼飯データベースを作りたい！【EP.0】](https://qiita.com/EconUC/items/cf1f69cfefb6c63e4c31)

#前回までのおさらい
Djangoのチュートリアルに沿って、投票アプリケーションの作成を行いながらDjangoの基礎に触れてきました。
前回のラストでTemplateを用いた画面表示を作るところまで来ました。
今回はTemplateの続きからです。

#Formの作成
前回、`index.html`の編集が終わりました。今回は`detail.html`にForm機能を搭載していくところから始めていきます。

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

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

<form action="{% url 'polls: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>
```
エラーメッセージを受け取り表示する`{% if error_message %}`～と`<form ...>`タグ以下が追加文です。
`<form>`タグについての解説は⇒[HTMLクイックリファレンス](http://www.htmq.com/html5/form.shtml)

`<input type="radio">`タグでラジオボタンを作り、`<input type="submit">`タグで投票ボタンを作ることができます。
現時点ではラジオボタンの作成に用いる選択肢を作っていないので投票ボタンのみ見ることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402157/738e4065-1579-da63-9bd8-505cc8e1ea02.png)

#投票ボタンに機能を持たせる
前々回で一応の作成を行った`vote()`関数をきちんと作っていきます。

```python:mysite/polls/views.py
def vote(request, question_id):
#ここでQuestionオブジェクトを取得
    question = get_object_or_404(Question, pk=question_id)
#ここでQuestionのChoiceを取得
    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 HttpResponseRedirect(reverse('polls:results', args=(question_id,)))
```
ここで私は一つ突っかかりました。
`choice_set`👈誰やねんお前
それもそのはず、前回の部分で私は選択肢作りをすっ飛ばしてしまったのだから。
それで先ほどの画面にも選択肢が出ないわけです。
以下Djangoの対話シェルです。

```python:cmd
>>> from polls.models import Choice, Question
>>> q=Question.objects.get(pk=1)
>>> q.choice_set.all()
<QuerySet []>
>>> q.choice_set.create(choice_text='Not much', votes=0)
<Choice: Not much>
>>> q.choice_set.create(choice_text='The sky', votes=0)
<Choice: The sky>
```
ここらへんです。この後、もう一つ`choice`をセットしますが結局deleteするので割愛しました。
ここで`choice_set`が何の前触れもなく登場しています。
これ実はDjangoのmetaclassの`RelatedManager`というのがなんやかんやしているみたいです。
端的に言うと`models.py`で`Choice`に設定した`Foreignkey`のおかげで`Question`と`Choice`のデータベースに関係性を持たせることができ、それを対話シェルでは`choice_set`の形で扱うことができるというわけです。
詳しい解説を探してみたら英語しかいいのがヒットしませんでした。

* [What is choice_set.all in Django tutorial-stack overflow](https://stackoverflow.com/questions/31746571/what-is-choice-set-all-in-django-tutorial)
* [Related objects reference](https://docs.djangoproject.com/ja/3.1/ref/models/relations/)

ですがこれで選択肢が表示されるようになりました。
こんな感じで
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402157/51b0c841-1fd5-47dc-9c23-5a9c7928deb6.png)
あと、戻って確認しているときに書き忘れがありました。

```python:mysite/polls/models.py
class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')
    def __str__(self) -> str:
        return self.question_text
#以下書き忘れ
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
```

`was_published_recently()`についてを完全に逃していました。APIでのいじりを端折ったせいで思わぬところで引っかかってしまいました。やっぱりチュートリアルはきちんと流れに沿って流していくのが吉です。

#投票結果を見れるようにする
投票を行うための機能を付けたので、その結果を見れる機能も載せます。
`template`フォルダの下に新たに`results.html`という結果を表示するための`html`ファイルを追加します。

```html:mysite/polls/templates/polls/results.html
<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 'polls:detail' question.id %}">Vote again?</a>
```

ここの`{{ question.question_text }}`で選択されている質問が表示され、その後の`{{ choice.choice_text }}`で選択肢の文を表示、`{{ choice.votes }}`でその時点の投票数を表示してくれます。
こんな感じで
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402157/dabafe6c-0e9d-4b6d-8606-513409c856b5.png)


#汎用ビューを使う
`django.views.generic`を使ってコードを簡単なものに書き直していきます。これでコードの冗長な部分を減らすことができます。
ですが、初めからこの汎用ビューを使うのは得策ではないようです。今の状況は使うのに適した状況化の吟味は必要ということですね。
汎用ビューを使うにあたり`urls.py`, `views.py`を修正します。

```python:mysite/polls/urls.py
urlpatterns = [

    path('', views.IndexView.as_view(), name='index'),

    path('<int:pk>/', views.DetailView.as_view(), name='detail'),

    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),

    path('<int:question_id>/vote/', views.vote, name='vote'),
]
```
`urlpatterns`の`index`, `detail`, `results`のPathのところと表示する内容のところをそれぞれいじっています。
`IndexView`, `DetailView`, `ResultsView`　はまだ作成していないのでこの後`views.py`に書きます。
ここではこれらクラスを足すときに古い`index`, `detail`, `results`のビューを消します。

```python:mysite/polls/views.py

class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]

class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'

class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'
```

ここでも`as_view()`という定義してないメソッドを使っていますが例のごとくDjangoのものなので、ちゃんと使えます。
`as_view`については[こちら](https://docs.djangoproject.com/ja/3.1/ref/class-based-views/base/#django.views.generic.base.View.as_view
)
んでこれまでスルーしてきたんですが、`pk`って何でしょうか。
`pk`とはPrimary Keyの略です。それでPrimary Keyはデータを一意に特定できるもののことです。
一意に特定するというのは、各々のデータで`Foreign Key`など他所から引いてきている値や重複のあるものではなく、その値から導かれるデータが一つしか存在しないものを指します。（だいたいは固有のidを持たせることがほとんどかと）

汎用ビューができたら早速確認しましょう

見た目には差が表れないですが、裏はしっかり変わっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402157/1741ea11-455d-4fd7-1833-786034eac7f2.png)


#テストをする
ここまでで投票システムとしては完成したかのように思われますが、テストを行うことでより素晴らしいものにすることができます。

##初めてのテスト
今まで書いてきたコードになんとバグがあるらしいってことで、まずはチェックしていきます。

```python:cmd
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> future_question.was_published_recently()
True
```
このコードで`pub_date`に未来の日時を入れています（`timezone.now()`にさらに30日足しています）が`was_published_recently()`では`True`が帰ってきてしまいます。未来のことなのに！

というわけで今対話シェルで行ったことをスクリプトに書きます。

```python:mysite/polls/tests.py
import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question


class QuestionModelTests(TestCase):

    def test_was_published_recently_with_future_question(self):
      
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)
```

行ったことを書くといいましたが、こうしてみると結構違いますね。
まず`TestCase`をインポートしてなんかインスタンス作ってますね。
このインスタンスがテストを行ってくれます。インスタンス内の`test_was_published_recently_with_future_question()`長いメソッドが未来の日時を入れたときの`pub_date`の検証を行ってくれます。
Test Caseについての詳細は[こちら](https://docs.djangoproject.com/ja/3.1/topics/testing/tools/#django.test.TestCase)

この`tests.py`を作ったら動かしてテストをしてみましょう。

```:cmd
(Django) C:\User\mysite>python manage.py test polls
```
これで`tests.py`が動くあたりやっぱりDjangoすごいです。
そしてこれは以下の形で失敗となります。

```:cmd
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
was_published_recently() returns False for questions whose pub_date
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\yushi\Study\Hirumeshi\mysite\polls\tests.py", line 18, in test_was_published_recently_with_future_question
    self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)
Destroying test database for alias 'default'...
```
無事？`AssertionError`が検出されましたね、めでたしめでたし。

では、直していきます。
今回、問題となっているのは未来の日時を入れても`True`を返してしまうところでした。
ということは過去の日付が入っているときにのみ`True`を返してくれるようになればいい感じです。

```python:mysite/polls/models.py
def was_published_recently(self):
    now = timezone.now()
    return now - datetime.timedelta(days=1) <= self.pub_date <= now
```
これで`pub_date`が昨日以上今以下の日時であるときに`True`を返してくれるようになります。
こうしてテストをしてみると以下のように成功するはずです。

```:cmd
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
Destroying test database for alias 'default'...
```
とりあえず目下のバグの修正ができました。


また長くなってきたんで今回はここまでにします。
次回、[テスト完了＋はじめてのDjango勉強編最終回]()になります。