132132 is_posting_allowed ,
133133 can_user_notify_all_learners , is_captcha_enabled , get_captcha_site_key_by_platform
134134)
135-
135+ from forum import api as forum_api
136+ import logging
137+ log = logging .getLogger (__name__ )
136138User = get_user_model ()
137139
138140ThreadType = Literal ["discussion" , "question" ]
@@ -916,6 +918,7 @@ def get_thread_list(
916918 order_direction : Literal ["desc" ] = "desc" ,
917919 requested_fields : Optional [List [Literal ["profile_image" ]]] = None ,
918920 count_flagged : bool = None ,
921+ show_deleted : bool = False ,
919922):
920923 """
921924 Return the list of all discussion threads pertaining to the given course
@@ -991,6 +994,8 @@ def get_thread_list(
991994
992995 if count_flagged and not context ["has_moderation_privilege" ]:
993996 raise PermissionDenied ("`count_flagged` can only be set by users with moderator access or higher." )
997+ if show_deleted and not context ["has_moderation_privilege" ]:
998+ raise PermissionDenied ("`show_deleted` can only be set by users with moderator access or higher." )
994999
9951000 group_id = None
9961001 allowed_roles = [
@@ -1023,6 +1028,7 @@ def get_thread_list(
10231028 "flagged" : flagged ,
10241029 "thread_type" : thread_type ,
10251030 "count_flagged" : count_flagged ,
1031+ "show_deleted" : show_deleted ,
10261032 }
10271033
10281034 if view :
@@ -1157,13 +1163,19 @@ def get_learner_active_thread_list(request, course_key, query_params):
11571163 group_id = query_params .get ('group_id' , None )
11581164 user_id = query_params .get ('user_id' , None )
11591165 count_flagged = query_params .get ('count_flagged' , None )
1166+ show_deleted = query_params .get ('show_deleted' , False )
1167+ if isinstance (show_deleted , str ):
1168+ show_deleted = show_deleted .lower () == 'true'
1169+
11601170 if user_id is None :
11611171 return Response ({'detail' : 'Invalid user id' }, status = status .HTTP_400_BAD_REQUEST )
11621172
11631173 if count_flagged and not context ["has_moderation_privilege" ]:
11641174 raise PermissionDenied ("count_flagged can only be set by users with moderation roles." )
11651175 if "flagged" in query_params .keys () and not context ["has_moderation_privilege" ]:
11661176 raise PermissionDenied ("Flagged filter is only available for moderators" )
1177+ if show_deleted and not context ["has_moderation_privilege" ]:
1178+ raise PermissionDenied ("show_deleted can only be set by users with moderation roles." )
11671179
11681180 if group_id is None :
11691181 comment_client_user = comment_client .User (id = user_id , course_id = course_key )
@@ -1173,14 +1185,34 @@ def get_learner_active_thread_list(request, course_key, query_params):
11731185 try :
11741186 threads , page , num_pages = comment_client_user .active_threads (query_params )
11751187 threads = set_attribute (threads , "pinned" , False )
1188+
1189+ # This portion below is temporary until we migrate to forum v2
1190+ filtered_threads = []
1191+ for thread in threads :
1192+ try :
1193+ forum_thread = forum_api .get_thread (thread .get ('id' ), course_id = str (course_key ))
1194+ is_deleted = forum_thread .get ('is_deleted' , False )
1195+
1196+ if show_deleted and is_deleted :
1197+ thread ['is_deleted' ] = True
1198+ thread ['deleted_at' ] = forum_thread .get ('deleted_at' )
1199+ thread ['deleted_by' ] = forum_thread .get ('deleted_by' )
1200+ filtered_threads .append (thread )
1201+ elif not show_deleted and not is_deleted :
1202+ filtered_threads .append (thread )
1203+ except Exception as e :
1204+ log .warning ("Failed to check thread %s deletion status: %s" , thread .get ('id' ), e )
1205+ if not show_deleted : # Fail safe: include thread for regular users
1206+ filtered_threads .append (thread )
1207+
11761208 results = _serialize_discussion_entities (
1177- request , context , threads , {'profile_image' }, DiscussionEntity .thread
1209+ request , context , filtered_threads , {'profile_image' }, DiscussionEntity .thread
11781210 )
11791211 paginator = DiscussionAPIPagination (
11801212 request ,
11811213 page ,
11821214 num_pages ,
1183- len (threads )
1215+ len (filtered_threads )
11841216 )
11851217 return paginator .get_paginated_response ({
11861218 "results" : results ,
@@ -1196,7 +1228,7 @@ def get_learner_active_thread_list(request, course_key, query_params):
11961228
11971229
11981230def get_comment_list (request , thread_id , endorsed , page , page_size , flagged = False , requested_fields = None ,
1199- merge_question_type_responses = False ):
1231+ merge_question_type_responses = False , show_deleted = False ):
12001232 """
12011233 Return the list of comments in the given thread.
12021234
@@ -1272,9 +1304,15 @@ def get_comment_list(request, thread_id, endorsed, page, page_size, flagged=Fals
12721304 raise PageNotFoundError ("Page not found (No results on this page)." )
12731305 num_pages = (resp_total + page_size - 1 ) // page_size if resp_total else 1
12741306
1307+ if not show_deleted :
1308+ responses = [response for response in responses if not response .get ('is_deleted' , False )]
1309+ else :
1310+ if not context ["has_moderation_privilege" ]:
1311+ raise PermissionDenied ("`show_deleted` can only be set by users with moderation roles." )
1312+
12751313 results = _serialize_discussion_entities (request , context , responses , requested_fields , DiscussionEntity .comment )
12761314
1277- paginator = DiscussionAPIPagination (request , page , num_pages , resp_total )
1315+ paginator = DiscussionAPIPagination (request , page , num_pages , len ( responses ) )
12781316 track_thread_viewed_event (request , context ["course" ], cc_thread , from_mfe_sidebar )
12791317 return paginator .get_paginated_response (results )
12801318
@@ -1700,6 +1738,7 @@ def get_response_comments(request, comment_id, page, page_size, requested_fields
17001738 try :
17011739 cc_comment = Comment (id = comment_id ).retrieve ()
17021740 reverse_order = request .GET .get ('reverse_order' , False )
1741+ show_deleted = request .GET .get ('show_deleted' , False )
17031742 cc_thread , context = _get_thread_and_context (
17041743 request ,
17051744 cc_comment ["thread_id" ],
@@ -1724,6 +1763,11 @@ def get_response_comments(request, comment_id, page, page_size, requested_fields
17241763 if not paged_response_comments and page != 1 :
17251764 raise PageNotFoundError ("Page not found (No results on this page)." )
17261765
1766+ if not show_deleted :
1767+ paged_response_comments = [response for response in paged_response_comments if not response .get ('is_deleted' , False )]
1768+ else :
1769+ if not context ["has_moderation_privilege" ]:
1770+ raise PermissionDenied ("`show_deleted` can only be set by users with moderation roles." )
17271771 results = _serialize_discussion_entities (
17281772 request , context , paged_response_comments , requested_fields , DiscussionEntity .comment
17291773 )
@@ -1822,7 +1866,7 @@ def delete_thread(request, thread_id):
18221866 """
18231867 cc_thread , context = _get_thread_and_context (request , thread_id )
18241868 if can_delete (cc_thread , context ):
1825- cc_thread .delete ()
1869+ cc_thread .delete (deleted_by = str ( request . user . id ) )
18261870 thread_deleted .send (sender = None , user = request .user , post = cc_thread )
18271871 track_thread_deleted_event (request , context ["course" ], cc_thread )
18281872 else :
@@ -1847,7 +1891,7 @@ def delete_comment(request, comment_id):
18471891 """
18481892 cc_comment , context = _get_comment_and_context (request , comment_id )
18491893 if can_delete (cc_comment , context ):
1850- cc_comment .delete ()
1894+ cc_comment .delete (deleted_by = str ( request . user . id ) )
18511895 comment_deleted .send (sender = None , user = request .user , post = cc_comment )
18521896 track_comment_deleted_event (request , context ["course" ], cc_comment )
18531897 else :
@@ -1999,3 +2043,134 @@ def add_stats_for_users_with_null_values(course_stats, users_in_course):
19992043 })
20002044 updated_course_stats = sorted (updated_course_stats , key = lambda d : len (d ['username' ]))
20012045 return updated_course_stats
2046+
2047+
2048+ def get_deleted_content_for_course (course_id , content_type = None , page = 1 , per_page = 20 , author_id = None ):
2049+ """
2050+ Retrieve all deleted content (threads, comments) for a course.
2051+
2052+ Args:
2053+ course_id (str): Course identifier
2054+ content_type (str, optional): Filter by 'thread' or 'comment'. If None, returns all types.
2055+ page (int): Page number for pagination (1-based)
2056+ per_page (int): Number of items per page
2057+ author_id (str, optional): Filter by author ID
2058+
2059+ Returns:
2060+ dict: Paginated results with deleted content
2061+ """
2062+
2063+ import math
2064+
2065+ try :
2066+ # Build query parameters for forum API
2067+ query_params = {
2068+ 'course_id' : course_id ,
2069+ 'is_deleted' : True , # Only get deleted content
2070+ 'page' : page ,
2071+ 'per_page' : per_page ,
2072+ }
2073+
2074+ if author_id :
2075+ query_params ['author_id' ] = author_id
2076+
2077+ deleted_content = []
2078+ total_count = 0
2079+
2080+ # Get deleted threads
2081+ if content_type is None or content_type == 'thread' :
2082+ try :
2083+ deleted_threads = forum_api .get_deleted_threads_for_course (
2084+ course_id = course_id ,
2085+ page = page if content_type == 'thread' else 1 ,
2086+ per_page = per_page if content_type == 'thread' else 1000 ,
2087+ author_id = author_id
2088+ )
2089+ for thread_data in deleted_threads .get ('threads' , []):
2090+ deleted_content .append ({
2091+ 'id' : str (thread_data .get ('_id' , thread_data .get ('id' ))),
2092+ 'type' : 'thread' ,
2093+ 'title' : thread_data .get ('title' , '' ),
2094+ 'body' : thread_data .get ('body' , '' ),
2095+ 'course_id' : course_id ,
2096+ 'author_id' : thread_data .get ('author_id' , '' ),
2097+ 'author_username' : thread_data .get ('author_username' , '' ),
2098+ 'commentable_id' : thread_data .get ('commentable_id' , '' ),
2099+ 'created_at' : thread_data .get ('created_at' ),
2100+ 'updated_at' : thread_data .get ('updated_at' ),
2101+ 'deleted_at' : thread_data .get ('deleted_at' ),
2102+ 'deleted_by' : thread_data .get ('deleted_by' ),
2103+ 'thread_type' : thread_data .get ('thread_type' , 'discussion' ),
2104+ 'anonymous' : thread_data .get ('anonymous' , False ),
2105+ 'anonymous_to_peers' : thread_data .get ('anonymous_to_peers' , False ),
2106+ })
2107+
2108+ if content_type == 'thread' :
2109+ total_count = deleted_threads .get ('total_count' , len (deleted_content ))
2110+ except Exception as e :
2111+ log .warning ("Failed to get deleted threads for course %s: %s" , course_id , e )
2112+
2113+ # Get deleted comments
2114+ if content_type is None or content_type == 'comment' :
2115+ try :
2116+ deleted_comments = forum_api .get_deleted_comments_for_course (
2117+ course_id = course_id ,
2118+ page = page if content_type == 'comment' else 1 ,
2119+ per_page = per_page if content_type == 'comment' else 1000 ,
2120+ author_id = author_id
2121+ )
2122+
2123+ for comment_data in deleted_comments .get ('comments' , []):
2124+ deleted_content .append ({
2125+ 'id' : str (comment_data .get ('_id' , comment_data .get ('id' ))),
2126+ 'type' : 'comment' ,
2127+ 'body' : comment_data .get ('body' , '' ),
2128+ 'course_id' : course_id ,
2129+ 'author_id' : comment_data .get ('author_id' , '' ),
2130+ 'author_username' : comment_data .get ('author_username' , '' ),
2131+ 'comment_thread_id' : str (comment_data .get ('comment_thread_id' , '' )),
2132+ 'parent_id' : str (comment_data .get ('parent_id' , '' )) if comment_data .get ('parent_id' ) else None ,
2133+ 'created_at' : comment_data .get ('created_at' ),
2134+ 'updated_at' : comment_data .get ('updated_at' ),
2135+ 'deleted_at' : comment_data .get ('deleted_at' ),
2136+ 'deleted_by' : comment_data .get ('deleted_by' ),
2137+ 'depth' : comment_data .get ('depth' , 0 ),
2138+ 'anonymous' : comment_data .get ('anonymous' , False ),
2139+ 'anonymous_to_peers' : comment_data .get ('anonymous_to_peers' , False ),
2140+ 'endorsed' : comment_data .get ('endorsed' , False ),
2141+ })
2142+
2143+ if content_type == 'comment' :
2144+ total_count = deleted_comments .get ('total_count' , len (deleted_content ))
2145+ except Exception as e :
2146+ log .warning ("Failed to get deleted comments for course %s: %s" , course_id , e )
2147+
2148+ # If getting all content types, handle pagination differently
2149+ if content_type is None :
2150+ total_count = len (deleted_content )
2151+ # Sort by deletion date (most recent first)
2152+ deleted_content .sort (key = lambda x : x .get ('deleted_at' , '' ), reverse = True )
2153+
2154+ # Apply pagination to combined results
2155+ start_idx = (page - 1 ) * per_page
2156+ end_idx = start_idx + per_page
2157+ deleted_content = deleted_content [start_idx :end_idx ]
2158+
2159+ # Calculate pagination info
2160+ num_pages = math .ceil (total_count / per_page ) if total_count > 0 else 1
2161+
2162+ return {
2163+ 'results' : deleted_content ,
2164+ 'pagination' : {
2165+ 'page' : page ,
2166+ 'per_page' : per_page ,
2167+ 'total_count' : total_count ,
2168+ 'num_pages' : num_pages ,
2169+ 'has_next' : page < num_pages ,
2170+ 'has_previous' : page > 1 ,
2171+ }
2172+ }
2173+
2174+ except Exception as e :
2175+ log .exception ("Error getting deleted content for course %s: %s" , course_id , e )
2176+ raise
0 commit comments