Render enrollment emails in the student's language
Enrollment, unenrollment and beta role emails should be rendered in the student's language, and not the instructor's language.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user