From 89190cc55a80417517043a062a5cb63f76ce7135 Mon Sep 17 00:00:00 2001 From: Hunzlah Malik <50262751+hunzlahmalik@users.noreply.github.com> Date: Thu, 24 Jul 2025 19:08:43 +0500 Subject: [PATCH] Certificate activation handler to drf (#37037) * feat: certificate_activation_handler to drf --- .../contentstore/views/certificates.py | 83 ++++++++++++------- .../contentstore/views/permissions.py | 22 +++++ .../contentstore/views/serializers.py | 16 ++++ cms/urls.py | 4 +- 4 files changed, 95 insertions(+), 30 deletions(-) create mode 100644 cms/djangoapps/contentstore/views/permissions.py create mode 100644 cms/djangoapps/contentstore/views/serializers.py diff --git a/cms/djangoapps/contentstore/views/certificates.py b/cms/djangoapps/contentstore/views/certificates.py index cf48119fce..c50ea54d9a 100644 --- a/cms/djangoapps/contentstore/views/certificates.py +++ b/cms/djangoapps/contentstore/views/certificates.py @@ -32,13 +32,19 @@ from django.core.exceptions import PermissionDenied from django.http import HttpResponse from django.shortcuts import redirect from django.utils.translation import gettext as _ +from django.utils.decorators import method_decorator from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import require_http_methods +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework import status from common.djangoapps.course_modes.models import CourseMode from eventtracking import tracker from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import AssetKey, CourseKey +from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin from common.djangoapps.edxmako.shortcuts import render_to_response from common.djangoapps.student.auth import has_studio_write_access from common.djangoapps.student.roles import GlobalStaff @@ -47,6 +53,10 @@ from common.djangoapps.util.json_request import JsonResponse from xmodule.modulestore import EdxJSONEncoder # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order +from cms.djangoapps.contentstore.views.serializers import CertificateActivationSerializer +from cms.djangoapps.contentstore.views.permissions import HasStudioWriteAccess + + from ..exceptions import AssetNotFoundException from ..toggles import use_new_certificates_page from ..utils import ( @@ -360,39 +370,56 @@ class Certificate: return self._certificate_data -@login_required -@require_http_methods(("POST",)) -@ensure_csrf_cookie -def certificate_activation_handler(request, course_key_string): +class ModulestoreMixin: """ - A handler for Certificate Activation/Deactivation - - POST - json: is_active. update the activation state of certificate + Mixin to provide a get_modulestore() method for views. + Makes it easier to override or patch in tests. """ - course_key = CourseKey.from_string(course_key_string) - store = modulestore() - try: - course = _get_course_and_check_access(course_key, request.user) - except PermissionDenied: - msg = _('PermissionDenied: Failed in authenticating {user}').format(user=request.user) - return JsonResponse({"error": msg}, status=403) + def get_modulestore(self): + return modulestore() - data = json.loads(request.body.decode('utf8')) - is_active = data.get('is_active', False) - certificates = CertificateManager.get_certificates(course) - # for certificate activation/deactivation, we are assuming one certificate in certificates collection. - for certificate in certificates: - certificate['is_active'] = is_active - break +class CertificateActivationAPIView( + DeveloperErrorViewMixin, + ModulestoreMixin, + APIView +): + """ + View for activating or deactivating course certificates. + This view allows instructors to toggle the activation state of course certificates. + """ + permission_classes = [IsAuthenticated, HasStudioWriteAccess] + serializer_class = CertificateActivationSerializer - store.update_item(course, request.user.id) - cert_event_type = 'activated' if is_active else 'deactivated' - CertificateManager.track_event(cert_event_type, { - 'course_id': str(course.id), - }) - return HttpResponse(status=200) + @method_decorator(ensure_csrf_cookie) + def post(self, request, course_key_string): + """ + A handler for Certificate Activation/Deactivation + + POST + json: is_active. update the activation state of certificate + """ + course_key = CourseKey.from_string(course_key_string) + course = self.get_modulestore().get_course(course_key, depth=0) + + serializer = self.serializer_class(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + is_active = serializer.validated_data['is_active'] + certificates = CertificateManager.get_certificates(course) + + # for certificate activation/deactivation, we are assuming one certificate in certificates collection. + for certificate in certificates: + certificate['is_active'] = is_active + break + + self.get_modulestore().update_item(course, request.user.id) + cert_event_type = 'activated' if is_active else 'deactivated' + CertificateManager.track_event(cert_event_type, { + 'course_id': str(course.id), + }) + return Response(status=status.HTTP_200_OK) @login_required diff --git a/cms/djangoapps/contentstore/views/permissions.py b/cms/djangoapps/contentstore/views/permissions.py new file mode 100644 index 0000000000..6d63c7a124 --- /dev/null +++ b/cms/djangoapps/contentstore/views/permissions.py @@ -0,0 +1,22 @@ +""" +Custom permissions for the content store views. +""" + +from rest_framework.permissions import BasePermission + +from common.djangoapps.student.auth import has_studio_write_access +from openedx.core.lib.api.view_utils import validate_course_key + + +class HasStudioWriteAccess(BasePermission): + """ + Check if the user has write access to studio. + """ + + def has_permission(self, request, view): + """ + Check if the user has write access to studio. + """ + course_key_string = view.kwargs.get("course_key_string") + course_key = validate_course_key(course_key_string) + return has_studio_write_access(request.user, course_key) diff --git a/cms/djangoapps/contentstore/views/serializers.py b/cms/djangoapps/contentstore/views/serializers.py new file mode 100644 index 0000000000..c96e716410 --- /dev/null +++ b/cms/djangoapps/contentstore/views/serializers.py @@ -0,0 +1,16 @@ +""" +Serializers for the contentstore.views module. + +This module contains DRF serializers for various features such as certificates, blocks, and others. +Add new serializers here as needed for API endpoints in this module. +""" + +from rest_framework import serializers + + +class CertificateActivationSerializer(serializers.Serializer): + """ + Serializer for activating or deactivating course certificates. + """ + # This field indicates whether the certificate should be activated or deactivated. + is_active = serializers.BooleanField(required=False, default=False) diff --git a/cms/urls.py b/cms/urls.py index f2c2c8b31a..af10c6b619 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -264,7 +264,7 @@ if core_toggles.ENTRANCE_EXAMS.is_enabled(): # Enable Web/HTML Certificates if settings.FEATURES.get('CERTIFICATES_HTML_VIEW'): from cms.djangoapps.contentstore.views.certificates import ( - certificate_activation_handler, + CertificateActivationAPIView, signatory_detail_handler, certificates_detail_handler, certificates_list_handler @@ -272,7 +272,7 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW'): urlpatterns += [ re_path(fr'^certificates/activation/{settings.COURSE_KEY_PATTERN}/', - certificate_activation_handler, + CertificateActivationAPIView.as_view(), name='certificate_activation_handler'), re_path(r'^certificates/{}/(?P\d+)/signatories/(?P\d+)?$'.format( settings.COURSE_KEY_PATTERN), signatory_detail_handler, name='signatory_detail_handler'),