Django+Reactで学ぶプログラミング基礎(7): Djangoチュートリアル(投票アプリその3)

##### [前回] [Django+Reactで学ぶプログラミング基礎(6): Djangoチュートリアル(投票アプリその2)](https://qiita.com/mingchun_zhao/items/8fe318c2e2a554818793)

## はじめに

[Django公式チュートリアル](https://docs.djangoproject.com/ja/4.0/intro/tutorial01/)、その3です。
前回は、アプリを雛形レベルに仕上げました。
今回は、Djangoビューの仕様を深掘りします。

## Djangoアプリ作成(その3): 投票(poll)アプリ

### 今回の内容

- ビューとURLのマッピング
- ビューのレスポンス
- テンプレートによるビューとデザインの分離

### ビューとは

- Djangoアプリで、ウェブページとコンテンツはビューによって提供される
- 各ビューは単純にPython関数(クラスベースビューの場合はメソッド)として実装される
- ビューは、リクエストされたURL(URLのドメイン以降の部分)から決定される
    - URLパターンは、URLを一般化したもの
        - たとえば、`/newsarchive/<year>/<month>/`
    - Djangoは`URLconf`を使用し、URLからビューを特定
        - `URLconf`は、URLパターンとビューのマッピング
- 投票アプリの4つのビュー
    - 質問の索引ページ
        - 最新の質問をいくつか表示
    - 質問の詳細ページ
        - 質問文と投票フォームを表示
    - 質問の結果ページ
        - 特定質問の結果を表示
    - 投票ページ
        - 特定質問の選択を投票として受付

### 投票アプリにビューを追加

- ビューを追加
```polls/views.py
def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)
```

- `path()`コールを追加し、新しいビューを`polls.urls`モジュールと結びつける
```polls/urls.py
from django.urls import path

from . import views

urlpatterns = [
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('<int:question_id>/', views.detail, name='detail'),
    # ex: /polls/5/results/
    path('<int:question_id>/results/', views.results, name='results'),
    # ex: /polls/5/vote/
    path('<int:question_id>/vote/', views.vote, name='vote'),
]
```

### ブラウザで、URLから該当ビューにアクセス

- VS Codeターミナルで、仮想環境をアクティベート
```
C:\kanban\pollsite>..\venv\.venv\Scripts\activate
```
- 開発サーバーを起動
```
(venv) C:\kanban\pollsite>python manage.py runserver
```

- ブラウザで`http://127.0.0.1:8000/polls/34/vote/`にアクセス

上記で追加された、投票ページ(ビュー)が表示される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/6dd477e0-293e-8899-a5a4-670b4af234e1.png)

### ビューのレスポンス

- 2種類のレスポンス
    - `HttpResponse`オブジェクトを返却
        - リクエストされたページのコンテンツを含む
    - 例外を返却
        - `Http 404 Not Found`エラーなど

### `index`ビューを作成

- 最新5件の質問項目をカンマで区切り、日付順に表示するビュー
- 試しに書いたコード(問題あり)
```polls/views.py
from django.http import HttpResponse
from .models import Question

def index(request):
    # Questionモデル(テーブル)から、最新質問5件を取得
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    # 画面表示のため、質問をカンマ区切りで連結
    output = ', '.join([q.question_text for q in latest_question_list])
    # 結果をHTTPレスポンスで返す
    return HttpResponse(output)

# 残りのビュー(detail, results, vote)はそのまま
```
- 上述コードの問題は?
    - ビューにページのデザインまで含まれている
    - デザインを、ビューから分離すべき
        - ページの見栄えを変えるたびに
            - 例えば、連結して表示ではなく、1質問につき1行表示など
            - Pythonコードを編集する必要あるため
    - 対処として、Djangoのテンプレートシステムを使用
        - ビューから使用できるテンプレートを作成

### テンプレートを作成

- Djangoはどのようにテンプレートを探してロードする?
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/f2a3b35c-6da1-7e04-2c10-59f8407f5941.png)

    - プロジェクト設定ファイル`settings.py`の`TEMPLATES`に設定される
        - DjangoTemplatesバックエンドが設定されている
        - `APP_DIRS`オプションが`True`
            - DjangoTemplatesはアプリそれぞれの`templates`サブディレクトリを検索
