2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Djangoチュートリアル④(テンプレート)

Last updated at Posted at 2021-04-04

はじめに

Djangoチュートリアル③(モデルの作成、Django Admin)に続いて、アプリを作成してみる。はじめての Django アプリ作成、その 3現場で使える Django の教科書(基礎編)を参考にする。
(※本記事後半で出てくる画面イメージは はじめての Django アプリ作成、その 2 の [API で遊んでみる] にしたがって、"What's up?" という Question の作成およびそれに紐づく "Not much" "The sky" という Choice 作成を実行していないと表示されないので注意。)

パスコンバータ

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

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)
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 というテンプレートシステムがあり、変数表示やフィルタ、テンプレートタグなどを実現することができる。どんなものがあるかなどの詳細は公式ドキュメントを参照。
ここまでで扱ったビュー関数は、デザインに関する部分もコーディングされていた(デザインといっても文字列を並べているだけだが)ため、ページのデザインを変更する場合には Python コードを編集しなくてはならない。そこで、DTL を用いてデザインを Python から分離する。

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

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 などを設定できるらしい。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/ にアクセスするとエラーが起きることが確認できる。)

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

テンプレートの作成

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

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 で指定されたコンテキストのキーで、キーに対応する変数を意味する。

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 を再掲する。

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/ にアクセスすると、以下のような箇条書きのリストが表示されるのが確認できる。index.png

404 エラー

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

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 を作成し、以下コードを書く。

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/ にアクセスすると、以下のような画面が表示される。また以下画面は templates/polls/index.html でリンク先に指定されているため、 http://localhost:8000/polls/ から What's up? をクリックしても表示される。
detail.png

http://localhost:8000/polls/2/ のように、他の数字をつけてアクセスすると、オブジェクトが存在しないためエラーとなる。
error.png

おわりに

現場で使える Django の教科書(基礎編)のおかげで理解が進む。いい本だ。続いて、はじめての Django アプリ作成、その 4を進めていく。

関連記事のリンクは以下。

2
3
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
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?