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:
Michael Terry
2020-08-31 09:31:24 -04:00
parent 315b974b82
commit c653ac2c8a
4 changed files with 123 additions and 30 deletions

View File

@@ -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):

View File

@@ -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'])

View File

@@ -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)

View File

@@ -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()