From ac473472bbd2ecfa5c17568e594a1d10da59d68c Mon Sep 17 00:00:00 2001 From: "m.boghani" Date: Wed, 18 Feb 2026 17:12:50 -0500 Subject: [PATCH 1/4] fix(course_home_api): avoid anonymous completion lookup in navigation outline. Return empty completions_dict for AnonymousUser in CourseNavigationBlocksView to prevent 500 on /api/course_home/v1/navigation/{course_key} for public anonymous access. Keeps existing outline access filtering unchanged. --- lms/djangoapps/course_home_api/outline/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lms/djangoapps/course_home_api/outline/views.py b/lms/djangoapps/course_home_api/outline/views.py index 0c62d8a34921..ba7bf7d0ca94 100644 --- a/lms/djangoapps/course_home_api/outline/views.py +++ b/lms/djangoapps/course_home_api/outline/views.py @@ -613,6 +613,12 @@ def completions_dict(self): Dictionary keys are block keys and values are int values representing the completion status of the block. """ + + # Anonymous users have no completion rows; return empty data to avoid + # querying BlockCompletion with AnonymousUser for public course navigation. + if self.request.user.is_anonymous: + return {} + course_key_string = self.kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) completions = BlockCompletion.objects.filter(user=self.request.user, context_key=course_key).values_list( From 5e907d36ac5ed803b215519ace56af24191ef0ab Mon Sep 17 00:00:00 2001 From: "m.boghani" Date: Thu, 26 Feb 2026 03:54:22 -0500 Subject: [PATCH 2/4] fix: trailng whitespace pylint C0303 issue. --- lms/djangoapps/course_home_api/outline/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lms/djangoapps/course_home_api/outline/views.py b/lms/djangoapps/course_home_api/outline/views.py index ba7bf7d0ca94..b9168c6ca5fa 100644 --- a/lms/djangoapps/course_home_api/outline/views.py +++ b/lms/djangoapps/course_home_api/outline/views.py @@ -613,12 +613,11 @@ def completions_dict(self): Dictionary keys are block keys and values are int values representing the completion status of the block. """ - # Anonymous users have no completion rows; return empty data to avoid # querying BlockCompletion with AnonymousUser for public course navigation. if self.request.user.is_anonymous: return {} - + course_key_string = self.kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) completions = BlockCompletion.objects.filter(user=self.request.user, context_key=course_key).values_list( From 3b135ce751de11b283bdaf205cb611c430932c7d Mon Sep 17 00:00:00 2001 From: "m.boghani" Date: Thu, 18 Jun 2026 04:18:11 -0400 Subject: [PATCH 3/4] fix(course_home_api): prevent completion data lookup for unauthenticated public courses --- .../outline/tests/test_view.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lms/djangoapps/course_home_api/outline/tests/test_view.py b/lms/djangoapps/course_home_api/outline/tests/test_view.py index e9a3570ea6bd..b5d64ba5841b 100644 --- a/lms/djangoapps/course_home_api/outline/tests/test_view.py +++ b/lms/djangoapps/course_home_api/outline/tests/test_view.py @@ -584,6 +584,25 @@ def test_get_unauthenticated_user(self): assert response.status_code == 200 assert response.data.get('blocks') is None + @override_waffle_flag(COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, active=True) + def test_get_unauthenticated_public_course_does_not_lookup_completions(self): + """ + Test that anonymous public course navigation does not query completion data. + """ + self.add_blocks_to_course() + BlockFactory.create(parent=self.vertical, category='problem', graded=True, has_score=True) + update_outline_from_modulestore(self.course.id) + self.course.course_visibility = COURSE_VISIBILITY_PUBLIC + self.update_course_and_overview() + self.client.logout() + + with patch('lms.djangoapps.course_home_api.outline.views.BlockCompletion.objects.filter') as mock_filter: + response = self.client.get(self.url) + + assert response.status_code == 200 + assert response.data['blocks'] is not None + mock_filter.assert_not_called() + def test_course_staff_can_see_non_user_specific_content_in_masquerade(self): """ Test that course staff can see the outline and other non-user-specific content when masquerading as a learner From a6af567186e1638f96f85f6fcf55a1a55ae56e0a Mon Sep 17 00:00:00 2001 From: "m.boghani" Date: Thu, 18 Jun 2026 10:23:12 -0400 Subject: [PATCH 4/4] fix(course_home_api): ensure anonymous users do not query completion data --- .../outline/tests/test_view.py | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/lms/djangoapps/course_home_api/outline/tests/test_view.py b/lms/djangoapps/course_home_api/outline/tests/test_view.py index b5d64ba5841b..acfe86ca8ed8 100644 --- a/lms/djangoapps/course_home_api/outline/tests/test_view.py +++ b/lms/djangoapps/course_home_api/outline/tests/test_view.py @@ -10,6 +10,7 @@ import ddt from completion.models import BlockCompletion from django.conf import settings +from django.contrib.auth.models import AnonymousUser from django.test import override_settings from django.urls import reverse from edx_toggles.toggles.testutils import override_waffle_flag @@ -20,6 +21,7 @@ from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.roles import CourseInstructorRole from common.djangoapps.student.tests.factories import UserFactory +from lms.djangoapps.course_home_api.outline.views import CourseNavigationBlocksView from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests from lms.djangoapps.course_home_api.toggles import COURSE_HOME_SEND_COURSE_PROGRESS_ANALYTICS_FOR_STUDENT from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory @@ -584,23 +586,18 @@ def test_get_unauthenticated_user(self): assert response.status_code == 200 assert response.data.get('blocks') is None - @override_waffle_flag(COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, active=True) - def test_get_unauthenticated_public_course_does_not_lookup_completions(self): + def test_anonymous_user_completion_dict_does_not_lookup_completions(self): """ - Test that anonymous public course navigation does not query completion data. + Test that anonymous users do not query completion data. """ - self.add_blocks_to_course() - BlockFactory.create(parent=self.vertical, category='problem', graded=True, has_score=True) - update_outline_from_modulestore(self.course.id) - self.course.course_visibility = COURSE_VISIBILITY_PUBLIC - self.update_course_and_overview() - self.client.logout() + view = CourseNavigationBlocksView() + view.request = Mock(user=AnonymousUser()) + view.kwargs = {'course_key_string': str(self.course.id)} with patch('lms.djangoapps.course_home_api.outline.views.BlockCompletion.objects.filter') as mock_filter: - response = self.client.get(self.url) + completions = view.completions_dict - assert response.status_code == 200 - assert response.data['blocks'] is not None + assert completions == {} mock_filter.assert_not_called() def test_course_staff_can_see_non_user_specific_content_in_masquerade(self):