diff --git a/edx_exams/apps/api/v1/tests/test_views.py b/edx_exams/apps/api/v1/tests/test_views.py index ba2db127..7fd5ae8c 100644 --- a/edx_exams/apps/api/v1/tests/test_views.py +++ b/edx_exams/apps/api/v1/tests/test_views.py @@ -1684,34 +1684,18 @@ def test_timed_exam(self): self.assertIsNone(response_exam['backend']) -class CourseProviderSettingsViewTest(ExamsAPITestCase): +class ProctoringSettingsViewTest(ExamsAPITestCase): """ - Tests CourseProviderSettings View + Tests ProctoringSettingsView """ def setUp(self): super().setUp() - self.course_id = 'course-v1:edx+test+f19' - self.content_id = 'block-v1:edX+test+2023+type@sequential+block@1111111111' + self.config = CourseExamConfigurationFactory() - self.course_exam_config = CourseExamConfiguration.objects.create( - course_id=self.course_id, - provider=self.test_provider, - allow_opt_out=False - ) - - self.exam = Exam.objects.create( - resource_id=str(uuid.uuid4()), - course_id=self.course_id, - provider=self.test_provider, - content_id=self.content_id, - exam_name='test_exam', - exam_type='proctored', - time_limit_mins=30, - due_date='2040-07-01T00:00:00Z', - hide_after_due=False, - is_active=True + self.exam = ExamFactory( + provider=self.test_provider ) def get_api(self, user, course_id, exam_id): @@ -1721,7 +1705,7 @@ def get_api(self, user, course_id, exam_id): headers = self.build_jwt_headers(user) url = reverse( - 'api:v1:exam-provider-settings', + 'api:v1:proctoring-settings', kwargs={'course_id': course_id, 'exam_id': exam_id} ) @@ -1731,15 +1715,15 @@ def test_no_exam(self): """ Test that the endpoint returns for an invalid exam ID """ - response = self.get_api(self.user, self.course_id, 12345) + response = self.get_api(self.user, self.exam.course_id, 12345) self.assertEqual(response.status_code, 200) - self.assertEqual(response.data, {}) + self.assertEqual(response.data, {'proctoring_escalation_email': self.config.escalation_email}) def test_exam_provider(self): """ Test that the exam provider info is returned """ - response = self.get_api(self.user, self.course_id, self.exam.id) + response = self.get_api(self.user, self.exam.course_id, self.exam.id) self.assertEqual(response.status_code, 200) self.assertEqual( response.data, @@ -1747,5 +1731,6 @@ def test_exam_provider(self): 'provider_tech_support_email': self.test_provider.tech_support_email, 'provider_tech_support_phone': self.test_provider.tech_support_phone, 'provider_name': self.test_provider.verbose_name, + 'proctoring_escalation_email': self.config.escalation_email, } ) diff --git a/edx_exams/apps/api/v1/urls.py b/edx_exams/apps/api/v1/urls.py index c783dc89..a6a80ec8 100644 --- a/edx_exams/apps/api/v1/urls.py +++ b/edx_exams/apps/api/v1/urls.py @@ -6,12 +6,12 @@ CourseExamAttemptView, CourseExamConfigurationsView, CourseExamsView, - CourseProviderSettingsView, ExamAccessTokensView, ExamAttemptView, InstructorAttemptsListView, LatestExamAttemptView, - ProctoringProvidersView + ProctoringProvidersView, + ProctoringSettingsView ) from edx_exams.apps.core.constants import CONTENT_ID_PATTERN, COURSE_ID_PATTERN, EXAM_ID_PATTERN @@ -65,7 +65,7 @@ ), re_path( fr'exam/provider_settings/course_id/{COURSE_ID_PATTERN}/exam_id/{EXAM_ID_PATTERN}', - CourseProviderSettingsView.as_view(), - name='exam-provider-settings' + ProctoringSettingsView.as_view(), + name='proctoring-settings' ), ] diff --git a/edx_exams/apps/api/v1/views.py b/edx_exams/apps/api/v1/views.py index 7ef35561..1c018c60 100644 --- a/edx_exams/apps/api/v1/views.py +++ b/edx_exams/apps/api/v1/views.py @@ -31,6 +31,7 @@ create_or_update_course_exam_configuration, get_active_attempt_for_user, get_attempt_by_id, + get_course_exam_configuration_by_course_id, get_course_exams, get_current_exam_attempt, get_exam_attempt_time_remaining, @@ -729,38 +730,44 @@ def get(self, request, course_id, content_id): # pylint: disable=unused-argumen return Response(data) -class CourseProviderSettingsView(ExamsAPIView): +class ProctoringSettingsView(ExamsAPIView): """ - Endpoint for getting provider related settings. + Endpoint for getting an exam's proctoring settings, including course-wide settings like the escalation email and + exam-specific settings like provider settings. exam/provider_settings/course_id/{course_id}/exam_id/{exam_id} Supports: HTTP GET: - Returns provider specific information given an exam_id + Returns exam configuration settings given an exam_id { provider_tech_support_email: '', provider_tech_support_phone: '', provider_name: 'test provider', + escalation_name: 'test@example.com', } """ authentication_classes = (JwtAuthentication,) permission_classes = (IsAuthenticated,) - def get(self, request, course_id, exam_id): # pylint: disable=unused-argument + def get(self, request, course_id, exam_id): """ - HTTP GET handler. Returns provider specific information given an exam_id + HTTP GET handler. Returns exam configuration settings given an exam_id - The course_id is provided as a path parameter to account for the exams middleware + The course_id is provided as a path parameter to account for the exams ExamRequestMiddleware router. """ provider = get_provider_by_exam_id(exam_id) + config_data = get_course_exam_configuration_by_course_id(course_id) + + data = {} if provider: - return Response({ - 'provider_tech_support_email': provider.tech_support_email, - 'provider_tech_support_phone': provider.tech_support_phone, - 'provider_name': provider.verbose_name, - }) - return Response({}) + data['provider_tech_support_email'] = provider.tech_support_email + data['provider_tech_support_phone'] = provider.tech_support_phone + data['provider_name'] = provider.verbose_name + + data['proctoring_escalation_email'] = config_data.escalation_email + + return Response(data) diff --git a/edx_exams/apps/core/api.py b/edx_exams/apps/core/api.py index 02730cf0..5c659514 100644 --- a/edx_exams/apps/core/api.py +++ b/edx_exams/apps/core/api.py @@ -9,6 +9,7 @@ from django.utils import dateparse, timezone from opaque_keys.edx.keys import CourseKey, UsageKey +from edx_exams.apps.core.data import CourseExamConfigurationData from edx_exams.apps.core.email import send_attempt_status_email from edx_exams.apps.core.exam_types import OnboardingExamType, PracticeExamType, get_exam_type from edx_exams.apps.core.exceptions import ( @@ -467,3 +468,26 @@ def create_or_update_course_exam_configuration(course_id, provider_name, escalat provider = None CourseExamConfiguration.create_or_update(course_id, provider, escalation_email) + + +def get_course_exam_configuration_by_course_id(course_id): + """ + Return the CourseExamConfiguration associated with the course ID. + + Parameters: + * course_id: the ID representing the course + + Returns: + * an instance of the CourseExamConfigurationData class, if the associated CourseExamConfiguration object exists; + else, None. + """ + try: + config = CourseExamConfiguration.objects.select_related('provider').get(course_id=course_id) + return CourseExamConfigurationData( + course_id=config.course_id, + provider=config.provider.name, + allow_opt_out=config.allow_opt_out, + escalation_email=config.escalation_email, + ) + except CourseExamConfiguration.DoesNotExist: + return None diff --git a/edx_exams/apps/core/data.py b/edx_exams/apps/core/data.py new file mode 100644 index 00000000..4454a78c --- /dev/null +++ b/edx_exams/apps/core/data.py @@ -0,0 +1,12 @@ +""" +Public data structures for the core applictaion. +""" +from attrs import field, frozen, validators + + +@frozen +class CourseExamConfigurationData: + course_id: str = field(validator=validators.instance_of(str)) + provider: str = field(validator=validators.instance_of(str)) + allow_opt_out: field(validator=validators.instance_of(str)) + escalation_email: str = field(validator=validators.instance_of(str)) diff --git a/edx_exams/apps/router/middleware.py b/edx_exams/apps/router/middleware.py index 1a83ed47..41a5a2cb 100644 --- a/edx_exams/apps/router/middleware.py +++ b/edx_exams/apps/router/middleware.py @@ -6,20 +6,16 @@ from django.utils.deprecation import MiddlewareMixin -from edx_exams.apps.api.v1.views import CourseExamAttemptView, CourseExamsView, CourseProviderSettingsView +from edx_exams.apps.api.v1.views import CourseExamAttemptView, CourseExamsView, ProctoringSettingsView from edx_exams.apps.core.models import CourseExamConfiguration -from edx_exams.apps.router.views import ( - CourseExamAttemptLegacyView, - CourseExamsLegacyView, - CourseProviderSettingsLegacyView -) +from edx_exams.apps.router.views import CourseExamAttemptLegacyView, CourseExamsLegacyView, ProctoringSettingsLegacyView log = logging.getLogger(__name__) LEGACY_VIEW_MAP = { CourseExamsView: CourseExamsLegacyView, CourseExamAttemptView: CourseExamAttemptLegacyView, - CourseProviderSettingsView: CourseProviderSettingsLegacyView, + ProctoringSettingsView: ProctoringSettingsLegacyView, } diff --git a/edx_exams/apps/router/tests/test_views.py b/edx_exams/apps/router/tests/test_views.py index e8b04be2..0e0819e6 100644 --- a/edx_exams/apps/router/tests/test_views.py +++ b/edx_exams/apps/router/tests/test_views.py @@ -284,7 +284,7 @@ def setUp(self): self.course_id = 'course-v1:edx+test+f19' self.exam_id = 2222 self.url = reverse( - 'api:v1:exam-provider-settings', + 'api:v1:proctoring-settings', kwargs={'course_id': self.course_id, 'exam_id': self.exam_id} ) diff --git a/edx_exams/apps/router/views.py b/edx_exams/apps/router/views.py index a6a111f7..c8248ff5 100644 --- a/edx_exams/apps/router/views.py +++ b/edx_exams/apps/router/views.py @@ -85,7 +85,7 @@ def get(self, request, course_id, content_id): ) -class CourseProviderSettingsLegacyView(APIView): +class ProctoringSettingsLegacyView(APIView): """ View to handle provider settings for exams managed by edx-proctoring """ diff --git a/requirements/base.in b/requirements/base.in index 14fb7445..06cf9408 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -1,6 +1,7 @@ # Core requirements for using this application -c constraints.txt +attrs Django # Web application framework django-cors-headers django-extensions diff --git a/requirements/base.txt b/requirements/base.txt index 843bd734..edef415d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -14,6 +14,7 @@ async-timeout==4.0.3 # via redis attrs==23.1.0 # via + # -r requirements/base.in # lti-consumer-xblock # openedx-events bleach==6.1.0 diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt index 38aa6a63..68e500b9 100644 --- a/requirements/common_constraints.txt +++ b/requirements/common_constraints.txt @@ -1,4 +1,5 @@ + # A central location for most common version constraints # (across edx repos) for pip-installation. # diff --git a/requirements/dev.txt b/requirements/dev.txt index 1de9bd06..c09d66e9 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -106,7 +106,6 @@ cryptography==41.0.7 # via # -r requirements/validation.txt # pyjwt - # secretstorage # social-auth-core ddt==1.7.0 # via -r requirements/validation.txt @@ -335,11 +334,6 @@ jaraco-classes==3.3.0 # via # -r requirements/validation.txt # keyring -jeepney==0.8.0 - # via - # -r requirements/validation.txt - # keyring - # secretstorage jinja2==3.1.2 # via # -r requirements/validation.txt @@ -626,10 +620,6 @@ s3transfer==0.8.0 # via # -r requirements/validation.txt # boto3 -secretstorage==3.3.3 - # via - # -r requirements/validation.txt - # keyring semantic-version==2.10.0 # via # -r requirements/validation.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index 454c2759..ec1a4d0c 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -105,7 +105,6 @@ cryptography==41.0.7 # via # -r requirements/test.txt # pyjwt - # secretstorage # social-auth-core ddt==1.7.0 # via -r requirements/test.txt @@ -330,10 +329,6 @@ itypes==1.2.0 # coreapi jaraco-classes==3.3.0 # via keyring -jeepney==0.8.0 - # via - # keyring - # secretstorage jinja2==3.1.2 # via # -r requirements/test.txt @@ -590,8 +585,6 @@ s3transfer==0.8.0 # via # -r requirements/test.txt # boto3 -secretstorage==3.3.3 - # via keyring semantic-version==2.10.0 # via # -r requirements/test.txt diff --git a/requirements/quality.txt b/requirements/quality.txt index 10f129c2..2fc0500e 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -99,7 +99,6 @@ cryptography==41.0.7 # via # -r requirements/test.txt # pyjwt - # secretstorage # social-auth-core ddt==1.7.0 # via -r requirements/test.txt @@ -315,10 +314,6 @@ itypes==1.2.0 # coreapi jaraco-classes==3.3.0 # via keyring -jeepney==0.8.0 - # via - # keyring - # secretstorage jinja2==3.1.2 # via # -r requirements/test.txt @@ -568,8 +563,6 @@ s3transfer==0.8.0 # via # -r requirements/test.txt # boto3 -secretstorage==3.3.3 - # via keyring semantic-version==2.10.0 # via # -r requirements/test.txt diff --git a/requirements/validation.txt b/requirements/validation.txt index 4cc72bc1..3961979f 100644 --- a/requirements/validation.txt +++ b/requirements/validation.txt @@ -120,7 +120,6 @@ cryptography==41.0.7 # -r requirements/quality.txt # -r requirements/test.txt # pyjwt - # secretstorage # social-auth-core ddt==1.7.0 # via @@ -404,11 +403,6 @@ jaraco-classes==3.3.0 # via # -r requirements/quality.txt # keyring -jeepney==0.8.0 - # via - # -r requirements/quality.txt - # keyring - # secretstorage jinja2==3.1.2 # via # -r requirements/quality.txt @@ -732,10 +726,6 @@ s3transfer==0.8.0 # -r requirements/quality.txt # -r requirements/test.txt # boto3 -secretstorage==3.3.3 - # via - # -r requirements/quality.txt - # keyring semantic-version==2.10.0 # via # -r requirements/quality.txt