Skip to content

Commit a4d3bf9

Browse files
authored
feat!: upgrade certificate_exception_view to DRF ( 28 ) (#35594)
* feat!: upgrading api to DRF.
1 parent 9cade7a commit a4d3bf9

File tree

4 files changed

+113
-32
lines changed

4 files changed

+113
-32
lines changed

lms/djangoapps/instructor/tests/test_certificates.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -488,9 +488,7 @@ def test_certificate_exception_missing_username_and_email_error(self):
488488
assert not res_json['success']
489489

490490
# Assert Error Message
491-
assert res_json['message'] ==\
492-
'Student username/email field is required and can not be empty.' \
493-
' Kindly fill in username/email and then press "Add to Exception List" button.'
491+
assert res_json['message'] == {'user': ['This field may not be blank.']}
494492

495493
def test_certificate_exception_duplicate_user_error(self):
496494
"""
@@ -604,6 +602,34 @@ def test_certificate_exception_removed_successfully(self):
604602
# Verify that certificate exception does not exist
605603
assert not certs_api.is_on_allowlist(self.user2, self.course.id)
606604

605+
def test_certificate_exception_removed_successfully_form_url(self):
606+
"""
607+
In case of deletion front-end is sending content-type x-www-form-urlencoded.
608+
Just to handle that some logic added in api and this test is for that part.
609+
Test certificates exception removal api endpoint returns success status
610+
when called with valid course key and certificate exception id
611+
"""
612+
GeneratedCertificateFactory.create(
613+
user=self.user2,
614+
course_id=self.course.id,
615+
status=CertificateStatuses.downloadable,
616+
grade='1.0'
617+
)
618+
# Verify that certificate exception exists
619+
assert certs_api.is_on_allowlist(self.user2, self.course.id)
620+
621+
response = self.client.post(
622+
self.url,
623+
data=json.dumps(self.certificate_exception_in_db),
624+
content_type='application/x-www-form-urlencoded',
625+
REQUEST_METHOD='DELETE'
626+
)
627+
# Assert successful request processing
628+
assert response.status_code == 204
629+
630+
# Verify that certificate exception does not exist
631+
assert not certs_api.is_on_allowlist(self.user2, self.course.id)
632+
607633
def test_remove_certificate_exception_invalid_request_error(self):
608634
"""
609635
Test certificates exception removal api endpoint returns error

lms/djangoapps/instructor/views/api.py

Lines changed: 78 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@
2222
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist, PermissionDenied, ValidationError
2323
from django.core.validators import validate_email
2424
from django.db import IntegrityError, transaction
25-
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotFound
25+
from django.http import QueryDict, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotFound
2626
from django.shortcuts import redirect
2727
from django.urls import reverse
2828
from django.utils.decorators import method_decorator
2929
from django.utils.html import strip_tags
3030
from django.utils.translation import gettext as _
3131
from django.views.decorators.cache import cache_control
3232
from django.views.decorators.csrf import ensure_csrf_cookie
33-
from django.views.decorators.http import require_POST, require_http_methods
33+
from django.views.decorators.http import require_POST
3434
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
3535
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
3636
from edx_when.api import get_date_for_block
@@ -3350,42 +3350,93 @@ def start_certificate_regeneration(request, course_id):
33503350
return JsonResponse(response_payload)
33513351

33523352

3353-
@transaction.non_atomic_requests
3354-
@ensure_csrf_cookie
3355-
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
3356-
@require_course_permission(permissions.CERTIFICATE_EXCEPTION_VIEW)
3357-
@require_http_methods(['POST', 'DELETE'])
3358-
def certificate_exception_view(request, course_id):
3353+
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
3354+
@method_decorator(transaction.non_atomic_requests, name='dispatch')
3355+
class CertificateExceptionView(DeveloperErrorViewMixin, APIView):
33593356
"""
33603357
Add/Remove students to/from the certificate allowlist.
3361-
3362-
:param request: HttpRequest object
3363-
:param course_id: course identifier of the course for whom to add/remove certificates exception.
3364-
:return: JsonResponse object with success/error message or certificate exception data.
33653358
"""
3366-
course_key = CourseKey.from_string(course_id)
3367-
# Validate request data and return error response in case of invalid data
3368-
try:
3369-
certificate_exception, student = parse_request_data_and_get_user(request)
3370-
except ValueError as error:
3371-
return JsonResponse({'success': False, 'message': str(error)}, status=400)
3359+
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
3360+
permission_name = permissions.CERTIFICATE_EXCEPTION_VIEW
3361+
serializer_class = CertificateSerializer
3362+
http_method_names = ['post', 'delete']
3363+
3364+
@method_decorator(transaction.non_atomic_requests, name='dispatch')
3365+
@method_decorator(ensure_csrf_cookie)
3366+
def post(self, request, course_id):
3367+
"""
3368+
Add certificate exception for a student.
3369+
"""
3370+
return self._handle_certificate_exception(request, course_id, action="post")
3371+
3372+
@method_decorator(ensure_csrf_cookie)
3373+
@method_decorator(transaction.non_atomic_requests)
3374+
def delete(self, request, course_id):
3375+
"""
3376+
Remove certificate exception for a student.
3377+
"""
3378+
return self._handle_certificate_exception(request, course_id, action="delete")
3379+
3380+
def _handle_certificate_exception(self, request, course_id, action):
3381+
"""
3382+
Handles adding or removing certificate exceptions.
3383+
"""
3384+
course_key = CourseKey.from_string(course_id)
3385+
try:
3386+
data = request.data
3387+
except Exception: # pylint: disable=broad-except
3388+
return JsonResponse(
3389+
{
3390+
'success': False,
3391+
'message':
3392+
_('The record is not in the correct format. Please add a valid username or email address.')},
3393+
status=400
3394+
)
3395+
3396+
# Extract and validate the student information
3397+
student, error_response = self._get_and_validate_user(data)
3398+
3399+
if error_response:
3400+
return error_response
33723401

3373-
# Add new Certificate Exception for the student passed in request data
3374-
if request.method == 'POST':
33753402
try:
3376-
exception = add_certificate_exception(course_key, student, certificate_exception)
3403+
if action == "post":
3404+
exception = add_certificate_exception(course_key, student, data)
3405+
return JsonResponse(exception)
3406+
elif action == "delete":
3407+
remove_certificate_exception(course_key, student)
3408+
return JsonResponse({}, status=204)
33773409
except ValueError as error:
33783410
return JsonResponse({'success': False, 'message': str(error)}, status=400)
3379-
return JsonResponse(exception)
33803411

3381-
# Remove Certificate Exception for the student passed in request data
3382-
elif request.method == 'DELETE':
3412+
def _get_and_validate_user(self, raw_data):
3413+
"""
3414+
Extracts the user data from the request and validates the student.
3415+
"""
3416+
# This is only happening in case of delete.
3417+
# because content-type is coming as x-www-form-urlencoded from front-end.
3418+
if isinstance(raw_data, QueryDict):
3419+
raw_data = list(raw_data.keys())[0]
3420+
try:
3421+
raw_data = json.loads(raw_data)
3422+
except Exception as error: # pylint: disable=broad-except
3423+
return None, JsonResponse({'success': False, 'message': str(error)}, status=400)
3424+
33833425
try:
3384-
remove_certificate_exception(course_key, student)
3426+
user_data = raw_data.get('user_name', '') or raw_data.get('user_email', '')
33853427
except ValueError as error:
3386-
return JsonResponse({'success': False, 'message': str(error)}, status=400)
3428+
return None, JsonResponse({'success': False, 'message': str(error)}, status=400)
33873429

3388-
return JsonResponse({}, status=204)
3430+
serializer_data = self.serializer_class(data={'user': user_data})
3431+
if not serializer_data.is_valid():
3432+
return None, JsonResponse({'success': False, 'message': serializer_data.errors}, status=400)
3433+
3434+
student = serializer_data.validated_data.get('user')
3435+
if not student:
3436+
response_payload = f'{user_data} does not exist in the LMS. Please check your spelling and retry.'
3437+
return None, JsonResponse({'success': False, 'message': response_payload}, status=400)
3438+
3439+
return student, None
33893440

33903441

33913442
def add_certificate_exception(course_key, student, certificate_exception):

lms/djangoapps/instructor/views/api_urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
path('enable_certificate_generation', api.enable_certificate_generation, name='enable_certificate_generation'),
8585
path('start_certificate_generation', api.StartCertificateGeneration.as_view(), name='start_certificate_generation'),
8686
path('start_certificate_regeneration', api.start_certificate_regeneration, name='start_certificate_regeneration'),
87-
path('certificate_exception_view/', api.certificate_exception_view, name='certificate_exception_view'),
87+
path('certificate_exception_view/', api.CertificateExceptionView.as_view(), name='certificate_exception_view'),
8888
re_path(r'^generate_certificate_exceptions/(?P<generate_for>[^/]*)', api.GenerateCertificateExceptions.as_view(),
8989
name='generate_certificate_exceptions'),
9090
path('generate_bulk_certificate_exceptions', api.generate_bulk_certificate_exceptions,

lms/djangoapps/instructor/views/serializer.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,11 @@ def __init__(self, *args, **kwargs):
232232

233233
class CertificateSerializer(serializers.Serializer):
234234
"""
235-
Serializer for resetting a students attempts counter or starts a task to reset all students
235+
Serializer for multiple operations related with certificates.
236+
resetting a students attempts counter or starts a task to reset all students
237+
attempts counters
238+
Also Add/Remove students to/from the certificate allowlist.
239+
Also For resetting a students attempts counter or starts a task to reset all students
236240
attempts counters.
237241
"""
238242
user = serializers.CharField(

0 commit comments

Comments
 (0)