- テンプレートはどこに書く?
    - `polls/templates/polls/index.html`
    - Django内で、テンプレートを単に`polls/index.html`のように参照できる
```polls/templates/polls/index.html
{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/e2d45dfa-42c9-f015-70bf-71ad17ba6c1e.png)

- `index`ビューを更新し、上記テンプレートを使用するように
    - テンプレートをロードし、そこにコンテキストを渡す
    - コンテキストは、テンプレート変数名をPythonオブジェクト(質問リスト)にマッピングする辞書

```polls/views.py
from django.http import HttpResponse
from django.template import loader

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    # テンプレートをロード
    template = loader.get_template('polls/index.html')
    # コンテキスト(辞書)
    context = {
        'latest_question_list': latest_question_list,
    }
    # テンプレートにコンテキストを渡し、レンダリング
    return HttpResponse(template.render(context, request))
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/74f0c3cc-3ed4-19ec-8121-e290f5f382ab.png)

- ブラウザで`http://127.0.0.1:8000/polls/`にアクセス

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/8a7b92d0-1035-2657-9740-dca89d5be3f6.png)

前回登録した質問事項から、最新5つが表示されます。

#### ショートカット: `render()`

- 以下複数処理を`render()`一発で
    - テンプレートをロード
    - コンテキストに値を入れる
    - テンプレートをレンダリングした結果を`HttpResponse`オブジェクトで返す
- `render`のみインポート
    - `loader`や`HttpResponse`をインポート不要
```polls/views.py
from django.shortcuts import render

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/dd34ff40-55a1-803d-3a8a-fec8aa1a7bed.png)

### 質問詳細の`detail()`ビューで、Http404エラーを返却

- 質問詳細ビュー
    - 指定された投票の質問文を表示するビュー
    - リクエストした`ID`を持つ質問が存在しないとき、Http404を返却
```polls/views.py
from django.http import Http404
from django.shortcuts import render

from .models import Question

def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})

# ...
```

- テンプレートを作成
```polls/templates/polls/detail.html
{{ question }}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/e9735a10-eb69-05ef-603d-43f9b3765a38.png)

#### ショートカット: `get_object_or_404()`

- 以下処理を`get_object_or_404()`一発で
    - `get()`を実行
    - オブジェクトが存在しない場合、Http404を返却
- `detail()`ビューを書き換え
```polls/views.py
from django.shortcuts import get_object_or_404, render

from .models import Question

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

# ...
```

- `detail()`ビューのテンプレート作成
```polls/detail.html
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
```

### テンプレート内のハードコードされたURLを削除

- ハードコードの問題
    - プロジェクトにテンプレートが多数ある場合、URLの変更が困難
- 変更前: パスに`/polls`が含まれる
```polls/index.html
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
```
- 変更後: テンプレートタグの`{%url%}`を使用
```polls/index.html
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
```
- 投票詳細ビューのURLを変更したい場合
    - 対象となるテンプレートを変更せず、`polls/urls.py`を変更
```polls/urls.py
...
# added the word 'specifics'
path('specifics/<int:question_id>/', views.detail, name='detail'),
...
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/d73d6c63-d90e-ec3c-e8e0-2eec9087fece.png)


### URLの名前空間

- Djangoプロジェクトに複数アプリがある場合、それぞれのURLを区別したい
    - `URLconf`にアプリケーションの名前空間を追加する
```polls/urls.py
from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]
```

- 名前空間つきの詳細ビューを指すように、テンプレートを変更
    - 変更前
    ```polls/templates/polls/index.html
    <li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
    ```
    - 変更後
    ```polls/templates/polls/index.html
    <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
    ```

## おわりに

Djangoビューのレスポンスなど深掘りしました。
テンプレートによるビューとデザインの分離も理解しました。
次回も続きます。お楽しみに。

##### [次回] [Django+Reactで学ぶプログラミング基礎(8): Djangoチュートリアル(投票アプリその4-1)](https://qiita.com/mingchun_zhao/items/7d35f8d4b8bf22bd6b36)