course_grading.py 8.7 KB
Newer Older
Calen Pennington committed
1
from datetime import timedelta
2
from xmodule.modulestore.django import modulestore
Don Mitchell committed
3 4


5
class CourseGradingModel(object):
Don Mitchell committed
6
    """
Calen Pennington committed
7
    Basically a DAO and Model combo for CRUD operations pertaining to grading policy.
Don Mitchell committed
8
    """
9 10
    # 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
11
    def __init__(self, course_descriptor):
Don Mitchell committed
12 13 14
        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
15 16
        self.grade_cutoffs = course_descriptor.grade_cutoffs
        self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
17
        self.minimum_grade_credit = course_descriptor.minimum_grade_credit
Calen Pennington committed
18 19

    @classmethod
20
    def fetch(cls, course_key):
Don Mitchell committed
21
        """
Don Mitchell committed
22
        Fetch the course grading policy for the given course from persistence and return a CourseGradingModel.
Don Mitchell committed
23
        """
24
        descriptor = modulestore().get_course(course_key)
Don Mitchell committed
25 26
        model = cls(descriptor)
        return model
Calen Pennington committed
27

Don Mitchell committed
28
    @staticmethod
29
    def fetch_grader(course_key, index):
Don Mitchell committed
30
        """
Calen Pennington committed
31
        Fetch the course's nth grader
Don Mitchell committed
32 33
        Returns an empty dict if there's no such grader.
        """
34
        descriptor = modulestore().get_course(course_key)
Calen Pennington committed
35 36
        index = int(index)
        if len(descriptor.raw_grader) > index:
Don Mitchell committed
37
            return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
Calen Pennington committed
38

Don Mitchell committed
39 40
        # return empty model
        else:
Chris Dodge committed
41
            return {"id": index,
Calen Pennington committed
42 43 44 45 46
                    "type": "",
                    "min_count": 0,
                    "drop_count": 0,
                    "short_label": None,
                    "weight": 0
Chris Dodge committed
47
                    }
Calen Pennington committed
48

Don Mitchell committed
49
    @staticmethod
50
    def update_from_json(course_key, jsondict, user):
Don Mitchell committed
51 52 53 54
        """
        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.
        """
55
        descriptor = modulestore().get_course(course_key)
Don Mitchell committed
56

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

Don Mitchell committed
59 60
        descriptor.raw_grader = graders_parsed
        descriptor.grade_cutoffs = jsondict['grade_cutoffs']
Calen Pennington committed
61

62
        modulestore().update_item(descriptor, user.id)
63

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

66 67
        CourseGradingModel.update_minimum_grade_credit_from_json(course_key, jsondict['minimum_grade_credit'], user)

68
        return CourseGradingModel.fetch(course_key)
Calen Pennington committed
69

Don Mitchell committed
70
    @staticmethod
71
    def update_grader_from_json(course_key, grader, user):
Don Mitchell committed
72
        """
Calen Pennington committed
73
        Create or update the grader of the given type (string key) for the given course. Returns the modified
Don Mitchell committed
74 75
        grader which is a full model on the client but not on the server (just a dict)
        """
76
        descriptor = modulestore().get_course(course_key)
Don Mitchell committed
77

Calen Pennington committed
78
        # parse removes the id; so, grab it before parse
Don Mitchell committed
79
        index = int(grader.get('id', len(descriptor.raw_grader)))
Don Mitchell committed
80 81 82 83 84 85
        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
86

87
        modulestore().update_item(descriptor, user.id)
Calen Pennington committed
88

89
        return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
Calen Pennington committed
90

Don Mitchell committed
91
    @staticmethod
92
    def update_cutoffs_from_json(course_key, cutoffs, user):
Don Mitchell committed
93 94 95 96
        """
        Create or update the grade cutoffs for the given course. Returns sent in cutoffs (ie., no extra
        db fetch).
        """
97
        descriptor = modulestore().get_course(course_key)
Don Mitchell committed
98
        descriptor.grade_cutoffs = cutoffs
99

100
        modulestore().update_item(descriptor, user.id)
Calen Pennington committed
101

Don Mitchell committed
102
        return cutoffs
Calen Pennington committed
103

Don Mitchell committed
104
    @staticmethod
105
    def update_grace_period_from_json(course_key, graceperiodjson, user):
