feat: override_problem_score to drf (#37006)

* feat: override_problem_score to drf
This commit is contained in:
Hunzlah Malik
2025-07-28 20:03:04 +05:00
committed by GitHub
parent e389addd0a
commit 96e5ce073f
3 changed files with 93 additions and 53 deletions

View File

@@ -114,8 +114,9 @@ from lms.djangoapps.instructor.views.serializer import (
UserSerializer,
UniqueStudentIdentifierSerializer,
ProblemResetSerializer,
RescoreEntranceExamSerializer,
UpdateForumRoleMembershipSerializer,
RescoreEntranceExamSerializer
OverrideProblemScoreSerializer
)
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
@@ -2189,64 +2190,88 @@ class RescoreProblem(DeveloperErrorViewMixin, APIView):
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", score='overriding score')
@common_exceptions_400
def override_problem_score(request, course_id): # lint-amnesty, pylint: disable=missing-function-docstring
course_key = CourseKey.from_string(course_id)
score = strip_if_string(request.POST.get('score'))
problem_to_reset = strip_if_string(request.POST.get('problem_to_reset'))
student_identifier = request.POST.get('unique_student_identifier', None)
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
@method_decorator(transaction.non_atomic_requests, name='dispatch')
class OverrideProblemScoreView(DeveloperErrorViewMixin, APIView):
"""
DRF view to override a student's score for a specific problem.
"""
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
permission_name = permissions.OVERRIDE_GRADES
serializer_class = OverrideProblemScoreSerializer
if not problem_to_reset:
return HttpResponseBadRequest("Missing query parameter problem_to_reset.")
@method_decorator(ensure_csrf_cookie)
@method_decorator(transaction.non_atomic_requests)
def post(self, request, course_id):
"""
Takes the following query parameters:
- problem_to_reset: a urlname of a problem
- unique_student_identifier: an email or username
- score: the score to override with
Returns a response indicating the success or failure of the operation.
If the user does not have permission to override scores for the problem,
a 403 Forbidden response is returned.
If the problem cannot be found or parsed, a 400 Bad Request response is returned.
If the score override is successful, a 200 OK response is returned with the task status
and the problem and student identifiers in the response payload.
"""
if not student_identifier:
return HttpResponseBadRequest("Missing query parameter student_identifier.")
serializer_data = self.serializer_class(data=request.data)
if not serializer_data.is_valid():
return HttpResponseBadRequest(reason=serializer_data.errors)
if student_identifier is not None:
student = get_student_from_identifier(student_identifier)
else:
return _create_error_response(request, f"Invalid student ID {student_identifier}.")
course_key = CourseKey.from_string(course_id)
problem_to_reset = serializer_data.validated_data['problem_to_reset']
score = serializer_data.validated_data['score']
student = serializer_data.validated_data['unique_student_identifier']
student_identifier = request.data.get('unique_student_identifier')
try:
usage_key = UsageKey.from_string(problem_to_reset).map_into_course(course_key)
block = modulestore().get_item(usage_key)
except InvalidKeyError:
return _create_error_response(request, f"Unable to parse problem id {problem_to_reset}.")
except ItemNotFoundError:
return _create_error_response(request, f"Unable to find problem id {problem_to_reset}.")
try:
usage_key = UsageKey.from_string(problem_to_reset).map_into_course(course_key)
block = modulestore().get_item(usage_key)
except InvalidKeyError:
return Response(
{"error": f"Unable to parse problem id {problem_to_reset}."},
status=status.HTTP_400_BAD_REQUEST
)
except ItemNotFoundError:
return Response(
{"error": f"Unable to find problem id {problem_to_reset}."},
status=status.HTTP_400_BAD_REQUEST
)
# check the user's access to this specific problem
if not has_access(request.user, "staff", block):
_create_error_response(request, "User {} does not have permission to override scores for problem {}.".format(
request.user.id,
problem_to_reset
))
if not has_access(request.user, "staff", block):
return Response(
{
"error": _(
"User {user_id} does not have permission to "
"override scores for problem {problem_to_reset}."
).format(
user_id=request.user.id,
problem_to_reset=problem_to_reset
)
},
status=status.HTTP_403_FORBIDDEN
)
response_payload = {
'problem_to_reset': problem_to_reset,
'student': student_identifier
}
try:
task_api.submit_override_score(
request,
usage_key,
student,
score,
)
except NotImplementedError as exc: # if we try to override the score of a non-scorable block, catch it here
return _create_error_response(request, str(exc))
response_payload = {
'problem_to_reset': problem_to_reset,
'student': student_identifier
}
try:
task_api.submit_override_score(
request,
usage_key,
student,
score,
)
except NotImplementedError as exc:
return Response({"error": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
except ValueError as exc:
return Response({"error": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
except ValueError as exc:
return _create_error_response(request, str(exc))
response_payload['task'] = TASK_SUBMISSION_OK
return JsonResponse(response_payload)
response_payload['task'] = TASK_SUBMISSION_OK
return Response(response_payload)
@method_decorator(transaction.non_atomic_requests, name='dispatch')

View File

@@ -39,7 +39,7 @@ urlpatterns = [
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.RescoreProblem.as_view(), name='rescore_problem'),
path('override_problem_score', api.override_problem_score, name='override_problem_score'),
path('override_problem_score', api.OverrideProblemScoreView.as_view(), 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.RescoreEntranceExamView.as_view(), name='rescore_entrance_exam'),

View File

@@ -483,3 +483,18 @@ class RescoreEntranceExamSerializer(serializers.Serializer):
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)
class OverrideProblemScoreSerializer(UniqueStudentIdentifierSerializer):
"""
Serializer for overriding a student's score for a specific problem.
"""
problem_to_reset = serializers.CharField(
help_text=_("The URL name of the problem to override the score for."),
error_messages={
'blank': _("Problem URL name cannot be blank."),
}
)
score = serializers.FloatField(
help_text=_("The overriding score to set."),
)