-
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: send proctoring update emails (#201)
Send an email when a proctored exam is submitted/verified/rejected
- Loading branch information
1 parent
9b87fb7
commit 793794d
Showing
19 changed files
with
326 additions
and
135 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 |
---|---|---|
@@ -0,0 +1,61 @@ | ||
""" | ||
Handles rendering templates and sending emails. | ||
""" | ||
import logging | ||
|
||
from django.conf import settings | ||
from django.core.mail.message import EmailMessage | ||
from django.template import loader | ||
|
||
from edx_exams.apps.core.exam_types import get_exam_type | ||
from edx_exams.apps.core.statuses import ExamAttemptStatus | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
def send_attempt_status_email(attempt): | ||
""" | ||
Send email for attempt status if necessary | ||
""" | ||
exam = attempt.exam | ||
exam_type = get_exam_type(exam.exam_type) | ||
|
||
# do not send emails on practice exams or non-proctored exams | ||
if not exam_type.is_proctored or exam_type.is_practice: | ||
return | ||
|
||
if attempt.status == ExamAttemptStatus.submitted: | ||
email_template = 'email/proctoring_attempt_submitted.html' | ||
elif attempt.status == ExamAttemptStatus.verified: | ||
email_template = 'email/proctoring_attempt_verified.html' | ||
elif attempt.status == ExamAttemptStatus.rejected: | ||
email_template = 'email/proctoring_attempt_rejected.html' | ||
else: | ||
return # do not send emails for other statuses | ||
|
||
email_template = loader.get_template(email_template) | ||
course_url = f'{settings.LEARNING_MICROFRONTEND_URL}/course/{exam.course_id}' | ||
contact_url = f'{settings.LMS_ROOT_URL}/support/contact_us' | ||
|
||
email_subject = f'Proctored exam {exam.exam_name} for user {attempt.user.username}' | ||
body = email_template.render({ | ||
'exam_name': exam.exam_name, | ||
'course_url': course_url, | ||
'contact_url': contact_url, | ||
}) | ||
|
||
email = EmailMessage( | ||
subject=email_subject, | ||
body=body, | ||
from_email=settings.DEFAULT_FROM_EMAIL, | ||
to=[attempt.user.email], | ||
) | ||
email.content_subtype = 'html' | ||
|
||
try: | ||
email.send() | ||
except Exception as err: # pylint: disable=broad-except | ||
log.error( | ||
'Error while sending proctoring status email for ' | ||
f'user_id {attempt.user.id}, exam_id {exam.id}: {err}' | ||
) |
33 changes: 33 additions & 0 deletions
33
edx_exams/apps/core/templates/email/proctoring_attempt_rejected.html
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 |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{% load i18n %} | ||
|
||
<p> | ||
{% block introduction %} | ||
{% blocktrans %} | ||
Hello {{ username }}, | ||
{% endblocktrans %} | ||
{% endblock %} | ||
</p> | ||
<p> | ||
{% block status_information %} | ||
{% blocktrans %} | ||
Your proctored exam "{{ exam_name }}" in | ||
<a href="{{ course_url }}">{{ course_url }}</a> was reviewed and the | ||
course team has identified one or more violations of the proctored exam rules. Examples | ||
of issues that may result in a rules violation include browsing | ||
the internet, using a phone, | ||
or getting help from another person. As a result of the identified issue(s), | ||
you did not successfully meet the proctored exam requirements. | ||
{% endblocktrans %} | ||
{% endblock %} | ||
</p> | ||
<p> | ||
{% block contact_information %} | ||
{% blocktrans %} | ||
To appeal your proctored exam results, please reach out with any relevant information | ||
about your exam at | ||
<a href="{{contact_url}}"> | ||
{{ contact_url_text }} | ||
</a>. | ||
{% endblocktrans %} | ||
{% endblock %} | ||
</p> |
20 changes: 20 additions & 0 deletions
20
edx_exams/apps/core/templates/email/proctoring_attempt_submitted.html
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 |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{% load i18n %} | ||
|
||
<p> | ||
{% block introduction %} | ||
{% blocktrans %} | ||
Hello {{ username }}, | ||
{% endblocktrans %} | ||
{% endblock %} | ||
</p> | ||
<p> | ||
{% block status_information %} | ||
{% blocktrans %} | ||
Your proctored exam "{{ exam_name }}" in | ||
<a href="{{ course_url }}">{{ course_url }}</a> was submitted | ||
successfully and will now be reviewed to ensure all exam | ||
rules were followed. You should receive an email with your exam | ||
status within 5 business days. | ||
{% endblocktrans %} | ||
{% endblock %} | ||
</p> |
28 changes: 28 additions & 0 deletions
28
edx_exams/apps/core/templates/email/proctoring_attempt_verified.html
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 |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{% load i18n %} | ||
|
||
<p> | ||
{% block introduction %} | ||
{% blocktrans %} | ||
Hello {{ username }}, | ||
{% endblocktrans %} | ||
{% endblock %} | ||
</p> | ||
<p> | ||
{% block status_information %} | ||
{% blocktrans %} | ||
Your proctored exam "{{ exam_name }}" in | ||
<a href="{{ course_url }}">{{ course_url }}</a> was reviewed and you | ||
met all proctoring requirements. | ||
{% endblocktrans %} | ||
{% endblock %} | ||
</p> | ||
<p> | ||
{% block contact_information %} | ||
{% blocktrans %} | ||
If you have any questions about your results, you can reach out at | ||
<a href="{{contact_url}}"> | ||
{{ contact_url }} | ||
</a>. | ||
{% endblocktrans %} | ||
{% endblock %} | ||
</p> |
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 |
---|---|---|
@@ -0,0 +1,105 @@ | ||
""" | ||
Test email notifications for attempt status change | ||
""" | ||
|
||
import ddt | ||
import mock | ||
from django.core import mail | ||
from django.test import TestCase | ||
|
||
from edx_exams.apps.core.api import update_attempt_status | ||
from edx_exams.apps.core.test_utils.factories import ExamAttemptFactory, ExamFactory, UserFactory | ||
|
||
|
||
@ddt.ddt | ||
class TestEmail(TestCase): | ||
""" | ||
Test email notifications for attempt status change | ||
""" | ||
def setUp(self): | ||
super().setUp() | ||
|
||
self.user = UserFactory.create() | ||
self.proctored_exam = ExamFactory.create( | ||
exam_type='proctored', | ||
) | ||
self.started_attempt = ExamAttemptFactory.create( | ||
exam=self.proctored_exam, | ||
user=self.user, | ||
status='started', | ||
) | ||
|
||
@staticmethod | ||
def _normalize_whitespace(string): | ||
""" | ||
Replaces newlines and multiple spaces with a single space. | ||
""" | ||
return ' '.join(string.replace('\n', '').split()) | ||
|
||
@ddt.data( | ||
('submitted', 'was submitted successfully'), | ||
('verified', 'was reviewed and you met all proctoring requirements'), | ||
('rejected', 'the course team has identified one or more violations'), | ||
) | ||
@ddt.unpack | ||
def test_send_email(self, status, expected_message): | ||
""" | ||
Test correct message is sent for statuses that trigger an email | ||
""" | ||
update_attempt_status(self.started_attempt.id, status) | ||
self.assertEqual(len(mail.outbox), 1) | ||
self.assertIn(self.started_attempt.user.email, mail.outbox[0].to) | ||
self.assertIn(expected_message, self._normalize_whitespace(mail.outbox[0].body)) | ||
|
||
@mock.patch('edx_exams.apps.core.email.log.error') | ||
def test_send_email_failure(self, mock_log_error): | ||
""" | ||
Test error is logged when an email fails to send | ||
""" | ||
with mock.patch('edx_exams.apps.core.email.EmailMessage.send', side_effect=Exception): | ||
update_attempt_status(self.started_attempt.id, 'submitted') | ||
mock_log_error.assert_called_once() | ||
self.assertIn('Error while sending proctoring status email', mock_log_error.call_args[0][0]) | ||
|
||
@ddt.data( | ||
'created', | ||
'ready_to_start', | ||
'download_software_clicked', | ||
'started', | ||
'ready_to_submit', | ||
'error', | ||
) | ||
def test_status_should_not_send_email(self, status): | ||
""" | ||
Test no email is sent for statuses that should not trigger | ||
""" | ||
update_attempt_status(self.started_attempt.id, status) | ||
self.assertEqual(len(mail.outbox), 0) | ||
|
||
def test_non_proctored_exam_should_not_send_email(self): | ||
""" | ||
Test no email is sent for non-proctored exams | ||
""" | ||
timed_attempt = ExamAttemptFactory.create( | ||
exam=ExamFactory.create( | ||
exam_type='timed', | ||
), | ||
user=self.user, | ||
status='started', | ||
) | ||
update_attempt_status(timed_attempt.id, 'submitted') | ||
self.assertEqual(len(mail.outbox), 0) | ||
|
||
def test_practice_exam_should_not_send_email(self): | ||
""" | ||
Test no email is sent for practice exams | ||
""" | ||
practice_proctored_attempt = ExamAttemptFactory.create( | ||
exam=ExamFactory.create( | ||
exam_type='onboarding', | ||
), | ||
user=self.user, | ||
status='started', | ||
) | ||
update_attempt_status(practice_proctored_attempt.id, 'submitted') | ||
self.assertEqual(len(mail.outbox), 0) |
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 |
---|---|---|
|
@@ -157,6 +157,9 @@ def root(*path_fragments): | |
root('static'), | ||
) | ||
|
||
# EMAIL CONFIGURATION | ||
DEFAULT_FROM_EMAIL: '[email protected]' | ||
|
||
# TEMPLATE CONFIGURATION | ||
# See: https://docs.djangoproject.com/en/2.2/ref/settings/#templates | ||
TEMPLATES = [ | ||
|
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
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 |
---|---|---|
@@ -1,4 +1,5 @@ | ||
|
||
|
||
# A central location for most common version constraints | ||
# (across edx repos) for pip-installation. | ||
# | ||
|
Oops, something went wrong.