Django汎用クラスビューの実装を理解する

### 始めに

Django の汎用クラスビューはとても便利ではあるが、内部で何やってるか分からないので最初はハードル高い。

内部処理を把握した上で実装に使えば更に生産性が高まるとの思いから以下の説明を行う。


### ビュー関数の呼び出し

http://docs.djangoproject.jp/en/latest/topics/http/views.html

こちらは分かりやすい。
urlsに関数をマッピングして、リクエストに応じて呼び出す。

views.schedule_list と url を関連付ける

```python:scheduler.urls.py
from django.conf.urls import patterns, include, url
import views

urlpatterns = patterns('',
    url(r'^$', views.schedule_list , name='scheduler_toppage'),
)
```

関数 views.schedule_list の処理

```python:scheduler.views.py
from django.shortcuts import render
from models import Schedule, VisitorSchedule, MemberSchedule, HelperSchedule
from datetime import datetime


def schedule_list(request):
    schedule_list = Schedule.objects.filter(event_date__gte=datetime.now()).select_related()

    for schedule in schedule_list:
        visitor_schedules = VisitorSchedule.objects.filter(schedule_id=schedule.id).select_related()
        schedule.visitor_schedules = visitor_schedules

        member_schedules = MemberSchedule.objects.filter(schedule_id=schedule.id)
        schedule.member_schedules = member_schedules

        helper_schedules = HelperSchedule.objects.filter(schedule_id=schedule.id)
        schedule.helper_schedules = helper_schedules

    return render(request, 'scheduler/schedule_list.html', {'schedule_list': schedule_list})
```

### 汎用クラスビューの呼び出し

一方、汎用クラスビューは開発は簡単だがよくわからない・・・
as_view って何？

```python:sample_app.urls
from django.conf.urls import patterns, url, include
from django.views.generic import ListView

urlpatterns = patterns('',
    (r'^items/$', ListView.as_view()),
)
```

```python:sample_app.views
from django.views.generic import ListView
from sample_app.models import Item

class ItemView(ListView):
    model = Item
```


### as_view は何をしてるのか

as_view は汎用クラスビューの基底クラスに定義されている。
以下はdjango1.9.7 のViewクラス

平たく言うと、dispatchのところで、HTTPメソッド(GETとかPOSTとか)に対応した名前のViewメソッドを呼ぶように書かれている。（GETメソッドなら、def getを呼ぶ）

```python:django.views.generic.base.View
class View(object):
    """
    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):
        """
        Constructor. Called in the URLconf; can contain helpful extra
        keyword arguments, and other things.
        """
        # Go through keyword arguments, and either save their values to our
        # instance, or raise an error.
        for key, value in six.iteritems(kwargs):
            setattr(self, key, value)

    # このメソッドをurlと関連付ける
    @classonlymethod
    def as_view(cls, **initkwargs):
        """
        Main entry point for a request-response process.
        """
        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.request = request
            self.args = args
            self.kwargs = kwargs
            return self.dispatch(request, *args, **kwargs)
        view.view_class = cls
        view.view_initkwargs = initkwargs

        # take name and docstring from class
        update_wrapper(view, cls, updated=())

        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
        update_wrapper(view, cls.dispatch, assigned=())
        return view

    # このメソッドでHTTPリクエストに応じたViewのメソッドを呼び出すよう、定義されている
    # 例えば、GETメソッドで呼び出されたら、def getというViewクラス内のメソッドを呼び出す
    # 以下に実例を書いたので参照の事
    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)

    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 http.HttpResponseNotAllowed(self._allowed_methods())

    def options(self, request, *args, **kwargs):
        """
        Handles responding to requests for the OPTIONS HTTP verb.
        """
        response = http.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)]
```

例えばdjango.views.generic.list.ListViewを継承したViewクラスのas_viewを、
HTTP GETメソッドで呼び出すと、ListViewが継承している、BaseListViewのgetメソッドが呼び出される。


```python:django.views.generic.list

class BaseListView(MultipleObjectMixin, View):
    """
    A base view for displaying a list of objects.
    """
    def get(self, request, *args, **kwargs):
        self.object_list = self.get_queryset()
        allow_empty = self.get_allow_empty()

        if not allow_empty:
            # When pagination is enabled and object_list is a queryset,
            # it's better to do a cheap query than to load the unpaginated
            # queryset in memory.
            if (self.get_paginate_by(self.object_list) is not None
                    and hasattr(self.object_list, 'exists')):
                is_empty = not self.object_list.exists()
            else:
                is_empty = len(self.object_list) == 0
            if is_empty:
                raise Http404(_("Empty list and '%(class_name)s.allow_empty' is False.")
                        % {'class_name': self.__class__.__name__})
        context = self.get_context_data()
        return self.render_to_response(context)

class ListView(MultipleObjectTemplateResponseMixin, BaseListView):
    """
    Render some list of objects, set by `self.model` or `self.queryset`.
    `self.queryset` can actually be any iterable of items, not just a queryset.
    """

```

この内容を理解すれば、汎用クラスビューを利用した上で色々応用が効くようになる。