Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
377cf4b
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Dec 1, 2025
cac5960
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Jan 25, 2026
b2a3368
Merge branch 'release-ulmo' into COSMO2-743
naincy128 Jan 25, 2026
a2e8808
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Jan 27, 2026
5d17da2
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Feb 3, 2026
870f0aa
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Feb 5, 2026
0155359
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Feb 5, 2026
80d74f6
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Feb 6, 2026
e67422f
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Feb 9, 2026
7db8289
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Feb 9, 2026
ebf3a78
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Feb 10, 2026
2435b28
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Feb 10, 2026
4bc3e14
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Feb 10, 2026
80d6b75
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Feb 10, 2026
6db4908
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Feb 11, 2026
7c4d163
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Feb 11, 2026
cedfd1b
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Feb 11, 2026
3bc97f4
feat: implement discussion mute/unmute feature with user and staff-le…
naincy128 Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 150 additions & 12 deletions lms/djangoapps/discussion/rest_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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,
Expand All @@ -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,
):
"""
Expand Down Expand Up @@ -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:
# Always filter out muted content for All Posts, even after restoration
filtered_threads = filter_muted_content(
request.user,
course_key,
paginated_results.collection
)

results = _serialize_discussion_entities(
request,
context,
paginated_results.collection,
filtered_threads,
requested_fields,
DiscussionEntity.thread,
)
Comment on lines 1223 to 1244
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

The pagination count uses paginated_results.thread_count which includes muted threads, but the actual results have been filtered to exclude muted content. This creates a mismatch where the pagination metadata (total count, page count) doesn't match the actual number of items returned, leading to incorrect pagination behavior and potentially confusing users. The paginator should use len(filtered_threads) instead to reflect the actual filtered count.

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -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:
try:
forum_thread = forum_api.get_thread(
thread.get("id"), course_id=str(course_key)
Expand All @@ -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)
Copy link

Copilot AI Feb 11, 2026

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_threads_with_deletion_status) which is correct, but this pagination is created after applying mute filtering. However, the original num_pages from the comment service doesn't account for the filtered count, which may cause pagination issues when many users are muted.

Suggested change
request, page, num_pages, len(filtered_threads_with_deletion_status)
request,
page_num=page,
num_pages=None,
count=len(filtered_threads_with_deletion_status),

Copilot uses AI. Check for mistakes.
)
return paginator.get_paginated_response(
{
Expand Down Expand Up @@ -1371,6 +1453,7 @@ def get_comment_list(
flagged=False,
requested_fields=None,
merge_question_type_responses=False,
include_muted=False,
show_deleted=False,
):
"""
Expand Down Expand Up @@ -1466,11 +1549,22 @@ def get_comment_list(
"`show_deleted` can only be set by users with moderation roles."
)

# Always filter muted content for All Posts tab
if include_muted:
filtered_responses = responses
else:
# Always filter out muted content for All Posts, even after restoration
filtered_responses = filter_muted_content(
request.user,
context["course"].id,
responses
)

results = _serialize_discussion_entities(
request, context, responses, requested_fields, DiscussionEntity.comment
request, context, filtered_responses, requested_fields, DiscussionEntity.comment
)

paginator = DiscussionAPIPagination(request, page, num_pages, len(responses))
paginator = DiscussionAPIPagination(request, page, num_pages, len(filtered_responses))
track_thread_viewed_event(request, context["course"], cc_thread, from_mfe_sidebar)
return paginator.get_paginated_response(results)

Comment on lines +1567 to 1570
Copy link

Copilot AI Feb 11, 2026

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.

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -1939,7 +2033,7 @@ def get_thread(request, thread_id, requested_fields=None, course_id=None):
)[0]


def get_response_comments(request, comment_id, page, page_size, requested_fields=None):
def get_response_comments(request, comment_id, page, page_size, requested_fields=None, include_muted=False):
"""
Return the list of comments for the given thread response.

Expand Down Expand Up @@ -2008,6 +2102,15 @@ def get_response_comments(request, comment_id, page, page_size, requested_fields
raise PermissionDenied(
"`show_deleted` can only be set by users with moderation roles."
)

# Apply muting filter if not including muted content
if not include_muted:
paged_response_comments = filter_muted_content(
request.user,
context["course"].id,
paged_response_comments
)

results = _serialize_discussion_entities(
request,
context,
Expand Down Expand Up @@ -2224,6 +2327,41 @@ def get_course_discussion_user_stats(
)
course_stats_response["user_stats"] = updated_course_stats

# Course-wide muted users should only be visible to staff and privileged users
if not is_privileged:
try:
course_mutes = forum_api.get_all_muted_users_for_course(
course_id=str(course_key),
requester_id=None,
scope="course",
requester_is_privileged=True,
)

# Get course-wide muted user IDs and convert to usernames in one operation
course_wide_muted_user_ids = {
int(user.get('muted_user_id'))
for user in course_mutes.get('muted_users', [])
if user.get('muted_user_id') is not None
}

if course_wide_muted_user_ids:
# Get usernames for muted users and filter user stats
course_wide_muted_usernames = set(
User.objects.filter(id__in=course_wide_muted_user_ids)
.values_list('username', flat=True)
)

# Filter out course-wide muted users from stats, but allow muted users to see themselves
requester_username = request.user.username
course_stats_response["user_stats"] = [
user_stat for user_stat in course_stats_response["user_stats"]
if (user_stat.get("username") not in course_wide_muted_usernames or
user_stat.get("username") == requester_username)
]

except Exception as e: # pylint: disable=broad-except
log.warning(f"Failed to filter course-wide muted users: {e}")

serializer = UserStatsSerializer(
course_stats_response["user_stats"],
context={"is_privileged": is_privileged},
Expand Down
3 changes: 3 additions & 0 deletions lms/djangoapps/discussion/rest_api/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class ThreadListGetForm(_PaginationForm):
)
count_flagged = ExtendedNullBooleanField(required=False)
flagged = ExtendedNullBooleanField(required=False)
include_muted = ExtendedNullBooleanField(required=False)
show_deleted = ExtendedNullBooleanField(required=False)
view = ChoiceField(
choices=[
Expand Down Expand Up @@ -144,6 +145,7 @@ class CommentListGetForm(_PaginationForm):
endorsed = ExtendedNullBooleanField(required=False)
requested_fields = MultiValueField(required=False)
merge_question_type_responses = BooleanField(required=False)
include_muted = BooleanField(required=False)
show_deleted = ExtendedNullBooleanField(required=False)


Expand Down Expand Up @@ -181,6 +183,7 @@ class CommentGetForm(_PaginationForm):
"""

requested_fields = MultiValueField(required=False)
include_muted = BooleanField(required=False)


class CourseDiscussionSettingsForm(Form):
Expand Down
Loading
Loading