Skip to content

Commit

Permalink
feat: [AXIMST-148] API for certificates DRF (#2489)
Browse files Browse the repository at this point in the history
  • Loading branch information
ruzniaievdm authored and monteri committed Jan 24, 2024
1 parent 990a6f1 commit d0d737f
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
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
)
7 changes: 7 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from openedx.core.constants import COURSE_ID_PATTERN

from .views import (
ContainerHandlerView,
CourseCertificatesView,
CourseDetailsView,
CourseTeamView,
CourseIndexView,
Expand Down Expand Up @@ -101,6 +103,11 @@
CourseRerunView.as_view(),
name="course_rerun"
),
re_path(
fr'^certificates/{COURSE_ID_PATTERN}$',
CourseCertificatesView.as_view(),
name="certificates"
),
re_path(
fr'^container_handler/{settings.USAGE_KEY_PATTERN}$',
ContainerHandlerView.as_view(),
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
103 changes: 103 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/certificates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
""" 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@[email protected]",
"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_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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
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)
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)
50 changes: 50 additions & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1910,6 +1910,56 @@ def get_container_handler_context(request, usage_key, course, xblock): # pylint
return context


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.
Expand Down
45 changes: 4 additions & 41 deletions cms/djangoapps/contentstore/views/certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
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
Expand All @@ -48,9 +47,8 @@

from ..exceptions import AssetNotFoundException
from ..utils import (
get_lms_link_for_certificate_web_view,
get_proctored_exam_settings_url,
reverse_course_url
get_certificates_context,
reverse_course_url,
)
from .assets import delete_asset

Expand Down Expand Up @@ -393,43 +391,8 @@ 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),
'certificate_activation_handler_url': activation_handler_url,
'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course.id),
})
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':
Expand Down

0 comments on commit d0d737f

Please sign in to comment.