-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add escalation_email to course exam configuration
This commit adds an escalation email to course exam configurations. This value is used to specify who learners can contact in the event of issues with or questions about their exam attempts.
- Loading branch information
1 parent
a610260
commit 2bc4859
Showing
13 changed files
with
509 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -358,23 +358,46 @@ def test_course_staff_write_access(self): | |
CourseStaffRole.objects.create(user=course_staff_user, course_id=self.course_id) | ||
response = self.patch_api(course_staff_user, { | ||
'provider': None, | ||
'escalation_email': None, | ||
}) | ||
self.assertEqual(204, response.status_code) | ||
|
||
def test_patch_invalid_data(self): | ||
@ddt.data( | ||
{}, | ||
{'escalation_email': '[email protected]'}, | ||
{'provider': 'test-provider'} | ||
) | ||
def test_patch_invalid_data_missing_data(self, data): | ||
""" | ||
Assert that endpoint returns 400 if any required parameter is missing | ||
""" | ||
response = self.patch_api(self.user, data) | ||
self.assertEqual(400, response.status_code) | ||
|
||
def test_patch_invalid_data_escalation_email(self): | ||
""" | ||
Assert that endpoint returns 400 if provider is missing | ||
Assert that endpoint returns 400 if provider is None but escalation_email is not | ||
""" | ||
data = {} | ||
data = {'provider': None, 'escalation_email': '[email protected]'} | ||
|
||
response = self.patch_api(self.user, data) | ||
self.assertEqual(400, response.status_code) | ||
|
||
def test_patch_invalid_provider(self): | ||
def test_patch_invalid_data_invalid_escalation_email(self): | ||
""" | ||
Assert that endpoint returns 400 if the escalation email is not a valid email. | ||
""" | ||
data = {'provider': 'test_provider', 'escalation_email': 'test'} | ||
|
||
response = self.patch_api(self.user, data) | ||
self.assertEqual(400, response.status_code) | ||
|
||
@ddt.data('nonexistent_provider', '') | ||
def test_patch_invalid_provider(self, provider_name): | ||
""" | ||
Assert endpoint returns 400 if provider is invalid | ||
""" | ||
data = {'provider': 'nonexistent_provider'} | ||
data = {'provider': provider_name} | ||
|
||
response = self.patch_api(self.user, data) | ||
self.assertEqual(400, response.status_code) | ||
|
@@ -392,14 +415,17 @@ def test_patch_config_update(self): | |
verbose_name='testing_provider_2', | ||
lti_configuration_id='223456789' | ||
) | ||
data = {'provider': provider.name} | ||
escalation_email = '[email protected]' | ||
|
||
data = {'provider': provider.name, 'escalation_email': escalation_email} | ||
|
||
response = self.patch_api(self.user, data) | ||
self.assertEqual(204, response.status_code) | ||
self.assertEqual(len(CourseExamConfiguration.objects.all()), 1) | ||
|
||
config = CourseExamConfiguration.get_configuration_for_course(self.course_id) | ||
self.assertEqual(config.provider, provider) | ||
self.assertEqual(config.escalation_email, escalation_email) | ||
|
||
def test_patch_config_update_exams(self): | ||
""" | ||
|
@@ -441,12 +467,15 @@ def test_patch_config_update_exams(self): | |
exams = Exam.objects.filter(course_id=self.course_id, is_active=True) | ||
self.assertEqual(2, len(exams)) | ||
|
||
data = {'provider': provider.name} | ||
escalation_email = '[email protected]' | ||
|
||
data = {'provider': provider.name, 'escalation_email': escalation_email} | ||
response = self.patch_api(self.user, data) | ||
self.assertEqual(204, response.status_code) | ||
self.assertEqual(len(CourseExamConfiguration.objects.all()), 1) | ||
config = CourseExamConfiguration.get_configuration_for_course(self.course_id) | ||
self.assertEqual(config.provider, provider) | ||
self.assertEqual(config.escalation_email, escalation_email) | ||
|
||
exams = Exam.objects.filter(course_id=self.course_id, is_active=True) | ||
self.assertEqual(2, len(exams)) | ||
|
@@ -459,12 +488,13 @@ def test_patch_config_update_exams(self): | |
self.assertEqual(self.test_provider, exam.provider) | ||
|
||
# updating to the same provider is a do nothing, no new exams | ||
data = {'provider': provider.name} | ||
data = {'provider': provider.name, 'escalation_email': escalation_email} | ||
response = self.patch_api(self.user, data) | ||
self.assertEqual(204, response.status_code) | ||
self.assertEqual(len(CourseExamConfiguration.objects.all()), 1) | ||
config = CourseExamConfiguration.get_configuration_for_course(self.course_id) | ||
self.assertEqual(config.provider, provider) | ||
self.assertEqual(config.escalation_email, escalation_email) | ||
|
||
exams = Exam.objects.filter(course_id=self.course_id, is_active=True) | ||
self.assertEqual(2, len(exams)) | ||
|
@@ -477,12 +507,13 @@ def test_patch_config_update_exams(self): | |
self.assertEqual(self.test_provider, exam.provider) | ||
|
||
# updating back to the original provider creates two new active exams, now 4 inactive | ||
data = {'provider': self.test_provider.name} | ||
data = {'provider': self.test_provider.name, 'escalation_email': '[email protected]'} | ||
response = self.patch_api(self.user, data) | ||
self.assertEqual(204, response.status_code) | ||
self.assertEqual(len(CourseExamConfiguration.objects.all()), 1) | ||
config = CourseExamConfiguration.get_configuration_for_course(self.course_id) | ||
self.assertEqual(config.provider, self.test_provider) | ||
self.assertEqual(config.escalation_email, escalation_email) | ||
|
||
exams = Exam.objects.filter(course_id=self.course_id, is_active=True) | ||
self.assertEqual(2, len(exams)) | ||
|
@@ -496,27 +527,30 @@ def test_patch_config_create(self): | |
""" | ||
Test that config is created | ||
""" | ||
data = {'provider': 'test_provider'} | ||
escalation_email = '[email protected]' | ||
data = {'provider': 'test_provider', 'escalation_email': escalation_email} | ||
|
||
response = self.patch_api(self.user, data) | ||
self.assertEqual(204, response.status_code) | ||
self.assertEqual(len(CourseExamConfiguration.objects.all()), 1) | ||
|
||
config = CourseExamConfiguration.get_configuration_for_course(self.course_id) | ||
self.assertEqual(config.provider, self.test_provider) | ||
self.assertEqual(config.escalation_email, escalation_email) | ||
|
||
def test_patch_null_provider(self): | ||
""" | ||
Assert provider can be explicitly set to null | ||
""" | ||
data = {'provider': None} | ||
data = {'provider': None, 'escalation_email': None} | ||
|
||
response = self.patch_api(self.user, data) | ||
self.assertEqual(204, response.status_code) | ||
self.assertEqual(len(CourseExamConfiguration.objects.all()), 1) | ||
|
||
config = CourseExamConfiguration.get_configuration_for_course(self.course_id) | ||
self.assertEqual(config.provider, None) | ||
self.assertEqual(config.escalation_email, None) | ||
|
||
def test_get_config(self): | ||
""" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,8 @@ | |
|
||
from edx_exams.apps.api.permissions import CourseStaffOrReadOnlyPermissions, CourseStaffUserPermissions | ||
from edx_exams.apps.api.serializers import ( | ||
CourseExamConfigurationReadSerializer, | ||
CourseExamConfigurationWriteSerializer, | ||
ExamSerializer, | ||
InstructorViewAttemptSerializer, | ||
ProctoringProviderSerializer, | ||
|
@@ -26,6 +28,7 @@ | |
from edx_exams.apps.core.api import ( | ||
check_if_exam_timed_out, | ||
create_exam_attempt, | ||
create_or_update_course_exam_configuration, | ||
get_active_attempt_for_user, | ||
get_attempt_by_id, | ||
get_course_exams, | ||
|
@@ -227,15 +230,19 @@ class CourseExamConfigurationsView(ExamsAPIView): | |
**Returns** | ||
{ | ||
'provider': 'test_provider', | ||
'escalation_email': '[email protected]', | ||
} | ||
HTTP PATCH | ||
Creates or updates a CourseExamConfiguration. | ||
Expected PATCH data: { | ||
'provider': 'test_provider', | ||
'escalation_email': '[email protected]', | ||
} | ||
**PATCH data Parameters** | ||
* name: This is the name of the proctoring provider. | ||
* provider: This is the name of the selected proctoring provider for the course. | ||
* escalation_email: This is the email to which learners should send emails to escalate problems for the course. | ||
**Exceptions** | ||
* HTTP_400_BAD_REQUEST | ||
""" | ||
|
@@ -246,46 +253,32 @@ class CourseExamConfigurationsView(ExamsAPIView): | |
def get(self, request, course_id): | ||
""" | ||
Get exam configuration for a course | ||
TODO: This view should use a serializer to ensure the read/write bodies are the same | ||
once more fields are added. | ||
""" | ||
try: | ||
provider = CourseExamConfiguration.objects.get(course_id=course_id).provider | ||
except ObjectDoesNotExist: | ||
provider = None | ||
configuration = CourseExamConfiguration.objects.get(course_id=course_id) | ||
except CourseExamConfiguration.DoesNotExist: | ||
# If configuration is set to None, then the provider is serialized to the empty string instead of None. | ||
configuration = {} | ||
|
||
return Response({ | ||
'provider': provider.name if provider else None | ||
}) | ||
serializer = CourseExamConfigurationReadSerializer(configuration) | ||
return Response(serializer.data) | ||
|
||
def patch(self, request, course_id): | ||
""" | ||
Create/update course exam configuration. | ||
""" | ||
error = None | ||
serializer = CourseExamConfigurationWriteSerializer(data=request.data) | ||
|
||
# check that proctoring provider is in request | ||
if 'provider' not in request.data: | ||
error = {'detail': 'No proctoring provider name in request.'} | ||
elif request.data.get('provider') is None: | ||
provider = None | ||
else: | ||
try: | ||
provider = ProctoringProvider.objects.get(name=request.data['provider']) | ||
# return 400 if proctoring provider does not exist | ||
except ObjectDoesNotExist: | ||
error = {'detail': 'Proctoring provider does not exist.'} | ||
|
||
if not error: | ||
CourseExamConfiguration.create_or_update(provider, course_id) | ||
response_status = status.HTTP_204_NO_CONTENT | ||
data = {} | ||
if serializer.is_valid(): | ||
validated_data = serializer.validated_data | ||
create_or_update_course_exam_configuration( | ||
course_id, | ||
validated_data['provider'], | ||
validated_data['escalation_email'], | ||
) | ||
return Response({}, status=status.HTTP_204_NO_CONTENT) | ||
else: | ||
response_status = status.HTTP_400_BAD_REQUEST | ||
data = error | ||
|
||
return Response(status=response_status, data=data) | ||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) | ||
|
||
|
||
class ProctoringProvidersView(ListAPIView): | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.