def score_published_handler(sender, block, user, raw_earned, raw_possible, only_if_higher, **kwargs): # pylint: disable=unused-argument """ Handles whenever a block's score is published. Returns whether the score was actually updated. """ update_score = True if only_if_higher: previous_score = get_score(user.id, block.location) if previous_score is not None: prev_raw_earned, prev_raw_possible = (previous_score.grade, previous_score.max_grade) if not is_score_higher_or_equal(prev_raw_earned, prev_raw_possible, raw_earned, raw_possible): update_score = False log.warning( u"Grades: Rescore is not higher than previous: " u"user: {}, block: {}, previous: {}/{}, new: {}/{} ". format( user, block.location, prev_raw_earned, prev_raw_possible, raw_earned, raw_possible, )) if update_score: # Set the problem score in CSM. score_modified_time = set_score(user.id, block.location, raw_earned, raw_possible) # Set the problem score on the xblock. if isinstance(block, ScorableXBlockMixin): block.set_score( Score(raw_earned=raw_earned, raw_possible=raw_possible)) # Fire a signal (consumed by enqueue_subsection_update, below) PROBLEM_RAW_SCORE_CHANGED.send( sender=None, raw_earned=raw_earned, raw_possible=raw_possible, weight=getattr(block, 'weight', None), user_id=user.id, course_id=six.text_type(block.location.course_key), usage_id=six.text_type(block.location), only_if_higher=only_if_higher, modified=score_modified_time, score_db_table=ScoreDatabaseTableEnum.courseware_student_module, score_deleted=kwargs.get('score_deleted', False), grader_response=kwargs.get('grader_response', False)) return update_score
def _has_db_updated_with_new_score(self, scored_block_usage_key, **kwargs): """ Returns whether the database has been updated with the expected new score values for the given problem and user. """ if kwargs[ 'score_db_table'] == ScoreDatabaseTableEnum.courseware_student_module: score = get_score(kwargs['user_id'], scored_block_usage_key) found_modified_time = score.modified if score is not None else None elif kwargs['score_db_table'] == ScoreDatabaseTableEnum.submissions: score = sub_api.get_score({ "student_id": kwargs['anonymous_user_id'], "course_id": six.text_type(scored_block_usage_key.course_key), "item_id": six.text_type(scored_block_usage_key), "item_type": scored_block_usage_key.block_type, }) found_modified_time = score['created_at'] if score is not None else None else: assert kwargs['score_db_table'] == ScoreDatabaseTableEnum.overrides from . import api score = api.get_subsection_grade_override( user_id=kwargs['user_id'], course_key_or_id=kwargs['course_id'], usage_key_or_id=kwargs['usage_id']) found_modified_time = score.modified if score is not None else None if score is None: # score should be None only if it was deleted. # Otherwise, it hasn't yet been saved. db_is_updated = kwargs['score_deleted'] else: db_is_updated = found_modified_time >= from_timestamp( kwargs['expected_modified_time']) if not db_is_updated: log.info( u"Grades: tasks._has_database_updated_with_new_score is False. Task ID: {}. Kwargs: {}. Found " u"modified time: {}".format( self.request.id, kwargs, found_modified_time, )) return db_is_updated
def test_scores_persisted(self): """ Test that a block's emitted scores are cached in StudentModule In the future if there is a REST API to retrieve individual block's scores, that should be used instead of checking StudentModule directly. """ block_id = library_api.create_library_block(self.library.key, "problem", "scored_problem").usage_key new_olx = """ <problem display_name="New Multi Choice Question" max_attempts="5"> <multiplechoiceresponse> <p>This is a normal capa problem. It has "maximum attempts" set to **5**.</p> <label>Blockstore is designed to store.</label> <choicegroup type="MultipleChoice"> <choice correct="false">XBlock metadata only</choice> <choice correct="true">XBlock data/metadata and associated static asset files</choice> <choice correct="false">Static asset files for XBlocks and courseware</choice> <choice correct="false">XModule metadata only</choice> </choicegroup> </multiplechoiceresponse> </problem> """.strip() library_api.set_library_block_olx(block_id, new_olx) library_api.publish_changes(self.library.key) # Now view the problem as Alice: client = APIClient() client.login(username=self.student_a.username, password='******') student_view_result = client.get( URL_BLOCK_RENDER_VIEW.format(block_key=block_id, view_name='student_view')) problem_key = "input_{}_2_1".format(block_id) self.assertIn(problem_key, student_view_result.data["content"]) # And submit a wrong answer: result = client.get( URL_BLOCK_GET_HANDLER_URL.format(block_key=block_id, handler_name='xmodule_handler')) problem_check_url = result.data["handler_url"] + 'problem_check' submit_result = client.post(problem_check_url, data={problem_key: "choice_3"}) self.assertEqual(submit_result.status_code, 200) submit_data = json.loads(submit_result.content) self.assertDictContainsSubset( { "current_score": 0, "total_possible": 1, "attempts_used": 1, }, submit_data) # Now test that the score is also persisted in StudentModule: # If we add a REST API to get an individual block's score, that should be checked instead of StudentModule. sm = get_score(self.student_a, block_id) self.assertEqual(sm.grade, 0) self.assertEqual(sm.max_grade, 1) # And submit a correct answer: submit_result = client.post(problem_check_url, data={problem_key: "choice_1"}) self.assertEqual(submit_result.status_code, 200) submit_data = json.loads(submit_result.content) self.assertDictContainsSubset( { "current_score": 1, "total_possible": 1, "attempts_used": 2, }, submit_data) # Now test that the score is also updated in StudentModule: # If we add a REST API to get an individual block's score, that should be checked instead of StudentModule. sm = get_score(self.student_a, block_id) self.assertEqual(sm.grade, 1) self.assertEqual(sm.max_grade, 1)