Remove offline grade calc
This management command has been broken since before the django 1.8 upgrade, so I'm deleting it and all its tests, and updating a few other files that used these methods but are easily refactored away.
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
django management command: dump grades to csv files
|
||||
for use by batch processes
|
||||
"""
|
||||
from instructor.offline_gradecalc import offline_grade_calculation
|
||||
from courseware.courses import get_course_by_id
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Compute grades for all students in a course, and store result in DB.\n"
|
||||
help += "Usage: compute_grades course_id_or_dir \n"
|
||||
help += " course_id_or_dir: either course_id or course_dir\n"
|
||||
help += 'Example course_id: MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
print "args = ", args
|
||||
|
||||
if len(args) > 0:
|
||||
course_id = args[0]
|
||||
else:
|
||||
print self.help
|
||||
return
|
||||
course_key = None
|
||||
# parse out the course id into a coursekey
|
||||
try:
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
# if it's not a new-style course key, parse it from an old-style
|
||||
# course key
|
||||
except InvalidKeyError:
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
try:
|
||||
_course = get_course_by_id(course_key)
|
||||
except Exception as err:
|
||||
print "-----------------------------------------------------------------------------"
|
||||
print "Sorry, cannot find course with id {}".format(course_id)
|
||||
print "Got exception {}".format(err)
|
||||
print "Please provide a course ID or course data directory name, eg content-mit-801rq"
|
||||
return
|
||||
|
||||
print "-----------------------------------------------------------------------------"
|
||||
print "Computing grades for {}".format(course_id)
|
||||
|
||||
offline_grade_calculation(course_key)
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Tests for the instructor app management commands.
|
||||
"""
|
||||
@@ -1,117 +0,0 @@
|
||||
"""
|
||||
======== Offline calculation of grades =============================================================
|
||||
|
||||
Computing grades of a large number of students can take a long time. These routines allow grades to
|
||||
be computed offline, by a batch process (eg cronjob).
|
||||
|
||||
The grades are stored in the OfflineComputedGrade table of the courseware model.
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
|
||||
from json import JSONEncoder
|
||||
from courseware import models
|
||||
from courseware.courses import get_course_by_id
|
||||
from django.contrib.auth.models import User
|
||||
from lms.djangoapps.grades import course_grades
|
||||
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):
|
||||
""" 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):
|
||||
'''
|
||||
Compute grades for all students for a specified course, and save results to the DB.
|
||||
'''
|
||||
|
||||
tstart = time.time()
|
||||
enrolled_students = User.objects.filter(
|
||||
courseenrollment__course_id=course_key,
|
||||
courseenrollment__is_active=1
|
||||
).prefetch_related("groups").order_by('username')
|
||||
|
||||
enc = MyEncoder()
|
||||
|
||||
print "{} enrolled students".format(len(enrolled_students))
|
||||
course = get_course_by_id(course_key)
|
||||
|
||||
for student in enrolled_students:
|
||||
request = DummyRequest()
|
||||
request.user = student
|
||||
request.session = {}
|
||||
|
||||
gradeset = course_grades.summary(student, course)
|
||||
# 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 = gradeset_str
|
||||
ocg.save()
|
||||
print "%s done" % student # print statement used because this is run by a management command
|
||||
|
||||
tend = time.time()
|
||||
dt = tend - tstart
|
||||
|
||||
ocgl = models.OfflineComputedGradeLog(course_id=course_key, seconds=dt, nstudents=len(enrolled_students))
|
||||
ocgl.save()
|
||||
print ocgl
|
||||
print "All Done!"
|
||||
|
||||
|
||||
def offline_grades_available(course_key):
|
||||
'''
|
||||
Returns False if no offline grades available for specified course.
|
||||
Otherwise returns latest log field entry about the available pre-computed grades.
|
||||
'''
|
||||
ocgl = models.OfflineComputedGradeLog.objects.filter(course_id=course_key)
|
||||
if not ocgl:
|
||||
return False
|
||||
return ocgl.latest('created')
|
||||
|
||||
|
||||
def student_grades(student, request, course, use_offline=False): # pylint: disable=unused-argument
|
||||
'''
|
||||
This is the main interface to get grades. It has the same parameters as grades.grade, as well
|
||||
as use_offline. If use_offline is True then this will look for an offline computed gradeset in the DB.
|
||||
'''
|
||||
if not use_offline:
|
||||
return course_grades.summary(student, course)
|
||||
|
||||
try:
|
||||
ocg = models.OfflineComputedGrade.objects.get(user=student, course_id=course.id)
|
||||
except models.OfflineComputedGrade.DoesNotExist:
|
||||
return dict(
|
||||
raw_scores=[],
|
||||
section_breakdown=[],
|
||||
msg='Error: no offline gradeset available for {}, {}'.format(student, course.id)
|
||||
)
|
||||
|
||||
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
|
||||
@@ -1,107 +0,0 @@
|
||||
"""
|
||||
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, 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('lms.djangoapps.grades.course_grades.summary', 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('lms.djangoapps.grades.course_grades.summary', 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, self.course))
|
||||
@@ -13,8 +13,8 @@ from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from courseware.courses import get_course_with_access
|
||||
from instructor.offline_gradecalc import student_grades
|
||||
from instructor.views.api import require_level
|
||||
from lms.djangoapps.grades import course_grades
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ def get_grade_book_page(request, course, course_key):
|
||||
'username': student.username,
|
||||
'id': student.id,
|
||||
'email': student.email,
|
||||
'grade_summary': student_grades(student, request, course),
|
||||
'grade_summary': course_grades.summary(student, course)
|
||||
}
|
||||
for student in enrolled_students
|
||||
]
|
||||
|
||||
@@ -437,7 +437,6 @@ def _section_course_info(course, access):
|
||||
section_data['grade_cutoffs'] = reduce(advance, sorted_cutoffs, "")[:-2]
|
||||
except Exception: # pylint: disable=broad-except
|
||||
section_data['grade_cutoffs'] = "Not Available"
|
||||
# section_data['offline_grades'] = offline_grades_available(course_key)
|
||||
|
||||
try:
|
||||
section_data['course_errors'] = [(escape(a), '') for (a, _unused) in modulestore().get_course_errors(course.id)]
|
||||
|
||||
Reference in New Issue
Block a user