Skip to content

Commit

Permalink
feat: upgrade reset_student_attempts api to drf ( 19th ) (#35404)
Browse files Browse the repository at this point in the history
* feat: upgrading simple api to drf compatible.
  • Loading branch information
awais786 authored Sep 10, 2024
1 parent 38cb0e1 commit 08bd8b3
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 65 deletions.
140 changes: 76 additions & 64 deletions lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
from lms.djangoapps.instructor_task.models import ReportStore
from lms.djangoapps.instructor.views.serializer import (
AccessSerializer, RoleNameSerializer, ShowStudentExtensionSerializer,
UserSerializer, SendEmailSerializer
UserSerializer, SendEmailSerializer, StudentAttemptsSerializer
)
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
Expand Down Expand Up @@ -1817,23 +1817,24 @@ def post(self, request, course_id):
return Response(serializer.data)


@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)
@require_post_params(
problem_to_reset="problem urlname to reset"
)
@common_exceptions_400
def reset_student_attempts(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 ResetStudentAttempts(DeveloperErrorViewMixin, APIView):
"""
Resets a students attempts counter or starts a task to reset all students
attempts counters. Optionally deletes student state for a problem. Limited
to staff access. Some sub-methods limited to instructor access.
"""
http_method_names = ['post']
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
permission_name = permissions.GIVE_STUDENT_EXTENSION
serializer_class = StudentAttemptsSerializer

Takes some of the following query parameters
@method_decorator(ensure_csrf_cookie)
@transaction.non_atomic_requests
def post(self, request, course_id):
"""
Takes some 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
Expand All @@ -1843,65 +1844,74 @@ def reset_student_attempts(request, course_id):
- 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
)
all_students = _get_boolean_param(request, 'all_students')

if all_students and not has_access(request.user, 'instructor', course):
return HttpResponseForbidden("Requires instructor access.")
"""
course_id = CourseKey.from_string(course_id)
serializer_data = self.serializer_class(data=request.data)

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)
delete_module = _get_boolean_param(request, 'delete_module')
if not serializer_data.is_valid():
return HttpResponseBadRequest(reason=serializer_data.errors)

# 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."
course = get_course_with_access(
request.user, 'staff', course_id, depth=None
)

try:
module_state_key = UsageKey.from_string(problem_to_reset).map_into_course(course_id)
except InvalidKeyError:
return HttpResponseBadRequest()
all_students = serializer_data.validated_data.get('all_students')

response_payload = {}
response_payload['problem_to_reset'] = problem_to_reset
if all_students and not has_access(request.user, 'instructor', course):
return HttpResponseForbidden("Requires instructor access.")

if student:
try:
enrollment.reset_student_attempts(
course_id,
student,
module_state_key,
requesting_user=request.user,
delete_module=delete_module
problem_to_reset = strip_if_string(serializer_data.validated_data.get('problem_to_reset'))
student_identifier = request.POST.get('unique_student_identifier', None)
student = serializer_data.validated_data.get('unique_student_identifier')
delete_module = serializer_data.validated_data.get('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."
)
except StudentModule.DoesNotExist:
return HttpResponseBadRequest(_("Module does not exist."))
except sub_api.SubmissionError:
# Trust the submissions API to log the error
error_msg = _("An error occurred while deleting the score.")
return HttpResponse(error_msg, status=500)
response_payload['student'] = student_identifier
elif all_students:
task_api.submit_reset_problem_attempts_for_all_students(request, module_state_key)
response_payload['task'] = TASK_SUBMISSION_OK
response_payload['student'] = 'All Students'
else:
return HttpResponseBadRequest()

return JsonResponse(response_payload)
try:
module_state_key = UsageKey.from_string(problem_to_reset).map_into_course(course_id)
except InvalidKeyError:
return HttpResponseBadRequest()

response_payload = {}
response_payload['problem_to_reset'] = problem_to_reset

if student:
try:
enrollment.reset_student_attempts(
course_id,
student,
module_state_key,
requesting_user=request.user,
delete_module=delete_module
)
except StudentModule.DoesNotExist:
return HttpResponseBadRequest(_("Module does not exist."))
except sub_api.SubmissionError:
# Trust the submissions API to log the error
error_msg = _("An error occurred while deleting the score.")
return HttpResponse(error_msg, status=500)
response_payload['student'] = student_identifier

elif all_students:
try:
task_api.submit_reset_problem_attempts_for_all_students(request, module_state_key)
response_payload['task'] = TASK_SUBMISSION_OK
response_payload['student'] = 'All Students'
except Exception: # pylint: disable=broad-except
error_msg = _("An error occurred while attempting to reset for all students.")
return HttpResponse(error_msg, status=500)
else:
return HttpResponseBadRequest()

return JsonResponse(response_payload)


@transaction.non_atomic_requests
Expand Down Expand Up @@ -1938,8 +1948,10 @@ def reset_student_attempts_for_entrance_exam(request, course_id):

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')

Expand Down
2 changes: 1 addition & 1 deletion lms/djangoapps/instructor/views/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
path('get_anon_ids', api.GetAnonIds.as_view(), name='get_anon_ids'),
path('get_student_enrollment_status', api.get_student_enrollment_status, name="get_student_enrollment_status"),
path('get_student_progress_url', api.StudentProgressUrl.as_view(), name='get_student_progress_url'),
path('reset_student_attempts', api.reset_student_attempts, name='reset_student_attempts'),
path('reset_student_attempts', api.ResetStudentAttempts.as_view(), name='reset_student_attempts'),
path('rescore_problem', api.rescore_problem, 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,
Expand Down
51 changes: 51 additions & 0 deletions lms/djangoapps/instructor/views/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,57 @@ def validate_student(self, value):
return user


class StudentAttemptsSerializer(serializers.Serializer):
"""
Serializer for resetting a students attempts counter or starts a task to reset all students
attempts counters.
"""
problem_to_reset = serializers.CharField(
help_text="The identifier or description of the problem that needs to be reset."
)

# following are optional params.
unique_student_identifier = serializers.CharField(
help_text="Email or username of student.", required=False
)
all_students = serializers.CharField(required=False)
delete_module = serializers.CharField(required=False)

def validate_all_students(self, value):
"""
converts the all_student params value to bool.
"""
return self.verify_bool(value)

def validate_delete_module(self, value):
"""
converts the all_student params value.
"""
return self.verify_bool(value)

def validate_unique_student_identifier(self, value):
"""
Validate that the student corresponds to an existing user.
"""
try:
user = get_student_from_identifier(value)
except User.DoesNotExist:
return None

return user

def verify_bool(self, value):
"""
Returns the value of the boolean parameter with the given
name in the POST request. Handles translation from string
values to boolean values.
"""
if value is not None:
return value in ['true', 'True', True]

return False


class SendEmailSerializer(serializers.Serializer):
"""
Serializer for sending an email with optional scheduling.
Expand Down

0 comments on commit 08bd8b3

Please sign in to comment.