Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions forum/api/threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
50 changes: 44 additions & 6 deletions forum/backends/mongodb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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)

Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions forum/backends/mysql/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
)
Expand All @@ -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,
Expand Down
13 changes: 8 additions & 5 deletions forum/serializers/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
34 changes: 21 additions & 13 deletions forum/serializers/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -205,21 +206,25 @@ 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,
many=True,
context={
"recursive": self.context_data.get("recursive", False),
"sort": sorting_order,
"show_deleted": self.show_deleted,
},
exclude_fields=["sk"],
backend=self.backend,
Expand All @@ -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]:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_views/test_comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading