course_grading.py 10.2 KB
Newer Older
1
from base64 import b64encode
Calen Pennington committed
2
from datetime import timedelta
3 4
from hashlib import sha1
import json
5 6

from contentstore.signals.signals import GRADING_POLICY_CHANGED
7
from eventtracking import tracker
8
from track.event_transaction_utils import create_new_event_transaction_id
9
from xmodule.modulestore.django import modulestore
Don Mitchell committed
10

11 12
GRADING_POLICY_CHANGED_EVENT_TYPE = 'edx.grades.grading_policy_changed'

Don Mitchell committed
13

14
class CourseGradingModel(object):
Don Mitchell committed
15
    """
Calen Pennington committed
16
    Basically a DAO and Model combo for CRUD operations pertaining to grading policy.
Don Mitchell committed
17
    """
18 19
    # Within this class, allow access to protected members of client classes.
    # This comes up when accessing kvs data and caches during kvs saves and modulestore writes.
Don Mitchell committed
20
    def __init__(self, course_descriptor):
Don Mitchell committed
21 22 23
        self.graders = [
            CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)
        ]  # weights transformed to ints [0..100]
Don Mitchell committed
24 25
        self.grade_cutoffs = course_descriptor.grade_cutoffs
        self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
26
        self.minimum_grade_credit = course_descriptor.minimum_grade_credit
Calen Pennington committed
27 28

    @classmethod
29
    def fetch(cls, course_key):
Don Mitchell committed
30
        """
Don Mitchell committed
31
        Fetch the course grading policy for the given course from persistence and return a CourseGradingModel.
Don Mitchell committed
32
        """
33
        descriptor = modulestore().get_course(course_key)
Don Mitchell committed
34 35
        model = cls(descriptor)
        return model
Calen Pennington committed
36

Don Mitchell committed
37
    @staticmethod
38
    def fetch_grader(course_key, index):
Don Mitchell committed
39
        """
Calen Pennington committed
40
        Fetch the course's nth grader
Don Mitchell committed
41 42
        Returns an empty dict if there's no such grader.
        """
43
        descriptor = modulestore().get_course(course_key)
Calen Pennington committed
44 45
        index = int(index)
        if len(descriptor.raw_grader) > index:
Don Mitchell committed
46
            return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
Calen Pennington committed
47

Don Mitchell committed
48 49
        # return empty model
        else:
Chris Dodge committed
50
            return {"id": index,
Calen Pennington committed
51 52 53 54 55
                    "type": "",
                    "min_count": 0,
                    "drop_count": 0,
                    "short_label": None,
                    "weight": 0
Chris Dodge committed
56
                    }
Calen Pennington committed
57

Don Mitchell committed
58
    @staticmethod
59
    def update_from_json(course_key, jsondict, user):
Don Mitchell committed
60 61 62 63
        """
        Decode the json into CourseGradingModel and save any changes. Returns the modified model.
        Probably not the usual path for updates as it's too coarse grained.
        """
64
        descriptor = modulestore().get_course(course_key)
Don Mitchell committed
65

Don Mitchell committed
66
        graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']]
Calen Pennington committed
67

Don Mitchell committed
68 69
        descriptor.raw_grader = graders_parsed
        descriptor.grade_cutoffs = jsondict['grade_cutoffs']
Calen Pennington committed
70

71
        modulestore().update_item(descriptor, user.id)
72

73
        CourseGradingModel.update_grace_period_from_json(course_key, jsondict['grace_period'], user)
Calen Pennington committed
74

75
        CourseGradingModel.update_minimum_grade_credit_from_json(course_key, jsondict['minimum_grade_credit'], user)
76
        _grading_event_and_signal(course_key, user.id)
77

78
        return CourseGradingModel.fetch(course_key)
Calen Pennington committed
79

Don Mitchell committed
80
    @staticmethod
81
    def update_grader_from_json(course_key, grader, user):
Don Mitchell committed
82
        """
Calen Pennington committed
83
        Create or update the grader of the given type (string key) for the given course. Returns the modified
Don Mitchell committed
84 85
        grader which is a full model on the client but not on the server (just a dict)
        """
86
        descriptor = modulestore().get_course(course_key)
Don Mitchell committed
87

Calen Pennington committed
88
        # parse removes the id; so, grab it before parse
