Skip to content

Commit

Permalink
Merge pull request #6082 from openfun/edx/translate-enrollment-emails
Browse files Browse the repository at this point in the history
Render enrollment emails in the student's language
  • Loading branch information
sarina committed Jan 21, 2015
2 parents 1eaf84a + f3419bb commit e70a5e2
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 15 deletions.
3 changes: 2 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -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]>
60 changes: 50 additions & 10 deletions lms/djangoapps/instructor/enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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)

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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`),
Expand All @@ -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.
"""

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
84 changes: 84 additions & 0 deletions lms/djangoapps/instructor/tests/test_api_email_localization.py
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")
53 changes: 52 additions & 1 deletion lms/djangoapps/instructor/tests/test_enrollment.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""
Unit tests for instructor.enrollment methods.
"""
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)
13 changes: 10 additions & 3 deletions lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@
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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down

0 comments on commit e70a5e2

Please sign in to comment.