add rest_api for contentstore app, including a proctored_exam_settings view

This commit is contained in:
Michael Roytman
2020-07-08 16:59:45 -04:00
parent 3879ad2252
commit 6601111982
8 changed files with 266 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
"""
Contentstore API URLs.
"""
from django.urls import include, re_path
from .v1 import urls as v1_urls
app_name = 'cms.djangoapps.contentstore'
urlpatterns = [
re_path(r'^v1/', include(v1_urls))
]

View File

@@ -0,0 +1,24 @@
"""
API Serializers for Contentstore
"""
from rest_framework import serializers
from common.lib.xmodule.xmodule.course_module import get_available_providers
class ProctoredExamSettingsSerializer(serializers.Serializer):
""" Serializer for proctored exam settings. """
enable_proctored_exams = serializers.BooleanField()
allow_proctoring_opt_out = serializers.BooleanField()
proctoring_provider = serializers.CharField()
proctoring_escalation_email = serializers.CharField()
create_zendesk_tickets = serializers.BooleanField()
class ProctoredExamConfigurationSerializer(serializers.Serializer):
""" Serializer for various metadata associated with proctored exam settings. """
proctored_exam_settings = ProctoredExamSettingsSerializer()
available_proctoring_providers = serializers.ChoiceField(get_available_providers())
course_start_date = serializers.DateTimeField()
is_staff = serializers.BooleanField()

View File

@@ -0,0 +1,107 @@
"""
Unit tests for Contentstore views.
"""
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from opaque_keys.edx.keys import CourseKey
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory, InstructorFactory
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
class ProctoringExamSettingsGetTests(SharedModuleStoreTestCase, APITestCase):
""" Tests for proctored exam settings GETs """
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course_key = CourseKey.from_string('course-v1:edX+ToyX+Toy_Course')
cls.other_course_key = CourseKey.from_string('course-v1:edX+ToyX_Other_Course+Toy_Course')
cls.course = cls.create_course_from_course_key(cls.course_key)
cls.other_course = cls.create_course_from_course_key(cls.other_course_key)
cls.password = 'password'
cls.student = UserFactory.create(username='student', password=cls.password)
cls.global_staff = GlobalStaffFactory(username='global-staff', password=cls.password)
cls.course_instructor = InstructorFactory(
username='instructor',
password=cls.password,
course_key=cls.course.id,
)
cls.other_course_instructor = InstructorFactory(
username='other-course-instructor',
password=cls.password,
course_key=cls.other_course.id,
)
def tearDown(self):
super().tearDown()
self.client.logout()
@classmethod
def create_course_from_course_key(cls, course_key):
return CourseFactory.create(
org=course_key.org,
course=course_key.course,
run=course_key.run
)
def get_url(self, course_key):
return reverse(
'cms.djangoapps.contentstore:v1:proctored_exam_settings',
kwargs={'course_id': course_key}
)
def get_expected_response_data(self, course, user):
return {
'proctored_exam_settings': {
'enable_proctored_exams': course.enable_proctored_exams,
'allow_proctoring_opt_out': course.allow_proctoring_opt_out,
'proctoring_provider': course.proctoring_provider,
'proctoring_escalation_email': course.proctoring_escalation_email,
'create_zendesk_tickets': course.create_zendesk_tickets,
},
'course_start_date': '2030-01-01T00:00:00Z',
'available_proctoring_providers': ['null'],
'is_staff': user.is_staff,
}
def test_403_if_student(self):
self.client.login(username=self.student.username, password=self.password)
url = self.get_url(self.course.id)
response = self.client.get(url)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_403_if_instructor_in_another_course(self):
self.client.login(
username=self.other_course_instructor.username,
password=self.password
)
url = self.get_url(self.course.id)
response = self.client.get(url)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_200_global_staff(self):
self.client.login(username=self.global_staff.username, password=self.password)
url = self.get_url(self.course.id)
response = self.client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.data == self.get_expected_response_data(self.course, self.global_staff)
def test_200_course_instructor(self):
self.client.login(username=self.course_instructor.username, password=self.password)
url = self.get_url(self.course.id)
response = self.client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.data == self.get_expected_response_data(self.course, self.course_instructor)
def test_404_no_course_module(self):
course_id = 'course-v1:edX+ToyX_Nonexistent_Course+Toy_Course'
self.client.login(username=self.global_staff, password=self.password)
url = self.get_url(course_id)
response = self.client.get(url)
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.data == 'Course with course_id {} does not exist.'.format(course_id)