Don Mitchell committed
89
        index = int(grader.get('id', len(descriptor.raw_grader)))
Don Mitchell committed
90 91 92 93 94 95
        grader = CourseGradingModel.parse_grader(grader)

        if index < len(descriptor.raw_grader):
            descriptor.raw_grader[index] = grader
        else:
            descriptor.raw_grader.append(grader)
Calen Pennington committed
96

97
        modulestore().update_item(descriptor, user.id)
98
        _grading_event_and_signal(course_key, user.id)
99

100
        return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
Calen Pennington committed
101

Don Mitchell committed
102
    @staticmethod
103
    def update_cutoffs_from_json(course_key, cutoffs, user):
Don Mitchell committed
104 105 106 107
        """
        Create or update the grade cutoffs for the given course. Returns sent in cutoffs (ie., no extra
        db fetch).
        """
108
        descriptor = modulestore().get_course(course_key)
Don Mitchell committed
109
        descriptor.grade_cutoffs = cutoffs
110

111
        modulestore().update_item(descriptor, user.id)
112
        _grading_event_and_signal(course_key, user.id)
Don Mitchell committed
113
        return cutoffs
Calen Pennington committed
114

Don Mitchell committed
115
    @staticmethod
116
    def update_grace_period_from_json(course_key, graceperiodjson, user):
Don Mitchell committed
117
        """
Calen Pennington committed
118
        Update the course's default grace period. Incoming dict is {hours: h, minutes: m} possibly as a
119 120
        grace_period entry in an enclosing dict. It is also safe to call this method with a value of
        None for graceperiodjson.
Don Mitchell committed
121
        """
122
        descriptor = modulestore().get_course(course_key)
123 124 125 126 127 128 129

        # Before a graceperiod has ever been created, it will be None (once it has been
        # created, it cannot be set back to None).
        if graceperiodjson is not None:
            if 'grace_period' in graceperiodjson:
                graceperiodjson = graceperiodjson['grace_period']

Calen Pennington committed
130
            grace_timedelta = timedelta(**graceperiodjson)
Calen Pennington committed
131
            descriptor.graceperiod = grace_timedelta
132

133
            modulestore().update_item(descriptor, user.id)
Calen Pennington committed
134

Don Mitchell committed
135
    @staticmethod
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
    def update_minimum_grade_credit_from_json(course_key, minimum_grade_credit, user):
        """Update the course's default minimum grade requirement for credit.

        Args:
            course_key(CourseKey): The course identifier
            minimum_grade_json(Float): Minimum grade value
            user(User): The user object

        """
        descriptor = modulestore().get_course(course_key)

        # 'minimum_grade_credit' cannot be set to None
        if minimum_grade_credit is not None:
            minimum_grade_credit = minimum_grade_credit

            descriptor.minimum_grade_credit = minimum_grade_credit
            modulestore().update_item(descriptor, user.id)

    @staticmethod
155
    def delete_grader(course_key, index, user):
Don Mitchell committed
156 157 158
        """
        Delete the grader of the given type from the given course.
        """
159
        descriptor = modulestore().get_course(course_key)
Calen Pennington committed
160

Calen Pennington committed
161
        index = int(index)
Don Mitchell committed
162 163
        if index < len(descriptor.raw_grader):
            del descriptor.raw_grader[index]
164
            # force propagation to definition
Don Mitchell committed
165
            descriptor.raw_grader = descriptor.raw_grader
Calen Pennington committed
166

167
        modulestore().update_item(descriptor, user.id)
168
        _grading_event_and_signal(course_key, user.id)
169

Don Mitchell committed
170
    @staticmethod
171
    def delete_grace_period(course_key, user):
Don Mitchell committed
172
        """
Don Mitchell committed
173
        Delete the course's grace period.
Don Mitchell committed
174
        """
175
        descriptor = modulestore().get_course(course_key)
Calen Pennington committed
176

Calen Pennington committed
177
        del descriptor.graceperiod
178

179
        modulestore().update_item(descriptor, user.id)
Calen Pennington committed
180

Don Mitchell committed
181
    @staticmethod
182
    def get_section_grader_type(location):
183
        descriptor = modulestore().get_item(location)
