grades.py 9.94 KB
Newer Older
1 2 3
# Compute grades using real division, with no integer truncation
from __future__ import division

4
import random
5
import logging
6 7

from django.conf import settings
8

9
from models import StudentModuleCache
10
from module_render import get_module, get_instance_module
11
from xmodule import graders
12
from xmodule.course_module import CourseDescriptor
13
from xmodule.graders import Score
14
from models import StudentModule
15

16
log = logging.getLogger("mitx.courseware")
17

18
def yield_module_descendents(module):
19
    stack = module.get_display_items()
20

21 22 23 24
    while len(stack) > 0:
        next_module = stack.pop()
        stack.extend( next_module.get_display_items() )
        yield next_module
25

26 27
def grade(student, request, course, student_module_cache=None):
    """
28
    This grades a student as quickly as possible. It retuns the
29 30
    output from the course grader, augmented with the final letter
    grade. The keys in the output are:
31

Victor Shnayder committed
32 33
    course: a CourseDescriptor

34 35 36 37 38 39
    - grade : A final letter grade.
    - percent : The final percent for the class (rounded up).
    - section_breakdown : A breakdown of each section that makes
        up the grade. (For display)
    - grade_breakdown : A breakdown of the major components that
        make up the final grade. (For display)
40

41
    More information on the format is in the docstring for CourseGrader.
42
    """
43

44
    grading_context = course.grading_context
45

46
    if student_module_cache == None:
47
        student_module_cache = StudentModuleCache(course.id, student, grading_context['all_descriptors'])
48

49
    totaled_scores = {}
50 51 52
    # This next complicated loop is just to collect the totaled_scores, which is
    # passed to the grader
    for section_format, sections in grading_context['graded_sections'].iteritems():
53 54 55 56
        format_scores = []
        for section in sections:
            section_descriptor = section['section_descriptor']
            section_name = section_descriptor.metadata.get('display_name')
57

58 59 60
            should_grade_section = False
            # If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
            for moduledescriptor in section['xmoduledescriptors']:
61 62
                if student_module_cache.lookup(
                        course.id, moduledescriptor.category, moduledescriptor.location.url()):
63 64
                    should_grade_section = True
                    break
65

66 67
            if should_grade_section:
                scores = []
68 69
                # TODO: We need the request to pass into here. If we could forgo that, our arguments
                # would be simpler
70
                section_module = get_module(student, request,
71
                                            section_descriptor.location, student_module_cache,
72
                                            course.id)
73 74 75 76
                if section_module is None:
                    # student doesn't have access to this module, or something else
                    # went wrong.
                    continue
77

78 79
                # TODO: We may be able to speed this up by only getting a list of children IDs from section_module
                # Then, we may not need to instatiate any problems if they are already in the database
80
                for module in yield_module_descendents(section_module):
81
                    (correct, total) = get_score(course.id, student, module, student_module_cache)
82 83
                    if correct is None and total is None:
                        continue
84

85 86 87 88 89
                    if settings.GENERATE_PROFILE_SCORES:
                        if total > 1:
                            correct = random.randrange(max(total - 2, 1), total + 1)
                        else:
                            correct = total
90

91
                    graded = module.metadata.get("graded", False)
92 93 94
                    if not total > 0:
                        #We simply cannot grade a problem that is 12/0, because we might need it as a percentage
                        graded = False
95

96
                    scores.append(Score(correct, total, graded, module.metadata.get('display_name')))
97

98 99 100
                section_total, graded_total = graders.aggregate_scores(scores, section_name)
            else:
                section_total = Score(0.0, 1.0, False, section_name)
101
                graded_total = Score(0.0, 1.0, True, section_name)
102

103 104 105
            #Add the graded total to totaled_scores
            if graded_total.possible > 0:
                format_scores.append(graded_total)
106
            else:
107
                log.exception("Unable to grade a section with a total possible score of zero. " + str(section_descriptor.location))
108

109
        totaled_scores[section_format] = format_scores
110

111
    grade_summary = course.grader.grade(totaled_scores)
112

113 114 115
    # We round the grade here, to make sure that the grade is an whole percentage and
    # doesn't get displayed differently than it gets grades
    grade_summary['percent'] = round(grade_summary['percent'] * 100 + 0.05) / 100
116

117 118
    letter_grade = grade_for_percentage(course.grade_cutoffs, grade_summary['percent'])
    grade_summary['grade'] = letter_grade
119

120
    return grade_summary
121 122 123 124

