feat: reset_student_attempts_for_entrance_exam to DRF (#37069)

* feat: reset_student_attempts_for_entrance_exam to DRF

* fix: update imports sequence

---------

Co-authored-by: Awais Qureshi <awais.qureshi@arbisoft.com>
This commit is contained in:
Hunzlah Malik
2025-08-06 16:29:54 +05:00
committed by GitHub
parent 96ace718f4
commit 13944afc91
3 changed files with 140 additions and 67 deletions

View File

@@ -117,7 +117,8 @@ from lms.djangoapps.instructor.views.serializer import (
UpdateForumRoleMembershipSerializer,
RescoreEntranceExamSerializer,
OverrideProblemScoreSerializer,
StudentsUpdateEnrollmentSerializer
StudentsUpdateEnrollmentSerializer,
ResetEntranceExamAttemptsSerializer
)
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
@@ -2021,81 +2022,90 @@ class ResetStudentAttempts(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.GIVE_STUDENT_EXTENSION)
@common_exceptions_400
def reset_student_attempts_for_entrance_exam(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 ResetStudentAttemptsForEntranceExam(DeveloperErrorViewMixin, APIView):
"""
Resets a students attempts counter or starts a task to reset all students
attempts counters for entrance exam. Optionally deletes student state for
entrance exam. Limited to staff access. Some sub-methods limited to instructor access.
Following are possible query parameters
- unique_student_identifier is an email or username
- all_students is a boolean
requires instructor access
mutually exclusive with delete_module
- delete_module is a boolean
requires instructor access
mutually exclusive with all_students
"""
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.GIVE_STUDENT_EXTENSION
if not course.entrance_exam_id:
return HttpResponseBadRequest(
_("Course has no entrance exam section.")
serializer_class = ResetEntranceExamAttemptsSerializer
@method_decorator(ensure_csrf_cookie)
@method_decorator(transaction.non_atomic_requests)
def post(self, request, course_id):
"""
Resets a student's entrance exam attempts or
deletes entrance exam state.
Parameters (in request.data):
- unique_student_identifier (str, optional):
Email or username of the student. If provided, must exist.
- all_students (bool, optional):
If True, applies to all students. Mutually exclusive with
unique_student_identifier and delete_module.
- delete_module (bool, optional):
If True, deletes entrance exam state for the student. Mutually
exclusive with all_students.
Behavior:
- At least one of unique_student_identifier, all_students, or
delete_module must be provided.
- If unique_student_identifier is provided but does not exist,
returns a validation error.
- If mutually exclusive parameters are provided, returns a
validation error.
- Requires staff access; instructor access required for
all_students or delete_module actions.
- Returns a JSON response with the task status and student
identifier.
"""
course_id = CourseKey.from_string(course_id)
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
course = get_course_with_access(
request.user, 'staff', course_id, depth=None
)
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 = _get_boolean_param(request, 'all_students')
delete_module = _get_boolean_param(request, 'delete_module')
# parameter combinations
if all_students and student:
return HttpResponseBadRequest(
_("all_students and unique_student_identifier are mutually exclusive.")
)
if all_students and delete_module:
return HttpResponseBadRequest(
_("all_students and delete_module are mutually exclusive.")
)
# instructor authorization
if all_students or delete_module:
if not has_access(request.user, 'instructor', course):
return HttpResponseForbidden(_("Requires instructor access."))
try:
entrance_exam_key = UsageKey.from_string(course.entrance_exam_id).map_into_course(course_id)
if delete_module:
task_api.submit_delete_entrance_exam_state_for_student(
request,
entrance_exam_key,
student
if not course.entrance_exam_id:
return HttpResponseBadRequest(
_("Course has no entrance exam section.")
)
else:
task_api.submit_reset_problem_attempts_in_entrance_exam(
request,
entrance_exam_key,
student
)
except InvalidKeyError:
return HttpResponseBadRequest(_("Course has no valid entrance exam section."))
response_payload = {'student': student_identifier or _('All Students'), 'task': TASK_SUBMISSION_OK}
return JsonResponse(response_payload)
student_identifier = serializer.initial_data.get('unique_student_identifier')
student = serializer.validated_data.get('unique_student_identifier')
all_students = serializer.validated_data.get('all_students')
delete_module = serializer.validated_data.get('delete_module')
# instructor authorization
if all_students or delete_module:
if not has_access(request.user, 'instructor', course):
return HttpResponseForbidden(_("Requires instructor access."))
try:
entrance_exam_key = UsageKey.from_string(course.entrance_exam_id).map_into_course(course_id)
if delete_module:
task_api.submit_delete_entrance_exam_state_for_student(
request,
entrance_exam_key,
student
)
else:
task_api.submit_reset_problem_attempts_in_entrance_exam(
request,
entrance_exam_key,
student
)
except InvalidKeyError:
return HttpResponseBadRequest(_("Course has no valid entrance exam section."))
response_payload = {'student': student_identifier or _('All Students'), 'task': TASK_SUBMISSION_OK}
return JsonResponse(response_payload)
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')

View File

@@ -40,7 +40,7 @@ urlpatterns = [
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.OverrideProblemScoreView.as_view(), name='override_problem_score'),
path('reset_student_attempts_for_entrance_exam', api.reset_student_attempts_for_entrance_exam,
path('reset_student_attempts_for_entrance_exam', api.ResetStudentAttemptsForEntranceExam.as_view(),
name='reset_student_attempts_for_entrance_exam'),
path('rescore_entrance_exam', api.RescoreEntranceExamView.as_view(), name='rescore_entrance_exam'),
path('list_entrance_exam_instructor_tasks', api.ListEntranceExamInstructorTasks.as_view(),

View File

@@ -485,6 +485,69 @@ class RescoreEntranceExamSerializer(serializers.Serializer):
only_if_higher = serializers.BooleanField(required=False, allow_null=True)
class ResetEntranceExamAttemptsSerializer(UniqueStudentIdentifierSerializer):
"""
Serializer for resetting entrance exam attempts or deleting entrance exam state.
Inherits user validation from UniqueStudentIdentifierSerializer.
"""
all_students = serializers.BooleanField(
required=False,
default=False,
help_text=_("Whether to reset for all students."),
)
delete_module = serializers.BooleanField(
required=False,
default=False,
help_text=_("Whether to delete entrance exam state for the student."),
)
# 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.")
)
def validate_unique_student_identifier(self, value):
"""
Validate that the unique_student_identifier corresponds to an existing user,
if a value is provided.
"""
if not value:
return None
user = super().validate_unique_student_identifier(value)
if user is None:
raise serializers.ValidationError(
"No user found with the provided identifier."
)
return user
def validate(self, attrs):
all_students = attrs.get("all_students", False)
student = attrs.get("unique_student_identifier")
delete_module = attrs.get("delete_module", False)
errors = []
if all_students and student:
errors.append(_("all_students and unique_student_identifier are mutually exclusive."))
if all_students and delete_module:
errors.append(_(
"all_students and delete_module are mutually exclusive."
))
if not (student or all_students):
errors.append(_("You must provide either unique_student_identifier or set all_students to True."))
if errors:
raise serializers.ValidationError({"non_field_errors": errors})
return attrs
class StudentsUpdateEnrollmentSerializer(serializers.Serializer):
"""Serializer for student enroll/unenroll actions."""
action = serializers.ChoiceField(choices=["enroll", "unenroll"])