Don Mitchell committed
184
        return {
185
            "graderType": descriptor.format if descriptor.format is not None else 'notgraded',
Don Mitchell committed
186 187
            "location": unicode(location),
        }
Calen Pennington committed
188

189
    @staticmethod
190
    def update_section_grader_type(descriptor, grader_type, user):
191
        if grader_type is not None and grader_type != u'notgraded':
Don Mitchell committed
192
            descriptor.format = grader_type
Calen Pennington committed
193
            descriptor.graded = True
194
        else:
Calen Pennington committed
195 196
            del descriptor.format
            del descriptor.graded
Calen Pennington committed
197

198
        modulestore().update_item(descriptor, user.id)
199
        _grading_event_and_signal(descriptor.location.course_key, user.id)
Don Mitchell committed
200
        return {'graderType': grader_type}
Calen Pennington committed
201

202
    @staticmethod
Don Mitchell committed
203 204
    def convert_set_grace_period(descriptor):
        # 5 hours 59 minutes 59 seconds => converted to iso format
Calen Pennington committed
205
        rawgrace = descriptor.graceperiod
Don Mitchell committed
206
        if rawgrace:
207
            hours_from_days = rawgrace.days * 24
208 209
            seconds = rawgrace.seconds
            hours_from_seconds = int(seconds / 3600)
Calen Pennington committed
210
            hours = hours_from_days + hours_from_seconds
211 212 213
            seconds -= hours_from_seconds * 3600
            minutes = int(seconds / 60)
            seconds -= minutes * 60
Calen Pennington committed
214

Calen Pennington committed
215
            graceperiod = {'hours': 0, 'minutes': 0, 'seconds': 0}
Calen Pennington committed
216
            if hours > 0:
Calen Pennington committed
217
                graceperiod['hours'] = hours
Calen Pennington committed
218 219

            if minutes > 0:
Calen Pennington committed
220
                graceperiod['minutes'] = minutes
Calen Pennington committed
221 222

            if seconds > 0:
Calen Pennington committed
223
                graceperiod['seconds'] = seconds
Calen Pennington committed
224 225

            return graceperiod
226 227
        else:
            return None
Don Mitchell committed
228 229 230 231

    @staticmethod
    def parse_grader(json_grader):
        # manual to clear out kruft
Chris Dodge committed
232 233 234 235 236 237
        result = {"type": json_grader["type"],
                  "min_count": int(json_grader.get('min_count', 0)),
                  "drop_count": int(json_grader.get('drop_count', 0)),
                  "short_label": json_grader.get('short_label', None),
                  "weight": float(json_grader.get('weight', 0)) / 100.0
                  }
Calen Pennington committed
238

Don Mitchell committed
239 240 241 242
        return result

    @staticmethod
    def jsonize_grader(i, grader):
243 244 245
        # Warning: converting weight to integer might give unwanted results due
        # to the reason how floating point arithmetic works
        # e.g, "0.29 * 100 = 28.999999999999996"
246 247 248 249 250 251 252 253
        return {
            "id": i,
            "type": grader["type"],
            "min_count": grader.get('min_count', 0),
            "drop_count": grader.get('drop_count', 0),
            "short_label": grader.get('short_label', ""),
            "weight": grader.get('weight', 0) * 100,
        }
254 255 256 257 258 259 260 261 262 263 264 265 266 267


def _grading_event_and_signal(course_key, user_id):
    name = GRADING_POLICY_CHANGED_EVENT_TYPE
    course = modulestore().get_course(course_key)

    data = {
        "course_id": unicode(course_key),
        "user_id": unicode(user_id),
        "grading_policy_hash": unicode(hash_grading_policy(course.grading_policy)),
        "event_transaction_id": unicode(create_new_event_transaction_id()),
        "event_transaction_type": GRADING_POLICY_CHANGED_EVENT_TYPE,
    }
    tracker.emit(name, data)
268
    GRADING_POLICY_CHANGED.send(sender=CourseGradingModel, user_id=user_id, course_key=course_key)
269 270 271 272 273 274 275 276 277


def hash_grading_policy(grading_policy):
    ordered_policy = json.dumps(
        grading_policy,
        separators=(',', ':'),  # Remove spaces from separators for more compact representation
        sort_keys=True,
    )
    return b64encode(sha1(ordered_policy).digest())