-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: send proctoring update emails #201
Changes from 2 commits
49c8976
4d25319
70ad8e3
7eda67c
a911aa6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
""" | ||
Handles sending emails to users about proctoring status changes. | ||
""" | ||
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 = exam.provider.tech_support_email | ||
|
||
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}' | ||
) |
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 }} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this email the correct place to escalate to? It's set up per provider, and I feel like it would make more sense for the course team to review this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a really good point. In my head this was the escalation email but that's not true, this email is just tech support for the tool. We don't save an escalation email for LTI but it sounds like we need one. I'm going to open a ticket to discuss adding this end to end. For now I'm going to update this so this falls back on edx support in absence of a course team email. |
||
</a>. | ||
{% endblocktrans %} | ||
{% endblock %} | ||
</p> |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These used to have the course name here. For right now we don't have that in the data model. |
||
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> |
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> |
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) |
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 = [ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
# Packages required in a production environment | ||
-r base.txt | ||
|
||
django-ses | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll compile the new reqs before merging. |
||
gevent | ||
gunicorn | ||
mysqlclient | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#NIT We are going to put all email sending functions into this file, correct? Then I would not be so specific on this comment. This comment is more for the function below, right?