From 6680aecbbebd0ed142a2d1d0f13fbf903398e4cd Mon Sep 17 00:00:00 2001 From: Awais Qureshi Date: Tue, 15 Apr 2025 15:24:05 +0500 Subject: [PATCH] Rescore problem to drf (#35627) * feat!: upgrading api to DRF. --- lms/djangoapps/instructor/views/api.py | 137 ++++++++++-------- lms/djangoapps/instructor/views/api_urls.py | 2 +- lms/djangoapps/instructor/views/serializer.py | 26 ++++ 3 files changed, 100 insertions(+), 65 deletions(-) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 1841ff8dad..9176d45ecc 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -115,7 +115,8 @@ from lms.djangoapps.instructor.views.serializer import ( ShowStudentExtensionSerializer, StudentAttemptsSerializer, UserSerializer, - UniqueStudentIdentifierSerializer + 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 @@ -2052,84 +2053,92 @@ 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. - - 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') + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + permission_name = permissions.OVERRIDE_GRADES + serializer_class = ProblemResetSerializer - if all_students and not has_access(request.user, 'instructor', course): - return HttpResponseForbidden("Requires instructor access.") + @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 - 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) + all_students and unique_student_identifier cannot both be present. + """ - if not (problem_to_reset and (all_students or student)): - return HttpResponseBadRequest("Missing query parameters.") + course_id = CourseKey.from_string(course_id) + course = get_course_with_access(request.user, 'staff', course_id) - if all_students and student: - return HttpResponseBadRequest( - "Cannot rescore with all_students and unique_student_identifier." - ) + serializer_data = self.serializer_class(data=request.data) - try: - module_state_key = UsageKey.from_string(problem_to_reset).map_into_course(course_id) - except InvalidKeyError: - return HttpResponseBadRequest("Unable to parse problem id") + if not serializer_data.is_valid(): + return HttpResponseBadRequest(reason=serializer_data.errors) - response_payload = {'problem_to_reset': problem_to_reset} + 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 student: - response_payload['student'] = student_identifier - try: - task_api.submit_rescore_problem_for_student( - request, - module_state_key, - student, - only_if_higher, + student = serializer_data.validated_data.get("unique_student_identifier") + student_identifier = request.data.get("unique_student_identifier") + + if all_students and not has_access(request.user, 'instructor', course): + return HttpResponseForbidden("Requires instructor access.") + + if not (problem_to_reset and (all_students or student)): + return HttpResponseBadRequest("Missing query parameters.") + + 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 dcb92df2ec..01bfbd5fab 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -36,7 +36,7 @@ urlpatterns = [ 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 7b20eed32f..f2146f68cd 100644 --- a/lms/djangoapps/instructor/views/serializer.py +++ b/lms/djangoapps/instructor/views/serializer.py @@ -233,6 +233,32 @@ class BlockDueDateSerializer(serializers.Serializer): self.fields['due_datetime'].required = False +class ProblemResetSerializer(UniqueStudentIdentifierSerializer): + """ + serializer for resetting problem. + """ + 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, + ) + + # Override the unique_student_identifier field to make it optional + unique_student_identifier = serializers.CharField( + required=False, # Make this field optional + allow_null=True, + help_text=_("unique student identifier.") + ) + + class ModifyAccessSerializer(serializers.Serializer): """ serializers for enroll or un-enroll users in beta testing program.