dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

[django]templatesディレクトリを配置する場所(ベストプラクティスを考えてみた)

djangoでテンプレートファイルを管理する時、置き場所の候補はたぶん2つ考えられます。(*1)

  1. project ディレクトリの直下にtemplatesディレクトリを作り、その下に application ごとのサブディレクトリを作る
  2. 各 application ディレクトリの直下にtemplatesディレクトリを作る

ただ、どういう風に管理するのがベスト(というか一般的)なのかということについて
ググったけどもなかなかこれだ!というのが出てきませんでした。

ので、色々考えてみましたという話。

使用している Django のバージョンは 1.5。

はじめは application 直下に管理していた

これまでは application ディレクトリの下にtemplatesディレクトリを作り、
その下にそれぞれのアプリで必要なテンプレートを作って管理してました。

.
├── app1
│   ├── templates
│   │   └── index.html
│   ├── urls.py
│   └── views.py
├── app2
│   ├── templates
│   │   └── index.html
│   ├── urls.py
│   └── views.py
├── manage.py
└── proj
    ├── settings.py
    ├── urls.py
    └── wsgi.py

(__init__.pyなどは省略してます)

上の管理方法によるメリット

application ごとにtemplatesディレクトリを用意しているので、
View でテンプレートを使用するときにアプリケーション名(正しくは「アプリケーションのディレクトリ名」か)を記述する必要がありません。
つまり、こうです。

views.py

# -*- coding: utf-8 -*-
from django.views.generic.base import View
from django.shortcuts import render


class AppView(View):
    template_name = 'index.html'

    def get(self, request):
        return render(request, self.template_name)

template_name='app/index.html'などとしているドキュメントを良く見ますが、
これだと View を新しく定義するたびにそれぞれにアプリケーション名を書く必要があり、
(あんまりないとはいえ)アプリケーション名を変更したい場合に影響範囲が大きいと考えたのです。

上の管理方法によるデメリット

ただ、この管理方法には重大な欠点があります。
それは、次のような場合です。

上述したようにapp1app2というアプリケーションがあり、
それぞれに index.html という同一ファイル名のテンプレートが定義されているとします。

settings.py (抜粋)

INSTALLED_APPS = (
    'django.contrib.auth',
    ...(略)...
    'app1',
    'app2',
)

urls.py

from django.conf.urls import patterns, include, url


urlpatterns = patterns('',
    url(r'^app1/', include('app1.urls')),
    url(r'^app2/', include('app2.urls')),
)

urls.py (app1)

# -*- coding: utf-8 -*-
from django.conf.urls import patterns, url
from .views import App1View

urlpatterns = patterns('',
    url(r'^', App1View.as_view()),
)

urls.py (app2)

# -*- coding: utf-8 -*-
from django.conf.urls import patterns, url
from .views import App2View

urlpatterns = patterns('',
    url(r'^', App2View.as_view()),
)

views.py (app1)

# -*- coding: utf-8 -*-
from django.views.generic.base import View
from django.shortcuts import render


class App1View(View):
    template_name = 'index.html'

    def get(self, request):
        return render(request, self.template_name)

views.py (app2)

# -*- coding: utf-8 -*-
from django.views.generic.base import View
from django.shortcuts import render


class App2View(View):
    template_name = 'index.html'

    def get(self, request):
        return render(request, self.template_name)

index.html (app1)

<html>
<head></head>
<body>
this is app1
</body>
</html>

index.html (app2)

<html>
<head></head>
<body>
this is app2
</body>
</html>



さて、この状態でプロジェクトを実行してみます。

$ python manage.py runserver

はじめに、http://localhost:8000/app1/ にアクセスしてみます。

f:id:dackdive:20141012233252p:plain

はい、問題ないですね。
app1/templatesindex.htmlが表示されています。

それでは、http://localhost:8000/app2/ にアクセスしてみます。

f:id:dackdive:20141012233300p:plain

あれ...?
app2ではなくapp1templatesディレクトリの方のindex.htmlから読み込んでいるみたいですね。

つまり、複数のアプリケーションで同一テンプレート名を定義すると、先に見つかった方が使用されてしまうんです。

index.html等はどのアプリケーションでも一般的に使用されるテンプレート名だと思うので、
この名前を異なるアプリケーション間でユニークになるようにするというのは現実的ではありません。

というわけで、アプリケーションディレクトリ直下に置いて管理するのはやめようと思いました。

project 直下にテンプレートを移動する

というわけで、templatesディレクトリは project 直下に置くように変更します。
先ほどの例では、ディレクトリ構成を以下のように修正します。

