From 3a93cbbed9c2f32936536f8ef0dc62e7947eb08e Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Mon, 19 Aug 2024 23:53:21 +0500 Subject: [PATCH 1/7] feat: implement comment APIs - these call are implemented: /comments/:comment_id DELETE (deletes parent/child comment) /comments/:comment_id GET (get parent comment) /comments/:comment_id POST (created child comment) /comments/:comment_id PUT (updates parent/child comment) --- forum/models/comments.py | 52 ++++++++-- forum/serializers/comments_api.py | 77 +++++++++++++++ forum/serializers/contents.py | 2 +- forum/urls.py | 13 ++- forum/utils.py | 4 + forum/views/comments.py | 152 ++++++++++++++++++++++++++++++ 6 files changed, 289 insertions(+), 11 deletions(-) create mode 100644 forum/serializers/comments_api.py create mode 100644 forum/views/comments.py diff --git a/forum/models/comments.py b/forum/models/comments.py index a804442c..957e9d68 100644 --- a/forum/models/comments.py +++ b/forum/models/comments.py @@ -7,6 +7,7 @@ from bson import ObjectId +from forum.models.users import Users from forum.models.contents import Contents @@ -21,9 +22,10 @@ def insert( # type: ignore # pylint: disable=arguments-renamed self, body: str, course_id: str, - comment_thread_id: str, + parent_id: str, author_id: str, - author_username: str, + comment_thread_id: str = None, + author_username: str = None, anonymous: bool = False, anonymous_to_peers: bool = False, depth: int = 0, @@ -44,13 +46,18 @@ def insert( # type: ignore # pylint: disable=arguments-renamed Returns: str: The ID of the inserted document. """ - date = datetime.now() + date = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") + comment = Contents().get(parent_id) + parent_child_count = comment.get("child_count") + if not comment_thread_id: + comment_thread_id = comment.get("comment_thread_id") + comment_data = { "votes": self.get_votes_dict(up=[], down=[]), "visible": True, "abuse_flaggers": [], "historical_abuse_flaggers": [], - "parent_ids": [], + "parent_ids": [ObjectId(parent_id)] if parent_id else [], "at_position_list": [], "body": body, "course_id": course_id, @@ -58,15 +65,20 @@ def insert( # type: ignore # pylint: disable=arguments-renamed "endorsed": False, "anonymous": anonymous, "anonymous_to_peers": anonymous_to_peers, + "parent_id": ObjectId(parent_id), "author_id": author_id, "comment_thread_id": ObjectId(comment_thread_id), "child_count": 0, "depth": depth, - "author_username": author_username, + "author_username": author_username or self.get_author_username(author_id), "created_at": date, "updated_at": date, } result = self._collection.insert_one(comment_data) + self._collection.update_one( + {"_id": ObjectId(parent_id)}, + {"$set": {"child_count": parent_child_count + 1}}, + ) return str(result.inserted_id) def update( # type: ignore @@ -86,6 +98,12 @@ def update( # type: ignore endorsed: Optional[bool] = None, child_count: Optional[int] = None, depth: Optional[int] = None, + closed: Optional[bool] = None, + edit_history: Optional[list[dict[str, Any]]] = [], + original_body: Optional[str] = None, + editing_user_id: Optional[str] = None, + edit_reason_code: Optional[str] = None, + endorsement_user_id: Optional[str] = None, ) -> int: """ Updates a comment document in the database. @@ -125,15 +143,35 @@ def update( # type: ignore ("endorsed", endorsed), ("child_count", child_count), ("depth", depth), + ("closed", closed), ] update_data: Dict[str, Any] = { field: value for field, value in fields if value is not None } + if endorsed and endorsement_user_id: + update_data["endorsement"] = { + "user_id": endorsement_user_id, + "time": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"), + } - date = datetime.now() - update_data["updated_at"] = date + if editing_user_id: + edit_history.append( + { + "original_body": original_body, + "reason_code": edit_reason_code, + "editor_username": self.get_author_username(editing_user_id), + "created_at": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"), + } + ) + update_data["edit_history"] = edit_history + + update_data["updated_at"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") result = self._collection.update_one( {"_id": ObjectId(comment_id)}, {"$set": update_data}, ) return result.modified_count + + def get_author_username(self, author_id): + user = Users().get(author_id) + return user.get("username") diff --git a/forum/serializers/comments_api.py b/forum/serializers/comments_api.py new file mode 100644 index 00000000..d1c1dea6 --- /dev/null +++ b/forum/serializers/comments_api.py @@ -0,0 +1,77 @@ +""" +Serializer for the comment data. +""" + +from rest_framework import serializers + +from forum.models.comments import Comment +from forum.serializers.contents import EditHistorySerializer +from forum.serializers.votes import VoteSummarySerializer + + +class EndorsementSerializer(serializers.Serializer): + user_id = serializers.IntegerField() + time = serializers.DateTimeField(format="%Y-%m-%dT%H:%M:%SZ") + + +class CommentsAPICommonSerializer(serializers.Serializer): + """ + Serializer for handling user comment api calls. + """ + + id = serializers.CharField() + user_id = serializers.CharField() + thread_id = serializers.CharField() + username = serializers.CharField() + parent_id = serializers.CharField() + endorsed = serializers.BooleanField(default=False) + anonymous = serializers.BooleanField(default=False) + anonymous_to_peers = serializers.BooleanField(default=False) + closed = serializers.BooleanField(default=False) + body = serializers.CharField() + course_id = serializers.CharField() + parent_id = serializers.CharField(default=None, allow_null=True) + commentable_id = serializers.CharField(default="course") + created_at = serializers.DateTimeField(format="%Y-%m-%dT%H:%M:%SZ") + updated_at = serializers.DateTimeField(format="%Y-%m-%dT%H:%M:%SZ") + depth = serializers.IntegerField(default=0) + abuse_flaggers = serializers.ListField( + child=serializers.CharField(), allow_null=True + ) + at_position_list = serializers.ListField(allow_null=True) + type = serializers.CharField() + child_count = serializers.IntegerField(default=0) + votes = VoteSummarySerializer() + edit_history = EditHistorySerializer(default=[], many=True) + sk = serializers.SerializerMethodField() + endorsement = EndorsementSerializer(default=None, required=False, allow_null=True) + + def get_sk(self, obj): + is_child = True if obj.get("parent_id") else False + if is_child: + return "{parent_id}-{id}".format( + parent_id=obj.get("parent_id"), id=obj.get("_id") + ) + else: + return "{id}".format(id=obj.get("_id")) + + +class CommentsAPIGetSerializer(CommentsAPICommonSerializer): + """ + Serializer for handling user comment get api calls. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields.pop("sk", None) + + +class CommentsAPIPostAndDeleteSerializer(CommentsAPICommonSerializer): + """ + Serializer for handling user comment post and delete api calls. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields.pop("sk", None) + self.fields.pop("endorsement", None) diff --git a/forum/serializers/contents.py b/forum/serializers/contents.py index 741b5ced..b893feab 100644 --- a/forum/serializers/contents.py +++ b/forum/serializers/contents.py @@ -21,7 +21,7 @@ class EditHistorySerializer(serializers.Serializer): # type: ignore original_body = serializers.CharField() reason_code = serializers.CharField(allow_null=True, default=None) editor_username = serializers.CharField() - created_at = serializers.DateTimeField() + created_at = serializers.DateTimeField(format="%Y-%m-%dT%H:%M:%SZ") def create(self, validated_data: Dict[str, Any]) -> Any: """Raise NotImplementedError""" diff --git a/forum/urls.py b/forum/urls.py index 24721c35..fec31a3d 100644 --- a/forum/urls.py +++ b/forum/urls.py @@ -8,9 +8,10 @@ from forum.views.pins import PinThreadAPIView, UnpinThreadAPIView from forum.views.proxy import ForumProxyAPIView from forum.views.votes import CommentVoteView, ThreadVoteView +from forum.views.comments import CommentsAPIView api_patterns = [ - # Thread APIs + # thread votes APIs path( "threads//votes", ThreadVoteView.as_view(), @@ -21,7 +22,7 @@ CommentVoteView.as_view(), name="comment-vote", ), - # Comment APIs + # abuse comment/thread APIs path( "comments//abuse_", CommentFlagAPIView.as_view(), @@ -32,13 +33,19 @@ ThreadFlagAPIView.as_view(), name="thread-flags-api", ), - # Pin/Unpin thread APIs + # pin/unpin thread APIs path("threads//pin", PinThreadAPIView.as_view(), name="pin-thread"), path( "threads//unpin", UnpinThreadAPIView.as_view(), name="unpin-thread", ), + # comments APIs + path( + "comments/", + CommentsAPIView.as_view(), + name="comments-api", + ), # Proxy view for various API endpoints path( "", diff --git a/forum/utils.py b/forum/utils.py index b4fa9359..2e4056f3 100644 --- a/forum/utils.py +++ b/forum/utils.py @@ -40,3 +40,7 @@ def handle_proxy_requests(request: HttpRequest, suffix: str, method: str) -> Res headers=request_headers, timeout=5.0, ) + + +def str_to_bool(value: str) -> bool: + return value.lower() in ("true", "1") diff --git a/forum/views/comments.py b/forum/views/comments.py new file mode 100644 index 00000000..e33d2d8b --- /dev/null +++ b/forum/views/comments.py @@ -0,0 +1,152 @@ +"""Forum Comments API Views.""" + +import logging +from typing import Any + +from django.core.exceptions import ObjectDoesNotExist +from rest_framework import status +from rest_framework.permissions import AllowAny +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.serializers import ValidationError +from rest_framework.views import APIView + +from forum.models import Comment +from forum.serializers.comments_api import ( + CommentsAPIGetSerializer, + CommentsAPIPostAndDeleteSerializer, +) +from forum.utils import str_to_bool + +log = logging.getLogger(__name__) + + +class CommentsAPIView(APIView): + """ + API View to handle GET, POST, PUT, and DELETE requests for comments. + """ + + permission_classes = (AllowAny,) + + def get(self, request: Request, comment_id: str = None) -> Response: + """ + Retrieves a parent comment. + For chile comments, below API is called that return all child comments in children field + url: http://localhost:8000/forum/api/v2/threads/66ab94950dead7001deb947a + """ + try: + comment = Comment().get(comment_id) + if not comment: + raise ObjectDoesNotExist + except ObjectDoesNotExist: + return Response( + {"error": "Comment does not exist"}, status=status.HTTP_404_NOT_FOUND + ) + data = self._prepare_response("get", comment) + return Response(data, status=status.HTTP_200_OK) + + def post(self, request: Request, comment_id: str = None) -> Response: + """ + Creates a new child comment. + For parent comment below API is called. + url: http://localhost:8000/forum/api/v2/threads/66ab94950dead7001deb947a/comments + """ + data = request.data + # TODO validations + new_comment_id = Comment().insert( + body=data.get("body"), + course_id=data.get("course_id"), + anonymous=data.get("anonymous", False), + anonymous_to_peers=data.get("anonymous_to_peers", False), + author_id=data.get("user_id"), + parent_id=comment_id, + depth=1, + ) + comment = Comment().get(new_comment_id) + data = self._prepare_response("post", comment) + + return Response(data, status=status.HTTP_200_OK) + + def put(self, request: Request, comment_id: str) -> Response: + """ + Update an existing child and parent comment. + """ + try: + comment = Comment().get(comment_id) + if not comment: + raise ObjectDoesNotExist + except ObjectDoesNotExist: + return Response( + {"error": "Comment does not exist"}, status=status.HTTP_404_NOT_FOUND + ) + + data = request.POST.dict() + update_comment_data: dict[str, Any] = { + "body": data.get("body", comment.get("body", "")), + "anonymous": str_to_bool(data.get("anonymous", False)), + "anonymous_to_peers": str_to_bool(data.get("anonymous_to_peers", False)), + "course_id": data.get("course_id", ""), + "closed": str_to_bool(data.get("closed", False)), + "author_id": data.get("user_id"), + "endorsed": str_to_bool(data.get("endorsed", False)), + } + if "editing_user_id" in data: + update_comment_data["editing_user_id"] = data.get("editing_user_id") + + if "edit_reason_code" in data: + update_comment_data["edit_reason_code"] = data.get("edit_reason_code") + + if "endorsement_user_id" in data: + update_comment_data["endorsement_user_id"] = data.get("endorsement_user_id") + + update_comment_data["edit_history"] = comment.get("edit_history", []) + update_comment_data["original_body"] = comment.get("body") + + Comment().update(comment_id, **update_comment_data) + updated_comment = Comment().get(comment_id) + data = self._prepare_response("put", updated_comment) + + return Response(data, status=status.HTTP_200_OK) + + def delete(self, request: Request, comment_id: str) -> Response: + """ + Delete a comment. + """ + try: + comment = Comment().get(comment_id) + if not comment: + raise ObjectDoesNotExist + except ObjectDoesNotExist: + return Response( + {"error": "Comment does not exist"}, status=status.HTTP_404_NOT_FOUND + ) + data = self._prepare_response("delete", comment) + Comment().delete(comment_id) + + return Response(data, status=status.HTTP_200_OK) + + def _prepare_response(self, method, comment): + comment_data = { + **comment, + "id": str(comment.get("_id")), + "user_id": comment.get("author_id"), + "thread_id": str(comment.get("comment_thread_id")), + "username": comment.get("author_username"), + "parent_id": str(comment.get("parent_id")), + "type": comment.get("_type").lower(), + } + if method == "get": + serializer = CommentsAPIGetSerializer(data=comment_data) + elif method == "post": + serializer = CommentsAPIPostAndDeleteSerializer(data=comment_data) + elif method == "put": + if comment.get("parent_id"): + serializer = CommentsAPIPostAndDeleteSerializer(data=comment_data) + else: + serializer = CommentsAPIGetSerializer(data=comment_data) + else: + serializer = CommentsAPIPostAndDeleteSerializer(data=comment_data) + if not serializer.is_valid(): + raise ValidationError(serializer.errors) + + return serializer.data From 774d25e75f37e09aae90a449c30d499b4c1cc37f Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Tue, 20 Aug 2024 11:54:53 +0500 Subject: [PATCH 2/7] feat: handle edge cases and code refactoring - handle child_count of parent comment in case of child comment deletion - simplify code and use one serializer --- forum/models/comments.py | 31 +++++-- forum/serializers/comments_api.py | 30 ++----- forum/views/comments.py | 144 +++++++++++++++++++----------- 3 files changed, 125 insertions(+), 80 deletions(-) diff --git a/forum/models/comments.py b/forum/models/comments.py index c4943e67..8e645d36 100644 --- a/forum/models/comments.py +++ b/forum/models/comments.py @@ -45,10 +45,10 @@ def insert( str: The ID of the inserted document. """ date = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") - comment = BaseContents().get(parent_id) - parent_child_count = comment.get("child_count") + parent_comment = self.get(parent_id) + parent_child_count = parent_comment.get("child_count") if not comment_thread_id: - comment_thread_id = comment.get("comment_thread_id") + comment_thread_id = parent_comment.get("comment_thread_id") comment_data = { "votes": self.get_votes_dict(up=[], down=[]), @@ -73,10 +73,7 @@ def insert( "updated_at": date, } result = self._collection.insert_one(comment_data) - self._collection.update_one( - {"_id": ObjectId(parent_id)}, - {"$set": {"child_count": parent_child_count + 1}}, - ) + self.update(parent_id, child_count=parent_child_count + 1) return str(result.inserted_id) def update( @@ -170,6 +167,26 @@ def update( ) return result.modified_count + def delete(self, _id: str) -> int: + """ + Deletes a comment from the database based on the id. + + Args: + _id: The ID of the comment. + + Returns: + The number of comments deleted. + """ + comment = self.get(_id) + parent_comment_id = comment.get("parent_id") + parent_comment = parent_comment_id and self.get(parent_comment_id) + result = self._collection.delete_one({"_id": ObjectId(_id)}) + if parent_comment: + self.update( + parent_comment_id, child_count=parent_comment.get("child_count") - 1 + ) + return result.deleted_count + def get_author_username(self, author_id): user = Users().get(author_id) return user.get("username") diff --git a/forum/serializers/comments_api.py b/forum/serializers/comments_api.py index d1c1dea6..dbd08dd8 100644 --- a/forum/serializers/comments_api.py +++ b/forum/serializers/comments_api.py @@ -14,7 +14,7 @@ class EndorsementSerializer(serializers.Serializer): time = serializers.DateTimeField(format="%Y-%m-%dT%H:%M:%SZ") -class CommentsAPICommonSerializer(serializers.Serializer): +class CommentsAPISerializer(serializers.Serializer): """ Serializer for handling user comment api calls. """ @@ -46,6 +46,13 @@ class CommentsAPICommonSerializer(serializers.Serializer): sk = serializers.SerializerMethodField() endorsement = EndorsementSerializer(default=None, required=False, allow_null=True) + def __init__(self, *args, **kwargs): + exclude_fields = kwargs.pop("exclude_fields", None) + super().__init__(*args, **kwargs) + if exclude_fields: + for field in exclude_fields: + self.fields.pop(field, None) + def get_sk(self, obj): is_child = True if obj.get("parent_id") else False if is_child: @@ -54,24 +61,3 @@ def get_sk(self, obj): ) else: return "{id}".format(id=obj.get("_id")) - - -class CommentsAPIGetSerializer(CommentsAPICommonSerializer): - """ - Serializer for handling user comment get api calls. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields.pop("sk", None) - - -class CommentsAPIPostAndDeleteSerializer(CommentsAPICommonSerializer): - """ - Serializer for handling user comment post and delete api calls. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields.pop("sk", None) - self.fields.pop("endorsement", None) diff --git a/forum/views/comments.py b/forum/views/comments.py index e33d2d8b..416c2bb1 100644 --- a/forum/views/comments.py +++ b/forum/views/comments.py @@ -1,6 +1,5 @@ """Forum Comments API Views.""" -import logging from typing import Any from django.core.exceptions import ObjectDoesNotExist @@ -12,14 +11,9 @@ from rest_framework.views import APIView from forum.models import Comment -from forum.serializers.comments_api import ( - CommentsAPIGetSerializer, - CommentsAPIPostAndDeleteSerializer, -) +from forum.serializers.comments_api import CommentsAPISerializer from forum.utils import str_to_bool -log = logging.getLogger(__name__) - class CommentsAPIView(APIView): """ @@ -28,21 +22,41 @@ class CommentsAPIView(APIView): permission_classes = (AllowAny,) + def _validate_comment(self, comment_id): + """ + Validates the comment if it exists or not. + + Parameters: + comment_id: The ID of the comment. + Response: + comment object for the given comment_id. + """ + comment = Comment().get(comment_id) + if not comment: + raise ObjectDoesNotExist + return comment + def get(self, request: Request, comment_id: str = None) -> Response: """ Retrieves a parent comment. For chile comments, below API is called that return all child comments in children field url: http://localhost:8000/forum/api/v2/threads/66ab94950dead7001deb947a + + Parameters: + request (Request): The incoming request. + comment_id: The ID of the comment. + Body: + Empty. + Response: + The details of the comment for the given comment_id. """ try: - comment = Comment().get(comment_id) - if not comment: - raise ObjectDoesNotExist + comment = self._validate_comment(comment_id) except ObjectDoesNotExist: return Response( {"error": "Comment does not exist"}, status=status.HTTP_404_NOT_FOUND ) - data = self._prepare_response("get", comment) + data = self._prepare_response(comment, exclude_fields=["sk"]) return Response(data, status=status.HTTP_200_OK) def post(self, request: Request, comment_id: str = None) -> Response: @@ -50,6 +64,18 @@ def post(self, request: Request, comment_id: str = None) -> Response: Creates a new child comment. For parent comment below API is called. url: http://localhost:8000/forum/api/v2/threads/66ab94950dead7001deb947a/comments + + Parameters: + request (Request): The incoming request. + comment_id: The ID of the parent comment for creating it's child comment. + Body: + body: The content of the comment. + course_id: The Id of the respective course. + user_id: The requesting user id. + anonymous: anonymous flag(True or False). + anonymous_to_peers: anonymous to peers flag(True or False). + Response: + The details of the comment that is created. """ data = request.data # TODO validations @@ -63,69 +89,93 @@ def post(self, request: Request, comment_id: str = None) -> Response: depth=1, ) comment = Comment().get(new_comment_id) - data = self._prepare_response("post", comment) + data = self._prepare_response(comment, exclude_fields=["endorsement", "sk"]) return Response(data, status=status.HTTP_200_OK) def put(self, request: Request, comment_id: str) -> Response: """ - Update an existing child and parent comment. + Updates an existing child/parent comment. + + Parameters: + request (Request): The incoming request. + comment_id: The ID of the comment to be edited. + Body: + fields to be updated. + Response: + The details of the comment that is updated. """ try: - comment = Comment().get(comment_id) - if not comment: - raise ObjectDoesNotExist + comment = self._validate_comment(comment_id) except ObjectDoesNotExist: return Response( {"error": "Comment does not exist"}, status=status.HTTP_404_NOT_FOUND ) data = request.POST.dict() + fields = [ + ("body", data.get("body")), + ("course_id", data.get("course_id")), + ("anonymous", str_to_bool(data.get("anonymous", False))), + ("anonymous_to_peers", str_to_bool(data.get("anonymous_to_peers", False))), + ("closed", str_to_bool(data.get("closed", False))), + ("endorsed", str_to_bool(data.get("endorsed", False))), + ("author_id", data.get("user_id")), + ("editing_user_id", data.get("editing_user_id")), + ("edit_reason_code", data.get("edit_reason_code")), + ("endorsement_user_id", data.get("endorsement_user_id")), + ] update_comment_data: dict[str, Any] = { - "body": data.get("body", comment.get("body", "")), - "anonymous": str_to_bool(data.get("anonymous", False)), - "anonymous_to_peers": str_to_bool(data.get("anonymous_to_peers", False)), - "course_id": data.get("course_id", ""), - "closed": str_to_bool(data.get("closed", False)), - "author_id": data.get("user_id"), - "endorsed": str_to_bool(data.get("endorsed", False)), + field: value for field, value in fields if value is not None } - if "editing_user_id" in data: - update_comment_data["editing_user_id"] = data.get("editing_user_id") - - if "edit_reason_code" in data: - update_comment_data["edit_reason_code"] = data.get("edit_reason_code") - - if "endorsement_user_id" in data: - update_comment_data["endorsement_user_id"] = data.get("endorsement_user_id") - update_comment_data["edit_history"] = comment.get("edit_history", []) update_comment_data["original_body"] = comment.get("body") Comment().update(comment_id, **update_comment_data) updated_comment = Comment().get(comment_id) - data = self._prepare_response("put", updated_comment) + data = self._prepare_response( + updated_comment, + exclude_fields=( + ["endorsement", "sk"] if updated_comment.get("parent_id") else ["sk"] + ), + ) return Response(data, status=status.HTTP_200_OK) def delete(self, request: Request, comment_id: str) -> Response: """ - Delete a comment. + Deletes a comment. + + Parameters: + request (Request): The incoming request. + comment_id: The ID of the comment to be deleted. + Body: + Empty. + Response: + The details of the comment that is deleted. """ try: - comment = Comment().get(comment_id) - if not comment: - raise ObjectDoesNotExist + comment = self._validate_comment(comment_id) except ObjectDoesNotExist: return Response( {"error": "Comment does not exist"}, status=status.HTTP_404_NOT_FOUND ) - data = self._prepare_response("delete", comment) + data = self._prepare_response(comment, exclude_fields=["endorsement", "sk"]) Comment().delete(comment_id) return Response(data, status=status.HTTP_200_OK) - def _prepare_response(self, method, comment): + def _prepare_response(self, comment, exclude_fields=[]): + """ + Return serialized validated data. + + Parameters: + comment: The comment details that needs to be serialized. + exclude_fields: Any fields that need to be excluded from response. + + Response: + serialized validated data of the comment. + """ comment_data = { **comment, "id": str(comment.get("_id")), @@ -135,18 +185,10 @@ def _prepare_response(self, method, comment): "parent_id": str(comment.get("parent_id")), "type": comment.get("_type").lower(), } - if method == "get": - serializer = CommentsAPIGetSerializer(data=comment_data) - elif method == "post": - serializer = CommentsAPIPostAndDeleteSerializer(data=comment_data) - elif method == "put": - if comment.get("parent_id"): - serializer = CommentsAPIPostAndDeleteSerializer(data=comment_data) - else: - serializer = CommentsAPIGetSerializer(data=comment_data) - else: - serializer = CommentsAPIPostAndDeleteSerializer(data=comment_data) + serializer = CommentsAPISerializer( + data=comment_data, exclude_fields=exclude_fields + ) if not serializer.is_valid(): raise ValidationError(serializer.errors) - return serializer.data + return serializer.validated_data From 05cbb6210746be9b3c4ac0eac58ae0904d7c9fae Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Tue, 20 Aug 2024 13:25:55 +0500 Subject: [PATCH 3/7] fix: resolved quality checks - resolved quality checks - fixed Comments models insert method definition --- forum/models/comments.py | 34 ++++++++++-------- forum/serializers/comments_api.py | 42 +++++++++++++++++----- forum/utils.py | 1 + forum/views/comments.py | 58 ++++++++++++++++++++----------- 4 files changed, 91 insertions(+), 44 deletions(-) diff --git a/forum/models/comments.py b/forum/models/comments.py index 8e645d36..de0e96ea 100644 --- a/forum/models/comments.py +++ b/forum/models/comments.py @@ -20,10 +20,10 @@ def insert( self, body: str, course_id: str, - parent_id: str, author_id: str, - comment_thread_id: str = None, - author_username: str = None, + parent_id: Optional[str] = None, + comment_thread_id: Optional[str] = None, + author_username: Optional[str] = None, anonymous: bool = False, anonymous_to_peers: bool = False, depth: int = 0, @@ -45,9 +45,9 @@ def insert( str: The ID of the inserted document. """ date = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") - parent_comment = self.get(parent_id) - parent_child_count = parent_comment.get("child_count") - if not comment_thread_id: + parent_comment = parent_id and self.get(parent_id) + parent_child_count = parent_comment and parent_comment.get("child_count") + if parent_comment and not comment_thread_id: comment_thread_id = parent_comment.get("comment_thread_id") comment_data = { @@ -73,7 +73,8 @@ def insert( "updated_at": date, } result = self._collection.insert_one(comment_data) - self.update(parent_id, child_count=parent_child_count + 1) + if parent_id and parent_child_count: + self.update(parent_id, child_count=parent_child_count + 1) return str(result.inserted_id) def update( @@ -94,7 +95,7 @@ def update( child_count: Optional[int] = None, depth: Optional[int] = None, closed: Optional[bool] = None, - edit_history: Optional[list[dict[str, Any]]] = [], + edit_history: Optional[list[dict[str, Any]]] = None, original_body: Optional[str] = None, editing_user_id: Optional[str] = None, edit_reason_code: Optional[str] = None, @@ -150,6 +151,7 @@ def update( } if editing_user_id: + edit_history = [] if edit_history is None else edit_history edit_history.append( { "original_body": original_body, @@ -178,15 +180,17 @@ def delete(self, _id: str) -> int: The number of comments deleted. """ comment = self.get(_id) - parent_comment_id = comment.get("parent_id") + parent_comment_id = comment and comment.get("parent_id") parent_comment = parent_comment_id and self.get(parent_comment_id) + parent_comment_child_count = parent_comment and parent_comment.get( + "child_count" + ) result = self._collection.delete_one({"_id": ObjectId(_id)}) - if parent_comment: - self.update( - parent_comment_id, child_count=parent_comment.get("child_count") - 1 - ) + if parent_comment_id and parent_comment_child_count: + self.update(parent_comment_id, child_count=parent_comment_child_count - 1) return result.deleted_count - def get_author_username(self, author_id): + def get_author_username(self, author_id: str) -> str | None: + """Return username for the respective author_id(user_id)""" user = Users().get(author_id) - return user.get("username") + return user.get("username") if user else None diff --git a/forum/serializers/comments_api.py b/forum/serializers/comments_api.py index dbd08dd8..b34fc29e 100644 --- a/forum/serializers/comments_api.py +++ b/forum/serializers/comments_api.py @@ -2,19 +2,36 @@ Serializer for the comment data. """ +from typing import Any + from rest_framework import serializers -from forum.models.comments import Comment from forum.serializers.contents import EditHistorySerializer from forum.serializers.votes import VoteSummarySerializer -class EndorsementSerializer(serializers.Serializer): - user_id = serializers.IntegerField() +class EndorsementSerializer(serializers.Serializer[dict[str, Any]]): + """ + Serializer for handling endorsement of a comment + + Attributes: + user_id (str): The endorsement user id. + time (datetime): The timestamp of when the user has endorsed the comment. + """ + + user_id = serializers.CharField() time = serializers.DateTimeField(format="%Y-%m-%dT%H:%M:%SZ") + def create(self, validated_data: dict[str, Any]) -> Any: + """Raise NotImplementedError""" + raise NotImplementedError + + def update(self, instance: Any, validated_data: dict[str, Any]) -> Any: + """Raise NotImplementedError""" + raise NotImplementedError + -class CommentsAPISerializer(serializers.Serializer): +class CommentsAPISerializer(serializers.Serializer[dict[str, Any]]): """ Serializer for handling user comment api calls. """ @@ -46,18 +63,27 @@ class CommentsAPISerializer(serializers.Serializer): sk = serializers.SerializerMethodField() endorsement = EndorsementSerializer(default=None, required=False, allow_null=True) - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: exclude_fields = kwargs.pop("exclude_fields", None) super().__init__(*args, **kwargs) if exclude_fields: for field in exclude_fields: self.fields.pop(field, None) - def get_sk(self, obj): - is_child = True if obj.get("parent_id") else False - if is_child: + def get_sk(self, obj: dict[str, Any]) -> str: + """Return sk field""" + is_child = obj.get("parent_id") + if is_child is not None: return "{parent_id}-{id}".format( parent_id=obj.get("parent_id"), id=obj.get("_id") ) else: return "{id}".format(id=obj.get("_id")) + + def create(self, validated_data: dict[str, Any]) -> Any: + """Raise NotImplementedError""" + raise NotImplementedError + + def update(self, instance: Any, validated_data: dict[str, Any]) -> Any: + """Raise NotImplementedError""" + raise NotImplementedError diff --git a/forum/utils.py b/forum/utils.py index 2e4056f3..6c4782f9 100644 --- a/forum/utils.py +++ b/forum/utils.py @@ -43,4 +43,5 @@ def handle_proxy_requests(request: HttpRequest, suffix: str, method: str) -> Res def str_to_bool(value: str) -> bool: + """Convert str to bool.""" return value.lower() in ("true", "1") diff --git a/forum/views/comments.py b/forum/views/comments.py index 416c2bb1..355ad6ce 100644 --- a/forum/views/comments.py +++ b/forum/views/comments.py @@ -1,6 +1,6 @@ """Forum Comments API Views.""" -from typing import Any +from typing import Any, Optional from django.core.exceptions import ObjectDoesNotExist from rest_framework import status @@ -22,7 +22,7 @@ class CommentsAPIView(APIView): permission_classes = (AllowAny,) - def _validate_comment(self, comment_id): + def _validate_comment(self, comment_id: str) -> dict[str, Any]: """ Validates the comment if it exists or not. @@ -36,7 +36,7 @@ def _validate_comment(self, comment_id): raise ObjectDoesNotExist return comment - def get(self, request: Request, comment_id: str = None) -> Response: + def get(self, request: Request, comment_id: str) -> Response: """ Retrieves a parent comment. For chile comments, below API is called that return all child comments in children field @@ -59,7 +59,7 @@ def get(self, request: Request, comment_id: str = None) -> Response: data = self._prepare_response(comment, exclude_fields=["sk"]) return Response(data, status=status.HTTP_200_OK) - def post(self, request: Request, comment_id: str = None) -> Response: + def post(self, request: Request, comment_id: str) -> Response: """ Creates a new child comment. For parent comment below API is called. @@ -80,16 +80,22 @@ def post(self, request: Request, comment_id: str = None) -> Response: data = request.data # TODO validations new_comment_id = Comment().insert( - body=data.get("body"), - course_id=data.get("course_id"), + body=data["body"], + course_id=data["course_id"], anonymous=data.get("anonymous", False), anonymous_to_peers=data.get("anonymous_to_peers", False), - author_id=data.get("user_id"), + author_id=data["user_id"], parent_id=comment_id, depth=1, ) comment = Comment().get(new_comment_id) - data = self._prepare_response(comment, exclude_fields=["endorsement", "sk"]) + try: + if comment: + data = self._prepare_response( + comment, exclude_fields=["endorsement", "sk"] + ) + except ValidationError as e: + return Response(e, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_200_OK) @@ -116,10 +122,13 @@ def put(self, request: Request, comment_id: str) -> Response: fields = [ ("body", data.get("body")), ("course_id", data.get("course_id")), - ("anonymous", str_to_bool(data.get("anonymous", False))), - ("anonymous_to_peers", str_to_bool(data.get("anonymous_to_peers", False))), - ("closed", str_to_bool(data.get("closed", False))), - ("endorsed", str_to_bool(data.get("endorsed", False))), + ("anonymous", str_to_bool(data.get("anonymous", "False"))), + ( + "anonymous_to_peers", + str_to_bool(data.get("anonymous_to_peers", "False")), + ), + ("closed", str_to_bool(data.get("closed", "False"))), + ("endorsed", str_to_bool(data.get("endorsed", "False"))), ("author_id", data.get("user_id")), ("editing_user_id", data.get("editing_user_id")), ("edit_reason_code", data.get("edit_reason_code")), @@ -133,13 +142,18 @@ def put(self, request: Request, comment_id: str) -> Response: Comment().update(comment_id, **update_comment_data) updated_comment = Comment().get(comment_id) - data = self._prepare_response( - updated_comment, - exclude_fields=( - ["endorsement", "sk"] if updated_comment.get("parent_id") else ["sk"] - ), - ) - + try: + if updated_comment: + data = self._prepare_response( + updated_comment, + exclude_fields=( + ["endorsement", "sk"] + if updated_comment.get("parent_id") + else ["sk"] + ), + ) + except ValidationError as e: + return Response(e, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_200_OK) def delete(self, request: Request, comment_id: str) -> Response: @@ -165,7 +179,9 @@ def delete(self, request: Request, comment_id: str) -> Response: return Response(data, status=status.HTTP_200_OK) - def _prepare_response(self, comment, exclude_fields=[]): + def _prepare_response( + self, comment: dict[str, Any], exclude_fields: Optional[list[str]] = None + ) -> dict[str, Any]: """ Return serialized validated data. @@ -183,7 +199,7 @@ def _prepare_response(self, comment, exclude_fields=[]): "thread_id": str(comment.get("comment_thread_id")), "username": comment.get("author_username"), "parent_id": str(comment.get("parent_id")), - "type": comment.get("_type").lower(), + "type": str(comment.get("_type", "")).lower(), } serializer = CommentsAPISerializer( data=comment_data, exclude_fields=exclude_fields From 8a534a5c063a5a11c628dc776f3d5f3959c21145 Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Tue, 20 Aug 2024 13:34:02 +0500 Subject: [PATCH 4/7] fix: previous tests for comments model --- tests/test_models/test_comments.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/test_models/test_comments.py b/tests/test_models/test_comments.py index 48ba9b7d..aab39378 100644 --- a/tests/test_models/test_comments.py +++ b/tests/test_models/test_comments.py @@ -43,13 +43,25 @@ def test_list() -> None: author_username = "edly" Comment().insert( - "

Comment 1

", course_id, thread_id, author_id, author_username + "

Comment 1

", + course_id, + author_id, + comment_thread_id=thread_id, + author_username=author_username, ) Comment().insert( - "

Comment 2

", course_id, thread_id, author_id, author_username + "

Comment 2

", + course_id, + author_id, + comment_thread_id=thread_id, + author_username=author_username, ) Comment().insert( - "

Comment 3

", course_id, thread_id, author_id, author_username + "

Comment 3

", + course_id, + author_id, + comment_thread_id=thread_id, + author_username=author_username, ) comments_list = Comment().list() From 8490cc6c7692b4e7b059ff53367b9fd70ba94be1 Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Tue, 20 Aug 2024 16:55:47 +0500 Subject: [PATCH 5/7] chore: remove comment --- forum/views/comments.py | 1 - 1 file changed, 1 deletion(-) diff --git a/forum/views/comments.py b/forum/views/comments.py index 355ad6ce..42992f4f 100644 --- a/forum/views/comments.py +++ b/forum/views/comments.py @@ -78,7 +78,6 @@ def post(self, request: Request, comment_id: str) -> Response: The details of the comment that is created. """ data = request.data - # TODO validations new_comment_id = Comment().insert( body=data["body"], course_id=data["course_id"], From 4355c207662ebb44812df49e3927c1c83cb91f2d Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Tue, 20 Aug 2024 17:13:08 +0500 Subject: [PATCH 6/7] fix: flags api tests as comments model is changed --- tests/test_views/test_flags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_views/test_flags.py b/tests/test_views/test_flags.py index 7de3adbd..d7cd8913 100644 --- a/tests/test_views/test_flags.py +++ b/tests/test_views/test_flags.py @@ -56,9 +56,9 @@ def test_comment_flag_api(api_client: APIClient) -> None: comment_id = Comment().insert( "

Comment 1

", course_id, - comment_thread_id, author_id, - author_username, + comment_thread_id=comment_thread_id, + author_username=author_username, abuse_flaggers=[], historical_abuse_flaggers=[], ) From b42c710063d82177a6e897a70ddf13d2975eb6d2 Mon Sep 17 00:00:00 2001 From: Muhammad Faraz Maqsood Date: Tue, 20 Aug 2024 19:35:17 +0500 Subject: [PATCH 7/7] feat: use single serializer for comments - make changes to the previous comments serializers - resolved comments - fixed adding childs count if it was zero before --- forum/models/comments.py | 20 +++---- forum/serializers/comment.py | 45 ++++++++++++++- forum/serializers/comments_api.py | 89 ------------------------------ forum/serializers/contents.py | 10 ++-- forum/views/comments.py | 92 ++++++++++++++++++++----------- 5 files changed, 119 insertions(+), 137 deletions(-) delete mode 100644 forum/serializers/comments_api.py diff --git a/forum/models/comments.py b/forum/models/comments.py index 720957e8..fbb5f56c 100644 --- a/forum/models/comments.py +++ b/forum/models/comments.py @@ -55,19 +55,17 @@ def insert( Returns: str: The ID of the inserted document. """ - date = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") parent_comment = parent_id and self.get(parent_id) - parent_child_count = parent_comment and parent_comment.get("child_count") + parent_child_count = parent_comment and parent_comment.get("child_count") or 0 if parent_comment and not comment_thread_id: comment_thread_id = parent_comment.get("comment_thread_id") + date = datetime.now() comment_data = { "votes": self.get_votes_dict(up=[], down=[]), "visible": visible, - "abuse_flaggers": [] if abuse_flaggers is None else abuse_flaggers, - "historical_abuse_flaggers": ( - [] if historical_abuse_flaggers is None else historical_abuse_flaggers - ), + "abuse_flaggers": abuse_flaggers or [], + "historical_abuse_flaggers": historical_abuse_flaggers or [], "parent_ids": [ObjectId(parent_id)] if parent_id else [], "at_position_list": [], "body": body, @@ -86,7 +84,7 @@ def insert( "updated_at": date, } result = self._collection.insert_one(comment_data) - if parent_id and parent_child_count: + if parent_id: self.update(parent_id, child_count=parent_child_count + 1) return str(result.inserted_id) @@ -163,7 +161,7 @@ def update( if endorsed and endorsement_user_id: update_data["endorsement"] = { "user_id": endorsement_user_id, - "time": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"), + "time": datetime.now(), } if editing_user_id: @@ -173,12 +171,12 @@ def update( "original_body": original_body, "reason_code": edit_reason_code, "editor_username": self.get_author_username(editing_user_id), - "created_at": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"), + "created_at": datetime.now(), } ) update_data["edit_history"] = edit_history - update_data["updated_at"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") + update_data["updated_at"] = datetime.now() result = self._collection.update_one( {"_id": ObjectId(comment_id)}, {"$set": update_data}, @@ -199,7 +197,7 @@ def delete(self, _id: str) -> int: parent_comment_id = comment and comment.get("parent_id") parent_comment = parent_comment_id and self.get(parent_comment_id) parent_comment_child_count = parent_comment and parent_comment.get( - "child_count" + "child_count", ) result = self._collection.delete_one({"_id": ObjectId(_id)}) if parent_comment_id and parent_comment_child_count: diff --git a/forum/serializers/comment.py b/forum/serializers/comment.py index 2e1558fb..697a0b6e 100644 --- a/forum/serializers/comment.py +++ b/forum/serializers/comment.py @@ -7,6 +7,28 @@ from rest_framework import serializers from forum.serializers.contents import UserContentSerializer +from forum.serializers.custom_datetime import CustomDateTimeField + + +class EndorsementSerializer(serializers.Serializer[dict[str, Any]]): + """ + Serializer for handling endorsement of a comment + + Attributes: + user_id (str): The endorsement user id. + time (datetime): The timestamp of when the user has endorsed the comment. + """ + + user_id = serializers.CharField() + time = CustomDateTimeField() + + def create(self, validated_data: dict[str, Any]) -> Any: + """Raise NotImplementedError""" + raise NotImplementedError + + def update(self, instance: Any, validated_data: dict[str, Any]) -> Any: + """Raise NotImplementedError""" + raise NotImplementedError class UserCommentSerializer(UserContentSerializer): @@ -21,13 +43,34 @@ class UserCommentSerializer(UserContentSerializer): thread_id (str): The ID of the thread the comment belongs to. parent_id (str or None): The ID of the parent comment, if any. child_count (int): The number of child comments nested under this comment. + sk (str or None): sk field, has ids data in it. + endorsement: saves endorsement data. """ endorsed = serializers.BooleanField(default=False) depth = serializers.IntegerField(default=0) thread_id = serializers.CharField() - parent_id = serializers.CharField(default=None) + parent_id = serializers.CharField(default=None, allow_null=True) child_count = serializers.IntegerField(default=0) + sk = serializers.SerializerMethodField() + endorsement = EndorsementSerializer(default=None, required=False, allow_null=True) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + exclude_fields = kwargs.pop("exclude_fields", None) + super().__init__(*args, **kwargs) + if exclude_fields: + for field in exclude_fields: + self.fields.pop(field, None) + + def get_sk(self, obj: dict[str, Any]) -> str: + """Return sk field""" + is_child = obj.get("parent_id") + if is_child is not None: + return "{parent_id}-{id}".format( + parent_id=obj.get("parent_id"), id=obj.get("_id") + ) + else: + return "{id}".format(id=obj.get("_id")) def create(self, validated_data: Dict[str, Any]) -> Any: """Raise NotImplementedError""" diff --git a/forum/serializers/comments_api.py b/forum/serializers/comments_api.py deleted file mode 100644 index b34fc29e..00000000 --- a/forum/serializers/comments_api.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Serializer for the comment data. -""" - -from typing import Any - -from rest_framework import serializers - -from forum.serializers.contents import EditHistorySerializer -from forum.serializers.votes import VoteSummarySerializer - - -class EndorsementSerializer(serializers.Serializer[dict[str, Any]]): - """ - Serializer for handling endorsement of a comment - - Attributes: - user_id (str): The endorsement user id. - time (datetime): The timestamp of when the user has endorsed the comment. - """ - - user_id = serializers.CharField() - time = serializers.DateTimeField(format="%Y-%m-%dT%H:%M:%SZ") - - def create(self, validated_data: dict[str, Any]) -> Any: - """Raise NotImplementedError""" - raise NotImplementedError - - def update(self, instance: Any, validated_data: dict[str, Any]) -> Any: - """Raise NotImplementedError""" - raise NotImplementedError - - -class CommentsAPISerializer(serializers.Serializer[dict[str, Any]]): - """ - Serializer for handling user comment api calls. - """ - - id = serializers.CharField() - user_id = serializers.CharField() - thread_id = serializers.CharField() - username = serializers.CharField() - parent_id = serializers.CharField() - endorsed = serializers.BooleanField(default=False) - anonymous = serializers.BooleanField(default=False) - anonymous_to_peers = serializers.BooleanField(default=False) - closed = serializers.BooleanField(default=False) - body = serializers.CharField() - course_id = serializers.CharField() - parent_id = serializers.CharField(default=None, allow_null=True) - commentable_id = serializers.CharField(default="course") - created_at = serializers.DateTimeField(format="%Y-%m-%dT%H:%M:%SZ") - updated_at = serializers.DateTimeField(format="%Y-%m-%dT%H:%M:%SZ") - depth = serializers.IntegerField(default=0) - abuse_flaggers = serializers.ListField( - child=serializers.CharField(), allow_null=True - ) - at_position_list = serializers.ListField(allow_null=True) - type = serializers.CharField() - child_count = serializers.IntegerField(default=0) - votes = VoteSummarySerializer() - edit_history = EditHistorySerializer(default=[], many=True) - sk = serializers.SerializerMethodField() - endorsement = EndorsementSerializer(default=None, required=False, allow_null=True) - - def __init__(self, *args: Any, **kwargs: Any) -> None: - exclude_fields = kwargs.pop("exclude_fields", None) - super().__init__(*args, **kwargs) - if exclude_fields: - for field in exclude_fields: - self.fields.pop(field, None) - - def get_sk(self, obj: dict[str, Any]) -> str: - """Return sk field""" - is_child = obj.get("parent_id") - if is_child is not None: - return "{parent_id}-{id}".format( - parent_id=obj.get("parent_id"), id=obj.get("_id") - ) - else: - return "{id}".format(id=obj.get("_id")) - - def create(self, validated_data: dict[str, Any]) -> Any: - """Raise NotImplementedError""" - raise NotImplementedError - - def update(self, instance: Any, validated_data: dict[str, Any]) -> Any: - """Raise NotImplementedError""" - raise NotImplementedError diff --git a/forum/serializers/contents.py b/forum/serializers/contents.py index 0652243e..22ebf14d 100644 --- a/forum/serializers/contents.py +++ b/forum/serializers/contents.py @@ -126,14 +126,16 @@ class UserContentSerializer(serializers.Serializer[dict[str, Any]]): course_id = serializers.CharField() anonymous = serializers.BooleanField(default=False) anonymous_to_peers = serializers.BooleanField(default=False) - created_at = CustomDateTimeField() - updated_at = CustomDateTimeField() - at_position_list = serializers.ListField(default=[]) + created_at = CustomDateTimeField(allow_null=True) + updated_at = CustomDateTimeField(allow_null=True) + at_position_list = serializers.ListField(allow_null=True) user_id = serializers.CharField(source="author_id") username = serializers.CharField(source="author_username") commentable_id = serializers.CharField(default="course") votes = VoteSummarySerializer() - abuse_flaggers = serializers.ListField(default=[]) + abuse_flaggers = serializers.ListField( + child=serializers.CharField(), allow_null=True + ) edit_history = EditHistorySerializer(default=[], many=True) closed = serializers.BooleanField(default=False) type = serializers.CharField() diff --git a/forum/views/comments.py b/forum/views/comments.py index 42992f4f..d470f575 100644 --- a/forum/views/comments.py +++ b/forum/views/comments.py @@ -11,7 +11,7 @@ from rest_framework.views import APIView from forum.models import Comment -from forum.serializers.comments_api import CommentsAPISerializer +from forum.serializers.comment import UserCommentSerializer from forum.utils import str_to_bool @@ -54,9 +54,13 @@ def get(self, request: Request, comment_id: str) -> Response: comment = self._validate_comment(comment_id) except ObjectDoesNotExist: return Response( - {"error": "Comment does not exist"}, status=status.HTTP_404_NOT_FOUND + {"error": "Comment does not exist"}, + status=status.HTTP_404_NOT_FOUND, ) - data = self._prepare_response(comment, exclude_fields=["sk"]) + data = self._prepare_response( + comment, + exclude_fields=["sk"], + ) return Response(data, status=status.HTTP_200_OK) def post(self, request: Request, comment_id: str) -> Response: @@ -78,6 +82,14 @@ def post(self, request: Request, comment_id: str) -> Response: The details of the comment that is created. """ data = request.data + fields_to_validate = ["body", "course_id", "user_id"] + for field in fields_to_validate: + if field not in data: + return Response( + f"{field} is missing.", + status=status.HTTP_400_BAD_REQUEST, + ) + new_comment_id = Comment().insert( body=data["body"], course_id=data["course_id"], @@ -91,10 +103,14 @@ def post(self, request: Request, comment_id: str) -> Response: try: if comment: data = self._prepare_response( - comment, exclude_fields=["endorsement", "sk"] + comment, + exclude_fields=["endorsement", "sk"], ) - except ValidationError as e: - return Response(e, status=status.HTTP_400_BAD_REQUEST) + except ValidationError as error: + return Response( + error, + status=status.HTTP_400_BAD_REQUEST, + ) return Response(data, status=status.HTTP_200_OK) @@ -114,28 +130,12 @@ def put(self, request: Request, comment_id: str) -> Response: comment = self._validate_comment(comment_id) except ObjectDoesNotExist: return Response( - {"error": "Comment does not exist"}, status=status.HTTP_404_NOT_FOUND + {"error": "Comment does not exist"}, + status=status.HTTP_404_NOT_FOUND, ) data = request.POST.dict() - fields = [ - ("body", data.get("body")), - ("course_id", data.get("course_id")), - ("anonymous", str_to_bool(data.get("anonymous", "False"))), - ( - "anonymous_to_peers", - str_to_bool(data.get("anonymous_to_peers", "False")), - ), - ("closed", str_to_bool(data.get("closed", "False"))), - ("endorsed", str_to_bool(data.get("endorsed", "False"))), - ("author_id", data.get("user_id")), - ("editing_user_id", data.get("editing_user_id")), - ("edit_reason_code", data.get("edit_reason_code")), - ("endorsement_user_id", data.get("endorsement_user_id")), - ] - update_comment_data: dict[str, Any] = { - field: value for field, value in fields if value is not None - } + update_comment_data: dict[str, Any] = self._get_update_comment_data(data) update_comment_data["edit_history"] = comment.get("edit_history", []) update_comment_data["original_body"] = comment.get("body") @@ -151,8 +151,11 @@ def put(self, request: Request, comment_id: str) -> Response: else ["sk"] ), ) - except ValidationError as e: - return Response(e, status=status.HTTP_400_BAD_REQUEST) + except ValidationError as error: + return Response( + error, + status=status.HTTP_400_BAD_REQUEST, + ) return Response(data, status=status.HTTP_200_OK) def delete(self, request: Request, comment_id: str) -> Response: @@ -171,9 +174,13 @@ def delete(self, request: Request, comment_id: str) -> Response: comment = self._validate_comment(comment_id) except ObjectDoesNotExist: return Response( - {"error": "Comment does not exist"}, status=status.HTTP_404_NOT_FOUND + {"error": "Comment does not exist"}, + status=status.HTTP_404_NOT_FOUND, ) - data = self._prepare_response(comment, exclude_fields=["endorsement", "sk"]) + data = self._prepare_response( + comment, + exclude_fields=["endorsement", "sk"], + ) Comment().delete(comment_id) return Response(data, status=status.HTTP_200_OK) @@ -200,10 +207,31 @@ def _prepare_response( "parent_id": str(comment.get("parent_id")), "type": str(comment.get("_type", "")).lower(), } - serializer = CommentsAPISerializer( - data=comment_data, exclude_fields=exclude_fields + serializer = UserCommentSerializer( + data=comment_data, + exclude_fields=exclude_fields, ) if not serializer.is_valid(): raise ValidationError(serializer.errors) - return serializer.validated_data + return serializer.data + + def _get_update_comment_data(self, data: dict[str, Any]) -> dict[str, Any]: + """convert request data to a dict excluding empty data""" + + fields = [ + ("body", data.get("body")), + ("course_id", data.get("course_id")), + ("anonymous", str_to_bool(data.get("anonymous", "False"))), + ( + "anonymous_to_peers", + str_to_bool(data.get("anonymous_to_peers", "False")), + ), + ("closed", str_to_bool(data.get("closed", "False"))), + ("endorsed", str_to_bool(data.get("endorsed", "False"))), + ("author_id", data.get("user_id")), + ("editing_user_id", data.get("editing_user_id")), + ("edit_reason_code", data.get("edit_reason_code")), + ("endorsement_user_id", data.get("endorsement_user_id")), + ] + return {field: value for field, value in fields if value is not None}