269
279

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 5 years have passed since last update.

Django でまず覚えたい TemplateView のパターン

Last updated at Posted at 2016-01-08

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_APPSmyapp が含まれており、settings.TEMPLATE_LOADERSdjango.template.loaders.app_directories.Loader が含まれているものとします。

1. urls で as_view に引数を与えるパターン

基本

TemplateView に限らず、View ( django.views.generic.base.View ) の機能なのですが、コンストラクタに与えられたキーワード引数をインスタンス変数に自動的に格納するという機能があります。as_view() でビュー関数化する際、as_view() の引数は通常すべてビュークラスのコンストラクタ引数になります。

そして、TemplateView は、デフォルトではインスタンス変数の template_name をテンプレートとして使うため、

urls.py

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 という名前のテンプレート引数とするため、

urls.py

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 にしておき、テンプレートで

myapp/index.html

{% if view.mode == 'a' %}
  モードA表示
{% elif view.mode == 'b' %}
  モードB表示
{% endif %}

このように表示を分岐させることもできます。

また、View の機能として、インスタンス引数に request, kwargs などを自動でつけますので、

urls.py

urlpatterns = [
    url(r'^myapp/(?P<mode_name>\w+)/$', TemplateView.as_view(template_name='myapp/index.html'),
        name='myapp-index'),
    ...
myapp/index.html
{{ view.kwargs.mode_name }} モード<br />
こんにちは! {{ view.request.user.username }}

このような表示も、urls 以外の Python コードを書かずにテンプレートだけで実現できます。

2. get メソッドをオーバーライドするパターン

親の get を呼ばずに、get 内でテンプレートコンテキストを作るパターン

手続き型でビュー処理を書きたい時によくやるパターンです。

myapp/views.py
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 を呼ぶパターン

myapp/views.py
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 をオーバーライドするのも良いでしょう。

myapp/views.py
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 はインスタンス変数についてますので、

myapp/views.py
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) と非常に相性が良いです。

myapp/views.py
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
myapp/index.html
合計 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 した結果をデコレートできますので

urls.py

urlpatterns = [
    url(r'^myapp/(?P<mode_name>\w+)/$',
        cache_page(3600)(TemplateView.as_view(template_name='myapp/index.html')),
        name='myapp-index'),
    ...

だったり

views.py
class IndexView(TemplateView):
    template_name = 'myapp/index.html'
    ....

index_view = cache_page(3600)(IndexView.as_view())
urls.py
urlpatterns = [
    url(r'^myapp/(?P<mode_name>\w+)/$',
        'index_view',
        name='myapp-index'),
    ...

だったりが、良く使うパターンだと思います。login_required なんかも同様ですね。

views.py
class MyPageView.as_view(TemplateView):
    template_name = 'mypage/index.html'
    ....

mypage_view = login_required(MyPageView.as_view())

django.utils.decorators.method_decorator を使うと、デコレータ関数をバウンドメソッドに適用できるよう変形してくれるので、

views.py
class IndexView(TemplateView):
    template_name = 'myapp/index.html'
    
    @method_decorator(cache_page(3600))
    def get(....):
        ....

get メソッドをデコレートしたい場合はこういうのも良いでしょう。

get メソッドをオーバーライドするのが嫌な場合は、オーバーライドした as_view 内でかけるという手もあります。

views.py
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) の簡単な使い方

269
279
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
269
279

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?