Commit 58f5a852 by Andy Armstrong Committed by GitHub

Merge pull request #333 from edx/andya/update-email

Improve learner email messages
parents 39c71d9f b9b86b34
...@@ -13,6 +13,7 @@ import pytz ...@@ -13,6 +13,7 @@ import pytz
from django.utils.translation import ugettext as _, ugettext_noop from django.utils.translation import ugettext as _, ugettext_noop
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User
from django.template import Context, loader from django.template import Context, loader
from django.core.urlresolvers import reverse, NoReverseMatch from django.core.urlresolvers import reverse, NoReverseMatch
from django.core.mail.message import EmailMessage from django.core.mail.message import EmailMessage
...@@ -885,38 +886,31 @@ def update_attempt_status(exam_id, user_id, to_status, ...@@ -885,38 +886,31 @@ def update_attempt_status(exam_id, user_id, to_status,
cascade_effects=False cascade_effects=False
) )
# email will be send when the exam is proctored and not practice exam # call service to get course name.
# and the status is verified, submitted or rejected credit_service = get_runtime_service('credit')
should_send_status_email = ( credit_state = credit_service.get_credit_state(
exam_attempt_obj.taking_as_proctored and exam_attempt_obj.user_id,
not exam_attempt_obj.is_sample_attempt and exam_attempt_obj.proctored_exam.course_id,
ProctoredExamStudentAttemptStatus.needs_status_change_email(exam_attempt_obj.status) return_course_info=True
) )
if should_send_status_email:
# trigger credit workflow, as needed
credit_service = get_runtime_service('credit')
# call service to get course name. default_name = _('your course')
credit_state = credit_service.get_credit_state( if credit_state:
course_name = credit_state.get('course_name', default_name)
else:
course_name = default_name
log.info(
"Could not find credit_state for user id %r in the course %r.",
exam_attempt_obj.user_id, exam_attempt_obj.user_id,
exam_attempt_obj.proctored_exam.course_id, exam_attempt_obj.proctored_exam.course_id
return_course_info=True
)
default_name = _('your course')
if credit_state:
course_name = credit_state.get('course_name', default_name)
else:
course_name = default_name
log.info(
"Could not find credit_state for user id %r in the course %r.",
exam_attempt_obj.user_id,
exam_attempt_obj.proctored_exam.course_id
)
send_proctoring_attempt_status_email(
exam_attempt_obj,
course_name
) )
email = create_proctoring_attempt_status_email(
user_id,
exam_attempt_obj,
course_name
)
if email:
email.send()
# emit an anlytics event based on the state transition # emit an anlytics event based on the state transition
# we re-read this from the database in case fields got updated # we re-read this from the database in case fields got updated
...@@ -929,20 +923,46 @@ def update_attempt_status(exam_id, user_id, to_status, ...@@ -929,20 +923,46 @@ def update_attempt_status(exam_id, user_id, to_status,
return attempt['id'] return attempt['id']
def send_proctoring_attempt_status_email(exam_attempt_obj, course_name): def create_proctoring_attempt_status_email(user_id, exam_attempt_obj, course_name):
""" """
Sends an email about change in proctoring attempt status. Creates an email about change in proctoring attempt status.
""" """
# Don't send an email unless this is a non-practice proctored exam
if not exam_attempt_obj.taking_as_proctored or exam_attempt_obj.is_sample_attempt:
return None
user = User.objects.get(id=user_id)
course_info_url = '' course_info_url = ''
email_template = loader.get_template('emails/proctoring_attempt_status_email.html') email_subject = (
_('Proctoring Results For {course_name} {exam_name}').format(
course_name=course_name,
exam_name=exam_attempt_obj.proctored_exam.exam_name
)
)
status = exam_attempt_obj.status
if status == ProctoredExamStudentAttemptStatus.submitted:
email_template_path = 'emails/proctoring_attempt_submitted_email.html'
email_subject = (
_('Proctoring Review In Progress For {course_name} {exam_name}').format(
course_name=course_name,
exam_name=exam_attempt_obj.proctored_exam.exam_name
)
)
elif status == ProctoredExamStudentAttemptStatus.verified:
email_template_path = 'emails/proctoring_attempt_satisfactory_email.html'
elif status == ProctoredExamStudentAttemptStatus.rejected:
email_template_path = 'emails/proctoring_attempt_unsatisfactory_email.html'
else:
# Don't send an email for any other attempt status codes
return None
email_template = loader.get_template(email_template_path)
try: try:
course_info_url = reverse( course_info_url = reverse(
'courseware.views.views.course_info', 'courseware.views.views.course_info',
args=[exam_attempt_obj.proctored_exam.course_id] args=[exam_attempt_obj.proctored_exam.course_id]
) )
except NoReverseMatch: except NoReverseMatch:
log.exception("Can't find Course Info url for course %s", exam_attempt_obj.proctored_exam.course_id) log.exception("Can't find course info url for course %s", exam_attempt_obj.proctored_exam.course_id)
scheme = 'https' if getattr(settings, 'HTTPS', 'on') == 'on' else 'http' scheme = 'https' if getattr(settings, 'HTTPS', 'on') == 'on' else 'http'
course_url = '{scheme}://{site_name}{course_info_url}'.format( course_url = '{scheme}://{site_name}{course_info_url}'.format(
...@@ -950,33 +970,34 @@ def send_proctoring_attempt_status_email(exam_attempt_obj, course_name): ...@@ -950,33 +970,34 @@ def send_proctoring_attempt_status_email(exam_attempt_obj, course_name):
site_name=constants.SITE_NAME, site_name=constants.SITE_NAME,
course_info_url=course_info_url course_info_url=course_info_url
) )
exam_name = exam_attempt_obj.proctored_exam.exam_name
support_email_subject = _('Proctored exam {exam_name} in {course_name} for user {username}').format(
exam_name=exam_name,
course_name=course_name,
username=user.username,
)
body = email_template.render( body = email_template.render(
Context({ Context({
'username': user.username,
'course_url': course_url, 'course_url': course_url,
'course_name': course_name, 'course_name': course_name,
'exam_name': exam_attempt_obj.proctored_exam.exam_name, 'exam_name': exam_name,
'status': ProctoredExamStudentAttemptStatus.get_status_alias(exam_attempt_obj.status), 'status': status,
'platform': constants.PLATFORM_NAME, 'platform': constants.PLATFORM_NAME,
'contact_email': constants.CONTACT_EMAIL, 'contact_email': constants.CONTACT_EMAIL,
'support_email_subject': support_email_subject,
}) })
) )
subject = (
_('Proctoring Session Results Update for {course_name} {exam_name}').format(
course_name=course_name,
exam_name=exam_attempt_obj.proctored_exam.exam_name
)
)
email = EmailMessage( email = EmailMessage(
body=body, body=body,
from_email=constants.FROM_EMAIL, from_email=constants.FROM_EMAIL,
to=[exam_attempt_obj.user.email], to=[exam_attempt_obj.user.email],
subject=subject subject=email_subject,
) )
email.content_subtype = "html" email.content_subtype = 'html'
email.send() return email
def remove_exam_attempt(attempt_id, requesting_user): def remove_exam_attempt(attempt_id, requesting_user):
......
...@@ -224,16 +224,6 @@ class ProctoredExamStudentAttemptStatus(object): ...@@ -224,16 +224,6 @@ class ProctoredExamStudentAttemptStatus(object):
] ]
@classmethod @classmethod
def needs_status_change_email(cls, to_status):
"""
We need to send out emails for rejected, verified and submitted statuses.
"""
return to_status in [
cls.rejected, cls.submitted, cls.verified
]
@classmethod
def get_status_alias(cls, status): def get_status_alias(cls, status):
""" """
Returns status alias used in email Returns status alias used in email
......
{% load i18n %}
<p>
{% blocktrans %}
Hi {{ username }},
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
Your proctored exam "{{ exam_name }}" in
<a href="{{ course_url }}">{{ course_name }}</a> was reviewed and you
met all exam requirements. You can view your grade on the course
progress page.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
If you have any questions about your results, contact {{ platform }}
support at
<a href="mailto:{{ contact_email }}?Subject={{ support_email_subject }}">
{{ contact_email }}
</a>.
{% endblocktrans %}
</p>
{% load i18n %}
{% blocktrans %}
This email is to let you know that the status of your proctoring session review for {{ exam_name }} in
<a href="{{ course_url }}">{{ course_name }} </a> is {{ status }}. If you have any questions about proctoring,
contact {{ platform }} support at {{ contact_email }}.
{% endblocktrans %}
\ No newline at end of file
{% load i18n %}
<p>
{% blocktrans %}
Hi {{ username }},
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
Your proctored exam "{{ exam_name }}" in
<a href="{{ course_url }}">{{ course_name }}</a> was submitted
successfully and will now be reviewed to ensure all proctoring exam
rules were followed. You should receive an email with your updated exam
status within 5 business days.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
If you have any questions about proctoring, contact {{ platform }}
support at
<a href="mailto:{{ contact_email }}?Subject={{ support_email_subject }}">
{{ contact_email }}
</a>.
{% endblocktrans %}
</p>
{% load i18n %}
<p>
{% blocktrans %}
Hi {{ username }},
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
Your proctored exam "{{ exam_name }}" in
<a href="{{ course_url }}">{{ course_name }}</a> was reviewed and the
team found one or more violations of the proctored exam rules. Examples
of behaviors 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 violation(s), you did not successfully meet the proctored
exam requirements.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
If you have any questions about your results, contact {{ platform }}
support at
<a href="mailto:{{ contact_email }}?Subject={{ support_email_subject }}">
{{ contact_email }}
</a>.
{% endblocktrans %}
</p>
...@@ -31,22 +31,25 @@ class ProctoredExamEmailTests(ProctoredExamTestCase): ...@@ -31,22 +31,25 @@ class ProctoredExamEmailTests(ProctoredExamTestCase):
All tests for proctored exam emails. All tests for proctored exam emails.
""" """
def setUp(self):
"""
Build out test harnessing
"""
super(ProctoredExamEmailTests, self).setUp()
# Messages for get_student_view
self.proctored_exam_email_subject = 'Proctoring Session Results Update'
self.proctored_exam_email_body = 'the status of your proctoring session review'
@ddt.data( @ddt.data(
ProctoredExamStudentAttemptStatus.submitted, [
ProctoredExamStudentAttemptStatus.verified, ProctoredExamStudentAttemptStatus.submitted,
ProctoredExamStudentAttemptStatus.rejected 'Proctoring Review In Progress',
'was submitted successfully',
],
[
ProctoredExamStudentAttemptStatus.verified,
'Proctoring Results',
'was reviewed and you met all exam requirements',
],
[
ProctoredExamStudentAttemptStatus.rejected,
'Proctoring Results',
'the team found one or more violations',
]
) )
def test_send_email(self, status): @ddt.unpack
def test_send_email(self, status, expected_subject, expected_message_string):
""" """
Assert that email is sent on the following statuses of proctoring attempt. Assert that email is sent on the following statuses of proctoring attempt.
""" """
...@@ -59,27 +62,18 @@ class ProctoredExamEmailTests(ProctoredExamTestCase): ...@@ -59,27 +62,18 @@ class ProctoredExamEmailTests(ProctoredExamTestCase):
status status
) )
self.assertEquals(len(mail.outbox), 1) self.assertEquals(len(mail.outbox), 1)
self.assertIn(self.proctored_exam_email_subject, mail.outbox[0].subject)
self.assertIn(self.proctored_exam_email_body, mail.outbox[0].body)
self.assertIn(ProctoredExamStudentAttemptStatus.get_status_alias(status), mail.outbox[0].body)
self.assertIn(credit_state['course_name'], mail.outbox[0].body)
@ddt.data( # Verify the subject
ProctoredExamStudentAttemptStatus.second_review_required, actual_subject = self._normalize_whitespace(mail.outbox[0].subject)
ProctoredExamStudentAttemptStatus.error self.assertIn(expected_subject, actual_subject)
) self.assertIn(self.exam_name, actual_subject)
def test_email_not_sent(self, status):
"""
Assert than email is not sent on the following statuses of proctoring attempt
"""
exam_attempt = self._create_started_exam_attempt() # Verify the body
update_attempt_status( actual_body = self._normalize_whitespace(mail.outbox[0].body)
exam_attempt.proctored_exam_id, self.assertIn('Hi tester,', actual_body)
self.user.id, self.assertIn('Your proctored exam "Test Exam"', actual_body)
status self.assertIn(credit_state['course_name'], actual_body)
) self.assertIn(expected_message_string, actual_body)
self.assertEquals(len(mail.outbox), 0)
def test_send_email_unicode(self): def test_send_email_unicode(self):
""" """
...@@ -97,16 +91,16 @@ class ProctoredExamEmailTests(ProctoredExamTestCase): ...@@ -97,16 +91,16 @@ class ProctoredExamEmailTests(ProctoredExamTestCase):
ProctoredExamStudentAttemptStatus.submitted ProctoredExamStudentAttemptStatus.submitted
) )
self.assertEquals(len(mail.outbox), 1) self.assertEquals(len(mail.outbox), 1)
self.assertIn(self.proctored_exam_email_subject, mail.outbox[0].subject)
self.assertIn(course_name, mail.outbox[0].subject) # Verify the subject
self.assertIn(self.proctored_exam_email_body, mail.outbox[0].body) actual_subject = self._normalize_whitespace(mail.outbox[0].subject)
self.assertIn( self.assertIn('Proctoring Review In Progress', actual_subject)
ProctoredExamStudentAttemptStatus.get_status_alias( self.assertIn(course_name, actual_subject)
ProctoredExamStudentAttemptStatus.submitted
), # Verify the body
mail.outbox[0].body actual_body = self._normalize_whitespace(mail.outbox[0].body)
) self.assertIn('was submitted successfully', actual_body)
self.assertIn(credit_state['course_name'], mail.outbox[0].body) self.assertIn(credit_state['course_name'], actual_body)
@ddt.data( @ddt.data(
ProctoredExamStudentAttemptStatus.eligible, ProctoredExamStudentAttemptStatus.eligible,
...@@ -117,12 +111,13 @@ class ProctoredExamEmailTests(ProctoredExamTestCase): ...@@ -117,12 +111,13 @@ class ProctoredExamEmailTests(ProctoredExamTestCase):
ProctoredExamStudentAttemptStatus.ready_to_submit, ProctoredExamStudentAttemptStatus.ready_to_submit,
ProctoredExamStudentAttemptStatus.declined, ProctoredExamStudentAttemptStatus.declined,
ProctoredExamStudentAttemptStatus.timed_out, ProctoredExamStudentAttemptStatus.timed_out,
ProctoredExamStudentAttemptStatus.error ProctoredExamStudentAttemptStatus.second_review_required,
ProctoredExamStudentAttemptStatus.error,
) )
@patch.dict('django.conf.settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True}) @patch.dict('django.conf.settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True})
def test_not_send_email(self, status): def test_email_not_sent(self, status):
""" """
Assert that email is not sent on the following statuses of proctoring attempt. Assert that an email is not sent for the following attempt status codes.
""" """
exam_attempt = self._create_started_exam_attempt() exam_attempt = self._create_started_exam_attempt()
......
...@@ -341,3 +341,10 @@ class ProctoredExamTestCase(LoggedInTestCase): ...@@ -341,3 +341,10 @@ class ProctoredExamTestCase(LoggedInTestCase):
status=ProctoredExamStudentAttemptStatus.started, status=ProctoredExamStudentAttemptStatus.started,
allowed_time_limit_mins=10 allowed_time_limit_mins=10
) )
@staticmethod
def _normalize_whitespace(string):
"""
Replaces newlines and multiple spaces with a single space.
"""
return ' '.join(string.replace('\n', '').split())
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment