Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [AXIMST-148] API for certificate drf #2489

Merged
merged 1 commit into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
)
6 changes: 6 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from .views import (
ContainerHandlerView,
CourseCertificatesView,
CourseDetailsView,
CourseTeamView,
CourseIndexView,
Expand Down Expand Up @@ -103,6 +104,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 @@ -1908,6 +1908,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
Loading