Fix Offline Grade Calculation
This commit is contained in:
@@ -13,19 +13,20 @@ from json import JSONEncoder
|
|||||||
from courseware import grades, models
|
from courseware import grades, models
|
||||||
from courseware.courses import get_course_by_id
|
from courseware.courses import get_course_by_id
|
||||||
from django.contrib.auth.models import User
|
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
|
from instructor.utils import DummyRequest
|
||||||
|
|
||||||
|
|
||||||
class MyEncoder(JSONEncoder):
|
class MyEncoder(JSONEncoder):
|
||||||
|
""" JSON Encoder that can encode OpaqueKeys """
|
||||||
def _iterencode(self, obj, markers=None):
|
def default(self, obj): # pylint: disable=method-hidden
|
||||||
if isinstance(obj, tuple) and hasattr(obj, '_asdict'):
|
""" Encode an object that the default encoder hasn't been able to. """
|
||||||
gen = self._iterencode_dict(obj._asdict(), markers)
|
if isinstance(obj, OpaqueKey):
|
||||||
else:
|
return unicode(obj)
|
||||||
gen = JSONEncoder._iterencode(self, obj, markers)
|
return JSONEncoder.default(self, obj)
|
||||||
for chunk in gen:
|
|
||||||
yield chunk
|
|
||||||
|
|
||||||
|
|
||||||
def offline_grade_calculation(course_key):
|
def offline_grade_calculation(course_key):
|
||||||
@@ -50,9 +51,15 @@ def offline_grade_calculation(course_key):
|
|||||||
request.session = {}
|
request.session = {}
|
||||||
|
|
||||||
gradeset = grades.grade(student, request, course, keep_raw_scores=True)
|
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, _created = models.OfflineComputedGrade.objects.get_or_create(user=student, course_id=course_key)
|
||||||
ocg.gradeset = gs
|
ocg.gradeset = gradeset_str
|
||||||
ocg.save()
|
ocg.save()
|
||||||
print "%s done" % student # print statement used because this is run by a management command
|
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)
|
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
|
||||||
|
|||||||
107
lms/djangoapps/instructor/tests/test_offline_gradecalc.py
Normal file
107
lms/djangoapps/instructor/tests/test_offline_gradecalc.py
Normal file
@@ -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))
|
||||||
Reference in New Issue
Block a user