def test_grade_with_max_score_cache(self):
        """
        Tests that the max score cache is populated after a grading run
        and that the results of grading runs before and after the cache
        warms are the same.
        """
        self.basic_setup()
        self.submit_question_answer('p1', {'2_1': 'Correct'})
        self.look_at_question('p2')
        self.assertTrue(
            StudentModule.objects.filter(
                module_state_key=self.problem_location('p2')
            ).exists()
        )
        location_to_cache = unicode(self.problem_location('p2'))
        max_scores_cache = MaxScoresCache.create_for_course(self.course)

        # problem isn't in the cache
        max_scores_cache.fetch_from_remote([location_to_cache])
        self.assertIsNone(max_scores_cache.get(location_to_cache))
        self.check_grade_percent(0.33)

        # problem is in the cache
        max_scores_cache.fetch_from_remote([location_to_cache])
        self.assertIsNotNone(max_scores_cache.get(location_to_cache))
        self.check_grade_percent(0.33)
Пример #2
0
    def test_grade_with_max_score_cache(self):
        """
        Tests that the max score cache is populated after a grading run
        and that the results of grading runs before and after the cache
        warms are the same.
        """
        self.basic_setup()
        self.submit_question_answer('p1', {'2_1': 'Correct'})
        self.look_at_question('p2')
        self.assertTrue(
            StudentModule.objects.filter(
                module_state_key=self.problem_location('p2')).exists())
        location_to_cache = unicode(self.problem_location('p2'))
        max_scores_cache = MaxScoresCache.create_for_course(self.course)

        # problem isn't in the cache
        max_scores_cache.fetch_from_remote([location_to_cache])
        self.assertIsNone(max_scores_cache.get(location_to_cache))
        self.check_grade_percent(0.33)

        # problem is in the cache
        max_scores_cache.fetch_from_remote([location_to_cache])
        self.assertIsNotNone(max_scores_cache.get(location_to_cache))
        self.check_grade_percent(0.33)
