본문 바로가기

카테고리 없음

DRF + ML 3일차

SA에서 와이어프레임, ERD, API 명세를 갱신하고 다음과 같은 수정사항들이 있었다.

- origin image 추가 : 튜터님의 조언으로 원본 이미지와 수정된 이미지를 함께 보여주기로 하였다.

- comment 수정/삭제 추가 : 이전프로젝트에서 생략된 부분 추가

- permission 추가 : 로그인한 사용자만 볼 수 있는 bookmark기능과 비로그인 사용자도 이용가능한 전체글목록 조회기능이 하나의 API로 묶이면서 새로운 권한 설정이 필요했다.

- 댓글 페이지네이션 추가 : 댓글이 너무 많이 달리면 페이지가 너무 길어지므로 페이지네이션을 댓글에도 적용하기로 하였다.

 

1. 댓글 페이지네이션 추가

class CommentView(APIView):

    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    def get(self, request, article_id):
        article = Article.objects.get(id=article_id)
        comments = article.comment_set.all()
        serializer = CommentSerializer(comments, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

 

기존 댓글 조회 API는 위와 같았다. APIView를 사용하여 페이지네이션을 적용하기 어려웠다.

 

class CommentView(generics.ListAPIView):

    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    paginations_class = CommentPagination
    serializer_class = CommentSerializer
    queryset = None

    def get(self, request, *args, **kwargs):
        print((self.pagination_class.page_query_param))
        comments = (
            get_object_or_404(Article, id=kwargs.get("article_id"))
            .comment_set.all()
            .order_by("-created_at")
        )
        self.queryset = comments
        return super().get(request, *args, **kwargs)
        # super().get()
            '''
            def get(self, request, *args, **kwargs):
                return self.list(request, *args, **kwargs)

            '''

따라서 위와같이 ListAPIView를 사용하기로 했다. get 메소드를 오버라이딩하여 path variable에 맞게 queryset을 filter하도록 한다.

또한 페이지네이션 클래스를 생성하여 적용하였다.

class CommentPagination(PageNumberPagination):
    """CommentPagination: 댓글 페이지네이션을 위한 클래스

    Attributes:
        page_size (int): 한 페이지에 몇 개의 댓글을 담을지 결정합니다.
        page_query_param (str): 페이지 이름(query string에서 페이지를 지정할 파라미터) ex)http://127.0.0.1:8000/article/?comment_page=2 를 사용하면 2페이지로 이동
        max_page_size(int): 최대 페이지 수
    """

    page_size = 50
    page_query_param = "comment_page"
    max_page_size = 100

문제 및 해결: 페이지네이션이 설정한대로 적용되지 않음

분명 페이지네이션을 올바르게 적용하였는데도 실제로 적용되지 않았다. 무엇이 문제인지 한참 찾아보니

class GenericAPIView(views.APIView):
    ...
    pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
    ...

오타때문이었다. pagination_class라고 써야하는데 오타로 인해 paginations_class로 써서 적용이 안됐던 것이다.

근데 그럼 아예 적용이 안되야지 왜 적용이 됐을까?

REST_FRAMEWORK = {
    ...
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 10,
}

바로 settings.py에서 기본 페이지네이션이 적용되어있었기 때문이다. 즉, 댓글이 아닌 게시글의 페이지네이션은 따로 클래스를 적용안해도 원하는 대로 페이지네이션이 적용된다.

 

2. permission 추가

class IsAuthenticatedOrReadOnlyExceptBookMark(permissions.BasePermission):
    """IsAuthenticatedOrReadOnlyExceptBookMark

    북마크 조회를 제외한 GET요청은 비로그인시에도 요청가능합니다.
    북마크 조회와 기타 요청(POST)는 로그인이 필요합니다.
    """

    def has_permission(self, request, view):
        if (
            request.GET.get("filter") == "bookmarked"
            or request.method not in permissions.SAFE_METHODS
        ):
            return bool(request.user and request.user.is_authenticated)
        return True

has_permission 메소드는 request를 인자로 받는다. 따라서 쿼리스트링을 이용하여 특정 쿼리파라미터값일 때 만 퍼미션을 다르게 줄 수 있다.

 

3. 댓글 삭제 퍼미션 추가

class CommentDetailView(APIView):
    permission_classes=permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    def delete(self, request, **kwargs):
        """CommentDetailView.delete

        댓글 삭제.

        KwArg:
            comment_id (int): 삭제할 댓글의 id로 지정.

        정상 시 200 / "삭제완료" 메시지 반환
        오류 시 401 / 권한없음(비로그인, 만료, 작성자 아님)
        오류 시 400 / 존재하지 않는 댓글
        """
        comment = get_object_or_404(Comment, id=kwargs.get("comment_id"))
        comment.delete()
        return Response({"message": "삭제완료"}, status=status.HTTP_204_NO_CONTENT)

댓글을 삭제할 때 작성자만 삭제할 수 있는 권한을 가지는 것이 정상적이므로, 이를 위한 퍼미션을 추가해준다.

class IsOwnerOrReadOnly(permissions.BasePermission):

    message = "권한이 없습니다"

    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.author == request.user

문제 : 퍼미션이 적용되지 않음

 

class CommentDetailView(APIView):
    permission_classes=permission_classes = [permissions.IsAuthenticatedOrReadOnly,IsOwnerOrReadOnly]

위와 같이 퍼미션 클래스를 적용해도, 실제로 적용되지가 않고 다른 사용자가 댓글을 수정/삭제할 수 있는 문제가 발생하였다.

 

원인찾기 및 해결 : 

공식문서를 참조해보면 다음과 같은 내용이 있다.

Object level permissions are run by REST framework's generic views when .get_object() is called. As with view level permissions, an exceptions.PermissionDenied exception will be raised if the user is not allowed to act on the given object.

 

즉, has_object_permission은 generic views에서 정의된 get_object()에서 호출된다.

    def get_object(self):
        queryset = self.filter_queryset(self.get_queryset())

        ...

        self.check_object_permissions(self.request, obj)

        return obj

따라서 generic views가 아닌 APIView를 사용한 경우, 혹은 get_object()가 호출되지 않을 경우에는 당연히 has_object_permission이 호출되지 않는다. 이런 경우 직접 호출해야한다.

def delete(self, request, **kwargs):
        comment = get_object_or_404(Comment, id=kwargs.get("comment_id"))
        self.check_object_permissions(self.request, comment)
        comment.delete()
        return Response({"message": "삭제완료"}, status=status.HTTP_204_NO_CONTENT)