diff --git a/lms/djangoapps/course_home_api/outline/serializers.py b/lms/djangoapps/course_home_api/outline/serializers.py index cfa518138a95..f66012362327 100644 --- a/lms/djangoapps/course_home_api/outline/serializers.py +++ b/lms/djangoapps/course_home_api/outline/serializers.py @@ -62,6 +62,7 @@ def get_blocks(self, block): # pylint: disable=missing-function-docstring 'type': block_type, 'has_scheduled_content': block.get('has_scheduled_content'), 'hide_from_toc': block.get('hide_from_toc'), + 'is_preview': block.get('is_preview', False), }, } if 'special_exam_info' in self.context.get('extra_fields', []) and block.get('special_exam_info'): 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 74e22e5fcc4b..6de5db83f94c 100644 --- a/lms/djangoapps/course_home_api/outline/tests/test_view.py +++ b/lms/djangoapps/course_home_api/outline/tests/test_view.py @@ -13,6 +13,7 @@ from django.test import override_settings from django.urls import reverse from edx_toggles.toggles.testutils import override_waffle_flag +from opaque_keys.edx.keys import UsageKey from cms.djangoapps.contentstore.outlines import update_outline_from_modulestore from common.djangoapps.course_modes.models import CourseMode @@ -43,6 +44,7 @@ BlockFactory, CourseFactory ) +from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID @ddt.ddt @@ -484,6 +486,89 @@ def test_course_progress_analytics_disabled(self, mock_task): self.client.get(self.url) mock_task.assert_not_called() + # Tests for verified content preview functionality + # These tests cover the feature that allows audit learners to preview + # the structure of verified-only content without access to the content itself + + @patch('lms.djangoapps.course_home_api.outline.views.learner_can_preview_verified_content') + def test_verified_content_preview_disabled_integration(self, mock_preview_function): + """Test that when verified preview is disabled, no preview markers are added.""" + # Given a course with some Verified only sequences + with self.store.bulk_operations(self.course.id): + chapter = BlockFactory.create(category='chapter', parent_location=self.course.location) + sequential = BlockFactory.create( + category='sequential', + parent_location=chapter.location, + display_name='Verified Sequential', + group_access={ENROLLMENT_TRACK_PARTITION_ID: [2]} # restrict to verified only + ) + update_outline_from_modulestore(self.course.id) + + # ... where the preview feature is disabled + mock_preview_function.return_value = False + + # When I access them as an audit user + CourseEnrollment.enroll(self.user, self.course.id, CourseMode.AUDIT) + response = self.client.get(self.url) + + # Then I get a valid response back + assert response.status_code == 200 + + # ... with course_blocks populated + course_blocks = response.data['course_blocks']["blocks"] + + # ... but with verified content omitted + assert str(sequential.location) not in course_blocks + + # ... and no block has preview set to true + for block in course_blocks: + assert course_blocks[block].get('is_preview') is not True + + @patch('lms.djangoapps.course_home_api.outline.views.learner_can_preview_verified_content') + @patch('lms.djangoapps.course_home_api.outline.views.get_user_course_outline') + def test_verified_content_preview_enabled_marks_previewable_content(self, mock_outline, mock_preview_enabled): + """Test that when verified preview is enabled, previewable sequences and chapters are marked.""" + # Given a course with some Verified only sequences and some regular sequences + with self.store.bulk_operations(self.course.id): + chapter = BlockFactory.create(category='chapter', parent_location=self.course.location) + verified_sequential = BlockFactory.create( + category='sequential', + parent_location=chapter.location, + display_name='Verified Sequential', + ) + regular_sequential = BlockFactory.create( + category='sequential', + parent_location=chapter.location, + display_name='Regular Sequential' + ) + update_outline_from_modulestore(self.course.id) + + # ... with an outline that correctly identifies previewable sequences + mock_course_outline = Mock() + mock_course_outline.sections = {Mock(usage_key=chapter.location)} + mock_course_outline.sequences = {verified_sequential.location, regular_sequential.location} + mock_course_outline.previewable_sequences = {verified_sequential.location} + mock_outline.return_value = mock_course_outline + + # When I access them as an audit user with preview enabled + CourseEnrollment.enroll(self.user, self.course.id, CourseMode.AUDIT) + mock_preview_enabled.return_value = True + + # Then I get a valid response back + response = self.client.get(self.url) + assert response.status_code == 200 + + # ... with course_blocks populated + course_blocks = response.data['course_blocks']["blocks"] + + for block in course_blocks: + # ... and the verified only content is marked as preview only + if UsageKey.from_string(block) in mock_course_outline.previewable_sequences: + assert course_blocks[block].get('is_preview') is True + # ... and the regular content is not marked as preview + else: + assert course_blocks[block].get('is_preview') is False + @ddt.ddt class SidebarBlocksTestViews(BaseCourseHomeTests): diff --git a/lms/djangoapps/course_home_api/outline/views.py b/lms/djangoapps/course_home_api/outline/views.py index 7c5307cba764..78d5767ffeed 100644 --- a/lms/djangoapps/course_home_api/outline/views.py +++ b/lms/djangoapps/course_home_api/outline/views.py @@ -36,7 +36,10 @@ ) from lms.djangoapps.course_home_api.utils import get_course_or_403 from lms.djangoapps.course_home_api.tasks import collect_progress_for_user_in_course -from lms.djangoapps.course_home_api.toggles import send_course_progress_analytics_for_student_is_enabled +from lms.djangoapps.course_home_api.toggles import ( + learner_can_preview_verified_content, + send_course_progress_analytics_for_student_is_enabled, +) from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_info_section @@ -209,6 +212,7 @@ def get(self, request, *args, **kwargs): # pylint: disable=too-many-statements allow_anonymous = COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled(course_key) allow_public = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC allow_public_outline = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC_OUTLINE + allow_preview_of_verified_content = learner_can_preview_verified_content(course_key, request.user) # User locale settings user_timezone_locale = user_timezone_locale_prefs(request) @@ -309,7 +313,8 @@ def get(self, request, *args, **kwargs): # pylint: disable=too-many-statements # so this is a tiny first step in that migration. if course_blocks: user_course_outline = get_user_course_outline( - course_key, request.user, datetime.now(tz=timezone.utc) + course_key, request.user, datetime.now(tz=timezone.utc), + preview_verified_content=allow_preview_of_verified_content ) available_seq_ids = {str(usage_key) for usage_key in user_course_outline.sequences} @@ -339,6 +344,19 @@ def get(self, request, *args, **kwargs): # pylint: disable=too-many-statements ) ] if 'children' in chapter_data else [] + # For audit preview of verified content, we don't remove verified content. + # Instead, we mark it as preview so the frontend can handle it appropriately. + if allow_preview_of_verified_content: + previewable_sequences = {str(usage_key) for usage_key in user_course_outline.previewable_sequences} + + # Iterate through course_blocks to mark previewable sequences and chapters + for chapter_data in course_blocks['children']: + if chapter_data['id'] in previewable_sequences: + chapter_data['is_preview'] = True + for seq_data in chapter_data.get('children', []): + if seq_data['id'] in previewable_sequences: + seq_data['is_preview'] = True + user_has_passing_grade = False if not request.user.is_anonymous: user_grade = CourseGradeFactory().read(request.user, course) diff --git a/lms/djangoapps/course_home_api/tests/test_toggles.py b/lms/djangoapps/course_home_api/tests/test_toggles.py new file mode 100644 index 000000000000..46ab545d0ade --- /dev/null +++ b/lms/djangoapps/course_home_api/tests/test_toggles.py @@ -0,0 +1,155 @@ +""" +Tests for Course Home API toggles. +""" + +from unittest.mock import Mock, patch + +from django.test import TestCase +from opaque_keys.edx.keys import CourseKey + +from common.djangoapps.course_modes.models import CourseMode +from common.djangoapps.course_modes.tests.factories import CourseModeFactory + +from ..toggles import learner_can_preview_verified_content + + +class TestLearnerCanPreviewVerifiedContent(TestCase): + """Test cases for learner_can_preview_verified_content function.""" + + def setUp(self): + """Set up test fixtures.""" + self.course_key = CourseKey.from_string("course-v1:TestX+CS101+2024") + self.user = Mock() + + # Set up patchers + self.feature_enabled_patcher = patch( + "lms.djangoapps.course_home_api.toggles.audit_learner_verified_preview_is_enabled" + ) + self.verified_mode_for_course_patcher = patch( + "common.djangoapps.course_modes.models.CourseMode.verified_mode_for_course" + ) + self.get_enrollment_patcher = patch( + "common.djangoapps.student.models.CourseEnrollment.get_enrollment" + ) + + # Course set up with verified, professional, and audit modes + self.verified_mode = CourseModeFactory( + course_id=self.course_key, + mode_slug=CourseMode.VERIFIED, + mode_display_name="Verified", + ) + self.professional_mode = CourseModeFactory( + course_id=self.course_key, + mode_slug=CourseMode.PROFESSIONAL, + mode_display_name="Professional", + ) + self.audit_mode = CourseModeFactory( + course_id=self.course_key, + mode_slug=CourseMode.AUDIT, + mode_display_name="Audit", + ) + self.course_modes_dict = { + "audit": self.audit_mode, + "verified": self.verified_mode, + "professional": self.professional_mode, + } + + # Start patchers + self.mock_feature_enabled = self.feature_enabled_patcher.start() + self.mock_verified_mode_for_course = ( + self.verified_mode_for_course_patcher.start() + ) + self.mock_get_enrollment = self.get_enrollment_patcher.start() + + def _enroll_user(self, mode): + """Helper method to set up user enrollment mock.""" + mock_enrollment = Mock() + mock_enrollment.mode = mode + self.mock_get_enrollment.return_value = mock_enrollment + + def tearDown(self): + """Clean up patchers.""" + self.feature_enabled_patcher.stop() + self.verified_mode_for_course_patcher.stop() + self.get_enrollment_patcher.stop() + + def test_all_conditions_met_returns_true(self): + """Test that function returns True when all conditions are met.""" + # Given the feature is enabled, course has verified mode, and user is enrolled as audit + self.mock_feature_enabled.return_value = True + self.mock_verified_mode_for_course.return_value = self.course_modes_dict[ + "professional" + ] + self._enroll_user(CourseMode.AUDIT) + + # When I check if the learner can preview verified content + result = learner_can_preview_verified_content(self.course_key, self.user) + + # Then the result should be True + self.assertTrue(result) + + def test_feature_disabled_returns_false(self): + """Test that function returns False when feature is disabled.""" + # Given the feature is disabled + self.mock_feature_enabled.return_value = False + + # ... even if all other conditions are met + self.mock_verified_mode_for_course.return_value = self.course_modes_dict[ + "professional" + ] + self._enroll_user(CourseMode.AUDIT) + + # When I check if the learner can preview verified content + result = learner_can_preview_verified_content(self.course_key, self.user) + + # Then the result should be False + self.assertFalse(result) + + def test_no_verified_mode_returns_false(self): + """Test that function returns False when course has no verified mode.""" + # Given the course does not have a verified mode + self.mock_verified_mode_for_course.return_value = None + + # ... even if all other conditions are met + self.mock_feature_enabled.return_value = True + self._enroll_user(CourseMode.AUDIT) + + # When I check if the learner can preview verified content + result = learner_can_preview_verified_content(self.course_key, self.user) + + # Then the result should be False + self.assertFalse(result) + + def test_no_enrollment_returns_false(self): + """Test that function returns False when user is not enrolled.""" + # Given the user is unenrolled + self.mock_get_enrollment.return_value = None + + # ... even if all other conditions are met + self.mock_feature_enabled.return_value = True + self.mock_verified_mode_for_course.return_value = self.course_modes_dict[ + "professional" + ] + + # When I check if the learner can preview verified content + result = learner_can_preview_verified_content(self.course_key, self.user) + + # Then the result should be False + self.assertFalse(result) + + def test_verified_enrollment_returns_false(self): + """Test that function returns False when user is enrolled in verified mode.""" + # Given the user is not enrolled as audit + self._enroll_user(CourseMode.VERIFIED) + + # ... even if all other conditions are met + self.mock_feature_enabled.return_value = True + self.mock_verified_mode_for_course.return_value = self.course_modes_dict[ + "professional" + ] + + # When I check if the learner can preview verified content + result = learner_can_preview_verified_content(self.course_key, self.user) + + # Then the result should be False + self.assertFalse(result) diff --git a/lms/djangoapps/course_home_api/toggles.py b/lms/djangoapps/course_home_api/toggles.py index 052862796c75..1f2d32b87e96 100644 --- a/lms/djangoapps/course_home_api/toggles.py +++ b/lms/djangoapps/course_home_api/toggles.py @@ -3,6 +3,9 @@ """ from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag +from openedx.core.lib.cache_utils import request_cached +from common.djangoapps.course_modes.models import CourseMode +from common.djangoapps.student.models import CourseEnrollment WAFFLE_FLAG_NAMESPACE = 'course_home' @@ -51,6 +54,21 @@ ) +# Waffle flag to enable audit learner preview of course structure visible to verified learners. +# +# .. toggle_name: course_home.audit_learner_verified_preview +# .. toggle_implementation: CourseWaffleFlag +# .. toggle_default: False +# .. toggle_description: Where enabled, audit learners can see the presence of the sections / units +# otherwise restricted to verified learners. The content itself remains inaccessible. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2025-11-07 +# .. toggle_target_removal_date: None +COURSE_HOME_AUDIT_LEARNER_VERIFIED_PREVIEW = CourseWaffleFlag( + f'{WAFFLE_FLAG_NAMESPACE}.audit_learner_verified_preview', __name__ +) + + def course_home_mfe_progress_tab_is_active(course_key): # Avoiding a circular dependency from .models import DisableProgressPageStackedConfig @@ -73,3 +91,40 @@ def send_course_progress_analytics_for_student_is_enabled(course_key): Returns True if the course completion analytics feature is enabled for a given course. """ return COURSE_HOME_SEND_COURSE_PROGRESS_ANALYTICS_FOR_STUDENT.is_enabled(course_key) + + +def audit_learner_verified_preview_is_enabled(course_key): + """ + Returns True if the audit learner verified preview feature is enabled for a given course. + """ + return COURSE_HOME_AUDIT_LEARNER_VERIFIED_PREVIEW.is_enabled(course_key) + + +@request_cached() +def learner_can_preview_verified_content(course_key, user): + """ + Determine if an audit learner can preview verified content in a course. + + Args: + course_key: The course identifier. + user: The user object + Returns: + True if the learner can preview verified content, False otherwise. + """ + # To preview verified content, the feature must be enabled for the course... + feature_enabled = audit_learner_verified_preview_is_enabled(course_key) + if not feature_enabled: + return False + + # ... the course must have a verified mode + course_has_verified_mode = CourseMode.verified_mode_for_course(course_key) + if not course_has_verified_mode: + return False + + # ... and the user must be enrolled as audit + enrollment = CourseEnrollment.get_enrollment(user, course_key) + user_enrolled_as_audit = enrollment is not None and enrollment.mode == CourseMode.AUDIT + if not user_enrolled_as_audit: + return False + + return True diff --git a/openedx/core/djangoapps/content/learning_sequences/api/outlines.py b/openedx/core/djangoapps/content/learning_sequences/api/outlines.py index cd2b12d03f1c..c91971f0fc67 100644 --- a/openedx/core/djangoapps/content/learning_sequences/api/outlines.py +++ b/openedx/core/djangoapps/content/learning_sequences/api/outlines.py @@ -258,17 +258,23 @@ def get_content_errors(course_key: CourseKey) -> List[ContentErrorData]: @function_trace('learning_sequences.api.get_user_course_outline') def get_user_course_outline(course_key: CourseKey, user: types.User, - at_time: datetime) -> UserCourseOutlineData: + at_time: datetime, + preview_verified_content: bool = False) -> UserCourseOutlineData: """ Get an outline customized for a particular user at a particular time. `user` is a Django User object (including the AnonymousUser) `at_time` should be a UTC datetime.datetime object. + If `preview_verified_content` is True, an audit user will be able to see the + presence of verified content even if they are not enrolled in verified mode. + See the definition of UserCourseOutlineData for details about the data returned. """ - user_course_outline, _ = _get_user_course_outline_and_processors(course_key, user, at_time) + user_course_outline, _ = _get_user_course_outline_and_processors( + course_key, user, at_time, preview_verified_content=preview_verified_content + ) return user_course_outline @@ -302,7 +308,8 @@ def get_user_course_outline_details(course_key: CourseKey, def _get_user_course_outline_and_processors(course_key: CourseKey, # lint-amnesty, pylint: disable=missing-function-docstring user: types.User, - at_time: datetime): + at_time: datetime, + preview_verified_content: bool = False): """ Helper function that runs the outline processors. @@ -340,6 +347,8 @@ def _get_user_course_outline_and_processors(course_key: CourseKey, # lint-amnes processors = {} usage_keys_to_remove = set() inaccessible_sequences = set() + preview_usage_keys = set() + for name, processor_cls in processor_classes: # Future optimization: This should be parallelizable (don't rely on a # particular ordering). @@ -349,6 +358,14 @@ def _get_user_course_outline_and_processors(course_key: CourseKey, # lint-amnes if not user_can_see_all_content: # function_trace lets us see how expensive each processor is being. with function_trace(f'learning_sequences.api.outline_processors.{name}'): + + # An exception is made for audit preview of verified content. + # Where enabled, we selectively disable the enrollment track partition processor + # so audit learners can preview (see presence of, but not access) of other track content. + if name == 'enrollment_track_partitions' and preview_verified_content: + preview_usage_keys |= processor.usage_keys_to_remove(full_course_outline) + continue + processor_usage_keys_removed = processor.usage_keys_to_remove(full_course_outline) processor_inaccessible_sequences = processor.inaccessible_sequences(full_course_outline) usage_keys_to_remove |= processor_usage_keys_removed @@ -357,12 +374,14 @@ def _get_user_course_outline_and_processors(course_key: CourseKey, # lint-amnes # Open question: Does it make sense to remove a Section if it has no Sequences in it? trimmed_course_outline = full_course_outline.remove(usage_keys_to_remove) accessible_sequences = frozenset(set(trimmed_course_outline.sequences) - inaccessible_sequences) + previewable_sequences = frozenset(preview_usage_keys) user_course_outline = UserCourseOutlineData( base_outline=full_course_outline, user=user, at_time=at_time, accessible_sequences=accessible_sequences, + previewable_sequences=previewable_sequences, **{ name: getattr(trimmed_course_outline, name) for name in [ diff --git a/openedx/core/djangoapps/content/learning_sequences/api/tests/test_outlines.py b/openedx/core/djangoapps/content/learning_sequences/api/tests/test_outlines.py index 20effa6b16cd..ecd1ce282998 100644 --- a/openedx/core/djangoapps/content/learning_sequences/api/tests/test_outlines.py +++ b/openedx/core/djangoapps/content/learning_sequences/api/tests/test_outlines.py @@ -9,6 +9,7 @@ from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.db.models import signals +from common.djangoapps.course_modes.tests.factories import CourseModeFactory from edx_proctoring.exceptions import ProctoredExamNotFoundException from edx_toggles.toggles.testutils import override_waffle_flag from edx_when.api import set_dates_for_course @@ -167,6 +168,8 @@ class UserCourseOutlineTestCase(CacheIsolationTestCase): @classmethod def setUpTestData(cls): # lint-amnesty, pylint: disable=super-method-not-called course_key = CourseKey.from_string("course-v1:OpenEdX+Outline+T1") + CourseModeFactory.create(course_id=course_key, mode_slug='verified') + # Users... cls.global_staff = UserFactory.create( username='global_staff', email='gstaff@example.com', is_staff=True @@ -176,6 +179,9 @@ def setUpTestData(cls): # lint-amnesty, pylint: disable=super-method-not-called ) cls.beta_tester = BetaTesterFactory(course_key=course_key) cls.anonymous_user = AnonymousUser() + cls.verified_student = UserFactory.create( + username='verified', email='verified@example.com', is_staff=False + ) # Seed with data cls.course_key = course_key @@ -196,6 +202,10 @@ def setUpTestData(cls): # lint-amnesty, pylint: disable=super-method-not-called cls.student.courseenrollment_set.create(course_id=cls.course_key, is_active=True, mode="audit") # Enroll beta tester in the course cls.beta_tester.courseenrollment_set.create(course_id=cls.course_key, is_active=True, mode="audit") + # Enroll verified student in the course as verified + cls.verified_student.courseenrollment_set.create( + course_id=cls.course_key, is_active=True, mode=CourseMode.VERIFIED + ) def test_simple_outline(self): """This outline is the same for everyone.""" @@ -228,6 +238,87 @@ def test_simple_outline(self): ) assert global_staff_outline_details.outline == global_staff_outline + def test_audit_preview_of_verified_content_enabled(self): + # Given an outline where some content is restricted to verified only + audit_outline = self.simple_outline + verified_sequence = attr.evolve( + audit_outline.sections[0].sequences[0], + user_partition_groups={ + ENROLLMENT_TRACK_PARTITION_ID: [2] # restrict to verified only + } + ) + audit_outline.sections[0].sequences[0] = verified_sequence + replace_course_outline(audit_outline) + at_time = datetime(2020, 5, 21, tzinfo=timezone.utc) + + # ... and where the audit learner verified preview feature is enabled + # When I access them as an audit user + audit_student_outline = get_user_course_outline( + self.course_key, self.student, at_time, preview_verified_content=True + ) + + # Then verified-only content is marked as previewable for the audit user + assert verified_sequence.usage_key in audit_student_outline.previewable_sequences + + # When I access them as a verified user, which would disable this preview check + verified_student_outline = get_user_course_outline( + self.course_key, self.verified_student, at_time + ) + + global_staff_outline = get_user_course_outline( + self.course_key, self.global_staff, at_time + ) + + # For verified and staff, the outline is unchanged + assert verified_student_outline.sections == global_staff_outline.sections + + # ... and do not contain any previewable sequences + assert verified_student_outline.previewable_sequences == set() + assert global_staff_outline.previewable_sequences == set() + + def test_audit_preview_of_verified_content_disabled(self): + """ + This outline has verified content that an audit user can preview + only when the feature is enabled. + """ + # Given an outline where some content is restricted to verified only + audit_outline = self.simple_outline + verified_sequence = attr.evolve( + audit_outline.sections[0].sequences[0], + user_partition_groups={ + ENROLLMENT_TRACK_PARTITION_ID: [2] # restrict to verified only + } + ) + audit_outline.sections[0].sequences[0] = verified_sequence + replace_course_outline(audit_outline) + at_time = datetime(2020, 5, 21, tzinfo=timezone.utc) + + # ... and where the audit learner verified preview feature is disabled + # When I access them as an audit user + audit_student_outline = get_user_course_outline( + self.course_key, self.student, at_time, + preview_verified_content=False + ) + + # Then verified-only content is removed from the outline for the audit user + assert verified_sequence not in audit_student_outline.sections[0].sequences + # ... and is not marked as previewable + assert audit_student_outline.previewable_sequences == set() + + verified_student_outline = get_user_course_outline( + self.course_key, self.verified_student, at_time + ) + global_staff_outline = get_user_course_outline( + self.course_key, self.global_staff, at_time + ) + + # For verified and staff, the outline is unchanged + assert verified_student_outline.sections == global_staff_outline.sections + + # ... and do not contain any previewable sequences + assert verified_student_outline.previewable_sequences == set() + assert global_staff_outline.previewable_sequences == set() + class OutlineProcessorTestCase(CacheIsolationTestCase): # lint-amnesty, pylint: disable=missing-class-docstring @classmethod diff --git a/openedx/core/djangoapps/content/learning_sequences/data.py b/openedx/core/djangoapps/content/learning_sequences/data.py index c13b451490ab..4a6e6da3a0d5 100644 --- a/openedx/core/djangoapps/content/learning_sequences/data.py +++ b/openedx/core/djangoapps/content/learning_sequences/data.py @@ -361,6 +361,9 @@ class is a pretty dumb container that doesn't understand anything about how # not be able to access anything inside. accessible_sequences: FrozenSet[UsageKey] + # Sequences that are not accessible, but are previewable by an audit learner. + previewable_sequences: FrozenSet[UsageKey] + @attr.s(frozen=True, auto_attribs=True) class UserCourseOutlineDetailsData: diff --git a/openedx/core/djangoapps/content/learning_sequences/services.py b/openedx/core/djangoapps/content/learning_sequences/services.py index a43d6ddd598c..e1c1a1402f8a 100644 --- a/openedx/core/djangoapps/content/learning_sequences/services.py +++ b/openedx/core/djangoapps/content/learning_sequences/services.py @@ -2,7 +2,6 @@ Learning Sequences Runtime Service """ - from .api import get_user_course_outline, get_user_course_outline_details @@ -17,8 +16,12 @@ def get_user_course_outline_details(self, course_key, user, at_time): """ return get_user_course_outline_details(course_key, user, at_time) - def get_user_course_outline(self, course_key, user, at_time): + def get_user_course_outline( + self, course_key, user, at_time, preview_verified_content=False + ): """ Returns UserCourseOutlineData """ - return get_user_course_outline(course_key, user, at_time) + return get_user_course_outline( + course_key, user, at_time, preview_verified_content=preview_verified_content + ) diff --git a/openedx/features/course_experience/utils.py b/openedx/features/course_experience/utils.py index c20b7f077136..8cc9b854f377 100644 --- a/openedx/features/course_experience/utils.py +++ b/openedx/features/course_experience/utils.py @@ -76,11 +76,11 @@ def recurse_num_graded_problems(block): def recurse_mark_auth_denial(block): """ - Mark this block as 'scored' if any of its descendents are 'scored' (that is, 'has_score' and 'weight' > 0). + Mark this block access as denied for any reason found in its descendents. """ own_denial_reason = {block['authorization_denial_reason']} if 'authorization_denial_reason' in block else set() # Use a list comprehension to force the recursion over all children, rather than just stopping - # at the first child that is scored. + # at the first child that is blocked. child_denial_reasons = own_denial_reason.union( *(recurse_mark_auth_denial(child) for child in block.get('children', [])) )