Пример #3
0
    def grade(student, request, course, keep_raw_scores, field_data_cache, scores_client):
        """
        This grades a student as quickly as possible. It returns the
        output from the course grader, augmented with the final letter
        grade. The keys in the output are:

        course: a CourseDescriptor

        - 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)
        - keep_raw_scores : if True, then value for key 'raw_scores' contains scores
          for every graded module

        More information on the format is in the docstring for CourseGrader.
        """
        if field_data_cache is None:
            with manual_transaction():
                field_data_cache = field_data_cache_for_grading(course, student)
        if scores_client is None:
            scores_client = ScoresClient.from_field_data_cache(field_data_cache)

        # Dict of item_ids -> (earned, possible) point tuples. This *only* grabs
        # scores that were registered with the submissions API, which for the moment
        # means only openassessment (edx-ora2)
        submissions_scores = sub_api.get_scores(
            course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id)
        )
        max_scores_cache = MaxScoresCache.create_for_course(course)
        # For the moment, we have to get scorable_locations from field_data_cache
        # and not from scores_client, because scores_client is ignorant of things
        # in the submissions API. As a further refactoring step, submissions should
        # be hidden behind the ScoresClient.
        max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations)

        grading_context = course.grading_context
        raw_scores = []

        totaled_scores = {}
        # 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():
            format_scores = []
            for section in sections:
                section_descriptor = section['section_descriptor']
                section_name = section_descriptor.display_name_with_default

                # some problems have state that is updated independently of interaction
                # with the LMS, so they need to always be scored. (E.g. foldit.,
                # combinedopenended)
                should_grade_section = any(
                    descriptor.always_recalculate_grades for descriptor in section['xmoduledescriptors']
                )

                # If there are no problems that always have to be regraded, check to
                # see if any of our locations are in the scores from the submissions
                # API. If scores exist, we have to calculate grades for this section.
                if not should_grade_section:
                    should_grade_section = any(
                        descriptor.location.to_deprecated_string() in submissions_scores
                        for descriptor in section['xmoduledescriptors']
                    )

                if not should_grade_section:
                    should_grade_section = any(
                        descriptor.location in scores_client for descriptor in section['xmoduledescriptors']
                    )

                # If we haven't seen a single problem in the section, we don't have
                # to grade it at all! We can assume 0%
                if should_grade_section:
                    scores = []

                    def create_module(descriptor):
                        '''creates an XModule instance given a descriptor'''
                        # TODO: We need the request to pass into here. If we could forego that, our arguments
                        # would be simpler
                        return get_module_for_descriptor(
                            student, request, descriptor, field_data_cache, course.id, course=course
                        )

                    descendants = yield_dynamic_descriptor_descendants(section_descriptor, student.id, create_module)
                    for module_descriptor in descendants:
                        (correct, total) = get_score(
                            student,
                            module_descriptor,
                            create_module,
                            scores_client,
                            submissions_scores,
                            max_scores_cache,
                        )
                        if correct is None and total is None:
                            continue

                        if settings.GENERATE_PROFILE_SCORES:    # for debugging!
                            if total > 1:
                                correct = random.randrange(max(total - 2, 1), total + 1)
                            else:
                                correct = total

                        graded = module_descriptor.graded
                        if not total > 0:
                            # We simply cannot grade a problem that is 12/0, because we might need it as a percentage
                            graded = False

                        scores.append(
                            Score(
                                correct,
                                total,
                                graded,
                                module_descriptor.display_name_with_default,
                                module_descriptor.location
                            )
                        )

                    __, graded_total = aggregate_section_scores(
                        scores, section_name, getattr(section_descriptor, 'weight', 1.0)
                    )
                    if keep_raw_scores:
                        raw_scores += scores
                else:
                    graded_total = WeightedScore(
                        0.0, 1.0, True, section_name, None, getattr(section_descriptor, 'weight', 1.0)
                    )

                # Add the graded total to totaled_scores
                if graded_total.possible > 0:
                    format_scores.append(graded_total)
                else:
                    log.info(
                        "Unable to grade a section with a total possible score of zero. {}".format(
                            section_descriptor.location
                        )
                    )

            totaled_scores[section_format] = format_scores

        # Grading policy might be overriden by a CCX, need to reset it
        course.set_grading_policy(course.grading_policy)
        grade_summary = course.grader.grade(totaled_scores, generate_random_scores=settings.GENERATE_PROFILE_SCORES)

        # 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

        letter_grade = grade_for_percentage(
            course.grade_cutoffs, grade_summary['percent'], grade_summary['sections_passed']
        )
        grade_summary['grade'] = letter_grade
        grade_summary['totaled_scores'] = totaled_scores   # make this available, eg for instructor download & debugging
        if keep_raw_scores:
            # way to get all RAW scores out to instructor
            # so grader can be double-checked
            grade_summary['raw_scores'] = raw_scores

        max_scores_cache.push_to_remote()

        return grade_summary
Пример #4
0
    def test_max_scores_cache(self):
        """
        Tests the behavior fo the MaxScoresCache
        """
        max_scores_cache = MaxScoresCache("test_max_scores_cache")
        self.assertEqual(max_scores_cache.num_cached_from_remote(), 0)
        self.assertEqual(max_scores_cache.num_cached_updates(), 0)

        # add score to cache
        max_scores_cache.set(self.locations[0], 1)
        self.assertEqual(max_scores_cache.num_cached_updates(), 1)

        # push to remote cache
        max_scores_cache.push_to_remote()

        # create a new cache with the same params, fetch from remote cache
        max_scores_cache = MaxScoresCache("test_max_scores_cache")
        max_scores_cache.fetch_from_remote(self.locations)

        # see cache is populated
        self.assertEqual(max_scores_cache.num_cached_from_remote(), 1)
