317 lines
12 KiB
Python
317 lines
12 KiB
Python
"""
|
|
Certificates Data Model:
|
|
|
|
course.certificates: {
|
|
'certificates': [
|
|
{
|
|
'version': 1, // data contract version
|
|
'id': 12345, // autogenerated identifier
|
|
'name': 'Certificate 1',
|
|
'description': 'Certificate 1 Description',
|
|
'course_title': 'course title',
|
|
'signatories': [
|
|
{
|
|
'id': 24680, // autogenerated identifier
|
|
'name': 'Dr. Bob Smith',
|
|
'title': 'Dean of the College',
|
|
'organization': 'Awesome College'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
|
|
|
|
import logging
|
|
|
|
from django.contrib.auth.decorators import login_required
|
|
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 opaque_keys.edx.keys import 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
|
|
|
|
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, CertificateSerializer
|
|
from cms.djangoapps.contentstore.views.permissions import HasStudioWriteAccess
|
|
|
|
from .certificate_manager import CertificateManager, CertificateValidationError
|
|
from ..toggles import use_new_certificates_page
|
|
from ..utils import (
|
|
get_certificates_context,
|
|
get_certificates_url,
|
|
reverse_course_url,
|
|
)
|
|
|
|
CERTIFICATE_MINIMUM_ID = 100
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
def _get_course_and_check_access(course_key, user, depth=0):
|
|
"""
|
|
Internal method used to calculate and return the locator and
|
|
course block for the view functions in this file.
|
|
"""
|
|
if not has_studio_write_access(user, course_key):
|
|
raise PermissionDenied()
|
|
course_block = modulestore().get_course(course_key, depth=depth)
|
|
return course_block
|
|
|
|
|
|
class ModulestoreMixin:
|
|
"""
|
|
Mixin to provide a get_modulestore() method for views.
|
|
Makes it easier to override or patch in tests.
|
|
"""
|
|
def get_modulestore(self):
|
|
return modulestore()
|
|
|
|
|
|
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
|
|
|
|
@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
|
|
@require_http_methods(("GET", "POST"))
|
|
@ensure_csrf_cookie
|
|
def certificates_list_handler(request, course_key_string):
|
|
"""
|
|
A RESTful handler for Course Certificates
|
|
|
|
GET
|
|
html: return Certificates list page (Backbone application)
|
|
POST
|
|
json: create new Certificate
|
|
"""
|
|
course_key = CourseKey.from_string(course_key_string)
|
|
store = modulestore()
|
|
with store.bulk_operations(course_key):
|
|
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)
|
|
|
|
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
|
|
if use_new_certificates_page(course_key):
|
|
return redirect(get_certificates_url(course_key))
|
|
certificates_context = get_certificates_context(course, request.user)
|
|
return render_to_response('certificates.html', certificates_context)
|
|
elif "application/json" in request.META.get('HTTP_ACCEPT'):
|
|
# Retrieve the list of certificates for the specified course
|
|
if request.method == 'GET':
|
|
certificates = CertificateManager.get_certificates(course)
|
|
return JsonResponse(certificates, encoder=EdxJSONEncoder)
|
|
elif request.method == 'POST':
|
|
# Add a new certificate to the specified course
|
|
try:
|
|
new_certificate = CertificateManager.deserialize_certificate(course, request.body)
|
|
except CertificateValidationError as err:
|
|
return JsonResponse({"error": str(err)}, status=400)
|
|
if course.certificates.get('certificates') is None:
|
|
course.certificates['certificates'] = []
|
|
course.certificates['certificates'].append(new_certificate.certificate_data)
|
|
response = JsonResponse(CertificateManager.serialize_certificate(new_certificate), status=201)
|
|
response["Location"] = reverse_course_url(
|
|
'certificates_detail_handler',
|
|
course.id,
|
|
kwargs={'certificate_id': new_certificate.id}
|
|
)
|
|
store.update_item(course, request.user.id)
|
|
CertificateManager.track_event('created', {
|
|
'course_id': str(course.id),
|
|
'configuration_id': new_certificate.id
|
|
})
|
|
course = _get_course_and_check_access(course_key, request.user)
|
|
return response
|
|
else:
|
|
return HttpResponse(status=406)
|
|
|
|
|
|
class CertificateDetailAPIView(ModulestoreMixin, APIView):
|
|
"""
|
|
JSON API endpoint for manipulating a course certificate via its internal identifier.
|
|
Utilized by the Backbone.js 'certificates' application model
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticated, HasStudioWriteAccess]
|
|
serializer_class = CertificateSerializer
|
|
|
|
@method_decorator(ensure_csrf_cookie)
|
|
def post(self, request, course_key_string, certificate_id=None):
|
|
"""
|
|
Create a certificate for the specified course based on provided information.
|
|
"""
|
|
return self._handle_create_or_update(request, course_key_string, certificate_id)
|
|
|
|
@method_decorator(ensure_csrf_cookie)
|
|
def put(self, request, course_key_string, certificate_id=None):
|
|
"""
|
|
Update a certificate for the specified course based on provided information.
|
|
"""
|
|
return self._handle_create_or_update(request, course_key_string, certificate_id)
|
|
|
|
@method_decorator(ensure_csrf_cookie)
|
|
def delete(self, request, course_key_string, certificate_id):
|
|
"""
|
|
Delete a certificate for the specified course.
|
|
"""
|
|
course_key = CourseKey.from_string(course_key_string)
|
|
course = _get_course_and_check_access(course_key, request.user)
|
|
|
|
certificates_list = course.certificates.get('certificates', [])
|
|
match_cert = next((cert for cert in certificates_list if int(cert['id']) == int(certificate_id)), None)
|
|
|
|
if not match_cert:
|
|
return JsonResponse(status=404)
|
|
|
|
active_certificates = CertificateManager.get_certificates(course, only_active=True)
|
|
if int(certificate_id) in [int(cert["id"]) for cert in active_certificates]:
|
|
if not GlobalStaff().has_user(request.user):
|
|
raise PermissionDenied()
|
|
|
|
store = self.get_modulestore()
|
|
CertificateManager.remove_certificate(
|
|
request=request,
|
|
store=store,
|
|
course=course,
|
|
certificate_id=certificate_id
|
|
)
|
|
CertificateManager.track_event('deleted', {
|
|
'course_id': str(course.id),
|
|
'configuration_id': certificate_id
|
|
})
|
|
return JsonResponse(status=204)
|
|
|
|
def _handle_create_or_update(self, request, course_key_string, certificate_id):
|
|
"""
|
|
Handle the creation or update of a certificate for the specified course."""
|
|
course_key = CourseKey.from_string(course_key_string)
|
|
course = self.get_modulestore().get_course(course_key, depth=0)
|
|
|
|
certificates_list = course.certificates.get('certificates', [])
|
|
match_index = None
|
|
match_cert = None
|
|
for index, cert in enumerate(certificates_list):
|
|
if certificate_id is not None and int(cert['id']) == int(certificate_id):
|
|
match_index = index
|
|
match_cert = cert
|
|
|
|
serializer = CertificateSerializer(
|
|
data=request.data,
|
|
context={"request": request, "course": course, "certificate_id": certificate_id}
|
|
)
|
|
if not serializer.is_valid():
|
|
return JsonResponse({"error": serializer.errors}, status=400)
|
|
|
|
new_certificate = serializer.save()
|
|
serialized_certificate = CertificateSerializer(new_certificate).data
|
|
|
|
cert_event_type = 'created'
|
|
if match_cert:
|
|
cert_event_type = 'modified'
|
|
certificates_list[match_index] = serialized_certificate
|
|
else:
|
|
certificates_list.append(serialized_certificate)
|
|
|
|
store = self.get_modulestore()
|
|
store.update_item(course, request.user.id)
|
|
CertificateManager.track_event(cert_event_type, {
|
|
'course_id': str(course.id),
|
|
'configuration_id': serialized_certificate["id"]
|
|
})
|
|
return JsonResponse(serialized_certificate, status=201)
|
|
|
|
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
@require_http_methods(("POST", "PUT", "DELETE"))
|
|
def signatory_detail_handler(request, course_key_string, certificate_id, signatory_id):
|
|
"""
|
|
JSON API endpoint for manipulating a specific course certificate signatory via its internal identifier.
|
|
Utilized by the Backbone 'certificates' application.
|
|
|
|
DELETE
|
|
json: Remove the specified signatory from the specified certificate
|
|
"""
|
|
course_key = CourseKey.from_string(course_key_string)
|
|
store = modulestore()
|
|
with store.bulk_operations(course_key):
|
|
course = _get_course_and_check_access(course_key, request.user)
|
|
certificates_list = course.certificates['certificates']
|
|
|
|
match_cert = None
|
|
# pylint: disable=unused-variable
|
|
for index, cert in enumerate(certificates_list):
|
|
if certificate_id is not None:
|
|
if int(cert['id']) == int(certificate_id):
|
|
match_cert = cert
|
|
|
|
if request.method == "DELETE":
|
|
if not match_cert:
|
|
return JsonResponse(status=404)
|
|
CertificateManager.remove_signatory(
|
|
request=request,
|
|
store=store,
|
|
course=course,
|
|
certificate_id=certificate_id,
|
|
signatory_id=signatory_id
|
|
)
|
|
return JsonResponse(status=204)
|