diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/grading.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/grading.py index 1ff8b794f83d..41d2b72811e0 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/grading.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/grading.py @@ -40,3 +40,6 @@ class CourseGradingSerializer(serializers.Serializer): course_details = CourseGradingModelSerializer() show_credit_eligibility = serializers.BooleanField() is_credit_course = serializers.BooleanField() + default_grade_designations = serializers.ListSerializer( + child=serializers.CharField() + ) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/grading.py b/cms/djangoapps/contentstore/rest_api/v1/views/grading.py index ca405446b8f1..9275fecb58ab 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/grading.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/grading.py @@ -85,7 +85,8 @@ def get(self, request: Request, course_id: str): "minimum_grade_credit": 0.7 }, "show_credit_eligibility": false, - "is_credit_course": true + "is_credit_course": true, + "default_grade_designations": ["A","B","C","D"] } ``` """ diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_grading.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_grading.py index f12bf13afe54..6123096048df 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_grading.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_grading.py @@ -42,11 +42,22 @@ def test_course_grading_response(self): "course_details": grading_data.__dict__, "show_credit_eligibility": False, "is_credit_course": False, + "default_grade_designations": ['A', 'B', 'C', 'D'], } self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertDictEqual(expected_response, response.data) + @patch("django.conf.settings.DEFAULT_GRADE_DESIGNATIONS", ['A', 'B']) + def test_default_grade_designations_setting(self): + """ + Check that DEFAULT_GRADE_DESIGNATIONS setting reflects correctly in API. + """ + response = self.client.get(self.url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(['A', 'B'], response.data["default_grade_designations"]) + @patch.dict("django.conf.settings.FEATURES", {"ENABLE_CREDIT_ELIGIBILITY": True}) def test_credit_eligibility_setting(self): """ diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 8fec374ae6e6..3fe718dfa149 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1486,7 +1486,8 @@ def get_course_grading(course_key): 'grading_url': reverse_course_url('grading_handler', course_key), 'is_credit_course': is_credit_course(course_key), 'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course_key), - 'course_assignment_lists': dict(course_assignment_lists) + 'course_assignment_lists': dict(course_assignment_lists), + 'default_grade_designations': settings.DEFAULT_GRADE_DESIGNATIONS } return grading_context diff --git a/cms/envs/common.py b/cms/envs/common.py index 6aa2dfb835b1..ddd79f6d169c 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2439,6 +2439,15 @@ # Rate limit for regrading tasks that a grading policy change can kick off POLICY_CHANGE_TASK_RATE_LIMIT = '900/h' +# .. setting_name: DEFAULT_GRADE_DESIGNATIONS +# .. setting_default: ['A', 'B', 'C', 'D'] +# .. setting_description: The default 'pass' grade cutoff designations to be used. The failure grade +# is always 'F' and should not be included in this list. +# .. setting_warning: The DEFAULT_GRADE_DESIGNATIONS list must have more than one designation, +# or else ['A', 'B', 'C', 'D'] will be used as the default grade designations. Also, only the first +# 11 grade designations are used by the UI, so it's advisable to restrict the list to 11 items. +DEFAULT_GRADE_DESIGNATIONS = ['A', 'B', 'C', 'D'] + ############## Settings for CourseGraph ############################ # .. setting_name: COURSEGRAPH_JOB_QUEUE diff --git a/cms/static/js/factories/settings_graders.js b/cms/static/js/factories/settings_graders.js index dc75029e0f26..61b8fcbcad03 100644 --- a/cms/static/js/factories/settings_graders.js +++ b/cms/static/js/factories/settings_graders.js @@ -3,7 +3,7 @@ define([ ], function($, GradingView, CourseGradingPolicyModel) { 'use strict'; - return function(courseDetails, gradingUrl, courseAssignmentLists) { + return function(courseDetails, gradingUrl, courseAssignmentLists, gradeDesignations) { var model, editor; $('form :input') @@ -19,7 +19,8 @@ define([ editor = new GradingView({ el: $('.settings-grading'), model: model, - courseAssignmentLists: courseAssignmentLists + courseAssignmentLists: courseAssignmentLists, + gradeDesignations: gradeDesignations }); editor.render(); }; diff --git a/cms/static/js/views/settings/grading.js b/cms/static/js/views/settings/grading.js index dcb2ce9b3e96..71fdfe3f9a8c 100644 --- a/cms/static/js/views/settings/grading.js +++ b/cms/static/js/views/settings/grading.js @@ -34,6 +34,7 @@ function(ValidatingView, _, $, ui, GraderView, StringUtils, HtmlUtils) { $('#course_grade_cutoff-tpl').text() ); this.setupCutoffs(); + this.setupGradeDesignations(options.gradeDesignations); this.listenTo(this.model, 'invalid', this.handleValidationError); this.listenTo(this.model, 'change', this.showNotificationBar); @@ -318,7 +319,7 @@ function(ValidatingView, _, $, ui, GraderView, StringUtils, HtmlUtils) { addNewGrade: function(e) { e.preventDefault(); var gradeLength = this.descendingCutoffs.length; // cutoffs doesn't include fail/f so this is only the passing grades - if (gradeLength > 3) { + if (gradeLength > this.GRADES.length - 1) { // TODO shouldn't we disable the button return; } @@ -399,6 +400,9 @@ function(ValidatingView, _, $, ui, GraderView, StringUtils, HtmlUtils) { this.descendingCutoffs = _.sortBy(this.descendingCutoffs, function(gradeEle) { return -gradeEle.cutoff; }); }, + setupGradeDesignations: function(gradeDesignations) { + if (Array.isArray(gradeDesignations) && gradeDesignations.length > 1) { this.GRADES = gradeDesignations.slice(0, 11); } + }, revertView: function() { var self = this; this.model.fetch({ diff --git a/cms/static/sass/views/_settings.scss b/cms/static/sass/views/_settings.scss index cc62fd436f0d..c571af4b8cb8 100644 --- a/cms/static/sass/views/_settings.scss +++ b/cms/static/sass/views/_settings.scss @@ -777,23 +777,23 @@ height: 17px; } - &:nth-child(1) { + &:nth-child(5n+1) { background: #4fe696; } - &:nth-child(2) { + &:nth-child(5n+2) { background: #ffdf7e; } - &:nth-child(3) { + &:nth-child(5n+3) { background: #ffb657; } - &:nth-child(4) { + &:nth-child(5n+4) { background: #ef54a1; } - &:nth-child(5), + &:nth-child(5n+5), &.bar-fail { background: #fb336c; } diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html index c3b6f8f73a2a..eb0a057b046e 100644 --- a/cms/templates/settings_graders.html +++ b/cms/templates/settings_graders.html @@ -37,6 +37,7 @@ ), "${grading_url | n, js_escaped_string}", ${course_assignment_lists | n, dump_js_escaped_json}, + ${default_grade_designations | n, dump_js_escaped_json}, ); });