Пример #5
0
    def progress_summary(student, request, course, field_data_cache=None, scores_client=None, grading_type='vertical'):
        """
        This pulls a summary of all problems in the course.

        Returns
        - 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,
        etc.

        Arguments:
            student: A User object for the student to grade
            course: A Descriptor containing the course to grade

        If the student does not have access to load the course module, this function
        will return None.

        """

        with manual_transaction():
            if field_data_cache is None:
                field_data_cache = field_data_cache_for_grading(course, student)
            if scores_client is None:
                scores_client = ScoresClient.from_field_data_cache(field_data_cache)

            course_module = get_module_for_descriptor(
                student, request, course, field_data_cache, course.id, course=course
            )
            if not course_module:
                return None

            course_module = getattr(course_module, '_x_module', course_module)

        submissions_scores = sub_api.get_scores(
            course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id)
        )
        max_scores_cache = MaxScoresCache.create_for_course(course)
        # For the moment, we have to get scorable_locations from field_data_cache
        # and not from scores_client, because scores_client is ignorant of things
        # in the submissions API. As a further refactoring step, submissions should
        # be hidden behind the ScoresClient.
        max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations)

        blocks_stack = [course_module]
        blocks_dict = {}

        while blocks_stack:
            curr_block = blocks_stack.pop()
            with manual_transaction():
                # Skip if the block is hidden
                if curr_block.hide_from_toc:
                    continue

                key = unicode(curr_block.scope_ids.usage_id)
                children = curr_block.get_display_items() if curr_block.category != grading_type else []
                block = {
                    'display_name': curr_block.display_name_with_default,
                    'block_type': curr_block.category,
                    'url_name': curr_block.url_name,
                    'children': [unicode(child.scope_ids.usage_id) for child in children],
                }

                if curr_block.category == grading_type:
                    graded = curr_block.graded
                    scores = []

                    module_creator = curr_block.xmodule_runtime.get_module
                    for module_descriptor in yield_dynamic_descriptor_descendants(
                            curr_block, student.id, module_creator
                    ):
                        (correct, total) = get_score(
                            student,
                            module_descriptor,
                            module_creator,
                            scores_client,
                            submissions_scores,
                            max_scores_cache,
                        )

                        if correct is None and total is None:
                            continue

                        scores.append(
                            Score(
                                correct,
                                total,
                                graded,
                                module_descriptor.display_name_with_default,
                                module_descriptor.location
                            )
                        )

                    scores.reverse()
                    total, _ = aggregate_scores(scores, curr_block.display_name_with_default)

                    module_format = curr_block.format if curr_block.format is not None else ''
                    block.update({
                        'scores': scores,
                        'total': total,
                        'format': module_format,
                        'due': curr_block.due,
                        'graded': graded,
                    })

                blocks_dict[key] = block
                # Add this blocks children to the stack so that we can traverse them as well.
                blocks_stack.extend(children)

        max_scores_cache.push_to_remote()

        return {
            'root': unicode(course.scope_ids.usage_id),
            'blocks': blocks_dict,
        }
