diff --git a/lms/djangoapps/instructor/offline_gradecalc.py b/lms/djangoapps/instructor/offline_gradecalc.py index ad0b727acc..d6ace314d6 100644 --- a/lms/djangoapps/instructor/offline_gradecalc.py +++ b/lms/djangoapps/instructor/offline_gradecalc.py @@ -13,19 +13,20 @@ from json import JSONEncoder from courseware import grades, models from courseware.courses import get_course_by_id from django.contrib.auth.models import User +from opaque_keys import OpaqueKey +from opaque_keys.edx.keys import UsageKey +from xmodule.graders import Score from instructor.utils import DummyRequest class MyEncoder(JSONEncoder): - - def _iterencode(self, obj, markers=None): - if isinstance(obj, tuple) and hasattr(obj, '_asdict'): - gen = self._iterencode_dict(obj._asdict(), markers) - else: - gen = JSONEncoder._iterencode(self, obj, markers) - for chunk in gen: - yield chunk + """ JSON Encoder that can encode OpaqueKeys """ + def default(self, obj): # pylint: disable=method-hidden + """ Encode an object that the default encoder hasn't been able to. """ + if isinstance(obj, OpaqueKey): + return unicode(obj) + return JSONEncoder.default(self, obj) def offline_grade_calculation(course_key): @@ -50,9 +51,15 @@ def offline_grade_calculation(course_key): request.session = {} gradeset = grades.grade(student, request, course, keep_raw_scores=True) - gs = enc.encode(gradeset) + # Convert Score namedtuples to dicts: + totaled_scores = gradeset['totaled_scores'] + for section in totaled_scores: + totaled_scores[section] = [score._asdict() for score in totaled_scores[section]] + gradeset['raw_scores'] = [score._asdict() for score in gradeset['raw_scores']] + # Encode as JSON and save: + gradeset_str = enc.encode(gradeset) ocg, _created = models.OfflineComputedGrade.objects.get_or_create(user=student, course_id=course_key) - ocg.gradeset = gs + ocg.gradeset = gradeset_str ocg.save() print "%s done" % student # print statement used because this is run by a management command @@ -93,4 +100,17 @@ def student_grades(student, request, course, keep_raw_scores=False, use_offline= msg='Error: no offline gradeset available for {}, {}'.format(student, course.id) ) - return json.loads(ocg.gradeset) + gradeset = json.loads(ocg.gradeset) + # Convert score dicts back to Score tuples: + + def score_from_dict(encoded): + """ Given a formerly JSON-encoded Score tuple, return the Score tuple """ + if encoded['module_id']: + encoded['module_id'] = UsageKey.from_string(encoded['module_id']) + return Score(**encoded) + + totaled_scores = gradeset['totaled_scores'] + for section in totaled_scores: + totaled_scores[section] = [score_from_dict(score) for score in totaled_scores[section]] + gradeset['raw_scores'] = [score_from_dict(score) for score in gradeset['raw_scores']] + return gradeset diff --git a/lms/djangoapps/instructor/tests/test_offline_gradecalc.py b/lms/djangoapps/instructor/tests/test_offline_gradecalc.py new file mode 100644 index 0000000000..cda1e03ec0 --- /dev/null +++ b/lms/djangoapps/instructor/tests/test_offline_gradecalc.py @@ -0,0 +1,107 @@ +""" +Tests for offline_gradecalc.py +""" +import json +from mock import patch + +from courseware.models import OfflineComputedGrade +from student.models import CourseEnrollment +from student.tests.factories import UserFactory +from xmodule.graders import Score +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + +from ..offline_gradecalc import offline_grade_calculation, student_grades + + +def mock_grade(_student, _request, course, **_kwargs): + """ Return some fake grade data to mock grades.grade() """ + return { + 'grade': u'Pass', + 'totaled_scores': { + u'Homework': [ + Score(earned=10.0, possible=10.0, graded=True, section=u'Subsection 1', module_id=None), + ] + }, + 'percent': 0.85, + 'raw_scores': [ + Score( + earned=5.0, possible=5.0, graded=True, section=u'Numerical Input', + module_id=course.id.make_usage_key('problem', 'problem1'), + ), + Score( + earned=5.0, possible=5.0, graded=True, section=u'Multiple Choice', + module_id=course.id.make_usage_key('problem', 'problem2'), + ), + ], + 'section_breakdown': [ + {'category': u'Homework', 'percent': 1.0, 'detail': u'Homework 1 - Test - 100% (10/10)', 'label': u'HW 01'}, + {'category': u'Final Exam', 'prominent': True, 'percent': 0, 'detail': u'Final = 0%', 'label': u'Final'} + ], + 'grade_breakdown': [ + {'category': u'Homework', 'percent': 0.85, 'detail': u'Homework = 85.00% of a possible 85.00%'}, + {'category': u'Final Exam', 'percent': 0.0, 'detail': u'Final Exam = 0.00% of a possible 15.00%'} + ] + } + + +class TestOfflineGradeCalc(ModuleStoreTestCase): + """ Test Offline Grade Calculation with some mocked grades """ + def setUp(self): + super(TestOfflineGradeCalc, self).setUp() + + with modulestore().default_store(ModuleStoreEnum.Type.split): # Test with split b/c old mongo keys are messy + self.course = CourseFactory.create() + self.user = UserFactory.create() + CourseEnrollment.enroll(self.user, self.course.id) + + patcher = patch('courseware.grades.grade', new=mock_grade) + patcher.start() + self.addCleanup(patcher.stop) + + def test_output(self): + offline_grades = OfflineComputedGrade.objects + self.assertEqual(offline_grades.filter(user=self.user, course_id=self.course.id).count(), 0) + offline_grade_calculation(self.course.id) + result = offline_grades.get(user=self.user, course_id=self.course.id) + decoded = json.loads(result.gradeset) + self.assertEqual(decoded['grade'], "Pass") + self.assertEqual(decoded['percent'], 0.85) + self.assertEqual(decoded['totaled_scores'], { + "Homework": [ + {"earned": 10.0, "possible": 10.0, "graded": True, "section": "Subsection 1", "module_id": None} + ] + }) + self.assertEqual(decoded['raw_scores'], [ + { + "earned": 5.0, + "possible": 5.0, + "graded": True, + "section": "Numerical Input", + "module_id": unicode(self.course.id.make_usage_key('problem', 'problem1')), + }, + { + "earned": 5.0, + "possible": 5.0, + "graded": True, + "section": "Multiple Choice", + "module_id": unicode(self.course.id.make_usage_key('problem', 'problem2')), + } + ]) + self.assertEqual(decoded['section_breakdown'], [ + {"category": "Homework", "percent": 1.0, "detail": "Homework 1 - Test - 100% (10/10)", "label": "HW 01"}, + {"category": "Final Exam", "label": "Final", "percent": 0, "detail": "Final = 0%", "prominent": True} + ]) + self.assertEqual(decoded['grade_breakdown'], [ + {"category": "Homework", "percent": 0.85, "detail": "Homework = 85.00% of a possible 85.00%"}, + {"category": "Final Exam", "percent": 0.0, "detail": "Final Exam = 0.00% of a possible 15.00%"} + ]) + + def test_student_grades(self): + """ Test that the data returned by student_grades() and grades.grade() match """ + offline_grade_calculation(self.course.id) + with patch('courseware.grades.grade', side_effect=AssertionError('Should not re-grade')): + result = student_grades(self.user, None, self.course, use_offline=True) + self.assertEqual(result, mock_grade(self.user, None, self.course))