From 009074ec4b5a04ac5fdedb63e868d19c024c0da0 Mon Sep 17 00:00:00 2001 From: Alex Dusenbery Date: Mon, 19 Nov 2018 17:27:15 -0500 Subject: [PATCH] Add an /api/courses/v1/grading endpoint to get assignment type and subsection info about a course. --- .../contentstore/api/tests/__init__.py | 0 cms/djangoapps/contentstore/api/tests/base.py | 93 ++++++++++ .../contentstore/api/tests/test_grading.py | 159 ++++++++++++++++++ .../contentstore/api/tests/test_quality.py | 92 +--------- .../contentstore/api/tests/test_validation.py | 3 + cms/djangoapps/contentstore/api/urls.py | 7 +- .../contentstore/api/views/course_grading.py | 101 +++++++++++ .../contentstore/api/views/utils.py | 92 +++++++++- common/lib/xmodule/xmodule/graders.py | 9 +- common/lib/xmodule/xmodule/util/misc.py | 37 ++++ .../grades/api/v1/tests/test_views.py | 2 +- lms/djangoapps/grades/api/v1/views.py | 14 +- 12 files changed, 511 insertions(+), 98 deletions(-) create mode 100644 cms/djangoapps/contentstore/api/tests/__init__.py create mode 100644 cms/djangoapps/contentstore/api/tests/base.py create mode 100644 cms/djangoapps/contentstore/api/tests/test_grading.py create mode 100644 cms/djangoapps/contentstore/api/views/course_grading.py diff --git a/cms/djangoapps/contentstore/api/tests/__init__.py b/cms/djangoapps/contentstore/api/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cms/djangoapps/contentstore/api/tests/base.py b/cms/djangoapps/contentstore/api/tests/base.py new file mode 100644 index 0000000000..d8bd02ee17 --- /dev/null +++ b/cms/djangoapps/contentstore/api/tests/base.py @@ -0,0 +1,93 @@ +""" +Base test case for the course API views. +""" +from django.core.urlresolvers import reverse +from rest_framework.test import APITestCase + +from lms.djangoapps.courseware.tests.factories import StaffFactory +from student.tests.factories import UserFactory +from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + + +# pylint: disable=unused-variable +class BaseCourseViewTest(SharedModuleStoreTestCase, APITestCase): + """ + Base test class for course data views. + """ + MODULESTORE = TEST_DATA_SPLIT_MODULESTORE + view_name = None # The name of the view to use in reverse() call in self.get_url() + + @classmethod + def setUpClass(cls): + super(BaseCourseViewTest, cls).setUpClass() + + cls.course = CourseFactory.create(display_name='test course', run="Testing_course") + cls.course_key = cls.course.id + + cls.password = 'test' + cls.student = UserFactory(username='dummy', password=cls.password) + cls.staff = StaffFactory(course_key=cls.course.id, password=cls.password) + + cls.initialize_course(cls.course) + + @classmethod + def initialize_course(cls, course): + """ + Sets up the structure of the test course. + """ + course.self_paced = True + cls.store.update_item(course, cls.staff.id) + + cls.section = ItemFactory.create( + parent_location=course.location, + category="chapter", + ) + cls.subsection1 = ItemFactory.create( + parent_location=cls.section.location, + category="sequential", + ) + unit1 = ItemFactory.create( + parent_location=cls.subsection1.location, + category="vertical", + ) + ItemFactory.create( + parent_location=unit1.location, + category="video", + ) + ItemFactory.create( + parent_location=unit1.location, + category="problem", + ) + + cls.subsection2 = ItemFactory.create( + parent_location=cls.section.location, + category="sequential", + ) + unit2 = ItemFactory.create( + parent_location=cls.subsection2.location, + category="vertical", + ) + unit3 = ItemFactory.create( + parent_location=cls.subsection2.location, + category="vertical", + ) + ItemFactory.create( + parent_location=unit3.location, + category="video", + ) + ItemFactory.create( + parent_location=unit3.location, + category="video", + ) + + def get_url(self, course_id): + """ + Helper function to create the url + """ + return reverse( + self.view_name, + kwargs={ + 'course_id': course_id + } + ) diff --git a/cms/djangoapps/contentstore/api/tests/test_grading.py b/cms/djangoapps/contentstore/api/tests/test_grading.py new file mode 100644 index 0000000000..520b7ccf47 --- /dev/null +++ b/cms/djangoapps/contentstore/api/tests/test_grading.py @@ -0,0 +1,159 @@ +""" +Tests for the course grading API view +""" +from rest_framework import status +from six import text_type + +from xmodule.modulestore.tests.factories import ItemFactory + +from .base import BaseCourseViewTest + + +class CourseGradingViewTest(BaseCourseViewTest): + """ + Test course grading view via a RESTful API + """ + view_name = 'courses_api:course_grading' + + @classmethod + def setUpClass(cls): + super(CourseGradingViewTest, cls).setUpClass() + cls.homework = ItemFactory.create( + parent_location=cls.section.location, + category="sequential", + graded=True, + format='Homework', + ) + cls.midterm = ItemFactory.create( + parent_location=cls.section.location, + category="sequential", + graded=True, + format='Midterm Exam', + ) + + def test_student_fails(self): + self.client.login(username=self.student.username, password=self.password) + resp = self.client.get(self.get_url(self.course_key)) + self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) + + def test_staff_succeeds(self): + self.client.login(username=self.staff.username, password=self.password) + resp = self.client.get(self.get_url(self.course_key)) + self.assertEqual(resp.status_code, status.HTTP_200_OK) + expected_data = { + 'assignment_types': { + 'Final Exam': { + 'drop_count': 0, + 'min_count': 1, + 'short_label': 'Final', + 'type': 'Final Exam', + 'weight': 0.4 + }, + 'Homework': { + 'drop_count': 2, + 'min_count': 12, + 'short_label': 'HW', + 'type': 'Homework', + 'weight': 0.15 + }, + 'Lab': { + 'drop_count': 2, + 'min_count': 12, + 'short_label': 'Lab', + 'type': 'Lab', + 'weight': 0.15 + }, + 'Midterm Exam': { + 'drop_count': 0, + 'min_count': 1, + 'short_label': 'Midterm', + 'type': 'Midterm Exam', + 'weight': 0.3 + } + }, + 'subsections': [ + { + 'assignment_type': None, + 'display_name': self.subsection1.display_name, + 'graded': False, + 'module_id': text_type(self.subsection1.location), + 'short_label': None + }, + { + 'assignment_type': None, + 'display_name': self.subsection2.display_name, + 'graded': False, + 'module_id': text_type(self.subsection2.location), + 'short_label': None + }, + { + 'assignment_type': 'Homework', + 'display_name': self.homework.display_name, + 'graded': True, + 'module_id': text_type(self.homework.location), + 'short_label': 'HW 01', + }, + { + 'assignment_type': 'Midterm Exam', + 'display_name': self.midterm.display_name, + 'graded': True, + 'module_id': text_type(self.midterm.location), + 'short_label': 'Midterm 01', + }, + ] + } + self.assertEqual(expected_data, resp.data) + + def test_staff_succeeds_graded_only(self): + self.client.login(username=self.staff.username, password=self.password) + resp = self.client.get(self.get_url(self.course_key), {'graded_only': True}) + self.assertEqual(resp.status_code, status.HTTP_200_OK) + expected_data = { + 'assignment_types': { + 'Final Exam': { + 'drop_count': 0, + 'min_count': 1, + 'short_label': 'Final', + 'type': 'Final Exam', + 'weight': 0.4 + }, + 'Homework': { + 'drop_count': 2, + 'min_count': 12, + 'short_label': 'HW', + 'type': 'Homework', + 'weight': 0.15 + }, + 'Lab': { + 'drop_count': 2, + 'min_count': 12, + 'short_label': 'Lab', + 'type': 'Lab', + 'weight': 0.15 + }, + 'Midterm Exam': { + 'drop_count': 0, + 'min_count': 1, + 'short_label': 'Midterm', + 'type': 'Midterm Exam', + 'weight': 0.3 + } + }, + 'subsections': [ + { + 'assignment_type': 'Homework', + 'display_name': self.homework.display_name, + 'graded': True, + 'module_id': text_type(self.homework.location), + 'short_label': 'HW 01', + }, + { + 'assignment_type': 'Midterm Exam', + 'display_name': self.midterm.display_name, + 'graded': True, + 'module_id': text_type(self.midterm.location), + 'short_label': 'Midterm 01', + }, + ] + } + self.assertEqual(expected_data, resp.data) diff --git a/cms/djangoapps/contentstore/api/tests/test_quality.py b/cms/djangoapps/contentstore/api/tests/test_quality.py index 4addfed5be..14249ffaa9 100644 --- a/cms/djangoapps/contentstore/api/tests/test_quality.py +++ b/cms/djangoapps/contentstore/api/tests/test_quality.py @@ -1,97 +1,16 @@ """ Tests for the course import API views """ -from django.core.urlresolvers import reverse from rest_framework import status -from rest_framework.test import APITestCase -from lms.djangoapps.courseware.tests.factories import StaffFactory -from student.tests.factories import UserFactory -from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from .base import BaseCourseViewTest -class CourseQualityViewTest(SharedModuleStoreTestCase, APITestCase): +class CourseQualityViewTest(BaseCourseViewTest): """ Test course quality view via a RESTful API """ - MODULESTORE = TEST_DATA_SPLIT_MODULESTORE - - @classmethod - def setUpClass(cls): - super(CourseQualityViewTest, cls).setUpClass() - - cls.course = CourseFactory.create(display_name='test course', run="Testing_course") - cls.course_key = cls.course.id - - cls.password = 'test' - cls.student = UserFactory(username='dummy', password=cls.password) - cls.staff = StaffFactory(course_key=cls.course.id, password=cls.password) - - cls.initialize_course(cls.course) - - @classmethod - def initialize_course(cls, course): - course.self_paced = True - cls.store.update_item(course, cls.staff.id) - - section = ItemFactory.create( - parent_location=course.location, - category="chapter", - ) - subsection1 = ItemFactory.create( - parent_location=section.location, - category="sequential", - ) - unit1 = ItemFactory.create( - parent_location=subsection1.location, - category="vertical", - ) - ItemFactory.create( - parent_location=unit1.location, - category="video", - ) - ItemFactory.create( - parent_location=unit1.location, - category="problem", - ) - - subsection2 = ItemFactory.create( - parent_location=section.location, - category="sequential", - ) - unit2 = ItemFactory.create( - parent_location=subsection2.location, - category="vertical", - ) - unit3 = ItemFactory.create( - parent_location=subsection2.location, - category="vertical", - ) - ItemFactory.create( - parent_location=unit3.location, - category="video", - ) - ItemFactory.create( - parent_location=unit3.location, - category="video", - ) - - def get_url(self, course_id): - """ - Helper function to create the url - """ - return reverse( - 'courses_api:course_quality', - kwargs={ - 'course_id': course_id - } - ) - - def test_student_fails(self): - self.client.login(username=self.student.username, password=self.password) - resp = self.client.get(self.get_url(self.course_key)) - self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) + view_name = 'courses_api:course_quality' def test_staff_succeeds(self): self.client.login(username=self.staff.username, password=self.password) @@ -141,3 +60,8 @@ class CourseQualityViewTest(SharedModuleStoreTestCase, APITestCase): 'is_self_paced': True, } self.assertDictEqual(resp.data, expected_data) + + def test_student_fails(self): + self.client.login(username=self.student.username, password=self.password) + resp = self.client.get(self.get_url(self.course_key)) + self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) diff --git a/cms/djangoapps/contentstore/api/tests/test_validation.py b/cms/djangoapps/contentstore/api/tests/test_validation.py index d7036e26c3..ecbd6cbffe 100644 --- a/cms/djangoapps/contentstore/api/tests/test_validation.py +++ b/cms/djangoapps/contentstore/api/tests/test_validation.py @@ -33,6 +33,9 @@ class CourseValidationViewTest(SharedModuleStoreTestCase, APITestCase): @classmethod def initialize_course(cls, course): + """ + Sets up test course structure. + """ course.start = datetime.now() course.self_paced = True cls.store.update_item(course, cls.staff.id) diff --git a/cms/djangoapps/contentstore/api/urls.py b/cms/djangoapps/contentstore/api/urls.py index 97cec4c1ee..88284c5ab1 100644 --- a/cms/djangoapps/contentstore/api/urls.py +++ b/cms/djangoapps/contentstore/api/urls.py @@ -2,7 +2,10 @@ from django.conf import settings from django.conf.urls import url -from cms.djangoapps.contentstore.api.views import course_import, course_validation, course_quality +from cms.djangoapps.contentstore.api.views import course_grading, course_import, course_quality, course_validation + + +app_name = 'contentstore' urlpatterns = [ url(r'^v0/import/{course_id}/$'.format(course_id=settings.COURSE_ID_PATTERN,), @@ -11,4 +14,6 @@ urlpatterns = [ course_validation.CourseValidationView.as_view(), name='course_validation'), url(r'^v1/quality/{course_id}/$'.format(course_id=settings.COURSE_ID_PATTERN,), course_quality.CourseQualityView.as_view(), name='course_quality'), + url(r'^v1/grading/{course_id}/$'.format(course_id=settings.COURSE_ID_PATTERN,), + course_grading.CourseGradingView.as_view(), name='course_grading'), ] diff --git a/cms/djangoapps/contentstore/api/views/course_grading.py b/cms/djangoapps/contentstore/api/views/course_grading.py new file mode 100644 index 0000000000..587aaa4b5e --- /dev/null +++ b/cms/djangoapps/contentstore/api/views/course_grading.py @@ -0,0 +1,101 @@ +""" +Defines an endpoint for retrieving assignment type and subsection info for a course. +""" +from rest_framework.response import Response +from six import text_type + +from xmodule.util.misc import get_default_short_labeler + +from .utils import BaseCourseView, course_author_access_required, get_bool_param + + +class CourseGradingView(BaseCourseView): + """ + Returns information about assignments and assignment types for a course. + **Example Requests** + + GET /api/courses/v1/grading/{course_id}/ + + **GET Parameters** + + A GET request may include the following parameters. + + * graded_only (boolean) - If true, only returns subsection data for graded subsections (defaults to False). + + **GET Response Values** + + The HTTP 200 response has the following values. + + * assignment_types - A dictionary keyed by the assignment type name with the following values: + * min_count - The minimum number of required assignments of this type. + * weight - The weight assigned to this assignment type for course grading. + * type - The name of the assignment type. + * drop_count - The maximum number of assignments of this type that can be dropped. + * short_label - The short label prefix used for short labels of assignments of this type (e.g. 'HW'). + + * subsections - A list of subsections contained in this course. + * module_id - The string version of this subsection's location. + * display_name - The display name of this subsection. + * graded - Boolean indicating whether this subsection is graded (for at least one user in the course). + * short_label - A short label for graded assignments (e.g. 'HW 01'). + * assignment_type - The assignment type of this subsection (for graded assignments only). + + """ + @course_author_access_required + def get(self, request, course_key): + """ + Returns grading information (which subsections are graded, assignment types) for + the requested course. + """ + graded_only = get_bool_param(request, 'graded_only', False) + + with self.get_course(request, course_key) as course: + results = { + 'assignment_types': self._get_assignment_types(course), + 'subsections': self._get_subsections(course, graded_only), + } + return Response(results) + + def _get_assignment_types(self, course): + """ + Helper function that returns a serialized dict of assignment types + for the given course. + Args: + course - A course object. + """ + serialized_grading_policies = {} + for grader, assignment_type, weight in course.grader.subgraders: + serialized_grading_policies[assignment_type] = { + 'type': assignment_type, + 'short_label': grader.short_label, + 'min_count': grader.min_count, + 'drop_count': grader.drop_count, + 'weight': weight, + } + return serialized_grading_policies + + def _get_subsections(self, course, graded_only=False): + """ + Helper function that returns a list of subsections contained in the given course. + Args: + course - A course object. + graded_only - If true, returns only graded subsections (defaults to False). + """ + subsections = [] + short_labeler = get_default_short_labeler(course) + for subsection in self._get_visible_subsections(course): + if graded_only and not subsection.graded: + continue + + short_label = None + if subsection.graded: + short_label = short_labeler(subsection.format) + + subsections.append({ + 'assignment_type': subsection.format, + 'graded': subsection.graded, + 'short_label': short_label, + 'module_id': text_type(subsection.location), + 'display_name': subsection.display_name, + }) + return subsections diff --git a/cms/djangoapps/contentstore/api/views/utils.py b/cms/djangoapps/contentstore/api/views/utils.py index 589ce3de04..bbf0534bc3 100644 --- a/cms/djangoapps/contentstore/api/views/utils.py +++ b/cms/djangoapps/contentstore/api/views/utils.py @@ -1,15 +1,103 @@ """ Common utilities for Contentstore APIs. """ -from rest_framework import status +from contextlib import contextmanager +from rest_framework import status +from rest_framework.generics import GenericAPIView from opaque_keys.edx.keys import CourseKey + from openedx.core.djangoapps.util.forms import to_bool -from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin +from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes +from openedx.core.lib.cache_utils import request_cached from student.auth import has_course_author_access +from xmodule.modulestore.django import modulestore + + +@view_auth_classes() +class BaseCourseView(DeveloperErrorViewMixin, GenericAPIView): + """ + A base class for contentstore course api views. + """ + @contextmanager + def get_course(self, request, course_key): + """ + Context manager that yields a course, given a request and course_key. + """ + store = modulestore() + with store.bulk_operations(course_key): + course = store.get_course(course_key, depth=self._required_course_depth(request)) + yield course + + @staticmethod + def _required_course_depth(request): + """ + Returns how far deep we need to go into the course tree to + get all of the information required. Will use entire tree if the request's + `all` param is truthy, otherwise goes to depth of 2 (subsections). + """ + all_requested = get_bool_param(request, 'all', False) + if all_requested: + return None + return 2 + + @classmethod + @request_cached() + def _get_visible_subsections(cls, course): + """ + Returns a list of all visible subsections for a course. + """ + _, visible_sections = cls._get_sections(course) + visible_subsections = [] + for section in visible_sections: + visible_subsections.extend(cls._get_visible_children(section)) + return visible_subsections + + @classmethod + @request_cached() + def _get_sections(cls, course): + """ + Returns all sections in the course. + """ + return cls._get_all_children(course) + + @classmethod + def _get_all_children(cls, parent): + """ + Returns all child nodes of the given parent. + """ + store = modulestore() + children = [store.get_item(child_usage_key) for child_usage_key in cls._get_children(parent)] + visible_children = [ + c for c in children + if not c.visible_to_staff_only and not c.hide_from_toc + ] + return children, visible_children + + @classmethod + def _get_visible_children(cls, parent): + """ + Returns only the visible children of the given parent. + """ + _, visible_chidren = cls._get_all_children(parent) + return visible_chidren + + @classmethod + def _get_children(cls, parent): + """ + Returns the value of the 'children' attribute of a node. + """ + if not hasattr(parent, 'children'): + return [] + else: + return parent.children def get_bool_param(request, param_name, default): + """ + Given a request, parameter name, and default value, returns + either a boolean value or the default. + """ param_value = request.query_params.get(param_name, None) bool_value = to_bool(param_value) if bool_value is None: diff --git a/common/lib/xmodule/xmodule/graders.py b/common/lib/xmodule/xmodule/graders.py index f04a4bcd9e..f3bf301d82 100644 --- a/common/lib/xmodule/xmodule/graders.py +++ b/common/lib/xmodule/xmodule/graders.py @@ -16,6 +16,9 @@ from contracts import contract from pytz import UTC from django.utils.translation import ugettext_lazy as _ +from xmodule.util.misc import get_short_labeler + + log = logging.getLogger("edx.courseware") @@ -378,6 +381,7 @@ class AssignmentFormatGrader(CourseGrader): def grade(self, grade_sheet, generate_random_scores=False): scores = grade_sheet.get(self.type, {}).values() breakdown = [] + labeler = get_short_labeler(self.short_label) for i in range(max(self.min_count, len(scores))): if i < len(scores) or generate_random_scores: if generate_random_scores: # for debugging! @@ -407,10 +411,7 @@ class AssignmentFormatGrader(CourseGrader): index=i + self.starting_index, section_type=self.section_type ) - short_label = u"{short_label} {index:02d}".format( - index=i + self.starting_index, - short_label=self.short_label - ) + short_label = labeler(i + self.starting_index) breakdown.append({'percent': percentage, 'label': short_label, 'detail': summary, 'category': self.category}) diff --git a/common/lib/xmodule/xmodule/util/misc.py b/common/lib/xmodule/xmodule/util/misc.py index 13c1288a27..9e3e0b83b3 100644 --- a/common/lib/xmodule/xmodule/util/misc.py +++ b/common/lib/xmodule/xmodule/util/misc.py @@ -58,3 +58,40 @@ def escape_html_characters(content): ) ) ) + + +def get_short_labeler(prefix): + """ + Returns a labeling function that prepends + `prefix` to an assignment index. + """ + def labeler(index): + return u"{prefix} {index:02d}".format(prefix=prefix, index=index) + return labeler + + +def get_default_short_labeler(course): + """ + Returns a helper function that creates a default + short_label for a subsection. + """ + default_labelers = {} + for grader, assignment_type, _ in course.grader.subgraders: + default_labelers[assignment_type] = { + 'labeler': get_short_labeler(grader.short_label), + 'index': 1, + } + + def default_labeler(assignment_type): + """ + Given an assignment type, returns the next short_label + for that assignment type. For example, if the assignment_type + is "Homework" and this is the 2nd time the function has been called + for that assignment type, this function would return "Ex 02", assuming + that "Ex" is the short_label assigned to a grader for Homework subsections. + """ + labeler = default_labelers[assignment_type]['labeler'] + index = default_labelers[assignment_type]['index'] + default_labelers[assignment_type]['index'] += 1 + return labeler(index) + return default_labeler diff --git a/lms/djangoapps/grades/api/v1/tests/test_views.py b/lms/djangoapps/grades/api/v1/tests/test_views.py index 15022ba104..4cbbfd6b0b 100644 --- a/lms/djangoapps/grades/api/v1/tests/test_views.py +++ b/lms/djangoapps/grades/api/v1/tests/test_views.py @@ -1001,7 +1001,7 @@ class GradebookViewTest(GradebookViewTestBase): ('is_ag', False), ('is_average', False), ('is_manually_graded', False), - ('label', 'Ch. 02-03'), + ('label', 'HW 03'), ('letter_grade', 'A'), ('module_id', text_type(ungraded_subsection.location)), ('percent', 0.0), diff --git a/lms/djangoapps/grades/api/v1/views.py b/lms/djangoapps/grades/api/v1/views.py index fa7eb01e6d..7928e36085 100644 --- a/lms/djangoapps/grades/api/v1/views.py +++ b/lms/djangoapps/grades/api/v1/views.py @@ -40,6 +40,7 @@ from track.event_transaction_utils import ( get_event_transaction_type, set_event_transaction_type ) +from xmodule.util.misc import get_default_short_labeler log = logging.getLogger(__name__) USER_MODEL = get_user_model() @@ -529,7 +530,7 @@ class GradebookView(GradeViewMixin, PaginatedAPIView): required_scopes = ['grades:read'] - def _section_breakdown(self, course_grade): + def _section_breakdown(self, course, course_grade): """ Given a course_grade, returns a list of grade data broken down by subsection and a dictionary containing aggregate grade data by subsection format for the course. @@ -548,10 +549,11 @@ class GradebookView(GradeViewMixin, PaginatedAPIView): # 'grade_description' should be 'description_ratio' label_finder = SubsectionLabelFinder(course_grade) + default_labeler = get_default_short_labeler(course) - for chapter_index, (chapter_location, section_data) in enumerate(course_grade.chapter_grades.items(), start=1): - for subsection_index, subsection_grade in enumerate(section_data['sections'], start=1): - default_label = 'Ch. {:02d}-{:02d}'.format(chapter_index, subsection_index) + for chapter_location, section_data in course_grade.chapter_grades.items(): + for subsection_grade in section_data['sections']: + default_short_label = default_labeler(subsection_grade.format) breakdown.append({ 'are_grades_published': True, 'auto_grade': False, @@ -568,7 +570,7 @@ class GradebookView(GradeViewMixin, PaginatedAPIView): 'is_ag': False, 'is_average': False, 'is_manually_graded': False, - 'label': label_finder.get_label(subsection_grade.display_name) or default_label, + 'label': label_finder.get_label(subsection_grade.display_name) or default_short_label, 'letter_grade': course_grade.letter_grade, 'module_id': text_type(subsection_grade.location), 'percent': subsection_grade.percent_graded, @@ -594,7 +596,7 @@ class GradebookView(GradeViewMixin, PaginatedAPIView): course_grade: A CourseGrade object. """ user_entry = self._serialize_user_grade(user, course.id, course_grade) - breakdown, aggregates = self._section_breakdown(course_grade) + breakdown, aggregates = self._section_breakdown(course, course_grade) user_entry['section_breakdown'] = breakdown user_entry['aggregates'] = aggregates