feat: Extending API functionality for proctoring errors (#32331)

This commit is contained in:
ruzniaievdm
2023-05-31 21:00:09 +03:00
committed by GitHub
parent 0ebf846670
commit a6ce487827
6 changed files with 195 additions and 4 deletions

View File

@@ -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

View File

@@ -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'],
))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"
),
]

View File

@@ -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)