Пример #6
0
    def grade(student, request, course, keep_raw_scores, field_data_cache,
              scores_client):
        """
        This grades a student as quickly as possible. It returns the
        output from the course grader, augmented with the final letter
        grade. The keys in the output are:

        course: a CourseDescriptor

        - 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)
        - keep_raw_scores : if True, then value for key 'raw_scores' contains scores
          for every graded module

        More information on the format is in the docstring for CourseGrader.
        """
        if field_data_cache is None:
            with manual_transaction():
                field_data_cache = field_data_cache_for_grading(
                    course, student)
        if scores_client is None:
            scores_client = ScoresClient.from_field_data_cache(
                field_data_cache)

        # Dict of item_ids -> (earned, possible) point tuples. This *only* grabs
        # scores that were registered with the submissions API, which for the moment
        # means only openassessment (edx-ora2)
        submissions_scores = sub_api.get_scores(
            course.id.to_deprecated_string(),
            anonymous_id_for_user(student, course.id))
        max_scores_cache = MaxScoresCache.create_for_course(course)
        # For the moment, we have to get scorable_locations from field_data_cache
        # and not from scores_client, because scores_client is ignorant of things
        # in the submissions API. As a further refactoring step, submissions should
        # be hidden behind the ScoresClient.
        max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations)

        grading_context = course.grading_context
        raw_scores = []

        totaled_scores = {}
        # 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():
            format_scores = []
            for section in sections:
                section_descriptor = section['section_descriptor']
                section_name = section_descriptor.display_name_with_default

                # some problems have state that is updated independently of interaction
                # with the LMS, so they need to always be scored. (E.g. foldit.,
                # combinedopenended)
                should_grade_section = any(
                    descriptor.always_recalculate_grades
                    for descriptor in section['xmoduledescriptors'])

                # If there are no problems that always have to be regraded, check to
                # see if any of our locations are in the scores from the submissions
                # API. If scores exist, we have to calculate grades for this section.
                if not should_grade_section:
                    should_grade_section = any(
                        descriptor.location.to_deprecated_string() in
                        submissions_scores
                        for descriptor in section['xmoduledescriptors'])

                if not should_grade_section:
                    should_grade_section = any(
                        descriptor.location in scores_client
                        for descriptor in section['xmoduledescriptors'])

                # If we haven't seen a single problem in the section, we don't have
                # to grade it at all! We can assume 0%
                if should_grade_section:
                    scores = []

                    def create_module(descriptor):
                        '''creates an XModule instance given a descriptor'''
                        # TODO: We need the request to pass into here. If we could forego that, our arguments
                        # would be simpler
                        return get_module_for_descriptor(student,
                                                         request,
                                                         descriptor,
                                                         field_data_cache,
                                                         course.id,
                                                         course=course)

                    descendants = yield_dynamic_descriptor_descendants(
                        section_descriptor, student.id, create_module)
                    for module_descriptor in descendants:
                        (correct, total) = get_score(
                            student,
                            module_descriptor,
                            create_module,
                            scores_client,
                            submissions_scores,
                            max_scores_cache,
                        )
                        if correct is None and total is None:
                            continue

                        if settings.GENERATE_PROFILE_SCORES:  # for debugging!
                            if total > 1:
                                correct = random.randrange(
                                    max(total - 2, 1), total + 1)
                            else:
                                correct = total

                        graded = module_descriptor.graded
                        if not total > 0:
                            # We simply cannot grade a problem that is 12/0, because we might need it as a percentage
                            graded = False

                        scores.append(
                            Score(correct, total, graded,
                                  module_descriptor.display_name_with_default,
                                  module_descriptor.location))

                    __, graded_total = aggregate_section_scores(
                        scores, section_name,
                        getattr(section_descriptor, 'weight', 1.0))
                    if keep_raw_scores:
                        raw_scores += scores
                else:
                    graded_total = WeightedScore(
                        0.0, 1.0, True, section_name, None,
                        getattr(section_descriptor, 'weight', 1.0))

                # Add the graded total to totaled_scores
                if graded_total.possible > 0:
                    format_scores.append(graded_total)
                else:
                    log.info(
                        "Unable to grade a section with a total possible score of zero. {}"
                        .format(section_descriptor.location))

            totaled_scores[section_format] = format_scores

        # Grading policy might be overriden by a CCX, need to reset it
        course.set_grading_policy(course.grading_policy)
        grade_summary = course.grader.grade(
            totaled_scores,
            generate_random_scores=settings.GENERATE_PROFILE_SCORES)

        # 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

        letter_grade = grade_for_percentage(course.grade_cutoffs,
                                            grade_summary['percent'],
                                            grade_summary['sections_passed'])
        grade_summary['grade'] = letter_grade
        grade_summary[
            'totaled_scores'] = totaled_scores  # make this available, eg for instructor download & debugging
        if keep_raw_scores:
            # way to get all RAW scores out to instructor
            # so grader can be double-checked
            grade_summary['raw_scores'] = raw_scores

        max_scores_cache.push_to_remote()

        return grade_summary
