|
22 | 22 | from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist, PermissionDenied, ValidationError |
23 | 23 | from django.core.validators import validate_email |
24 | 24 | 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 |
26 | 26 | from django.shortcuts import redirect |
27 | 27 | from django.urls import reverse |
28 | 28 | from django.utils.decorators import method_decorator |
29 | 29 | from django.utils.html import strip_tags |
30 | 30 | from django.utils.translation import gettext as _ |
31 | 31 | from django.views.decorators.cache import cache_control |
32 | 32 | 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 |
34 | 34 | from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication |
35 | 35 | from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser |
36 | 36 | from edx_when.api import get_date_for_block |
@@ -3350,42 +3350,93 @@ def start_certificate_regeneration(request, course_id): |
3350 | 3350 | return JsonResponse(response_payload) |
3351 | 3351 |
|
3352 | 3352 |
|
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): |
3359 | 3356 | """ |
3360 | 3357 | 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. |
3365 | 3358 | """ |
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 |
3372 | 3401 |
|
3373 | | - # Add new Certificate Exception for the student passed in request data |
3374 | | - if request.method == 'POST': |
3375 | 3402 | 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) |
3377 | 3409 | except ValueError as error: |
3378 | 3410 | return JsonResponse({'success': False, 'message': str(error)}, status=400) |
3379 | | - return JsonResponse(exception) |
3380 | 3411 |
|
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 | + |
3383 | 3425 | try: |
3384 | | - remove_certificate_exception(course_key, student) |
| 3426 | + user_data = raw_data.get('user_name', '') or raw_data.get('user_email', '') |
3385 | 3427 | 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) |
3387 | 3429 |
|
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 |
3389 | 3440 |
|
3390 | 3441 |
|
3391 | 3442 | def add_certificate_exception(course_key, student, certificate_exception): |
|
0 commit comments