TemplateView
Web サービスを作るとしたら、サーバ処理の大半は HTML テンプレートを読み込んでそこに表示用変数をあてはめていくことになると思います。
そのために Django で用意されているのが、TemplateView (django.views.generic.base.TemplateView)。シンプルなクラスながら、ページ生成に必要な機能が含まれているとても便利なビュークラスです。
クラスベースドビュー形式でビューを作る場合、ブラウザから HTTP GET されるビューは、ほとんどが TemplateView で書かれることになるでしょう。
POST の場合は、FormView を使ったりとか、 生 View で処理して HttpResponseRedirect を返す…の処理が多いと思います。
(サービス内容によりますね)
TemplateView は、Python の言語特性もあり 様々なパターンで書くことができます。どの書き方が良いかはケースバイケースになりますが、僕がよく書くいくつかの方法を書きます。
前提条件 (環境)
+ manage.py
+ myapp
+ models.py
+ urls.py
+ views.py
+ templates
+ myapp
+ index.html
このようなディレクトリ構成になっているとします。
また、settings.INSTALLED_APPS
に myapp
が含まれており、settings.TEMPLATE_LOADERS
に django.template.loaders.app_directories.Loader
が含まれているものとします。
1. urls で as_view に引数を与えるパターン
基本
TemplateView
に限らず、View
( django.views.generic.base.View
) の機能なのですが、コンストラクタに与えられたキーワード引数をインスタンス変数に自動的に格納するという機能があります。as_view()
でビュー関数化する際、as_view()
の引数は通常すべてビュークラスのコンストラクタ引数になります。
そして、TemplateView
は、デフォルトではインスタンス変数の template_name
をテンプレートとして使うため、
urlpatterns = [
url(r'^myapp/$', TemplateView.as_view(template_name='myapp/index.html'),
name='myapp-index'),
...
こう書くと、ブラウザで myapp/ をリクエストした際に myapp/index.html の内容が評価され、ブラウザに出力されます。
as_view()
実行時、デコレータのようにラッピングされたビュー関数が生成されますが、キーワード引数はその中に束縛され、実際にユーザーがリクエストした際に TemplateView(template_name='myapp/index.html')
相当のコンストラクト処理が動き、get メソッドが動いてレスポンスが作られます。
応用
TemplateView 、というか ContextMixin の機能として、インスタンス化されたビュークラスを view という名前のテンプレート引数とするため、
urlpatterns = [
url(r'^myapp/a/$', TemplateView.as_view(template_name='myapp/index.html', mode='a'),
name='myapp-a'),
url(r'^myapp/b/$', TemplateView.as_view(template_name='myapp/index.html', mode='b'),
name='myapp-b'), ...
このような urls にしておき、テンプレートで
{% if view.mode == 'a' %}
モードA表示
{% elif view.mode == 'b' %}
モードB表示
{% endif %}
このように表示を分岐させることもできます。
また、View の機能として、インスタンス引数に request, kwargs などを自動でつけますので、
urlpatterns = [
url(r'^myapp/(?P<mode_name>\w+)/$', TemplateView.as_view(template_name='myapp/index.html'),
name='myapp-index'),
...
{{ view.kwargs.mode_name }} モード<br />
こんにちは! {{ view.request.user.username }}
このような表示も、urls 以外の Python コードを書かずにテンプレートだけで実現できます。
2. get メソッドをオーバーライドするパターン
親の get を呼ばずに、get 内でテンプレートコンテキストを作るパターン
手続き型でビュー処理を書きたい時によくやるパターンです。
class IndexView(TemplateView):
template_name = 'myapp/index.html'
def get(self, request, **kwargs):
手続き型処理 ...
context = {
'items': ....
}
return self.render_to_response(context)
直感的に処理が理解しやすい形です。
複雑な処理になると、メソッドが縦に長くなってしまうので、そのような場合は処理を他のクラスに出すと良いでしょう。モデルを扱う場合、大抵はモデルにメソッドを生やすか、モデルマネージャ ( MyAppModel.objects ← こいつ) にメソッドを生やすとうまくいくと思います。
モデルマネージャ を延長し、メソッドを生やす例は、django.contrib.auth.models の AbstractUser が UserManager をマネージャとして使ってるあたりが参考になると思います。
さて、上図のように書いてしまった場合、テンプレートから view でビューインスタンスを参照できなくなってしまうため、テンプレートから view を参照したい場合は
context = {
'view': self,
'items': ....
}
このように明示的に view を入れても良いでしょう。
個人的には、TemplateView の get メソッドをオーバーライドしたくなった時は、本当に必要なのか少し落ち着いて考えてみるべきだと思っています。モデルインスタンスの取得であれば、後述の get_context_data をオーバーライドするなり view にメソッド生やすなりした方が、各処理を疎にでき、変数のスコープも小さくなるため処理の副作用のリスクも小さくできます。
親の get を呼ぶパターン
class IndexView(TemplateView):
template_name = 'myapp/index.html'
def get(self, request, **kwargs):
if XXX:
return HttpResponseBadRequest(...)
return super().get(request, **kwargs)
# python2: return super(IndexView, self).get(request, **kwargs)
こちらの方がオブジェクト指向言語で見慣れた形ですね。
テンプレートコンテキストをいじるパターンではないので、できることは限られ、用途が限定されます。リクエストにエラーを返す処理だけ書きたい時によく使います。
3. get_context_data をオーバーライドするパターン
get をオーバーライドするのはロジックが汚れる気がしてなんかやだ、という場合は、ContextMixin のメソッド get_context_data をオーバーライドするのも良いでしょう。
class IndexView(TemplateView):
template_name = 'myapp/index.html'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['items'] = ...
ctx[xxx] = ...
return ctx
このようになります。
TemplateView を延長した基底クラスを作り、さらにそれを延長して(多段で継承して) 実際のビュークラスを作る場合なんかは、get をオーバーライドするより get_context_data をオーバーライドするように書いていった方が簡潔に読めるような気がします。
さて、get をオーバーライドした場合と違って、こちらは request 引数にアクセスできないように見えますが、View の機能で request はインスタンス変数についてますので、
class IndexView(TemplateView):
template_name = 'myapp/index.html'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['authenticated'] = self.request.user.is_authenticated
ctx[xxx] = self.request.session.get(...)
return ctx
このように、問題なく request も扱えます。
get_context_data の引数の kwargs には、urls.py で指定した名前つき正規表現のプレースホルダにマッチした内容が入ってくるわけですが、この内容は self.kwargs にも入っていてアクセスできるので少し冗長な気がします。
4. そして cached_property
View クラスは、リクエスト時にインスタンスが生成されるため、cached_property
(django.utils.functional.cached_property) と非常に相性が良いです。
class IndexView(TemplateView):
template_name = 'myapp/index.html'
@cached_property
def items(self):
"""所持アイテム一覧"""
return Item.objects.filter(user=self.request.user)
@cached_property
def power_total(self):
"""パワー合計"""
return sum(i.power for i in self.items)
@cached_property
def power_average(self):
"""パワー平均"""
return self.power_total / len(self.items)
def _get_special_items(self):
"""アイテムの中から、スペシャルアイテムだけを抽出するジェネレータ"""
for item in self.items:
if item.is_not_special:
continue
if ....:
continue
yield item
@cached_property
def special_power_total(self):
"""スペシャルアイテムのパワー合計"""
return sum(i.power for i in self._get_special_items())
@cached_property
def special_power_rate(self):
"""スペシャルアイテムと全アイテムのパワー合計値比率"""
return self.special_power_total / self.power_total
合計 power: {{ view.power_total }}
合計スペシャル power: {{ view.special_power_total }}
スペシャル率: {{ view.special_power_rate }}
まあ、この例では viewに書かずに Item を総括するクラスを別で作れよ! と言われますが、例としてこんな感じでも書けます。
cached_property を使うと、特に意識せずともビュー内で重い処理が1回しか走らないことが保障されるので、見通しも良く処理速度にも良いです。処理の上下関係を気にする必要もありません。
例えば、この例では items メソッドに @cached_property
がついているため Item モデルを検索する SQL は1回しか発行されず、全 item の power の合計をsum する処理も 1回しか行われないのがすぐにわかります。
Python ぽいコードの書き方になっている思うのですが、どうでしょうか。
HTML 生成結果のキャッシング
HTML 生成結果を共通キャッシュにキャッシングするるには、Django ドキュメントにもある通り django.views.decorators.cache_page
デコレータを使うのが良いでしょう。
as_view した結果をデコレートできますので
urlpatterns = [
url(r'^myapp/(?P<mode_name>\w+)/$',
cache_page(3600)(TemplateView.as_view(template_name='myapp/index.html')),
name='myapp-index'),
...
だったり
class IndexView(TemplateView):
template_name = 'myapp/index.html'
....
index_view = cache_page(3600)(IndexView.as_view())
urlpatterns = [
url(r'^myapp/(?P<mode_name>\w+)/$',
'index_view',
name='myapp-index'),
...
だったりが、良く使うパターンだと思います。login_required なんかも同様ですね。
class MyPageView.as_view(TemplateView):
template_name = 'mypage/index.html'
....
mypage_view = login_required(MyPageView.as_view())
django.utils.decorators.method_decorator を使うと、デコレータ関数をバウンドメソッドに適用できるよう変形してくれるので、
class IndexView(TemplateView):
template_name = 'myapp/index.html'
@method_decorator(cache_page(3600))
def get(....):
....
get メソッドをデコレートしたい場合はこういうのも良いでしょう。
get メソッドをオーバーライドするのが嫌な場合は、オーバーライドした as_view 内でかけるという手もあります。
class IndexView(TemplateView):
template_name = '...'
@classonlymethod
def as_view(cls, **initkwargs):
return cache_page(3600)(
super().as_view(**initkwargs)
HTTP POST を処理したいケース
HTML を返す web アプリで、 TemplateView
に、def post(...)
を書きそうになったら、ひとまず落ち着きましょう。
おそらく、TemplateView ではなく、FormView の方が適しているのではないでしょうか。
FormView は、Django のフォームと連携して「フォームにエラーがあった際にエラー表示を表示しつつ再入力を促す」という機能が入ってます。
「フォームにエラーがなければ◯◯処理をする」 も、テンプレートメソッドとして用意されています。検討してみてはいかがでしょうか。
追記 CRUD ジェネリックビュー
CRUD ジェネリックビューについて、自社ブログに書きました。
Django の CRUD ジェネリックビュー (ListView, DetailView, CreateView, UpdateView, DeleteView) の簡単な使い方