Don Mitchell committed
106
        """
Calen Pennington committed
107
        Update the course's default grace period. Incoming dict is {hours: h, minutes: m} possibly as a
108 109
        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
110
        """
111
        descriptor = modulestore().get_course(course_key)
112 113 114 115 116 117 118

        # 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
119
            grace_timedelta = timedelta(**graceperiodjson)
Calen Pennington committed
120
            descriptor.graceperiod = grace_timedelta
121

122
            modulestore().update_item(descriptor, user.id)
Calen Pennington committed
123

Don Mitchell committed
124
    @staticmethod
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
    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
144
    def delete_grader(course_key, index, user):
Don Mitchell committed
145 146 147
        """
        Delete the grader of the given type from the given course.
        """
148
        descriptor = modulestore().get_course(course_key)
Calen Pennington committed
149

Calen Pennington committed
150
        index = int(index)
Don Mitchell committed
151 152
        if index < len(descriptor.raw_grader):
            del descriptor.raw_grader[index]
153
            # force propagation to definition
Don Mitchell committed
154
            descriptor.raw_grader = descriptor.raw_grader
Calen Pennington committed
155

156
        modulestore().update_item(descriptor, user.id)
Calen Pennington committed
157

Don Mitchell committed
158
    @staticmethod
159
    def delete_grace_period(course_key, user):
Don Mitchell committed
160
        """
Don Mitchell committed
161
        Delete the course's grace period.
Don Mitchell committed
162
        """
163
        descriptor = modulestore().get_course(course_key)
Calen Pennington committed
164

Calen Pennington committed
165
        del descriptor.graceperiod
166

167
        modulestore().update_item(descriptor, user.id)
Calen Pennington committed
168

Don Mitchell committed
169
    @staticmethod
170
    def get_section_grader_type(location):
171
        descriptor = modulestore().get_item(location)
Don Mitchell committed
172
        return {
173
            "graderType": descriptor.format if descriptor.format is not None else 'notgraded',
Don Mitchell committed
174 175
            "location": unicode(location),
        }
Calen Pennington committed
176

177
    @staticmethod
178
    def update_section_grader_type(descriptor, grader_type, user):
179
        if grader_type is not None and grader_type != u'notgraded':
Don Mitchell committed
180
            descriptor.format = grader_type
Calen Pennington committed
181
            descriptor.graded = True
182
        else:
Calen Pennington committed
183 184
            del descriptor.format
            del descriptor.graded
Calen Pennington committed
185

186
        modulestore().update_item(descriptor, user.id)
Don Mitchell committed
187
        return {'graderType': grader_type}
Calen Pennington committed
188

189
    @staticmethod
Don Mitchell committed
190 191
    def convert_set_grace_period(descriptor):
        # 5 hours 59 minutes 59 seconds => converted to iso format
Calen Pennington committed
192
        rawgrace = descriptor.graceperiod
Don Mitchell committed
193
        if rawgrace:
194
            hours_from_days = rawgrace.days * 24
195 196
            seconds = rawgrace.seconds
            hours_from_seconds = int(seconds / 3600)
Calen Pennington committed
197
            hours = hours_from_days + hours_from_seconds
198 199 200
            seconds -= hours_from_seconds * 3600
            minutes = int(seconds / 60)
            seconds -= minutes * 60
Calen Pennington committed
201

Calen Pennington committed
202
            graceperiod = {'hours': 0, 'minutes': 0, 'seconds': 0}
Calen Pennington committed
203
            if hours > 0:
Calen Pennington committed
204
                graceperiod['hours'] = hours
Calen Pennington committed
205 206

            if minutes > 0:
Calen Pennington committed
207
                graceperiod['minutes'] = minutes
Calen Pennington committed
208 209

            if seconds > 0:
Calen Pennington committed
210
                graceperiod['seconds'] = seconds
Calen Pennington committed
211 212

            return graceperiod
213 214
        else:
            return None
Don Mitchell committed
215 216 217 218

    @staticmethod
    def parse_grader(json_grader):
        # manual to clear out kruft
Chris Dodge committed
219 220 221 222 223 224
        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
225

Don Mitchell committed
226 227 228 229
        return result

    @staticmethod
    def jsonize_grader(i, grader):
230 231 232
        # 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"
233 234 235 236 237 238 239 240
        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,
        }