diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 94184f6d9373..93f0adc36747 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -108,7 +108,10 @@ from lms.djangoapps.instructor_task.models import ReportStore from lms.djangoapps.instructor.views.serializer import ( AccessSerializer, BlockDueDateSerializer, RoleNameSerializer, ShowStudentExtensionSerializer, UserSerializer, - SendEmailSerializer, StudentAttemptsSerializer, ListInstructorTaskInputSerializer, UniqueStudentIdentifierSerializer + SendEmailSerializer, StudentAttemptsSerializer, + ListInstructorTaskInputSerializer, + UniqueStudentIdentifierSerializer, + ProblemResetSerializer ) from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, is_course_cohorted @@ -1987,84 +1990,91 @@ def reset_student_attempts_for_entrance_exam(request, course_id): return JsonResponse(response_payload) -@transaction.non_atomic_requests -@require_POST -@ensure_csrf_cookie -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(permissions.OVERRIDE_GRADES) -@require_post_params(problem_to_reset="problem urlname to reset") -@common_exceptions_400 -def rescore_problem(request, course_id): +@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') +@method_decorator(transaction.non_atomic_requests, name='dispatch') +class RescoreProblem(DeveloperErrorViewMixin, APIView): """ Starts a background process a students attempts counter. Optionally deletes student state for a problem. Rescore for all students is limited to instructor access. + """ + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + permission_name = permissions.OVERRIDE_GRADES + serializer_class = ProblemResetSerializer - Takes either of the following query parameters - - problem_to_reset is a urlname of a problem - - unique_student_identifier is an email or username - - all_students is a boolean + @method_decorator(ensure_csrf_cookie) + @method_decorator(transaction.non_atomic_requests) + def post(self, request, course_id): + """ + Takes either of the following query parameters + - problem_to_reset is a urlname of a problem + - unique_student_identifier is an email or username + - all_students is a boolean - all_students and unique_student_identifier cannot both be present. - """ - course_id = CourseKey.from_string(course_id) - course = get_course_with_access(request.user, 'staff', course_id) - all_students = _get_boolean_param(request, 'all_students') + all_students and unique_student_identifier cannot both be present. + """ + course_id = CourseKey.from_string(course_id) + course = get_course_with_access(request.user, 'staff', course_id) - if all_students and not has_access(request.user, 'instructor', course): - return HttpResponseForbidden("Requires instructor access.") + serializer_data = self.serializer_class(data=request.data) + student = None - only_if_higher = _get_boolean_param(request, 'only_if_higher') - problem_to_reset = strip_if_string(request.POST.get('problem_to_reset')) - student_identifier = request.POST.get('unique_student_identifier', None) - student = None - if student_identifier is not None: - student = get_student_from_identifier(student_identifier) + if not serializer_data.is_valid(): + return HttpResponseBadRequest(reason=serializer_data.errors) - if not (problem_to_reset and (all_students or student)): - return HttpResponseBadRequest("Missing query parameters.") + problem_to_reset = serializer_data.validated_data.get("problem_to_reset") + all_students = serializer_data.validated_data.get("all_students") + only_if_higher = serializer_data.validated_data.get("only_if_higher") - if all_students and student: - return HttpResponseBadRequest( - "Cannot rescore with all_students and unique_student_identifier." - ) + if all_students and not has_access(request.user, 'instructor', course): + return HttpResponseForbidden("Requires instructor access.") - try: - module_state_key = UsageKey.from_string(problem_to_reset).map_into_course(course_id) - except InvalidKeyError: - return HttpResponseBadRequest("Unable to parse problem id") + student_identifier = serializer_data.validated_data.get("unique_student_identifier") - response_payload = {'problem_to_reset': problem_to_reset} + if not (problem_to_reset and (all_students or student)): + return HttpResponseBadRequest("Missing query parameters.") - if student: - response_payload['student'] = student_identifier - try: - task_api.submit_rescore_problem_for_student( - request, - module_state_key, - student, - only_if_higher, + if all_students and student: + return HttpResponseBadRequest( + "Cannot rescore with all_students and unique_student_identifier." ) - except NotImplementedError as exc: - return HttpResponseBadRequest(str(exc)) - except ItemNotFoundError as exc: - return HttpResponseBadRequest(f"{module_state_key} not found") - elif all_students: try: - task_api.submit_rescore_problem_for_all_students( - request, - module_state_key, - only_if_higher, - ) - except NotImplementedError as exc: - return HttpResponseBadRequest(str(exc)) - except ItemNotFoundError as exc: - return HttpResponseBadRequest(f"{module_state_key} not found") - else: - return HttpResponseBadRequest() + module_state_key = UsageKey.from_string(problem_to_reset).map_into_course(course_id) + except InvalidKeyError: + return HttpResponseBadRequest("Unable to parse problem id") - response_payload['task'] = TASK_SUBMISSION_OK - return JsonResponse(response_payload) + response_payload = {'problem_to_reset': problem_to_reset} + + if student: + response_payload['student'] = student_identifier + try: + task_api.submit_rescore_problem_for_student( + request, + module_state_key, + student, + only_if_higher, + ) + except NotImplementedError as exc: + return HttpResponseBadRequest(str(exc)) + except ItemNotFoundError as exc: + return HttpResponseBadRequest(f"{module_state_key} not found") + + elif all_students: + try: + task_api.submit_rescore_problem_for_all_students( + request, + module_state_key, + only_if_higher, + ) + except NotImplementedError as exc: + return HttpResponseBadRequest(str(exc)) + except ItemNotFoundError as exc: + return HttpResponseBadRequest(f"{module_state_key} not found") + else: + return HttpResponseBadRequest() + + response_payload['task'] = TASK_SUBMISSION_OK + return JsonResponse(response_payload) @transaction.non_atomic_requests diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 5976411a9756..61de4fd4a267 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -36,7 +36,7 @@ name="get_student_enrollment_status"), path('get_student_progress_url', api.StudentProgressUrl.as_view(), name='get_student_progress_url'), path('reset_student_attempts', api.ResetStudentAttempts.as_view(), name='reset_student_attempts'), - path('rescore_problem', api.rescore_problem, name='rescore_problem'), + path('rescore_problem', api.RescoreProblem.as_view(), name='rescore_problem'), path('override_problem_score', api.override_problem_score, name='override_problem_score'), path('reset_student_attempts_for_entrance_exam', api.reset_student_attempts_for_entrance_exam, name='reset_student_attempts_for_entrance_exam'), diff --git a/lms/djangoapps/instructor/views/serializer.py b/lms/djangoapps/instructor/views/serializer.py index 59ac66ab838b..e366fe30075a 100644 --- a/lms/djangoapps/instructor/views/serializer.py +++ b/lms/djangoapps/instructor/views/serializer.py @@ -228,3 +228,20 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if disable_due_datetime: self.fields['due_datetime'].required = False + + +class ProblemResetSerializer(UniqueStudentIdentifierSerializer): + problem_to_reset = serializers.CharField( + help_text=_("The URL name of the problem to reset."), + error_messages={ + 'blank': _("Problem URL name cannot be blank."), + } + ) + all_students = serializers.BooleanField( + default=False, + help_text=_("Whether to reset the problem for all students."), + ) + only_if_higher = serializers.BooleanField( + default=False, + help_text=_("Whether to reset the problem for all students."), + )