def clear_student_state(self, user_id, course_id, item_id, requesting_user_id): """ This xblock method is called (from our LMS runtime, which defines this method signature) to clear student state for a given problem. It will cancel the workflow using traditional methods to remove it from the grading pools, and pass through to the submissions API to orphan the submission so that the user can create a new one. """ # Import is placed here to avoid model import at project startup. from submissions import api as submission_api # Note that student_item cannot be constructed using get_student_item_dict, since we're in a staff context student_item = { 'course_id': course_id, 'student_id': user_id, 'item_id': item_id, 'item_type': 'openassessment', } # There *should* only be one submission, but the logic is easy to extend for multiples so we may as well do it submissions = submission_api.get_submissions(student_item) for sub in submissions: # Remove the submission from grading pools self._cancel_workflow(sub['uuid'], "Student state cleared", requesting_user_id=requesting_user_id) # Tell the submissions API to orphan the submission to prevent it from being accessed submission_api.reset_score( user_id, course_id, item_id, clear_state=True )
def clear_student_state(self, user_id, course_id, item_id): """ This xblock method is called (from our LMS runtime, which defines this method signature) to clear student state for a given problem. It will cancel the workflow using traditional methods to remove it from the grading pools, and pass through to the submissions API to orphan the submission so that the user can create a new one. """ # Note that student_item cannot be constructed using get_student_item_dict, since we're in a staff context student_item = { 'course_id': course_id, 'student_id': user_id, 'item_id': item_id, 'item_type': 'openassessment', } # There *should* only be one submission, but the logic is easy to extend for multiples so we may as well do it submissions = submission_api.get_submissions(student_item) for sub in submissions: # Remove the submission from grading pools self._cancel_workflow(sub['uuid'], "Student state cleared") # Tell the submissions API to orphan the submission to prevent it from being accessed submission_api.reset_score( user_id, course_id, item_id, clear_state=True # pylint: disable=unexpected-keyword-arg )
def set_staff_score(self, score, reason=None): """ Set a staff score for the workflow. Allows for staff scores to be set on a submission, with annotations to provide an audit trail if needed. This method can be used for both required staff grading, and staff overrides. Args: score (dict): A dict containing 'points_earned', 'points_possible', and 'staff_id'. is_override (bool): Optionally True if staff is overriding a previous score. reason (string): An optional parameter specifying the reason for the staff grade. A default value will be used in the event that this parameter is not provided. """ if reason is None: reason = "A staff member has defined the score for this submission" sub_dict = sub_api.get_submission_and_student(self.submission_uuid) sub_api.reset_score( sub_dict['student_item']['student_id'], self.course_id, self.item_id, emit_signal=False ) sub_api.set_score( self.submission_uuid, score["points_earned"], score["points_possible"], annotation_creator=score["staff_id"], annotation_type=self.STAFF_ANNOTATION_TYPE, annotation_reason=reason )
def remove_grade(self, request, suffix=''): # pylint: disable=unused-argument """ Reset a students score request by staff. """ require(self.is_course_staff()) student_id = request.params['student_id'] submissions_api.reset_score(student_id, self.course_id, self.block_id) module = StudentModule.objects.get(pk=request.params['module_id']) state = json.loads(module.state) state['staff_score'] = None state['comment'] = '' state['annotated_sha1'] = None state['annotated_filename'] = None state['annotated_mimetype'] = None state['annotated_timestamp'] = None module.state = json.dumps(state) module.save() log.info( "remove_grade for course:%s module:%s student:%s", module.course_id, module.module_state_key, module.student.username ) return Response(json_body=self.staff_grading_data())
def remove_grade(self, request, suffix=''): # pylint: disable=unused-argument """ Reset a students score request by staff. """ require(self.is_course_staff()) student_id = request.params['student_id'] submissions_api.reset_score(student_id, unicode(self.course_id), unicode(self.block_id)) module = StudentModule.objects.get(pk=request.params['module_id']) state = json.loads(module.state) state['staff_score'] = None state['comment'] = '' state['annotated_sha1'] = None state['annotated_filename'] = None state['annotated_mimetype'] = None state['annotated_timestamp'] = None module.state = json.dumps(state) module.save() log.info( "remove_grade for course:%s module:%s student:%s", module.course_id, module.module_state_key, module.student.username ) return Response(json_body=self.staff_grading_data())
def test_reset_different_student_item(self, changed): # Create a submissions for two students submission = sub_api.create_submission(self.STUDENT_ITEM, 'test answer') sub_api.set_score(submission['uuid'], 1, 2) other_student = copy.copy(self.STUDENT_ITEM) other_student.update(changed) submission = sub_api.create_submission(other_student, 'other test answer') sub_api.set_score(submission['uuid'], 3, 4) # Reset the score for the first student sub_api.reset_score( self.STUDENT_ITEM['student_id'], self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['item_id'], ) # The first student's scores should be reset self.assertIs(sub_api.get_score(self.STUDENT_ITEM), None) scores = sub_api.get_scores(self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['student_id']) self.assertNotIn(self.STUDENT_ITEM['item_id'], scores) # But the second student should still have a score score = sub_api.get_score(other_student) self.assertEqual(score['points_earned'], 3) self.assertEqual(score['points_possible'], 4) scores = sub_api.get_scores(other_student['course_id'], other_student['student_id']) self.assertIn(other_student['item_id'], scores)
def clear_student_state(self, *args, **kwargs): # pylint: disable=unused-argument """ For a given user, clears submissions and uploaded files for this XBlock. Staff users are able to delete a learner's state for a block in LMS. When that capability is used, the block's "clear_student_state" function is called if it exists. """ student_id = kwargs['user_id'] for submission in submissions_api.get_submissions( self.get_student_item_dict(student_id) ): submission_file_sha1 = submission['answer'].get('sha1') submission_filename = submission['answer'].get('filename', None) if submission_filtename: submission_file_path = self.file_storage_path(submission_file_sha1, submission_filename) if default_storage.exists(submission_file_path): default_storage.delete(submission_file_path) submissions_api.reset_score( student_id, self.block_course_id, self.block_id, clear_state=True )
def test_reset_different_student_item(self, changed): # Create a submissions for two students submission = sub_api.create_submission(self.STUDENT_ITEM, "test answer") sub_api.set_score(submission["uuid"], 1, 2) other_student = copy.copy(self.STUDENT_ITEM) other_student.update(changed) submission = sub_api.create_submission(other_student, "other test answer") sub_api.set_score(submission["uuid"], 3, 4) # Reset the score for the first student sub_api.reset_score( self.STUDENT_ITEM["student_id"], self.STUDENT_ITEM["course_id"], self.STUDENT_ITEM["item_id"] ) # The first student's scores should be reset self.assertIs(sub_api.get_score(self.STUDENT_ITEM), None) scores = sub_api.get_scores(self.STUDENT_ITEM["course_id"], self.STUDENT_ITEM["student_id"]) self.assertNotIn(self.STUDENT_ITEM["item_id"], scores) # But the second student should still have a score score = sub_api.get_score(other_student) self.assertEqual(score["points_earned"], 3) self.assertEqual(score["points_possible"], 4) scores = sub_api.get_scores(other_student["course_id"], other_student["student_id"]) self.assertIn(other_student["item_id"], scores)
def test_reset_then_add_score(self): # Create a submission for the student and score it submission = sub_api.create_submission(self.STUDENT_ITEM, 'test answer') sub_api.set_score(submission['uuid'], 1, 2) # Reset scores sub_api.reset_score( self.STUDENT_ITEM['student_id'], self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['item_id'], ) # Score the student again sub_api.set_score(submission['uuid'], 3, 4) # Expect that the new score is available score = sub_api.get_score(self.STUDENT_ITEM) self.assertEqual(score['points_earned'], 3) self.assertEqual(score['points_possible'], 4) scores = sub_api.get_scores(self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['student_id']) self.assertIn(self.STUDENT_ITEM['item_id'], scores) self.assertEqual(scores[self.STUDENT_ITEM['item_id']], (3, 4))
def test_reset_with_no_scores(self): sub_api.reset_score( self.STUDENT_ITEM["student_id"], self.STUDENT_ITEM["course_id"], self.STUDENT_ITEM["item_id"] ) self.assertIs(sub_api.get_score(self.STUDENT_ITEM), None) scores = sub_api.get_scores(self.STUDENT_ITEM["course_id"], self.STUDENT_ITEM["student_id"]) self.assertEqual(len(scores), 0)
def reset_student_attempts(course_id, student, module_state_key, delete_module=False): """ Reset student attempts for a problem. Optionally deletes all student state for the specified problem. In the previous instructor dashboard it was possible to modify/delete modules that were not problems. That has been disabled for safety. `student` is a User `problem_to_reset` is the name of a problem e.g. 'L2Node1'. To build the module_state_key 'problem/' and course information will be appended to `problem_to_reset`. Raises: ValueError: `problem_state` is invalid JSON. StudentModule.DoesNotExist: could not load the student module. submissions.SubmissionError: unexpected error occurred while resetting the score in the submissions API. """ try: # A block may have children. Clear state on children first. block = modulestore().get_item(module_state_key) if block.has_children: for child in block.children: try: reset_student_attempts(course_id, student, child, delete_module=delete_module) except StudentModule.DoesNotExist: # If a particular child doesn't have any state, no big deal, as long as the parent does. pass except ItemNotFoundError: log.warning( "Could not find %s in modulestore when attempting to reset attempts.", module_state_key) # Reset the student's score in the submissions API # Currently this is used only by open assessment (ORA 2) # We need to do this *before* retrieving the `StudentModule` model, # because it's possible for a score to exist even if no student module exists. if delete_module: sub_api.reset_score( anonymous_id_for_user(student, course_id), course_id.to_deprecated_string(), module_state_key.to_deprecated_string(), ) module_to_reset = StudentModule.objects.get( student_id=student.id, course_id=course_id, module_state_key=module_state_key) if delete_module: module_to_reset.delete() else: _reset_module_attempts(module_to_reset)
def test_database_error(self, create_mock): # Create a submission for the student and score it submission = sub_api.create_submission(self.STUDENT_ITEM, "test answer") sub_api.set_score(submission["uuid"], 1, 2) # Simulate a database error when creating the reset score create_mock.side_effect = DatabaseError("Test error") with self.assertRaises(sub_api.SubmissionInternalError): sub_api.reset_score( self.STUDENT_ITEM["student_id"], self.STUDENT_ITEM["course_id"], self.STUDENT_ITEM["item_id"] )
def reset_student_attempts(course_id, student, module_state_key, delete_module=False): """ Reset student attempts for a problem. Optionally deletes all student state for the specified problem. In the previous instructor dashboard it was possible to modify/delete modules that were not problems. That has been disabled for safety. `student` is a User `problem_to_reset` is the name of a problem e.g. 'L2Node1'. To build the module_state_key 'problem/' and course information will be appended to `problem_to_reset`. Raises: ValueError: `problem_state` is invalid JSON. StudentModule.DoesNotExist: could not load the student module. submissions.SubmissionError: unexpected error occurred while resetting the score in the submissions API. """ user_id = anonymous_id_for_user(student, course_id) submission_cleared = False try: # A block may have children. Clear state on children first. block = modulestore().get_item(module_state_key) if block.has_children: for child in block.children: try: reset_student_attempts(course_id, student, child, delete_module=delete_module) except StudentModule.DoesNotExist: # If a particular child doesn't have any state, no big deal, as long as the parent does. pass if delete_module: # Some blocks (openassessment) use StudentModule data as a key for internal submission data. # Inform these blocks of the reset and allow them to handle their data. clear_student_state = getattr(block, "clear_student_state", None) if callable(clear_student_state): clear_student_state(user_id=user_id, course_id=unicode(course_id), item_id=unicode(module_state_key)) submission_cleared = True except ItemNotFoundError: log.warning("Could not find %s in modulestore when attempting to reset attempts.", module_state_key) # Reset the student's score in the submissions API, if xblock.clear_student_state has not done so already. # TODO: Remove this once we've finalized and communicated how xblocks should handle clear_student_state # and made sure that other xblocks relying on the submission api understand this is going away. # We need to do this before retrieving the `StudentModule` model, because a score may exist with no student module. if delete_module and not submission_cleared: sub_api.reset_score(user_id, course_id.to_deprecated_string(), module_state_key.to_deprecated_string()) module_to_reset = StudentModule.objects.get( student_id=student.id, course_id=course_id, module_state_key=module_state_key ) if delete_module: module_to_reset.delete() else: _reset_module_attempts(module_to_reset)
def test_reset_with_no_scores(self): sub_api.reset_score( self.STUDENT_ITEM['student_id'], self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['item_id'], ) self.assertIs(sub_api.get_score(self.STUDENT_ITEM), None) scores = sub_api.get_scores(self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['student_id']) self.assertEqual(len(scores), 0)
def test_reset_then_get_score_for_submission(self): # Create a submission for the student and score it submission = sub_api.create_submission(self.STUDENT_ITEM, "test answer") sub_api.set_score(submission["uuid"], 1, 2) # Reset scores sub_api.reset_score( self.STUDENT_ITEM["student_id"], self.STUDENT_ITEM["course_id"], self.STUDENT_ITEM["item_id"] ) # If we're retrieving the score for a particular submission, # instead of a student item, then we should STILL get a score. self.assertIsNot(sub_api.get_latest_score_for_submission(submission["uuid"]), None)
def reset_student_attempts(course_id, student, module_state_key, delete_module=False): """ Reset student attempts for a problem. Optionally deletes all student state for the specified problem. In the previous instructor dashboard it was possible to modify/delete modules that were not problems. That has been disabled for safety. `student` is a User `problem_to_reset` is the name of a problem e.g. 'L2Node1'. To build the module_state_key 'problem/' and course information will be appended to `problem_to_reset`. Raises: ValueError: `problem_state` is invalid JSON. StudentModule.DoesNotExist: could not load the student module. submissions.SubmissionError: unexpected error occurred while resetting the score in the submissions API. """ try: # A block may have children. Clear state on children first. block = modulestore().get_item(module_state_key) if block.has_children: for child in block.children: try: reset_student_attempts(course_id, student, child, delete_module=delete_module) except StudentModule.DoesNotExist: # If a particular child doesn't have any state, no big deal, as long as the parent does. pass except ItemNotFoundError: log.warning("Could not find %s in modulestore when attempting to reset attempts.", module_state_key) # Reset the student's score in the submissions API # Currently this is used only by open assessment (ORA 2) # We need to do this *before* retrieving the `StudentModule` model, # because it's possible for a score to exist even if no student module exists. if delete_module: sub_api.reset_score( anonymous_id_for_user(student, course_id), course_id.to_deprecated_string(), module_state_key.to_deprecated_string(), ) module_to_reset = StudentModule.objects.get( student_id=student.id, course_id=course_id, module_state_key=module_state_key ) if delete_module: module_to_reset.delete() else: _reset_module_attempts(module_to_reset)
def test_reset_with_one_score(self): # Create a submission for the student and score it submission = sub_api.create_submission(self.STUDENT_ITEM, "test answer") sub_api.set_score(submission["uuid"], 1, 2) # Reset scores sub_api.reset_score( self.STUDENT_ITEM["student_id"], self.STUDENT_ITEM["course_id"], self.STUDENT_ITEM["item_id"] ) # Expect that no scores are available for the student self.assertIs(sub_api.get_score(self.STUDENT_ITEM), None) scores = sub_api.get_scores(self.STUDENT_ITEM["course_id"], self.STUDENT_ITEM["student_id"]) self.assertEqual(len(scores), 0)
def test_database_error(self, create_mock): # Create a submission for the student and score it submission = sub_api.create_submission(self.STUDENT_ITEM, 'test answer') sub_api.set_score(submission['uuid'], 1, 2) # Simulate a database error when creating the reset score create_mock.side_effect = DatabaseError("Test error") with self.assertRaises(sub_api.SubmissionInternalError): sub_api.reset_score( self.STUDENT_ITEM['student_id'], self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['item_id'], )
def remove_grade(self, request, suffix=''): require(self.is_course_staff()) student_id = request.params['student_id'] submissions_api.reset_score(student_id, self.course_id, self.block_id) module = StudentModule.objects.get(pk=request.params['module_id']) state = json.loads(module.state) state['staff_score'] = None state['comment'] = '' state['annotated_sha1'] = None state['annotated_filename'] = None state['annotated_mimetype'] = None state['annotated_timestamp'] = None module.state = json.dumps(state) module.save() return Response(json_body=self.staff_grading_data())
def clear_student_state(self, *args, **kwargs): # pylint: disable=unused-argument """ For a given user, clears submissions and uploaded files for this XBlock. Staff users are able to delete a learner's state for a block in LMS. When that capability is used, the block's "clear_student_state" function is called if it exists. """ student_id = kwargs['user_id'] for submission in submissions_api.get_submissions( self.get_student_item_dict(student_id)): submissions_api.reset_score(student_id, self.block_course_id, self.block_id, clear_state=True)
def reset_scores(team_submission_uuid, clear_state=False): """ Reset scores for a specific team submission to a problem. Note: this does *not* delete `Score` models from the database, since these are immutable. It simply creates a new score with the "reset" flag set to True. Args: team_submission_uuid (str): The uuid for the team submission for which to reset scores. clear_state (bool): If True, soft delete the team submission and any individual submissions by setting their status to DELETED Returns: None Raises: TeamSubmissionInternalError: An unexpected error occurred while resetting scores. """ # Get the team submission try: team_submission = TeamSubmission.get_team_submission_by_uuid( team_submission_uuid) for submission in team_submission.submissions.select_related( 'student_item').all(): _api.reset_score( submission.student_item.student_id, submission.student_item.course_id, submission.student_item.item_id, clear_state=clear_state, ) if clear_state: # soft-delete the TeamSubmission team_submission.status = DELETED team_submission.save(update_fields=["status"]) except (DatabaseError, SubmissionInternalError) as error: msg = ( f"Error occurred while reseting scores for team submission {team_submission_uuid}" ) logger.exception(msg) raise TeamSubmissionInternalError(msg) from error else: logger.info("Score reset for team submission %(team_submission_uuid)s", { 'team_submission_uuid': team_submission_uuid, })
def test_reset_with_multiple_scores(self): # Create a submission for the student and score it submission = sub_api.create_submission(self.STUDENT_ITEM, 'test answer') sub_api.set_score(submission['uuid'], 1, 2) sub_api.set_score(submission['uuid'], 2, 2) # Reset scores sub_api.reset_score( self.STUDENT_ITEM['student_id'], self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['item_id'], ) # Expect that no scores are available for the student self.assertIs(sub_api.get_score(self.STUDENT_ITEM), None) scores = sub_api.get_scores(self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['student_id']) self.assertEqual(len(scores), 0)
def test_reset_then_get_score_for_submission(self): # Create a submission for the student and score it submission = sub_api.create_submission(self.STUDENT_ITEM, 'test answer') sub_api.set_score(submission['uuid'], 1, 2) # Reset scores sub_api.reset_score( self.STUDENT_ITEM['student_id'], self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['item_id'], ) # If we're retrieving the score for a particular submission, # instead of a student item, then we should STILL get a score. self.assertIsNot( sub_api.get_latest_score_for_submission(submission['uuid']), None)
def test_reset_with_one_score(self): # Create a submission for the student and score it submission = sub_api.create_submission(self.STUDENT_ITEM, 'test answer') sub_api.set_score(submission['uuid'], 1, 2) # Reset scores sub_api.reset_score( self.STUDENT_ITEM['student_id'], self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['item_id'], ) # Expect that no scores are available for the student self.assertIs(sub_api.get_score(self.STUDENT_ITEM), None) scores = sub_api.get_scores(self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['student_id']) self.assertEqual(len(scores), 0)
def test_reset_score_signal(self, send_mock): # Create a submission for the student and score it submission = sub_api.create_submission(self.STUDENT_ITEM, 'test answer') sub_api.set_score(submission['uuid'], 1, 2) # Reset scores sub_api.reset_score( self.STUDENT_ITEM['student_id'], self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['item_id'], ) # Verify that the send method was properly called send_mock.assert_called_with( sender=None, anonymous_user_id=self.STUDENT_ITEM['student_id'], course_id=self.STUDENT_ITEM['course_id'], item_id=self.STUDENT_ITEM['item_id'])
def test_reset_score_signal(self, send_mock): # Create a submission for the student and score it submission = sub_api.create_submission(self.STUDENT_ITEM, 'test answer') sub_api.set_score(submission['uuid'], 1, 2) # Reset scores sub_api.reset_score( self.STUDENT_ITEM['student_id'], self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['item_id'], ) # Verify that the send method was properly called send_mock.assert_called_with( sender = None, anonymous_user_id=self.STUDENT_ITEM['student_id'], course_id=self.STUDENT_ITEM['course_id'], item_id=self.STUDENT_ITEM['item_id'] )
def test_override_after_reset_score(self): # Create a submission for the student and score it submission = sub_api.create_submission(self.STUDENT_ITEM, 'test answer') sub_api.set_score(submission['uuid'], 1, 10) # Reset score sub_api.reset_score( self.STUDENT_ITEM['student_id'], self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['item_id'], ) sub_api.score_override( self.STUDENT_ITEM, 5, 10, ) self.assertEqual(sub_api.get_score(self.STUDENT_ITEM)['points_earned'], 5) self.assertEqual(sub_api.get_score(self.STUDENT_ITEM)['points_possible'], 10)
def test_clear_state(self): # Create a submission, give it a score, and verify that score exists submission = api.create_submission(STUDENT_ITEM, ANSWER_ONE) api.set_score(submission["uuid"], 11, 12) score = api.get_score(STUDENT_ITEM) self._assert_score(score, 11, 12) self.assertEqual(score['submission_uuid'], submission['uuid']) # Reset the score with clear_state=True # This should set the submission's score to None, and make it unavailable to get_submissions api.reset_score( STUDENT_ITEM["student_id"], STUDENT_ITEM["course_id"], STUDENT_ITEM["item_id"], clear_state=True, ) score = api.get_score(STUDENT_ITEM) self.assertIsNone(score) subs = api.get_submissions(STUDENT_ITEM) self.assertEqual(subs, [])
def test_reset_then_add_score(self): # Create a submission for the student and score it submission = sub_api.create_submission(self.STUDENT_ITEM, "test answer") sub_api.set_score(submission["uuid"], 1, 2) # Reset scores sub_api.reset_score( self.STUDENT_ITEM["student_id"], self.STUDENT_ITEM["course_id"], self.STUDENT_ITEM["item_id"] ) # Score the student again sub_api.set_score(submission["uuid"], 3, 4) # Expect that the new score is available score = sub_api.get_score(self.STUDENT_ITEM) self.assertEqual(score["points_earned"], 3) self.assertEqual(score["points_possible"], 4) scores = sub_api.get_scores(self.STUDENT_ITEM["course_id"], self.STUDENT_ITEM["student_id"]) self.assertIn(self.STUDENT_ITEM["item_id"], scores) self.assertEqual(scores[self.STUDENT_ITEM["item_id"]], (3, 4))
def reset_student_attempts(course_id, student, module_state_key, delete_module=False): """ Reset student attempts for a problem. Optionally deletes all student state for the specified problem. In the previous instructor dashboard it was possible to modify/delete modules that were not problems. That has been disabled for safety. `student` is a User `problem_to_reset` is the name of a problem e.g. 'L2Node1'. To build the module_state_key 'problem/' and course information will be appended to `problem_to_reset`. Raises: ValueError: `problem_state` is invalid JSON. StudentModule.DoesNotExist: could not load the student module. submissions.SubmissionError: unexpected error occurred while resetting the score in the submissions API. """ # Reset the student's score in the submissions API # Currently this is used only by open assessment (ORA 2) # We need to do this *before* retrieving the `StudentModule` model, # because it's possible for a score to exist even if no student module exists. if delete_module: sub_api.reset_score( anonymous_id_for_user(student, course_id), course_id.to_deprecated_string(), module_state_key.to_deprecated_string(), ) module_to_reset = StudentModule.objects.get( student_id=student.id, course_id=course_id, module_state_key=module_state_key) if delete_module: module_to_reset.delete() else: _reset_module_attempts(module_to_reset)
def reset_student_attempts(course_id, student, module_state_key, delete_module=False): """ Reset student attempts for a problem. Optionally deletes all student state for the specified problem. In the previous instructor dashboard it was possible to modify/delete modules that were not problems. That has been disabled for safety. `student` is a User `problem_to_reset` is the name of a problem e.g. 'L2Node1'. To build the module_state_key 'problem/' and course information will be appended to `problem_to_reset`. Raises: ValueError: `problem_state` is invalid JSON. StudentModule.DoesNotExist: could not load the student module. submissions.SubmissionError: unexpected error occurred while resetting the score in the submissions API. """ # Reset the student's score in the submissions API # Currently this is used only by open assessment (ORA 2) # We need to do this *before* retrieving the `StudentModule` model, # because it's possible for a score to exist even if no student module exists. if delete_module: sub_api.reset_score( anonymous_id_for_user(student, course_id), course_id.to_deprecated_string(), module_state_key.to_deprecated_string(), ) module_to_reset = StudentModule.objects.get( student_id=student.id, course_id=course_id, module_state_key=module_state_key ) if delete_module: module_to_reset.delete() else: _reset_module_attempts(module_to_reset)
def test_override_after_reset_score(self): # Create a submission for the student and score it submission = sub_api.create_submission(self.STUDENT_ITEM, 'test answer') sub_api.set_score(submission['uuid'], 1, 10) # Reset score sub_api.reset_score( self.STUDENT_ITEM['student_id'], self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['item_id'], ) sub_api.score_override( self.STUDENT_ITEM, 5, 10, ) self.assertEqual( sub_api.get_score(self.STUDENT_ITEM)['points_earned'], 5) self.assertEqual( sub_api.get_score(self.STUDENT_ITEM)['points_possible'], 10)
def clear_student_state(self, *args, **kwargs): # pylint: disable=unused-argument """ For a given user, clears submissions and uploaded files for this XBlock. Staff users are able to delete a learner's state for a block in LMS. When that capability is used, the block's "clear_student_state" function is called if it exists. """ student_id = kwargs['user_id'] for submission in submissions_api.get_submissions( self.get_student_item_dict(student_id) ): submission_file_sha1 = submission['answer'].get('sha1') submission_filename = submission['answer'].get('filename') submission_file_path = self.file_storage_path(submission_file_sha1, submission_filename) if default_storage.exists(submission_file_path): default_storage.delete(submission_file_path) submissions_api.reset_score( student_id, self.block_course_id, self.block_id, clear_state=True )
def test_reset_then_add_score(self): # Create a submission for the student and score it submission = sub_api.create_submission(self.STUDENT_ITEM, 'test answer') sub_api.set_score(submission['uuid'], 1, 2) # Reset scores sub_api.reset_score( self.STUDENT_ITEM['student_id'], self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['item_id'], ) # Score the student again sub_api.set_score(submission['uuid'], 3, 4) # Expect that the new score is available score = sub_api.get_score(self.STUDENT_ITEM) self.assertEqual(score['points_earned'], 3) self.assertEqual(score['points_possible'], 4) scores = sub_api.get_scores(self.STUDENT_ITEM['course_id'], self.STUDENT_ITEM['student_id']) self.assertIn(self.STUDENT_ITEM['item_id'], scores) item_score = scores[self.STUDENT_ITEM['item_id']] self.assertEqual((item_score['points_earned'], item_score['points_possible']), (3, 4))
def reset_student_attempts(course_id, student, module_state_key, requesting_user, delete_module=False): """ Reset student attempts for a problem. Optionally deletes all student state for the specified problem. In the previous instructor dashboard it was possible to modify/delete modules that were not problems. That has been disabled for safety. `student` is a User `problem_to_reset` is the name of a problem e.g. 'L2Node1'. To build the module_state_key 'problem/' and course information will be appended to `problem_to_reset`. Raises: ValueError: `problem_state` is invalid JSON. StudentModule.DoesNotExist: could not load the student module. submissions.SubmissionError: unexpected error occurred while resetting the score in the submissions API. """ user_id = anonymous_id_for_user(student, course_id) requesting_user_id = anonymous_id_for_user(requesting_user, course_id) submission_cleared = False teams_enabled = False selected_teamset_id = None try: # A block may have children. Clear state on children first. block = modulestore().get_item(module_state_key) if block.has_children: for child in block.children: try: reset_student_attempts(course_id, student, child, requesting_user, delete_module=delete_module) except StudentModule.DoesNotExist: # If a particular child doesn't have any state, no big deal, as long as the parent does. pass if delete_module: # Some blocks (openassessment) use StudentModule data as a key for internal submission data. # Inform these blocks of the reset and allow them to handle their data. clear_student_state = getattr(block, "clear_student_state", None) if callable(clear_student_state): with disconnect_submissions_signal_receiver(score_set): clear_student_state( user_id=user_id, course_id=six.text_type(course_id), item_id=six.text_type(module_state_key), requesting_user_id=requesting_user_id) submission_cleared = True teams_enabled = getattr(block, 'teams_enabled', False) if teams_enabled: selected_teamset_id = getattr(block, 'selected_teamset_id', None) except ItemNotFoundError: block = None log.warning( u"Could not find %s in modulestore when attempting to reset attempts.", module_state_key) # Reset the student's score in the submissions API, if xblock.clear_student_state has not done so already. # We need to do this before retrieving the `StudentModule` model, because a score may exist with no student module. # TODO: Should the LMS know about sub_api and call this reset, or should it generically call it on all of its # xblock services as well? See JIRA ARCH-26. if delete_module and not submission_cleared: sub_api.reset_score( user_id, text_type(course_id), text_type(module_state_key), ) def _reset_or_delete_module(studentmodule): if delete_module: studentmodule.delete() create_new_event_transaction_id() set_event_transaction_type(grades_events.STATE_DELETED_EVENT_TYPE) tracker.emit( six.text_type(grades_events.STATE_DELETED_EVENT_TYPE), { 'user_id': six.text_type(student.id), 'course_id': six.text_type(course_id), 'problem_id': six.text_type(module_state_key), 'instructor_id': six.text_type(requesting_user.id), 'event_transaction_id': six.text_type(get_event_transaction_id()), 'event_transaction_type': six.text_type(grades_events.STATE_DELETED_EVENT_TYPE), }) if not submission_cleared: _fire_score_changed_for_block( course_id, student, block, module_state_key, ) else: _reset_module_attempts(studentmodule) team = None if teams_enabled: from lms.djangoapps.teams.api import get_team_for_user_course_topic team = get_team_for_user_course_topic(student, str(course_id), selected_teamset_id) if team: modules_to_reset = StudentModule.objects.filter( student__teams=team, course_id=course_id, module_state_key=module_state_key) for module_to_reset in modules_to_reset: _reset_or_delete_module(module_to_reset) return else: # Teams are not enabled or the user does not have a team module_to_reset = StudentModule.objects.get( student_id=student.id, course_id=course_id, module_state_key=module_state_key) _reset_or_delete_module(module_to_reset)
def reset_student_attempts(course_id, student, module_state_key, requesting_user, delete_module=False): """ Reset student attempts for a problem. Optionally deletes all student state for the specified problem. In the previous instructor dashboard it was possible to modify/delete modules that were not problems. That has been disabled for safety. `student` is a User `problem_to_reset` is the name of a problem e.g. 'L2Node1'. To build the module_state_key 'problem/' and course information will be appended to `problem_to_reset`. Raises: ValueError: `problem_state` is invalid JSON. StudentModule.DoesNotExist: could not load the student module. submissions.SubmissionError: unexpected error occurred while resetting the score in the submissions API. """ user_id = anonymous_id_for_user(student, course_id) requesting_user_id = anonymous_id_for_user(requesting_user, course_id) submission_cleared = False try: # A block may have children. Clear state on children first. block = modulestore().get_item(module_state_key) if block.has_children: for child in block.children: try: reset_student_attempts(course_id, student, child, requesting_user, delete_module=delete_module) except StudentModule.DoesNotExist: # If a particular child doesn't have any state, no big deal, as long as the parent does. pass if delete_module: # Some blocks (openassessment) use StudentModule data as a key for internal submission data. # Inform these blocks of the reset and allow them to handle their data. clear_student_state = getattr(block, "clear_student_state", None) if callable(clear_student_state): with disconnect_submissions_signal_receiver(score_set): clear_student_state( user_id=user_id, course_id=unicode(course_id), item_id=unicode(module_state_key), requesting_user_id=requesting_user_id ) submission_cleared = True except ItemNotFoundError: block = None log.warning("Could not find %s in modulestore when attempting to reset attempts.", module_state_key) # Reset the student's score in the submissions API, if xblock.clear_student_state has not done so already. # We need to do this before retrieving the `StudentModule` model, because a score may exist with no student module. # TODO: Should the LMS know about sub_api and call this reset, or should it generically call it on all of its # xblock services as well? See JIRA ARCH-26. if delete_module and not submission_cleared: sub_api.reset_score( user_id, course_id.to_deprecated_string(), module_state_key.to_deprecated_string(), ) module_to_reset = StudentModule.objects.get( student_id=student.id, course_id=course_id, module_state_key=module_state_key ) if delete_module: module_to_reset.delete() create_new_event_transaction_id() grade_update_root_type = 'edx.grades.problem.state_deleted' set_event_transaction_type(grade_update_root_type) tracker.emit( unicode(grade_update_root_type), { 'user_id': unicode(student.id), 'course_id': unicode(course_id), 'problem_id': unicode(module_state_key), 'instructor_id': unicode(requesting_user.id), 'event_transaction_id': unicode(get_event_transaction_id()), 'event_transaction_type': unicode(grade_update_root_type), } ) if not submission_cleared: _fire_score_changed_for_block( course_id, student, block, module_state_key, ) else: _reset_module_attempts(module_to_reset)
def reset_student_attempts(course_id, student, module_state_key, requesting_user, delete_module=False): """ Reset student attempts for a problem. Optionally deletes all student state for the specified problem. In the previous instructor dashboard it was possible to modify/delete modules that were not problems. That has been disabled for safety. `student` is a User `problem_to_reset` is the name of a problem e.g. 'L2Node1'. To build the module_state_key 'problem/' and course information will be appended to `problem_to_reset`. Raises: ValueError: `problem_state` is invalid JSON. StudentModule.DoesNotExist: could not load the student module. submissions.SubmissionError: unexpected error occurred while resetting the score in the submissions API. """ user_id = anonymous_id_for_user(student, course_id) requesting_user_id = anonymous_id_for_user(requesting_user, course_id) submission_cleared = False try: # A block may have children. Clear state on children first. block = modulestore().get_item(module_state_key) if block.has_children: for child in block.children: try: reset_student_attempts(course_id, student, child, requesting_user, delete_module=delete_module) except StudentModule.DoesNotExist: # If a particular child doesn't have any state, no big deal, as long as the parent does. pass if delete_module: # Some blocks (openassessment) use StudentModule data as a key for internal submission data. # Inform these blocks of the reset and allow them to handle their data. clear_student_state = getattr(block, "clear_student_state", None) if callable(clear_student_state): clear_student_state( user_id=user_id, course_id=unicode(course_id), item_id=unicode(module_state_key), requesting_user_id=requesting_user_id ) submission_cleared = True except ItemNotFoundError: block = None log.warning("Could not find %s in modulestore when attempting to reset attempts.", module_state_key) # Reset the student's score in the submissions API, if xblock.clear_student_state has not done so already. # We need to do this before retrieving the `StudentModule` model, because a score may exist with no student module. # TODO: Should the LMS know about sub_api and call this reset, or should it generically call it on all of its # xblock services as well? See JIRA ARCH-26. if delete_module and not submission_cleared: sub_api.reset_score( user_id, course_id.to_deprecated_string(), module_state_key.to_deprecated_string(), ) module_to_reset = StudentModule.objects.get( student_id=student.id, course_id=course_id, module_state_key=module_state_key ) if delete_module: module_to_reset.delete() _fire_score_changed_for_block(course_id, student, block, module_state_key) else: _reset_module_attempts(module_to_reset)