diff --git a/common/test/acceptance/tests/lms/test_lms_course_home.py b/common/test/acceptance/tests/lms/test_lms_course_home.py index 04f5a347f5..ba9f7cfa34 100644 --- a/common/test/acceptance/tests/lms/test_lms_course_home.py +++ b/common/test/acceptance/tests/lms/test_lms_course_home.py @@ -134,16 +134,6 @@ class CourseHomeA11yTest(CourseHomeBaseTest): self.logout_page = LogoutPage(self.browser) - self.studio_course_outline = StudioCourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - # adds graded assignments to course home for testing course outline a11y - self._set_policy_for_subsection("Homework", 0) - def test_course_home_a11y(self): """ Test the accessibility of the course home page with course outline. @@ -153,18 +143,6 @@ class CourseHomeA11yTest(CourseHomeBaseTest): course_home_page.visit() course_home_page.a11y_audit.check_for_accessibility_errors() - def _set_policy_for_subsection(self, policy, section=0): - """ - Set the grading policy for the first subsection in the specified section. - If a section index is not provided, 0 is assumed. - """ - with self._logged_in_session(staff=True): - self.studio_course_outline.visit() - modal = self.studio_course_outline.section_at(section).subsection_at( - 0).edit() - modal.policy = policy - modal.save() - @contextmanager def _logged_in_session(self, staff=False): """ diff --git a/lms/djangoapps/course_api/blocks/api.py b/lms/djangoapps/course_api/blocks/api.py index 64c62af896..234358d3a0 100644 --- a/lms/djangoapps/course_api/blocks/api.py +++ b/lms/djangoapps/course_api/blocks/api.py @@ -51,12 +51,12 @@ def get_blocks( """ # create ordered list of transformers, adding BlocksAPITransformer at end. transformers = BlockStructureTransformers() - can_view_special_exam = False - if requested_fields is not None and 'special_exam' in requested_fields: - can_view_special_exam = True + include_special_exams = False + if requested_fields is not None and 'special_exam_info' in requested_fields: + include_special_exams = True if user is not None: transformers += COURSE_BLOCK_ACCESS_TRANSFORMERS - transformers += [MilestonesTransformer(can_view_special_exam), HiddenContentTransformer()] + transformers += [MilestonesTransformer(include_special_exams), HiddenContentTransformer()] transformers += [ BlocksAPITransformer( block_counts, diff --git a/lms/djangoapps/course_api/blocks/transformers/__init__.py b/lms/djangoapps/course_api/blocks/transformers/__init__.py index 5a7a21b400..9d34d0a743 100644 --- a/lms/djangoapps/course_api/blocks/transformers/__init__.py +++ b/lms/djangoapps/course_api/blocks/transformers/__init__.py @@ -45,7 +45,7 @@ SUPPORTED_FIELDS = [ # 'student_view_multi_device' SupportedFieldType(StudentViewTransformer.STUDENT_VIEW_MULTI_DEVICE, StudentViewTransformer), - SupportedFieldType('special_exam', MilestonesTransformer), + SupportedFieldType('special_exam_info', MilestonesTransformer), # set the block_field_name to None so the entire data for the transformer is serialized SupportedFieldType(None, BlockCountsTransformer, BlockCountsTransformer.BLOCK_COUNTS), diff --git a/lms/djangoapps/course_api/blocks/transformers/milestones.py b/lms/djangoapps/course_api/blocks/transformers/milestones.py index 0f2ee9f01e..ad70563779 100644 --- a/lms/djangoapps/course_api/blocks/transformers/milestones.py +++ b/lms/djangoapps/course_api/blocks/transformers/milestones.py @@ -19,7 +19,8 @@ log = logging.getLogger(__name__) class MilestonesTransformer(BlockStructureTransformer): """ - Excludes all special exams (timed, proctored, practice proctored) from the student view. + Adds special exams (timed, proctored, practice proctored) to the student view. + May exclude special exams. Excludes all blocks with unfulfilled milestones from the student view. """ WRITE_VERSION = 1 @@ -29,8 +30,8 @@ class MilestonesTransformer(BlockStructureTransformer): def name(cls): return "milestones" - def __init__(self, can_view_special_exams=True): - self.can_view_special_exams = can_view_special_exams + def __init__(self, include_special_exams=True): + self.include_special_exams = include_special_exams @classmethod def collect(cls, block_structure): @@ -51,48 +52,9 @@ class MilestonesTransformer(BlockStructureTransformer): Modify block structure according to the behavior of milestones and special exams. """ - def add_special_exam_info(block_key): - """ - Adds special exam information to course blocks. - """ - if self.is_special_exam(block_key, block_structure): - - # - # call into edx_proctoring subsystem - # to get relevant proctoring information regarding this - # level of the courseware - # - # This will return None, if (user, course_id, content_id) - # is not applicable - # - timed_exam_attempt_context = None - try: - timed_exam_attempt_context = get_attempt_status_summary( - usage_info.user.id, - unicode(block_key.course_key), - unicode(block_key) - ) - except ProctoredExamNotFoundException as ex: - log.exception(ex) - - if timed_exam_attempt_context: - # yes, user has proctoring context about - # this level of the courseware - # so add to the accordion data context - block_structure.set_transformer_block_field( - block_key, - self, - 'special_exam', - timed_exam_attempt_context, - ) - - root_key = block_structure.root_block_usage_key - course_key = root_key.course_key + course_key = block_structure.root_block_usage_key.course_key user_can_skip = EntranceExamConfiguration.user_can_skip_entrance_exam(usage_info.user, course_key) - exam_id = block_structure.get_xblock_field(root_key, 'entrance_exam_id') required_content = milestones_helpers.get_required_content(course_key, usage_info.user) - if user_can_skip: - required_content = [content for content in required_content if not content == exam_id] def user_gated_from_block(block_key): """ @@ -103,12 +65,11 @@ class MilestonesTransformer(BlockStructureTransformer): return False elif self.has_pending_milestones_for_user(block_key, usage_info): return True - elif required_content: - if block_key.block_type == 'chapter' and unicode(block_key) not in required_content: - return True + elif self.gated_by_required_content(block_key, block_structure, user_can_skip, required_content): + return True elif (settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and (self.is_special_exam(block_key, block_structure) and - not self.can_view_special_exams)): + not self.include_special_exams)): return True return False @@ -116,7 +77,7 @@ class MilestonesTransformer(BlockStructureTransformer): if user_gated_from_block(block_key): block_structure.remove_block(block_key, False) else: - add_special_exam_info(block_key) + self.add_special_exam_info(block_key, block_structure, usage_info) @staticmethod def is_special_exam(block_key, block_structure): @@ -142,3 +103,50 @@ class MilestonesTransformer(BlockStructureTransformer): 'requires', usage_info.user.id )) + + def add_special_exam_info(self, block_key, block_structure, usage_info): + """ + Adds special exam information to course blocks. + """ + if self.is_special_exam(block_key, block_structure): + + # call into edx_proctoring subsystem to get relevant special exam information + # + # This will return None, if (user, course_id, content_id) is not applicable + special_exam_attempt_context = None + try: + special_exam_attempt_context = get_attempt_status_summary( + usage_info.user.id, + unicode(block_key.course_key), + unicode(block_key) + ) + except ProctoredExamNotFoundException as ex: + log.exception(ex) + + if special_exam_attempt_context: + # yes, user has proctoring context about + # this level of the courseware + # so add to the accordion data context + block_structure.set_transformer_block_field( + block_key, + self, + 'special_exam_info', + special_exam_attempt_context, + ) + + @staticmethod + def gated_by_required_content(block_key, block_structure, user_can_skip, required_content): + """ + Returns True if the current block associated with the block_key should be gated by the given required_content. + Returns False otherwise. + """ + if not required_content: + return False + exam_id = block_structure.get_xblock_field(block_structure.root_block_usage_key, 'entrance_exam_id') + if user_can_skip: + required_content = [content for content in required_content if not content == exam_id] + + if block_key.block_type == 'chapter' and unicode(block_key) not in required_content: + return True + + return False diff --git a/lms/djangoapps/course_api/blocks/transformers/tests/test_milestones.py b/lms/djangoapps/course_api/blocks/transformers/tests/test_milestones.py index 63a3816934..9955b5a4d0 100644 --- a/lms/djangoapps/course_api/blocks/transformers/tests/test_milestones.py +++ b/lms/djangoapps/course_api/blocks/transformers/tests/test_milestones.py @@ -186,7 +186,7 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM self.setup_gated_section(self.blocks['H'], self.blocks['A']) self.get_blocks_and_check_against_expected(self.staff, expected_blocks) - def test_can_view_special(self): + def test_special_exams(self): """ When the block structure transformers are set to allow users to view special exams, ensure that we can see the special exams and not any of the otherwise gated blocks. diff --git a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html index 6640440c08..74929a1f23 100644 --- a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html @@ -51,26 +51,26 @@ from django.utils.translation import ugettext as _ if subsection.get('due') is None: data_string = subsection.get('format') else: - if 'special_exam' in subsection: + if 'special_exam_info' in subsection: data_string = _('due {date}') else: data_string = _("{subsection_format} due {{date}}").format(subsection_format=subsection.get('format')) %> - % if subsection.get('format') or 'special_exam' in subsection: + % if subsection.get('format') or 'special_exam_info' in subsection: % if 'special_exam' in subsection: ## Display the proctored exam status icon and status message - ${subsection['special_exam'].get('short_description', '')} + ${subsection['special_exam_info'].get('short_description', '')} ## completed proctored exam statuses should not show the due date ## since the exam has already been submitted by the user - % if not subsection['special_exam'].get('in_completed_state', False): + % if not subsection['special_exam_info'].get('in_completed_state', False):