From 19de36176c9564c8b7d0702d0d50ece58d46d32b Mon Sep 17 00:00:00 2001 From: Naincy Chourasia Date: Thu, 2 Jul 2026 10:31:53 +0000 Subject: [PATCH] fix: correct load more button visibility for discussion responses --- forum/api/threads.py | 1 + forum/backends/mongodb/api.py | 50 +++++++++++++++++++++++++++---- forum/backends/mysql/api.py | 9 ++++++ forum/serializers/comment.py | 13 ++++---- forum/serializers/thread.py | 34 +++++++++++++-------- tests/test_views/test_comments.py | 2 +- 6 files changed, 84 insertions(+), 25 deletions(-) diff --git a/forum/api/threads.py b/forum/api/threads.py index 2034f927..b92aecca 100644 --- a/forum/api/threads.py +++ b/forum/api/threads.py @@ -122,6 +122,7 @@ def prepare_thread_api_response( "mark_as_read", "reverse_order", "merge_question_type_responses", + "show_deleted", ] for param in params: value = data_or_params.get(param) diff --git a/forum/backends/mongodb/api.py b/forum/backends/mongodb/api.py index 0f05ad20..4f33b9a4 100644 --- a/forum/backends/mongodb/api.py +++ b/forum/backends/mongodb/api.py @@ -1717,6 +1717,8 @@ def get_comments(**kwargs: Any) -> list[dict[str, Any]]: kwargs["comment_thread_id"] = ObjectId(kwargs["comment_thread_id"]) if parent_id := kwargs.get("parent_id"): kwargs["parent_id"] = ObjectId(parent_id) + if "is_deleted" in kwargs and kwargs["is_deleted"] is False: + kwargs["is_deleted"] = {"$ne": True} return list(Comment().get_list(**kwargs)) @@ -1727,6 +1729,8 @@ def get_comments_count(**kwargs: Any) -> int: kwargs["comment_thread_id"] = ObjectId(kwargs["comment_thread_id"]) if parent_id := kwargs.get("parent_id"): kwargs["parent_id"] = ObjectId(parent_id) + if "is_deleted" in kwargs and kwargs["is_deleted"] is False: + kwargs["is_deleted"] = {"$ne": True} return Comment().count_documents(kwargs) @@ -1740,16 +1744,32 @@ def delete_comment(comment_id: str) -> None: """Delete comment.""" Comment().delete(comment_id) - @staticmethod + @classmethod def soft_delete_comment( - comment_id: str, deleted_by: Optional[str] = None + cls, comment_id: str, deleted_by: Optional[str] = None ) -> tuple[int, int]: """Soft delete comment by marking it as deleted. Returns: tuple: (responses_deleted, replies_deleted) """ - return Comment().delete(comment_id, mode="soft", deleted_by=deleted_by) + comment = Comment().get(comment_id) + result = Comment().delete(comment_id, mode="soft", deleted_by=deleted_by) + + if comment: + if comment.get("parent_id"): + # It's a reply — decrement parent's child_count + parent_id = str(comment["parent_id"]) + parent = Comment().get(parent_id) + if parent: + current_count = parent.get("child_count", 0) + if current_count > 0: + Comment().update(parent_id, child_count=current_count - 1) + else: + # It's a response — set child_count to 0 (all children soft-deleted) + Comment().update(str(comment["_id"]), child_count=0) + + return result @staticmethod def get_thread_id_from_comment(comment_id: str) -> dict[str, Any] | None: @@ -1791,10 +1811,28 @@ def soft_delete_thread(thread_id: str, deleted_by: Optional[str] = None) -> int: thread_id, is_deleted=True, deleted_at=datetime.now(), deleted_by=deleted_by ) - @staticmethod - def restore_comment(comment_id: str, restored_by: Optional[str] = None) -> bool: + @classmethod + def restore_comment( + cls, comment_id: str, restored_by: Optional[str] = None + ) -> bool: """Restore a soft-deleted comment.""" - return Comment().restore_comment(comment_id, restored_by=restored_by) + comment = Comment().get(comment_id) + result = Comment().restore_comment(comment_id, restored_by=restored_by) + + if comment: + if comment.get("parent_id"): + # It's a reply — increment parent's child_count + parent_id = str(comment["parent_id"]) + parent = Comment().get(parent_id) + if parent: + current_count = parent.get("child_count", 0) + Comment().update(parent_id, child_count=current_count + 1) + else: + # It's a response — restore child_count to total children + children = Comment().find({"parent_id": comment["_id"]}) + total_children = sum(1 for _ in children) + Comment().update(str(comment["_id"]), child_count=total_children) + return result @staticmethod def restore_thread(thread_id: str, restored_by: Optional[str] = None) -> bool: diff --git a/forum/backends/mysql/api.py b/forum/backends/mysql/api.py index d443a9a6..49be84b8 100644 --- a/forum/backends/mysql/api.py +++ b/forum/backends/mysql/api.py @@ -1727,6 +1727,9 @@ def soft_delete_comment( comment.deleted_at = timezone.now() comment.deleted_by = deleted_user # type: ignore[assignment] comment.save() + Comment.objects.filter(pk=comment.parent.pk, child_count__gt=0).update( + child_count=F("child_count") - 1 + ) # replies_deleted = 1 (one reply), responses_deleted = 0 return 0, 1 @@ -1747,6 +1750,7 @@ def soft_delete_comment( deleted_by=deleted_user, ) # responses_deleted = 1 (the parent), replies_deleted = number updated + Comment.objects.filter(pk=comment.pk).update(child_count=0) return 1, int(replies_deleted) @classmethod @@ -1773,6 +1777,9 @@ def restore_comment( # Update user course stats if is_reply: # This is a reply - increment replies, decrement deleted_replies + Comment.objects.filter(pk=comment.parent.pk).update( + child_count=F("child_count") + 1 + ) cls.update_stats_for_course( author_id, course_id, replies=1, deleted_replies=-1 ) @@ -1782,6 +1789,8 @@ def restore_comment( deleted_child_count = Comment.objects.filter( parent=comment, is_deleted=True ).count() + total_children = Comment.objects.filter(parent=comment).count() + Comment.objects.filter(pk=comment.pk).update(child_count=total_children) cls.update_stats_for_course( author_id, diff --git a/forum/serializers/comment.py b/forum/serializers/comment.py index 4c68b0f1..7abfb3c2 100644 --- a/forum/serializers/comment.py +++ b/forum/serializers/comment.py @@ -70,11 +70,14 @@ def get_children(self, obj: Any) -> list[dict[str, Any]]: if not self.context.get("recursive", False): return [] - children = self.backend.get_comments( - parent_id=obj["_id"], - depth=1, - sort=self.context.get("sort", -1), - ) + filter_kwargs: dict[str, Any] = { + "parent_id": obj["_id"], + "depth": 1, + "sort": self.context.get("sort", -1), + } + if not self.context.get("show_deleted", False): + filter_kwargs["is_deleted"] = False + children = self.backend.get_comments(**filter_kwargs) children_data = prepare_comment_data_for_get_children(children) serializer = CommentSerializer( children_data, diff --git a/forum/serializers/thread.py b/forum/serializers/thread.py index c501c3c2..3720f54e 100644 --- a/forum/serializers/thread.py +++ b/forum/serializers/thread.py @@ -89,6 +89,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.merge_question_type_responses = self.context_data.pop( "merge_question_type_responses", False ) + self.show_deleted = self.context_data.pop("show_deleted", False) # Customize fields based on context if not self.with_responses: @@ -205,14 +206,17 @@ def get_children(self, obj: dict[str, Any]) -> Optional[Any]: """ if self.with_responses: sorting_order = -1 if self.context_data.get("reverse_order", True) else 1 - children = self.backend.get_comments( - comment_thread_id=obj["_id"], - depth=0, - parent_id=None, - sort=sorting_order, - resp_skip=obj["resp_skip"], - resp_limit=obj["resp_limit"], - ) + filter_kwargs: dict[str, Any] = { + "comment_thread_id": obj["_id"], + "depth": 0, + "parent_id": None, + "sort": sorting_order, + "resp_skip": obj["resp_skip"], + "resp_limit": obj["resp_limit"], + } + if not self.show_deleted: + filter_kwargs["is_deleted"] = False + children = self.backend.get_comments(**filter_kwargs) children_data = prepare_comment_data_for_get_children(children) serializer = CommentSerializer( data=children_data, @@ -220,6 +224,7 @@ def get_children(self, obj: dict[str, Any]) -> Optional[Any]: context={ "recursive": self.context_data.get("recursive", False), "sort": sorting_order, + "show_deleted": self.show_deleted, }, exclude_fields=["sk"], backend=self.backend, @@ -240,11 +245,14 @@ def get_resp_total(self, obj: dict[str, Any]) -> int: int: The total number of responses, defaulting to 0 if not included. """ if self.with_responses: - return self.backend.get_comments_count( - comment_thread_id=obj["_id"], - depth=0, - parent_id=None, - ) + filter_kwargs: dict[str, Any] = { + "comment_thread_id": obj["_id"], + "depth": 0, + "parent_id": None, + } + if not self.show_deleted: + filter_kwargs["is_deleted"] = False + return self.backend.get_comments_count(**filter_kwargs) return 0 def to_representation(self, instance: dict[str, Any]) -> dict[str, Any]: diff --git a/tests/test_views/test_comments.py b/tests/test_views/test_comments.py index ff0b89ae..ab2bc2bd 100644 --- a/tests/test_views/test_comments.py +++ b/tests/test_views/test_comments.py @@ -179,7 +179,7 @@ def test_delete_child_comment(api_client: APIClient, patched_get_backend: Any) - new_child_count = parent_comment.get("child_count") assert new_child_count is not None - assert new_child_count == previous_child_count + assert new_child_count == previous_child_count - 1 def test_returns_400_when_comment_does_not_exist(