Skip to content

Commit

Permalink
feat!: upgrading api to DRF.
Browse files Browse the repository at this point in the history
  • Loading branch information
awais786 committed Sep 24, 2024
1 parent 5d28cda commit 79d1e8b
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 46 deletions.
8 changes: 3 additions & 5 deletions lms/djangoapps/instructor/tests/test_certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -1085,9 +1085,7 @@ def test_missing_username_and_email_error(self):
res_json = json.loads(response.content.decode('utf-8'))

# Assert Error Message
assert res_json['message'] == \
'Student username/email field is required and can not be empty.' \
' Kindly fill in username/email and then press "Invalidate Certificate" button.'
assert res_json['errors'] == {'user': ['This field may not be blank.']}

def test_invalid_user_name_error(self):
"""
Expand All @@ -1106,9 +1104,9 @@ def test_invalid_user_name_error(self):
# Assert 400 status code in response
assert response.status_code == 400
res_json = json.loads(response.content.decode('utf-8'))

# Assert Error Message
assert res_json['message'] == f'{invalid_user} does not exist in the LMS. Please check your spelling and retry.'
assert res_json['errors'] == ('test_invalid_user_name does not exist in the LMS. '
'Please check your spelling and retry.')

def test_no_generated_certificate_error(self):
"""
Expand Down
86 changes: 45 additions & 41 deletions lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
from edx_when.api import get_date_for_block
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey

from cms.djangoapps.contentstore.views.transcripts_ajax import error_response
from openedx.core.djangoapps.course_groups.cohorts import get_cohort_by_name
from rest_framework.exceptions import MethodNotAllowed
from rest_framework import serializers, status # lint-amnesty, pylint: disable=wrong-import-order
Expand Down Expand Up @@ -107,8 +109,15 @@
from lms.djangoapps.instructor_task.data import InstructorTaskTypes
from lms.djangoapps.instructor_task.models import ReportStore
from lms.djangoapps.instructor.views.serializer import (
AccessSerializer, BlockDueDateSerializer, RoleNameSerializer, ShowStudentExtensionSerializer, UserSerializer,
SendEmailSerializer, StudentAttemptsSerializer, ListInstructorTaskInputSerializer
AccessSerializer,
BlockDueDateSerializer,
CertificateSerializer,
ListInstructorTaskInputSerializer,
RoleNameSerializer,
SendEmailSerializer,
ShowStudentExtensionSerializer,
StudentAttemptsSerializer,
UserSerializer
)
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 @@ -3641,6 +3650,7 @@ class CertificateInvalidationView(APIView):
"""
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
permission_name = permissions.CERTIFICATE_INVALIDATION_VIEW
serializer_class = CertificateSerializer
http_method_names = ['post', 'delete']

@method_decorator(ensure_csrf_cookie)
Expand All @@ -3651,14 +3661,25 @@ def post(self, request, course_id):
"""
course_key = CourseKey.from_string(course_id)
# Validate request data and return error response in case of invalid data
serializer_data = self.serializer_class(data=request.data)
if not serializer_data.is_valid():
# return HttpResponseBadRequest(reason=serializer_data.errors)
return JsonResponse({'errors': serializer_data.errors}, status=400)

student = serializer_data.validated_data.get('user')
notes = serializer_data.validated_data.get('notes')

if not student:
invalid_user = request.data.get('user')
response_payload = f'{invalid_user} does not exist in the LMS. Please check your spelling and retry.'

validation_response = self.validate_and_get_data(course_key, "post")
# If validation fails, return the error response
if isinstance(validation_response, JsonResponse):
return validation_response
# Otherwise, extract the validated data
return JsonResponse({'errors': response_payload}, status=400)

try:
certificate = _get_certificate_for_user(course_key, student)
except Exception as ex:
return JsonResponse({'errors': str(ex)}, status=400)

