feat: Extending API functionality for proctoring errors (#32331)
This commit is contained in:
@@ -81,3 +81,13 @@ class CourseAdvanceSettingViewTest(CourseTestCase, MilestonesTestCaseMixin):
|
||||
assert field in content.keys()
|
||||
for field in absent_fields:
|
||||
assert field not in content.keys()
|
||||
|
||||
@ddt.data(
|
||||
("ENABLE_EDXNOTES", "edxnotes"),
|
||||
("ENABLE_OTHER_COURSE_SETTINGS", "other_course_settings"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_disabled_fetch_all_query_param(self, setting, excluded_field):
|
||||
with override_settings(FEATURES={setting: False}):
|
||||
resp = self.client.get(self.url, {"fetch_all": 0})
|
||||
assert excluded_field not in resp.data
|
||||
|
||||
@@ -10,6 +10,7 @@ from rest_framework.views import APIView
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
|
||||
from cms.djangoapps.contentstore.api.views.utils import get_bool_param
|
||||
from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes
|
||||
from ..serializers import CourseAdvancedSettingsSerializer
|
||||
@@ -39,9 +40,14 @@ class AdvancedCourseSettingsView(DeveloperErrorViewMixin, APIView):
|
||||
apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"),
|
||||
apidocs.string_parameter(
|
||||
"filter_fields",
|
||||
apidocs.ParameterLocation.PATH,
|
||||
apidocs.ParameterLocation.QUERY,
|
||||
description="Comma separated list of fields to filter",
|
||||
),
|
||||
apidocs.string_parameter(
|
||||
"fetch_all",
|
||||
apidocs.ParameterLocation.QUERY,
|
||||
description="Specifies whether to fetch all settings or only enabled ones",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: CourseAdvancedSettingsSerializer,
|
||||
@@ -112,7 +118,13 @@ class AdvancedCourseSettingsView(DeveloperErrorViewMixin, APIView):
|
||||
if not has_studio_read_access(request.user, course_key):
|
||||
self.permission_denied(request)
|
||||
course_block = modulestore().get_course(course_key)
|
||||
return Response(CourseMetadata.fetch_all(
|
||||
fetch_all = get_bool_param(request, 'fetch_all', True)
|
||||
if fetch_all:
|
||||
return Response(CourseMetadata.fetch_all(
|
||||
course_block,
|
||||
filter_fields=filter_query_data.cleaned_data['filter_fields'],
|
||||
))
|
||||
return Response(CourseMetadata.fetch(
|
||||
course_block,
|
||||
filter_fields=filter_query_data.cleaned_data['filter_fields'],
|
||||
))
|
||||
|
||||
@@ -29,3 +29,31 @@ class ProctoredExamConfigurationSerializer(serializers.Serializer):
|
||||
proctored_exam_settings = ProctoredExamSettingsSerializer()
|
||||
available_proctoring_providers = serializers.ChoiceField(get_available_providers())
|
||||
course_start_date = serializers.DateTimeField()
|
||||
|
||||
|
||||
class ProctoringErrorModelSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for proctoring error model item.
|
||||
"""
|
||||
deprecated = serializers.BooleanField()
|
||||
display_name = serializers.CharField()
|
||||
help = serializers.CharField()
|
||||
hide_on_enabled_publisher = serializers.BooleanField()
|
||||
value = serializers.CharField()
|
||||
|
||||
|
||||
class ProctoringErrorListSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for proctoring error list.
|
||||
"""
|
||||
key = serializers.CharField()
|
||||
message = serializers.CharField()
|
||||
model = ProctoringErrorModelSerializer()
|
||||
|
||||
|
||||
class ProctoringErrorsSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for proctoring errors with url to proctored exam settings.
|
||||
"""
|
||||
mfe_proctored_exam_settings_url = serializers.CharField(required=False, allow_null=True, allow_blank=True)
|
||||
proctoring_errors = ProctoringErrorListSerializer(many=True)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Unit tests for Contentstore views.
|
||||
"""
|
||||
import json
|
||||
|
||||
import ddt
|
||||
from mock import patch
|
||||
@@ -12,6 +13,7 @@ from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
|
||||
from common.djangoapps.student.tests.factories import GlobalStaffFactory
|
||||
from common.djangoapps.student.tests.factories import InstructorFactory
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
@@ -437,3 +439,54 @@ class ProctoringExamSettingsPostTests(ProctoringExamSettingsTestMixin, ModuleSto
|
||||
updated = modulestore().get_item(self.course.location)
|
||||
assert updated.enable_proctored_exams is False
|
||||
assert updated.proctoring_provider == 'null'
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CourseProctoringErrorsViewTest(CourseTestCase):
|
||||
"""
|
||||
Tests for ProctoringErrorsView.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.url = reverse(
|
||||
'cms.djangoapps.contentstore:v1:proctoring_errors',
|
||||
kwargs={"course_id": self.course.id},
|
||||
)
|
||||
self.non_staff_client, _ = self.create_non_staff_authed_user_client()
|
||||
|
||||
def get_and_check_developer_response(self, response):
|
||||
"""
|
||||
Make basic asserting about the presence of an error response, and return the developer response.
|
||||
"""
|
||||
content = json.loads(response.content.decode("utf-8"))
|
||||
assert "developer_message" in content
|
||||
return content["developer_message"]
|
||||
|
||||
def test_permissions_unauthenticated(self):
|
||||
"""
|
||||
Test that an error is returned in the absence of auth credentials.
|
||||
"""
|
||||
self.client.logout()
|
||||
response = self.client.get(self.url)
|
||||
error = self.get_and_check_developer_response(response)
|
||||
assert error == "Authentication credentials were not provided."
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_ADVANCED_SETTINGS': True})
|
||||
def test_permissions_unauthorized(self):
|
||||
"""
|
||||
Test that an error is returned if the user is unauthorised.
|
||||
"""
|
||||
response = self.non_staff_client.get(self.url)
|
||||
error = self.get_and_check_developer_response(response)
|
||||
assert error == "You do not have permission to perform this action."
|
||||
|
||||
@ddt.data(False, True)
|
||||
def test_disable_advanced_settings_feature(self, disable_advanced_settings):
|
||||
"""
|
||||
If this feature is enabled, only Django Staff/Superuser should be able to see the proctoring errors.
|
||||
For non-staff users the proctoring errors should be unavailable.
|
||||
"""
|
||||
with override_settings(FEATURES={'DISABLE_ADVANCED_SETTINGS': disable_advanced_settings}):
|
||||
response = self.non_staff_client.get(self.url)
|
||||
self.assertEqual(response.status_code, 403 if disable_advanced_settings else 200)
|
||||
|
||||
@@ -14,4 +14,9 @@ urlpatterns = [
|
||||
views.ProctoredExamSettingsView.as_view(),
|
||||
name="proctored_exam_settings"
|
||||
),
|
||||
re_path(
|
||||
fr'^proctoring_errors/{COURSE_ID_PATTERN}$',
|
||||
views.ProctoringErrorsView.as_view(),
|
||||
name="proctoring_errors"
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
"Contentstore Views"
|
||||
import copy
|
||||
|
||||
from django.conf import settings
|
||||
import edx_api_doc_tools as apidocs
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from cms.djangoapps.contentstore.views.course import get_course_and_check_access
|
||||
from cms.djangoapps.contentstore.utils import get_proctored_exam_settings_url
|
||||
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
|
||||
from common.djangoapps.student.auth import has_studio_advanced_settings_access
|
||||
from xmodule.course_block import get_available_providers # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from openedx.core.djangoapps.course_apps.toggles import exams_ida_enabled
|
||||
from openedx.core.lib.api.view_utils import view_auth_classes
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from .serializers import (
|
||||
LimitedProctoredExamSettingsSerializer,
|
||||
ProctoredExamConfigurationSerializer,
|
||||
ProctoredExamSettingsSerializer
|
||||
ProctoredExamSettingsSerializer,
|
||||
ProctoringErrorsSerializer,
|
||||
)
|
||||
|
||||
|
||||
@@ -182,3 +188,80 @@ class ProctoredExamSettingsView(APIView):
|
||||
)
|
||||
|
||||
return course_block
|
||||
|
||||
|
||||
@view_auth_classes(is_authenticated=True)
|
||||
class ProctoringErrorsView(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
View for getting the proctoring errors for a course with url to proctored exam settings.
|
||||
"""
|
||||
@apidocs.schema(
|
||||
parameters=[
|
||||
apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"),
|
||||
],
|
||||
responses={
|
||||
200: ProctoringErrorsSerializer,
|
||||
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) -> Response:
|
||||
"""
|
||||
Get an object containing proctoring errors in a course.
|
||||
|
||||
**Example Request**
|
||||
|
||||
GET /api/contentstore/v1/proctoring_errors/{course_id}
|
||||
|
||||
**Response Values**
|
||||
|
||||
If the request is successful, an HTTP 200 "OK" response is returned.
|
||||
|
||||
The HTTP 200 response contains a list of object proctoring errors.
|
||||
Also response contains mfe proctored exam settings url.
|
||||
For each item returned an object that contains the following fields:
|
||||
|
||||
* **key**: This is proctoring settings key.
|
||||
* **message**: This is a description for proctoring error.
|
||||
* **model**: This is proctoring provider model object.
|
||||
|
||||
**Example Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"mfe_proctored_exam_settings_url": "http://course-authoring-mfe/course/course_key/proctored-exam-settings",
|
||||
"proctoring_errors": [
|
||||
{
|
||||
"key": "proctoring_provider",
|
||||
"message": "The proctoring provider cannot be modified after a course has started.",
|
||||
"model": {
|
||||
"value": "null",
|
||||
"display_name": "Proctoring Provider",
|
||||
"help": "Enter the proctoring provider you want to use for this course run.",
|
||||
"deprecated": false,
|
||||
"hide_on_enabled_publisher": false
|
||||
}}
|
||||
],
|
||||
}
|
||||
```
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
if not has_studio_advanced_settings_access(request.user):
|
||||
self.permission_denied(request)
|
||||
|
||||
course_block = modulestore().get_course(course_key)
|
||||
advanced_dict = CourseMetadata.fetch(course_block)
|
||||
if settings.FEATURES.get('DISABLE_MOBILE_COURSE_AVAILABLE', False):
|
||||
advanced_dict.get('mobile_available')['deprecated'] = True
|
||||
|
||||
proctoring_errors = CourseMetadata.validate_proctoring_settings(course_block, advanced_dict, request.user)
|
||||
proctoring_context = {
|
||||
'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course_key),
|
||||
'proctoring_errors': proctoring_errors,
|
||||
}
|
||||
|
||||
serializer = ProctoringErrorsSerializer(data=proctoring_context)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
Reference in New Issue
Block a user