Commit e70a5e26 by Sarina Canelake

Merge pull request #6082 from openfun/edx/translate-enrollment-emails

Render enrollment emails in the student's language
parents 1eaf84a4 f3419bb5
......@@ -188,4 +188,5 @@ Wenjie Wu <wuwenjie718@gmail.com>
Aamir <aamir.nu.206@gmail.com>
Steve Jackson <sjackso@ixoreus.net>
Steffan Sluis <steffansluis@gmail.com>
Siem Kok <siem@feedbackfruits.com>
\ No newline at end of file
Siem Kok <siem@feedbackfruits.com>
Régis Behmo <regis.behmo@openfun.fr>
......@@ -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
......
# -*- 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")
# -*- 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)
......@@ -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)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment