def test_gated(self, gated_block_ref, gating_block_ref, expected_blocks_before_completion): """ First, checks that a student cannot see the gated block when it is gated by the gating block and no attempt has been made to complete the gating block. Then, checks that the student can see the gated block after the gating block has been completed. expected_blocks_before_completion is the set of blocks we expect to be visible to the student before the student has completed the gating block. The test data includes one special exam and one non-special block as the gating blocks. """ self.course.enable_subsection_gating = True self.setup_gated_section(self.blocks[gated_block_ref], self.blocks[gating_block_ref]) with self.assertNumQueries(8): self.get_blocks_and_check_against_expected(self.user, expected_blocks_before_completion) # clear the request cache to simulate a new request self.clear_caches() # this call triggers reevaluation of prerequisites fulfilled by the gating block. with patch('gating.api._get_subsection_percentage', Mock(return_value=100)): lms_gating_api.evaluate_prerequisite( self.course, Mock(location=self.blocks[gating_block_ref].location), self.user, ) with self.assertNumQueries(8): self.get_blocks_and_check_against_expected(self.user, self.ALL_BLOCKS_EXCEPT_SPECIAL)
def test_gated(self, gated_block_ref, gating_block_ref, gating_block_child, expected_blocks_before_completion): """ First, checks that a student cannot see the gated block when it is gated by the gating block and no attempt has been made to complete the gating block. Then, checks that the student can see the gated block after the gating block has been completed. expected_blocks_before_completion is the set of blocks we expect to be visible to the student before the student has completed the gating block. The test data includes one special exam and one non-special block as the gating blocks. """ self.course.enable_subsection_gating = True self.setup_gated_section(self.blocks[gated_block_ref], self.blocks[gating_block_ref]) with self.assertNumQueries(3): self.get_blocks_and_check_against_expected(self.user, expected_blocks_before_completion) # clear the request cache to simulate a new request self.clear_caches() # mock the api that the lms gating api calls to get the score for each block to always return 1 (ie 100%) with patch('gating.api.get_module_score', Mock(return_value=1)): # this call triggers reevaluation of prerequisites fulfilled by the parent of the # block passed in, so we pass in a child of the gating block lms_gating_api.evaluate_prerequisite( self.course, UsageKey.from_string(unicode(self.blocks[gating_block_child].location)), self.user.id) with self.assertNumQueries(2): self.get_blocks_and_check_against_expected(self.user, self.ALL_BLOCKS_EXCEPT_SPECIAL)
def test_invalid_min_score(self, module_score, result, mock_min_score, mock_score): self._setup_gating_milestone(None) mock_score.return_value = module_score mock_min_score.return_value = 100 evaluate_prerequisite(self.course, self.subsection_grade, self.user) self.assertEqual(milestones_api.user_has_milestone(self.user_dict, self.prereq_milestone), result)
def task_evaluate_subsection_completion_milestones(course_id, block_id, user_id): """ Updates users' milestones related to completion of a subsection. Args: course_id(str): Course id which triggered a completion event block_id(str): Id of the completed block user_id(int): Id of the user who completed a block """ store = modulestore() course_key = CourseKey.from_string(course_id) with store.bulk_operations(course_key): course = store.get_course(course_key) if not course or not course.enable_subsection_gating: log.debug( u"Gating: ignoring evaluation of completion milestone because it disabled for course [%s]", course_id ) else: try: user = User.objects.get(id=user_id) course_structure = get_course_blocks(user, store.make_course_usage_key(course_key)) completed_block_usage_key = UsageKey.from_string(block_id).map_into_course(course.id) subsection_block = _get_subsection_of_block(completed_block_usage_key, course_structure) subsection = course_structure[subsection_block] log.debug( u"Gating: Evaluating completion milestone for subsection [%s] and user [%s]", unicode(subsection.location), user.id ) gating_api.evaluate_prerequisite(course, subsection, user) except KeyError: log.error(u"Gating: Given prerequisite subsection [%s] not found in course structure", block_id)
def task_evaluate_subsection_completion_milestones(course_id, block_id, user_id): """ Updates users' milestones related to completion of a subsection. Args: course_id(str): Course id which triggered a completion event block_id(str): Id of the completed block user_id(int): Id of the user who completed a block """ store = modulestore() course_key = CourseKey.from_string(course_id) with store.bulk_operations(course_key): course = store.get_course(course_key) if not course or not course.enable_subsection_gating: log.debug( "Gating: ignoring evaluation of completion milestone because it disabled for course [%s]", course_id ) else: try: user = User.objects.get(id=user_id) course_structure = get_course_blocks(user, store.make_course_usage_key(course_key)) completed_block_usage_key = UsageKey.from_string(block_id).map_into_course(course.id) subsection_block = _get_subsection_of_block(completed_block_usage_key, course_structure) subsection = course_structure[subsection_block] log.debug( "Gating: Evaluating completion milestone for subsection [%s] and user [%s]", unicode(subsection.location), user.id ) gating_api.evaluate_prerequisite(course, subsection, user) except KeyError: log.error("Gating: Given prerequisite subsection [%s] not found in course structure", block_id)
def test_gated(self, gated_block_ref, gating_block_ref, expected_blocks_before_completion): """ Students should be able to see gated content blocks before and after they have completed the prerequisite for it. First, checks that a student can see the gated block when it is gated by the gating block and no attempt has been made to complete the gating block. Then, checks that the student can see the gated block after the gating block has been completed. expected_blocks_before_completion is the set of blocks we expect to be visible to the student before the student has completed the gating block. The test data includes one special exam and one non-special block as the gating blocks. """ self.course.enable_subsection_gating = True self.setup_gated_section(self.blocks[gated_block_ref], self.blocks[gating_block_ref]) with self.assertNumQueries(6): self.get_blocks_and_check_against_expected(self.user, expected_blocks_before_completion) # clear the request cache to simulate a new request self.clear_caches() # this call triggers reevaluation of prerequisites fulfilled by the gating block. lms_gating_api.evaluate_prerequisite( self.course, Mock(location=self.blocks[gating_block_ref].location, percent_graded=1.0), self.user, ) with self.assertNumQueries(6): self.get_blocks_and_check_against_expected(self.user, self.ALL_BLOCKS_EXCEPT_SPECIAL)
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. """ self.transformers = BlockStructureTransformers( [self.TRANSFORMER_CLASS_TO_TEST(True)]) self.course.enable_subsection_gating = True self.setup_gated_section(self.blocks['H'], self.blocks['A']) expected_blocks = ('course', 'A', 'B', 'C', 'ProctoredExam', 'D', 'E', 'PracticeExam', 'F', 'G', 'TimedExam', 'J', 'K', 'H', 'I') self.get_blocks_and_check_against_expected(self.user, expected_blocks) # clear the request cache to simulate a new request self.clear_caches() # this call triggers reevaluation of prerequisites fulfilled by the gating block. with patch('openedx.core.lib.gating.api._get_subsection_percentage', Mock(return_value=100)): lms_gating_api.evaluate_prerequisite( self.course, Mock(location=self.blocks['A'].location), self.user, ) self.get_blocks_and_check_against_expected(self.user, self.ALL_BLOCKS)
def test_min_score_achieved(self, module_score, result, mock_module_score): """ Test test_min_score_achieved """ self._setup_gating_milestone(50) mock_module_score.return_value = module_score evaluate_prerequisite(self.course, self.prob1.location, self.user.id) self.assertEqual(milestones_api.user_has_milestone(self.user_dict, self.prereq_milestone), result)
def test_no_gated_content(self, mock_module_score): """ Test test_no_gated_content """ # Setup gating milestones data gating_api.add_prerequisite(self.course.id, self.seq1.location) evaluate_prerequisite(self.course, self.prob1.location, self.user.id) self.assertFalse(mock_module_score.called)
def test_invalid_min_score(self, module_score, module_completion, result, mock_min_score, mock_completion): self._setup_gating_milestone(None, None) mock_completion.return_value = module_completion self.subsection_grade.percent_graded = module_score / 100.0 mock_min_score.return_value = 100, 100 evaluate_prerequisite(self.course, self.subsection_grade, self.user) self.assertEqual(milestones_api.user_has_milestone(self.user_dict, self.prereq_milestone), result)
def test_invalid_min_score(self, module_score, result, mock_module_score, mock_log): """ Test test_invalid_min_score """ self._setup_gating_milestone(None) mock_module_score.return_value = module_score evaluate_prerequisite(self.course, self.prob1.location, self.user.id) self.assertEqual(milestones_api.user_has_milestone(self.user_dict, self.prereq_milestone), result) self.assertTrue(mock_log.called)
def evaluate_subsection_gated_milestones(**kwargs): """ Receives the SUBSECTION_SCORE_CHANGED signal and triggers the evaluation of any milestone relationships which are attached to the subsection. Arguments: kwargs (dict): Contains user, course, course_structure, subsection_grade Returns: None """ subsection_grade = kwargs['subsection_grade'] gating_api.evaluate_prerequisite(kwargs['course'], subsection_grade, kwargs.get('user'))
def handle_score_changed(**kwargs): """ Receives the PROBLEM_SCORE_CHANGED signal sent by LMS when a student's score has changed for a given component and triggers the evaluation of any milestone relationships which are attached to the updated content. Arguments: kwargs (dict): Contains user ID, course key, and content usage key Returns: None """ course = modulestore().get_course(CourseKey.from_string(kwargs.get('course_id'))) block = modulestore().get_item(UsageKey.from_string(kwargs.get('usage_id'))) gating_api.evaluate_prerequisite(course, block, kwargs.get('user_id')) gating_api.evaluate_entrance_exam(course, block, kwargs.get('user_id'))
def test_content_unlocked(self): """ Test that a sequential/subsection with unmet prereqs correctly indicated that its content is locked """ course = self.course self.setup_gated_section(self.course_blocks['gated_content'], self.course_blocks['prerequisite']) # complete the prerequisite to unlock the gated content # this call triggers reevaluation of prerequisites fulfilled by the gating block. with patch('openedx.core.lib.gating.api._get_subsection_percentage', Mock(return_value=100)): lms_gating_api.evaluate_prerequisite( self.course, Mock(location=self.course_blocks['prerequisite'].location), self.user, ) response = self.client.get(course_home_url(course)) self.assertEqual(response.status_code, 200) response_content = pq(response.content) # check unlock icon is present unlock_icon = response_content('.fa-unlock') self.assertTrue(unlock_icon, "unlock icon is not present, but should be") subsection = unlock_icon.parents('.subsection-title') # check that subsection-title-name is the display name of gated content section gated_subsection_title = self.course_blocks[ 'gated_content'].display_name self.assertIn(gated_subsection_title, subsection.children('.subsection-title-name').html()) # check that it doesn't say prerequisite required self.assertNotIn(self.PREREQ_REQUIRED, subsection.children('.subsection-title-name').html()) # check that there is a screen reader message self.assertTrue(subsection.children('.sr')) # check that the screen reader message is correct self.assertIn(self.UNLOCKED, subsection.children('.sr').html())
def test_content_unlocked(self): """ Test that a sequential/subsection with met prereqs correctly indicated that its content is unlocked """ course = self.course self.setup_gated_section(self.course_blocks['gated_content'], self.course_blocks['prerequisite']) # complete the prerequisite to unlock the gated content # this call triggers reevaluation of prerequisites fulfilled by the gating block. with patch( 'openedx.core.lib.gating.api.get_subsection_completion_percentage', Mock(return_value=100)): lms_gating_api.evaluate_prerequisite( self.course, Mock(location=self.course_blocks['prerequisite'].location, percent_graded=1.0), self.user, ) response = self.client.get(course_home_url(course)) self.assertEqual(response.status_code, 200) response_content = pq(response.content) # check unlock icon is not present unlock_icon = response_content('.fa-unlock') self.assertFalse(unlock_icon, "unlock icon is present, yet shouldn't be.") gated_subsection_title = self.course_blocks[ 'gated_content'].display_name every_subsection_on_outline = response_content('.subsection-title') subsection_has_gated_text = False says_prerequisite_required = False for subsection_contents in every_subsection_on_outline.contents(): subsection_has_gated_text = gated_subsection_title in subsection_contents says_prerequisite_required = "Prerequisite:" in subsection_contents # check that subsection-title-name is the display name of gated content section self.assertTrue(subsection_has_gated_text) self.assertFalse(says_prerequisite_required)
def handle_subsection_score_changed(**kwargs): """ Receives the SUBSECTION_SCORE_CHANGED signal and triggers the evaluation of any milestone relationships which are attached to the updated content. Arguments: kwargs (dict): Contains user ID, course key, and content usage key Returns: None """ course = kwargs['course'] if course.enable_subsection_gating: subsection_grade = kwargs['subsection_grade'] new_score = subsection_grade.graded_total.earned / subsection_grade.graded_total.possible * 100.0 gating_api.evaluate_prerequisite( course, kwargs['user'], subsection_grade.location, new_score, )
def test_content_unlocked(self): """ Test that a sequential/subsection with unmet prereqs correctly indicated that its content is locked """ course = self.course self.setup_gated_section(self.course_blocks['gated_content'], self.course_blocks['prerequisite']) # complete the prerequisite to unlock the gated content # this call triggers reevaluation of prerequisites fulfilled by the gating block. with patch('openedx.core.lib.gating.api._get_subsection_percentage', Mock(return_value=100)): lms_gating_api.evaluate_prerequisite( self.course, Mock(location=self.course_blocks['prerequisite'].location), self.user, ) response = self.client.get(course_home_url(course)) self.assertEqual(response.status_code, 200) response_content = pq(response.content) # check unlock icon is present unlock_icon = response_content('.fa-unlock') self.assertTrue(unlock_icon, "unlock icon is not present, but should be") subsection = unlock_icon.parents('.subsection-title') # check that subsection-title-name is the display name of gated content section gated_subsection_title = self.course_blocks['gated_content'].display_name self.assertIn(gated_subsection_title, subsection.children('.subsection-title-name').html()) # check that it doesn't say prerequisite required self.assertNotIn(self.PREREQ_REQUIRED, subsection.children('.subsection-title-name').html()) # check that there is a screen reader message self.assertTrue(subsection.children('.sr')) # check that the screen reader message is correct self.assertIn(self.UNLOCKED, subsection.children('.sr').html())
def handle_subsection_score_updated(**kwargs): """ Receives the SCORE_CHANGED signal sent by LMS when a student's score has changed for a given component and triggers the evaluation of any milestone relationships which are attached to the updated content. Arguments: kwargs (dict): Contains user ID, course key, and content usage key Returns: None """ course = kwargs['course'] if course.enable_subsection_gating: subsection_grade = kwargs['subsection_grade'] new_score = subsection_grade.graded_total.earned / subsection_grade.graded_total.possible * 100.0 gating_api.evaluate_prerequisite( course, kwargs['user'], subsection_grade.location, new_score, )
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. """ self.transformers = BlockStructureTransformers([self.TRANSFORMER_CLASS_TO_TEST(True)]) self.course.enable_subsection_gating = True self.setup_gated_section(self.blocks['H'], self.blocks['A']) expected_blocks = ( 'course', 'A', 'B', 'C', 'ProctoredExam', 'D', 'E', 'PracticeExam', 'F', 'G', 'TimedExam', 'J', 'K', 'H', 'I' ) self.get_blocks_and_check_against_expected(self.user, expected_blocks) # clear the request cache to simulate a new request self.clear_caches() # this call triggers reevaluation of prerequisites fulfilled by the gating block. lms_gating_api.evaluate_prerequisite( self.course, Mock(location=self.blocks['A'].location, percent_graded=1.0), self.user, ) self.get_blocks_and_check_against_expected(self.user, self.ALL_BLOCKS)
def test_content_unlocked(self): """ Test that a sequential/subsection with met prereqs correctly indicated that its content is unlocked """ course = self.course self.setup_gated_section(self.course_blocks['gated_content'], self.course_blocks['prerequisite']) # complete the prerequisite to unlock the gated content # this call triggers reevaluation of prerequisites fulfilled by the gating block. with patch('openedx.core.lib.gating.api._get_subsection_percentage', Mock(return_value=100)): lms_gating_api.evaluate_prerequisite( self.course, Mock(location=self.course_blocks['prerequisite'].location), self.user, ) response = self.client.get(course_home_url(course)) self.assertEqual(response.status_code, 200) response_content = pq(response.content) # check unlock icon is not present unlock_icon = response_content('.fa-unlock') self.assertFalse(unlock_icon, "unlock icon is present, yet shouldn't be.") gated_subsection_title = self.course_blocks['gated_content'].display_name every_subsection_on_outline = response_content('.subsection-title') subsection_has_gated_text = False says_prerequisite_required = False for subsection_contents in every_subsection_on_outline.contents(): subsection_has_gated_text = gated_subsection_title in subsection_contents says_prerequisite_required = "Prerequisite:" in subsection_contents # check that subsection-title-name is the display name of gated content section self.assertTrue(subsection_has_gated_text) self.assertFalse(says_prerequisite_required)
def test_no_prerequisites(self, mock_module_score): """ Test test_no_prerequisites """ evaluate_prerequisite(self.course, self.prob1.location, self.user.id) self.assertFalse(mock_module_score.called)
def test_orphaned_xblock(self, mock_module_score): """ Test test_orphaned_xblock """ evaluate_prerequisite(self.course, self.prob2.location, self.user.id) self.assertFalse(mock_module_score.called)
def test_no_gated_content(self, mock_score): gating_api.add_prerequisite(self.course.id, self.seq1.location) evaluate_prerequisite(self.course, self.subsection_grade, self.user) self.assertFalse(mock_score.called)
def test_no_prerequisites(self, mock_score): evaluate_prerequisite(self.course, self.subsection_grade, self.user) self.assertFalse(mock_score.called)