-
Notifications
You must be signed in to change notification settings - Fork 8
feat: implement discussion mute/unmute feature with user and staff-level controls #54
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
base: release-ulmo
Are you sure you want to change the base?
Changes from all commits
377cf4b
cac5960
b2a3368
a2e8808
5d17da2
870f0aa
0155359
80d74f6
e67422f
7db8289
ebf3a78
2435b28
4bc3e14
80d6b75
6db4908
7c4d163
cedfd1b
3bc97f4
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 | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -150,6 +150,64 @@ | |||||||||||
| log = logging.getLogger(__name__) | ||||||||||||
| User = get_user_model() | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def filter_muted_content(request_user, course_key, content_list): | ||||||||||||
| """ | ||||||||||||
| Filter out content from muted users. | ||||||||||||
|
|
||||||||||||
| Args: | ||||||||||||
| request_user: The user making the request | ||||||||||||
| course_key: The course key | ||||||||||||
| content_list: List of thread or comment objects | ||||||||||||
|
|
||||||||||||
| Returns: | ||||||||||||
| list: Filtered list with muted users' content removed | ||||||||||||
| """ | ||||||||||||
|
|
||||||||||||
| if not request_user.is_authenticated: | ||||||||||||
| return content_list | ||||||||||||
|
|
||||||||||||
| # Get muted user IDs directly from forum_api for maximum efficiency | ||||||||||||
| try: | ||||||||||||
| all_mutes = forum_api.get_all_muted_users_for_course( | ||||||||||||
| course_id=str(course_key), | ||||||||||||
| requester_id=str(request_user.id), | ||||||||||||
| scope="all", | ||||||||||||
| requester_is_privileged=True | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| # Build muted user IDs set with optimized comprehension | ||||||||||||
| muted_user_ids = { | ||||||||||||
| int(str(user['muted_user_id'])) | ||||||||||||
| for user in all_mutes.get('muted_users', []) | ||||||||||||
| if ( | ||||||||||||
| user.get('muted_user_id') and | ||||||||||||
| str(user.get('muted_user_id')).isdigit() and | ||||||||||||
| ( | ||||||||||||
| user.get('scope') == 'course' or | ||||||||||||
| (user.get('scope') == 'personal' and | ||||||||||||
| str(user.get('muter_id')) == str(request_user.id)) | ||||||||||||
| ) | ||||||||||||
| ) | ||||||||||||
| } - {request_user.id} # Exclude self-muting | ||||||||||||
| except Exception: # pylint: disable=broad-except | ||||||||||||
| log.exception("Error getting muted user IDs") | ||||||||||||
| return content_list | ||||||||||||
|
|
||||||||||||
| if not muted_user_ids: | ||||||||||||
| return content_list | ||||||||||||
|
|
||||||||||||
| # Filter content with optimized comprehension | ||||||||||||
| return [ | ||||||||||||
| item for item in content_list | ||||||||||||
| if ( | ||||||||||||
| not item.get("user_id") or | ||||||||||||
| not str(item.get("user_id")).isdigit() or | ||||||||||||
| int(str(item["user_id"])) == request_user.id or | ||||||||||||
| int(str(item["user_id"])) not in muted_user_ids | ||||||||||||
| ) | ||||||||||||
| ] | ||||||||||||
|
|
||||||||||||
| ThreadType = Literal["discussion", "question"] | ||||||||||||
| ViewType = Literal["unread", "unanswered"] | ||||||||||||
| ThreadOrderingType = Literal["last_activity_at", "comment_count", "vote_count"] | ||||||||||||
|
|
@@ -993,7 +1051,7 @@ def _serialize_discussion_entities( | |||||||||||
| return results | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def get_thread_list( | ||||||||||||
| def get_thread_list( # pylint: disable=too-many-statements | ||||||||||||
| request: Request, | ||||||||||||
| course_key: CourseKey, | ||||||||||||
| page: int, | ||||||||||||
|
|
@@ -1009,6 +1067,7 @@ def get_thread_list( | |||||||||||
| order_direction: Literal["desc"] = "desc", | ||||||||||||
| requested_fields: Optional[List[Literal["profile_image"]]] = None, | ||||||||||||
| count_flagged: bool = None, | ||||||||||||
| include_muted: bool = None, | ||||||||||||
| show_deleted: bool = False, | ||||||||||||
| ): | ||||||||||||
| """ | ||||||||||||
|
|
@@ -1164,10 +1223,22 @@ def get_thread_list( | |||||||||||
| if paginated_results.page != page: | ||||||||||||
| raise PageNotFoundError("Page not found (No results on this page).") | ||||||||||||
|
|
||||||||||||
| # Always filter muted content for All Posts tab (unless include_muted is explicitly True) | ||||||||||||
| if include_muted: | ||||||||||||
| # Only the muted section should set include_muted True | ||||||||||||
| filtered_threads = paginated_results.collection | ||||||||||||
| else: | ||||||||||||
naincy128 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
| # Always filter out muted content for All Posts, even after restoration | ||||||||||||
| filtered_threads = filter_muted_content( | ||||||||||||
| request.user, | ||||||||||||
| course_key, | ||||||||||||
| paginated_results.collection | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
naincy128 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
| results = _serialize_discussion_entities( | ||||||||||||
| request, | ||||||||||||
| context, | ||||||||||||
| paginated_results.collection, | ||||||||||||
| filtered_threads, | ||||||||||||
| requested_fields, | ||||||||||||
| DiscussionEntity.thread, | ||||||||||||
| ) | ||||||||||||
|
Comment on lines
1223
to
1244
|
||||||||||||
|
|
@@ -1308,13 +1379,24 @@ def get_learner_active_thread_list(request, course_key, query_params): | |||||||||||
| id=user_id, course_id=course_key, group_id=group_id | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| include_muted = query_params.pop('include_muted', False) | ||||||||||||
|
|
||||||||||||
| try: | ||||||||||||
| threads, page, num_pages = comment_client_user.active_threads(query_params) | ||||||||||||
| threads = set_attribute(threads, "pinned", False) | ||||||||||||
|
|
||||||||||||
| if include_muted: | ||||||||||||
| filtered_threads = threads | ||||||||||||
| else: | ||||||||||||
| filtered_threads = filter_muted_content( | ||||||||||||
| request.user, | ||||||||||||
| course_key, | ||||||||||||
| threads | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| # This portion below is temporary until we migrate to forum v2 | ||||||||||||
| filtered_threads = [] | ||||||||||||
| for thread in threads: | ||||||||||||
| filtered_threads_with_deletion_status = [] | ||||||||||||
| for thread in filtered_threads: | ||||||||||||
naincy128 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
| try: | ||||||||||||
| forum_thread = forum_api.get_thread( | ||||||||||||
| thread.get("id"), course_id=str(course_key) | ||||||||||||
|
|
@@ -1325,25 +1407,25 @@ def get_learner_active_thread_list(request, course_key, query_params): | |||||||||||
| thread["is_deleted"] = True | ||||||||||||
| thread["deleted_at"] = forum_thread.get("deleted_at") | ||||||||||||
| thread["deleted_by"] = forum_thread.get("deleted_by") | ||||||||||||
| filtered_threads.append(thread) | ||||||||||||
| filtered_threads_with_deletion_status.append(thread) | ||||||||||||
| elif not show_deleted and not is_deleted: | ||||||||||||
| filtered_threads.append(thread) | ||||||||||||
| filtered_threads_with_deletion_status.append(thread) | ||||||||||||
| except Exception as e: # pylint: disable=broad-exception-caught | ||||||||||||
| log.warning( | ||||||||||||
| "Failed to check thread %s deletion status: %s", thread.get("id"), e | ||||||||||||
| ) | ||||||||||||
| if not show_deleted: # Fail safe: include thread for regular users | ||||||||||||
| filtered_threads.append(thread) | ||||||||||||
| filtered_threads_with_deletion_status.append(thread) | ||||||||||||
|
|
||||||||||||
| results = _serialize_discussion_entities( | ||||||||||||
| request, | ||||||||||||
| context, | ||||||||||||
| filtered_threads, | ||||||||||||
| filtered_threads_with_deletion_status, | ||||||||||||
| {"profile_image"}, | ||||||||||||
| DiscussionEntity.thread, | ||||||||||||
| ) | ||||||||||||
| paginator = DiscussionAPIPagination( | ||||||||||||
| request, page, num_pages, len(filtered_threads) | ||||||||||||
| request, page, num_pages, len(filtered_threads_with_deletion_status) | ||||||||||||
|
||||||||||||
| request, page, num_pages, len(filtered_threads_with_deletion_status) | |
| request, | |
| page_num=page, | |
| num_pages=None, | |
| count=len(filtered_threads_with_deletion_status), |
naincy128 marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Feb 11, 2026
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 pagination count uses len(filtered_responses) which is correct. However, similar to other locations, the num_pages from the comment service doesn't account for muted content filtering, which may cause pagination issues when users with many responses are muted.
Uh oh!
There was an error while loading. Please reload this page.