Bulk Email: Add design styling

Switch to using decorators; refactor and cleanup tests.
This commit is contained in:
Sarina Canelake
2013-08-16 12:00:14 -04:00
parent 3b32d4216c
commit 65f7b098dd
12 changed files with 381 additions and 148 deletions

View File

@@ -33,7 +33,6 @@ class Migration(SchemaMigration):
# Adding unique constraint on 'Optout', fields ['email', 'course_id']
db.create_unique('bulk_email_optout', ['email', 'course_id'])
def backwards(self, orm):
# Removing unique constraint on 'Optout', fields ['email', 'course_id']
db.delete_unique('bulk_email_optout', ['email', 'course_id'])
@@ -44,7 +43,6 @@ class Migration(SchemaMigration):
# Deleting model 'Optout'
db.delete_table('bulk_email_optout')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
@@ -102,4 +100,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['bulk_email']
complete_apps = ['bulk_email']

View File

@@ -10,6 +10,7 @@ class Email(models.Model):
Abstract base class for common information for an email.
"""
sender = models.ForeignKey(User, default=1, blank=True, null=True)
# The unique hash for this email. Used to quickly look up an email (see `tasks.py`)
hash = models.CharField(max_length=128, db_index=True)
subject = models.CharField(max_length=128, blank=True)
html_message = models.TextField(null=True, blank=True)
@@ -24,10 +25,20 @@ class CourseEmail(Email, models.Model):
"""
Stores information for an email to a course.
"""
TO_OPTIONS = (('myself', 'Myself'),
('staff', 'Staff and instructors'),
('all', 'All')
)
# Three options for sending that we provide from the instructor dashboard:
# * Myself: This sends an email to the staff member that is composing the email.
#
# * Staff and instructors: This sends an email to anyone in the staff group and
# anyone in the instructor group
#
# * All: This sends an email to anyone enrolled in the course, with any role
# (student, staff, or instructor)
#
TO_OPTIONS = (
('myself', 'Myself'),
('staff', 'Staff and instructors'),
('all', 'All')
)
course_id = models.CharField(max_length=255, db_index=True)
to = models.CharField(max_length=64, choices=TO_OPTIONS, default='myself')

View File

@@ -31,7 +31,7 @@ def delegate_email_batches(hash_for_msg, to_option, course_id, course_url, user_
get the mail, chopping up into batches of settings.EMAILS_PER_TASK size,
and queueing up worker jobs.
`to_option` is {'students', 'staff', or 'all'}
`to_option` is {'myself', 'staff', or 'all'}
Returns the number of batches (workers) kicked off.
"""
@@ -49,7 +49,8 @@ def delegate_email_batches(hash_for_msg, to_option, course_id, course_url, user_
if to_option == "myself":
recipient_qset = User.objects.filter(id=user_id).values('profile__name', 'email')
else:
elif to_option == "all" or to_option == "staff":
staff_grpname = _course_staff_group_name(course.location)
staff_group, _ = Group.objects.get_or_create(name=staff_grpname)
staff_qset = staff_group.user_set.values('profile__name', 'email')
@@ -66,8 +67,12 @@ def delegate_email_batches(hash_for_msg, to_option, course_id, course_url, user_
recipient_qset = recipient_qset | enrollment_qset
recipient_qset = recipient_qset.distinct()
else:
log.error("Unexpected bulk email TO_OPTION found: %s", to_option)
raise Exception("Unexpected bulk email TO_OPTION found: {0}".format(to_option))
recipient_list = list(recipient_qset)
total_num_emails = recipient_qset.count()
total_num_emails = len(recipient_list)
num_workers = int(math.ceil(float(total_num_emails) / float(settings.EMAILS_PER_TASK)))
chunk = int(math.ceil(float(total_num_emails) / float(num_workers)))
@@ -97,7 +102,7 @@ def course_email(hash_for_msg, to_list, course_title, course_url, throttle=False
(plaintext, err_from_stderr) = process.communicate(input=msg.html_message.encode('utf-8')) # use lynx to get plaintext
course_title_no_quotes = re.sub(r'"', '', course_title)
from_addr = '"%s" Course Staff <%s>' % (course_title_no_quotes, settings.DEFAULT_BULK_FROM_EMAIL)
from_addr = '"{0}" Course Staff <{1}>'.format(course_title_no_quotes, settings.DEFAULT_BULK_FROM_EMAIL)
if err_from_stderr:
log.info(err_from_stderr)
@@ -108,20 +113,33 @@ def course_email(hash_for_msg, to_list, course_title, course_url, throttle=False
num_sent = 0
num_error = 0
email_context = {
'name': '',
'email': '',
'course_title': course_title,
'course_url': course_url
}
while to_list:
(name, email) = to_list[-1].values()
html_footer = render_to_string('emails/email_footer.html',
{'name': name,
'email': email,
'course_title': course_title,
'course_url': course_url})
plain_footer = render_to_string('emails/email_footer.txt',
{'name': name,
'email': email,
'course_title': course_title,
'course_url': course_url})
email_context['name'] = name
email_context['email'] = email
email_msg = EmailMultiAlternatives(subject, plaintext + plain_footer.encode('utf-8'), from_addr, [email], connection=connection)
html_footer = render_to_string(
'emails/email_footer.html',
email_context
)
plain_footer = render_to_string(
'emails/email_footer.txt',
email_context
)
email_msg = EmailMultiAlternatives(
subject,
plaintext + plain_footer.encode('utf-8'),
from_addr,
[email],
connection=connection
)
email_msg.attach_alternative(msg.html_message + html_footer.encode('utf-8'), 'text/html')
if throttle or current_task.request.retries > 0: # throttle if we tried a few times and got the rate limiter
@@ -132,11 +150,12 @@ def course_email(hash_for_msg, to_list, course_title, course_url, throttle=False
log.info('Email with hash ' + hash_for_msg + ' sent to ' + email)
num_sent += 1
except SMTPDataError as exc:
#According to SMTP spec, we'll retry error codes in the 4xx range. 5xx range indicates hard failure
# According to SMTP spec, we'll retry error codes in the 4xx range. 5xx range indicates hard failure
if exc.smtp_code >= 400 and exc.smtp_code < 500:
raise exc # this will cause the outer handler to catch the exception and retry the entire task
# This will cause the outer handler to catch the exception and retry the entire task
raise exc
else:
#this will fall through and not retry the message, since it will be popped
# This will fall through and not retry the message, since it will be popped
log.warning('Email with hash ' + hash_for_msg + ' not delivered to ' + email + ' due to error: ' + exc.smtp_error)
num_error += 1
@@ -146,8 +165,18 @@ def course_email(hash_for_msg, to_list, course_title, course_url, throttle=False
return course_email_result(num_sent, num_error)
except (SMTPDataError, SMTPConnectError, SMTPServerDisconnected) as exc:
#error caught here cause the email to be retried. The entire task is actually retried without popping the list
raise course_email.retry(arg=[hash_for_msg, to_list, course_title, course_url, current_task.request.retries > 0], exc=exc, countdown=(2 ** current_task.request.retries) * 15)
# Error caught here cause the email to be retried. The entire task is actually retried without popping the list
raise course_email.retry(
arg=[
hash_for_msg,
to_list,
course_title,
course_url,
current_task.request.retries > 0
],
exc=exc,
countdown=(2 ** current_task.request.retries) * 15
)
# This string format code is wrapped in this function to allow mocking for a unit test

View File

@@ -4,12 +4,16 @@ Unit tests for student optouts from course email
import json
from django.core import mail
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from django.conf import settings
from django.test.utils import override_settings
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
from mock import patch
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
@@ -27,6 +31,24 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
self.client.login(username=self.student.username, password="test")
def navigate_to_email_view(self):
"""Navigate to the instructor dash's email view"""
# Pull up email view on instructor dashboard
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
response = self.client.get(url)
email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
# If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False
self.assertTrue(email_link in response.content)
# Select the Email view of the instructor dash
session = self.client.session
session['idash_mode'] = 'Email'
session.save()
response = self.client.get(url)
selected_email_link = '<a href="#" onclick="goto(\'Email\')" class="selectedmode">Email</a>'
self.assertTrue(selected_email_link in response.content)
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True})
def test_optout_course(self):
"""
Make sure student does not receive course email after opting out.
@@ -36,15 +58,24 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
self.assertEquals(json.loads(response.content), {'success': True})
self.client.logout()
self.client.login(username=self.instructor.username, password="test")
self.navigate_to_email_view()
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
response = self.client.post(url, {'action': 'Send email', 'to_option': 'all', 'subject': 'test subject for all', 'message': 'test message for all'})
test_email = {
'action': 'Send email',
'to_option': 'all',
'subject': 'test subject for all',
'message': 'test message for all'
}
response = self.client.post(url, test_email)
self.assertContains(response, "Your email was successfully queued for sending.")
#assert that self.student.email not in mail.to, outbox should be empty
# Assert that self.student.email not in mail.to, outbox should be empty
self.assertEqual(len(mail.outbox), 0)
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True})
def test_optin_course(self):
"""
Make sure student receives course email after opting in.
@@ -54,13 +85,22 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
self.assertEquals(json.loads(response.content), {'success': True})
self.client.logout()
self.client.login(username=self.instructor.username, password="test")
self.navigate_to_email_view()
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
response = self.client.post(url, {'action': 'Send email', 'to_option': 'all', 'subject': 'test subject for all', 'message': 'test message for all'})
test_email = {
'action': 'Send email',
'to_option': 'all',
'subject': 'test subject for all',
'message': 'test message for all'
}
response = self.client.post(url, test_email)
self.assertContains(response, "Your email was successfully queued for sending.")
#assert that self.student.email in mail.to
# Assert that self.student.email in mail.to
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(len(mail.outbox[0].to), 1)
self.assertEquals(mail.outbox[0].to[0], self.student.email)

View File

@@ -3,52 +3,79 @@ Unit tests for sending course email
"""
from django.test.utils import override_settings
from django.conf import settings
from django.core import mail
from django.core.urlresolvers import reverse
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from student.tests.factories import UserFactory, GroupFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import UserFactory, GroupFactory, CourseEnrollmentFactory
from django.core import mail
from bulk_email.tasks import delegate_email_batches, course_email
from bulk_email.models import CourseEmail
from mock import patch
STAFF_COUNT = 3
STUDENT_COUNT = 10
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestEmail(ModuleStoreTestCase):
class TestEmailSendFromDashboard(ModuleStoreTestCase):
"""
Test that emails send correctly.
"""
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True})
def setUp(self):
self.course = CourseFactory.create()
self.instructor = UserFactory.create(username="instructor", email="robot+instructor@edx.org")
#Create instructor group for course
# Create instructor group for course
instructor_group = GroupFactory.create(name="instructor_MITx/999/Robot_Super_Course")
instructor_group.user_set.add(self.instructor)
#create staff
# Create staff
self.staff = [UserFactory() for _ in xrange(STAFF_COUNT)]
staff_group = GroupFactory()
for staff in self.staff:
staff_group.user_set.add(staff) # pylint: disable=E1101
#create students
# Create students
self.students = [UserFactory() for _ in xrange(STUDENT_COUNT)]
for student in self.students:
CourseEnrollmentFactory.create(user=student, course_id=self.course.id)
self.client.login(username=self.instructor.username, password="test")
# Pull up email view on instructor dashboard
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
response = self.client.get(self.url)
email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
# If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False
self.assertTrue(email_link in response.content)
# Select the Email view of the instructor dash
session = self.client.session
session['idash_mode'] = 'Email'
session.save()
response = self.client.get(self.url)
selected_email_link = '<a href="#" onclick="goto(\'Email\')" class="selectedmode">Email</a>'
self.assertTrue(selected_email_link in response.content)
def test_send_to_self(self):
"""
Make sure email send to myself goes to myself.
"""
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
response = self.client.post(url, {'action': 'Send email', 'to_option': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself'})
# Now we know we have pulled up the instructor dash's email view
# (in the setUp method), we can test sending an email.
test_email = {
'action': 'Send email',
'to_option': 'myself',
'subject': 'test subject for myself',
'message': 'test message for myself'
}
response = self.client.post(self.url, test_email)
self.assertContains(response, "Your email was successfully queued for sending.")
@@ -61,8 +88,15 @@ class TestEmail(ModuleStoreTestCase):
"""
Make sure email send to staff and instructors goes there.
"""
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
response = self.client.post(url, {'action': 'Send email', 'to_option': 'staff', 'subject': 'test subject for staff', 'message': 'test message for subject'})
# Now we know we have pulled up the instructor dash's email view
# (in the setUp method), we can test sending an email.
test_email = {
'action': 'Send email',
'to_option': 'staff',
'subject': 'test subject for staff',
'message': 'test message for subject'
}
response = self.client.post(self.url, test_email)
self.assertContains(response, "Your email was successfully queued for sending.")
@@ -73,24 +107,35 @@ class TestEmail(ModuleStoreTestCase):
"""
Make sure email send to all goes there.
"""
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
response = self.client.post(url, {'action': 'Send email', 'to_option': 'all', 'subject': 'test subject for all', 'message': 'test message for all'})
# Now we know we have pulled up the instructor dash's email view
# (in the setUp method), we can test sending an email.
test_email = {
'action': 'Send email',
'to_option': 'all',
'subject': 'test subject for all',
'message': 'test message for all'
}
response = self.client.post(self.url, test_email)
self.assertContains(response, "Your email was successfully queued for sending.")
self.assertEquals(len(mail.outbox), 1 + len(self.staff) + len(self.students))
self.assertItemsEqual([e.to[0] for e in mail.outbox], [self.instructor.email] + [s.email for s in self.staff] + [s.email for s in self.students])
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestEmailSendExceptions(ModuleStoreTestCase):
"""
Test that exceptions are handled correctly.
"""
def test_get_course_exc(self):
"""
Make sure delegate_email_batches handles Http404 exception from get_course_by_id.
"""
# Make sure delegate_email_batches handles Http404 exception from get_course_by_id.
with self.assertRaises(Exception):
delegate_email_batches("_", "_", "blah/blah/blah", "_", "_")
def test_no_course_email_obj(self):
"""
Make sure course_email handles CourseEmail.DoesNotExist exception.
"""
# Make sure course_email handles CourseEmail.DoesNotExist exception.
with self.assertRaises(CourseEmail.DoesNotExist):
course_email("dummy hash", [], "_", "_", False)

View File

@@ -5,10 +5,12 @@ Unit tests for handling email sending errors
from django.test.utils import override_settings
from django.conf import settings
from django.core.urlresolvers import reverse
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
from bulk_email.tests.smtp_server_thread import FakeSMTPServerThread
from mock import patch
@@ -17,9 +19,13 @@ from smtplib import SMTPDataError, SMTPServerDisconnected, SMTPConnectError
TEST_SMTP_PORT = 1025
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE, EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend', EMAIL_HOST='localhost', EMAIL_PORT=TEST_SMTP_PORT)
@override_settings(
MODULESTORE=TEST_DATA_MONGO_MODULESTORE,
EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend',
EMAIL_HOST='localhost',
EMAIL_PORT=TEST_SMTP_PORT
)
class TestEmailErrors(ModuleStoreTestCase):
"""
Test that errors from sending email are handled properly.
"""
@@ -32,6 +38,8 @@ class TestEmailErrors(ModuleStoreTestCase):
self.smtp_server_thread = FakeSMTPServerThread('localhost', TEST_SMTP_PORT)
self.smtp_server_thread.start()
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
def tearDown(self):
self.smtp_server_thread.stop()
@@ -40,9 +48,20 @@ class TestEmailErrors(ModuleStoreTestCase):
"""
Test that celery handles transient SMTPDataErrors by retrying.
"""
self.smtp_server_thread.server.set_errtype("DATA", "454 Throttling failure: Daily message quota exceeded.")
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
self.client.post(url, {'action': 'Send email', 'to_option': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself'})
self.smtp_server_thread.server.set_errtype(
"DATA",
"454 Throttling failure: Daily message quota exceeded."
)
test_email = {
'action': 'Send email',
'to_option': 'myself',
'subject': 'test subject for myself',
'message': 'test message for myself'
}
self.client.post(self.url, test_email)
# Test that we retry upon hitting a 4xx error
self.assertTrue(retry.called)
(_, kwargs) = retry.call_args
exc = kwargs['exc']
@@ -54,16 +73,26 @@ class TestEmailErrors(ModuleStoreTestCase):
"""
Test that celery handles permanent SMTPDataErrors by failing and not retrying.
"""
self.smtp_server_thread.server.set_errtype("DATA", "554 Message rejected: Email address is not verified.")
self.smtp_server_thread.server.set_errtype(
"DATA",
"554 Message rejected: Email address is not verified."
)
students = [UserFactory() for _ in xrange(settings.EMAILS_PER_TASK)]
for student in students:
CourseEnrollmentFactory.create(user=student, course_id=self.course.id)
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
self.client.post(url, {'action': 'Send email', 'to_option': 'all', 'subject': 'test subject for all', 'message': 'test message for all'})
self.assertFalse(retry.called)
test_email = {
'action': 'Send email',
'to_option': 'all',
'subject': 'test subject for all',
'message': 'test message for all'
}
self.client.post(self.url, test_email)
#test that after the failed email, the rest send successfully
# We shouldn't retry when hitting a 5xx error
self.assertFalse(retry.called)
# Test that after the rejected email, the rest still successfully send
((sent, fail), _) = result.call_args
self.assertEquals(fail, 1)
self.assertEquals(sent, settings.EMAILS_PER_TASK - 1)
@@ -73,9 +102,18 @@ class TestEmailErrors(ModuleStoreTestCase):
"""
Test that celery handles SMTPServerDisconnected by retrying.
"""
self.smtp_server_thread.server.set_errtype("DISCONN", "Server disconnected, please try again later.")
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
self.client.post(url, {'action': 'Send email', 'to_option': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself'})
self.smtp_server_thread.server.set_errtype(
"DISCONN",
"Server disconnected, please try again later."
)
test_email = {
'action': 'Send email',
'to_option': 'myself',
'subject': 'test subject for myself',
'message': 'test message for myself'
}
self.client.post(self.url, test_email)
self.assertTrue(retry.called)
(_, kwargs) = retry.call_args
exc = kwargs['exc']
@@ -86,10 +124,17 @@ class TestEmailErrors(ModuleStoreTestCase):
"""
Test that celery handles SMTPConnectError by retrying.
"""
#SMTP reply is already specified in fake SMTP Channel created
# SMTP reply is already specified in fake SMTP Channel created
self.smtp_server_thread.server.set_errtype("CONN")
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
self.client.post(url, {'action': 'Send email', 'to_option': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself'})
test_email = {
'action': 'Send email',
'to_option': 'myself',
'subject': 'test subject for myself',
'message': 'test message for myself'
}
self.client.post(self.url, test_email)
self.assertTrue(retry.called)
(_, kwargs) = retry.call_args
exc = kwargs['exc']

View File

@@ -26,18 +26,23 @@ class TestInstructorDashboardEmailView(ModuleStoreTestCase):
instructor = AdminFactory.create()
self.client.login(username=instructor.username, password="test")
# URL for instructor dash
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
# URL for email view
self.email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True})
def test_email_flag_true(self):
response = self.client.get(reverse('instructor_dashboard',
kwargs={'course_id': self.course.id}))
email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
self.assertTrue(email_link in response.content)
response = self.client.get(self.url)
self.assertTrue(self.email_link in response.content)
# Select the Email view of the instructor dash
session = self.client.session
session['idash_mode'] = 'Email'
session.save()
response = self.client.get(reverse('instructor_dashboard',
kwargs={'course_id': self.course.id}))
response = self.client.get(self.url)
# Ensure we've selected the view properly and that the send_to field is present.
selected_email_link = '<a href="#" onclick="goto(\'Email\')" class="selectedmode">Email</a>'
self.assertTrue(selected_email_link in response.content)
send_to_label = '<label for="id_to">Send to:</label>'
@@ -45,7 +50,5 @@ class TestInstructorDashboardEmailView(ModuleStoreTestCase):
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False})
def test_email_flag_false(self):
response = self.client.get(reverse('instructor_dashboard',
kwargs={'course_id': self.course.id}))
email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
self.assertFalse(email_link in response.content)
response = self.client.get(self.url)
self.assertFalse(self.email_link in response.content)

View File

@@ -83,6 +83,7 @@ def instructor_dashboard(request, course_id):
forum_admin_access = has_forum_access(request.user, course_id, FORUM_ROLE_ADMINISTRATOR)
msg = ''
email_msg = ''
to_option = None
subject = None
html_message = ''
@@ -717,10 +718,9 @@ def instructor_dashboard(request, course_id):
tasks.delegate_email_batches.delay(email.hash, email.to, course_id, course_url, request.user.id)
if to_option == "all":
msg = "<font color='green'>Your email was successfully queued for sending. Please note that for large public classe\
s (~10k), it may take 1-2 hours to send all emails.</font>"
email_msg = '<div class="msg msg-confirm"><p class="copy">Your email was successfully queued for sending. Please note that for large public classes (~10k), it may take 1-2 hours to send all emails.</p></div>'
else:
msg = "<font color='green'>Your email was successfully queued for sending.</font>"
email_msg = '<div class="msg msg-confirm"><p class="copy">Your email was successfully queued for sending.</p></div>'
#----------------------------------------
# psychometrics
@@ -809,6 +809,7 @@ s (~10k), it may take 1-2 hours to send all emails.</font>"
'datatable': datatable,
'course_stats': course_stats,
'msg': msg,
'email_msg': email_msg,
'modeflag': {idash_mode: 'selectedmode'},
'to_option': to_option, # email
'subject': subject, # email