Skip to content
Draft
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
8 changes: 6 additions & 2 deletions forum/api/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,14 @@ def update_comment(
raise error


def delete_comment(comment_id: str, course_id: Optional[str] = None) -> dict[str, Any]:
def delete_comment(comment_id: str, course_id: Optional[str] = None, deleted_by: Optional[str] = None) -> dict[str, Any]:
"""
Delete a comment.

Parameters:
comment_id: The ID of the comment to be deleted.
course_id: The ID of the course (optional).
deleted_by: The ID of the user performing the delete (optional).
Body:
Empty.
Response:
Expand All @@ -244,7 +246,9 @@ def delete_comment(comment_id: str, course_id: Optional[str] = None) -> dict[str
backend,
exclude_fields=["endorsement", "sk"],
)
backend.delete_comment(comment_id)
# Soft delete comment instead of hard delete
backend.soft_delete_comment(comment_id, deleted_by)
# backend.delete_comment(comment_id)
author_id = comment["author_id"]
comment_course_id = comment["course_id"]
parent_comment_id = data["parent_id"]
Expand Down
12 changes: 9 additions & 3 deletions forum/api/threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,14 @@ def get_thread(
raise ForumV2RequestError("Failed to prepare thread API response") from error


def delete_thread(thread_id: str, course_id: Optional[str] = None) -> dict[str, Any]:
def delete_thread(thread_id: str, course_id: Optional[str] = None, deleted_by: Optional[str] = None) -> dict[str, Any]:
"""
Delete the thread for the given thread_id.

Parameters:
thread_id: The ID of the thread to be deleted.
course_id: The ID of the course (optional).
deleted_by: The ID of the user performing the delete (optional).
Response:
The details of the thread that is deleted.
"""
Expand All @@ -177,7 +179,9 @@ def delete_thread(thread_id: str, course_id: Optional[str] = None) -> dict[str,
f"Thread does not exist with Id: {thread_id}"
) from exc

backend.delete_comments_of_a_thread(thread_id)
# Soft delete comments and thread instead of hard delete
backend.soft_delete_comments_of_a_thread(thread_id, deleted_by)
# backend.delete_comments_of_a_thread(thread_id)
thread = backend.validate_object("CommentThread", thread_id)

try:
Expand All @@ -187,7 +191,9 @@ def delete_thread(thread_id: str, course_id: Optional[str] = None) -> dict[str,
raise ForumV2RequestError("Failed to prepare thread API response") from error

backend.delete_subscriptions_of_a_thread(thread_id)
result = backend.delete_thread(thread_id)
# Soft delete thread instead of hard delete
result = backend.soft_delete_thread(thread_id, deleted_by)
# result = backend.delete_thread(thread_id)
if result and not (thread["anonymous"] or thread["anonymous_to_peers"]):
backend.update_stats_for_course(
thread["author_id"], thread["course_id"], threads=-1
Expand Down
74 changes: 73 additions & 1 deletion forum/backends/mongodb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@ def handle_threads_query(
base_query: dict[str, Any] = {
"_id": {"$in": comment_thread_obj_ids},
"context": context,
"is_deleted": {"$ne": True}, # Exclude soft deleted threads
}

# Group filtering
Expand Down Expand Up @@ -1495,6 +1496,9 @@ def get_user_by_username(username: str | None) -> dict[str, Any] | None:
def get_comment(comment_id: str) -> dict[str, Any] | None:
"""Get comment from id."""
comment = Comment().get(comment_id)
# Return None if comment is soft deleted
if comment and comment.get('is_deleted'):
return None
return comment

@staticmethod
Expand All @@ -1503,6 +1507,9 @@ def get_thread(thread_id: str) -> dict[str, Any] | None:
thread = CommentThread().get(thread_id)
if not thread:
return None
# Return None if thread is soft deleted
if thread.get('is_deleted'):
return None
return thread

@staticmethod
Expand Down Expand Up @@ -1568,6 +1575,42 @@ def delete_thread(thread_id: str) -> int:
"""Delete thread."""
return CommentThread().delete(thread_id)

@staticmethod
def soft_delete_thread(thread_id: str, deleted_by: str = None) -> int:
"""Soft delete thread by marking it as deleted."""
return CommentThread().update(
thread_id,
is_deleted=True,
deleted_at=datetime.now(),
deleted_by=deleted_by
)

@staticmethod
def soft_delete_comments_of_a_thread(thread_id: str, deleted_by: str = None) -> None:
"""Soft delete all comments of a thread by marking them as deleted."""
deleted_at = datetime.now()
for comment in Comment().get_list(
comment_thread_id=ObjectId(thread_id),
depth=0,
parent_id=None,
):
Comment().update(
comment["_id"],
is_deleted=True,
deleted_at=deleted_at,
deleted_by=deleted_by
)

@staticmethod
def soft_delete_comment(comment_id: str, deleted_by: str = None) -> None:
"""Soft delete comment by marking it as deleted."""
Comment().update(
comment_id,
is_deleted=True,
deleted_at=datetime.now(),
deleted_by=deleted_by
)

@staticmethod
def create_thread(data: dict[str, Any]) -> str:
"""Create thread."""
Expand Down Expand Up @@ -1723,14 +1766,43 @@ def get_paginated_user_stats(
@staticmethod
def get_contents(**kwargs: Any) -> list[dict[str, Any]]:
"""Return contents."""
return list(Contents().get_list(**kwargs))
# Add soft delete filtering
kwargs['is_deleted'] = {'$ne': True}
contents = list(Contents().get_list(**kwargs))

# Get all thread IDs mentioned in comments
comment_thread_ids = set()
for content in contents:
if content.get('_type') == 'Comment' and content.get('comment_thread_id'):
comment_thread_ids.add(content['comment_thread_id'])

# Get all deleted thread IDs in one query
deleted_thread_ids = set()
if comment_thread_ids:
deleted_threads = CommentThread()._collection.find(
{"_id": {"$in": list(comment_thread_ids)}, "is_deleted": True},
{"_id": 1}
)
deleted_thread_ids = {thread['_id'] for thread in deleted_threads}

# Filter out comments that belong to deleted threads
filtered_contents = []
for content in contents:
if content.get('_type') == 'Comment':
thread_id = content.get('comment_thread_id')
if thread_id and thread_id in deleted_thread_ids:
continue # Skip comment if its thread is deleted
filtered_contents.append(content)

return filtered_contents

@staticmethod
def get_user_thread_filter(course_id: str) -> dict[str, Any]:
"""Get user thread filter."""
return {
"_type": {"$in": [CommentThread.content_type]},
"course_id": {"$in": [course_id]},
"is_deleted": {"$ne": True}, # Exclude soft deleted threads
}

@staticmethod
Expand Down
9 changes: 9 additions & 0 deletions forum/backends/mongodb/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ def doc_to_hash(cls, doc: dict[str, Any]) -> dict[str, Any]:
"created_at": doc.get("created_at"),
"updated_at": doc.get("updated_at"),
"title": doc.get("title"),
"is_deleted": doc.get("is_deleted", False),
"deleted_at": doc.get("deleted_at"),
"deleted_by": doc.get("deleted_by"),
}

def insert(
Expand Down Expand Up @@ -166,6 +169,9 @@ def update(
endorsement_user_id: Optional[str] = None,
sk: Optional[str] = None,
is_spam: Optional[bool] = None,
is_deleted: Optional[bool] = None,
deleted_at: Optional[datetime] = None,
deleted_by: Optional[str] = None,
) -> int:
"""
Updates a comment document in the database.
Expand Down Expand Up @@ -210,6 +216,9 @@ def update(
("closed", closed),
("sk", sk),
("is_spam", is_spam),
("is_deleted", is_deleted),
("deleted_at", deleted_at),
("deleted_by", deleted_by),
]
update_data: dict[str, Any] = {
field: value for field, value in fields if value is not None
Expand Down
9 changes: 9 additions & 0 deletions forum/backends/mongodb/threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def doc_to_hash(cls, doc: dict[str, Any]) -> dict[str, Any]:
"author_id": doc.get("author_id"),
"group_id": doc.get("group_id"),
"thread_id": str(doc.get("_id")),
"is_deleted": doc.get("is_deleted", False),
"deleted_at": doc.get("deleted_at"),
"deleted_by": doc.get("deleted_by"),
}

def insert(
Expand Down Expand Up @@ -208,6 +211,9 @@ def update(
group_id: Optional[int] = None,
skip_timestamp_update: bool = False,
is_spam: Optional[bool] = None,
is_deleted: Optional[bool] = None,
deleted_at: Optional[datetime] = None,
deleted_by: Optional[str] = None,
) -> int:
"""
Updates a thread document in the database.
Expand Down Expand Up @@ -262,6 +268,9 @@ def update(
("closed_by_id", closed_by_id),
("group_id", group_id),
("is_spam", is_spam),
("is_deleted", is_deleted),
("deleted_at", deleted_at),
("deleted_by", deleted_by),
]
update_data: dict[str, Any] = {
field: value for field, value in fields if value is not None
Expand Down
48 changes: 41 additions & 7 deletions forum/backends/mysql/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ def handle_threads_query(
raise ValueError("User does not exist") from exc
# Base query
base_query = CommentThread.objects.filter(
pk__in=mysql_comment_thread_ids, context=context
pk__in=mysql_comment_thread_ids, context=context, is_deleted=False # Exclude soft deleted threads
)

# Group filtering
Expand Down Expand Up @@ -986,6 +986,15 @@ def delete_comments_of_a_thread(thread_id: str) -> None:
"""Delete comments of a thread."""
Comment.objects.filter(comment_thread__pk=thread_id, parent=None).delete()

@staticmethod
def soft_delete_comments_of_a_thread(thread_id: str, deleted_by: str = None) -> None:
"""Soft delete comments of a thread by marking them as deleted."""
Comment.objects.filter(comment_thread__pk=thread_id, parent=None).update(
is_deleted=True,
deleted_at=timezone.now(),
deleted_by=deleted_by
)

@classmethod
def delete_subscriptions_of_a_thread(cls, thread_id: str) -> None:
"""Delete subscriptions of a thread."""
Expand Down Expand Up @@ -1450,7 +1459,7 @@ def find_or_create_user(
def get_comment(comment_id: str) -> dict[str, Any] | None:
"""Return comment from comment_id."""
try:
comment = Comment.objects.get(pk=comment_id)
comment = Comment.objects.get(pk=comment_id, is_deleted=False) # Exclude soft deleted comments
except Comment.DoesNotExist:
return None
return comment.to_dict()
Expand Down Expand Up @@ -1530,6 +1539,15 @@ def delete_comment(cls, comment_id: str) -> None:

comment.delete()

@staticmethod
def soft_delete_comment(comment_id: str, deleted_by: str = None) -> None:
"""Soft delete comment by marking it as deleted."""
comment = Comment.objects.get(pk=comment_id)
comment.is_deleted = True
comment.deleted_at = timezone.now()
comment.deleted_by = deleted_by
comment.save()

@staticmethod
def get_commentables_counts_based_on_type(course_id: str) -> dict[str, Any]:
"""Return commentables counts in a course based on thread's type."""
Expand Down Expand Up @@ -1716,7 +1734,7 @@ def get_user(user_id: str, get_full_dict: bool = True) -> dict[str, Any] | None:
def get_thread(thread_id: str) -> dict[str, Any] | None:
"""Return thread from thread_id."""
try:
thread = CommentThread.objects.get(pk=thread_id)
thread = CommentThread.objects.get(pk=thread_id, is_deleted=False) # Exclude soft deleted threads
except CommentThread.DoesNotExist:
return None
return thread.to_dict()
Expand Down Expand Up @@ -1771,6 +1789,19 @@ def delete_thread(thread_id: str) -> int:
thread.delete()
return 1

@staticmethod
def soft_delete_thread(thread_id: str, deleted_by: str = None) -> int:
"""Soft delete thread by marking it as deleted."""
try:
thread = CommentThread.objects.get(pk=thread_id)
except ObjectDoesNotExist:
return 0
thread.is_deleted = True
thread.deleted_at = timezone.now()
thread.deleted_by = deleted_by
thread.save()
return 1

@staticmethod
def create_thread(data: dict[str, Any]) -> str:
"""Create thread."""
Expand Down Expand Up @@ -1911,14 +1942,14 @@ def update_thread(
@staticmethod
def get_user_thread_filter(course_id: str) -> dict[str, Any]:
"""Get user thread filter"""
return {"course_id": course_id}
return {"course_id": course_id, "is_deleted": False} # Exclude soft deleted threads

@staticmethod
def get_filtered_threads(
query: dict[str, Any], ids_only: bool = False
) -> list[dict[str, Any]]:
"""Return a list of threads that match the given filter."""
threads = CommentThread.objects.filter(**query)
threads = CommentThread.objects.filter(**query).filter(is_deleted=False) # Exclude soft deleted threads
if ids_only:
return [{"_id": str(thread.pk)} for thread in threads]
return [thread.to_dict() for thread in threads]
Expand Down Expand Up @@ -2158,8 +2189,11 @@ def get_contents(**kwargs: Any) -> list[dict[str, Any]]:
key: value for key, value in kwargs.items() if hasattr(CommentThread, key)
}

comments = Comment.objects.filter(**comment_filters)
threads = CommentThread.objects.filter(**thread_filters)
comments = Comment.objects.filter(**comment_filters).filter(
is_deleted=False, # Exclude soft deleted comments
comment_thread__is_deleted=False # Exclude comments on deleted threads
)
threads = CommentThread.objects.filter(**thread_filters).filter(is_deleted=False) # Exclude soft deleted threads

sort_key = kwargs.get("sort_key")
if sort_key:
Expand Down
Loading
Loading