-
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 configurations
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
8d9369f
commit fe4505a
Showing
13 changed files
with
491 additions
and
82 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 |
---|---|---|
|
@@ -361,7 +361,7 @@ def test_course_staff_write_access(self): | |
}) | ||
self.assertEqual(204, response.status_code) | ||
|
||
def test_patch_invalid_data(self): | ||
def test_patch_invalid_data_no_provider(self): | ||
""" | ||
Assert that endpoint returns 400 if provider is missing | ||
""" | ||
|
@@ -370,11 +370,21 @@ def test_patch_invalid_data(self): | |
response = self.patch_api(self.user, data) | ||
self.assertEqual(400, response.status_code) | ||
|
||
def test_patch_invalid_provider(self): | ||
def test_patch_invalid_data_no_escalation_email(self): | ||
""" | ||
Assert that endpoint returns 400 if provider is provided but escalation_email is missing | ||
""" | ||
data = {'provider': 'test_provider'} | ||
|
||
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 +402,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 +454,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 +475,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 +494,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,14 +514,16 @@ 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): | ||
""" | ||
|
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,7 @@ | |
|
||
from edx_exams.apps.api.permissions import CourseStaffOrReadOnlyPermissions, CourseStaffUserPermissions | ||
from edx_exams.apps.api.serializers import ( | ||
CourseExamConfigurationSerializer, | ||
ExamSerializer, | ||
InstructorViewAttemptSerializer, | ||
ProctoringProviderSerializer, | ||
|
@@ -26,6 +27,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 +229,20 @@ 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. | ||
This parameter is only required if the provider is not None. | ||
**Exceptions** | ||
* HTTP_400_BAD_REQUEST | ||
""" | ||
|
@@ -246,46 +253,33 @@ 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 = CourseExamConfigurationSerializer(configuration) | ||
return Response(serializer.data) | ||
|
||
def patch(self, request, course_id): | ||
""" | ||
Create/update course exam configuration. | ||
""" | ||
error = None | ||
serializer = CourseExamConfigurationSerializer(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']['name'], | ||
# Escalation email may be optional; use None if it's not provided. | ||
validated_data.get('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
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
Oops, something went wrong.