diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 94184f6d9373..4467e31b5d64 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -108,7 +108,8 @@ 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, BaseProblemSerializer ) 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 @@ -2127,62 +2128,68 @@ 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): - """ - Starts a background process a students attempts counter for entrance exam. - Optionally deletes student state for a problem. Limited to instructor access. +@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') +@method_decorator(transaction.non_atomic_requests, name='dispatch') +class RescoreEntranceExam(DeveloperErrorViewMixin, APIView): + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + permission_name = permissions.OVERRIDE_GRADES + serializer_class = BaseProblemSerializer - Takes either of the following query parameters - - 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): + """ + Starts a background process a students attempts counter for entrance exam. + Optionally deletes student state for a problem. Limited to instructor access. - 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 - ) + Takes either of the following query parameters + - unique_student_identifier is an email or username + - all_students is a boolean - 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) + 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 + ) - all_students = _get_boolean_param(request, 'all_students') + serializer_data = self.serializer_class(data=request.data) - if not course.entrance_exam_id: - return HttpResponseBadRequest( - _("Course has no entrance exam section.") - ) + if not serializer_data.is_valid(): + return HttpResponseBadRequest(reason=serializer_data.errors) - if all_students and student: - return HttpResponseBadRequest( - _("Cannot rescore with all_students and unique_student_identifier.") - ) + all_students = serializer_data.validated_data.get("all_students") + only_if_higher = serializer_data.validated_data.get("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.")) + student = serializer_data.validated_data.get("unique_student_identifier") + student_identifier = request.data.get("unique_student_identifier") - response_payload = {} - if student: - response_payload['student'] = student_identifier - else: - response_payload['student'] = _("All Students") + if not course.entrance_exam_id: + return HttpResponseBadRequest( + _("Course has no entrance exam section.") + ) - 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) + if all_students and student: + return HttpResponseBadRequest( + _("Cannot rescore with all_students and unique_student_identifier.") + ) + + 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) @require_POST diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 5976411a9756..61d05faf36e4 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -40,7 +40,7 @@ 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.RescoreEntranceExam.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 59ac66ab838b..172a7061ddcf 100644 --- a/lms/djangoapps/instructor/views/serializer.py +++ b/lms/djangoapps/instructor/views/serializer.py @@ -228,3 +228,29 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if disable_due_datetime: self.fields['due_datetime'].required = False + + +class BaseProblemSerializer(UniqueStudentIdentifierSerializer): + all_students = serializers.BooleanField( + default=False, + help_text=_("Whether to reset the problem for all students."), + ) + only_if_higher = serializers.BooleanField( + default=False, + ) + + # Override unique_student_identifier to be optional + unique_student_identifier = serializers.CharField( + required=False, # Make this field optional + allow_null=True, + help_text=_("Unique student identifier.") + ) + + +class ProblemResetSerializer(BaseProblemSerializer): + problem_to_reset = serializers.CharField( + help_text=_("The URL name of the problem to reset."), + error_messages={ + 'blank': _("Problem URL name cannot be blank."), + } + )