Django 클래스 뷰 및 Mixin

20870 단어 Django
이전 Django 처리 http 요청 프로세스 분석에서 필자는 Django 프레임워크가 WSGI 프로토콜에 따라 Http 요청을 어떻게 처리하는지 상세하게 설명했다.여기서 개발자가 직접 정의한 View를 처리하는 코드는 다음과 같습니다.
# django.core.handlers.base.py

#     
resolver_match = resolver.resolve(request.path_info)
callback, callback_args, callback_kwargs = resolver_match
request.resolver_match = resolver_match

#   view  
if response is None:
    wrapped_callback = self.make_view_atomic(callback)
    try:
        response = wrapped_callback(request, *callback_args, **callback_kwargs)
    except Exception as e:
        response = self.process_exception_by_middleware(e, request)

만약 개발자가 함수 보기(FBV)를 사용한다면 이 코드는 매우 이해하기 쉽다. wrapped백은 이 함수 보기입니다. 직접 호출하면 됩니다.
그러나 클래스 보기라면, Django는 어떻게 클래스 보기를 함수로 바꿉니까?
CBV.as_view()
Django의 루트에서 URL에view를 지정할 때, 클래스 보기 (모든 클래스 보기가 기본 보기 계승되어야 함) 를 사용하려면 asview 방법은 클래스 보기를 하나의 함수로 전환시킨다. 다음에 필자는 as 를 분석해 보겠다.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)

    # as_view          ,      
    @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

    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())

사실 as방법은 방법에서view 함수를 정의했다. 이 함수가 받아들인 매개 변수는 FBV가 받아들인 매개 변수와 같지만 이 함수는 http의method에 따라 구체적인 CBV의method에 분배되는 것이 아니라 디스패치 방법에 맡겨 완성한다.마지막 asview 방법이 이view 함수를 되돌려줍니다.
디스패치 방법에서 개발자가 스스로 정의한 get,post 등 방법에 대응하는 업무 논리를 찾아 실행 결과를 되돌려줍니다. 개발자가 이 방법을 정의하지 않으면 405로 되돌려주는 것은 이 방법이 허용되지 않는다는 것을 의미합니다.
Mixin
FBV에서는 뷰 액세스에 제한을 가하고자 하면 직접 인테리어를 사용할 수 있지만, CBV에서는 직접 인테리어를 사용할 수 없다.
예를 들어django auth에서 제공하는 기본loginrequired 장식기와permissionRequired 장식기는 CBV에서 LoginRequired Mixin과PermissionRequired Mixin이다.
두 Mixin 모두 Access Mixin (이로 인해 발생하는 이상 점프가 같은 URL을 가리킨다) 에 계승되어 있으며, 다음은 그들의 원본 분석을 보겠습니다.
class LoginRequiredMixin(AccessMixin):
    """
    CBV mixin which verifies that the current user is authenticated.
    """
    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            #       AccessMixin
            return self.handle_no_permission()
        return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)


class PermissionRequiredMixin(AccessMixin):
    """
    CBV mixin which verifies that the current user has all specified
    permissions.
    """
    permission_required = None

    def get_permission_required(self):
        """
        Override this method to override the permission_required attribute.
        Must return an iterable.
        """
        if self.permission_required is None:
            raise ImproperlyConfigured(
                '{0} is missing the permission_required attribute. Define {0}.permission_required, or override '
                '{0}.get_permission_required().'.format(self.__class__.__name__)
            )
        if isinstance(self.permission_required, six.string_types):
            perms = (self.permission_required, )
        else:
            perms = self.permission_required
        return perms

    def has_permission(self):
        """
        Override this method to customize the way permissions are checked.
        """
        perms = self.get_permission_required()
        return self.request.user.has_perms(perms)

    def dispatch(self, request, *args, **kwargs):
        if not self.has_permission():
            #       AccessMixin
            return self.handle_no_permission()
        return super(PermissionRequiredMixin, self).dispatch(request, *args, **kwargs)

여러분 보시다시피 이 두 Mixin은 모두 디스패치 방법을 실현했고 실행이 끝난 후에 MRO 그룹의 다음 종류에 따라 디스패치 방법을 사용했습니다.(MRO는 파이톤을 다중으로 계승할 때 사용하는 방법입니다. 잘 모르는 학생은 이 글을 참고하세요. 파이톤의 MRO 알고리즘을 정말 이해합니까?)
간단한 예를 들어 필자가 CBV를 사용자 정의했다고 가정해 보자.
class MyView(LoginRequiredMixin, PermissionRequiredMixin, View)

