diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py index ca35291..da7874e 100644 --- a/lms/djangoapps/instructor/enrollment.py +++ b/lms/djangoapps/instructor/enrollment.py @@ -9,13 +9,16 @@ from django.contrib.auth.models import User 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 @@ class EmailEnrollmentState(object): } -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,7 +111,7 @@ 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 @@ -107,20 +119,21 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal 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. "foo@bar.com" `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,7 +146,7 @@ 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() @@ -141,7 +154,7 @@ def unenroll_email(course_id, student_email, email_students=False, email_params= 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 diff --git a/lms/djangoapps/instructor/tests/test_api_email_localization.py b/lms/djangoapps/instructor/tests/test_api_email_localization.py new file mode 100644 index 0000000..3c695cb --- /dev/null +++ b/lms/djangoapps/instructor/tests/test_api_email_localization.py @@ -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", "newuser@hotmail.com") + self.check_outbox("You have been") diff --git a/lms/djangoapps/instructor/tests/test_enrollment.py b/lms/djangoapps/instructor/tests/test_enrollment.py index e120162..ffb8041 100644 --- a/lms/djangoapps/instructor/tests/test_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_enrollment.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Unit tests for instructor.enrollment methods. """ @@ -9,6 +10,8 @@ from courseware.models import StudentModule 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 @@ from instructor.enrollment import ( 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 @@ class TestGetEmailParams(ModuleStoreTestCase): 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"] = "user@example.com" + 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) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 7738cfc..e50d45d 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -57,11 +57,12 @@ from instructor_task.api_helper import AlreadyRunningError from instructor_task.models import ReportStore import instructor.enrollment as enrollment from instructor.enrollment import ( + get_user_email_language, enroll_email, send_mail_to_student, get_email_params, send_beta_role_email, - unenroll_email + unenroll_email, ) from instructor.access import list_with_level, allow_access, revoke_access, update_forum_role from instructor.offline_gradecalc import student_grades @@ -497,12 +498,14 @@ def students_update_enrollment(request, course_id): # First try to get a user object from the identifer user = None email = None + language = None try: user = get_student_from_identifier(identifier) except User.DoesNotExist: email = identifier else: email = user.email + language = get_user_email_language(user) try: # Use django.core.validators.validate_email to check email address @@ -511,9 +514,13 @@ def students_update_enrollment(request, course_id): validate_email(email) # Raises ValidationError if invalid if action == 'enroll': - before, after = enroll_email(course_id, email, auto_enroll, email_students, email_params) + before, after = enroll_email( + course_id, email, auto_enroll, email_students, email_params, language=language + ) elif action == 'unenroll': - before, after = unenroll_email(course_id, email, email_students, email_params) + before, after = unenroll_email( + course_id, email, email_students, email_params, language=language + ) else: return HttpResponseBadRequest(strip_tags( "Unrecognized action '{}'".format(action)