offline_gradecalc.py 4.27 KB
Newer Older
1 2
"""
======== Offline calculation of grades =============================================================
3

4 5 6 7 8
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.
"""
9 10 11 12 13 14
import json
import time

from json import JSONEncoder
from courseware import grades, models
from courseware.courses import get_course_by_id
15
from django.contrib.auth.models import User
16 17 18
from opaque_keys import OpaqueKey
from opaque_keys.edx.keys import UsageKey
from xmodule.graders import Score
19

20
from instructor.utils import DummyRequest
21

22

23
class MyEncoder(JSONEncoder):
24 25 26 27 28 29
    """ 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)
30 31


32
def offline_grade_calculation(course_key):
33
    '''
Calen Pennington committed
34
    Compute grades for all students for a specified course, and save results to the DB.
35 36 37
    '''

    tstart = time.time()
38
    enrolled_students = User.objects.filter(
39
        courseenrollment__course_id=course_key,
40 41
        courseenrollment__is_active=1
    ).prefetch_related("groups").order_by('username')
42 43 44

    enc = MyEncoder()

45 46
    print "{} enrolled students".format(len(enrolled_students))
    course = get_course_by_id(course_key)
47 48

    for student in enrolled_students:
Kristin Stephens committed
49 50 51 52
        request = DummyRequest()
        request.user = student
        request.session = {}

53
        gradeset = grades.grade(student, request, course, keep_raw_scores=True)
54 55 56 57 58 59 60
        # 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)
61
        ocg, _created = models.OfflineComputedGrade.objects.get_or_create(user=student, course_id=course_key)
62
        ocg.gradeset = gradeset_str
63
        ocg.save()
Calen Pennington committed
64
        print "%s done" % student  	# print statement used because this is run by a management command
65 66 67

    tend = time.time()
    dt = tend - tstart
Calen Pennington committed
68

69
    ocgl = models.OfflineComputedGradeLog(course_id=course_key, seconds=dt, nstudents=len(enrolled_students))
70 71 72
    ocgl.save()
    print ocgl
    print "All Done!"
Calen Pennington committed
73

74

75
def offline_grades_available(course_key):
76 77 78 79
    '''
    Returns False if no offline grades available for specified course.
    Otherwise returns latest log field entry about the available pre-computed grades.
    '''
80
    ocgl = models.OfflineComputedGradeLog.objects.filter(course_id=course_key)
81 82 83 84
    if not ocgl:
        return False
    return ocgl.latest('created')

Calen Pennington committed
85

86 87 88 89 90 91 92
def student_grades(student, request, course, keep_raw_scores=False, use_offline=False):
    '''
    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 grades.grade(student, request, course, keep_raw_scores=keep_raw_scores)
Calen Pennington committed
93

94 95 96
    try:
        ocg = models.OfflineComputedGrade.objects.get(user=student, course_id=course.id)
    except models.OfflineComputedGrade.DoesNotExist:
97 98 99 100 101
        return dict(
            raw_scores=[],
            section_breakdown=[],
            msg='Error: no offline gradeset available for {}, {}'.format(student, course.id)
        )
102

103 104 105 106 107 108 109 110 111 112 113 114 115 116
    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