Django generics view 以及看源碼爲何這麼重要

關於閱讀代碼

咱們知道,得益於python的語言特性,python的源碼是直接能夠看的到的,而django是一種大而全的東東,雖然django的文檔看似全面,但實際上有些模塊寫的確實不怎麼樣,並且平時遇到的需求也是多變的,有的時候你須要實現某種詭異的功能,固然咱們能夠用google或者作github的搬運工,可是有的時候框架已經給你作了不少事情,你只須要在多作那麼一丟丟就能實現某種需求,這個時候看代碼比去扒github是快的多的。html

先稍微扯一下Django generics view

這玩意兒一般你百度或者google的時候,大部分時間會跟你講這是Class-based viewspython

第一次接觸的時候,我以爲他牛逼的地方在於只要在子類裏面定義get post方法,就不須要寫相似這種東西了android

if request.method.lower() == 'get':
        do_something()
    else:
        do_otherthing()

其實與普通的method view不一樣的是,他的父類定義了一堆有用的方法,當你知道這些的時候你能夠少些超多的代碼來作一些屌屌的事情。git

來看下使用示例

防止文件被刪除,貼幾段過來github

class BaseMixin(object):
    
    def get_context_data(self,*args,**kwargs):
        context = super(BaseMixin,self).get_context_data(**kwargs)
        try:
            #熱門文章
            context['hot_article_list'] = Article.objects.order_by("-view_times")[0:10]
            #導航條
            context['nav_list'] =  Nav.objects.filter(status=0)
            #最新評論
            context['latest_comment_list'] = Comment.objects.order_by("-create_time")[0:10]

        except Exception as e:
            logger.error(u'[BaseMixin]加載基本信息出錯')

        return context


class IndexView(BaseMixin,ListView):
    template_name = 'blog/index.html'
    context_object_name = 'article_list'
    paginate_by = PAGE_NUM #分頁--每頁的數目
    
    def get_context_data(self,**kwargs):
        #輪播
        kwargs['carousel_page_list'] = Carousel.objects.all()
        return super(IndexView,self).get_context_data(**kwargs)

    def get_queryset(self):
        article_list = Article.objects.filter(status=0)
        return article_list

這是項目原來的地址web

若是你以前只寫過method的view,你可能會以爲很詭異,爲毛這段代碼定義了幾個屬性,重寫了幾個方法就實現了我以前寫的那麼一大坨長長的代碼?這種時候就必須看源碼了,比文檔來的直觀。django

generic view代碼講解

OK先貼一波源碼,django的generic view有不少,就講下最多見的list。app

這是 base.py裏面的東東框架

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', '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 kwargs.iteritems():
            setattr(self, key, value)

    @classonlymethod
    def as_view(cls, **initkwargs):
        """
        Main entry point for a request-response process.
        """
        # sanitize keyword arguments
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError(u"You tried to pass in the %s method name as a "
                                u"keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError(u"%s() received an invalid keyword %r" % (
                    cls.__name__, key))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            return self.dispatch(request, *args, **kwargs)

        # 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

    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
        self.request = request
        self.args = args
        self.kwargs = kwargs
        return handler(request, *args, **kwargs)

    def http_method_not_allowed(self, request, *args, **kwargs):
        allowed_methods = [m for m in self.http_method_names if hasattr(self, m)]
        logger.warning('Method Not Allowed (%s): %s', request.method, request.path,
            extra={
                'status_code': 405,
                'request': self.request
            }
        )
        return http.HttpResponseNotAllowed(allowed_methods)

這是list.py裏面的東東iphone

class MultipleObjectMixin(object):
    allow_empty = True
    queryset = None
    model = None
    paginate_by = None
    context_object_name = None
    paginator_class = Paginator

    def get_queryset(self):
        """
        Get the list of items for this view. This must be an interable, and may
        be a queryset (in which qs-specific behavior will be enabled).
        """
        if self.queryset is not None:
            queryset = self.queryset
            if hasattr(queryset, '_clone'):
                queryset = queryset._clone()
        elif self.model is not None:
            queryset = self.model._default_manager.all()
        else:
            raise ImproperlyConfigured(u"'%s' must define 'queryset' or 'model'"
                                       % self.__class__.__name__)
        return queryset

    def paginate_queryset(self, queryset, page_size):
        """
        Paginate the queryset, if needed.
        """
        paginator = self.get_paginator(queryset, page_size, allow_empty_first_page=self.get_allow_empty())
        page = self.kwargs.get('page') or self.request.GET.get('page') or 1
        try:
            page_number = int(page)
        except ValueError:
            if page == 'last':
                page_number = paginator.num_pages
            else:
                raise Http404(_(u"Page is not 'last', nor can it be converted to an int."))
        try:
            page = paginator.page(page_number)
            return (paginator, page, page.object_list, page.has_other_pages())
        except InvalidPage:
            raise Http404(_(u'Invalid page (%(page_number)s)') % {
                                'page_number': page_number
            })

    def get_paginate_by(self, queryset):
        """
        Get the number of items to paginate by, or ``None`` for no pagination.
        """
        return self.paginate_by

    def get_paginator(self, queryset, per_page, orphans=0, allow_empty_first_page=True):
        """
        Return an instance of the paginator for this view.
        """
        return self.paginator_class(queryset, per_page, orphans=orphans, allow_empty_first_page=allow_empty_first_page)

    def get_allow_empty(self):
        """
        Returns ``True`` if the view should display empty lists, and ``False``
        if a 404 should be raised instead.
        """
        return self.allow_empty

    def get_context_object_name(self, object_list):
        """
        Get the name of the item to be used in the context.
        """
        if self.context_object_name:
            return self.context_object_name
        elif hasattr(object_list, 'model'):
            return smart_str('%s_list' % object_list.model._meta.object_name.lower())
        else:
            return None

    def get_context_data(self, **kwargs):
        """
        Get the context for this view.
        """
        queryset = kwargs.pop('object_list')
        page_size = self.get_paginate_by(queryset)
        context_object_name = self.get_context_object_name(queryset)
        if page_size:
            paginator, page, queryset, is_paginated = self.paginate_queryset(queryset, page_size)
            context = {
                'paginator': paginator,
                'page_obj': page,
                'is_paginated': is_paginated,
                'object_list': queryset
            }
        else:
            context = {
                'paginator': None,
                'page_obj': None,
                'is_paginated': False,
                'object_list': queryset
            }
        context.update(kwargs)
        if context_object_name is not None:
            context[context_object_name] = queryset
        return context


