Djangoチュートリアル④（テンプレート）

# はじめに

[Djangoチュートリアル③（モデルの作成、Django Admin）](https://qiita.com/honda28/items/04bc74c28b816aa10e2c)に続いて、アプリを作成してみる。[はじめての Django アプリ作成、その 3](https://docs.djangoproject.com/ja/3.1/intro/tutorial03/)、[現場で使える Django の教科書（基礎編）](https://www.amazon.co.jp/%E7%8F%BE%E5%A0%B4%E3%81%A7%E4%BD%BF%E3%81%88%E3%82%8B-Django-%E3%81%AE%E6%95%99%E7%A7%91%E6%9B%B8%E3%80%8A%E5%9F%BA%E7%A4%8E%E7%B7%A8%E3%80%8B-%E6%A8%AA%E7%80%AC-%E6%98%8E%E4%BB%81/dp/4802094744)を参考にする。
（※本記事後半で出てくる画面イメージは [はじめての Django アプリ作成、その 2](https://docs.djangoproject.com/ja/3.1/intro/tutorial02/) の [API で遊んでみる] にしたがって、"What's up?" という Question の作成およびそれに紐づく "Not much"  "The sky" という Choice 作成を実行していないと表示されないので注意。）

# パスコンバータ

チュートリアルにしたがって以下のように polls/views.py にビュー関数を追加し、追加したビューと結びつく URLconf を polls/urls.py に登録する。

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

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

ここで重要なのは、urls.py にて記述されている　__'\<int:question_id>'__ である。<> を使用すると、URL の一部をキーワード変数としてビュー関数に渡すことができる。たとえば "/polls/34/" というリクエストが来た場合、detail 関数は引数 question_id として 34 を受け取る。これは__パスコンバータ__という記法で、Django は「int, str, path, slug, uuid」の5種類のコンバータを提供しており、またコンバータを自作することもできる。

# DTL (Django Template Language)

Django には DTL というテンプレートシステムがあり、変数表示やフィルタ、テンプレートタグなどを実現することができる。どんなものがあるかなどの詳細は[公式ドキュメント](https://docs.djangoproject.com/ja/3.1/ref/templates/builtins/)を参照。
ここまでで扱ったビュー関数は、デザインに関する部分もコーディングされていた（デザインといっても文字列を並べているだけだが）ため、ページのデザインを変更する場合には Python コードを編集しなくてはならない。そこで、DTL を用いてデザインを Python から分離する。

まず config/settings.py の TEMPLATES を設定する。DIRS のみデフォルトから設定を変更している。

```python:config/settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
```

- BACKEND : テンプレートエンジンを設定する。デフォルト以外にも [Jinja2](https://jinja.palletsprojects.com/en/2.11.x/) などを設定できるらしい。Jinja2 の方が機能が豊富ということも聞いたが、ここは一旦デフォルトのままで Django に慣れてきたら試したい。
- DIRS : どのディレクトリを優先してテンプレートを探しに行くかという順番を指定する。デフォルトでは [(空白)] だが、ここでは上記のように [os.path.join(BASE_DIR, 'templates')] としておく。（`import os` を忘れずに。）設定を変えた理由は後述。
- APP_DIRS : テンプレートを探す際に各アプリケーションディレクトリ直下の templates ディレクトリを優先的に探しにいくかどうかを設定する。デフォルトでは True 。
- OPTIONS : 正直あんまりよくわからない。わかる人教えてください。上記コードはデフォルトのまま。

ここで DIRS の設定を変更した理由を述べる。チュートリアルでは、デフォルト設定のまま polls/templates/polls なるディレクトリを作成し、その中にテンプレート index.html を作成している。この構成は明らかにわかりにくく、また polls/templates ディレクトリの直下に polls サブディレクトリを作らなければよいという考えもチュートリアルに記載の以下理由から適切でない。

> テンプレートの名前空間

> 作ったテンプレートを (polls という別のサブディレクトリを作らずに) 直接 polls/templates の中に置いてもいいのではないか、と思うかもしれませんね。しかし、それは実際には悪い考えです。Django は、名前がマッチした最初のテンプレートを使用するので、もし 異なる アプリケーションの中に同じ名前のテンプレートがあった場合、Django はそれらを区別することができません。そのため、Django に正しいテンプレートを教えてあげる必要がありますが、一番簡単な方法は、それらに 名前空間を与える ことです。アプリケーションと同じ名前をつけた もう一つの ディレクトリの中にテンプレートを置いたのは、そういうわけなのです。

そこでよりわかりやすいディレクトリ構成とするために、ベースディレクトリ直下に templates ディレクトリを作成し、そのサブディレクトリとして各アプリケーション単位のテンプレートを扱うようにした。実際の構成を以下に示す。

```
mysite       (<- ベースディレクトリ)
|-- manage.py
|-- config   (<- 設定ディレクトリ)
|   |-- __init__.py
|   |-- asgi.py
|   |-- settings.py
|   |-- urls.py
|   `-- wsgi.py
|-- polls    (<- アプリケーション)
|   |-- __init__.py
|   |-- admin.py
|   |-- apps.py
|   |-- migrations
|   |   `-- __init__.py
|   |-- models.py
|   |-- tests.py
|   `-- views.py
`-- templates (<- テンプレート)
    `-- polls
        `-- index.html
```

上記構成にすることで、わかりやすく、かつ各アプリケーションのテンプレートをベースディレクトリから一元管理することができる。このような理由から DIRS の設定を変更している。「この構成にするのであれば、各アプリケーションディレクトリ直下の templates を優先的に探す APP_DIRS は __False__ にすべきじゃないの？」という声があるかと思うが、その場合管理サイトのテンプレートが見つからなくなるなどの問題が起きるので True のままにする。（試したい場合は `'APP_DIRS': False` として開発サーバーを立ち上げた後、 [ http://localhost:8000/admin/](http://localhost:8000/admin/) にアクセスするとエラーが起きることが確認できる。）

ちなみに admin に関するテンプレートは `django/contrib/admin/templates/admin` にあった。django のソースファイルの位置は `$ python -c "import django; print(django.__path__)"` でわかる。

# テンプレートの作成

テンプレートを作成する前に、テンプレートを表示するために polls/views.py の index 関数を変更する。

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

render() 関数は、第1引数に request オブジェクトを、第2引数にテンプレート名を、第3引数（任意）に dict 型で指定したコンテキストを受け取る。テンプレートを指定のコンテキストでレンダリングして、その HttpResponse オブジェクトを返す。

templates/polls ディレクトリ直下に上記で指定した index.html ファイルを作成し、以下のコードを書く。コード内の latest_question_list は上記 polls/views.py で指定されたコンテキストのキーで、キーに対応する変数を意味する。

```python:templates/polls/index.html
{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}
```

上記コードは DTL 記法で書かれている。__{% %} で囲まれたものはテンプレートタグ__で、{% if (条件分岐) %} や {% for (ループ) %} などの便利なテンプレートタグが用意されている。ここでは latest_question_list が存在する場合はその数の分だけリストで表示し、存在しない場合は "No polls are availabel. と表示されるようなものとなっている。また a タグ内の href は `<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>` のようにリンクの一部をハードコードしても書けるが、URL の変更が煩雑になるため、ここでは {% url %} というテンプレートタグを用いている。 このテンプレートタグは polls.urls.py に path() 関数で定義した URL を逆引きできるようになる。本記事のはじめに書いた polls/urls.py を再掲する。

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

{% url %} の第1引数は URL のパターン名（ [app_name]:[path_name] ）を、第2引数以降は正規表現グループが含まれている場合にそのパターンにマッチするように値を指定する。つまり `<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>` の場合は、上記 polls/urls.py に登録された detail のパスである '\<int:question_id>/' を逆引きしている。こうすることで URL パターンを変更したい場合にテンプレートを変更することなく、 polls/urls.py のパスのみ変更すればよくなる。app_name はなくても逆引きできるが、アプリケーションが多数になったときに同名のビューが含まれていても問題ないように app_name で名前空間を定義しておくのが望ましい。

[ http://localhost:8000/polls/](http://localhost:8000/polls/) にアクセスすると、以下のような箇条書きのリストが表示されるのが確認できる。![index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/769545/39946881-e206-5869-6fae-2478e7ff8044.png)


# 404 エラー

ビューにはリクエストに対応する HttpResponse オブジェクトを返す以外に、例外を返すという役割をもつ。ここでは、指定された投票の質問文を表示する detail ビューに例外処理を実装する。以下コードを polls/views.py に追記する。

```python: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})
```

get_object_or_404() 関数は、 get() を実行し、オブジェクトが存在しない場合は Http404 を発生させる。上記 detail ビューを動かすために、polls/detail.html を作成し、以下コードを書く。

```python:templates/polls/detail.html
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
```

[ http://localhost:8000/polls/1/](http://localhost:8000/polls/1/) にアクセスすると、以下のような画面が表示される。また以下画面は templates/polls/index.html でリンク先に指定されているため、[ http://localhost:8000/polls/](http://localhost:8000/polls/) から What's up? をクリックしても表示される。
![detail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/769545/a7cb8665-073c-0ffc-d19e-4a158222eea8.png)

[ http://localhost:8000/polls/2/](http://localhost:8000/polls/2/) のように、他の数字をつけてアクセスすると、オブジェクトが存在しないためエラーとなる。
![error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/769545/02eaa3fd-0d5e-e1b2-f7ba-bc2a30a78b20.png)

# おわりに

[現場で使える Django の教科書（基礎編）](https://www.amazon.co.jp/%E7%8F%BE%E5%A0%B4%E3%81%A7%E4%BD%BF%E3%81%88%E3%82%8B-Django-%E3%81%AE%E6%95%99%E7%A7%91%E6%9B%B8%E3%80%8A%E5%9F%BA%E7%A4%8E%E7%B7%A8%E3%80%8B-%E6%A8%AA%E7%80%AC-%E6%98%8E%E4%BB%81/dp/4802094744)のおかげで理解が進む。いい本だ。続いて、[はじめての Django アプリ作成、その 4](https://docs.djangoproject.com/ja/3.1/intro/tutorial04/)を進めていく。

関連記事のリンクは以下。
- [Djangoチュートリアル①（インストールとプロジェクトの作成）](https://qiita.com/honda28/items/34dceaeeb1cdddb4c9dd)
- [Djangoチュートリアル②（ビューの作成）](https://qiita.com/honda28/items/c91f78238ea3d53e776d)
- [Djangoチュートリアル③（モデルの作成、Django Admin）](https://qiita.com/honda28/items/04bc74c28b816aa10e2c)
- [Djangoチュートリアル⑤（汎用ビュー）](https://qiita.com/honda28/items/7150a5df0213dc6a3927)
- [Djangoチュートリアル⑥（自動テスト）](https://qiita.com/honda28/items/f21fe43d29ffe7e15b5d)
- [Djangoチュートリアル⑦（静的ファイル）](https://qiita.com/honda28/items/bd0bfe8731a17609907c)