Django Tips 1 – 汎用view(Genericview)でOverrideするときのメモ

最近Djangoを使って開発をすることが多いのだが、日本語のドキュメントがあまりなくてちょっと行き詰まったところを自分なりにまとめてみた。

環境
Python 3.6.3
Django 2.2.3

汎用viewの動き

ListViewやDetailViewをはじめとした汎用viewは従来の関数型viewの実装とは異なり、get,postをif文で分岐をさせる必要がない。その代わり、getやpostがメソッドとして定義されており、dispatchと呼ばれるメソッドでHTTPのメソッドを判別して、適切な関数を呼び出すという設計になっている。

base.pyのViewクラスから抜粋。
https://github.com/django/django/blob/master/django/views/generic/base.py

def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        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)

dispatchではHTTPのメソッドを判別してhandlerを返している。この内容に従い、GETメソッドであれば def get() , POSTメソッドであればdef post()が呼び出されることになる。また、Viewクラスではget,postなどのメソッドは実装されていないため、Viewクラスを継承したクラスで実装をしていく必要がある。

base.pyのTemplateViewクラスから抜粋。
https://github.com/django/django/blob/master/django/views/generic/base.py

class TemplateView(TemplateResponseMixin, ContextMixin, View):
    """
    Render a template. Pass keyword arguments from the URLconf to the context.
    """
    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)

TemplateViewではViewクラスとその他のクラスを継承した上で、getメソッドが初めて定義されている。getメソッドが呼び出されたときはget_context_dataでkwagrsを取得し、それらを引数としてrender_to_resposnseを呼び出すというのがもっともSimpleなgetメソッドということになる。

なお、この段階で呼び出されているget_context_dataはContextMixinで下記のように定義されているものを呼び出している。

base.pyのContextMixinクラスから抜粋。
https://github.com/django/django/blob/master/django/views/generic/base.py

class ContextMixin:
    """
    A default context mixin that passes the keyword arguments received by
    get_context_data() as the template context.
    """
    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

後々説明するがこのメソッドをオーバーライドするケースは多いため、ここで初めて定義されているということは覚えておくと良いかもしれない。

こういった設計になっているため、Djangoでトラブルシューティングをする時には

  • HTTPメソッドは何が呼び出されているか
  • 仮にGETメソッドとした場合、getメソッドはどの継承元クラスで定義されているか
  • getメソッドの中でどういった処理が行われているか

    このあたりを見ていくと、なんでここをオーバーライドするのだろう?とか、変更をしたのに意図した挙動をしないときに役にたつだろう。

    ここから先はよくオーバーライドする関数をユースケース別にまとめていく。

    get_context_data

    Templateに渡すデータを定義するためのメソッド。TemplateViewのgetの中で呼び出されており、これからtemplateに渡すcontextを準備するメソッドとなる。基本的に汎用viewでは指定されたmodelのデータがcontextとして渡されるが、そのデータだけでは足りないので追加をしたいときにオーバーライドするメソッドとなる。

    これをオーバーライドするケースでよくあるのは

  • Listviewなどで指定しているmodelとは別のmodelのデータをTemplateで描画したい
  • URLの中に渡されてきたPrimary key(pk)の値を元に処理をしてTemplateにデータを渡したい

    といった例があるだろう。

    下記の例はProjectmanagerというmodelからすべてのオブジェクトを取得して、contextに渡している例となる。

        def get_context_data(self, **kwargs):
            context = super().get_context_data(**kwargs)
            pms = Projectmanager.objects.all()
            context['pms'] = pms
            return context
    

    渡されたデータは下記のような形でtemplate内で利用ができる。

     {% for pm in pms %}
       {{ pm.name}}
     {% endfor}
    

    get_queryset

    Templateに渡すデータをDBから取得するためのメソッド。SingleObjectMixinの中で定義されており、渡されたmodelのデータを取得してくるメソッドとなる。defaultでは特別なフィルタ等はかけずにall()で取得をしてくるが、そのデータを加工をしたいときにオーバーライドするメソッドとなる。

    これをオーバーライドするケースでよくあるのは

  • 絞り込み条件を受け取って、表示するデータを絞り込みたい
  • とあるデータをキーとして並び替えをして表示をしたい

    下記の例はweeklyという外部参照データの中のdateの最大値をlatest_dayとして、latest_dayを降順で並び替えている例となる。

        def get_queryset(self):
            results = Report.objects.annotate(latest_day=Max('weekly__date')).order_by('-latest_day')
            return results
    

    その他、Listviewを全てのデータではなく特定のユーザーのデータのみ表示するといった場合は、filterで該当するユーザーのデータのみ取得するクエリセットに加工してあげることで様々なviewを実装することができる。

    form_valid

    formの入力のvalidationが成功した時に呼び出されるメソッド。form_validでは基本的にはformのvalidationがtrueであればDBへの書き込み動作であるsave()を実行する。defaultの動作ではformで入力をされた値のみ保存をするため、formで入力をされてない値を追記したり、formから入力をされたデータを変更したいケースにオーバーライドするメソッドとなる。

    これをオーバーライドするケースでよくあるのは

  • ユーザー登録をしてもらう際に、ユーザーには入力してもらいたくないが、保存するときには保存したいデータを補完する
  • 入力されたデータからさらにDBを検索にいって、その値をDBに保存をする
  • 子モデルのデータを作成する際に、親モデルとの関係を持たせながら保存をしたい(Foreign key)の補完

    下記の例ではWeeklyReportという子モデルがデータを作成する際に、formから受け取ったデータの中には存在しない親モデルのobjectをpkから取得をして補完するという例。

        def form_valid(self, form):
            form.instance.project_name = Report.objects.get(pk = self.kwargs.get('pk'))
            return super(WeeklyReportCreateView, self).form_valid(form)
    

    なお、formのvalidationが失敗した場合はform_invalidが呼ばれるのでinvalidの際に何かをしたいケースはこちらを利用する。

    get_success_url

    CreateなどのPOSTメソッドが成功した際にどのURLに遷移するかを処理するメソッド。get_success_urlを利用せずに、遷移先が固定である場合はsuccess_url=’url’で変数として保持することも可能。遷移先のurlが動的になるケースにオーバーライドするメソッドとなる。

    これをオーバーライドするケースでよくあるのは

  • 新規作成をした後に、作成したオブジェクトのDetailviewに飛びたい
  • 更新をした後に、更新後のオブジェクトのDetailviewに飛びたい
  • 子モデルのデータを作成した後に、関係のある親モデルのDetailviewに飛びたい
        def get_success_url(self):
            return reverse_lazy('report_detail', kwargs={'pk': self.object.id})
    

    まとめ
    いろいろ調べれば調べるほど、Djangoはよくできてる。ただ、Documentが少ない分最初の習熟には時間がかかるFrameworkかもしれない。ただ、ソースコードが公開されているので、ある程度のレベル以上は読んで理解しろ!というスタンスなのでそれはそれで正しい気がする。

    これからもいじり続けるので何かTipsがあれば書いていこうと思う。Viewは書いたから次はformとかかな。

  • スポンサーリンク