Пример #7
0
    def progress_summary(student,
                         request,
                         course,
                         field_data_cache=None,
                         scores_client=None,
                         grading_type='vertical'):
        """
        This pulls a summary of all problems in the course.

        Returns
        - 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,
        etc.

        Arguments:
            student: A User object for the student to grade
            course: A Descriptor containing the course to grade

        If the student does not have access to load the course module, this function
        will return None.

        """

        with manual_transaction():
            if field_data_cache is None:
                field_data_cache = field_data_cache_for_grading(
                    course, student)
            if scores_client is None:
                scores_client = ScoresClient.from_field_data_cache(
                    field_data_cache)

            course_module = get_module_for_descriptor(student,
                                                      request,
                                                      course,
                                                      field_data_cache,
                                                      course.id,
                                                      course=course)
            if not course_module:
                return None

            course_module = getattr(course_module, '_x_module', course_module)

        submissions_scores = sub_api.get_scores(
            course.id.to_deprecated_string(),
            anonymous_id_for_user(student, course.id))
        max_scores_cache = MaxScoresCache.create_for_course(course)
        # For the moment, we have to get scorable_locations from field_data_cache
        # and not from scores_client, because scores_client is ignorant of things
        # in the submissions API. As a further refactoring step, submissions should
        # be hidden behind the ScoresClient.
        max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations)

        blocks_stack = [course_module]
        blocks_dict = {}

        while blocks_stack:
            curr_block = blocks_stack.pop()
            with manual_transaction():
                # Skip if the block is hidden
                if curr_block.hide_from_toc:
                    continue

                key = unicode(curr_block.scope_ids.usage_id)
                children = curr_block.get_display_items(
                ) if curr_block.category != grading_type else []
                block = {
                    'display_name':
                    curr_block.display_name_with_default,
                    'block_type':
                    curr_block.category,
                    'url_name':
                    curr_block.url_name,
                    'children':
                    [unicode(child.scope_ids.usage_id) for child in children],
                }

                if curr_block.category == grading_type:
                    graded = curr_block.graded
                    scores = []

                    module_creator = curr_block.xmodule_runtime.get_module
                    for module_descriptor in yield_dynamic_descriptor_descendants(
                            curr_block, student.id, module_creator):
                        (correct, total) = get_score(
                            student,
                            module_descriptor,
                            module_creator,
                            scores_client,
                            submissions_scores,
                            max_scores_cache,
                        )

                        if correct is None and total is None:
                            continue

                        scores.append(
                            Score(correct, total, graded,
                                  module_descriptor.display_name_with_default,
                                  module_descriptor.location))

                    scores.reverse()
                    total, _ = aggregate_scores(
                        scores, curr_block.display_name_with_default)

                    module_format = curr_block.format if curr_block.format is not None else ''
                    block.update({
                        'scores': scores,
                        'total': total,
                        'format': module_format,
                        'due': curr_block.due,
                        'graded': graded,
                    })

                blocks_dict[key] = block
                # Add this blocks children to the stack so that we can traverse them as well.
                blocks_stack.extend(children)

        max_scores_cache.push_to_remote()

        return {
            'root': unicode(course.scope_ids.usage_id),
            'blocks': blocks_dict,
        }
Пример #8
0
    def progress_summary(student, request, course, field_data_cache=None, scores_client=None):
        """
        This pulls a summary of all problems in the course.
        Returns
        - 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,
        etc.
        Arguments:
            student: A User object for the student to grade
            course: A Descriptor containing the course to grade
        If the student does not have access to load the course module, this function
        will return None.
        """
        with manual_transaction():
            if field_data_cache is None:
                field_data_cache = field_data_cache_for_grading(course, student)
            if scores_client is None:
                scores_client = ScoresClient.from_field_data_cache(field_data_cache)

            course_module = get_module_for_descriptor(
                student, request, course, field_data_cache, course.id, course=course
            )
            if not course_module:
                return None

            course_module = getattr(course_module, '_x_module', course_module)

        submissions_scores = sub_api.get_scores(
            course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id)
        )
        max_scores_cache = MaxScoresCache.create_for_course(course)
        # For the moment, we have to get scorable_locations from field_data_cache
        # and not from scores_client, because scores_client is ignorant of things
        # in the submissions API. As a further refactoring step, submissions should
        # be hidden behind the ScoresClient.
        max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations)

        chapters = []
        # Don't include chapters that aren't displayable (e.g. due to error)
        for chapter_module in course_module.get_display_items():
            # Skip if the chapter is hidden
            if chapter_module.hide_from_toc:
                continue

            sections = []

            for section_module in chapter_module.get_display_items():
                # Skip if the section is hidden
                with manual_transaction():
                    if section_module.hide_from_toc:
                        continue

                    graded = section_module.graded
                    scores = []

                    module_creator = section_module.xmodule_runtime.get_module

                    for module_descriptor in yield_dynamic_descriptor_descendants(
                            section_module, student.id, module_creator
                    ):
                        (correct, total) = get_score(
                            student,
                            module_descriptor,
                            module_creator,
                            scores_client,
                            submissions_scores,
                            max_scores_cache,
                        )
                        if correct is None and total is None:
                            continue

                        scores.append(
                            Score(
                                correct,
                                total,
                                graded,
                                module_descriptor.display_name_with_default,
                                module_descriptor.location
                            )
                        )

                    scores.reverse()
                    section_total, _ = graders.aggregate_scores(
                        scores, section_module.display_name_with_default)

                    module_format = section_module.format if section_module.format is not None else ''
                    sections.append({
                        'display_name': section_module.display_name_with_default,
                        'url_name': section_module.url_name,
                        'scores': scores,
                        'section_total': section_total,
                        'format': module_format,
                        'due': section_module.due,
                        'graded': graded,
                    })

            chapters.append({
                'course': course.display_name_with_default,
                'display_name': chapter_module.display_name_with_default,
                'url_name': chapter_module.url_name,
                'sections': sections
            })

        max_scores_cache.push_to_remote()

        return chapters
