prevent changes to settings via POST prevent changes to settings via POST updated test name updated based on feedback removed comments
178 lines
6.8 KiB
Python
178 lines
6.8 KiB
Python
"Contentstore Views"
|
|
import copy
|
|
|
|
from opaque_keys.edx.keys import CourseKey
|
|
from rest_framework import status
|
|
from rest_framework.exceptions import NotFound
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
|
|
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,
|
|
ProctoredExamSettingsSerializer,
|
|
LimitedProctoredExamSettingsSerializer,
|
|
)
|
|
|
|
|
|
@view_auth_classes()
|
|
class ProctoredExamSettingsView(APIView):
|
|
"""
|
|
A view for retrieving information about proctored exam settings for a course.
|
|
|
|
Path: ``/api/contentstore/v1/proctored_exam_settings/{course_id}``
|
|
|
|
Accepts: [GET, POST]
|
|
|
|
------------------------------------------------------------------------------------
|
|
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
|
|
}
|
|
|
|
------------------------------------------------------------------------------------
|
|
POST
|
|
------------------------------------------------------------------------------------
|
|
|
|
**Returns**
|
|
|
|
* 200: OK - Proctored exam settings saved.
|
|
* 400: Bad Request - Unable to save requested 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 echo the updated proctored
|
|
exam settings data.
|
|
"""
|
|
PROCTORED_EXAM_SETTINGS_KEYS = [
|
|
'enable_proctored_exams',
|
|
'allow_proctoring_opt_out',
|
|
'proctoring_provider',
|
|
'proctoring_escalation_email',
|
|
'create_zendesk_tickets',
|
|
]
|
|
|
|
def get(self, request, course_id):
|
|
""" GET handler """
|
|
with modulestore().bulk_operations(CourseKey.from_string(course_id)):
|
|
course_module = self._get_and_validate_course_access(request.user, course_id)
|
|
course_metadata = CourseMetadata().fetch_all(course_module)
|
|
proctored_exam_settings = self._get_proctored_exam_setting_values(course_metadata)
|
|
|
|
data = {}
|
|
|
|
data['proctored_exam_settings'] = proctored_exam_settings
|
|
data['available_proctoring_providers'] = get_available_providers()
|
|
data['course_start_date'] = course_metadata['start'].get('value')
|
|
data['is_staff'] = request.user.is_staff
|
|
|
|
serializer = ProctoredExamConfigurationSerializer(data)
|
|
|
|
return Response(serializer.data)
|
|
|
|
def post(self, request, course_id):
|
|
""" POST handler """
|
|
serializer = ProctoredExamSettingsSerializer if request.user.is_staff else LimitedProctoredExamSettingsSerializer
|
|
exam_config = serializer(data=request.data.get('proctored_exam_settings', {}))
|
|
valid_request = exam_config.is_valid()
|
|
if not request.user.is_staff and valid_request and ProctoredExamSettingsSerializer(data=request.data.get('proctored_exam_settings', {})).is_valid():
|
|
return Response(status=status.HTTP_403_FORBIDDEN)
|
|
|
|
with modulestore().bulk_operations(CourseKey.from_string(course_id)):
|
|
course_module = self._get_and_validate_course_access(request.user, course_id)
|
|
course_metadata = CourseMetadata().fetch_all(course_module)
|
|
|
|
models_to_update = {}
|
|
for setting_key, value in exam_config.data.items():
|
|
model = course_metadata.get(setting_key)
|
|
if model:
|
|
models_to_update[setting_key] = copy.deepcopy(model)
|
|
models_to_update[setting_key]['value'] = value
|
|
|
|
# validate data formats and update the course module object
|
|
is_valid, errors, updated_data = CourseMetadata.validate_and_update_from_json(
|
|
course_module,
|
|
models_to_update,
|
|
user=request.user,
|
|
)
|
|
|
|
if not is_valid:
|
|
error_messages = [{error.get('key'): error.get('message')} for error in errors]
|
|
return Response(
|
|
{'detail': error_messages},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# save to mongo
|
|
modulestore().update_item(course_module, request.user.id)
|
|
|
|
# merge updated settings with all existing settings.
|
|
# do this because fields that could not be modified are excluded from the result
|
|
course_metadata = {**course_metadata, **updated_data}
|
|
updated_setttings = self._get_proctored_exam_setting_values(course_metadata)
|
|
serializer = ProctoredExamSettingsSerializer(updated_setttings)
|
|
return Response({
|
|
'proctored_exam_settings': serializer.data
|
|
})
|
|
|
|
@classmethod
|
|
def _get_proctored_exam_setting_values(cls, course_metadata):
|
|
return {
|
|
setting_key: course_metadata[setting_key].get('value')
|
|
for setting_key in cls.PROCTORED_EXAM_SETTINGS_KEYS
|
|
}
|
|
|
|
@staticmethod
|
|
def _get_and_validate_course_access(user, course_id):
|
|
"""
|
|
Check if course_id exists and is accessible by the user.
|
|
|
|
Returns a course_module object
|
|
"""
|
|
course_key = CourseKey.from_string(course_id)
|
|
course_module = get_course_and_check_access(course_key, user)
|
|
|
|
if not course_module:
|
|
raise NotFound(
|
|
'Course with course_id {} does not exist.'.format(course_id)
|
|
)
|
|
|
|
return course_module
|