View File

@@ -0,0 +1,16 @@
""" Contenstore API v1 URLs. """
from django.urls import re_path
from . import views
from openedx.core.constants import COURSE_ID_PATTERN
app_name = 'v1'
urlpatterns = [
re_path(
r'^proctored_exam_settings/{}$'.format(COURSE_ID_PATTERN),
views.proctored_exam_settings,
name="proctored_exam_settings"
),
]

View File

@@ -0,0 +1,102 @@
"Contentstore Views"
from opaque_keys.edx.keys import CourseKey
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from common.lib.xmodule.xmodule.course_module import get_available_providers
from contentstore.views.course import get_course_and_check_access
from models.settings.course_metadata import CourseMetadata
from openedx.core.lib.api.view_utils import view_auth_classes
from xmodule.modulestore.django import modulestore
from contentstore.rest_api.v1.serializers import ProctoredExamConfigurationSerializer
@api_view(['GET'])
@view_auth_classes()
def proctored_exam_settings(request, course_id):
"""
A view for retrieving information about proctored exam settings for a course.
Path: ``/api/contentstore/v1/proctored_exam_settings/{course_id}``
Accepts: [GET]
------------------------------------------------------------------------------------
GET
------------------------------------------------------------------------------------
**Returns**
* 200: OK - Contains a set of course proctored exam settings.
* 401: The requesting user is not authenticated.
* 403: The requesting user lacks access to the course.
* 404: The requested course does not exist.
**Response**
In the case of a 200 response code, the response will proctored exam settings data
as well as other metadata about the course or the requesting user that are necessary
for rendering the settings page.
**Example**
{
"proctored_exam_settings": {
"enable_proctored_exams": true,
"allow_proctoring_opt_out": true,
"proctoring_provider": "mockprock",
"proctoring_escalation_email": null,
"create_zendesk_tickets": true
},
"available_proctoring_providers": [
"mockprock",
"proctortrack"
],
"course_start_date": "2013-02-05T05:00:00Z",
"is_staff": true
}
"""
course_key = CourseKey.from_string(course_id)
with modulestore().bulk_operations(course_key):
if request.method == 'GET':
course_module = get_course_and_check_access(course_key, request.user)
if not course_module:
return Response(
'Course with course_id {} does not exist.'.format(course_id),
status=status.HTTP_404_NOT_FOUND
)
course_metadata = CourseMetadata().fetch_all(course_module)
data = {}
# specify only the advanced settings we want to return
proctored_exam_settings_advanced_settings_keys = [
'enable_proctored_exams',
'allow_proctoring_opt_out',
'proctoring_provider',
'proctoring_escalation_email',
'create_zendesk_tickets',
'start'
]
proctored_exam_settings_data = {
setting_key: setting_value.get('value')
for (setting_key, setting_value) in course_metadata.items()
if setting_key in proctored_exam_settings_advanced_settings_keys
}
data['proctored_exam_settings'] = proctored_exam_settings_data
data['available_proctoring_providers'] = get_available_providers()
# move start key:value out of proctored_exam_settings dictionary and change key
data['course_start_date'] = proctored_exam_settings_data['start']
del data['proctored_exam_settings']['start']
data['is_staff'] = request.user.is_staff
serializer = ProctoredExamConfigurationSerializer(data)
return Response(serializer.data)

View File

@@ -300,3 +300,8 @@ if 'openedx.testing.coverage_context_listener' in settings.INSTALLED_APPS:
from openedx.core.djangoapps.plugins import constants as plugin_constants, plugin_urls
urlpatterns.extend(plugin_urls.get_patterns(plugin_constants.ProjectType.CMS))
# Contentstore
urlpatterns += [
url(r'^api/contentstore/', include('cms.djangoapps.contentstore.rest_api.urls'))
]