Skip to content

Commit

Permalink
feat: add escalation email to ProctoringSettingsView
Browse files Browse the repository at this point in the history
This commit renames the CourseProviderSettingsView view to ProctoringSettingsView to accomodate additional settings beyond just provider settings; this more closely mirrors the purpose of the analogous edx-proctoring view. This commit also adds the escalation email to the response under the proctoring_escalation_email key. This changes allows the escalation email to appear in the error interstitial in the learning MFE.
  • Loading branch information
MichaelRoytman committed Dec 12, 2023
1 parent ceceedd commit 8ea7452
Show file tree
Hide file tree
Showing 15 changed files with 77 additions and 84 deletions.
35 changes: 10 additions & 25 deletions edx_exams/apps/api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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}
)

Expand All @@ -1731,21 +1715,22 @@ 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,
{
'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,
}
)
8 changes: 4 additions & 4 deletions edx_exams/apps/api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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'
),
]
31 changes: 19 additions & 12 deletions edx_exams/apps/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: '[email protected]',
}
"""

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)
24 changes: 24 additions & 0 deletions edx_exams/apps/core/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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

Check warning on line 493 in edx_exams/apps/core/api.py

View check run for this annotation

Codecov / codecov/patch

edx_exams/apps/core/api.py#L492-L493

Added lines #L492 - L493 were not covered by tests
12 changes: 12 additions & 0 deletions edx_exams/apps/core/data.py
Original file line number Diff line number Diff line change
@@ -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))
10 changes: 3 additions & 7 deletions edx_exams/apps/router/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}


Expand Down
2 changes: 1 addition & 1 deletion edx_exams/apps/router/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
)

Expand Down
2 changes: 1 addition & 1 deletion edx_exams/apps/router/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand Down
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Core requirements for using this application
-c constraints.txt

attrs
Django # Web application framework
django-cors-headers
django-extensions
Expand Down
1 change: 1 addition & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions requirements/common_constraints.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@


# A central location for most common version constraints
# (across edx repos) for pip-installation.
#
Expand Down
10 changes: 0 additions & 10 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 0 additions & 7 deletions requirements/doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 0 additions & 7 deletions requirements/quality.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 0 additions & 10 deletions requirements/validation.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 8ea7452

Please sign in to comment.