Пример #9
0
    def progress_summary(student,
                         request,
                         course,
                         field_data_cache=None,
                         scores_client=None):
        """
        This pulls a summary of all problems in the course.
        Returns
        - 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,
        etc.
        Arguments:
            student: A User object for the student to grade
            course: A Descriptor containing the course to grade
        If the student does not have access to load the course module, this function
        will return None.
        """
        with manual_transaction():
            if field_data_cache is None:
                field_data_cache = field_data_cache_for_grading(
                    course, student)
            if scores_client is None:
                scores_client = ScoresClient.from_field_data_cache(
                    field_data_cache)

            course_module = get_module_for_descriptor(student,
                                                      request,
                                                      course,
                                                      field_data_cache,
                                                      course.id,
                                                      course=course)
            if not course_module:
                return None

            course_module = getattr(course_module, '_x_module', course_module)

        submissions_scores = sub_api.get_scores(
            course.id.to_deprecated_string(),
            anonymous_id_for_user(student, course.id))
        max_scores_cache = MaxScoresCache.create_for_course(course)
        # For the moment, we have to get scorable_locations from field_data_cache
        # and not from scores_client, because scores_client is ignorant of things
        # in the submissions API. As a further refactoring step, submissions should
        # be hidden behind the ScoresClient.
        max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations)

        chapters = []
        # Don't include chapters that aren't displayable (e.g. due to error)
        for chapter_module in course_module.get_display_items():
            # Skip if the chapter is hidden
            if chapter_module.hide_from_toc:
                continue

            sections = []

            for section_module in chapter_module.get_display_items():
                # Skip if the section is hidden
                with manual_transaction():
                    if section_module.hide_from_toc:
                        continue

                    graded = section_module.graded
                    scores = []

                    module_creator = section_module.xmodule_runtime.get_module

                    for module_descriptor in yield_dynamic_descriptor_descendants(
                            section_module, student.id, module_creator):
                        (correct, total) = get_score(
                            student,
                            module_descriptor,
                            module_creator,
                            scores_client,
                            submissions_scores,
                            max_scores_cache,
                        )
                        if correct is None and total is None:
                            continue

                        scores.append(
                            Score(correct, total, graded,
                                  module_descriptor.display_name_with_default,
                                  module_descriptor.location))

                    scores.reverse()
                    section_total, _ = graders.aggregate_scores(
                        scores, section_module.display_name_with_default)

                    module_format = section_module.format if section_module.format is not None else ''
                    sections.append({
                        'display_name':
                        section_module.display_name_with_default,
                        'url_name': section_module.url_name,
                        'scores': scores,
                        'section_total': section_total,
                        'format': module_format,
                        'due': section_module.due,
                        'graded': graded,
                    })

            chapters.append({
                'course': course.display_name_with_default,
                'display_name': chapter_module.display_name_with_default,
                'url_name': chapter_module.url_name,
                'sections': sections
            })

        max_scores_cache.push_to_remote()

        return chapters