[Django] TemplateViewについて詳しく見てみる

Djangoでクラスベースビューを利用するときにまず、最初に使うことになるのがTemplateViewだと思うのですが、今回はこのTemplateViewがどんな実装になっているのか見てみたいと思います。

Django(GitHub)
https://github.com/django/django

TemplateView

TemplateViewの実装はdjango/django/views/generic/base.pyに書いてあります。

その中から重要そうなところを一部抜粋

class TemplateView(TemplateResponseMixin, ContextMixin, View):

    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)

何やら、三つほどクラスを継承しています。

それぞれ見ていきましょう。
まずは、Viewから

View

class View:
    """
    Intentionally simple parent class for all views. Only implements
    dispatch-by-method and simple sanity checking.
    """

    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)

    @classonlymethod
    def as_view(cls, **initkwargs):
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError("You tried to pass in the %s method name as a "
                                "keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            self.setup(request, *args, **kwargs)
            if not hasattr(self, 'request'):
                raise AttributeError(
                    "%s instance has no 'request' attribute. Did you override "
                    "setup() and forget to call super()?" % cls.__name__
                )
            return self.dispatch(request, *args, **kwargs)
        view.view_class = cls
        view.view_initkwargs = initkwargs

        update_wrapper(view, cls, updated=())

        update_wrapper(view, cls.dispatch, assigned=())
        return view

    def setup(self, request, *args, **kwargs):
        self.request = request
        self.args = args
        self.kwargs = kwargs

    def dispatch(self, request, *args, **kwargs):
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)

    def http_method_not_allowed(self, request, *args, **kwargs):
        logger.warning(
            'Method Not Allowed (%s): %s', request.method, request.path,
            extra={'status_code': 405, 'request': request}
        )
        return HttpResponseNotAllowed(self._allowed_methods())

    def options(self, request, *args, **kwargs):
        response = HttpResponse()
        response['Allow'] = ', '.join(self._allowed_methods())
        response['Content-Length'] = '0'
        return response

    def _allowed_methods(self):
        return [m.upper() for m in self.http_method_names if hasattr(self, m)]

何やら、いっぱい書いてありますが、要はas_viewメソッドで、クラスビューを関数ビューへ変換するようなことをやっています。

もう少し詳しい説明を付け加えると、dispatchメソッドで、httpリクエストのメソッドの種類が正常なものかをチェックし、対応するメソッドを返す処理を行っています。

例えば、
GETメソッドでリクエストが来た場合、自身のgetメソッドを返す。
その時、自身にgetメソッドが定義されていなければ、ステータスコード405を返す。

同様に、POSTメソッドでリクエストが来た場合、自身のpostメソッドを返す。
その時、自身にpostメソッドが定義されていなければ、ステータスコード405を返す。

というような処理を行っています。

また、http_method_namesで利用できるメソッドをコントロールしているため、特定のメソッドのみを許可したい場合には継承先のクラスでhttp_method_namesの値を上書きしてあげればよいです。

例えば、

class OnlyGetView(View):
    http_method_names = ["get"]

とすることで実現できます。

※Django2.2よりsetupメソッドが追加されました。

このメソッドはdispatchメソッドより前に呼び出されて、requestkwargs,argsなどの属性をViewクラスに付加しています。

Viewクラスに属性を付加したい場合、2.2系以前ではdispatchメソッドをオーバーライドして実装することが多かったですが、2.2系以降ではsetupをオーバーライドして属性の追加を行った方が良さそうです。

続いて、ContextMixinを見てみます。

ContextMixin


class ContextMixin:

    extra_context = None

    def get_context_data(self, **kwargs):
        kwargs.setdefault('view', self)
        if self.extra_context is not None:
            kwargs.update(self.extra_context)
        return kwargs

ContextMixinでは、テンプレートファイルに渡す、変数を追加しています。

継承先のクラスで、get_context_dataメソッドをオーバーライド又は、クラス変数extra_contextを上書きしてあげれば、テンプレートに渡す変数を追加できます。

オーバーライドする場合は、親クラスのget_context_dataを呼んであげないとviewという変数がテンプレート内で使えなくなってしまうので注意。

最後に、TemplateResponseMixinです。

TemplateResponseMixin

class TemplateResponseMixin:
    template_name = None
    template_engine = None
    response_class = TemplateResponse
    content_type = None

    def render_to_response(self, context, **response_kwargs):
        response_kwargs.setdefault('content_type', self.content_type)
        return self.response_class(
            request=self.request,
            template=self.get_template_names(),
            context=context,
            using=self.template_engine,
            **response_kwargs
        )

    def get_template_names(self):
        if self.template_name is None:
            raise ImproperlyConfigured(
                "TemplateResponseMixin requires either a definition of "
                "'template_name' or an implementation of 'get_template_names()'")
        else:
            return [self.template_name]

継承先のクラスでtemplate_nameに利用するテンプレートファイルへのパスを記述します。

template_engineは特に指定しなければ、settings.pyで指定されているテンプレートエンジンが利用されます。デフォルトでは、Djangoテンプレートエンジンが利用されます。

response_classは基本的に触ることはないと思いますが、httpレスポンスを返すようなクラスを指定します。
render関数と同じようなものと思っていただければいいです。

content_typeには、MIME TYPEを入れておきます。
基本的には何も指定しなければ、text/htmlになるはずです。

まとめ

最後に、TemplateViewの実装を見てわかったことを簡単にまとめてみましょう。

利用するテンプレートファイルを指定するには

class CustomView(TemplateView):
    template_name = "app/template.html"

template_nameを書き換える

テンプレートで使える変数を増やすには

class CustomView(TemplateView):
    extra_context = {"model_data":Model.objects.all()}

extra_contextを書き換える
又は、

class CustomView(TemplateView):
    def get_context_data(self,**kw):
        conetext = super().get_context_data(**kw)
        context.update({"model_data":Model.objects.all()})
        return context

get_context_dataをオーバーライド

この時に、親クラスのget_context_dataを呼び出しておかないとデフォルトで使える変数が使えなくなるので注意!

利用できるメソッドを制限するには

class CustomView(TemplateView):
    http_method_names = ["get"]

http_method_namesに許可するメソッドを指定
上記だと、GETメソッドだけを利用可能に

利用するテンプレートエンジンを変更するには

class CustomView(TemplateView):
    template_engine = CustomTemplateEngine

以上、非常に簡単にでしたがDjangoTemplateViewの実装をのぞいてみました。