From 64e45ba4c86cc27c7ec697cebb3385cdca6dbb41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Tue, 12 Mar 2024 12:05:42 +0200 Subject: [PATCH] feat: [AXIMST-584] create view for course navigation sidebar --- .../course_home_api/outline/serializers.py | 3 +- .../course_home_api/outline/views.py | 88 +++++++++++++++++++ lms/djangoapps/course_home_api/urls.py | 11 ++- 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/course_home_api/outline/serializers.py b/lms/djangoapps/course_home_api/outline/serializers.py index db6cbdf3a1ab..0d0bf4632cfb 100644 --- a/lms/djangoapps/course_home_api/outline/serializers.py +++ b/lms/djangoapps/course_home_api/outline/serializers.py @@ -19,7 +19,8 @@ class CourseBlockSerializer(serializers.Serializer): def get_blocks(self, block): # pylint: disable=missing-function-docstring block_key = block['id'] block_type = block['type'] - children = block.get('children', []) if block_type != 'sequential' else [] # Don't descend past sequential + last_parent_block_type = 'vertical' if self.context.get('include_vertical') else 'sequential' + children = block.get('children', []) if block_type != last_parent_block_type else [] description = block.get('format') display_name = block['display_name'] enable_links = self.context.get('enable_links') diff --git a/lms/djangoapps/course_home_api/outline/views.py b/lms/djangoapps/course_home_api/outline/views.py index 91c8a6d7f15b..07c1aeb0e96e 100644 --- a/lms/djangoapps/course_home_api/outline/views.py +++ b/lms/djangoapps/course_home_api/outline/views.py @@ -28,6 +28,7 @@ ) from lms.djangoapps.course_goals.models import CourseGoal from lms.djangoapps.course_home_api.outline.serializers import ( + CourseBlockSerializer, OutlineTabSerializer, ) from lms.djangoapps.course_home_api.utils import get_course_or_403 @@ -375,6 +376,93 @@ def finalize_response(self, request, response, *args, **kwargs): return expose_header('Date', response) +class CourseSidebarBlocksView(RetrieveAPIView): + """ + **Use Cases** + Request details for the sidebar navigation of the course. + **Example Requests** + GET api/course_home/v1/sidebar/{course_key} + **Response Values** + For a good 200 response, the response will include: + blocks: List of serialized Course Block objects. Each serialization has the following fields: + id: (str) The usage ID of the block. + type: (str) The type of block. Possible values the names of any + XBlock type in the system, including custom blocks. Examples are + course, chapter, sequential, vertical, html, problem, video, and + discussion. + display_name: (str) The display name of the block. + lms_web_url: (str) The URL to the navigational container of the + xBlock on the web LMS. + children: (list) If the block has child blocks, a list of IDs of + the child blocks. + resume_block: (bool) Whether the block is the resume block + has_scheduled_content: (bool) Whether the block has more content scheduled for the future + **Returns** + * 200 on success. + * 403 if the user does not currently have access to the course and should be redirected. + * 404 if the course is not available or cannot be seen. + """ + + authentication_classes = ( + JwtAuthentication, + BearerAuthenticationAllowInactiveUser, + SessionAuthenticationAllowInactiveUser, + ) + + serializer_class = CourseBlockSerializer + + def get(self, request, *args, **kwargs): + """ + Get the visible course blocks (from course to vertical types) for the given course. + """ + course_key_string = kwargs.get('course_key_string') + course_key = CourseKey.from_string(course_key_string) + course = get_course_or_403(request.user, 'load', course_key, check_if_enrolled=False) + + masquerade_object, request.user = setup_masquerade( + request, + course_key, + staff_access=has_access(request.user, 'staff', course_key), + reset_masquerade_data=True, + ) + + user_is_masquerading = is_masquerading(request.user, course_key, course_masquerade=masquerade_object) + + enrollment = CourseEnrollment.get_enrollment(request.user, course_key) + 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 + course_blocks = None + + is_staff = bool(has_access(request.user, 'staff', course_key)) + if getattr(enrollment, 'is_active', False) or is_staff: + 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 course_blocks: + user_course_outline = get_user_course_outline(course_key, request.user, datetime.now(tz=timezone.utc)) + available_section_ids = {str(section.usage_key) for section in user_course_outline.sections} + available_sequence_ids = {str(usage_key) for usage_key in user_course_outline.sequences} + + course_blocks['children'] = [ + chapter_data for chapter_data in course_blocks.get('children', []) + if chapter_data['id'] in available_section_ids + ] + + for chapter_data in course_blocks['children']: + chapter_data['children'] = [ + seq_data for seq_data in chapter_data['children'] + if (seq_data['id'] in available_sequence_ids or seq_data['type'] != 'sequential') + ] if 'children' in chapter_data else [] + + context = self.get_serializer_context() + context['include_vertical'] = True + serializer = self.get_serializer_class()(course_blocks, context=context) + + return Response(serializer.data) + + @api_view(['POST']) @permission_classes((IsAuthenticated,)) def dismiss_welcome_message(request): # pylint: disable=missing-function-docstring diff --git a/lms/djangoapps/course_home_api/urls.py b/lms/djangoapps/course_home_api/urls.py index b5ffc08481a7..62e356d34710 100644 --- a/lms/djangoapps/course_home_api/urls.py +++ b/lms/djangoapps/course_home_api/urls.py @@ -9,7 +9,11 @@ from lms.djangoapps.course_home_api.course_metadata.views import CourseHomeMetadataView from lms.djangoapps.course_home_api.dates.views import DatesTabView from lms.djangoapps.course_home_api.outline.views import ( - OutlineTabView, dismiss_welcome_message, save_course_goal, unsubscribe_from_course_goal_by_token, + CourseSidebarBlocksView, + OutlineTabView, + dismiss_welcome_message, + save_course_goal, + unsubscribe_from_course_goal_by_token, ) from lms.djangoapps.course_home_api.progress.views import ProgressTabView @@ -44,6 +48,11 @@ OutlineTabView.as_view(), name='outline-tab' ), + re_path( + fr'sidebar/{settings.COURSE_KEY_PATTERN}', + CourseSidebarBlocksView.as_view(), + name='course-sidebar-blocks' + ), re_path( r'dismiss_welcome_message', dismiss_welcome_message,