◀ 5.

djangoにおけるテンプレート名の重複について(同名のhtml)

▶
この記事の要点
  • Django はテンプレートを全アプリの templates フォルダから名前で探し、最初に見つかったものを使う
  • そのため別アプリに同じ名前の index.html があると、意図しないアプリのテンプレートが表示される
  • 対処: アプリ名/templates/アプリ名/index.html のようにアプリ名のサブフォルダを作る(名前空間化)
  • ビューでは render(request, 'アプリ名/index.html') とパス込みで指定する
  • どのファイルが使われたかは、テンプレートの origin やエラー画面の Template-loader postmortem で確認できる

結論: テンプレートはアプリ名のフォルダに入れる

template_name = 'index.html'
return render(request, template_name)

Django において上記のように単に index.html を指定するだけでは非常に危険です。異なるアプリに同名の index.html が存在すると、意図しない index.html が読み込まれる可能性があります。テンプレート名を指定する場合は、アプリケーション名込みで指定します。

template_name = 'calculator/index.html'
return render(request, template_name)

原因: Django のテンプレート検索の仕組み

Django は settings.py の TEMPLATES 設定に従ってテンプレートを探します。標準的な設定は次のとおりです。

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                # 省略
            ],
        },
    },
]

この設定では、render(request, 'index.html') と書くと、Django は次の順番で index.html を探します。

  1. DIRS に書いたフォルダ(この例ではプロジェクト直下の templates)
  2. APP_DIRS が True の場合、INSTALLED_APPS に書かれた順に、各アプリの templates フォルダ

そして最初に見つかったファイルを使い、残りは見ません。ここで重要なのは、アプリの templates フォルダはアプリごとに分離されているわけではなく、すべて 1 つの検索対象としてまとめて扱われる点です。

たとえば blog/templates/index.html と calculator/templates/index.html があり、INSTALLED_APPS で blog が先に書かれていると、calculator アプリのビューで 'index.html' を指定しても blog の index.html が表示されます。エラーにならないため、気づきにくいのが厄介な点です。

対処法: テンプレートを名前空間化する

まずテンプレートディレクトリの配置構造を次のようにします。templates フォルダの中に、もう一度アプリ名のフォルダを作るのがポイントです。

プロジェクト名/
    manage.py
    プロジェクト名/
        settings.py
    calculator/
        templates/
            calculator/
                index.html
    blog/
        templates/
            blog/
                index.html

少し冗長ですが、静的ファイル(static)のディレクトリ構造と同様に、テンプレートもこの構造にするのが Django 公式チュートリアルでも推奨されている方法です。フォルダ名は templates(複数形)でなければ APP_DIRS の検索対象になりません。

ビューからの呼び出しは次のように、サブフォルダ名を含めて指定します。

from django.shortcuts import render

def index(request):
    return render(request, "calculator/index.html")

テンプレート同士の継承や読み込みでも同じです。

{% extends "calculator/base.html" %}
{% include "calculator/_form.html" %}

クラスベースビューの既定テンプレート名

ListView や DetailView などの汎用ビューは、template_name を指定しないと アプリ名/モデル名_list.html のような名前を自動で使います。最初からアプリ名のフォルダを前提にした命名になっているため、名前空間化の構成と相性が良くなっています。

あえて同名テンプレートで上書きする使い方

この「最初に見つかったものが勝つ」仕組みは、逆に他のアプリのテンプレートを上書きするために使われます。たとえば Django 管理画面のテンプレートを変更したいときは、プロジェクトの templates フォルダに admin/base_site.html を置くと、DIRS が先に検索されるため django.contrib.admin 本来のテンプレートより優先されます。

同名の衝突は、このような意図的な上書きのときだけ起こるように構成しておくのが理想です。

よくある落とし穴

  • フォルダ名が template(単数形): APP_DIRS は templates だけを探すため、TemplateDoesNotExist になる
  • アプリを INSTALLED_APPS に追加していない: 追加していないアプリの templates は検索されない
  • 共通テンプレートの置き場所: base.html のように全アプリ共通のものは、プロジェクト直下の templates(DIRS)に置き、アプリ固有のものと分ける
  • INSTALLED_APPS の順番を変えたら表示が変わった: 名前空間化されていない同名テンプレートがある証拠。サブフォルダ構成に直す
  • 拡張子の違い: index.htm と index.html は別のファイルとして扱われる。指定と実ファイル名を一致させる

確認方法: どのテンプレートが使われたか調べる

python manage.py shell で次を実行すると、その名前で実際に読み込まれるファイルのパスが分かります。

from django.template.loader import get_template

t = get_template("index.html")
print(t.origin.name)

また、存在しない名前を指定したときの TemplateDoesNotExist エラー画面(DEBUG=True)には「Template-loader postmortem」が表示され、どのフォルダをどの順で探したかが一覧で確認できます。開発中は django-debug-toolbar の Templates パネルでも、使われたテンプレートのパスを確認できます。

関連

子ページ

子ページはありません

同階層のページ
  1. Templateの使用準備
  2. Template の定義方法
  3. テンプレートの作成と共通化
  4. setting.pyにおけるテンプレートの設定
  5. テンプレートの名前の重複について
  6. 静的ファイルの読み込み
  7. if文
  8. テンプレートで定数を使用する方法
  9. aタグのhrefの記載方法