class ExistsIdView(APIView):
"""
아이디 중복 체크
---
"""
permission_classes = (permissions.AllowAny,)
id_param = openapi.Parameter(
"id", openapi.IN_PATH, description="user id", type=openapi.TYPE_STRING
)
@swagger_auto_schema(
manual_parameters=[id_param], responses={200: BooleanResponseSerializer}
)
def get(self, request, id):
try:
member = Member.objects.get(pk=id)
except Member.DoesNotExist:
raise exceptions.NotFound("member not found")
res = BooleanResponseSerializer(data={"result": True})
if res.is_valid():
return Response(res.data)
path("member/<str:id>/exists",
views.ExistsIdView.as_view(), name="exists_id_view")
클래스 메서드인 as_view(cls, initkwargs)가 실행 될 때 내부 코드중 아래를 눈여겨 보아야한다.
super().as_view(**initkwargs)
해당 코드가 실행되면서 as_view() 함수가 실행된다.
첫번 째 as_view(cls, initkwargs) 함수는 @classmethod 이고 두번 째 as_view(**initkwargs) 함수는 @classonlymethod 이다.
참고 링크
둘 간의 차이는 전자의 경우 인스턴스도 as_view 함수에 접근할 수 있지만 후자의 경우 클래스만 as_view 함수에 접근할 수 있다.
첫번 째 as_view(cls, initkwargs) 메서드의 경우 APIView(View) 클래스 내의 클래스 메서드이므로 super()의 경우 class View에 해당한다.
참고 링크
첫번 째 as_view(cls, **initkwargs) 함수는 csrf_exempt(view)를 return 하는데 csrf_exempt 함수는 인자로 받는 함수에게 csrf(Cross Site Request Forgery) 관련 token이 불필요 하다는 처리 기능을 담당한다.
위 함수가 처리하는 주요 기능은 View.as_view() 함수를 호출한 결과 값인 view 함수를 변수 view에 할당하는 것이다. view 함수는 self.dispatch(request, *args, **kwargs)의 결과값을 리턴한다. dispatch 함수는 HttpResponse를 리턴한다.
@classmethod
def as_view(cls, **initkwargs):
"""
Store the original class on the view function.
This allows us to discover information about the view when we do URL
reverse lookups. Used for breadcrumb generation.
"""
if isinstance(getattr(cls, 'queryset', None), models.query.QuerySet):
def force_evaluation():
raise RuntimeError(
'Do not evaluate the `.queryset` attribute directly, '
'as the result will be cached and reused between requests. '
'Use `.all()` or call `.get_queryset()` instead.'
)
cls.queryset._fetch_all = force_evaluation
view = super().as_view(**initkwargs)
view.cls = cls
view.initkwargs = initkwargs
# Note: session based authentication is explicitly CSRF validated,
# all other authentication is CSRF exempt.
return csrf_exempt(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(
'The method name %s is not accepted as a keyword argument '
'to %s().' % (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)
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
# 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
dispatch 함수는 HTTP Response를 리턴한다.
def dispatch(self, request, *args, **kwargs):
"""
`.dispatch()` is pretty much the same as Django's regular dispatch,
but with extra hooks for startup, finalize, and exception handling.
"""
self.args = args
self.kwargs = kwargs
request = self.initialize_request(request, *args, **kwargs)
self.request = request
self.headers = self.default_response_headers # deprecate?
try:
self.initial(request, *args, **kwargs)
# Get the appropriate handler method
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
response = handler(request, *args, **kwargs)
except Exception as exc:
response = self.handle_exception(exc)
self.response = self.finalize_response(request, response, *args, **kwargs)
return self.response
path 함수 내 views.ExistsIdView.as_view()의 실행은 as_view(cls, **initkwargs) 함수의 호출로 view 함수를 리턴하고 해당 함수가 path 함수의 인자로 들어간다. 그 과정에서 ¹ csrf 토큰의 무효처리와 ² HttpResponse를 처리하는 것을 볼 수 있었다.
@swagger_auto_schema 중 manual_parameters 인자에 대해서 알아보고자 한다.
해당 인자는 요청 시 추가 정보를 더함으로써 응답을 커스터마이징 하는 역할을 수행한다. 리스트 형태로 여러개를 담을 수 있으며 Class Parameter의 인스턴스 객체를 리스트 안에 담는다.
해당 api의 id_param 인스턴스의 경우 경로에 대한 정보를 제공해주는 역할을 한다. swagger 문서에선 Parameters 부문에 아래 그림과 같이 기입된다.
id_param = openapi.Parameter("id", openapi.IN_PATH,
description="user id", type=openapi.TYPE_STRING)
@swagger_auto_schema(
manual_parameters=[id_param],
responses={200: BooleanResponseSerializer}
)
class Parameter(SwaggerDict):
def __init__(self, name, in_, description=None, required=None,
schema=None, type=None, format=None, enum=None, pattern=None,
items=None, default=None, **extra):
...
name -> id: parameter name
in_ -> openapi.IN_PATH: parameter location
description: parameter description
type: Schema or SchemaRef
api/member/{id}/exists로 api 요청이 들어오면 get메서드가 실행된다. request 인자로 Request 객체의 인스턴스가 들어오고 id로는 해당 api 요청시 {id} 값이 들어오게 된다.
path("member/<str:id>/exists", ...
해당 id의 값은 URL.conf에서 그 변수를 설정해 줄 수 있다. <str:id> 에서 id가 그 부분이다.
...
def get(self, request, id):
try:
member = Member.objects.get(pk=id)
...
#print(Member.objects) -> member.Member.objects
#print(type(Member.objects)) -> member.managers.MemberManager
Member.objects의 경우 Member class 안에 MemberManager의 인스턴스를 objects 변수로 받고 있다. MemberManager의 경우BaseUserManager 클래스를 상속받고 있다. 그래서 print로 objects의 값과 타입을 찍으면 위와 같다.
Member 모델의 객체 중에서 기본키의 값이 id가 가진 값과 일치하는 객체를 조회하는 코드이다. get의 경우 하나의 객체를 반환시킨다. 객체가 존재하지 않을 경우 예외 발생시킨다.
class ExistsIdView(APIView):
"""
아이디 중복 체크
"""
...
def get(self, request, id):
try:
member = Member.objects.get(pk=id)
except Member.DoesNotExist:
raise exceptions.NotFound("member not found")
request -> <rest_framework.request.Request: GET '/api/member/anonymous/exists'>
id -> anonymous
# rest_framework/serializers.py
class BaseSerializer(Field):
def __init__(self, instance=None, data=empty, **kwargs):
# instance -> 직렬화 목적
# data -> 유효성 검사
class Serializer(BaseSerializer):
pass
.is_valid() 호출 되서야
.update 함수 .create 함수를 통해 관련 필드에 값을 할당하고 DB에 저장
- .update(): self.instance 인자 지정했을 때
- .create(): self.instance 인자 지정하지 않았을 때
참고링크1
참고링크2
BooleanResponseSerializer(serializers.Serializer) -> Serializer(BaseSerializer) -> BaseSerializer(Field) 으로 상속을 받는다.
BaseSerializer의 생성자 함수에서 self.initial_data = data로 data의 값이 들어간다. 상위 부모 클래스에서 초기화 됐으므로 data는 이제 BaseSerializer 지점에서 정착해 있다.
이후 is_valid 함수를 호출하게 될 경우 인스턴스는 _validated_data 프로퍼티 메서드가 실행 된다.
1) 인스턴스 생성 시 data 인자에 값을 넘겨 주어야지 is_valid 함수를 호출할 수 있다.
2) res.is_valid()를 호출하지 않고 return 값으로 Response(res.data)를 return 하면 해당 api를 요청 시 아래와 같은 error를 맞는다.
data
keyword argument you must call .is_valid()
before attempting to access the serialized .data
representation..is_valid()
first, or access .initial_data
instead3) self.initial_data가 생성되면 is_valid 함수 내부 _validated_data의 속성을 생성하게 된다. self_validated_data의 값은 OrderedDict(['result', True])
를 갖고 self.errors의 경우 {}
값을 갖는다. is_valid의 return 값은 not bool(self.errors)인데 빈 {}
가 False
를 반환하므로 결과적으로True
를 반환하게 된다. 에러메세지가 있을 경우 False
를 반환한다.
4) is_valid -> run_validation 함수는 2가지 로직을 갖는다. 첫번째는 data 매개인자로 들어온 값이 없을 때 이를 처리하여 값을 반환하는 경우 나머지는 안에 있는 값을 추출 후 유효성 검사를 하는 경우가 있다.
def is_valid(self, raise_exception=False):
assert hasattr(self, 'initial_data'), (
'Cannot call `.is_valid()` as no `data=` keyword argument was '
'passed when instantiating the serializer instance.'
)
if not hasattr(self, '_validated_data'):
try:
self._validated_data = self.run_validation(self.initial_data)
except ValidationError as exc:
self._validated_data = {}
self._errors = exc.detail
else:
self._errors = {}
if self._errors and raise_exception:
raise ValidationError(self.errors)
return not bool(self._errors)
def run_validation(self, data=empty):
"""
Validate a simple representation and return the internal value.
The provided data may be `empty` if no representation was included
in the input.
May raise `SkipField` if the field should not be included in the
validated data.
"""
(is_empty_value, data) = self.validate_empty_values(data)
if is_empty_value:
return data
value = self.to_internal_value(data)
self.run_validators(value)
return value
def validate_empty_values(self, data):
"""
Validate empty values, and either:
* Raise `ValidationError`, indicating invalid data.
* Raise `SkipField`, indicating that the field should be ignored.
* Return (True, data), indicating an empty value that should be
returned without any further validation being applied.
* Return (False, data), indicating a non-empty value, that should
have validation applied as normal.
"""
if self.read_only:
return (True, self.get_default())
if data is empty:
if getattr(self.root, 'partial', False):
raise SkipField()
if self.required:
self.fail('required')
return (True, self.get_default())
if data is None:
if not self.allow_null:
self.fail('null')
# Nullable `source='*'` fields should not be skipped when its named
# field is given a null value. This is because `source='*'` means
# the field is passed the entire object, which is not null.
elif self.source == '*':
return (False, None)
return (True, None)
return (False, data)
def get_default(self):
"""
Return the default value to use when validating data if no input
is provided for this field.
If a default has not been set for this field then this will simply
raise `SkipField`, indicating that no value should be set in the
validated data for this field.
"""
if self.default is empty or getattr(self.root, 'partial', False):
# No default, or this is a partial update.
raise SkipField()
if callable(self.default):
if hasattr(self.default, 'set_context'):
warnings.warn(
"Method `set_context` on defaults is deprecated and will "
"no longer be called starting with 3.13. Instead set "
"`requires_context = True` on the class, and accept the "
"context as an additional argument.",
RemovedInDRF313Warning, stacklevel=2
)
self.default.set_context(self)
if getattr(self.default, 'requires_context', False):
return self.default(self)
else:
return self.default()
return self.default
def to_internal_value(self, data):
"""
Transform the *incoming* primitive data into a native value.
"""
raise NotImplementedError(
'{cls}.to_internal_value() must be implemented for field '
'{field_name}. If you do not need to support write operations '
'you probably want to subclass `ReadOnlyField` instead.'.format(
cls=self.__class__.__name__,
field_name=self.field_name,
)
)
serialer.is_valid()를 통해 유효성 검사를 수행할 수 있다. 인스턴스를 매개변수로 넘길 경우 직렬화에 그 목적이 있고 data 인자로 넘길 경우 값에 대한 유효성을 검사하는 것에 그 목적이 있다.
멤버 테이블의 기본키 목록에 인자로 받은 값이 있는지 없는지를 조회한 후 값이 없을 때 에러를 뱉고 값이 있을 때에는 T/F를 뱉는 시리얼라이저에 데이터를 넣어 유효성 검사를 수행한 이후 그 결과 값을 리턴한다. 그 값은 True이다.
class ExistsIdView(APIView):
"""
아이디 중복 체크를
---
"""
permission_classes = (permissions.AllowAny,)
id_param = openapi.Parameter(
"id", openapi.IN_PATH, description="user id", type=openapi.TYPE_STRING
)
@swagger_auto_schema(
manual_parameters=[id_param], responses={200: BooleanResponseSerializer}
)
def get(self, request, id):
try:
member = Member.objects.get(pk=id)
except Member.DoesNotExist:
raise exceptions.NotFound("member not found")
res = BooleanResponseSerializer(data={"result": True})
if res.is_valid():
return Response(res.data)