Commit c9395662 by Sarina Canelake

Merge pull request #4746 from Stanford-Online/njdupoux/instructor-email-history-fixes

Email content history fix and additions
parents f3a03275 55521590
...@@ -1980,22 +1980,27 @@ class TestInstructorEmailContentList(ModuleStoreTestCase, LoginEnrollmentTestCas ...@@ -1980,22 +1980,27 @@ class TestInstructorEmailContentList(ModuleStoreTestCase, LoginEnrollmentTestCas
""" """
patch.stopall() patch.stopall()
def setup_fake_email_info(self, num_emails): def setup_fake_email_info(self, num_emails, with_failures=False):
""" Initialize the specified number of fake emails """ """ Initialize the specified number of fake emails """
for email_id in range(num_emails): for email_id in range(num_emails):
num_sent = random.randint(1, 15401) num_sent = random.randint(1, 15401)
self.tasks[email_id] = FakeContentTask(email_id, num_sent, 'expected') if with_failures:
failed = random.randint(1, 15401)
else:
failed = 0
self.tasks[email_id] = FakeContentTask(email_id, num_sent, failed, 'expected')
self.emails[email_id] = FakeEmail(email_id) self.emails[email_id] = FakeEmail(email_id)
self.emails_info[email_id] = FakeEmailInfo(self.emails[email_id], num_sent) self.emails_info[email_id] = FakeEmailInfo(self.emails[email_id], num_sent, failed)
def get_matching_mock_email(self, **kwargs): def get_matching_mock_email(self, **kwargs):
""" Returns the matching mock emails for the given id """ """ Returns the matching mock emails for the given id """
email_id = kwargs.get('id', 0) email_id = kwargs.get('id', 0)
return self.emails[email_id] return self.emails[email_id]
def get_email_content_response(self, num_emails, task_history_request): def get_email_content_response(self, num_emails, task_history_request, with_failures=False):
""" Calls the list_email_content endpoint and returns the repsonse """ """ Calls the list_email_content endpoint and returns the repsonse """
self.setup_fake_email_info(num_emails) self.setup_fake_email_info(num_emails, with_failures)
task_history_request.return_value = self.tasks.values() task_history_request.return_value = self.tasks.values()
url = reverse('list_email_content', kwargs={'course_id': self.course.id.to_deprecated_string()}) url = reverse('list_email_content', kwargs={'course_id': self.course.id.to_deprecated_string()})
with patch('instructor.views.api.CourseEmail.objects.get') as mock_email_info: with patch('instructor.views.api.CourseEmail.objects.get') as mock_email_info:
...@@ -2004,6 +2009,19 @@ class TestInstructorEmailContentList(ModuleStoreTestCase, LoginEnrollmentTestCas ...@@ -2004,6 +2009,19 @@ class TestInstructorEmailContentList(ModuleStoreTestCase, LoginEnrollmentTestCas
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
return response return response
def check_emails_sent(self, num_emails, task_history_request, with_failures=False):
""" Tests sending emails with or without failures """
response = self.get_email_content_response(num_emails, task_history_request, with_failures)
self.assertTrue(task_history_request.called)
expected_email_info = [email_info.to_dict() for email_info in self.emails_info.values()]
actual_email_info = json.loads(response.content)['emails']
self.assertEqual(len(actual_email_info), num_emails)
for exp_email, act_email in zip(expected_email_info, actual_email_info):
self.assertDictEqual(exp_email, act_email)
self.assertEqual(expected_email_info, actual_email_info)
def test_content_list_one_email(self, task_history_request): def test_content_list_one_email(self, task_history_request):
""" Test listing of bulk emails when email list has one email """ """ Test listing of bulk emails when email list has one email """
response = self.get_email_content_response(1, task_history_request) response = self.get_email_content_response(1, task_history_request)
...@@ -2030,20 +2048,11 @@ class TestInstructorEmailContentList(ModuleStoreTestCase, LoginEnrollmentTestCas ...@@ -2030,20 +2048,11 @@ class TestInstructorEmailContentList(ModuleStoreTestCase, LoginEnrollmentTestCas
def test_content_list_email_content_many(self, task_history_request): def test_content_list_email_content_many(self, task_history_request):
""" Test listing of bulk emails sent large amount of emails """ """ Test listing of bulk emails sent large amount of emails """
response = self.get_email_content_response(50, task_history_request) self.check_emails_sent(50, task_history_request)
self.assertTrue(task_history_request.called)
expected_email_info = [email_info.to_dict() for email_info in self.emails_info.values()]
actual_email_info = json.loads(response.content)['emails']
self.assertEqual(len(actual_email_info), 50)
for exp_email, act_email in zip(expected_email_info, actual_email_info):
self.assertDictEqual(exp_email, act_email)
self.assertEqual(actual_email_info, expected_email_info)
def test_list_email_content_error(self, task_history_request): def test_list_email_content_error(self, task_history_request):
""" Test handling of error retrieving email """ """ Test handling of error retrieving email """
invalid_task = FakeContentTask(0, 0, 'test') invalid_task = FakeContentTask(0, 0, 0, 'test')
invalid_task.make_invalid_input() invalid_task.make_invalid_input()
task_history_request.return_value = [invalid_task] task_history_request.return_value = [invalid_task]
url = reverse('list_email_content', kwargs={'course_id': self.course.id.to_deprecated_string()}) url = reverse('list_email_content', kwargs={'course_id': self.course.id.to_deprecated_string()})
...@@ -2054,9 +2063,36 @@ class TestInstructorEmailContentList(ModuleStoreTestCase, LoginEnrollmentTestCas ...@@ -2054,9 +2063,36 @@ class TestInstructorEmailContentList(ModuleStoreTestCase, LoginEnrollmentTestCas
returned_email_info = json.loads(response.content)['emails'] returned_email_info = json.loads(response.content)['emails']
self.assertEqual(len(returned_email_info), 1) self.assertEqual(len(returned_email_info), 1)
returned_info = returned_email_info[0] returned_info = returned_email_info[0]
for info in ['created', 'sent_to', 'email', 'number_sent']: for info in ['created', 'sent_to', 'email', 'number_sent', 'requester']:
self.assertEqual(returned_info[info], None) self.assertEqual(returned_info[info], None)
def test_list_email_with_failure(self, task_history_request):
""" Test the handling of email task that had failures """
self.check_emails_sent(1, task_history_request, True)
def test_list_many_emails_with_failures(self, task_history_request):
""" Test the handling of many emails with failures """
self.check_emails_sent(50, task_history_request, True)
def test_list_email_with_no_successes(self, task_history_request):
task_info = FakeContentTask(0, 0, 10, 'expected')
email = FakeEmail(0)
email_info = FakeEmailInfo(email, 0, 10)
task_history_request.return_value = [task_info]
url = reverse('list_email_content', kwargs={'course_id': self.course.id.to_deprecated_string()})
with patch('instructor.views.api.CourseEmail.objects.get') as mock_email_info:
mock_email_info.return_value = email
response = self.client.get(url, {})
self.assertEqual(response.status_code, 200)
self.assertTrue(task_history_request.called)
returned_info_list = json.loads(response.content)['emails']
self.assertEqual(len(returned_info_list), 1)
returned_info = returned_info_list[0]
expected_info = email_info.to_dict()
self.assertDictEqual(expected_info, returned_info)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
@override_settings(ANALYTICS_SERVER_URL="http://robotanalyticsserver.netbot:900/") @override_settings(ANALYTICS_SERVER_URL="http://robotanalyticsserver.netbot:900/")
......
...@@ -26,14 +26,16 @@ class FakeContentTask(FakeInfo): ...@@ -26,14 +26,16 @@ class FakeContentTask(FakeInfo):
FEATURES = [ FEATURES = [
'task_input', 'task_input',
'task_output', 'task_output',
'requester',
] ]
def __init__(self, email_id, num_sent, sent_to): def __init__(self, email_id, num_sent, num_failed, sent_to):
super(FakeContentTask, self).__init__() super(FakeContentTask, self).__init__()
self.task_input = {'email_id': email_id, 'to_option': sent_to} self.task_input = {'email_id': email_id, 'to_option': sent_to}
self.task_input = json.dumps(self.task_input) self.task_input = json.dumps(self.task_input)
self.task_output = {'total': num_sent} self.task_output = {'succeeded': num_sent, 'failed': num_failed}
self.task_output = json.dumps(self.task_output) self.task_output = json.dumps(self.task_output)
self.requester = 'expected'
def make_invalid_input(self): def make_invalid_input(self):
"""Corrupt the task input field to test errors""" """Corrupt the task input field to test errors"""
...@@ -67,7 +69,8 @@ class FakeEmailInfo(FakeInfo): ...@@ -67,7 +69,8 @@ class FakeEmailInfo(FakeInfo):
u'created', u'created',
u'sent_to', u'sent_to',
u'email', u'email',
u'number_sent' u'number_sent',
u'requester',
] ]
EMAIL_FEATURES = [ EMAIL_FEATURES = [
...@@ -76,9 +79,15 @@ class FakeEmailInfo(FakeInfo): ...@@ -76,9 +79,15 @@ class FakeEmailInfo(FakeInfo):
u'id' u'id'
] ]
def __init__(self, fake_email, num_sent): def __init__(self, fake_email, num_sent, num_failed):
super(FakeEmailInfo, self).__init__() super(FakeEmailInfo, self).__init__()
self.created = get_default_time_display(fake_email.created) self.created = get_default_time_display(fake_email.created)
self.number_sent = num_sent
number_sent = str(num_sent) + ' sent'
if num_failed > 0:
number_sent += ', ' + str(num_failed) + " failed"
self.number_sent = number_sent
fake_email_dict = fake_email.to_dict() fake_email_dict = fake_email.to_dict()
self.email = {feature: fake_email_dict[feature] for feature in self.EMAIL_FEATURES} self.email = {feature: fake_email_dict[feature] for feature in self.EMAIL_FEATURES}
self.requester = u'expected'
...@@ -7,6 +7,7 @@ import logging ...@@ -7,6 +7,7 @@ import logging
from util.date_utils import get_default_time_display from util.date_utils import get_default_time_display
from bulk_email.models import CourseEmail from bulk_email.models import CourseEmail
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ungettext
from instructor_task.views import get_task_completion_info from instructor_task.views import get_task_completion_info
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -21,7 +22,8 @@ def email_error_information(): ...@@ -21,7 +22,8 @@ def email_error_information():
'created', 'created',
'sent_to', 'sent_to',
'email', 'email',
'number_sent' 'number_sent',
'requester',
] ]
return {info: None for info in expected_info} return {info: None for info in expected_info}
...@@ -33,6 +35,7 @@ def extract_email_features(email_task): ...@@ -33,6 +35,7 @@ def extract_email_features(email_task):
Expects that the given task has the following attributes: Expects that the given task has the following attributes:
* task_input (dict containing email_id and to_option) * task_input (dict containing email_id and to_option)
* task_output (optional, dict containing total emails sent) * task_output (optional, dict containing total emails sent)
* requester, the user who executed the task
With this information, gets the corresponding email object from the With this information, gets the corresponding email object from the
bulk emails table, and loads up a dict containing the following: bulk emails table, and loads up a dict containing the following:
...@@ -40,6 +43,7 @@ def extract_email_features(email_task): ...@@ -40,6 +43,7 @@ def extract_email_features(email_task):
* sent_to, the group the email was delivered to * sent_to, the group the email was delivered to
* email, dict containing the subject, id, and html_message of an email * email, dict containing the subject, id, and html_message of an email
* number_sent, int number of emails sent * number_sent, int number of emails sent
* requester, the user who sent the emails
If task_input cannot be loaded, then the email cannot be loaded If task_input cannot be loaded, then the email cannot be loaded
and None is returned for these fields. and None is returned for these fields.
""" """
...@@ -51,26 +55,43 @@ def extract_email_features(email_task): ...@@ -51,26 +55,43 @@ def extract_email_features(email_task):
return email_error_information() return email_error_information()
email = CourseEmail.objects.get(id=task_input_information['email_id']) email = CourseEmail.objects.get(id=task_input_information['email_id'])
email_feature_dict = {
creation_time = get_default_time_display(email.created) 'created': get_default_time_display(email.created),
email_feature_dict = {'created': creation_time, 'sent_to': task_input_information['to_option']} 'sent_to': task_input_information['to_option'],
'requester': str(getattr(email_task, 'requester')),
}
features = ['subject', 'html_message', 'id'] features = ['subject', 'html_message', 'id']
email_info = {feature: unicode(getattr(email, feature)) for feature in features} email_info = {feature: unicode(getattr(email, feature)) for feature in features}
# Pass along email as an object with the information we desire # Pass along email as an object with the information we desire
email_feature_dict['email'] = email_info email_feature_dict['email'] = email_info
number_sent = None # Translators: number sent refers to the number of emails sent
number_sent = _('0 sent')
if hasattr(email_task, 'task_output') and email_task.task_output is not None: if hasattr(email_task, 'task_output') and email_task.task_output is not None:
try: try:
task_output = json.loads(email_task.task_output) task_output = json.loads(email_task.task_output)
except ValueError: except ValueError:
log.error("Could not parse task output as valid json; task output: %s", email_task.task_output) log.error("Could not parse task output as valid json; task output: %s", email_task.task_output)
else: else:
if 'total' in task_output: if 'succeeded' in task_output and task_output['succeeded'] > 0:
number_sent = int(task_output['total']) num_emails = task_output['succeeded']
email_feature_dict['number_sent'] = number_sent number_sent = ungettext(
"{num_emails} sent",
"{num_emails} sent",
num_emails
).format(num_emails=num_emails)
if 'failed' in task_output and task_output['failed'] > 0:
num_emails = task_output['failed']
number_sent += ", "
number_sent += ungettext(
"{num_emails} failed",
"{num_emails} failed",
num_emails
).format(num_emails=num_emails)
email_feature_dict['number_sent'] = number_sent
return email_feature_dict return email_feature_dict
......
...@@ -121,17 +121,21 @@ create_task_list_table = ($table_tasks, tasks_data) -> ...@@ -121,17 +121,21 @@ create_task_list_table = ($table_tasks, tasks_data) ->
# Formats the subject field for email content history table # Formats the subject field for email content history table
subject_formatter = (row, cell, value, columnDef, dataContext) -> subject_formatter = (row, cell, value, columnDef, dataContext) ->
if !value then return gettext("An error occurred retrieving your email. Please try again later, and contact technical support if the problem persists.") if value is null then return gettext("An error occurred retrieving your email. Please try again later, and contact technical support if the problem persists.")
subject_text = $('<span>').text(value['subject']).html() subject_text = $('<span>').text(value['subject']).html()
return '<p><a href="#email_message_' + value['id']+ '" id="email_message_' + value['id'] + '_trig">' + subject_text + '</a></p>' return '<p><a href="#email_message_' + value['id']+ '" id="email_message_' + value['id'] + '_trig">' + subject_text + '</a></p>'
# Formats the author field for the email content history table
sent_by_formatter = (row, cell, value, columnDef, dataContext) ->
if value is null then return "<p>" + gettext("Unknown") + "</p>" else return '<p>' + value + '</p>'
# Formats the created field for the email content history table # Formats the created field for the email content history table
created_formatter = (row, cell, value, columnDef, dataContext) -> created_formatter = (row, cell, value, columnDef, dataContext) ->
if !value then return "<p>" + gettext("Unknown") + "</p>" else return '<p>' + value + '</p>' if value is null then return "<p>" + gettext("Unknown") + "</p>" else return '<p>' + value + '</p>'
# Formats the number sent field for the email content history table # Formats the number sent field for the email content history table
number_sent_formatter = (row, cell, value, columndDef, dataContext) -> number_sent_formatter = (row, cell, value, columndDef, dataContext) ->
if !value then return "<p>" + gettext("Unknown") + "</p>" else return '<p>' + value + '</p>' if value is null then return "<p>" + gettext("Unknown") + "</p>" else return '<p>' + value + '</p>'
# Creates a table to display the content of bulk course emails # Creates a table to display the content of bulk course emails
# sent in the past # sent in the past
...@@ -154,6 +158,14 @@ create_email_content_table = ($table_emails, $table_emails_inner, email_data) -> ...@@ -154,6 +158,14 @@ create_email_content_table = ($table_emails, $table_emails_inner, email_data) ->
cssClass: "email-content-cell" cssClass: "email-content-cell"
formatter: subject_formatter formatter: subject_formatter
, ,
id: 'requester'
field: 'requester'
name: gettext('Sent By')
minWidth: 80
maxWidth: 100
cssClass: "email-content-cell"
formatter: sent_by_formatter
,
id: 'created' id: 'created'
field: 'created' field: 'created'
name: gettext('Time Sent') name: gettext('Time Sent')
...@@ -203,7 +215,7 @@ create_email_message_views = ($messages_wrapper, emails) -> ...@@ -203,7 +215,7 @@ create_email_message_views = ($messages_wrapper, emails) ->
# HTML escape the subject line # HTML escape the subject line
subject_text = $('<span>').text(email_info.email['subject']).html() subject_text = $('<span>').text(email_info.email['subject']).html()
$email_header.append $('<h2>', class: "message-bold").html('<em>' + gettext('Subject:') + '</em> ' + subject_text) $email_header.append $('<h2>', class: "message-bold").html('<em>' + gettext('Subject:') + '</em> ' + subject_text)
$email_header.append $('<h2>', class: "message-bold").html('<em>' + gettext('Sent By:') + '</em> ' + email_info.requester)
$email_header.append $('<h2>', class: "message-bold").html('<em>' + gettext('Time Sent:') + '</em> ' + email_info.created) $email_header.append $('<h2>', class: "message-bold").html('<em>' + gettext('Time Sent:') + '</em> ' + email_info.created)
$email_header.append $('<h2>', class: "message-bold").html('<em>' + gettext('Sent To:') + '</em> ' + email_info.sent_to) $email_header.append $('<h2>', class: "message-bold").html('<em>' + gettext('Sent To:') + '</em> ' + email_info.sent_to)
$email_wrapper.append $email_header $email_wrapper.append $email_header
......
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