diff --git a/common/test/acceptance/pages/lms/instructor_dashboard.py b/common/test/acceptance/pages/lms/instructor_dashboard.py index 3d6dab0ebd..7a7a82735a 100644 --- a/common/test/acceptance/pages/lms/instructor_dashboard.py +++ b/common/test/acceptance/pages/lms/instructor_dashboard.py @@ -101,6 +101,11 @@ class InstructorDashboardPage(CoursePage): ecommerce_section.wait_for_page() return ecommerce_section + def is_rescore_unsupported_message_visible(self): + return u'This component cannot be rescored.' in unicode( + self.q(css='.request-response-error').html + ) + @staticmethod def get_asset_path(file_name): """ diff --git a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py index 690d539b5a..9b03d69de0 100644 --- a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py +++ b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py @@ -15,14 +15,18 @@ from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage from common.test.acceptance.pages.lms.create_mode import ModeCreationPage from common.test.acceptance.pages.lms.courseware import CoursewarePage -from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage, EntranceExamAdmin +from common.test.acceptance.pages.lms.instructor_dashboard import ( + InstructorDashboardPage, + EntranceExamAdmin, + StudentSpecificAdmin, +) from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc from common.test.acceptance.pages.lms.dashboard import DashboardPage from common.test.acceptance.pages.lms.problem import ProblemPage from common.test.acceptance.pages.lms.pay_and_verify import PaymentAndVerificationFlow from common.test.acceptance.pages.lms.login_and_register import CombinedLoginAndRegisterPage from common.test.acceptance.pages.common.utils import enroll_user_track -from common.test.acceptance.tests.helpers import disable_animations +from common.test.acceptance.tests.helpers import disable_animations, create_multiple_choice_problem from common.test.acceptance.fixtures.certificates import CertificateConfigFixture @@ -1337,3 +1341,50 @@ class EcommerceTest(BaseInstructorDashboardTest): # Log in and visit E-commerce section under Instructor dashboard self.assertNotIn(u'Coupon Code List', self.visit_ecommerce_section().get_sections_header_values()) + + +class StudentAdminTest(BaseInstructorDashboardTest): + SECTION_NAME = 'Test Section 1' + SUBSECTION_NAME = 'Test Subsection 1' + UNIT_NAME = 'Test Unit 1' + PROBLEM_NAME = 'Test Problem 1' + + def setUp(self): + super(StudentAdminTest, self).setUp() + self.course_fix = CourseFixture( + self.course_info['org'], + self.course_info['number'], + self.course_info['run'], + self.course_info['display_name'] + ) + + self.problem = create_multiple_choice_problem(self.PROBLEM_NAME) + self.vertical = XBlockFixtureDesc('vertical', "Lab Unit") + self.course_fix.add_children( + XBlockFixtureDesc('chapter', self.SECTION_NAME).add_children( + XBlockFixtureDesc('sequential', self.SUBSECTION_NAME).add_children( + self.vertical.add_children(self.problem) + ) + ), + ).install() + + self.username, _ = self.log_in_as_instructor() + self.instructor_dashboard_page = self.visit_instructor_dashboard() + + def test_rescore_nonrescorable(self): + student_admin_section = self.instructor_dashboard_page.select_student_admin(StudentSpecificAdmin) + student_admin_section.set_student_email_or_username(self.username) + + # not a rescorable block + student_admin_section.set_problem_location(self.vertical.locator) + getattr(student_admin_section, 'rescore_button').click() + self.assertTrue(self.instructor_dashboard_page.is_rescore_unsupported_message_visible()) + + def test_rescore_rescorable(self): + student_admin_section = self.instructor_dashboard_page.select_student_admin(StudentSpecificAdmin) + student_admin_section.set_student_email_or_username(self.username) + student_admin_section.set_problem_location(self.problem.locator) + getattr(student_admin_section, 'rescore_button').click() + alert = get_modal_alert(student_admin_section.browser) + alert.dismiss() + self.assertFalse(self.instructor_dashboard_page.is_rescore_unsupported_message_visible()) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 12dd3f5819..9877119864 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -2118,18 +2118,24 @@ def rescore_problem(request, course_id): if student: response_payload['student'] = student_identifier - lms.djangoapps.instructor_task.api.submit_rescore_problem_for_student( - request, - module_state_key, - student, - only_if_higher, - ) + try: + lms.djangoapps.instructor_task.api.submit_rescore_problem_for_student( + request, + module_state_key, + student, + only_if_higher, + ) + except NotImplementedError as exc: + return HttpResponseBadRequest(exc.message) elif all_students: - lms.djangoapps.instructor_task.api.submit_rescore_problem_for_all_students( - request, - module_state_key, - only_if_higher, - ) + try: + lms.djangoapps.instructor_task.api.submit_rescore_problem_for_all_students( + request, + module_state_key, + only_if_higher, + ) + except NotImplementedError as exc: + return HttpResponseBadRequest(exc.message) else: return HttpResponseBadRequest() diff --git a/lms/djangoapps/instructor_task/api_helper.py b/lms/djangoapps/instructor_task/api_helper.py index 0523db6425..3a8a1a66c5 100644 --- a/lms/djangoapps/instructor_task/api_helper.py +++ b/lms/djangoapps/instructor_task/api_helper.py @@ -255,10 +255,14 @@ def check_arguments_for_rescoring(usage_key): in). An ItemNotFoundException is raised if the corresponding module descriptor doesn't exist. NotImplementedError is raised if the corresponding module doesn't support rescoring calls. + + Note: the string returned here is surfaced as the error + message on the instructor dashboard when a rescore is + submitted for a non-rescorable block. """ descriptor = modulestore().get_item(usage_key) if not _supports_rescore(descriptor): - msg = "Specified module does not support rescoring." + msg = _("This component cannot be rescored.") raise NotImplementedError(msg) diff --git a/lms/static/js/instructor_dashboard/student_admin.js b/lms/static/js/instructor_dashboard/student_admin.js index a2bc1ee0b2..63e92a87c8 100644 --- a/lms/static/js/instructor_dashboard/student_admin.js +++ b/lms/static/js/instructor_dashboard/student_admin.js @@ -436,7 +436,7 @@ } StudentAdmin.prototype.rescore_problem_single = function(onlyIfHigher) { - var errorMessage, fullErrorMessage, fullSuccessMessage, + var defaultErrorMessage, fullDefaultErrorMessage, fullSuccessMessage, problemToReset, sendData, successMessage, uniqStudentIdentifier, that = this; uniqStudentIdentifier = this.$field_student_select_grade.val(); @@ -461,8 +461,8 @@ student_id: uniqStudentIdentifier, problem_id: problemToReset }); - errorMessage = gettext("Error starting a task to rescore problem '<%- problem_id %>' for student '<%- student_id %>'. Make sure that the the problem and student identifiers are complete and correct."); // eslint-disable-line max-len - fullErrorMessage = _.template(errorMessage)({ + defaultErrorMessage = gettext("Error starting a task to rescore problem '<%- problem_id %>' for student '<%- student_id %>'. Make sure that the the problem and student identifiers are complete and correct."); // eslint-disable-line max-len + fullDefaultErrorMessage = _.template(defaultErrorMessage)({ student_id: uniqStudentIdentifier, problem_id: problemToReset }); @@ -474,8 +474,11 @@ success: this.clear_errors_then(function() { return alert(fullSuccessMessage); // eslint-disable-line no-alert }), - error: statusAjaxError(function() { - return that.$request_err_grade.text(fullErrorMessage); + error: statusAjaxError(function(response) { + if (response.responseText) { + return that.$request_err_grade.text(response.responseText); + } + return that.$request_err_grade.text(fullDefaultErrorMessage); }) }); }; @@ -518,8 +521,9 @@ }; StudentAdmin.prototype.rescore_problem_all = function(onlyIfHigher) { - var confirmMessage, errorMessage, fullConfirmMessage, - fullErrorMessage, fullSuccessMessage, problemToReset, sendData, successMessage, + var confirmMessage, defaultErrorMessage, fullConfirmMessage, + fullDefaultErrorMessage, fullSuccessMessage, problemToReset, + sendData, successMessage, that = this; problemToReset = this.$field_problem_select_all.val(); if (!problemToReset) { @@ -541,8 +545,8 @@ fullSuccessMessage = _.template(successMessage)({ problem_id: problemToReset }); - errorMessage = gettext("Error starting a task to rescore problem '<%- problem_id %>'. Make sure that the problem identifier is complete and correct."); // eslint-disable-line max-len - fullErrorMessage = _.template(errorMessage)({ + defaultErrorMessage = gettext("Error starting a task to rescore problem '<%- problem_id %>'. Make sure that the problem identifier is complete and correct."); // eslint-disable-line max-len + fullDefaultErrorMessage = _.template(defaultErrorMessage)({ problem_id: problemToReset }); return $.ajax({ @@ -553,8 +557,11 @@ success: this.clear_errors_then(function() { return alert(fullSuccessMessage); // eslint-disable-line no-alert }), - error: statusAjaxError(function() { - return that.$request_response_error_all.text(fullErrorMessage); + error: statusAjaxError(function(response) { + if (response.responseText) { + return that.$request_response_error_all.text(response.responseText); + } + return that.$request_response_error_all.text(fullDefaultErrorMessage); }) }); } else {