AA-126: Expose more course-outline info to learning MFE
- Use the same get_course_outline_block_tree call that the current outline uses - Show number of problems in subsection display names - Don't send links if the user is not enrolled or course isn't public - Send subsection icons to MFE - Send subsection descriptions to MFE - Send completion info to MFE
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user