diff --git a/lms/djangoapps/course_home_api/outline/v1/serializers.py b/lms/djangoapps/course_home_api/outline/v1/serializers.py index a730a3211a..735ec43d85 100644 --- a/lms/djangoapps/course_home_api/outline/v1/serializers.py +++ b/lms/djangoapps/course_home_api/outline/v1/serializers.py @@ -2,9 +2,10 @@ Outline Tab Serializers. """ +from django.utils.translation import ngettext from rest_framework import serializers + from lms.djangoapps.course_home_api.dates.v1.serializers import DateSummarySerializer -from rest_framework.reverse import reverse class CourseBlockSerializer(serializers.Serializer): @@ -13,21 +14,45 @@ class CourseBlockSerializer(serializers.Serializer): """ blocks = serializers.SerializerMethodField() - def get_blocks(self, blocks): - return { - str(block_key): { - 'id': str(block_key), - 'type': block_key.category, - 'display_name': blocks.get_xblock_field(block_key, 'display_name', block_key.category), - 'lms_web_url': reverse( - 'jump_to', - kwargs={'course_id': str(block_key.course_key), 'location': str(block_key)}, - request=self.context['request'], - ), - 'children': [str(child_key) for child_key in blocks.get_children(block_key)], - } - for block_key in blocks + def get_blocks(self, block): + block_key = block['id'] + block_type = block['type'] + children = block.get('children', []) if block_type != 'sequential' else [] # Don't descend past sequential + description = block.get('format') + display_name = block['display_name'] + enable_links = self.context.get('enable_links') + graded = block.get('graded') + icon = None + num_graded_problems = block.get('num_graded_problems', 0) + scored = block.get('scored') + + if num_graded_problems and block_type == 'sequential': + questions = ngettext('({number} Question)', '({number} Questions)', num_graded_problems) + display_name += ' ' + questions.format(number=num_graded_problems) + + if graded and scored: + icon = 'fa-pencil-square-o' + + if 'special_exam_info' in block: + description = block['special_exam_info'].get('short_description') + icon = block['special_exam_info'].get('suggested_icon', 'fa-pencil-square-o') + + serialized = { + block_key: { + 'children': [child['id'] for child in children], + 'complete': block.get('complete', False), + 'description': description, + 'display_name': display_name, + 'due': block.get('due'), + 'icon': icon, + 'id': block_key, + 'lms_web_url': block['lms_web_url'] if enable_links else None, + 'type': block_type, + }, } + for child in children: + serialized.update(self.get_blocks(child)) + return serialized class CourseGoalSerializer(serializers.Serializer): diff --git a/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py b/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py index fff38d645a..a64b48c3c9 100644 --- a/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py +++ b/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py @@ -2,7 +2,10 @@ Tests for Outline Tab API in the Course Home API """ +from datetime import datetime + import ddt +from django.conf import settings from django.urls import reverse from mock import patch @@ -15,6 +18,7 @@ from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_F from student.models import CourseEnrollment from student.tests.factories import UserFactory from xmodule.course_module import COURSE_VISIBILITY_PUBLIC +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @ddt.ddt @@ -205,3 +209,78 @@ class OutlineTabTestViews(BaseCourseHomeTests): selected_goal = course_goals['selected_goal'] self.assertIsNotNone(selected_goal) self.assertEqual(selected_goal['key'], 'certify') + + @COURSE_HOME_MICROFRONTEND.override(active=True) + @COURSE_HOME_MICROFRONTEND_OUTLINE_TAB.override(active=True) + @patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True}) + @patch('lms.djangoapps.course_api.blocks.transformers.milestones.get_attempt_status_summary') + def test_proctored_exam(self, mock_summary): + course = CourseFactory.create( + org='edX', + course='900', + run='test_run', + enable_proctored_exams=True, + proctoring_provider=settings.PROCTORING_BACKENDS['DEFAULT'], + ) + chapter = ItemFactory.create(parent=course, category='chapter', display_name='Test Section') + sequence = ItemFactory.create( + parent=chapter, + category='sequential', + display_name='Test Proctored Exam', + graded=True, + is_time_limited=True, + default_time_limit_minutes=10, + is_proctored_exam=True, + is_practice_exam=True, + due=datetime.now(), + exam_review_rules='allow_use_of_paper', + hide_after_due=False, + is_onboarding_exam=False, + ) + mock_summary.return_value = { + 'short_description': 'My Exam', + 'suggested_icon': 'fa-foo-bar', + } + url = reverse('course-home-outline-tab', args=[course.id]) + + CourseEnrollment.enroll(self.user, course.id) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + exam_data = response.data['course_blocks']['blocks'][str(sequence.location)] + self.assertFalse(exam_data['complete']) + self.assertEqual(exam_data['description'], 'My Exam') + self.assertEqual(exam_data['display_name'], 'Test Proctored Exam') + self.assertIsNotNone(exam_data['due']) + self.assertEqual(exam_data['icon'], 'fa-foo-bar') + + @COURSE_HOME_MICROFRONTEND.override(active=True) + @COURSE_HOME_MICROFRONTEND_OUTLINE_TAB.override(active=True) + def test_assignment(self): + course = CourseFactory.create() + with self.store.bulk_operations(course.id): + chapter = ItemFactory.create(category='chapter', parent_location=course.location) + sequential = ItemFactory.create(display_name='Test', category='sequential', graded=True, has_score=True, + parent_location=chapter.location) + problem1 = ItemFactory.create(category='problem', graded=True, has_score=True, + parent_location=sequential.location) + problem2 = ItemFactory.create(category='problem', graded=True, has_score=True, + parent_location=sequential.location) + sequential2 = ItemFactory.create(display_name='Ungraded', category='sequential', + parent_location=chapter.location) + course.children = [chapter] + chapter.children = [sequential, sequential2] + sequential.children = [problem1, problem2] + url = reverse('course-home-outline-tab', args=[course.id]) + + CourseEnrollment.enroll(self.user, course.id) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + exam_data = response.data['course_blocks']['blocks'][str(sequential.location)] + self.assertEqual(exam_data['display_name'], 'Test (2 Questions)') + self.assertEqual(exam_data['icon'], 'fa-pencil-square-o') + + ungraded_data = response.data['course_blocks']['blocks'][str(sequential2.location)] + self.assertEqual(ungraded_data['display_name'], 'Ungraded') + self.assertIsNone(ungraded_data['icon']) diff --git a/lms/djangoapps/course_home_api/outline/v1/views.py b/lms/djangoapps/course_home_api/outline/v1/views.py index 75c016264c..909bd8dd23 100644 --- a/lms/djangoapps/course_home_api/outline/v1/views.py +++ b/lms/djangoapps/course_home_api/outline/v1/views.py @@ -18,8 +18,6 @@ from rest_framework.response import Response from completion.exceptions import UnavailableCompletionData from completion.utilities import get_key_to_last_completed_block from course_modes.models import CourseMode -from lms.djangoapps.course_api.blocks.transformers.blocks_api import BlocksAPITransformer -from lms.djangoapps.course_blocks.api import get_course_block_access_transformers, get_course_blocks from lms.djangoapps.course_goals.api import (add_course_goal, get_course_goal, get_course_goal_text, has_course_goal_permission, valid_course_goals_ordered) from lms.djangoapps.course_home_api.outline.v1.serializers import OutlineTabSerializer @@ -31,12 +29,12 @@ from lms.djangoapps.courseware.context_processor import user_timezone_locale_pre from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_info_section, get_course_with_access from lms.djangoapps.courseware.date_summary import TodaysDate from lms.djangoapps.courseware.masquerade import setup_masquerade -from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.user_api.course_tag.api import get_course_tag, set_course_tag from openedx.features.course_duration_limits.access import generate_course_expired_message from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, LATEST_UPDATE_FLAG from openedx.features.course_experience.course_tools import CourseToolsPluginManager +from openedx.features.course_experience.utils import get_course_outline_block_tree from openedx.features.course_experience.views.latest_update import LatestUpdateFragmentView from openedx.features.course_experience.views.welcome_message import PREFERENCE_KEY, WelcomeMessageFragmentView from openedx.features.discounts.utils import generate_offer_html @@ -192,13 +190,7 @@ class OutlineTabView(RetrieveAPIView): if course_home_mfe_dates_tab_is_active(course.id): dates_tab_link = get_microfrontend_url(course_key=course.id, view_name='dates') - transformers = BlockStructureTransformers() - transformers += get_course_block_access_transformers(request.user) - transformers += [ - BlocksAPITransformer(None, None, depth=3), - ] - - course_blocks = get_course_blocks(request.user, course_usage_key, transformers, include_completion=True) + course_blocks = get_course_outline_block_tree(request, course_key_string, request.user if is_enrolled else None) has_visited_course = False try: @@ -260,6 +252,7 @@ class OutlineTabView(RetrieveAPIView): } context = self.get_serializer_context() context['course_key'] = course_key + context['enable_links'] = show_enrolled or allow_public serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data) diff --git a/lms/djangoapps/course_home_api/tests/utils.py b/lms/djangoapps/course_home_api/tests/utils.py index 39cdc47c37..1bbcb3891c 100644 --- a/lms/djangoapps/course_home_api/tests/utils.py +++ b/lms/djangoapps/course_home_api/tests/utils.py @@ -12,10 +12,8 @@ from course_modes.tests.factories import CourseModeFactory from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin from lms.djangoapps.verify_student.models import VerificationDeadline from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory -from student.tests.factories import UserFactory -from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase -from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @@ -25,8 +23,6 @@ class BaseCourseHomeTests(ModuleStoreTestCase, MasqueradeMixin): Creates a course to """ - MODULESTORE = TEST_DATA_SPLIT_MODULESTORE - def setUp(self): super().setUp()