Merge pull request #28698 from edx/mikix/hide-after-due-mfe

feat: notify MFE when a sequence is hidden-after-due
This commit is contained in:
Michael Terry
2021-09-10 09:12:57 -04:00
committed by GitHub
4 changed files with 76 additions and 19 deletions

View File

@@ -366,23 +366,37 @@ class SequenceBlock(
return json.dumps(self._get_completion(data))
raise NotFoundError('Unexpected dispatch type')
def get_metadata(self, view=STUDENT_VIEW):
def get_metadata(self, view=STUDENT_VIEW, context=None):
"""Returns a dict of some common block properties"""
context = {'exclude_units': True}
context = context or {}
context['exclude_units'] = True
prereq_met = True
prereq_meta_info = {}
banner_text = None
display_items = self.get_display_items()
course = self._get_course()
is_hidden_after_due = False
if self._required_prereq():
if self.runtime.user_is_staff:
banner_text = _('This subsection is unlocked for learners when they meet the prerequisite requirements.') # lint-amnesty, pylint: disable=line-too-long
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
meta = self._get_render_metadata(context, display_items, prereq_met, prereq_meta_info, banner_text, view)
meta['display_name'] = self.display_name_with_default
meta['format'] = getattr(self, 'format', '')
meta['is_hidden_after_due'] = is_hidden_after_due
return meta
@classmethod
@@ -434,7 +448,10 @@ class SequenceBlock(
self, self.course_id
)
def student_view(self, context): # lint-amnesty, pylint: disable=missing-function-docstring
def student_view(self, context):
"""
Renders the normal student view of the block in the LMS.
"""
_ = self.runtime.service(self, "i18n").ugettext
context = context or {}
self._capture_basic_metrics()
@@ -443,7 +460,9 @@ class SequenceBlock(
prereq_meta_info = {}
if self._required_prereq():
if self.runtime.user_is_staff:
banner_text = _('This subsection is unlocked for learners when they meet the prerequisite requirements.') # lint-amnesty, pylint: disable=line-too-long
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)
@@ -496,6 +515,16 @@ class SequenceBlock(
banner_text = _("This exam is hidden from the learner.")
return banner_text, special_exam_html
def _hidden_content_banner_text(self, course):
"""
Chooses a banner message to show for hidden content
"""
_ = self.runtime.service(self, 'i18n').gettext
if course.self_paced:
return _('Because the course has ended, this assignment is hidden from the learner.')
else:
return _('Because the due date has passed, this assignment is hidden from the learner.')
def _hidden_content_student_view(self, context):
"""
Checks whether the content of this sequential is hidden from the
@@ -505,10 +534,7 @@ class SequenceBlock(
_ = self.runtime.service(self, "i18n").ugettext
course = self._get_course()
if not self._can_user_view_content(course):
if course.self_paced:
banner_text = _("Because the course has ended, this assignment is hidden from the learner.")
else:
banner_text = _("Because the due date has passed, this assignment is hidden from the learner.")
banner_text = self._hidden_content_banner_text(course)
hidden_content_html = self.system.render_template(
'hidden_content.html',
@@ -535,7 +561,9 @@ class SequenceBlock(
# NOTE (CCB): We default to true to maintain the behavior in place prior to allowing anonymous access access.
return context.get('user_authenticated', True)
def _get_render_metadata(self, context, display_items, prereq_met, prereq_meta_info, banner_text=None, view=STUDENT_VIEW, fragment=None): # lint-amnesty, pylint: disable=line-too-long, missing-function-docstring
def _get_render_metadata(self, context, display_items, prereq_met, prereq_meta_info, banner_text=None,
view=STUDENT_VIEW, fragment=None):
"""Returns a dictionary of sequence metadata, used by render methods and for the courseware API"""
if prereq_met and not self._is_gate_fulfilled():
_ = self.runtime.service(self, "i18n").ugettext
banner_text = _(

View File

@@ -110,6 +110,11 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
module_system.descriptor_runtime = block._runtime # pylint: disable=protected-access
block.xmodule_runtime = module_system
# The render operation will ask modulestore for the current course to get some data. As these tests were
# originally not written to be compatible with a real modulestore, we've mocked out the relevant return values.
module_system.modulestore = Mock()
module_system.modulestore.get_course.return_value = self.course
def _get_rendered_view(self,
sequence,
requested_child=None,
@@ -124,12 +129,8 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
if extra_context:
context.update(extra_context)
# The render operation will ask modulestore for the current course to get some data. As these tests were
# originally not written to be compatible with a real modulestore, we've mocked out the relevant return values.
with patch.object(SequenceBlock, '_get_course') as mock_course:
self.course.self_paced = self_paced
mock_course.return_value = self.course
return sequence.xmodule_runtime.render(sequence, view, context).content
self.course.self_paced = self_paced
return sequence.xmodule_runtime.render(sequence, view, context).content
def _assert_view_at_position(self, rendered_html, expected_position):
"""

View File

@@ -387,7 +387,8 @@ class CourseApiTestViews(BaseCoursewareTests, MasqueradeMixin):
assert courseware_data['is_mfe_proctored_exams_enabled'] == (is_globally_enabled and is_waffle_enabled)
class SequenceApiTestViews(BaseCoursewareTests):
@ddt.ddt
class SequenceApiTestViews(MasqueradeMixin, BaseCoursewareTests):
"""
Tests for the sequence REST API
"""
@@ -407,6 +408,32 @@ class SequenceApiTestViews(BaseCoursewareTests):
assert response.data['display_name'] == 'sequence'
assert len(response.data['items']) == 1
@ddt.data(
(False, None, False, False),
(True, None, True, False),
(True, {'username': 'student'}, False, True),
# Masquerading as a limited-access learner here, but specific partition/group doesn't matter.
# We just want to test that masquerading as a non-specific learner has a different outcome.
(True, {'user_partition_id': 51, 'group_id': 1}, True, False),
)
@ddt.unpack
def test_hidden_after_due(self, is_past_due, masquerade_config, expected_hidden, expected_banner):
"""Validate the metadata when hide-after-due is set for a sequence"""
due = datetime.now() + timedelta(days=-1 if is_past_due else 1)
sequence = ItemFactory(parent=self.chapter, category='sequential', hide_after_due=True, due=due)
CourseEnrollment.enroll(self.user, self.course.id)
user = self.instructor if masquerade_config else self.user
self.client.login(username=user.username, password='foo')
if masquerade_config:
self.update_masquerade(**masquerade_config)
response = self.client.get(f'/api/courseware/sequence/{sequence.location}')
assert response.status_code == 200
assert response.data['is_hidden_after_due'] == expected_hidden
assert bool(response.data['banner_text']) == expected_banner
class ResumeApiTestViews(BaseCoursewareTests, CompletionWaffleTestMixin):
"""

View File

@@ -29,7 +29,7 @@ from lms.djangoapps.courseware.access_response import (
)
from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs
from lms.djangoapps.courseware.courses import check_course_access
from lms.djangoapps.courseware.masquerade import setup_masquerade
from lms.djangoapps.courseware.masquerade import is_masquerading_as_specific_student, setup_masquerade
from lms.djangoapps.courseware.module_render import get_module_by_usage_id
from lms.djangoapps.courseware.tabs import get_course_tab_list
from lms.djangoapps.courseware.toggles import (
@@ -545,7 +545,8 @@ class SequenceMetadata(DeveloperErrorViewMixin, APIView):
if request.user.is_anonymous:
view = PUBLIC_VIEW
return Response(sequence.get_metadata(view=view))
context = {'specific_masquerade': is_masquerading_as_specific_student(request.user, usage_key.course_key)}
return Response(sequence.get_metadata(view=view, context=context))
class Resume(DeveloperErrorViewMixin, APIView):