Added REST API for certificates in LMS (#12055)
This commit is contained in:
committed by
Jim Abramson
parent
0d27b5ed87
commit
090cefcf44
@@ -8,28 +8,28 @@ import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from eventtracking import tracker
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from branding import api as branding_api
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule_django.models import CourseKeyField
|
||||
from util.organizations_helpers import get_course_organizations
|
||||
|
||||
from certificates.models import (
|
||||
CertificateStatuses,
|
||||
certificate_status_for_student,
|
||||
CertificateGenerationCourseSetting,
|
||||
CertificateGenerationConfiguration,
|
||||
ExampleCertificateSet,
|
||||
GeneratedCertificate,
|
||||
CertificateGenerationCourseSetting,
|
||||
CertificateStatuses,
|
||||
CertificateTemplate,
|
||||
CertificateTemplateAsset,
|
||||
ExampleCertificateSet,
|
||||
GeneratedCertificate,
|
||||
certificate_status_for_student,
|
||||
)
|
||||
from certificates.queue import XQueueCertInterface
|
||||
from branding import api as branding_api
|
||||
|
||||
|
||||
log = logging.getLogger("edx.certificate")
|
||||
|
||||
@@ -43,6 +43,36 @@ def is_passing_status(cert_status):
|
||||
return CertificateStatuses.is_passing_status(cert_status)
|
||||
|
||||
|
||||
def format_certificate_for_user(username, cert):
|
||||
"""
|
||||
Helper function to serialize an user certificate.
|
||||
|
||||
Arguments:
|
||||
username (unicode): The identifier of the user.
|
||||
cert (GeneratedCertificate): a user certificate
|
||||
|
||||
Returns: dict
|
||||
"""
|
||||
return {
|
||||
"username": username,
|
||||
"course_key": cert.course_id,
|
||||
"type": cert.mode,
|
||||
"status": cert.status,
|
||||
"grade": cert.grade,
|
||||
"created": cert.created_date,
|
||||
"modified": cert.modified_date,
|
||||
|
||||
# NOTE: the download URL is not currently being set for webview certificates.
|
||||
# In the future, we can update this to construct a URL to the webview certificate
|
||||
# for courses that have this feature enabled.
|
||||
"download_url": (
|
||||
cert.download_url or get_certificate_url(cert.user.id, cert.course_id)
|
||||
if cert.status == CertificateStatuses.downloadable
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_certificates_for_user(username):
|
||||
"""
|
||||
Retrieve certificate information for a particular user.
|
||||
@@ -57,7 +87,7 @@ def get_certificates_for_user(username):
|
||||
[
|
||||
{
|
||||
"username": "bob",
|
||||
"course_key": "edX/DemoX/Demo_Course",
|
||||
"course_key": CourseLocator('edX', 'DemoX', 'Demo_Course', None, None),
|
||||
"type": "verified",
|
||||
"status": "downloadable",
|
||||
"download_url": "http://www.example.com/cert.pdf",
|
||||
@@ -69,28 +99,30 @@ def get_certificates_for_user(username):
|
||||
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"username": username,
|
||||
"course_key": cert.course_id,
|
||||
"type": cert.mode,
|
||||
"status": cert.status,
|
||||
"grade": cert.grade,
|
||||
"created": cert.created_date,
|
||||
"modified": cert.modified_date,
|
||||
|
||||
# NOTE: the download URL is not currently being set for webview certificates.
|
||||
# In the future, we can update this to construct a URL to the webview certificate
|
||||
# for courses that have this feature enabled.
|
||||
"download_url": (
|
||||
cert.download_url or get_certificate_url(cert.user.id, cert.course_id)
|
||||
if cert.status == CertificateStatuses.downloadable
|
||||
else None
|
||||
),
|
||||
}
|
||||
format_certificate_for_user(username, cert)
|
||||
for cert in GeneratedCertificate.eligible_certificates.filter(user__username=username).order_by("course_id")
|
||||
]
|
||||
|
||||
|
||||
def get_certificate_for_user(username, course_key):
|
||||
"""
|
||||
Retrieve certificate information for a particular user for a specific course.
|
||||
|
||||
Arguments:
|
||||
username (unicode): The identifier of the user.
|
||||
course_key (CourseKey): A Course Key.
|
||||
Returns: dict
|
||||
"""
|
||||
try:
|
||||
cert = GeneratedCertificate.eligible_certificates.get(
|
||||
user__username=username,
|
||||
course_id=course_key
|
||||
)
|
||||
except GeneratedCertificate.DoesNotExist:
|
||||
return None
|
||||
return format_certificate_for_user(username, cert)
|
||||
|
||||
|
||||
def generate_user_certificates(student, course_key, course=None, insecure=False, generation_mode='batch',
|
||||
forced_grade=None):
|
||||
"""
|
||||
|
||||
0
lms/djangoapps/certificates/apis/__init__.py
Normal file
0
lms/djangoapps/certificates/apis/__init__.py
Normal file
11
lms/djangoapps/certificates/apis/urls.py
Normal file
11
lms/djangoapps/certificates/apis/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
""" Certificates API URLs. """
|
||||
from django.conf.urls import (
|
||||
include,
|
||||
patterns,
|
||||
url,
|
||||
)
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^v0/', include('lms.djangoapps.certificates.apis.v0.urls', namespace='v0')),
|
||||
)
|
||||
0
lms/djangoapps/certificates/apis/v0/__init__.py
Normal file
0
lms/djangoapps/certificates/apis/v0/__init__.py
Normal file
116
lms/djangoapps/certificates/apis/v0/tests/test_views.py
Normal file
116
lms/djangoapps/certificates/apis/v0/tests/test_views.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Tests for the Certificate REST APIs.
|
||||
"""
|
||||
from django.core.urlresolvers import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from certificates.models import CertificateStatuses
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
from course_modes.models import CourseMode
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
class CertificatesRestApiTest(SharedModuleStoreTestCase, APITestCase):
|
||||
"""
|
||||
Test for the Certificates REST APIs
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CertificatesRestApiTest, cls).setUpClass()
|
||||
cls.course = CourseFactory.create(
|
||||
org='edx',
|
||||
number='verified',
|
||||
display_name='Verified Course'
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(CertificatesRestApiTest, self).setUp()
|
||||
|
||||
self.student = UserFactory.create(password='test')
|
||||
self.student_no_cert = UserFactory.create(password='test')
|
||||
self.staff_user = UserFactory.create(password='test', is_staff=True)
|
||||
|
||||
GeneratedCertificateFactory.create(
|
||||
user=self.student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.downloadable,
|
||||
mode='verified',
|
||||
download_url='www.google.com',
|
||||
grade="0.88"
|
||||
)
|
||||
|
||||
self.namespaced_url = 'certificates_api:v0:certificates:detail'
|
||||
|
||||
def get_url(self, username):
|
||||
"""
|
||||
Helper function to create the url for certificates
|
||||
"""
|
||||
return reverse(
|
||||
self.namespaced_url,
|
||||
kwargs={
|
||||
'course_id': self.course.id,
|
||||
'username': username
|
||||
}
|
||||
)
|
||||
|
||||
def test_permissions(self):
|
||||
"""
|
||||
Test that only the owner of the certificate can access the url
|
||||
"""
|
||||
# anonymous user
|
||||
resp = self.client.get(self.get_url(self.student.username))
|
||||
self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# another student
|
||||
self.client.login(username=self.student_no_cert.username, password='test')
|
||||
resp = self.client.get(self.get_url(self.student.username))
|
||||
# gets 404 instead of 403 for security reasons
|
||||
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertEqual(resp.data, {u'detail': u'Not found.'}) # pylint: disable=no-member
|
||||
self.client.logout()
|
||||
|
||||
# same student of the certificate
|
||||
self.client.login(username=self.student.username, password='test')
|
||||
resp = self.client.get(self.get_url(self.student.username))
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
self.client.logout()
|
||||
|
||||
# staff user
|
||||
self.client.login(username=self.staff_user.username, password='test')
|
||||
resp = self.client.get(self.get_url(self.student.username))
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_no_certificate_for_user(self):
|
||||
"""
|
||||
Test for case with no certificate available
|
||||
"""
|
||||
self.client.login(username=self.student_no_cert.username, password='test')
|
||||
resp = self.client.get(self.get_url(self.student_no_cert.username))
|
||||
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertIn('error_code', resp.data) # pylint: disable=no-member
|
||||
self.assertEqual(
|
||||
resp.data['error_code'], # pylint: disable=no-member
|
||||
'no_certificate_for_user'
|
||||
)
|
||||
|
||||
def test_certificate_for_user(self):
|
||||
"""
|
||||
Tests case user that pulls her own certificate
|
||||
"""
|
||||
self.client.login(username=self.student.username, password='test')
|
||||
resp = self.client.get(self.get_url(self.student.username))
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
resp.data, # pylint: disable=no-member
|
||||
{
|
||||
'username': self.student.username,
|
||||
'status': CertificateStatuses.downloadable,
|
||||
'grade': '0.88',
|
||||
'download_url': 'www.google.com',
|
||||
'certificate_type': CourseMode.VERIFIED,
|
||||
'course_id': unicode(self.course.id),
|
||||
}
|
||||
)
|
||||
27
lms/djangoapps/certificates/apis/v0/urls.py
Normal file
27
lms/djangoapps/certificates/apis/v0/urls.py
Normal file
@@ -0,0 +1,27 @@
|
||||
""" Certificates API v0 URLs. """
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import (
|
||||
include,
|
||||
patterns,
|
||||
url,
|
||||
)
|
||||
|
||||
from lms.djangoapps.certificates.apis.v0 import views
|
||||
|
||||
|
||||
CERTIFICATES_URLS = patterns(
|
||||
'',
|
||||
url(
|
||||
r'^{username}/courses/{course_id}/$'.format(
|
||||
username=settings.USERNAME_PATTERN,
|
||||
course_id=settings.COURSE_ID_PATTERN
|
||||
),
|
||||
views.CertificatesDetailView.as_view(), name='detail'
|
||||
),
|
||||
)
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^certificates/', include(CERTIFICATES_URLS, namespace='certificates')),
|
||||
)
|
||||
106
lms/djangoapps/certificates/apis/v0/views.py
Normal file
106
lms/djangoapps/certificates/apis/v0/views.py
Normal file
@@ -0,0 +1,106 @@
|
||||
""" API v0 views. """
|
||||
import logging
|
||||
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_oauth.authentication import OAuth2Authentication
|
||||
|
||||
from lms.djangoapps.certificates.api import get_certificate_for_user
|
||||
from openedx.core.lib.api import permissions
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CertificatesDetailView(GenericAPIView):
|
||||
"""
|
||||
**Use Case**
|
||||
|
||||
* Get the details of a certificate for a specific user in a course.
|
||||
|
||||
**Example Request**
|
||||
|
||||
GET /api/certificates/v0/certificates/{username}/{course_id}
|
||||
|
||||
**GET Parameters**
|
||||
|
||||
A GET request must include the following parameters.
|
||||
|
||||
* username: A string representation of an user's username.
|
||||
* course_id: A string representation of a Course ID.
|
||||
|
||||
**GET Response Values**
|
||||
|
||||
If the request for information about the Certificate is successful, an HTTP 200 "OK" response
|
||||
is returned.
|
||||
|
||||
The HTTP 200 response has the following values.
|
||||
|
||||
* username: A string representation of an user's username passed in the request.
|
||||
|
||||
* course_id: A string representation of a Course ID.
|
||||
|
||||
* certificate_type: A string representation of the certificate type.
|
||||
Can be honor|verified|professional
|
||||
|
||||
* status: A string representation of the certificate status.
|
||||
|
||||
* download_url: A string representation of the certificate url.
|
||||
|
||||
* grade: A string representation of a float for the user's course grade.
|
||||
|
||||
**Example GET Response**
|
||||
|
||||
{
|
||||
"username": "bob",
|
||||
"course_id": "edX/DemoX/Demo_Course",
|
||||
"certificate_type": "verified",
|
||||
"status": "downloadable",
|
||||
"download_url": "http://www.example.com/cert.pdf",
|
||||
"grade": "0.98"
|
||||
}
|
||||
"""
|
||||
|
||||
authentication_classes = (OAuth2Authentication, SessionAuthentication,)
|
||||
permission_classes = (IsAuthenticated, permissions.IsUserInUrlOrStaff)
|
||||
|
||||
def get(self, request, username, course_id):
|
||||
"""
|
||||
Gets a certificate information.
|
||||
|
||||
Args:
|
||||
request (Request): Django request object.
|
||||
username (string): URI element specifying the user's username.
|
||||
course_id (string): URI element specifying the course location.
|
||||
|
||||
Return:
|
||||
A JSON serialized representation of the certificate.
|
||||
"""
|
||||
try:
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
except InvalidKeyError:
|
||||
log.warning('Course ID string "%s" is not valid', course_id)
|
||||
return Response(
|
||||
status=404,
|
||||
data={'error_code': 'course_id_not_valid'}
|
||||
)
|
||||
|
||||
user_cert = get_certificate_for_user(username=username, course_key=course_key)
|
||||
if user_cert is None:
|
||||
return Response(
|
||||
status=404,
|
||||
data={'error_code': 'no_certificate_for_user'}
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"username": user_cert.get('username'),
|
||||
"course_id": unicode(user_cert.get('course_key')),
|
||||
"certificate_type": user_cert.get('type'),
|
||||
"status": user_cert.get('status'),
|
||||
"download_url": user_cert.get('download_url'),
|
||||
"grade": user_cert.get('grade')
|
||||
}
|
||||
)
|
||||
@@ -8,16 +8,20 @@ from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
from mock import patch
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
from config_models.models import cache
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from config_models.models import cache
|
||||
from microsite_configuration import microsite
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from util.testing import EventTestMixin
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase,
|
||||
SharedModuleStoreTestCase,
|
||||
)
|
||||
|
||||
from certificates import api as certs_api
|
||||
from certificates.models import (
|
||||
@@ -30,7 +34,6 @@ from certificates.models import (
|
||||
from certificates.queue import XQueueCertInterface, XQueueAddToQueueError
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
|
||||
from microsite_configuration import microsite
|
||||
|
||||
FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy()
|
||||
FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True
|
||||
@@ -200,6 +203,93 @@ class CertificateDownloadableStatusTests(WebCertificateTestMixin, ModuleStoreTes
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class CertificateGetTests(SharedModuleStoreTestCase):
|
||||
"""Tests for the `test_get_certificate_for_user` helper function. """
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CertificateGetTests, cls).setUpClass()
|
||||
cls.student = UserFactory()
|
||||
cls.student_no_cert = UserFactory()
|
||||
cls.course_1 = CourseFactory.create(
|
||||
org='edx',
|
||||
number='verified_1',
|
||||
display_name='Verified Course 1'
|
||||
)
|
||||
cls.course_2 = CourseFactory.create(
|
||||
org='edx',
|
||||
number='verified_2',
|
||||
display_name='Verified Course 2'
|
||||
)
|
||||
# certificate for the first course
|
||||
GeneratedCertificateFactory.create(
|
||||
user=cls.student,
|
||||
course_id=cls.course_1.id,
|
||||
status=CertificateStatuses.downloadable,
|
||||
mode='verified',
|
||||
download_url='www.google.com',
|
||||
grade="0.88",
|
||||
)
|
||||
# certificate for the second course
|
||||
GeneratedCertificateFactory.create(
|
||||
user=cls.student,
|
||||
course_id=cls.course_2.id,
|
||||
status=CertificateStatuses.downloadable,
|
||||
mode='honor',
|
||||
download_url='www.gmail.com',
|
||||
grade="0.99",
|
||||
)
|
||||
|
||||
def test_get_certificate_for_user(self):
|
||||
"""
|
||||
Test to get a certificate for a user for a specific course.
|
||||
"""
|
||||
cert = certs_api.get_certificate_for_user(self.student.username, self.course_1.id)
|
||||
|
||||
self.assertEqual(cert['username'], self.student.username)
|
||||
self.assertEqual(cert['course_key'], self.course_1.id)
|
||||
self.assertEqual(cert['type'], CourseMode.VERIFIED)
|
||||
self.assertEqual(cert['status'], CertificateStatuses.downloadable)
|
||||
self.assertEqual(cert['grade'], "0.88")
|
||||
self.assertEqual(cert['download_url'], 'www.google.com')
|
||||
|
||||
def test_get_certificates_for_user(self):
|
||||
"""
|
||||
Test to get all the certificates for a user
|
||||
"""
|
||||
certs = certs_api.get_certificates_for_user(self.student.username)
|
||||
self.assertEqual(len(certs), 2)
|
||||
self.assertEqual(certs[0]['username'], self.student.username)
|
||||
self.assertEqual(certs[1]['username'], self.student.username)
|
||||
self.assertEqual(certs[0]['course_key'], self.course_1.id)
|
||||
self.assertEqual(certs[1]['course_key'], self.course_2.id)
|
||||
self.assertEqual(certs[0]['type'], CourseMode.VERIFIED)
|
||||
self.assertEqual(certs[1]['type'], CourseMode.HONOR)
|
||||
self.assertEqual(certs[0]['status'], CertificateStatuses.downloadable)
|
||||
self.assertEqual(certs[1]['status'], CertificateStatuses.downloadable)
|
||||
self.assertEqual(certs[0]['grade'], '0.88')
|
||||
self.assertEqual(certs[1]['grade'], '0.99')
|
||||
self.assertEqual(certs[0]['download_url'], 'www.google.com')
|
||||
self.assertEqual(certs[1]['download_url'], 'www.gmail.com')
|
||||
|
||||
def test_no_certificate_for_user(self):
|
||||
"""
|
||||
Test the case when there is no certificate for a user for a specific course.
|
||||
"""
|
||||
self.assertIsNone(
|
||||
certs_api.get_certificate_for_user(self.student_no_cert.username, self.course_1.id)
|
||||
)
|
||||
|
||||
def test_no_certificates_for_user(self):
|
||||
"""
|
||||
Test the case when there are no certificates for a user.
|
||||
"""
|
||||
self.assertEqual(
|
||||
certs_api.get_certificates_for_user(self.student_no_cert.username),
|
||||
[]
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@override_settings(CERT_QUEUE='certificates')
|
||||
class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, ModuleStoreTestCase):
|
||||
|
||||
@@ -925,6 +925,10 @@ urlpatterns += (
|
||||
url(r'^update_certificate$', 'certificates.views.update_certificate'),
|
||||
url(r'^update_example_certificate$', 'certificates.views.update_example_certificate'),
|
||||
url(r'^request_certificate$', 'certificates.views.request_certificate'),
|
||||
|
||||
# REST APIs
|
||||
url(r'^api/certificates/',
|
||||
include('lms.djangoapps.certificates.apis.urls', namespace='certificates_api')),
|
||||
)
|
||||
|
||||
# XDomain proxy
|
||||
|
||||
Reference in New Issue
Block a user