class BaseListView(MultipleObjectMixin, View):
    def get(self, request, *args, **kwargs):
        self.object_list = self.get_queryset()
        allow_empty = self.get_allow_empty()
        if not allow_empty and len(self.object_list) == 0:
            raise Http404(_(u"Empty list and '%(class_name)s.allow_empty' is False.")
                          % {'class_name': self.__class__.__name__})
        context = self.get_context_data(object_list=self.object_list)
        return self.render_to_response(context)

首先是爲何不須要寫if else判斷,而在子類裏面定義get post便可,能夠看到是下面的dispatch方法作了通用的處理。

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
        self.request = request
        self.args = args
        self.kwargs = kwargs
        return handler(request, *args, **kwargs)

而通用的listview實際上就是一個父類實現了get方法,其中的MultipleObjectMixin實現了超多的方法供你重寫,基本上大部分時間咱們重寫MultipleObjectMixin裏面的對應方法以及設置合適的屬性就能夠完成大部分的業務邏輯。

BaseListView首先經過 self.get_queryset() 方法拿到了你須要list的對象,中間一坨是分頁的東東,下面的context則是最後須要render_to_response時傳的參數,這樣看就跟之前本身經過method view寫的東西一一對應了。而咱們只須要根據不一樣的業務邏輯實現裏面的覆蓋實現裏面的方法便可,有些連覆蓋都不須要是要在設置合適的屬性,到這裏我就不須要根據具體的方法具體講解了,你們本身看。

class BaseListView(MultipleObjectMixin, View):
    def get(self, request, *args, **kwargs):
        self.object_list = self.get_queryset()
        allow_empty = self.get_allow_empty()
        if not allow_empty and len(self.object_list) == 0:
            raise Http404(_(u"Empty list and '%(class_name)s.allow_empty' is False.")
                          % {'class_name': self.__class__.__name__})
        context = self.get_context_data(object_list=self.object_list)
        return self.render_to_response(context)

一個現實中的例子

上面僅僅是如何使用django默認的各類generic view,可是有時候默認的機制並不能實現你的需求,例如如今在移動端的大潮下,html配飾移動端通常有兩種方案:1.響應式;2.兩套頁面徹底分開。兩種方案各有利弊,若是咱們須要根據根據user agent來作不一樣終端的頁面渲染,甚至是不一樣的邏輯(web和移動端邏輯不一樣這很正常),這種時候很容易想到重寫dispatch方法,來作到一種通用的處理方式。

from django.views.generic import View as DjangoView

class View(DjangoView):

    def _get_handler(self, request):
        ''' 
        根據ua獲取handler
        '''
        handler_name = request.method.lower()
        if handler_name in self.http_method_names:
            handler = getattr(self, handler_name)
            if not handler:
                return self.http_method_not_allowed

        user_agent = request.META.get('HTTP_USER_AGENT', '').lower()
        handler = getattr(self, handler_name, self.http_method_not_allowed)
        user_agents = ['ipad', 'iphone', 'ipod', 'androidtv', 'android']
        for ua in user_agents:
            if ua in user_agent:
                handler_name = '{}_{}'.format(handler_name, ua) 
                break
        return getattr(self, handler_name) or handler
        
    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.
        # 支持user agent跳轉
        # 若是實現了對應的方法,則直接使用對應ua的規則
        # 例如method=get, ua爲iphone的ua,子類實現 get_iphone, 則使用get_iphone進行render
        handler = self._get_handler(request)
        self.request = request
        self.args = args
        self.kwargs = kwargs
        return handler(request, *args, **kwargs)
相關文章
相關標籤/搜索