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)