def grade_for_percentage(grade_cutoffs, percentage):
    """
    Returns a letter grade 'A' 'B' 'C' or None.
125

126 127 128 129 130
    Arguments
    - grade_cutoffs is a dictionary mapping a grade to the lowest
        possible percentage to earn that grade.
    - percentage is the final percent across all problems in a course
    """
131

132 133 134 135 136
    letter_grade = None
    for possible_grade in ['A', 'B', 'C']:
        if percentage >= grade_cutoffs[possible_grade]:
            letter_grade = possible_grade
            break
137 138

    return letter_grade
139

140
def progress_summary(student, course, grader, student_module_cache):
141
    """
142
    This pulls a summary of all problems in the course.
143

144
    Returns
145 146 147 148
    - courseware_summary is a summary of all sections with problems in the course.
    It is organized as an array of chapters, each containing an array of sections,
    each containing an array of scores. This contains information for graded and
    ungraded problems, and is good for displaying a course summary with due dates,
149
    etc.
150

151 152 153
    Arguments:
        student: A User object for the student to grade
        course: An XModule containing the course to grade
154 155
        student_module_cache: A StudentModuleCache initialized with all
             instance_modules for the student
156 157
    """
    chapters = []
158 159
    # Don't include chapters that aren't displayable (e.g. due to error)
    for c in course.get_display_items():
160 161
        # Skip if the chapter is hidden
        hidden = c.metadata.get('hide_from_toc','false')
kimth committed
162
        if hidden.lower() == 'true':
163 164
            continue

165
        sections = []
166
        for s in c.get_display_items():
167 168
            # Skip if the section is hidden
            hidden = s.metadata.get('hide_from_toc','false')
kimth committed
169
            if hidden.lower() == 'true':
170 171
                continue

172
            # Same for sections
173
            graded = s.metadata.get('graded', False)
174
            scores = []
175
            for module in yield_module_descendents(s):
176 177 178
                # course is a module, not a descriptor...
                course_id = course.descriptor.id
                (correct, total) = get_score(course_id, student, module, student_module_cache)
179 180 181
                if correct is None and total is None:
                    continue

182
                scores.append(Score(correct, total, graded,
183
                    module.metadata.get('display_name')))
184 185 186

            section_total, graded_total = graders.aggregate_scores(
                scores, s.metadata.get('display_name'))
187

188
            format = s.metadata.get('format', "")
189
            sections.append({
190 191
                'display_name': s.display_name,
                'url_name': s.url_name,
192 193 194
                'scores': scores,
                'section_total': section_total,
                'format': format,
195
                'due': s.metadata.get("due", ""),
196 197 198
                'graded': graded,
            })

199 200 201
        chapters.append({'course': course.display_name,
                         'display_name': c.display_name,
                         'url_name': c.url_name,
202 203
                         'sections': sections})

204
    return chapters
205 206


207
def get_score(course_id, user, problem, student_module_cache):
208
    """
209
    Return the score for a user on a problem, as a tuple (correct, total).
210 211

    user: a Student object
212 213
    problem: an XModule
    cache: A StudentModuleCache
214
    """
215
    if not (problem.descriptor.stores_state and problem.descriptor.has_score):
216
        # These are not problems, and do not have a score
217
        return (None, None)
218

219
    correct = 0.0
220

221
    # If the ID is not in the cache, add the item
222
    instance_module = get_instance_module(course_id, user, problem, student_module_cache)
223 224 225
    # instance_module = student_module_cache.lookup(problem.category, problem.id)
    # if instance_module is None:
    #     instance_module = StudentModule(module_type=problem.category,
226
    #                                     course_id=????,
227 228 229 230 231 232 233 234
    #                                     module_state_key=problem.id,
    #                                     student=user,
    #                                     state=None,
    #                                     grade=0,
    #                                     max_grade=problem.max_score(),
    #                                     done='i')
    #     cache.append(instance_module)
    #     instance_module.save()
235 236 237 238 239 240 241 242 243 244

    # If this problem is ungraded/ungradable, bail
    if instance_module.max_grade is None:
        return (None, None)

    correct = instance_module.grade if instance_module.grade is not None else 0
    total = instance_module.max_grade

    if correct is not None and total is not None:
        #Now we re-weight the problem, if specified
245 246 247 248 249
        weight = getattr(problem, 'weight', None)
        if weight is not None:
            if total == 0:
                log.exception("Cannot reweight a problem with zero weight. Problem: " + str(instance_module))
                return (correct, total)
250
            correct = correct * weight / total
251
            total = weight
252 253

    return (correct, total)