diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 4fea30a5c5..8481f3a707 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -152,3 +152,5 @@ Common: Updated CodeJail.
Common: Allow setting of authentication session cookie name.
+LMS: Option to email students when enroll/un-enroll them.
+
diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py
index decce42368..2ed78aaa9f 100644
--- a/common/djangoapps/terrain/factories.py
+++ b/common/djangoapps/terrain/factories.py
@@ -44,7 +44,7 @@ class GroupFactory(sf.GroupFactory):
@world.absorb
-class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowed):
+class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowedFactory):
"""
Users allowed to enroll in the course outside of the usual window
"""
diff --git a/lms/djangoapps/instructor/tests/test_enrollment.py b/lms/djangoapps/instructor/tests/test_enrollment.py
index 3ce82b700b..3b5bdc2ce9 100644
--- a/lms/djangoapps/instructor/tests/test_enrollment.py
+++ b/lms/djangoapps/instructor/tests/test_enrollment.py
@@ -1,177 +1,256 @@
-'''
+"""
Unit tests for enrollment methods in views.py
-'''
+"""
from django.test.utils import override_settings
-from django.contrib.auth.models import Group, User
+from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from courseware.access import _course_staff_group_name
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.tests.factories import CourseFactory
+from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory
+from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
+from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE, LoginEnrollmentTestCase
from student.models import CourseEnrollment, CourseEnrollmentAllowed
-from instructor.views import get_and_clean_student_list
+from instructor.views import get_and_clean_student_list, send_mail_to_student
+from django.core import mail
+
+USER_COUNT = 4
-@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
-class TestInstructorEnrollsStudent(LoginEnrollmentTestCase):
- '''
- Check Enrollment/Unenrollment with/without auto-enrollment on activation
- '''
+@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
+class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase):
+ """
+ Check Enrollment/Unenrollment with/without auto-enrollment on activation and with/without email notification
+ """
def setUp(self):
- self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
- self.toy = modulestore().get_course("edX/toy/2012_Fall")
+ instructor = AdminFactory.create()
+ self.client.login(username=instructor.username, password='test')
- #Create instructor and student accounts
- self.instructor = 'instructor1@test.com'
- self.student1 = 'student1@test.com'
- self.student2 = 'student2@test.com'
- self.password = 'foo'
- self.create_account('it1', self.instructor, self.password)
- self.create_account('st1', self.student1, self.password)
- self.create_account('st2', self.student2, self.password)
- self.activate_user(self.instructor)
- self.activate_user(self.student1)
- self.activate_user(self.student2)
+ self.course = CourseFactory.create()
- def make_instructor(course):
- group_name = _course_staff_group_name(course.location)
- g = Group.objects.create(name=group_name)
- g.user_set.add(get_user(self.instructor))
+ self.users = [
+ UserFactory.create(username="student%d" % i, email="student%d@test.com" % i)
+ for i in xrange(USER_COUNT)
+ ]
- make_instructor(self.toy)
+ for user in self.users:
+ CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
- #Enroll Students
- self.logout()
- self.login(self.student1, self.password)
- self.enroll(self.toy)
+ # Empty the test outbox
+ mail.outbox = []
- self.logout()
- self.login(self.student2, self.password)
- self.enroll(self.toy)
+ def test_unenrollment_email_off(self):
+ """
+ Do un-enrollment email off test
+ """
- #Enroll Instructor
- self.logout()
- self.login(self.instructor, self.password)
- self.enroll(self.toy)
+ course = self.course
- def test_unenrollment(self):
- '''
- Do un-enrollment test
- '''
-
- course = self.toy
+ #Run the Un-enroll students command
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
- response = self.client.post(url, {'action': 'Unenroll multiple students', 'multiple_students': 'student1@test.com, student2@test.com'})
+ response = self.client.post(url, {'action': 'Unenroll multiple students', 'multiple_students': 'student0@test.com student1@test.com'})
- #Check the page output
+ #Check the page output
+ self.assertContains(response, '
student0@test.com
')
self.assertContains(response, '
student1@test.com
')
- self.assertContains(response, '
student2@test.com
')
self.assertContains(response, '
un-enrolled
')
#Check the enrollment table
+ user = User.objects.get(email='student0@test.com')
+ ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
+ self.assertEqual(0, len(ce))
+
user = User.objects.get(email='student1@test.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
- user = User.objects.get(email='student2@test.com')
- ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
- self.assertEqual(0, len(ce))
+ #Check the outbox
+ self.assertEqual(len(mail.outbox), 0)
- def test_enrollment_new_student_autoenroll_on(self):
- '''
- Do auto-enroll on test
- '''
+ def test_enrollment_new_student_autoenroll_on_email_off(self):
+ """
+ Do auto-enroll on, email off test
+ """
+
+ course = self.course
#Run the Enroll students command
- course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
- response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'test1_1@student.com, test1_2@student.com', 'auto_enroll': 'on'})
+ response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student1_1@test.com, student1_2@test.com', 'auto_enroll': 'on'})
#Check the page output
- self.assertContains(response, '
test1_1@student.com
')
- self.assertContains(response, '
test1_2@student.com
')
+ self.assertContains(response, '
student1_1@test.com
')
+ self.assertContains(response, '
student1_2@test.com
')
self.assertContains(response, '
user does not exist, enrollment allowed, pending with auto enrollment on
')
+ #Check the outbox
+ self.assertEqual(len(mail.outbox), 0)
+
#Check the enrollmentallowed db entries
- cea = CourseEnrollmentAllowed.objects.filter(email='test1_1@student.com', course_id=course.id)
+ cea = CourseEnrollmentAllowed.objects.filter(email='student1_1@test.com', course_id=course.id)
self.assertEqual(1, cea[0].auto_enroll)
- cea = CourseEnrollmentAllowed.objects.filter(email='test1_2@student.com', course_id=course.id)
+ cea = CourseEnrollmentAllowed.objects.filter(email='student1_2@test.com', course_id=course.id)
self.assertEqual(1, cea[0].auto_enroll)
- #Check there is no enrollment db entry other than for the setup instructor and students
+ #Check there is no enrollment db entry other than for the other students
ce = CourseEnrollment.objects.filter(course_id=course.id)
- self.assertEqual(3, len(ce))
+ self.assertEqual(4, len(ce))
#Create and activate student accounts with same email
- self.student1 = 'test1_1@student.com'
+ self.student1 = 'student1_1@test.com'
self.password = 'bar'
self.create_account('s1_1', self.student1, self.password)
self.activate_user(self.student1)
- self.student2 = 'test1_2@student.com'
+ self.student2 = 'student1_2@test.com'
self.create_account('s1_2', self.student2, self.password)
self.activate_user(self.student2)
#Check students are enrolled
- user = User.objects.get(email='test1_1@student.com')
+ user = User.objects.get(email='student1_1@test.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(1, len(ce))
- user = User.objects.get(email='test1_2@student.com')
+ user = User.objects.get(email='student1_2@test.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(1, len(ce))
- def test_enrollmemt_new_student_autoenroll_off(self):
- '''
- Do auto-enroll off test
- '''
+ def test_repeat_enroll(self):
+ """
+ Try to enroll an already enrolled student
+ """
+
+ course = self.course
+
+ url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
+ response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student0@test.com', 'auto_enroll': 'on'})
+ self.assertContains(response, '
user does not exist, enrollment allowed, pending with auto enrollment off
')
+ #Check the outbox
+ self.assertEqual(len(mail.outbox), 0)
+
#Check the enrollmentallowed db entries
- cea = CourseEnrollmentAllowed.objects.filter(email='test2_1@student.com', course_id=course.id)
+ cea = CourseEnrollmentAllowed.objects.filter(email='student2_1@test.com', course_id=course.id)
self.assertEqual(0, cea[0].auto_enroll)
- cea = CourseEnrollmentAllowed.objects.filter(email='test2_2@student.com', course_id=course.id)
+ cea = CourseEnrollmentAllowed.objects.filter(email='student2_2@test.com', course_id=course.id)
self.assertEqual(0, cea[0].auto_enroll)
#Check there is no enrollment db entry other than for the setup instructor and students
ce = CourseEnrollment.objects.filter(course_id=course.id)
- self.assertEqual(3, len(ce))
+ self.assertEqual(4, len(ce))
#Create and activate student accounts with same email
- self.student = 'test2_1@student.com'
+ self.student = 'student2_1@test.com'
self.password = 'bar'
self.create_account('s2_1', self.student, self.password)
self.activate_user(self.student)
- self.student = 'test2_2@student.com'
+ self.student = 'student2_2@test.com'
self.create_account('s2_2', self.student, self.password)
self.activate_user(self.student)
#Check students are not enrolled
- user = User.objects.get(email='test2_1@student.com')
+ user = User.objects.get(email='student2_1@test.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
- user = User.objects.get(email='test2_2@student.com')
+ user = User.objects.get(email='student2_2@test.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
def test_get_and_clean_student_list(self):
- '''
+ """
Clean user input test
- '''
+ """
- string = "abc@test.com, def@test.com ghi@test.com \n \n jkl@test.com "
+ string = "abc@test.com, def@test.com ghi@test.com \n \n jkl@test.com \n mno@test.com "
cleaned_string, cleaned_string_lc = get_and_clean_student_list(string)
- self.assertEqual(cleaned_string, ['abc@test.com', 'def@test.com', 'ghi@test.com', 'jkl@test.com'])
+ self.assertEqual(cleaned_string, ['abc@test.com', 'def@test.com', 'ghi@test.com', 'jkl@test.com', 'mno@test.com'])
+
+ def test_enrollment_email_on(self):
+ """
+ Do email on enroll test
+ """
+
+ course = self.course
+
+ #Create activated, but not enrolled, user
+ UserFactory.create(username="student3_0", email="student3_0@test.com")
+
+ url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
+ response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student3_0@test.com, student3_1@test.com, student3_2@test.com', 'auto_enroll': 'on', 'email_students': 'on'})
+
+ #Check the page output
+ self.assertContains(response, '
student3_0@test.com
')
+ self.assertContains(response, '
student3_1@test.com
')
+ self.assertContains(response, '
student3_2@test.com
')
+ self.assertContains(response, '
added, email sent
')
+ self.assertContains(response, '
user does not exist, enrollment allowed, pending with auto enrollment on, email sent
')
+
+ #Check the outbox
+ self.assertEqual(len(mail.outbox), 3)
+ self.assertEqual(mail.outbox[0].subject, 'You have been enrolled in MITx/999/Robot_Super_Course')
+
+ self.assertEqual(mail.outbox[1].subject, 'You have been invited to register for MITx/999/Robot_Super_Course')
+ self.assertEqual(mail.outbox[1].body, "Dear student,\n\nYou have been invited to join MITx/999/Robot_Super_Course at edx.org by a member of the course staff.\n\n" +
+ "To finish your registration, please visit https://edx.org/register and fill out the registration form.\n" +
+ "Once you have registered and activated your account, you will see MITx/999/Robot_Super_Course listed on your dashboard.\n\n" +
+ "----\nThis email was automatically sent from edx.org to student3_1@test.com")
+
+ def test_unenrollment_email_on(self):
+ """
+ Do email on unenroll test
+ """
+
+ course = self.course
+
+ #Create invited, but not registered, user
+ cea = CourseEnrollmentAllowed(email='student4_0@test.com', course_id=course.id)
+ cea.save()
+
+ url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
+ response = self.client.post(url, {'action': 'Unenroll multiple students', 'multiple_students': 'student4_0@test.com, student2@test.com, student3@test.com', 'email_students': 'on'})
+
+ #Check the page output
+ self.assertContains(response, '
student2@test.com
')
+ self.assertContains(response, '
student3@test.com
')
+ self.assertContains(response, '
un-enrolled, email sent
')
+
+ #Check the outbox
+ self.assertEqual(len(mail.outbox), 3)
+ self.assertEqual(mail.outbox[0].subject, 'You have been un-enrolled from MITx/999/Robot_Super_Course')
+ self.assertEqual(mail.outbox[0].body, "Dear Student,\n\nYou have been un-enrolled from course MITx/999/Robot_Super_Course by a member of the course staff. " +
+ "Please disregard the invitation previously sent.\n\n" +
+ "----\nThis email was automatically sent from edx.org to student4_0@test.com")
+ self.assertEqual(mail.outbox[1].subject, 'You have been un-enrolled from MITx/999/Robot_Super_Course')
+
+ def test_send_mail_to_student(self):
+ """
+ Do invalid mail template test
+ """
+
+ d = {'message': 'message_type_that_doesn\'t_exist'}
+
+ send_mail_ret = send_mail_to_student('student0@test.com', d)
+ self.assertFalse(send_mail_ret)
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index e9fff63698..ea96901bca 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -20,6 +20,8 @@ from django.http import HttpResponse
from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.cache import cache_control
from django.core.urlresolvers import reverse
+from django.core.mail import send_mail
+
import xmodule.graders as xmgraders
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
@@ -45,6 +47,7 @@ from mitxmako.shortcuts import render_to_response
from psychometrics import psychoanalyze
from student.models import CourseEnrollment, CourseEnrollmentAllowed
import track.views
+from mitxmako.shortcuts import render_to_string
log = logging.getLogger(__name__)
@@ -634,13 +637,15 @@ def instructor_dashboard(request, course_id):
students = request.POST.get('multiple_students', '')
auto_enroll = bool(request.POST.get('auto_enroll'))
- ret = _do_enroll_students(course, course_id, students, auto_enroll=auto_enroll)
+ email_students = bool(request.POST.get('email_students'))
+ ret = _do_enroll_students(course, course_id, students, auto_enroll=auto_enroll, email_students=email_students)
datatable = ret['datatable']
elif action == 'Unenroll multiple students':
students = request.POST.get('multiple_students', '')
- ret = _do_unenroll_students(course_id, students)
+ email_students = bool(request.POST.get('email_students'))
+ ret = _do_unenroll_students(course_id, students, email_students=email_students)
datatable = ret['datatable']
elif action == 'List sections available in remote gradebook':
@@ -1068,9 +1073,17 @@ def grade_summary(request, course_id):
#-----------------------------------------------------------------------------
# enrollment
-def _do_enroll_students(course, course_id, students, overload=False, auto_enroll=False):
- """Do the actual work of enrolling multiple students, presented as a string
- of emails separated by commas or returns"""
+def _do_enroll_students(course, course_id, students, overload=False, auto_enroll=False, email_students=False):
+ """
+ Do the actual work of enrolling multiple students, presented as a string
+ of emails separated by commas or returns
+ `course` is course object
+ `course_id` id of course (a `str`)
+ `students` string of student emails separated by commas or returns (a `str`)
+ `overload` un-enrolls all existing students (a `boolean`)
+ `auto_enroll` is user input preference (a `boolean`)
+ `email_students` is user input preference (a `boolean`)
+ """
new_students, new_students_lc = get_and_clean_student_list(students)
status = dict([x, 'unprocessed'] for x in new_students)
@@ -1088,12 +1101,22 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll
status[cea.email] = 'removed from pending enrollment list'
ceaset.delete()
+ if email_students:
+ registration_url = 'https://' + settings.SITE_NAME + reverse('student.views.register_user')
+ #Composition of email
+ d = {'site_name': settings.SITE_NAME,
+ 'registration_url': registration_url,
+ 'course_id': course_id,
+ 'auto_enroll': auto_enroll,
+ 'course_url': registration_url + '/courses/' + course_id,
+ }
+
for student in new_students:
try:
user = User.objects.get(email=student)
except User.DoesNotExist:
- #User not signed up yet, put in pending enrollment allowed table
+ #Student not signed up yet, put in pending enrollment allowed table
cea = CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_id)
#If enrollmentallowed already exists, update auto_enroll flag to however it was set in UI
@@ -1104,18 +1127,42 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll
status[student] = 'user does not exist, enrollment already allowed, pending with auto enrollment ' \
+ ('on' if auto_enroll else 'off')
continue
+
+ #EnrollmentAllowed doesn't exist so create it
cea = CourseEnrollmentAllowed(email=student, course_id=course_id, auto_enroll=auto_enroll)
cea.save()
- status[student] = 'user does not exist, enrollment allowed, pending with auto enrollment ' + ('on' if auto_enroll else 'off')
+
+ status[student] = 'user does not exist, enrollment allowed, pending with auto enrollment ' \
+ + ('on' if auto_enroll else 'off')
+
+ if email_students:
+ #User is allowed to enroll but has not signed up yet
+ d['email_address'] = student
+ d['message'] = 'allowed_enroll'
+ send_mail_ret = send_mail_to_student(student, d)
+ status[student] += (', email sent' if send_mail_ret else '')
continue
+ #Student has already registered
if CourseEnrollment.objects.filter(user=user, course_id=course_id):
status[student] = 'already enrolled'
continue
+
try:
+ #Not enrolled yet
ce = CourseEnrollment(user=user, course_id=course_id)
ce.save()
status[student] = 'added'
+
+ if email_students:
+ #User enrolled for first time, populate dict with user specific info
+ d['email_address'] = student
+ d['first_name'] = user.first_name
+ d['last_name'] = user.last_name
+ d['message'] = 'enrolled_enroll'
+ send_mail_ret = send_mail_to_student(student, d)
+ status[student] += (', email sent' if send_mail_ret else '')
+
except:
status[student] = 'rejected'
@@ -1133,13 +1180,23 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll
#Unenrollment
-def _do_unenroll_students(course_id, students):
- """Do the actual work of un-enrolling multiple students, presented as a string
- of emails separated by commas or returns"""
+def _do_unenroll_students(course_id, students, email_students=False):
+ """
+ Do the actual work of un-enrolling multiple students, presented as a string
+ of emails separated by commas or returns
+ `course_id` is id of course (a `str`)
+ `students` is string of student emails separated by commas or returns (a `str`)
+ `email_students` is user input preference (a `boolean`)
+ """
old_students, _ = get_and_clean_student_list(students)
status = dict([x, 'unprocessed'] for x in old_students)
+ if email_students:
+ #Composition of email
+ d = {'site_name': settings.SITE_NAME,
+ 'course_id': course_id}
+
for student in old_students:
isok = False
@@ -1153,6 +1210,14 @@ def _do_unenroll_students(course_id, students):
try:
user = User.objects.get(email=student)
except User.DoesNotExist:
+
+ if isok and email_students:
+ #User was allowed to join but had not signed up yet
+ d['email_address'] = student
+ d['message'] = 'allowed_unenroll'
+ send_mail_ret = send_mail_to_student(student, d)
+ status[student] += (', email sent' if send_mail_ret else '')
+
continue
ce = CourseEnrollment.objects.filter(user=user, course_id=course_id)
@@ -1161,6 +1226,15 @@ def _do_unenroll_students(course_id, students):
try:
ce[0].delete()
status[student] = "un-enrolled"
+ if email_students:
+ #User was enrolled
+ d['email_address'] = student
+ d['first_name'] = user.first_name
+ d['last_name'] = user.last_name
+ d['message'] = 'enrolled_unenroll'
+ send_mail_ret = send_mail_to_student(student, d)
+ status[student] += (', email sent' if send_mail_ret else '')
+
except Exception:
if not isok:
status[student] = "Error! Failed to un-enroll"
@@ -1173,13 +1247,48 @@ def _do_unenroll_students(course_id, students):
return data
+def send_mail_to_student(student, param_dict):
+ """
+ Construct the email using templates and then send it.
+ `student` is the student's email address (a `str`),
+
+ `param_dict` is a `dict` with keys [
+ `site_name`: name given to edX instance (a `str`)
+ `registration_url`: url for registration (a `str`)
+ `course_id`: id of course (a `str`)
+ `auto_enroll`: user input option (a `str`)
+ `course_url`: url of course (a `str`)
+ `email_address`: email of student (a `str`)
+ `first_name`: student first name (a `str`)
+ `last_name`: student last name (a `str`)
+ `message`: type of email to send and template to use (a `str`)
+ ]
+ Returns a boolean indicating whether the email was sent successfully.
+ """
+
+ EMAIL_TEMPLATE_DICT = {'allowed_enroll': ('emails/enroll_email_allowedsubject.txt', 'emails/enroll_email_allowedmessage.txt'),
+ 'enrolled_enroll': ('emails/enroll_email_enrolledsubject.txt', 'emails/enroll_email_enrolledmessage.txt'),
+ 'allowed_unenroll': ('emails/unenroll_email_subject.txt', 'emails/unenroll_email_allowedmessage.txt'),
+ 'enrolled_unenroll': ('emails/unenroll_email_subject.txt', 'emails/unenroll_email_enrolledmessage.txt')}
+
+ subject_template, message_template = EMAIL_TEMPLATE_DICT.get(param_dict['message'], (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)
+
+ # Email subject *must not* contain newlines
+ subject = ''.join(subject.splitlines())
+ send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [student], fail_silently=False)
+ return True
+ else:
+ return False
+
+
def get_and_clean_student_list(students):
"""
Separate out individual student email from the comma, or space separated string.
-
- In:
- students: string coming from the input text area
- Return:
+ `students` is string of student emails separated by commas or returns (a `str`)
+ Returns:
students: list of cleaned student emails
students_lc: list of lower case cleaned student emails
"""
diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html
index d541962906..bc49cda427 100644
--- a/lms/templates/courseware/instructor_dashboard.html
+++ b/lms/templates/courseware/instructor_dashboard.html
@@ -382,6 +382,8 @@ function goto( mode)
Enroll or un-enroll one or many students: enter emails, separated by new lines or commas;
+ Notify students by email
+
Auto-enroll students when they activate
diff --git a/lms/templates/emails/enroll_email_allowedmessage.txt b/lms/templates/emails/enroll_email_allowedmessage.txt
new file mode 100644
index 0000000000..eab347166e
--- /dev/null
+++ b/lms/templates/emails/enroll_email_allowedmessage.txt
@@ -0,0 +1,13 @@
+Dear student,
+
+You have been invited to join ${course_id} at ${site_name} by a member of the course staff.
+
+To finish your registration, please visit ${registration_url} and fill out the registration form.
+% if auto_enroll:
+Once you have registered and activated your account, you will see ${course_id} listed on your dashboard.
+% else:
+Once you have registered and activated your account, visit ${course_url} to join the course.
+% endif
+
+----
+This email was automatically sent from ${site_name} to ${email_address}
\ No newline at end of file
diff --git a/lms/templates/emails/enroll_email_allowedsubject.txt b/lms/templates/emails/enroll_email_allowedsubject.txt
new file mode 100644
index 0000000000..41da60d1db
--- /dev/null
+++ b/lms/templates/emails/enroll_email_allowedsubject.txt
@@ -0,0 +1 @@
+You have been invited to register for ${course_id}
\ No newline at end of file
diff --git a/lms/templates/emails/enroll_email_enrolledmessage.txt b/lms/templates/emails/enroll_email_enrolledmessage.txt
new file mode 100644
index 0000000000..8e8f24efed
--- /dev/null
+++ b/lms/templates/emails/enroll_email_enrolledmessage.txt
@@ -0,0 +1,8 @@
+Dear ${first_name} ${last_name}
+
+You have been enrolled in ${course_id} at ${site_name} by a member of the course staff. The course should now appear on your ${site_name} dashboard.
+
+To start accessing course materials, please visit ${course_url}
+
+----
+This email was automatically sent from ${site_name} to ${first_name} ${last_name}
\ No newline at end of file
diff --git a/lms/templates/emails/enroll_email_enrolledsubject.txt b/lms/templates/emails/enroll_email_enrolledsubject.txt
new file mode 100644
index 0000000000..db897a3299
--- /dev/null
+++ b/lms/templates/emails/enroll_email_enrolledsubject.txt
@@ -0,0 +1 @@
+You have been enrolled in ${course_id}
\ No newline at end of file
diff --git a/lms/templates/emails/unenroll_email_allowedmessage.txt b/lms/templates/emails/unenroll_email_allowedmessage.txt
new file mode 100644
index 0000000000..9bd0bd3cfd
--- /dev/null
+++ b/lms/templates/emails/unenroll_email_allowedmessage.txt
@@ -0,0 +1,6 @@
+Dear Student,
+
+You have been un-enrolled from course ${course_id} by a member of the course staff. Please disregard the invitation previously sent.
+
+----
+This email was automatically sent from ${site_name} to ${email_address}
\ No newline at end of file
diff --git a/lms/templates/emails/unenroll_email_enrolledmessage.txt b/lms/templates/emails/unenroll_email_enrolledmessage.txt
new file mode 100644
index 0000000000..8a7f9f996e
--- /dev/null
+++ b/lms/templates/emails/unenroll_email_enrolledmessage.txt
@@ -0,0 +1,8 @@
+Dear ${first_name} ${last_name}
+
+You have been un-enrolled in ${course_id} at ${site_name} by a member of the course staff. The course will no longer appear on your ${site_name} dashboard.
+
+Your other courses have not been affected.
+
+----
+This email was automatically sent from ${site_name} to ${first_name} ${last_name}
\ No newline at end of file
diff --git a/lms/templates/emails/unenroll_email_subject.txt b/lms/templates/emails/unenroll_email_subject.txt
new file mode 100644
index 0000000000..f79218ff22
--- /dev/null
+++ b/lms/templates/emails/unenroll_email_subject.txt
@@ -0,0 +1 @@
+You have been un-enrolled from ${course_id}
\ No newline at end of file