Skip to content

Conversation

@Alam-2U
Copy link
Collaborator

@Alam-2U Alam-2U commented Nov 3, 2025

Description

Implements soft delete functionality for discussion threads, responses, and comments using the is_deleted flag instead of permanently deleting records.


Changes Made

  • Added soft delete and restore logic for threads, responses, and comments
  • Implemented bulk delete and bulk restore APIs
  • Added support for handling nested responses and comment structures
  • Updated serializers and database queries to exclude deleted items by default
  • Added API support to fetch deleted items for moderator/admin views

JIRA Ticket


Related Tickets


Comment on lines 293 to 461
def bulk_soft_delete_threads(
thread_ids: list[str], user_id: str, course_id: Optional[str] = None
) -> dict[str, Any]:
"""
Bulk soft delete threads for the given thread_ids.
Parameters:
thread_ids: List of thread IDs to be soft deleted.
user_id: The ID of the user performing the soft delete.
course_id: The course ID for backend selection.
Response:
Dictionary with success count and any errors.
"""
backend = get_backend(course_id)()

# Validate all threads exist and are not already deleted
# Also collect author info for stats updates
valid_thread_ids = []
thread_authors = {} # {thread_id: author_id}
errors = []

for thread_id in thread_ids:
try:
thread = backend.validate_object("CommentThread", thread_id)
# Treat NULL/missing as False for legacy data
if thread.get("is_deleted", False):
errors.append(f"Thread {thread_id} is already deleted")
else:
valid_thread_ids.append(thread_id)
thread_authors[thread_id] = thread.get("author_id")
except ObjectDoesNotExist:
errors.append(f"Thread does not exist with Id: {thread_id}")

if not valid_thread_ids:
raise ForumV2RequestError("No valid threads to soft delete")

result = backend.bulk_soft_delete_threads(valid_thread_ids, user_id)

# Update stats for each author
for thread_id in valid_thread_ids:
author_id = thread_authors.get(thread_id)
if author_id and course_id:
try:
backend.update_stats_for_course(
author_id, course_id, threads=-1, deleted_count=1
)
except Exception as e:
log.warning(f"Failed to update stats for thread {thread_id}: {e}")

return {
"success_count": result,
"errors": errors,
"processed_threads": valid_thread_ids,
}


def bulk_restore_threads(
thread_ids: list[str], course_id: Optional[str] = None
) -> dict[str, Any]:
"""
Bulk restore soft deleted threads for the given thread_ids.
Parameters:
thread_ids: List of thread IDs to be restored.
course_id: The course ID for backend selection.
Response:
Dictionary with success count and any errors.
"""
backend = get_backend(course_id)()

# Validate all threads exist and are soft deleted
# Also collect author info for stats updates
valid_thread_ids = []
thread_authors = {} # {thread_id: author_id}
errors = []

for thread_id in thread_ids:
try:
thread = backend.validate_object("CommentThread", thread_id)
# Treat NULL/missing as False for legacy data
if not thread.get("is_deleted", False):
errors.append(f"Thread {thread_id} is not deleted")
else:
valid_thread_ids.append(thread_id)
thread_authors[thread_id] = thread.get("author_id")
except ObjectDoesNotExist:
errors.append(f"Thread does not exist with Id: {thread_id}")

if not valid_thread_ids:
raise ForumV2RequestError("No valid threads to restore")

result = backend.bulk_restore_threads(valid_thread_ids)

# Update stats for each author
for thread_id in valid_thread_ids:
author_id = thread_authors.get(thread_id)
if author_id and course_id:
try:
backend.update_stats_for_course(
author_id, course_id, threads=1, deleted_count=-1
)
except Exception as e:
log.warning(f"Failed to update stats for thread {thread_id}: {e}")

return {
"success_count": result,
"errors": errors,
"processed_threads": valid_thread_ids,
}


def get_deleted_threads(
course_id: str,
user_id: Optional[str] = None,
resp_skip: int = 0,
resp_limit: Optional[int] = None,
sort_key: Optional[str] = None,
) -> dict[str, Any]:
"""
Get soft deleted threads for a course.
Parameters:
course_id: The course ID.
user_id: Optional user ID to filter by author.
resp_skip: Number of threads to skip for pagination.
resp_limit: Maximum number of threads to return.
sort_key: Sort key for ordering results.
Response:
Dictionary with deleted threads and pagination info.
"""
backend = get_backend(course_id)()

filters = {"course_id": course_id}
if user_id:
filters["author_id"] = user_id

threads_cursor = backend.get_deleted_list(
resp_skip=resp_skip,
resp_limit=resp_limit,
sort=sort_key,
**filters
)

threads = []
for thread in threads_cursor:
try:
serialized_thread = prepare_thread_api_response(thread, backend)
threads.append(serialized_thread)
except ValidationError as error:
log.error(f"Validation error in get_deleted_threads for thread {thread.get('_id', 'unknown')}: {error}")
continue
except Exception as error:
log.error(f"Unexpected error in get_deleted_threads for thread {thread.get('_id', 'unknown')}: {error}")
continue

return {
"threads": threads,
"count": len(threads),
"skip": resp_skip,
"limit": resp_limit,
}


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be some duplication, can you compare and remove not used portion ?

Comment on lines 209 to 213
# Debug logging
print(f"[FORUM DEBUG] get_user_active_threads called")
print(f"[FORUM DEBUG] show_deleted={show_deleted} (type: {type(show_deleted)})")
print(f"[FORUM DEBUG] user_id={user_id}, course_id={course_id}")
print(f"[FORUM DEBUG] flagged={flagged}, unread={unread}, unanswered={unanswered}, unresponded={unresponded}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be removed.


params: dict[str, Any] = {
# Import backend check here to avoid circular imports
from forum.backend import is_mysql_backend_enabled
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linters will fail here, so you can add comment here to check

Comment on lines 288 to 292
print(f"[FORUM DEBUG] Using {backend.__class__.__name__} backend")
print(f"[FORUM DEBUG] show_deleted={show_deleted} -> {deleted_param_name}={deleted_param_value}")
print(f"[FORUM DEBUG] Calling handle_threads_query with thread_ids={len(active_thread_ids)}")
print(f"[FORUM DEBUG] Key params: {deleted_param_name}={params.get(deleted_param_name)}")

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be removed

Comment on lines 199 to 211
def save(self, *args: Any, **kwargs: Any) -> None:
"""Set author_username on creation if not already set."""
if not self.pk and not self.author_username:
# On creation, store the current username
if self.retired_username:
self.author_username = self.retired_username
elif self.author:
self.author_username = self.author.username
super().save(*args, **kwargs)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check why this is removed ?

else:
self.stdout.write(f'Successfully updated existing posts with soft delete fields')

client.close() No newline at end of file
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last line missing

@@ -1,0 +1 @@
# Management commands
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to check the base as these files already exist.

@@ -0,0 +1,90 @@
"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may not need a management command as there is no need for it now.

forum/urls.py Outdated
"threads/<str:thread_id>/soft-delete",
SoftDeleteThreadAPIView.as_view(),
name="soft-delete-thread-api",
),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need restore API

Copy link
Collaborator

@chintanjoshi-apphelix-2u chintanjoshi-apphelix-2u left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some changes

  • Check for duplication
  • Check for debug/print/ statements
  • Check for non-required comments and non-required code (i.e. management command)

@Alam-2U Alam-2U force-pushed the ealam/COSMO2-742 branch 2 times, most recently from 009fde4 to 2f9e19e Compare November 6, 2025 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants