feat: [FC-0044] certificates API DRF (#34339)
* feat: [FC-0044] certificates API DRF * fix: remove unused import from previous commits
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Serializers for v1 contentstore API.
|
||||
"""
|
||||
from .certificates import CourseCertificatesSerializer
|
||||
from .course_details import CourseDetailsSerializer
|
||||
from .course_rerun import CourseRerunSerializer
|
||||
from .course_team import CourseTeamSerializer
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
API Serializers for certificates page
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class CertificateSignatorySerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for representing certificate's signatory.
|
||||
"""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
organization = serializers.CharField(required=False)
|
||||
signature_image_path = serializers.CharField()
|
||||
title = serializers.CharField()
|
||||
|
||||
|
||||
class CertificateItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for representing certificate item created for current course.
|
||||
"""
|
||||
|
||||
course_title = serializers.CharField(required=False)
|
||||
description = serializers.CharField()
|
||||
editing = serializers.BooleanField(required=False)
|
||||
id = serializers.IntegerField()
|
||||
is_active = serializers.BooleanField()
|
||||
name = serializers.CharField()
|
||||
signatories = CertificateSignatorySerializer(many=True)
|
||||
version = serializers.IntegerField()
|
||||
|
||||
|
||||
class CourseCertificatesSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for representing course's certificates.
|
||||
"""
|
||||
|
||||
certificate_activation_handler_url = serializers.CharField()
|
||||
certificate_web_view_url = serializers.CharField(allow_null=True)
|
||||
certificates = CertificateItemSerializer(many=True, allow_null=True)
|
||||
course_modes = serializers.ListField(child=serializers.CharField())
|
||||
has_certificate_modes = serializers.BooleanField()
|
||||
is_active = serializers.BooleanField()
|
||||
is_global_staff = serializers.BooleanField()
|
||||
mfe_proctored_exam_settings_url = serializers.CharField(
|
||||
required=False, allow_null=True, allow_blank=True
|
||||
)
|
||||
course_number = serializers.CharField(source="context_course.number")
|
||||
course_title = serializers.CharField(source="context_course.display_name_with_default")
|
||||
course_number_override = serializers.CharField(source="context_course.display_coursenumber")
|
||||
@@ -7,6 +7,7 @@ from openedx.core.constants import COURSE_ID_PATTERN
|
||||
|
||||
from .views import (
|
||||
ContainerHandlerView,
|
||||
CourseCertificatesView,
|
||||
CourseDetailsView,
|
||||
CourseTeamView,
|
||||
CourseTextbooksView,
|
||||
@@ -109,6 +110,11 @@ urlpatterns = [
|
||||
CourseTextbooksView.as_view(),
|
||||
name="textbooks"
|
||||
),
|
||||
re_path(
|
||||
fr'^certificates/{COURSE_ID_PATTERN}$',
|
||||
CourseCertificatesView.as_view(),
|
||||
name="certificates"
|
||||
),
|
||||
re_path(
|
||||
fr'^container_handler/{settings.USAGE_KEY_PATTERN}$',
|
||||
ContainerHandlerView.as_view(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Views for v1 contentstore API.
|
||||
"""
|
||||
from .certificates import CourseCertificatesView
|
||||
from .course_details import CourseDetailsView
|
||||
from .course_index import CourseIndexView
|
||||
from .course_team import CourseTeamView
|
||||
|
||||
106
cms/djangoapps/contentstore/rest_api/v1/views/certificates.py
Normal file
106
cms/djangoapps/contentstore/rest_api/v1/views/certificates.py
Normal file
@@ -0,0 +1,106 @@
|
||||
""" API Views for course certificates """
|
||||
|
||||
import edx_api_doc_tools as apidocs
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from cms.djangoapps.contentstore.utils import get_certificates_context
|
||||
from cms.djangoapps.contentstore.rest_api.v1.serializers import (
|
||||
CourseCertificatesSerializer,
|
||||
)
|
||||
from common.djangoapps.student.auth import has_studio_write_access
|
||||
from openedx.core.lib.api.view_utils import (
|
||||
DeveloperErrorViewMixin,
|
||||
verify_course_exists,
|
||||
view_auth_classes,
|
||||
)
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
@view_auth_classes(is_authenticated=True)
|
||||
class CourseCertificatesView(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
View for course certificate page.
|
||||
"""
|
||||
|
||||
@apidocs.schema(
|
||||
parameters=[
|
||||
apidocs.string_parameter(
|
||||
"course_id", apidocs.ParameterLocation.PATH, description="Course ID"
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: CourseCertificatesSerializer,
|
||||
401: "The requester is not authenticated.",
|
||||
403: "The requester cannot access the specified course.",
|
||||
404: "The requested course does not exist.",
|
||||
},
|
||||
)
|
||||
@verify_course_exists()
|
||||
def get(self, request: Request, course_id: str):
|
||||
"""
|
||||
Get an object containing course's certificates.
|
||||
|
||||
**Example Request**
|
||||
|
||||
GET /api/contentstore/v1/certificates/{course_id}
|
||||
|
||||
**Response Values**
|
||||
|
||||
If the request is successful, an HTTP 200 "OK" response is returned.
|
||||
|
||||
The HTTP 200 response contains a single dict that contains keys that
|
||||
are the course's certificates.
|
||||
|
||||
**Example Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"certificate_activation_handler_url": "/certificates/activation/course-v1:org+101+101/",
|
||||
"certificate_web_view_url": "///certificates/course/course-v1:org+101+101?preview=honor",
|
||||
"certificates": [
|
||||
{
|
||||
"course_title": "Course title",
|
||||
"description": "Description of the certificate",
|
||||
"editing": false,
|
||||
"id": 1622146085,
|
||||
"is_active": false,
|
||||
"name": "Name of the certificate",
|
||||
"signatories": [
|
||||
{
|
||||
"id": 268550145,
|
||||
"name": "name_sign",
|
||||
"organization": "org",
|
||||
"signature_image_path": "/asset-v1:org+101+101+type@asset+block@camera.png",
|
||||
"title": "title_sign"
|
||||
}
|
||||
],
|
||||
"version": 1
|
||||
},
|
||||
],
|
||||
"course_modes": [
|
||||
"honor"
|
||||
],
|
||||
"has_certificate_modes": true,
|
||||
"is_active": false,
|
||||
"is_global_staff": true,
|
||||
"mfe_proctored_exam_settings_url": "",
|
||||
"course_number": "DemoX",
|
||||
"course_title": "Demonstration Course",
|
||||
"course_number_override": "Course Number Display String"
|
||||
}
|
||||
```
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
store = modulestore()
|
||||
|
||||
if not has_studio_write_access(request.user, course_key):
|
||||
self.permission_denied(request)
|
||||
|
||||
with store.bulk_operations(course_key):
|
||||
course = modulestore().get_course(course_key)
|
||||
certificates_context = get_certificates_context(course, request.user)
|
||||
serializer = CourseCertificatesSerializer(certificates_context)
|
||||
return Response(serializer.data)
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Unit tests for the course's certificate.
|
||||
"""
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
|
||||
from cms.djangoapps.contentstore.views.tests.test_certificates import HelperMethods
|
||||
|
||||
from ...mixins import PermissionAccessMixin
|
||||
|
||||
|
||||
class CourseCertificatesViewTest(CourseTestCase, PermissionAccessMixin, HelperMethods):
|
||||
"""
|
||||
Tests for CourseCertificatesView.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.url = reverse(
|
||||
"cms.djangoapps.contentstore:v1:certificates",
|
||||
kwargs={"course_id": self.course.id},
|
||||
)
|
||||
|
||||
def test_success_response(self):
|
||||
"""
|
||||
Check that endpoint is valid and success response.
|
||||
"""
|
||||
self._add_course_certificates(count=2, signatory_count=2)
|
||||
response = self.client.get(self.url)
|
||||
response_data = response.data
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response_data["certificates"]), 2)
|
||||
self.assertEqual(len(response_data["certificates"][0]["signatories"]), 2)
|
||||
self.assertEqual(len(response_data["certificates"][1]["signatories"]), 2)
|
||||
self.assertEqual(response_data["course_number_override"], self.course.display_coursenumber)
|
||||
self.assertEqual(response_data["course_title"], self.course.display_name_with_default)
|
||||
self.assertEqual(response_data["course_number"], self.course.number)
|
||||
@@ -2049,6 +2049,56 @@ def get_textbooks_context(course):
|
||||
}
|
||||
|
||||
|
||||
def get_certificates_context(course, user):
|
||||
"""
|
||||
Utils is used to get context for container xblock requests.
|
||||
It is used for both DRF and django views.
|
||||
"""
|
||||
|
||||
from cms.djangoapps.contentstore.views.certificates import CertificateManager
|
||||
|
||||
course_key = course.id
|
||||
certificate_url = reverse_course_url('certificates_list_handler', course_key)
|
||||
course_outline_url = reverse_course_url('course_handler', course_key)
|
||||
upload_asset_url = reverse_course_url('assets_handler', course_key)
|
||||
activation_handler_url = reverse_course_url(
|
||||
handler_name='certificate_activation_handler',
|
||||
course_key=course_key
|
||||
)
|
||||
course_modes = [
|
||||
mode.slug for mode in CourseMode.modes_for_course(
|
||||
course_id=course_key, include_expired=True
|
||||
) if mode.slug != 'audit'
|
||||
]
|
||||
|
||||
has_certificate_modes = len(course_modes) > 0
|
||||
|
||||
if has_certificate_modes:
|
||||
certificate_web_view_url = get_lms_link_for_certificate_web_view(
|
||||
course_key=course_key,
|
||||
mode=course_modes[0] # CourseMode.modes_for_course returns default mode if doesn't find anyone.
|
||||
)
|
||||
else:
|
||||
certificate_web_view_url = None
|
||||
|
||||
is_active, certificates = CertificateManager.is_activated(course)
|
||||
context = {
|
||||
'context_course': course,
|
||||
'certificate_url': certificate_url,
|
||||
'course_outline_url': course_outline_url,
|
||||
'upload_asset_url': upload_asset_url,
|
||||
'certificates': certificates,
|
||||
'has_certificate_modes': has_certificate_modes,
|
||||
'course_modes': course_modes,
|
||||
'certificate_web_view_url': certificate_web_view_url,
|
||||
'is_active': is_active,
|
||||
'is_global_staff': GlobalStaff().has_user(user),
|
||||
'certificate_activation_handler_url': activation_handler_url,
|
||||
'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course_key),
|
||||
}
|
||||
return context
|
||||
|
||||
|
||||
class StudioPermissionsService:
|
||||
"""
|
||||
Service that can provide information about a user's permissions.
|
||||
|
||||
@@ -30,6 +30,7 @@ from django.conf import settings
|
||||
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.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.http import require_http_methods
|
||||
@@ -37,20 +38,20 @@ from eventtracking import tracker
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import AssetKey, CourseKey
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
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, CourseInstructorRole
|
||||
from common.djangoapps.student.roles import GlobalStaff
|
||||
from common.djangoapps.util.db import MYSQL_MAX_INT, generate_int_id
|
||||
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 ..exceptions import AssetNotFoundException
|
||||
from ..toggles import use_new_certificates_page
|
||||
from ..utils import (
|
||||
get_lms_link_for_certificate_web_view,
|
||||
get_proctored_exam_settings_url,
|
||||
reverse_course_url
|
||||
get_certificates_context,
|
||||
get_certificates_url,
|
||||
reverse_course_url,
|
||||
)
|
||||
from .assets import delete_asset
|
||||
|
||||
@@ -393,44 +394,10 @@ def certificates_list_handler(request, course_key_string):
|
||||
return JsonResponse({"error": msg}, status=403)
|
||||
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
|
||||
certificate_url = reverse_course_url('certificates_list_handler', course_key)
|
||||
course_outline_url = reverse_course_url('course_handler', course_key)
|
||||
upload_asset_url = reverse_course_url('assets_handler', course_key)
|
||||
activation_handler_url = reverse_course_url(
|
||||
handler_name='certificate_activation_handler',
|
||||
course_key=course_key
|
||||
)
|
||||
course_modes = [
|
||||
mode.slug for mode in CourseMode.modes_for_course(
|
||||
course_id=course.id, include_expired=True
|
||||
) if mode.slug != 'audit'
|
||||
]
|
||||
|
||||
has_certificate_modes = len(course_modes) > 0
|
||||
|
||||
if has_certificate_modes:
|
||||
certificate_web_view_url = get_lms_link_for_certificate_web_view(
|
||||
course_key=course_key,
|
||||
mode=course_modes[0] # CourseMode.modes_for_course returns default mode if doesn't find anyone.
|
||||
)
|
||||
else:
|
||||
certificate_web_view_url = None
|
||||
is_active, certificates = CertificateManager.is_activated(course)
|
||||
return render_to_response('certificates.html', {
|
||||
'context_course': course,
|
||||
'certificate_url': certificate_url,
|
||||
'course_outline_url': course_outline_url,
|
||||
'upload_asset_url': upload_asset_url,
|
||||
'certificates': certificates,
|
||||
'has_certificate_modes': has_certificate_modes,
|
||||
'course_modes': course_modes,
|
||||
'certificate_web_view_url': certificate_web_view_url,
|
||||
'is_active': is_active,
|
||||
'is_global_staff': GlobalStaff().has_user(request.user),
|
||||
'is_course_instructor': CourseInstructorRole(course.id).has_user(request.user),
|
||||
'certificate_activation_handler_url': activation_handler_url,
|
||||
'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course.id),
|
||||
})
|
||||
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':
|
||||
|
||||
Reference in New Issue
Block a user