From a4d21181cebc85448e1b2304acabe5c8a0d8e77d Mon Sep 17 00:00:00 2001 From: sundarthapa2u Date: Mon, 3 Nov 2025 18:58:22 +0000 Subject: [PATCH] feat: content gating --- .../course_home_api/outline/serializers.py | 10 +++++--- .../course_home_api/outline/views.py | 13 +++++++--- .../core/djangoapps/courseware_api/views.py | 6 +++-- xmodule/seq_block.py | 25 ++++++++++++++++--- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/lms/djangoapps/course_home_api/outline/serializers.py b/lms/djangoapps/course_home_api/outline/serializers.py index cfa518138a95..afcf315c0dcd 100644 --- a/lms/djangoapps/course_home_api/outline/serializers.py +++ b/lms/djangoapps/course_home_api/outline/serializers.py @@ -8,7 +8,8 @@ from lms.djangoapps.course_home_api.dates.serializers import DateSummarySerializer from lms.djangoapps.course_home_api.progress.serializers import CertificateDataSerializer from lms.djangoapps.course_home_api.serializers import DatesBannerSerializer, VerifiedModeSerializer - +from lms.djangoapps.course_blocks.api import get_course_blocks +from opaque_keys.edx.keys import UsageKey class CourseBlockSerializer(serializers.Serializer): """ @@ -28,6 +29,7 @@ def get_blocks(self, block): # pylint: disable=missing-function-docstring icon = None num_graded_problems = block.get('num_graded_problems', 0) scored = block.get('scored') + block_data = set(get_course_blocks(self.context['request'].user, UsageKey.from_string(block_key)).get_block_keys()) if num_graded_problems and block_type == 'sequential': questions = ngettext('({number} Question)', '({number} Questions)', num_graded_problems) @@ -37,7 +39,7 @@ def get_blocks(self, block): # pylint: disable=missing-function-docstring icon = 'fa-pencil-square-o' if block_type == 'vertical': - icon = self.get_vertical_icon_class(block) + icon = self.get_vertical_icon_class(block, block_data) if 'special_exam_info' in block: description = block['special_exam_info'].get('short_description') @@ -74,7 +76,7 @@ def get_blocks(self, block): # pylint: disable=missing-function-docstring return serialized @staticmethod - def get_vertical_icon_class(block): + def get_vertical_icon_class(block, block_data): """ Get the icon class for a vertical block based priority of child blocks types. Currently, the priority for the icon is as follows: @@ -87,6 +89,8 @@ def get_vertical_icon_class(block): for child in children for value in (child.get('type'), child.get('icon_class')) } + if not block_data: + return 'lock' if 'problem' in child_classes: return 'problem' if 'video' in child_classes: diff --git a/lms/djangoapps/course_home_api/outline/views.py b/lms/djangoapps/course_home_api/outline/views.py index 7c5307cba764..21421f34fcec 100644 --- a/lms/djangoapps/course_home_api/outline/views.py +++ b/lms/djangoapps/course_home_api/outline/views.py @@ -307,7 +307,8 @@ def get(self, request, *args, **kwargs): # pylint: disable=too-many-statements # # The long term goal is to remove the Course Blocks API call entirely, # so this is a tiny first step in that migration. - if course_blocks: + user_is_audit = getattr(enrollment, 'mode', '') == 'audit' + if course_blocks and not user_is_audit: user_course_outline = get_user_course_outline( course_key, request.user, datetime.now(tz=timezone.utc) ) @@ -451,6 +452,8 @@ def get(self, request, *args, **kwargs): allow_public_outline = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC_OUTLINE enrollment = CourseEnrollment.get_enrollment(request.user, course_key) + user_is_audit = getattr(enrollment, 'mode', '') == 'audit' + try: user_cohort = get_cohort(request.user, course_key, use_cached=True) except ValueError: @@ -472,15 +475,17 @@ def get(self, request, *args, **kwargs): course_blocks = cache.get(cache_key) if not course_blocks: - if getattr(enrollment, 'is_active', False) or bool(staff_access): + if bool(staff_access) or user_is_audit: + course_blocks = get_course_outline_block_tree(request, course_key_string, None) + elif getattr(enrollment, 'is_active', False): course_blocks = get_course_outline_block_tree(request, course_key_string, request.user) elif allow_public_outline or allow_public or user_is_masquerading: course_blocks = get_course_outline_block_tree(request, course_key_string, None) - if not navigation_sidebar_caching_is_disabled: cache.set(cache_key, course_blocks, self.COURSE_BLOCKS_CACHE_TIMEOUT) - course_blocks = self.filter_inaccessible_blocks(course_blocks, course_key) + if not user_is_audit: + course_blocks = self.filter_inaccessible_blocks(course_blocks, course_key) course_blocks = self.mark_complete_recursive(course_blocks) context = self.get_serializer_context() diff --git a/openedx/core/djangoapps/courseware_api/views.py b/openedx/core/djangoapps/courseware_api/views.py index ee37835b4841..de5763bec105 100644 --- a/openedx/core/djangoapps/courseware_api/views.py +++ b/openedx/core/djangoapps/courseware_api/views.py @@ -815,9 +815,11 @@ def get(self, request, usage_key_string, *args, **kwargs): # lint-amnesty, pyli view = STUDENT_VIEW if request.user.is_anonymous: view = PUBLIC_VIEW - + enrollment = CourseEnrollment.get_enrollment(request.user, usage_key.course_key) + user_is_audit = getattr(enrollment, 'mode', '') == 'audit' context = { - 'specific_masquerade': is_masquerading_as_specific_student(request.user, usage_key.course_key) + 'specific_masquerade': is_masquerading_as_specific_student(request.user, usage_key.course_key), + 'user_is_audit': user_is_audit } return Response(sequence.get_metadata(view=view, context=context)) diff --git a/xmodule/seq_block.py b/xmodule/seq_block.py index f06d3030f5e9..aff1999c742b 100644 --- a/xmodule/seq_block.py +++ b/xmodule/seq_block.py @@ -348,22 +348,31 @@ def get_metadata(self, view=STUDENT_VIEW, context=None): prereq_met = True prereq_meta_info = {} banner_text = None - children = self.get_children() course = self._get_course() is_hidden_after_due = False - + children = list(self.get_children()) + + if not children and getattr(self, "children", None) and context.get("user_is_audit", False): + all_children = [] + for child_locator in self.children: + try: + child_block = self.runtime.modulestore.get_item(child_locator) + setattr(child_block, "access_restricted", True) + all_children.append(child_block) + except Exception as e: + continue + if all_children: + children = all_children if self._required_prereq(): if self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_IS_STAFF): banner_text = _( 'This subsection is unlocked for learners when they meet the prerequisite requirements.' ) else: - # check if prerequisite has been met prereq_met, prereq_meta_info = self._compute_is_prereq_met(True) if prereq_met and view == STUDENT_VIEW and not self._can_user_view_content(course): if context.get('specific_masquerade', False): - # Still show the content, but flag to the staff user that the learner wouldn't be able to see it banner_text = self._hidden_content_banner_text(course) else: is_hidden_after_due = True @@ -570,6 +579,14 @@ def _get_render_metadata(self, context, children, prereq_met, prereq_meta_info, 'is_gated': True, # Mark as blocked 'content': '', # Real content not included }) + for block in blocks: + for child in children: + # Match block by usage ID + if str(child.scope_ids.usage_id) == str(block.get('id')): + # Check if child has the flag + if getattr(child, "access_restricted", False): + block['access_restricted'] = True + break # stop inner loop once matched params = { 'items': blocks,