.
├── app1
│   ├── urls.py
│   └── views.py
├── app2
│   ├── urls.py
│   └── views.py
├── manage.py
├── manage.py
└── proj
    ├── __init__.py
    ├── settings.py
    ├── templates
    │   ├── app1
    │   │   └── index.html
    │   └── app2
    │       └── index.html
    ├── urls.py
    └── wsgi.py

そうすると、上で述べたように、View内でtemplate_nameを記載する時にはアプリケーション名が必要です。

views.py (app1)

# -*- coding: utf-8 -*-
from django.views.generic.base import View
from django.shortcuts import render


class App1View(View):
    # アプリケーション名が必要
    template_name = 'app1/index.html'

    def get(self, request):
        return render(request, self.template_name)

1つのアプリケーションで使用する View が増えると、そのたびにtemplate_nameにアプリケーション名を書かないといけません。

これをどうにかしたい。

改善策1: アプリケーション名は1ヶ所にまとめる

「Viewごとにtemplate_nameにアプリケーション名直書き」状態を回避する方法を考えてみます。

一般的には、定数としてくくりだしてしまって、template_nameでそれを利用する方法ですね。
というわけで今回は__init__.py

# -*- coding: utf-8 -*-
APP_LABEL = 'app1'

と書いてみました。

そしてView側からこれを import します。

# -*- coding: utf-8 -*-
from django.views.generic.base import View
from django.shortcuts import render

from . import APP_LABEL

class App1View(View):
    template_name = '%s/index.html' % APP_LABEL

    def get(self, request):
        return render(request, self.template_name)

これで、Viewのあちこちにアプリケーション名埋め込んで保守が大変、ということはなくなりました。

今回の例では、APP_LABELはもちろんviews.pyの先頭で定義しちゃっても問題ないんですが
その他のファイルで使う可能性を考えて__init__.pyに書いてみました。

改善策2: urls.py の app_name を使用する

せっかくなので、もう1個別の方法を考えてみます。
以前、テンプレートのタグ内でアプリケーション名を取得する方法について書きました。

これを View にも使ってみます。

まず、urls.pyapp_nameを定義しましょう。

urls.py

from django.conf.urls import patterns, include, url


urlpatterns = patterns('',
    url(r'^app1/', include('app1.urls', app_name='app1')),
    url(r'^app2/', include('app2.urls')),
)

そして View 側を次のように変更します。

views.py

# -*- coding: utf-8 -*-
from django.views.generic.base import View
from django.shortcuts import render

class App1View(View):
    template_name = '{app_name}/index.html'

    def get(self, request):
        self.template_name = self.template_name.format(app_name=request.resolver_match.app_name)
        return render(request, self.template_name)

これでもいいですね。

どっちがいいのか?

1 でも 2 でもとりあえず View のあちこちにアプリケーション名を直書きするという事態は回避できています。
どちらがいいか、今のところは判断がつかないんですが、

  • 1 だとapp_nameの他にAPP_LABELを定義する必要がある
  • 2 だと(逆に)アプリケーションのディレクトリ名=app_nameという制約がある
  • 2 だと毎回 View の先頭で{app_name}を置換する必要がある

というあたりが両者を比較した時に感じたところです。

個人的には特に最後に挙げたものが気になるかな...って感じなので
しばらくはAPP_LABELに書く方法で実装してみようかなと思いました。




注記

(*1)
1, 2の方法はsettings.pyTEMPLATE_LOADERS
以下のように2つの loader を定義していることが前提となっています。

settings.py

TEMPLATE_LOADERS = (
    'django.template.loaders.filesystem.Loader',  # 1. の方法を使う場合
    'django.template.loaders.app_directories.Loader',  # 2. の方法を使う場合
)

また、1 のproject直下に配置する場合は
同じくsettings.pyTEMPLATE_DIRS絶対パスで指定する必要があります。

# Django settings for proj project.
import os

...(略)...

ROOT_PATH = os.path.dirname(__file__)
TEMPLATE_DIRS = (
    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
    os.path.join(ROOT_PATH, 'templates'),
)

参考: https://docs.djangoproject.com/en/1.5/ref/templates/api/#loader-types

(2015/11/06追記)
Django 1.8 から設定方法が変わっているみたいです。
https://docs.djangoproject.com/en/1.8/topics/templates/

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            # ... some options here ...
        },
    },
]

DIRS defines a list of directories where the engine should look for template source files, in search order.
APP_DIRS tells whether the engine should look for templates inside installed applications. Each backend defines a conventional name for the subdirectory inside applications where its templates should be stored.