-
Notifications
You must be signed in to change notification settings - Fork 0
feat: added soft delete functionality #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,4 +2,4 @@ | |
| Openedx forum app. | ||
| """ | ||
|
|
||
| __version__ = "0.3.9" | ||
| __version__ = "0.4.0" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -220,12 +220,16 @@ 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: | ||
|
|
@@ -244,14 +248,33 @@ 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"] | ||
| if parent_comment_id: | ||
| backend.update_stats_for_course(author_id, comment_course_id, replies=-1) | ||
|
|
||
| # soft_delete_comment returns (responses_deleted, replies_deleted) | ||
| responses_deleted, replies_deleted = backend.soft_delete_comment( | ||
| comment_id, deleted_by | ||
| ) | ||
|
|
||
| # Update stats based on what was actually deleted | ||
| if responses_deleted > 0: | ||
| # A response (parent comment) was deleted | ||
| backend.update_stats_for_course( | ||
| author_id, | ||
| comment_course_id, | ||
| responses=-responses_deleted, | ||
| deleted_responses=responses_deleted, | ||
| replies=-replies_deleted, | ||
| deleted_replies=replies_deleted, | ||
| ) | ||
| else: | ||
| backend.update_stats_for_course(author_id, comment_course_id, responses=-1) | ||
| # Only a reply was deleted (no response) | ||
| backend.update_stats_for_course( | ||
| author_id, | ||
| comment_course_id, | ||
| replies=-replies_deleted, | ||
| deleted_replies=replies_deleted, | ||
| ) | ||
| return data | ||
|
|
||
|
|
||
|
|
@@ -388,3 +411,64 @@ 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: Optional[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) | ||
|
|
||
|
Comment on lines
+435
to
+451
|
||
|
|
||
| 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 | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -159,12 +159,16 @@ 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. | ||
| """ | ||
|
|
@@ -177,7 +181,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) | ||
| count_of_response_deleted, count_of_replies_deleted = ( | ||
| backend.soft_delete_comments_of_a_thread(thread_id, deleted_by) | ||
| ) | ||
| thread = backend.validate_object("CommentThread", thread_id) | ||
|
|
||
| try: | ||
|
|
@@ -187,10 +193,17 @@ 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) | ||
| result = backend.soft_delete_thread(thread_id, deleted_by) | ||
| 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 | ||
|
|
@@ -393,6 +406,7 @@ def get_user_threads( | |
| "user_id": user_id, | ||
| "group_id": group_id, | ||
| "group_ids": group_ids, | ||
| "is_deleted": kwargs.get("is_deleted", False), | ||
| "context": kwargs.get("context"), | ||
| } | ||
| params = {k: v for k, v in params.items() if v is not None} | ||
|
|
@@ -420,3 +434,64 @@ 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: Optional[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) | ||
|
Comment on lines
+439
to
+455
|
||
|
|
||
|
|
||
| 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 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 | ||
| ) | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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)() | ||||||
|
|
@@ -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) | ||||||
|
|
||||||
|
|
@@ -320,7 +322,7 @@ def get_user_course_stats( | |||||
| """Get user course stats.""" | ||||||
| backend = get_backend(course_id)() | ||||||
| sort_criterion = backend.get_user_sort_criterion(sort_key) | ||||||
| exclude_from_stats = ["_id", "course_id"] | ||||||
| exclude_from_stats = ["_id", "course_id", "deleted_count"] | ||||||
|
||||||
| exclude_from_stats = ["_id", "course_id", "deleted_count"] | |
| exclude_from_stats = ["_id", "course_id"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The delete_comment function updates stats for all comments regardless of whether they are anonymous. However, in the restore_comment and delete_thread functions, stats are only updated when content is not anonymous (checking anonymous or anonymous_to_peers flags). This inconsistency means that deleting anonymous content will incorrectly update stats, but restoring it won't restore those stats, leading to stat drift. The delete_comment function should check if the comment is anonymous before updating stats.