|
22 | 22 | from django.urls import reverse |
23 | 23 | from django.utils.html import strip_tags |
24 | 24 | from edx_django_utils.monitoring import function_trace, set_custom_attribute |
| 25 | +from forum.backend import get_backend |
25 | 26 | from opaque_keys import InvalidKeyError |
26 | 27 | from opaque_keys.edx.locator import CourseKey |
27 | 28 | from pytz import UTC |
@@ -1671,7 +1672,7 @@ def get_comment_list( |
1671 | 1672 | discussion.rest_api.views.CommentViewSet for more detail. |
1672 | 1673 | """ |
1673 | 1674 | response_skip = page_size * (page - 1) |
1674 | | - reverse_order = request.GET.get("reverse_order", False) |
| 1675 | + reverse_order = request.GET.get("reverse_order", "").lower() in ("true", "1") |
1675 | 1676 | from_mfe_sidebar = request.GET.get("enable_in_context_sidebar", False) |
1676 | 1677 | cc_thread, context = _get_thread_and_context( |
1677 | 1678 | request, |
@@ -2292,74 +2293,107 @@ def get_response_comments(request, comment_id, page, page_size, requested_fields |
2292 | 2293 | """ |
2293 | 2294 | try: |
2294 | 2295 | cc_comment = Comment(id=comment_id).retrieve() |
2295 | | - reverse_order = request.GET.get("reverse_order", False) |
| 2296 | + reverse_order = request.GET.get("reverse_order", "").lower() in ("true", "1") |
2296 | 2297 | show_deleted = request.GET.get("show_deleted", False) |
2297 | 2298 | show_deleted = show_deleted in ["true", "True", True] |
2298 | 2299 |
|
| 2300 | + # Only fetch thread for context/permissions — NOT the full response tree |
2299 | 2301 | cc_thread, context = _get_thread_and_context( |
2300 | 2302 | request, |
2301 | 2303 | cc_comment["thread_id"], |
2302 | 2304 | retrieve_kwargs={ |
2303 | | - "with_responses": True, |
2304 | | - "recursive": True, |
2305 | | - "reverse_order": reverse_order, |
2306 | | - "show_deleted": show_deleted, |
| 2305 | + "with_responses": False, |
| 2306 | + "recursive": False, |
2307 | 2307 | }, |
2308 | 2308 | ) |
2309 | | - if cc_thread["thread_type"] == "question": |
2310 | | - thread_responses = itertools.chain( |
2311 | | - cc_thread["endorsed_responses"], cc_thread["non_endorsed_responses"] |
| 2309 | + |
| 2310 | + if show_deleted and not context["has_moderation_privilege"]: |
| 2311 | + raise PermissionDenied( |
| 2312 | + "`show_deleted` can only be set by users with moderation roles." |
2312 | 2313 | ) |
2313 | | - else: |
2314 | | - thread_responses = cc_thread["children"] |
2315 | | - response_comments = [] |
2316 | | - for response in thread_responses: |
2317 | | - if response["id"] == comment_id: |
2318 | | - response_comments = response["children"] |
2319 | | - break |
2320 | | - |
2321 | | - # Filter deleted content from the FULL list first |
| 2314 | + |
| 2315 | + # Determine sort order |
| 2316 | + sorting_order = -1 if reverse_order else 1 |
| 2317 | + |
| 2318 | + # Get the course_id from the thread for backend initialization |
| 2319 | + course_id = cc_thread["course_id"] |
| 2320 | + backend = get_backend(course_id)() |
| 2321 | + |
| 2322 | + # Fetch ONLY this comment's children directly from the backend |
| 2323 | + # This avoids loading the entire thread tree (the main performance fix) |
| 2324 | + response_comments = backend.get_comments( |
| 2325 | + parent_id=str(comment_id), |
| 2326 | + depth=1, |
| 2327 | + sort=sorting_order, |
| 2328 | + ) |
| 2329 | + |
| 2330 | + # Filter deleted content |
2322 | 2331 | if not show_deleted: |
2323 | 2332 | response_comments = [ |
2324 | | - response |
2325 | | - for response in response_comments |
2326 | | - if not response.get("is_deleted", False) |
| 2333 | + comment for comment in response_comments |
| 2334 | + if not comment.get("is_deleted", False) |
2327 | 2335 | ] |
2328 | | - else: |
2329 | | - if not context["has_moderation_privilege"]: |
2330 | | - raise PermissionDenied( |
2331 | | - "`show_deleted` can only be set by users with moderation roles." |
2332 | | - ) |
2333 | 2336 |
|
2334 | | - # Filter muted content from the FULL list |
2335 | | - include_muted = request.GET.get("include_muted", False) |
2336 | | - include_muted = include_muted in ["true", "True", True] |
2337 | | - if not include_muted: |
| 2337 | + # Filter muted content |
| 2338 | + include_muted_param = request.GET.get("include_muted", False) |
| 2339 | + include_muted_param = include_muted_param in ["true", "True", True] |
| 2340 | + if not include_muted_param: |
2338 | 2341 | response_comments = filter_muted_content( |
2339 | 2342 | request.user, |
2340 | 2343 | context["course"].id, |
2341 | 2344 | response_comments |
2342 | 2345 | ) |
2343 | 2346 |
|
2344 | | - # NOW calculate pagination based on FILTERED total |
| 2347 | + # Calculate pagination based on FILTERED total |
2345 | 2348 | total_comments_count = len(response_comments) |
2346 | 2349 | num_pages = ( |
2347 | 2350 | (total_comments_count + page_size - 1) // page_size |
2348 | 2351 | if total_comments_count else 1 |
2349 | 2352 | ) |
2350 | 2353 |
|
2351 | | - # Then paginate the filtered list |
| 2354 | + # Paginate the filtered list |
2352 | 2355 | response_skip = page_size * (page - 1) |
2353 | 2356 | paged_response_comments = response_comments[ |
2354 | 2357 | response_skip: (response_skip + page_size) |
2355 | 2358 | ] |
| 2359 | + |
2356 | 2360 | if not paged_response_comments and page != 1: |
2357 | 2361 | raise PageNotFoundError("Page not found (No results on this page).") |
2358 | 2362 |
|
| 2363 | + # Normalize comment data for serialization (backend returns raw dict format) |
| 2364 | + normalized_comments = [] |
| 2365 | + for comment in paged_response_comments: |
| 2366 | + normalized = { |
| 2367 | + "id": str(comment.get("_id", comment.get("id", ""))), |
| 2368 | + "body": comment.get("body", ""), |
| 2369 | + "course_id": comment.get("course_id", course_id), |
| 2370 | + "user_id": comment.get("author_id", comment.get("user_id")), |
| 2371 | + "username": comment.get("author_username", comment.get("username", "")), |
| 2372 | + "thread_id": str(comment.get("comment_thread_id", comment.get("thread_id", ""))), |
| 2373 | + "parent_id": str(comment.get("parent_id", "")) if comment.get("parent_id") else None, |
| 2374 | + "created_at": comment.get("created_at"), |
| 2375 | + "updated_at": comment.get("updated_at"), |
| 2376 | + "depth": comment.get("depth", 1), |
| 2377 | + "type": "comment", |
| 2378 | + "anonymous": comment.get("anonymous", False), |
| 2379 | + "anonymous_to_peers": comment.get("anonymous_to_peers", False), |
| 2380 | + "endorsed": comment.get("endorsed", False), |
| 2381 | + "abuse_flaggers": comment.get("abuse_flaggers", []), |
| 2382 | + "votes": comment.get("votes", {"up_count": 0}), |
| 2383 | + "child_count": comment.get("child_count", 0), |
| 2384 | + "children": [], |
| 2385 | + "closed": comment.get("closed", False), |
| 2386 | + } |
| 2387 | + # Carry over any additional fields |
| 2388 | + for key in ("endorsement", "edit_history", "is_deleted", "deleted_at", "deleted_by"): |
| 2389 | + if key in comment: |
| 2390 | + normalized[key] = comment[key] |
| 2391 | + normalized_comments.append(normalized) |
| 2392 | + |
2359 | 2393 | results = _serialize_discussion_entities( |
2360 | 2394 | request, |
2361 | 2395 | context, |
2362 | | - paged_response_comments, |
| 2396 | + normalized_comments, |
2363 | 2397 | requested_fields, |
2364 | 2398 | DiscussionEntity.comment, |
2365 | 2399 | ) |
|
0 commit comments