-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6082 from openfun/edx/translate-enrollment-emails
Render enrollment emails in the student's language
- Loading branch information
Showing
5 changed files
with
198 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -188,4 +188,5 @@ Wenjie Wu <[email protected]> | |
Aamir <[email protected]> | ||
Steve Jackson <[email protected]> | ||
Steffan Sluis <[email protected]> | ||
Siem Kok <[email protected]> | ||
Siem Kok <[email protected]> | ||
Régis Behmo <[email protected]> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,13 +9,16 @@ | |
from django.conf import settings | ||
from django.core.urlresolvers import reverse | ||
from django.core.mail import send_mail | ||
from django.utils.translation import override as override_language | ||
|
||
from student.models import CourseEnrollment, CourseEnrollmentAllowed | ||
from courseware.models import StudentModule | ||
from edxmako.shortcuts import render_to_string | ||
from lang_pref import LANGUAGE_KEY | ||
|
||
from submissions import api as sub_api # installed from the edx-submissions repository | ||
from student.models import anonymous_id_for_user | ||
from openedx.core.djangoapps.user_api.models import UserPreference | ||
|
||
from microsite_configuration import microsite | ||
|
||
|
@@ -71,7 +74,15 @@ def to_dict(self): | |
} | ||
|
||
|
||
def enroll_email(course_id, student_email, auto_enroll=False, email_students=False, email_params=None): | ||
def get_user_email_language(user): | ||
""" | ||
Return the language most appropriate for writing emails to user. Returns | ||
None if the preference has not been set, or if the user does not exist. | ||
""" | ||
return UserPreference.get_preference(user, LANGUAGE_KEY) | ||
|
||
|
||
def enroll_email(course_id, student_email, auto_enroll=False, email_students=False, email_params=None, language=None): | ||
""" | ||
Enroll a student by email. | ||
|
@@ -81,6 +92,7 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal | |
enrolled in the course automatically. | ||
`email_students` determines if student should be notified of action by email. | ||
`email_params` parameters used while parsing email templates (a `dict`). | ||
`language` is the language used to render the email. | ||
returns two EmailEnrollmentState's | ||
representing state before and after the action. | ||
|
@@ -99,28 +111,29 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal | |
email_params['message'] = 'enrolled_enroll' | ||
email_params['email_address'] = student_email | ||
email_params['full_name'] = previous_state.full_name | ||
send_mail_to_student(student_email, email_params) | ||
send_mail_to_student(student_email, email_params, language=language) | ||
else: | ||
cea, _ = CourseEnrollmentAllowed.objects.get_or_create(course_id=course_id, email=student_email) | ||
cea.auto_enroll = auto_enroll | ||
cea.save() | ||
if email_students: | ||
email_params['message'] = 'allowed_enroll' | ||
email_params['email_address'] = student_email | ||
send_mail_to_student(student_email, email_params) | ||
send_mail_to_student(student_email, email_params, language=language) | ||
|
||
after_state = EmailEnrollmentState(course_id, student_email) | ||
|
||
return previous_state, after_state | ||
|
||
|
||
def unenroll_email(course_id, student_email, email_students=False, email_params=None): | ||
def unenroll_email(course_id, student_email, email_students=False, email_params=None, language=None): | ||
""" | ||
Unenroll a student by email. | ||
`student_email` is student's emails e.g. "[email protected]" | ||
`email_students` determines if student should be notified of action by email. | ||
`email_params` parameters used while parsing email templates (a `dict`). | ||
`language` is the language used to render the email. | ||
returns two EmailEnrollmentState's | ||
representing state before and after the action. | ||
|
@@ -133,15 +146,15 @@ def unenroll_email(course_id, student_email, email_students=False, email_params= | |
email_params['message'] = 'enrolled_unenroll' | ||
email_params['email_address'] = student_email | ||
email_params['full_name'] = previous_state.full_name | ||
send_mail_to_student(student_email, email_params) | ||
send_mail_to_student(student_email, email_params, language=language) | ||
|
||
if previous_state.allowed: | ||
CourseEnrollmentAllowed.objects.get(course_id=course_id, email=student_email).delete() | ||
if email_students: | ||
email_params['message'] = 'allowed_unenroll' | ||
email_params['email_address'] = student_email | ||
# Since no User object exists for this student there is no "full_name" available. | ||
send_mail_to_student(student_email, email_params) | ||
send_mail_to_student(student_email, email_params, language=language) | ||
|
||
after_state = EmailEnrollmentState(course_id, student_email) | ||
|
||
|
@@ -169,7 +182,7 @@ def send_beta_role_email(action, user, email_params): | |
else: | ||
raise ValueError("Unexpected action received '{}' - expected 'add' or 'remove'".format(action)) | ||
|
||
send_mail_to_student(user.email, email_params) | ||
send_mail_to_student(user.email, email_params, language=get_user_email_language(user)) | ||
|
||
|
||
def reset_student_attempts(course_id, student, module_state_key, delete_module=False): | ||
|
@@ -279,7 +292,7 @@ def get_email_params(course, auto_enroll, secure=True): | |
return email_params | ||
|
||
|
||
def send_mail_to_student(student, param_dict): | ||
def send_mail_to_student(student, param_dict, language=None): | ||
""" | ||
Construct the email using templates and then send it. | ||
`student` is the student's email address (a `str`), | ||
|
@@ -297,6 +310,10 @@ def send_mail_to_student(student, param_dict): | |
`is_shib_course`: (a `boolean`) | ||
] | ||
`language` is the language used to render the email. If None the language | ||
of the currently-logged in user (that is, the user sending the email) will | ||
be used. | ||
Returns a boolean indicating whether the email was sent successfully. | ||
""" | ||
|
||
|
@@ -349,8 +366,9 @@ def send_mail_to_student(student, param_dict): | |
|
||
subject_template, message_template = email_template_dict.get(message_type, (None, None)) | ||
if subject_template is not None and message_template is not None: | ||
subject = render_to_string(subject_template, param_dict) | ||
message = render_to_string(message_template, param_dict) | ||
subject, message = render_message_to_string( | ||
subject_template, message_template, param_dict, language=language | ||
) | ||
|
||
if subject and message: | ||
# Remove leading and trailing whitespace from body | ||
|
@@ -366,6 +384,28 @@ def send_mail_to_student(student, param_dict): | |
send_mail(subject, message, from_address, [student], fail_silently=False) | ||
|
||
|
||
def render_message_to_string(subject_template, message_template, param_dict, language=None): | ||
""" | ||
Render a mail subject and message templates using the parameters from | ||
param_dict and the given language. If language is None, the platform | ||
default language is used. | ||
Returns two strings that correspond to the rendered, translated email | ||
subject and message. | ||
""" | ||
with override_language(language): | ||
return get_subject_and_message(subject_template, message_template, param_dict) | ||
|
||
|
||
def get_subject_and_message(subject_template, message_template, param_dict): | ||
""" | ||
Return the rendered subject and message with the appropriate parameters. | ||
""" | ||
subject = render_to_string(subject_template, param_dict) | ||
message = render_to_string(message_template, param_dict) | ||
return subject, message | ||
|
||
|
||
def uses_shib(course): | ||
""" | ||
Used to return whether course has Shibboleth as the enrollment domain | ||
|
84 changes: 84 additions & 0 deletions
84
lms/djangoapps/instructor/tests/test_api_email_localization.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
Unit tests for the localization of emails sent by instructor.api methods. | ||
""" | ||
|
||
from django.core import mail | ||
from django.core.urlresolvers import reverse | ||
from django.test import TestCase | ||
|
||
from courseware.tests.factories import InstructorFactory | ||
from lang_pref import LANGUAGE_KEY | ||
from student.models import CourseEnrollment | ||
from student.tests.factories import UserFactory | ||
from openedx.core.djangoapps.user_api.models import UserPreference | ||
from xmodule.modulestore.tests.factories import CourseFactory | ||
|
||
|
||
class TestInstructorAPIEnrollmentEmailLocalization(TestCase): | ||
""" | ||
Test whether the enroll, unenroll and beta role emails are sent in the | ||
proper language, i.e: the student's language. | ||
""" | ||
|
||
def setUp(self): | ||
# Platform language is English, instructor's language is Chinese, | ||
# student's language is French, so the emails should all be sent in | ||
# French. | ||
self.course = CourseFactory.create() | ||
self.instructor = InstructorFactory(course_key=self.course.id) | ||
UserPreference.set_preference(self.instructor, LANGUAGE_KEY, 'zh-cn') | ||
self.client.login(username=self.instructor.username, password='test') | ||
|
||
self.student = UserFactory.create() | ||
UserPreference.set_preference(self.student, LANGUAGE_KEY, 'fr') | ||
|
||
def update_enrollement(self, action, student_email): | ||
""" | ||
Update the current student enrollment status. | ||
""" | ||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) | ||
args = {'identifiers': student_email, 'email_students': 'true', 'action': action} | ||
response = self.client.post(url, args) | ||
return response | ||
|
||
def check_outbox_is_french(self): | ||
""" | ||
Check that the email outbox contains exactly one message for which both | ||
the message subject and body contain a certain French string. | ||
""" | ||
return self.check_outbox(u"Vous avez été") | ||
|
||
def check_outbox(self, expected_message): | ||
""" | ||
Check that the email outbox contains exactly one message for which both | ||
the message subject and body contain a certain string. | ||
""" | ||
self.assertEqual(1, len(mail.outbox)) | ||
self.assertIn(expected_message, mail.outbox[0].subject) | ||
self.assertIn(expected_message, mail.outbox[0].body) | ||
|
||
def test_enroll(self): | ||
self.update_enrollement("enroll", self.student.email) | ||
|
||
self.check_outbox_is_french() | ||
|
||
def test_unenroll(self): | ||
CourseEnrollment.enroll( | ||
self.student, | ||
self.course.id | ||
) | ||
self.update_enrollement("unenroll", self.student.email) | ||
|
||
self.check_outbox_is_french() | ||
|
||
def test_set_beta_role(self): | ||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) | ||
self.client.post(url, {'identifiers': self.student.email, 'action': 'add', 'email_students': 'true'}) | ||
|
||
self.check_outbox_is_french() | ||
|
||
def test_enroll_unsubscribed_student(self): | ||
# Student is unknown, so the platform language should be used | ||
self.update_enrollement("enroll", "[email protected]") | ||
self.check_outbox("You have been") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
Unit tests for instructor.enrollment methods. | ||
""" | ||
|
@@ -9,6 +10,8 @@ | |
from django.conf import settings | ||
from django.test import TestCase | ||
from django.test.utils import override_settings | ||
from django.utils.translation import get_language | ||
from django.utils.translation import override as override_language | ||
from student.tests.factories import UserFactory | ||
from xmodule.modulestore.tests.factories import CourseFactory | ||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE | ||
|
@@ -20,7 +23,8 @@ | |
get_email_params, | ||
reset_student_attempts, | ||
send_beta_role_email, | ||
unenroll_email | ||
unenroll_email, | ||
render_message_to_string, | ||
) | ||
from opaque_keys.edx.locations import SlashSeparatedCourseKey | ||
|
||
|
@@ -472,3 +476,50 @@ def test_marketing_params(self): | |
self.assertEqual(result['course_about_url'], None) | ||
self.assertEqual(result['registration_url'], self.registration_url) | ||
self.assertEqual(result['course_url'], self.course_url) | ||
|
||
|
||
class TestRenderMessageToString(TestCase): | ||
""" | ||
Test that email templates can be rendered in a language chosen manually. | ||
""" | ||
|
||
def setUp(self): | ||
self.subject_template = 'emails/enroll_email_allowedsubject.txt' | ||
self.message_template = 'emails/enroll_email_allowedmessage.txt' | ||
self.course = CourseFactory.create() | ||
|
||
def get_email_params(self): | ||
""" | ||
Returns a dictionary of parameters used to render an email. | ||
""" | ||
email_params = get_email_params(self.course, True) | ||
email_params["email_address"] = "[email protected]" | ||
email_params["full_name"] = "Jean Reno" | ||
|
||
return email_params | ||
|
||
def get_subject_and_message(self, language): | ||
""" | ||
Returns the subject and message rendered in the specified language. | ||
""" | ||
return render_message_to_string( | ||
self.subject_template, | ||
self.message_template, | ||
self.get_email_params(), | ||
language=language | ||
) | ||
|
||
def test_subject_and_message_translation(self): | ||
subject, message = self.get_subject_and_message('fr') | ||
language_after_rendering = get_language() | ||
|
||
you_have_been_invited_in_french = u"Vous avez été invité" | ||
self.assertIn(you_have_been_invited_in_french, subject) | ||
self.assertIn(you_have_been_invited_in_french, message) | ||
self.assertEqual(settings.LANGUAGE_CODE, language_after_rendering) | ||
|
||
def test_platform_language_is_used_for_logged_in_user(self): | ||
with override_language('zh_CN'): # simulate a user login | ||
subject, message = self.get_subject_and_message(None) | ||
self.assertIn("You have been", subject) | ||
self.assertIn("You have been", message) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters