해결된 질문
작성
·
20
·
수정됨
0
강의 수강 내용에 따라
test_author_can_update_post 와 test_non_author_cannot_update_post를 수행하면 하기의 에러가 발생합니다.
list, retrieve, create, destroy 전부 정상 작동하는데 update만 해당 오류가 발생합니다
/blog/api.py, line 75, in has_permission
if request.method in SAFE_METHODS:
NameError: name 'SAFE_METHODS' is not defined
수업 내용대로 따라가고 있는데 ㅠㅠ 오류가 발생한 곳이 어디인지 알 수 가 없습니다.
수업 내용을 따라서 구현한 코드는 다음과 같습니다.
# blog/tests/test_api.py
import base64
import pytest
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIClient
from accounts.models import User
from accounts.tests.factories import UserFactory
from blog.models import Post
from blog.tests.factories import PostFactory
def create_user(raw_password: str = None) -> User:
"""새로운 User 레코드를 생성 및 반환"""
return UserFactory(raw_password=raw_password)
def get_api_client_with_basic_auth(user: User, raw_password: str) -> APIClient:
"""인자의 User 인스턴스와 암호 기반에서 Basic 인증을 적용한 APIClient 인스턴스 반환"""
# *.http 파일에서는 자동으로 base64 인코딩을 수행해줬었습니다.
base64_data: bytes = f"{user.username}:{raw_password}".encode()
authorization_header: str = base64.b64encode(base64_data).decode()
client = APIClient()
client.credentials(HTTP_AUTHORIZATION=f"Basic {authorization_header}")
return client
@pytest.fixture
def unauthenticated_api_client() -> APIClient:
"""Authorization 인증 헤더가 없는 기본 APIClient 인스턴스 반환"""
return APIClient()
@pytest.fixture
def api_client_with_new_user_basic_auth(faker) -> APIClient:
"""새로운 User 레코드를 생성하고, 그 User의 인증 정보가 Authorization 헤더로 지정된 APIClient 인스턴스 반환"""
raw_password: str = faker.password()
user: User = create_user(raw_password)
api_client: APIClient = get_api_client_with_basic_auth(user, raw_password)
return api_client
@pytest.fixture
def new_user() -> User:
"""새로운 User 레코드를 생성 및 반환"""
return create_user()
@pytest.fixture
def new_post() -> Post:
"""새로운 Post 레코드를 반환"""
return PostFactory()
@pytest.mark.it("작성자가 아닌 유저가 수정 요청하면 거부")
@pytest.mark.django_db
def test_non_author_cannot_update_post(new_post, api_client_with_new_user_basic_auth):
url = reverse("api-v1:post_edit", args=[new_post.pk])
response: Response = api_client_with_new_user_basic_auth.patch(url, data={})
assert status.HTTP_403_FORBIDDEN == response.status_code
@pytest.mark.it("작성자가 수정 요청하면 성공")
@pytest.mark.django_db
def test_author_can_update_post(faker):
raw_password = faker.password()
author = create_user(raw_password=raw_password)
created_post = PostFactory(author=author)
url = reverse("api-v1:post_edit", args=[created_post.pk])
api_client = get_api_client_with_basic_auth(author, raw_password)
data = {"title": faker.sentence()}
response: Response = api_client.patch(url, data=data)
assert status.HTTP_200_OK == response.status_code
assert data["title"] == response.data["title"]
## core/mixins.py
from typing import List, Optional, Type
from colorama import Fore
from django.conf import settings
from django.db.models import Model, QuerySet
from rest_framework import permissions
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.utils.serializer_helpers import ReturnDict
from rest_framework.views import APIView
from core.permissions import make_drf_permission_class
class JSONResponseWrapperMixin:
def finalize_response(
self, request: Request, response: Response, *args, **kwargs
) -> Response:
is_ok = 200 <= response.status_code < 400
accepted_renderer = getattr(request, "accepted_renderer", None)
if accepted_renderer is None or response.exception is True:
response.data = {
"ok": is_ok,
"result": response.data,
}
elif isinstance(
request.accepted_renderer, (JSONRenderer, BrowsableAPIRenderer)
):
response.data = ReturnDict(
{
"ok": is_ok,
"result": response.data, # ReturnList
},
serializer=response.data.serializer,
)
return super().finalize_response(request, response, *args, **kwargs)
class PermissionDebugMixin:
if settings.DEBUG:
def get_label_text(self, is_permit: bool) -> str:
return (
f"{Fore.GREEN}Permit{Fore.RESET}" # colorama 라이브러리 활용
if is_permit
else f"{Fore.RED}Deny{Fore.RESET}"
)
def check_permissions(self, request: Request) -> None:
print(f"{request.method} {request.path} has_permission")
for permission in self.get_permissions():
is_permit: bool = permission.has_permission(request, self)
print(
f"\t{permission.__class__.__name__} = {self.get_label_text(is_permit)}"
)
if not is_permit:
self.permission_denied(
request,
message=getattr(permission, "message", None),
code=getattr(permission, "code", None),
)
def check_object_permissions(self, request: Request, obj: Model) -> None:
print(f"{request.method} {request.path} has_object_permission")
for permission in self.get_permissions():
is_permit: bool = permission.has_object_permission(request, self, obj)
print(
f"\t{permission.__class__.__name__} = {self.get_label_text(is_permit)}"
)
if not is_permit:
self.permission_denied(
request,
message=getattr(permission, "message", None),
code=getattr(permission, "code", None),
)
class TestFuncPermissionMixin:
TEST_FUNC_PERMISSION_CLASS_NAME = "TestFuncPermissionMixin"
@classmethod
def get_test_func_permission_instance(cls) -> permissions.BasePermission:
permission_class = make_drf_permission_class(
class_name=cls.TEST_FUNC_PERMISSION_CLASS_NAME,
# *_test_func_name 속성이 지정되면, 이 권한 클래스가 사용된 APIView 클래스에서
# 지정 이름의 메서드를 찾습니다.
has_permission_test_func_name="has_permission",
has_object_permission_test_func_name="has_object_permission",
)
return permission_class()
def get_permissions(self) -> List[permissions.BasePermission]:
# 기존 permission_classes 설정에 권한 정책을 추가하는 방식으로 동작
return super().get_permissions() + [self.get_test_func_permission_instance()]
def has_permission(self, request: Request, view: APIView) -> bool:
return True
def has_object_permission(
self, request: Request, view: APIView, obj: Model
) -> bool:
return True
답변 2
1
안녕하세요. SAFE_METHODS 값은 rest_framework.permissions 에 SAFE_METHODS 가 정의되어있습니다. 보여주신 코드에서도 permissions.SAFE_METHODS 코드로 참조하신 부분이 있네요.
실제 예외가 발생한 곳은 /blog/api.py 인데요.
blog/api.py 코드에서 임포트 코드가 누락되신 듯 합니다. 아래 코드를 추가해보시겠어요?
from rest_framework.permissions import SAFE_METHODS
살펴보시고 댓글 부탁드립니다.
화이팅입니다. :-)
0
10000자 제한으로 이어 답글 답니다
# core/permissions.py
from typing import Callable, Type, cast
from django.db.models import Model
from rest_framework import permissions
from rest_framework.request import Request
from rest_framework.views import APIView
class IsAuthorOrReadonly(permissions.BasePermission):
def has_permission(self, request: Request, view: APIView) -> bool:
if request.method in permissions.SAFE_METHODS:
return True
return request.user.is_authenticated
# 2차 필터
def has_object_permission(
self, request: Request, view: APIView, obj: Model
) -> bool:
if request.method in permissions.SAFE_METHODS:
return True
if not hasattr(obj, "author"):
return False
return obj.author == request.user
def make_drf_permission_class(
class_name: str = "PermissionClass",
permit_safe_methods: bool = False,
has_permission_test_func: Callable[[Request, APIView], bool] = None,
has_permission_test_func_name: str = None,
has_object_permission_test_func: Callable[[Request, APIView, Model], bool] = None,
has_object_permission_test_func_name: str = None,
) -> Type[permissions.BasePermission]:
def has_permission(self, request: Request, view: APIView) -> bool:
if permit_safe_methods and request.method in permissions.SAFE_METHODS:
return True
if has_permission_test_func is not None:
return has_permission_test_func(request, view)
if has_permission_test_func_name is not None:
test_func = getattr(view, has_permission_test_func_name)
return test_func(request, view)
return True
def has_object_permission(
self, request: Request, view: APIView, obj: Model
) -> bool:
if permit_safe_methods and request.method in permissions.SAFE_METHODS:
return True
if has_object_permission_test_func is not None:
return has_object_permission_test_func(request, view, obj)
if has_object_permission_test_func_name is not None:
test_func = getattr(view, has_object_permission_test_func_name)
return test_func(request, view, obj)
return True
permission_class = type(
class_name,
(permissions.BasePermission,),
{
"has_permission": has_permission,
"has_object_permission": has_object_permission,
},
)
return cast(Type[permissions.BasePermission], permission_class)
class TestFuncPermissionMixin:
TEST_FUNC_PERMISSION_CLASS_NAME = None
@classmethod
def get_test_func_permission_instance(cls) -> permissions.BasePermission:
permission_class = make_drf_permission_class(
class_name=cls.TEST_FUNC_PERMISSION_CLASS_NAME or cls.__name__,
has_permission_test_func_name="has_permission",
has_object_permission_test_func_name="has_object_permission",
)
return permission_class()
def get_permissions(self) -> list[permissions.BasePermission]:
return super().get_permissions() + [self.get_test_func_permission_instance()]
def has_permission(self, request: Request, view: APIView) -> bool:
return True
def has_object_permission(
self, request: Request, view: APIView, obj: Model
) -> bool:
return True
PermitSafeMethods = make_drf_permission_class(
permit_safe_methods=True,
)
감사합니다 해결되었습니다. 이걸 빼먹었네요 ㅠㅠ 코드 부분만 확인하다가 헤맸습니다. vscode 로 작업 중인데 pylance가 안먹는 매서드들이 존재해 debug할 때 어렵네요