Skip to content
10 changes: 10 additions & 0 deletions forum/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
create_parent_comment,
delete_comment,
get_course_id_by_comment,
get_deleted_comments_for_course,
get_parent_comment,
get_user_comments,
get_user_deleted_comment_count,
restore_comment,
restore_user_deleted_comments,
update_comment,
)
from .flags import (
Expand All @@ -28,8 +32,12 @@
create_thread,
delete_thread,
get_course_id_by_thread,
get_deleted_threads_for_course,
get_thread,
get_user_deleted_threads_count,
get_user_threads,
restore_thread,
restore_user_deleted_threads,
update_thread,
)
from .users import (
Expand Down Expand Up @@ -73,6 +81,8 @@
"get_user_course_stats",
"get_user_subscriptions",
"get_user_threads",
"get_deleted_comments_for_course",
"get_deleted_threads_for_course",
"mark_thread_as_read",
"pin_thread",
"retire_user",
Expand Down
91 changes: 87 additions & 4 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,14 +246,24 @@ def delete_comment(comment_id: str, course_id: Optional[str] = None) -> dict[str
backend,
exclude_fields=["endorsement", "sk"],
)
backend.delete_comment(comment_id)
author_id = comment["author_id"]
comment_course_id = comment["course_id"]
parent_comment_id = data["parent_id"]
backend.soft_delete_comment(comment_id, deleted_by)
if parent_comment_id:
backend.update_stats_for_course(author_id, comment_course_id, replies=-1)
backend.update_stats_for_course(author_id, comment_course_id, replies=-1, deleted_replies=1)
else:
backend.update_stats_for_course(author_id, comment_course_id, responses=-1)
child_count = comment.get("child_count", 0)
if child_count > 0:
for child in backend.get_comments(parent_id=comment_id):
if child.get('is_deleted', False):
child_count -= 1
backend.update_stats_for_course(
author_id, comment_course_id,
responses=-1, deleted_responses=1,
replies=-child_count, deleted_replies=child_count
)
# backend.delete_comment(comment_id)
return data


Expand Down Expand Up @@ -388,3 +400,74 @@ def get_user_comments(
"num_pages": num_pages,
"page": page,
}


def get_deleted_comments_for_course(course_id: str, page: int = 1, per_page: int = 20, author_id: str = None) -> dict[str, Any]:
"""
Get deleted comments for a specific course.

Args:
course_id (str): The course identifier
page (int): Page number for pagination (default: 1)
per_page (int): Number of comments per page (default: 20)
author_id (str, optional): Filter by author ID

Returns:
dict: Dictionary containing deleted comments and pagination info
"""
backend = get_backend(course_id)()
return backend.get_deleted_comments_for_course(course_id, page, per_page, author_id)


def restore_comment(comment_id: str, course_id: Optional[str] = None, restored_by: Optional[str] = None) -> bool:
"""
Restore a soft-deleted comment.

Args:
comment_id (str): The ID of the comment to restore
course_id (str, optional): The course ID for backend selection
restored_by (str, optional): The ID of the user performing the restoration

Returns:
bool: True if comment was restored, False if not found
"""
backend = get_backend(course_id)()
return backend.restore_comment(comment_id, restored_by=restored_by)


def get_user_deleted_comment_count(user_id: str, course_ids: list[str], course_id: Optional[str] = None) -> int:
"""
Get count of deleted comments for a user across courses.

Args:
user_id (str): The ID of the user
course_ids (list): List of course IDs to search in
course_id (str, optional): Course ID for backend selection (uses first from list if not provided)

Returns:
int: Count of deleted comments
"""
backend = get_backend(course_id or course_ids[0])()
return backend.get_user_deleted_comment_count(user_id, course_ids)


def restore_user_deleted_comments(
user_id: str,
course_ids: list[str],
course_id: Optional[str] = None,
restored_by: Optional[str] = None
) -> int:
"""
Restore all deleted comments for a user across courses.

Args:
user_id (str): The ID of the user whose comments to restore
course_ids (list): List of course IDs to restore comments in
course_id (str, optional): Course ID for backend selection (uses first from list if not provided)
restored_by (str, optional): The ID of the user performing the restoration

Returns:
int: Number of comments restored
"""
backend = get_backend(course_id or course_ids[0])()
return backend.restore_user_deleted_comments(user_id, course_ids, restored_by=restored_by)
2 changes: 2 additions & 0 deletions forum/api/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def search_threads(
page: int = FORUM_DEFAULT_PAGE,
per_page: int = FORUM_DEFAULT_PER_PAGE,
is_moderator: bool = False,
is_deleted: bool = False,
) -> dict[str, Any]:
"""
Search for threads based on the provided data.
Expand Down Expand Up @@ -107,6 +108,7 @@ def search_threads(
raw_query=False,
commentable_ids=commentable_ids,
is_moderator=is_moderator,
is_deleted=is_deleted,
)

if collections := data.get("collection"):
Expand Down
86 changes: 82 additions & 4 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
count_of_response_deleted, count_of_replies_deleted = 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,10 +191,12 @@ 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
thread["author_id"], thread["course_id"], threads=-1, responses=-count_of_response_deleted, replies=-count_of_replies_deleted, deleted_threads=1, deleted_responses=count_of_response_deleted, deleted_replies=count_of_replies_deleted
)

return serialized_data
Expand Down Expand Up @@ -393,6 +399,7 @@ def get_user_threads(
"user_id": user_id,
"group_id": group_id,
"group_ids": group_ids,
"is_deleted": kwargs.get("is_deleted", False),
}
params = {k: v for k, v in params.items() if v is not None}
backend.validate_params(params)
Expand All @@ -419,3 +426,74 @@ def get_course_id_by_thread(thread_id: str) -> str | None:
or MySQLBackend.get_course_id_by_thread_id(thread_id)
or None
)


def get_deleted_threads_for_course(course_id: str, page: int = 1, per_page: int = 20, author_id: str = None) -> dict[str, Any]:
"""
Get deleted threads for a specific course.

Args:
course_id (str): The course identifier
page (int): Page number for pagination (default: 1)
per_page (int): Number of threads per page (default: 20)
author_id (str, optional): Filter by author ID

Returns:
dict: Dictionary containing deleted threads and pagination info
"""
backend = get_backend(course_id)()
return backend.get_deleted_threads_for_course(course_id, page, per_page, author_id)


def restore_thread(thread_id: str, course_id: Optional[str] = None, restored_by: Optional[str] = None) -> bool:
"""
Restore a soft-deleted thread.

Args:
thread_id (str): The ID of the thread to restore
course_id (str, optional): The course ID for backend selection
restored_by (str, optional): The ID of the user performing the restoration

Returns:
bool: True if thread was restored, False if not found
"""
backend = get_backend(course_id)()
return backend.restore_thread(thread_id, restored_by=restored_by)


def get_user_deleted_threads_count(user_id: str, course_ids: list[str], course_id: Optional[str] = None) -> int:
"""
Get count of deleted threads for a user across courses.

Args:
user_id (str): The ID of the user
course_ids (list): List of course IDs to search in
course_id (str, optional): Course ID for backend selection (uses first from list if not provided)

Returns:
int: Count of deleted threads
"""
backend = get_backend(course_id or course_ids[0])()
return backend.get_user_deleted_threads_count(user_id, course_ids)


def restore_user_deleted_threads(
user_id: str,
course_ids: list[str],
course_id: Optional[str] = None,
restored_by: Optional[str] = None
) -> int:
"""
Restore all deleted threads for a user across courses.

Args:
user_id (str): The ID of the user whose threads to restore
course_ids (list): List of course IDs to restore threads in
course_id (str, optional): Course ID for backend selection (uses first from list if not provided)
restored_by (str, optional): The ID of the user performing the restoration

Returns:
int: Number of threads restored
"""
backend = get_backend(course_id or course_ids[0])()
return backend.restore_user_deleted_threads(user_id, course_ids, restored_by=restored_by)
2 changes: 2 additions & 0 deletions forum/api/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ def get_user_active_threads(
per_page: Optional[int] = FORUM_DEFAULT_PER_PAGE,
group_id: Optional[str] = None,
is_moderator: Optional[bool] = False,
show_deleted: Optional[bool] = False,
) -> dict[str, Any]:
"""Get user active threads."""
backend = get_backend(course_id)()
Expand Down Expand Up @@ -251,6 +252,7 @@ def get_user_active_threads(
"context": "course",
"raw_query": raw_query,
"is_moderator": is_moderator,
"is_deleted": show_deleted,
}
data = backend.handle_threads_query(**params)

Expand Down
14 changes: 14 additions & 0 deletions forum/backends/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,3 +476,17 @@ def get_user_contents_by_username(username: str) -> list[dict[str, Any]]:
Retrieve all threads and comments authored by a specific user.
"""
raise NotImplementedError

@staticmethod
def get_deleted_threads_for_course(course_id: str, page: int = 1, per_page: int = 20, author_id: str = None) -> dict[str, Any]:
"""
Get deleted threads for a specific course.
"""
raise NotImplementedError

@staticmethod
def get_deleted_comments_for_course(course_id: str, page: int = 1, per_page: int = 20, author_id: str = None) -> dict[str, Any]:
"""
Get deleted comments for a specific course.
"""
raise NotImplementedError
Loading