그럼 디스패치가 순서대로 호출되는 순서는LoginRequiredMixin,PermissionRequiredMixn,AccessMixin(disptach 방법 없음),View입니다.따라서 기본 뷰는 다중 계승의 맨 뒤에 두어야 한다. 왜냐하면 디스패치 방법의 호출은 업무 논리를 대표하는handler이기 때문이다.
LoginRequiredMixin이나PermissionRequiredMixin이AccessMixin을 계승하는handleno_permission 방법 시 raiseexception이 True로 설정되어 Permission Denied의 이상을 던집니다. 그렇지 않으면 login 으로 이동합니다.url.
class AccessMixin(object):
    """
    Abstract CBV mixin that gives access mixins the same customizable
    functionality.
    """
    login_url = None
    permission_denied_message = ''
    raise_exception = False
    redirect_field_name = REDIRECT_FIELD_NAME

    def get_login_url(self):
        """
        Override this method to override the login_url attribute.
        """
        login_url = self.login_url or settings.LOGIN_URL
        if not login_url:
            raise ImproperlyConfigured(
                '{0} is missing the login_url attribute. Define {0}.login_url, settings.LOGIN_URL, or override '
                '{0}.get_login_url().'.format(self.__class__.__name__)
            )
        return force_text(login_url)

    def get_permission_denied_message(self):
        """
        Override this method to override the permission_denied_message attribute.
        """
        return self.permission_denied_message

    def get_redirect_field_name(self):
        """
        Override this method to override the redirect_field_name attribute.
        """
        return self.redirect_field_name

    def handle_no_permission(self):
        if self.raise_exception:
            raise PermissionDenied(self.get_permission_denied_message())
        return redirect_to_login(self.request.get_full_path(), self.get_login_url(), self.get_redirect_field_name())

이 이상은 먼저view 함수를 실행하는 코드에 포착됩니다. 즉, 본고의 첫 번째 코드 세그먼트입니다. 그리고self를 호출합니다.process_exception_by_middleware.
    def process_exception_by_middleware(self, exception, request):
        """
        Pass the exception to the exception middleware. If no middleware
        return a response for this exception, raise it.
        """
        for middleware_method in self._exception_middleware:
            response = middleware_method(request, exception)
            if response:
                return response
        raise

이상에 대한 정의가 없기 때문에 프로그램은 마지막 줄에 있는 raise의 이상을 직접 실행합니다. 그러면 이 이상은 누가 포착했습니까?
필자가 이전 블로그(Django 처리 http 요청 프로세스 분석)에서 언급한convertexception_to_response가 self. 를 장식했어요.get_legacy_response라는 거요? 이 이상은 convert 함수에 포착되었습니다.
def convert_exception_to_response(get_response):
    """
    Wrap the given get_response callable in exception-to-response conversion.

    All exceptions will be converted. All known 4xx exceptions (Http404,
    PermissionDenied, MultiPartParserError, SuspiciousOperation) will be
    converted to the appropriate response, and all other exceptions will be
    converted to 500 responses.

    This decorator is automatically applied to all middleware to ensure that
    no middleware leaks an exception and that the next middleware in the stack
    can rely on getting a response instead of an exception.
    """
    @wraps(get_response, assigned=available_attrs(get_response))
    def inner(request):
        try:
            response = get_response(request)
        except Exception as exc:
            response = response_for_exception(request, exc)
        return response
    return inner


def response_for_exception(request, exc):
    if isinstance(exc, Http404):
        if settings.DEBUG:
            response = debug.technical_404_response(request, exc)
        else:
            response = get_exception_response(request, get_resolver(get_urlconf()), 404, exc)

    elif isinstance(exc, PermissionDenied):
        logger.warning(
            'Forbidden (Permission denied): %s', request.path,
            extra={'status_code': 403, 'request': request},
        )
        response = get_exception_response(request, get_resolver(get_urlconf()), 403, exc)

    elif isinstance(exc, MultiPartParserError):
        logger.warning(
            'Bad request (Unable to parse request body): %s', request.path,
            extra={'status_code': 400, 'request': request},
        )
        response = get_exception_response(request, get_resolver(get_urlconf()), 400, exc)

    elif isinstance(exc, SuspiciousOperation):
        # The request logger receives events for any problematic request
        # The security logger receives events for all SuspiciousOperations
        security_logger = logging.getLogger('django.security.%s' % exc.__class__.__name__)
        security_logger.error(
            force_text(exc),
            extra={'status_code': 400, 'request': request},
        )
        if settings.DEBUG:
            response = debug.technical_500_response(request, *sys.exc_info(), status_code=400)
        else:
            response = get_exception_response(request, get_resolver(get_urlconf()), 400, exc)

    elif isinstance(exc, SystemExit):
        # Allow sys.exit() to actually exit. See tickets #1023 and #4701
        raise

    else:
        signals.got_request_exception.send(sender=None, request=request)
        response = handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info())

    return response

예를 들어PermissionDenied 이상에 대응하는 상태 코드는 403이고 urls에서 대응하는 상태 코드의view 함수를 정의하면 이 함수를 호출합니다.
예를 들면 다음과 같습니다.
# urls.py
handler403 = 'path/to/handler_view'

마지막 상황은 LoginRequiredMixin과PermissionRequiredMixin이 다른login으로 이동하기를 원한다면url 어떡하지?계승할 때 LoginRequiredMixin을 먼저 사용한 다음에 구체적인 http 방법에method 를 추가하는 것을 고려할 수 있다decorator.

좋은 웹페이지 즐겨찾기