student, certificate, certificate_invalidation_data = validation_response
# Invalidate certificate of the given student for the course course
try:
if certs_api.is_on_allowlist(student, course_key):
Expand All @@ -3672,9 +3693,10 @@ def post(self, request, course_id):
certificate_invalidation = invalidate_certificate(
request,
certificate,
certificate_invalidation_data,
notes,
student
)

except ValueError as error:
return JsonResponse({'message': str(error)}, status=400)
return JsonResponse(certificate_invalidation)
Expand All @@ -3687,15 +3709,18 @@ def delete(self, request, course_id):
"""
# Re-Validate student certificate for the course course
course_key = CourseKey.from_string(course_id)
# Validate request data and return error response in case of invalid data
validation_response = self.validate_and_get_data(course_key, "delete")
try:
data = json.loads(self.request.body.decode('utf8') or '{}')
except Exception: # pylint: disable=broad-except
data = {}

serializer_data = self.serializer_class(data=data)

# If validation fails, return the error response
if isinstance(validation_response, JsonResponse):
return validation_response
if not serializer_data.is_valid():
return HttpResponseBadRequest(reason=serializer_data.errors)

# Otherwise, extract the validated data
student, certificate, certificate_invalidation_data = validation_response
student = serializer_data.validated_data.get('user')
certificate = _get_certificate_for_user(course_key, student)

try:
re_validate_certificate(request, course_key, certificate, student)
Expand All @@ -3704,35 +3729,14 @@ def delete(self, request, course_id):

return JsonResponse({}, status=204)

def validate_and_get_data(self, course_key, method):
"""
Validates the request data, retrieves the student and certificate for the specified course.
This method performs the following steps:
1. Parses the request data to extract the necessary information.
2. Retrieves the student object from the parsed request data.
3. Fetches the certificate for the user and course specified by the course_key.
If any of the steps fail (e.g., invalid request data or missing certificate), a ValueError is raised,
and the method returns a JsonResponse with a 400 status code.
"""
try:
certificate_invalidation_data = parse_request_data_drf(self.request, method)
student = _get_student_from_request_data(certificate_invalidation_data)
certificate = _get_certificate_for_user(course_key, student)
# Return the validated student and certificate
return student, certificate, certificate_invalidation_data
except ValueError as error:
# Return a JsonResponse with an error message if validation fails
return JsonResponse({'message': str(error)}, status=400)


def invalidate_certificate(request, generated_certificate, certificate_invalidation_data, student):
def invalidate_certificate(request, generated_certificate, notes, student):
"""
Invalidate given GeneratedCertificate and add CertificateInvalidation record for future reference or re-validation.
:param request: HttpRequest object
:param generated_certificate: GeneratedCertificate object, the certificate we want to invalidate
:param certificate_invalidation_data: dict object containing data for CertificateInvalidation.
:param notes: notes values.
:param student: User object, this user is tied to the generated_certificate we are going to invalidate
:return: dict object containing updated certificate invalidation data.
"""
Expand All @@ -3755,7 +3759,7 @@ def invalidate_certificate(request, generated_certificate, certificate_invalidat
certificate_invalidation = certs_api.create_certificate_invalidation_entry(
generated_certificate,
request.user,
certificate_invalidation_data.get("notes", ""),
notes,
)

# Invalidate the certificate
Expand All @@ -3766,7 +3770,7 @@ def invalidate_certificate(request, generated_certificate, certificate_invalidat
'user': student.username,
'invalidated_by': certificate_invalidation.invalidated_by.username,
'created': certificate_invalidation.created.strftime("%B %d, %Y"),
'notes': certificate_invalidation.notes,
'notes': notes,
}


Expand Down
22 changes: 22 additions & 0 deletions lms/djangoapps/instructor/views/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,25 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if disable_due_datetime:
self.fields['due_datetime'].required = False


class CertificateSerializer(serializers.Serializer):
"""
Serializer for resetting a students attempts counter or starts a task to reset all students
attempts counters.
"""
notes = serializers.CharField(max_length=128, write_only=True, required=False)
user = serializers.CharField(
help_text="Email or username of student.", required=True
)

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

return user

0 comments on commit 79d1e8b

Please sign in to comment.