diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index c813c400a0..a56d4714a4 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -116,7 +116,8 @@ from lms.djangoapps.instructor.views.serializer import ( StudentAttemptsSerializer, UserSerializer, UniqueStudentIdentifierSerializer, - ProblemResetSerializer + ProblemResetSerializer, + RescoreEntranceExamSerializer ) 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 @@ -2201,62 +2202,75 @@ def override_problem_score(request, course_id): # lint-amnesty, pylint: disable 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.RESCORE_EXAMS) -@common_exceptions_400 -def rescore_entrance_exam(request, course_id): +@method_decorator(transaction.non_atomic_requests, name='dispatch') +class RescoreEntranceExamView(DeveloperErrorViewMixin, APIView): """ - Starts a background process a students attempts counter for entrance exam. + Starts a background process for a student's attempts counter for entrance exam. Optionally deletes student state for a problem. Limited to instructor access. - Takes either of the following query parameters - - unique_student_identifier is an email or username - - all_students is a boolean + Takes either of the following parameters: + - unique_student_identifier: an email or username + - all_students: 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, depth=None - ) + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + permission_name = permissions.RESCORE_EXAMS + serializer_class = RescoreEntranceExamSerializer - student_identifier = request.POST.get('unique_student_identifier', None) - only_if_higher = request.POST.get('only_if_higher', None) - student = None - if student_identifier is not None: - student = get_student_from_identifier(student_identifier) + @method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True)) + @method_decorator(ensure_csrf_cookie) + def post(self, request, course_id): + """ + Initiates a Celery task to rescore the entrance exam for a student or all students. + """ + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + data = serializer.validated_data - all_students = _get_boolean_param(request, 'all_students') - - if not course.entrance_exam_id: - return HttpResponseBadRequest( - _("Course has no entrance exam section.") + course_id = CourseKey.from_string(course_id) + course = get_course_with_access( + request.user, 'staff', course_id, depth=None ) - if all_students and student: - return HttpResponseBadRequest( - _("Cannot rescore with all_students and unique_student_identifier.") + if not course.entrance_exam_id: + return Response( + {"error": _("Course has no entrance exam section.")}, + status=status.HTTP_400_BAD_REQUEST + ) + + student_identifier = data.get('unique_student_identifier') + only_if_higher = data.get('only_if_higher') + all_students = data.get('all_students', False) + student = None + + if student_identifier: + student = get_student_from_identifier(student_identifier) + + if all_students and student: + return Response( + {"error": _("Cannot rescore with all_students and unique_student_identifier.")}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + entrance_exam_key = UsageKey.from_string(course.entrance_exam_id).map_into_course(course_id) + except InvalidKeyError: + return Response( + {"error": _("Course has no valid entrance exam section.")}, + status=status.HTTP_400_BAD_REQUEST + ) + + response_payload = { + 'student': student_identifier if student else _("All Students"), + 'task': TASK_SUBMISSION_OK + } + + task_api.submit_rescore_entrance_exam_for_student( + request, entrance_exam_key, student, only_if_higher, ) - try: - entrance_exam_key = UsageKey.from_string(course.entrance_exam_id).map_into_course(course_id) - except InvalidKeyError: - return HttpResponseBadRequest(_("Course has no valid entrance exam section.")) - - response_payload = {} - if student: - response_payload['student'] = student_identifier - else: - response_payload['student'] = _("All Students") - - task_api.submit_rescore_entrance_exam_for_student( - request, entrance_exam_key, student, only_if_higher, - ) - response_payload['task'] = TASK_SUBMISSION_OK - return JsonResponse(response_payload) + return Response(response_payload) @method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 9ffdd0e65f..cb06e42d74 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -40,7 +40,7 @@ urlpatterns = [ 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'), - path('rescore_entrance_exam', api.rescore_entrance_exam, name='rescore_entrance_exam'), + path('rescore_entrance_exam', api.RescoreEntranceExamView.as_view(), name='rescore_entrance_exam'), path('list_entrance_exam_instructor_tasks', api.ListEntranceExamInstructorTasks.as_view(), name='list_entrance_exam_instructor_tasks'), path('mark_student_can_skip_entrance_exam', api.MarkStudentCanSkipEntranceExam.as_view(), diff --git a/lms/djangoapps/instructor/views/serializer.py b/lms/djangoapps/instructor/views/serializer.py index f2146f68cd..cf6ee85de6 100644 --- a/lms/djangoapps/instructor/views/serializer.py +++ b/lms/djangoapps/instructor/views/serializer.py @@ -373,3 +373,10 @@ class CertificateSerializer(serializers.Serializer): return None return user + + +class RescoreEntranceExamSerializer(serializers.Serializer): + """Serializer for entrance exam rescoring""" + unique_student_identifier = serializers.CharField(required=False, allow_null=True) + all_students = serializers.BooleanField(required=False) + only_if_higher = serializers.BooleanField(required=False, allow_null=True)