diff --git a/submissions/api.py b/submissions/api.py index 45ef6581..ef7eb12f 100644 --- a/submissions/api.py +++ b/submissions/api.py @@ -10,7 +10,7 @@ from django.conf import settings from django.core.cache import cache -from django.db import IntegrityError, DatabaseError, transaction +from django.db import IntegrityError, DatabaseError from dogapi import dog_stats_api from submissions.serializers import ( @@ -220,7 +220,7 @@ def get_submission(submission_uuid, read_replica=False): cache_key = Submission.get_cache_key(submission_uuid) try: cached_submission_data = cache.get(cache_key) - except Exception as ex: + except Exception: # The cache backend could raise an exception # (for example, memcache keys that contain spaces) logger.exception("Error occurred while retrieving submission from the cache") @@ -413,6 +413,64 @@ def get_all_submissions(course_id, item_id, item_type, read_replica=True): yield data +def get_all_course_submission_information(course_id, item_type, read_replica=True): + """ For the given course, get all the submissions, student items, and scores of the given item type. + + 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: + student_item + submission + points_earned + points_possible + created_at + submission_uuid + """ + + student_item_qs = StudentItem.objects + if read_replica: + student_item_qs = _use_read_replica(student_item_qs) + + query = student_item_qs.prefetch_related('submission').select_related('scoresummary').filter( + course_id=course_id, + item_type=item_type, + ).iterator() + + for student_item in query: + for submission in student_item.submissions: + if submission.status == Submission.DELETED: + continue + if submission.hasattr('scoresummary') and not submission.score_summary.latest.is_hidden(): + yield ( + StudentItemSerializer(student_item).data, + SubmissionSerializer(submission).data, + ScoreSerializer(submission.score_summary.latest).data + ) + else: + # Make sure we return submission information even if there isn't a score associated with it. + yield ( + StudentItemSerializer(student_item).data, + SubmissionSerializer(submission).data, + {} + ) + + def get_top_submissions(course_id, item_id, item_type, number_of_top_scores, use_cache=True, read_replica=True): """Get a number of top scores for an assessment based on a particular student item @@ -755,7 +813,7 @@ def set_score(submission_uuid, points_earned, points_possible, u"No submission matching uuid {}".format(submission_uuid) ) except DatabaseError: - error_msg = u"Could not retrieve student item: {} or submission {}.".format( + error_msg = u"Could not retrieve submission {}.".format( submission_uuid ) logger.exception(error_msg) diff --git a/submissions/tests/test_api.py b/submissions/tests/test_api.py index 74ba1c9c..8a5a2b38 100644 --- a/submissions/tests/test_api.py +++ b/submissions/tests/test_api.py @@ -101,6 +101,42 @@ def test_get_all_submissions(self): self._assert_submission(submissions[1], ANSWER_TWO, student_item.pk, 2) self.assertEqual(submissions[1]['student_id'], STUDENT_ITEM['student_id']) + def test_get_course_submissions(self): + submission1 = api.create_submission(STUDENT_ITEM, ANSWER_ONE) + submission2 = api.create_submission(STUDENT_ITEM, ANSWER_TWO) + submission3 = api.create_submission(SECOND_STUDENT_ITEM, ANSWER_ONE) + submission4 = api.create_submission(SECOND_STUDENT_ITEM, ANSWER_TWO) + + api.set_score(submission1['uuid'], 1, 4) + api.set_score(submission2['uuid'], 2, 4) + api.set_score(submission3['uuid'], 3, 4) + api.set_score(submission4['uuid'], 4, 4) + + submissions_and_scores = list(api.get_all_course_submission_information( + STUDENT_ITEM['course_id'], + STUDENT_ITEM['item_type'], + read_replica=False, + )) + + student_item1 = self._get_student_item(STUDENT_ITEM) + student_item2 = self._get_student_item(SECOND_STUDENT_ITEM) + + self.assertDictEqual(SECOND_STUDENT_ITEM, submissions_and_scores[0][0]) + self._assert_submission(submissions_and_scores[0][1], submission4['answer'], student_item2.pk, 2) + self._assert_score(submissions_and_scores[0][2], 4, 4) + + self.assertDictEqual(SECOND_STUDENT_ITEM, submissions_and_scores[1][0]) + self._assert_submission(submissions_and_scores[1][1], submission3['answer'], student_item2.pk, 1) + self._assert_score(submissions_and_scores[1][2], 3, 4) + + self.assertDictEqual(STUDENT_ITEM, submissions_and_scores[2][0]) + self._assert_submission(submissions_and_scores[2][1], submission2['answer'], student_item1.pk, 2) + self._assert_score(submissions_and_scores[2][2], 2, 4) + + self.assertDictEqual(STUDENT_ITEM, submissions_and_scores[3][0]) + self._assert_submission(submissions_and_scores[3][1], submission1['answer'], student_item1.pk, 1) + self._assert_score(submissions_and_scores[3][2], 1, 4) + def test_get_submission(self): # Test base case that we can create a submission and get it back sub_dict1 = api.create_submission(STUDENT_ITEM, ANSWER_ONE)