Commit 8cc82f76 by Braden MacDonald

Fix Offline Grade Calculation

parent aa5fed71
......@@ -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
"""
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))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment