Add an /api/courses/v1/grading endpoint to get assignment type and subsection info about a course.
This commit is contained in:
committed by
Alex Dusenbery
parent
30eb003b2e
commit
009074ec4b
0
cms/djangoapps/contentstore/api/tests/__init__.py
Normal file
0
cms/djangoapps/contentstore/api/tests/__init__.py
Normal file
93
cms/djangoapps/contentstore/api/tests/base.py
Normal file
93
cms/djangoapps/contentstore/api/tests/base.py
Normal file
@@ -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
|
||||
}
|
||||
)
|
||||
159
cms/djangoapps/contentstore/api/tests/test_grading.py
Normal file
159
cms/djangoapps/contentstore/api/tests/test_grading.py
Normal file
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
101
cms/djangoapps/contentstore/api/views/course_grading.py
Normal file
101
cms/djangoapps/contentstore/api/views/course_grading.py
Normal file
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user