diff --git a/cms/templates/js/mock/mock-settings-page.underscore b/cms/templates/js/mock/mock-settings-page.underscore
index 064e80ac27..c03fb697a2 100644
--- a/cms/templates/js/mock/mock-settings-page.underscore
+++ b/cms/templates/js/mock/mock-settings-page.underscore
@@ -103,7 +103,7 @@
- Identify the course language here. This is used to assist users find courses that are taught in a specific language.
+ Identify the course language here. This is used to assist users find courses that are taught in a specific language. It is also used to localize the 'From:' field in bulk emails.
diff --git a/cms/templates/settings.html b/cms/templates/settings.html
index 1663a12476..2a5c51b887 100644
--- a/cms/templates/settings.html
+++ b/cms/templates/settings.html
@@ -284,7 +284,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
% endfor
- ${_("Identify the course language here. This is used to assist users find courses that are taught in a specific language.")}
+ ${_("Identify the course language here. This is used to assist users find courses that are taught in a specific language. It is also used to localize the 'From:' field in bulk emails.")}
diff --git a/lms/djangoapps/bulk_email/admin.py b/lms/djangoapps/bulk_email/admin.py
index e42f7f535f..ac6937a7da 100644
--- a/lms/djangoapps/bulk_email/admin.py
+++ b/lms/djangoapps/bulk_email/admin.py
@@ -37,6 +37,7 @@ Other tags that may be used (surrounded by one curly brace on each side):
{platform_name} : the name of the platform
{course_title} : the name of the course
{course_root} : the URL path to the root of the course
+{course_language} : the course language. The default is None.
{course_url} : the course's full URL
{email} : the user's email address
{account_settings_url} : URL at which users can change account preferences
diff --git a/lms/djangoapps/bulk_email/tasks.py b/lms/djangoapps/bulk_email/tasks.py
index e6dca1f895..f64da3e29c 100644
--- a/lms/djangoapps/bulk_email/tasks.py
+++ b/lms/djangoapps/bulk_email/tasks.py
@@ -35,6 +35,7 @@ from django.contrib.auth.models import User
from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.mail.message import forbid_multi_line_headers
from django.core.urlresolvers import reverse
+from django.utils.translation import override as override_language, ugettext as _
from bulk_email.models import CourseEmail, Optout
from courseware.courses import get_course
@@ -109,6 +110,7 @@ def _get_course_email_context(course):
email_context = {
'course_title': course_title,
'course_root': course_root,
+ 'course_language': course.language,
'course_url': course_url,
'course_image_url': image_url,
'course_end_date': course_end_date,
@@ -350,7 +352,7 @@ def _filter_optouts_from_recipients(to_list, course_id):
return to_list, num_optout
-def _get_source_address(course_id, course_title, truncate=True):
+def _get_source_address(course_id, course_title, course_language, truncate=True):
"""
Calculates an email address to be used as the 'from-address' for sent emails.
@@ -373,7 +375,17 @@ def _get_source_address(course_id, course_title, truncate=True):
# character appears.
course_name = re.sub(r"[^\w.-]", '_', course_id.course)
- from_addr_format = u'"{course_title}" Course Staff <{course_name}-{from_email}>'
+ # Use course.language if present
+ language = course_language if course_language else settings.LANGUAGE_CODE
+ with override_language(language):
+ # RFC2821 requires the byte order of the email address to be the name then email
+ # e.g. "John Doe "
+ # Although the display will be flipped in RTL languages, the byte order is still the same.
+ from_addr_format = u'{name} {email}'.format(
+ # Translators: Bulk email from address e.g. ("Physics 101" Course Staff)
+ name=_('"{course_title}" Course Staff'),
+ email=u'<{course_name}-{from_email}>',
+ )
def format_address(course_title_no_quotes):
"""
@@ -475,10 +487,11 @@ def _send_course_email(entry_id, email_id, to_list, global_email_context, subtas
subtask_status.increment(skipped=num_optout)
course_title = global_email_context['course_title']
+ course_language = global_email_context['course_language']
# use the email from address in the CourseEmail, if it is present, otherwise compute it
from_addr = course_email.from_addr if course_email.from_addr else \
- _get_source_address(course_email.course_id, course_title)
+ _get_source_address(course_email.course_id, course_title, course_language)
# use the CourseEmailTemplate that was associated with the CourseEmail
course_email_template = course_email.get_template()
diff --git a/lms/djangoapps/bulk_email/tests/test_email.py b/lms/djangoapps/bulk_email/tests/test_email.py
index 1834eedcb4..6d2f5680b1 100644
--- a/lms/djangoapps/bulk_email/tests/test_email.py
+++ b/lms/djangoapps/bulk_email/tests/test_email.py
@@ -8,6 +8,7 @@ from mock import patch, Mock
from nose.plugins.attrib import attr
import os
from unittest import skipIf
+import ddt
from django.conf import settings
from django.core import mail
@@ -15,6 +16,7 @@ from django.core.mail.message import forbid_multi_line_headers
from django.core.urlresolvers import reverse
from django.core.management import call_command
from django.test.utils import override_settings
+from django.utils.translation import get_language
from bulk_email.models import Optout, BulkEmailFlag
from bulk_email.tasks import _get_source_address, _get_course_email_context
@@ -132,6 +134,99 @@ class EmailSendFromDashboardTestCase(SharedModuleStoreTestCase):
BulkEmailFlag.objects.all().delete()
+class SendEmailWithMockedUgettextMixin(object):
+ """
+ Mock uggetext for EmailSendFromDashboardTestCase.
+ """
+ def send_email(self):
+ """
+ Sends a dummy email to check the `from_addr` translation.
+ """
+ test_email = {
+ 'action': 'send',
+ 'send_to': '["myself"]',
+ 'subject': 'test subject for myself',
+ 'message': 'test message for myself'
+ }
+
+ def mock_ugettext(text):
+ """
+ Mocks ugettext to return the lang code with the original string.
+
+ e.g.
+
+ >>> mock_ugettext('Hello') == '@AR Hello@'
+ """
+ return u'@{lang} {text}@'.format(
+ lang=get_language().upper(),
+ text=text,
+ )
+
+ with patch('bulk_email.tasks._', side_effect=mock_ugettext):
+ self.client.post(self.send_mail_url, test_email)
+
+ return mail.outbox[0]
+
+
+@attr(shard=1)
+@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
+@ddt.ddt
+class LocalizedFromAddressPlatformLangTestCase(SendEmailWithMockedUgettextMixin, EmailSendFromDashboardTestCase):
+ """
+ Tests to ensure that the bulk email has the "From" address localized according to LANGUAGE_CODE.
+ """
+ @override_settings(LANGUAGE_CODE='en')
+ def test_english_platform(self):
+ """
+ Ensures that the source-code language (English) works well.
+ """
+ self.assertIsNone(self.course.language) # Sanity check
+ message = self.send_email()
+ self.assertRegexpMatches(message.from_email, '.*Course Staff.*')
+
+ @override_settings(LANGUAGE_CODE='eo')
+ def test_esperanto_platform(self):
+ """
+ Tests the fake Esperanto language to ensure proper gettext calls.
+ """
+ self.assertIsNone(self.course.language) # Sanity check
+ message = self.send_email()
+ self.assertRegexpMatches(message.from_email, '@EO .* Course Staff@')
+
+
+@attr(shard=1)
+@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
+@ddt.ddt
+class LocalizedFromAddressCourseLangTestCase(SendEmailWithMockedUgettextMixin, EmailSendFromDashboardTestCase):
+ """
+ Test if the bulk email "From" address uses the course.language if present instead of LANGUAGE_CODE.
+
+ This is similiar to LocalizedFromAddressTestCase but creating a different test case to allow
+ changing the class-wide course object.
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ """
+ Creates a different course.
+ """
+ super(LocalizedFromAddressCourseLangTestCase, cls).setUpClass()
+ course_title = u"ẗëṡẗ イэ"
+ cls.course = CourseFactory.create(
+ display_name=course_title,
+ language='ar',
+ default_store=ModuleStoreEnum.Type.split
+ )
+
+ @override_settings(LANGUAGE_CODE='eo')
+ def test_esperanto_platform_arabic_course(self):
+ """
+ The course language should override the platform's.
+ """
+ message = self.send_email()
+ self.assertRegexpMatches(message.from_email, '@AR .* Course Staff@')
+
+
@attr(shard=1)
@patch('bulk_email.models.html_to_text', Mock(return_value='Mocking CourseEmail.text_message', autospec=True))
class TestEmailSendFromDashboardMockedHtmlToText(EmailSendFromDashboardTestCase):
@@ -394,7 +489,7 @@ class TestEmailSendFromDashboardMockedHtmlToText(EmailSendFromDashboardTestCase)
instructor = InstructorFactory(course_key=course.id)
unexpected_from_addr = _get_source_address(
- course.id, course.display_name, truncate=False
+ course.id, course.display_name, course_language=None, truncate=False
)
__, encoded_unexpected_from_addr = forbid_multi_line_headers(
"from", unexpected_from_addr, 'utf-8'
diff --git a/lms/djangoapps/bulk_email/tests/test_tasks.py b/lms/djangoapps/bulk_email/tests/test_tasks.py
index c5eca9edd8..f01d85eb33 100644
--- a/lms/djangoapps/bulk_email/tests/test_tasks.py
+++ b/lms/djangoapps/bulk_email/tests/test_tasks.py
@@ -440,6 +440,7 @@ class TestBulkEmailInstructorTask(InstructorTaskCourseTestCase):
result = _get_course_email_context(self.course)
self.assertIn('course_title', result)
self.assertIn('course_root', result)
+ self.assertIn('course_language', result)
self.assertIn('course_url', result)
self.assertIn('course_image_url', result)
self.assertIn('course_end_date', result)