Bulk Email: Add design styling
Switch to using decorators; refactor and cleanup tests.
This commit is contained in:
@@ -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']
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user