def test_score_annotations(self, annotation_types): """ Ensure that annotation types are returned with serialized scores. """ annotation_kwargs = { 'creator': 'test_annotator', 'reason': 'tests for the test god' } for test_type in annotation_types: ScoreAnnotation.objects.create( score=self.score, annotation_type=test_type, **annotation_kwargs ) score_dict = ScoreSerializer(self.score).data self.assertEqual( score_dict['annotations'], [ { 'reason': annotation_kwargs['reason'], 'annotation_type': annotation_type, 'creator': annotation_kwargs['creator'], } for annotation_type in annotation_types ] )
def get_course_item_submissions(course_id, item_id, item_type, read_replica=True): submission_qs = Submission.objects if read_replica: submission_qs = _use_read_replica(submission_qs) query = submission_qs.select_related( 'student_item__scoresummary__latest__submission').filter( student_item__course_id=course_id, student_item__item_type=item_type, student_item__item_id=item_id, ).iterator() for submission in query: student_item = submission.student_item serialized_score = {} if hasattr(student_item, 'scoresummary'): latest_score = student_item.scoresummary.latest if (not latest_score.is_hidden() ) and latest_score.submission.uuid == submission.uuid: serialized_score = ScoreSerializer(latest_score).data yield (StudentItemSerializer(student_item).data, SubmissionSerializer(submission).data, serialized_score)
def get_latest_score_for_submission(submission_uuid, read_replica=False): """ Retrieve the latest score for a particular submission. Args: submission_uuid (str): The UUID of the submission to retrieve. Kwargs: read_replica (bool): If true, attempt to use the read replica database. If no read replica is available, use the default database. Returns: dict: The serialized score model, or None if no score is available. """ try: # Ensure that submission_uuid is valid before fetching score submission_model = _get_submission_model(submission_uuid, read_replica) score_qs = Score.objects.filter( submission__uuid=submission_model.uuid).order_by( "-id").select_related("submission") if read_replica: score_qs = _use_read_replica(score_qs) score = score_qs[0] if score.is_hidden(): return None except (IndexError, Submission.DoesNotExist): return None return ScoreSerializer(score).data
def get_score_override(student_item): """ Get latest override score for peer assessment question. Args: student_item (dict): The dictionary representation of a student item. Returns: The latest override score if there is one, otherwise None. """ try: student_item_model = StudentItem.objects.get(**student_item) except (TypeError, StudentItem.DoesNotExist): return None score = Score.objects.filter( student_item=student_item_model, submission=None, reset=False, ) if score: return ScoreSerializer(score.latest('created_at')).data else: return None
def get_all_course_submission_information(course_id, item_type, read_replica=True): """ For the given course, get all student items of the given item type, all the submissions for those itemes, and the latest scores for each item. If a submission was given a score that is not the latest score for the relevant student item, it will still be included but without score. Args: course_id (str): The course that we are getting submissions from. item_type (str): The type of items that we are getting submissions for. read_replica (bool): Try to use the database's read replica if it's available. Yields: A tuple of three dictionaries representing: (1) a student item with the following fields: student_id course_id student_item item_type (2) a submission with the following fields: student_item attempt_number submitted_at created_at answer (3) a score with the following fields, if one exists and it is the latest score: (if both conditions are not met, an empty dict is returned here) student_item submission points_earned points_possible created_at submission_uuid """ submission_qs = Submission.objects if read_replica: submission_qs = _use_read_replica(submission_qs) query = submission_qs.select_related( 'student_item__scoresummary__latest__submission').filter( student_item__course_id=course_id, student_item__item_type=item_type, ).iterator() for submission in query: student_item = submission.student_item serialized_score = {} if hasattr(student_item, 'scoresummary'): latest_score = student_item.scoresummary.latest # Only include the score if it is not a reset score (is_hidden), and if the current submission is the same # as the student_item's latest score's submission. This matches the behavior of the API's get_score method. if (not latest_score.is_hidden() ) and latest_score.submission.uuid == submission.uuid: serialized_score = ScoreSerializer(latest_score).data yield (StudentItemSerializer(student_item).data, SubmissionSerializer(submission).data, serialized_score)
def test_score_with_null_submission(self): # Create a score with a null submission null_sub_score = Score.objects.create( student_item=self.item, submission=None, points_earned=3, points_possible=8, ) null_sub_score_dict = ScoreSerializer(null_sub_score).data self.assertIs(null_sub_score_dict['submission_uuid'], None) self.assertEqual(null_sub_score_dict['points_earned'], 3) self.assertEqual(null_sub_score_dict['points_possible'], 8)
def get_score(student_item): """Get the score for a particular student item Each student item should have a unique score. This function will return the score if it is available. A score is only calculated for a student item if it has completed the workflow for a particular assessment module. Args: student_item (dict): The dictionary representation of a student item. Function returns the score related to this student item. Returns: score (dict): The score associated with this student item. None if there is no score found. Raises: SubmissionInternalError: Raised if a score cannot be retrieved because of an internal server error. Examples: >>> student_item = { >>> "student_id":"Tim", >>> "course_id":"TestCourse", >>> "item_id":"u_67", >>> "item_type":"openassessment" >>> } >>> >>> get_score(student_item) [{ 'student_item': 2, 'submission': 2, 'points_earned': 8, 'points_possible': 20, 'created_at': datetime.datetime(2014, 2, 7, 18, 30, 1, 807911, tzinfo=<UTC>) }] """ try: student_item_model = StudentItem.objects.get(**student_item) score = ScoreSummary.objects.get( student_item=student_item_model).latest except (ScoreSummary.DoesNotExist, StudentItem.DoesNotExist): return None # By convention, scores are hidden if "points possible" is set to 0. # This can occur when an instructor has reset scores for a student. if score.is_hidden(): return None else: return ScoreSerializer(score).data
def test_score_with_null_submission(self): item = StudentItem.objects.create( student_id="score_test_student", course_id="score_test_course", item_id="i4x://mycourse/special_presentation") # Create a score with a null submission score = Score.objects.create(student_item=item, submission=None, points_earned=2, points_possible=6) score_dict = ScoreSerializer(score).data self.assertIs(score_dict['submission_uuid'], None) self.assertEqual(score_dict['points_earned'], 2) self.assertEqual(score_dict['points_possible'], 6)
def get_latest_score_for_submission(submission_uuid): """ Retrieve the latest score for a particular submission. Args: submission_uuid (str): The UUID of the submission to retrieve. Returns: dict: The serialized score model, or None if no score is available. """ try: score = Score.objects.filter( submission__uuid=submission_uuid).order_by("-id").select_related( "submission")[0] if score.is_hidden(): return None except IndexError: return None return ScoreSerializer(score).data
def set_score(submission_uuid, points_earned, points_possible, annotation_creator=None, annotation_type=None, annotation_reason=None): """Set a score for a particular submission. Sets the score for a particular submission. This score is calculated externally to the API. Args: submission_uuid (str): UUID for the submission (must exist). points_earned (int): The earned points for this submission. points_possible (int): The total points possible for this particular student item. annotation_creator (str): An optional field for recording who gave this particular score annotation_type (str): An optional field for recording what type of annotation should be created, e.g. "staff_override". annotation_reason (str): An optional field for recording why this score was set to its value. Returns: None Raises: SubmissionInternalError: Thrown if there was an internal error while attempting to save the score. SubmissionRequestError: Thrown if the given student item or submission are not found. Examples: >>> set_score("a778b933-9fb3-11e3-9c0f-040ccee02800", 11, 12) { 'student_item': 2, 'submission': 1, 'points_earned': 11, 'points_possible': 12, 'created_at': datetime.datetime(2014, 2, 7, 20, 6, 42, 331156, tzinfo=<UTC>) } """ try: submission_model = _get_submission_model(submission_uuid) except Submission.DoesNotExist as error: raise SubmissionNotFoundError( f"No submission matching uuid {submission_uuid}") from error except DatabaseError as error: error_msg = "Could not retrieve submission {}.".format(submission_uuid) logger.exception(error_msg) raise SubmissionRequestError(msg=error_msg) from error score = ScoreSerializer( data={ "student_item": submission_model.student_item.pk, "submission": submission_model.pk, "points_earned": points_earned, "points_possible": points_possible, }) if not score.is_valid(): logger.exception(score.errors) raise SubmissionInternalError(score.errors) # When we save the score, a score summary will be created if # it does not already exist. # When the database's isolation level is set to repeatable-read, # it's possible for a score summary to exist for this student item, # even though we cannot retrieve it. # In this case, we assume that someone else has already created # a score summary and ignore the error. try: with transaction.atomic(): score_model = score.save() _log_score(score_model) if annotation_creator is not None: score_annotation = ScoreAnnotation( score=score_model, creator=annotation_creator, annotation_type=annotation_type, reason=annotation_reason) score_annotation.save() # Send a signal out to any listeners who are waiting for scoring events. score_set.send( sender=None, points_possible=points_possible, points_earned=points_earned, anonymous_user_id=submission_model.student_item.student_id, course_id=submission_model.student_item.course_id, item_id=submission_model.student_item.item_id, created_at=score_model.created_at, ) except IntegrityError: pass
def set_score(submission_uuid, points_earned, points_possible): """Set a score for a particular submission. Sets the score for a particular submission. This score is calculated externally to the API. Args: submission_uuid (str): UUID for the submission (must exist). points_earned (int): The earned points for this submission. points_possible (int): The total points possible for this particular student item. Returns: None Raises: SubmissionInternalError: Thrown if there was an internal error while attempting to save the score. SubmissionRequestError: Thrown if the given student item or submission are not found. Examples: >>> set_score("a778b933-9fb3-11e3-9c0f-040ccee02800", 11, 12) { 'student_item': 2, 'submission': 1, 'points_earned': 11, 'points_possible': 12, 'created_at': datetime.datetime(2014, 2, 7, 20, 6, 42, 331156, tzinfo=<UTC>) } """ try: submission_model = Submission.objects.get(uuid=submission_uuid) except Submission.DoesNotExist: raise SubmissionNotFoundError( u"No submission matching uuid {}".format(submission_uuid) ) except DatabaseError: error_msg = u"Could not retrieve student item: {} or submission {}.".format( submission_uuid ) logger.exception(error_msg) raise SubmissionRequestError(msg=error_msg) score = ScoreSerializer( data={ "student_item": submission_model.student_item.pk, "submission": submission_model.pk, "points_earned": points_earned, "points_possible": points_possible, } ) if not score.is_valid(): logger.exception(score.errors) raise SubmissionInternalError(score.errors) # When we save the score, a score summary will be created if # it does not already exist. # When the database's isolation level is set to repeatable-read, # it's possible for a score summary to exist for this student item, # even though we cannot retrieve it. # In this case, we assume that someone else has already created # a score summary and ignore the error. try: score_model = score.save() _log_score(score_model) # Send a signal out to any listeners who are waiting for scoring events. score_set.send( sender=None, points_possible=points_possible, points_earned=points_earned, anonymous_user_id=submission_model.student_item.student_id, course_id=submission_model.student_item.course_id, item_id=submission_model.student_item.item_id, ) except IntegrityError: pass
def set_score(submission_uuid, points_earned, points_possible): """Set a score for a particular submission. Sets the score for a particular submission. This score is calculated externally to the API. Args: submission_uuid (str): UUID for the submission (must exist). points_earned (int): The earned points for this submission. points_possible (int): The total points possible for this particular student item. Returns: None Raises: SubmissionInternalError: Thrown if there was an internal error while attempting to save the score. SubmissionRequestError: Thrown if the given student item or submission are not found. Examples: >>> set_score("a778b933-9fb3-11e3-9c0f-040ccee02800", 11, 12) { 'student_item': 2, 'submission': 1, 'points_earned': 11, 'points_possible': 12, 'created_at': datetime.datetime(2014, 2, 7, 20, 6, 42, 331156, tzinfo=<UTC>) } """ try: submission_model = Submission.objects.get(uuid=submission_uuid) except Submission.DoesNotExist: raise SubmissionNotFoundError( u"No submission matching uuid {}".format(submission_uuid)) except DatabaseError: error_msg = u"Could not retrieve student item: {} or submission {}.".format( submission_uuid) logger.exception(error_msg) raise SubmissionRequestError(error_msg) score = ScoreSerializer( data={ "student_item": submission_model.student_item.pk, "submission": submission_model.pk, "points_earned": points_earned, "points_possible": points_possible, }) if not score.is_valid(): logger.exception(score.errors) raise SubmissionInternalError(score.errors) # When we save the score, a score summary will be created if # it does not already exist. # When the database's isolation level is set to repeatable-read, # it's possible for a score summary to exist for this student item, # even though we cannot retrieve it. # In this case, we assume that someone else has already created # a score summary and ignore the error. try: score_model = score.save() _log_score(score_model) except IntegrityError: pass
def set_score(student_item, submission, score, points_possible): """Set a score for a particular student item, submission pair. Sets the score for a particular student item and submission pair. This score is calculated externally to the API. Args: student_item (dict): The student item associated with this score. This dictionary must contain a course_id, student_id, and item_id. submission (dict): The submission associated with this score. This dictionary must contain all submission fields to properly get a unique submission item. score (int): The score to associate with the given submission and student item. points_possible (int): The total points possible for this particular student item. Returns: (dict): The dictionary representation of the saved score. Raises: SubmissionInternalError: Thrown if there was an internal error while attempting to save the score. SubmissionRequestError: Thrown if the given student item or submission are not found. Examples: >>> student_item_dict = dict( >>> student_id="Tim", >>> item_id="item_1", >>> course_id="course_1", >>> item_type="type_one" >>> ) >>> >>> submission_dict = dict( >>> student_item=2, >>> attempt_number=1, >>> submitted_at=datetime.datetime(2014, 1, 29, 23, 14, 52, 649284, tzinfo=<UTC>), >>> created_at=datetime.datetime(2014, 1, 29, 17, 14, 52, 668850, tzinfo=<UTC>), >>> answer=u'The answer is 42.' >>> ) >>> set_score(student_item_dict, submission_dict, 11, 12) { 'student_item': 2, 'submission': 1, 'points_earned': 11, 'points_possible': 12, 'created_at': datetime.datetime(2014, 2, 7, 20, 6, 42, 331156, tzinfo=<UTC>) } """ try: student_item_model = StudentItem.objects.get(**student_item) submission_model = Submission.objects.get(**submission) except DatabaseError: error_msg = u"Could not retrieve student item: {} or submission {}.".format( student_item, submission ) logger.exception(error_msg) raise SubmissionRequestError(error_msg) score = ScoreSerializer( data={ "student_item": student_item_model.pk, "submission": submission_model.pk, "points_earned": score, "points_possible": points_possible, } ) if not score.is_valid(): logger.exception(score.errors) raise SubmissionInternalError(score.errors) score.save() return score.data