Commit 496eff88 by Se Won Jang

Implemented Enrollment Email Feature

There was a need to notify students via email upon enrolling in a course, since
they were confused that they did not receive any confirmation of their course
enrollments.

Since the scope of this feature is pretty narrow (confined to course
enrollments only), this commit does not focus on building a general
email-notification system.

================ Changes to the Model =============

This commit adds two new fields(booleans) to the Course model:

	- enable_enrollment_email
	- enable_default_enrollment_email

that allows course administrators to enable / disable the enrollment emailing
feature, and  upon enabling, decide whether they want to use the default
enrollment email template.

These new fields are linked to the CourseDetails object, so that course
administrators can edit these two flag settings in the Settings -
Schedule&Details Tab through a friendly UI.

This commit also adds the following four new documents to CourseDetails:

	- pre_enrollment_email
	- pre_enrollment_email_subject
	- post_enrollment_email
	- post_enrollment_email_subject

================ Changes to the controller ===============

This commit adds notify_enrollment_by_email function to:

	- common/djangoapps/student/views.py

and calls it from the change_enrollment controller that receives requests upon
a student's clicking of the Registration button. It is called after enrolling
the student to the course in the database.

This function first checks to make sure that it's not in automated testing env,
and that enable_enrollment_email field of the specified course is set to True.

Then it checkes whether the enable_default_enrollment_email field is set to
True or not, and handles the email message accordingly by either:

	1) Pulling the default static enrollment_email template
	2) Using the template specified by the Course Admin

When using the configured template, it checks if the course has already started
or not and appropriately chooses between pre_enrollment_email & subject and
post_enrollment_email & subject.

At the end of the function, it returns a json object specifying whether the
emailing was successful or not, so that the students are notified of the
error, and for unit testing.

================ Changes to the view (frontend ) =========

This commit applies a few changes to the front-end javascript:

	- cms/static/js/views/settings/main.js

so that upon receiving a modified CourseDetail Object via ajax,
the template gets updated appropriately, and also so that it
makes put ajax requests back to the server when the Course Admin applies
changes to enrollment email settings.

================ Unit Tests ============================

The written unit tests are very basic, and is done by checking the json
response object returned by the notify_enrollment_by_email method.

========================================================

bugfix: line 69 & 75 in course_details.py. course->course_details

added update_user_by_email to CourseEnrollment Model. Still implementing

fixed a few indentation errors with vim

pushing enrollment email functionality from model to the view

added email boolean to course_details, modified courseware/courses.py to be able to throw pre & post_enrollment_email attributes

course enrollment email functionality implemented. TODO: What to put as the from_address / subject. + access enable_enrollment_email column in the CourseDetails Object

changed datetime calculation method to use given function from CourseDescriptor Objectw

added enable_enrollment_email field to the course descriptor & course details. Changing the frontend to store the value.

fixed frontend to send appropriate ajax calls. Not being saved correctly tho...

added settings option to the enable_enrollment_email in course_module.py. editted course_details to turn dirty flag to true upon receiving enable_enrollment_email edit flag

got rid of comments

cleaned up modules, ran module tests & passed all

adding feature: use default enrollment emails. Changed front-end. TODO: add fields to course & course details models

implemented default course enrollment notification settings & views

writing unit_tests

finished writing up unit tests for enrollment_emails

applied some changes to thes test_email

fixing with Joe's styling guide :)

changed url managing from hardcoding to reverse()

views changed..?

fetched from upstream master

passing all tests

applying sef's PR comment to break lines at courses.py

removed println

remove indentation errors

Conflicts:
	AUTHORS

re-run jenkins
parent f67faa7c
...@@ -154,4 +154,6 @@ Abdallah Nassif <abdoosh00@gmail.com> ...@@ -154,4 +154,6 @@ Abdallah Nassif <abdoosh00@gmail.com>
Johnny Brown <johnnybrown7@gmail.com> Johnny Brown <johnnybrown7@gmail.com>
Ben McMorran <bmcmorran@edx.org> Ben McMorran <bmcmorran@edx.org>
Mat Peterson <mpeterson@edx.org> Mat Peterson <mpeterson@edx.org>
Tim Babych <tim.babych@gmail.com> Tim Babych <tim.babych@gmail.com>
\ No newline at end of file Se Won Jang <swjang@stanford.edu>
...@@ -42,6 +42,8 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -42,6 +42,8 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(details.syllabus, "syllabus somehow initialized" + str(details.syllabus)) self.assertIsNone(details.syllabus, "syllabus somehow initialized" + str(details.syllabus))
self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video)) self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video))
self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort)) self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
self.assertFalse(details.enable_enrollment_email, "Enrollment Email should be initialized as false")
self.assertTrue(details.enable_default_enrollment_email, "Default Template option for enrollment email should be initialized as true")
def test_encoder(self): def test_encoder(self):
details = CourseDetails.fetch(self.course.id) details = CourseDetails.fetch(self.course.id)
......
...@@ -26,10 +26,14 @@ class CourseDetails(object): ...@@ -26,10 +26,14 @@ class CourseDetails(object):
self.overview = "" # html to render as the overview self.overview = "" # html to render as the overview
self.pre_enrollment_email = "" # html to render as the pre-enrollment email self.pre_enrollment_email = "" # html to render as the pre-enrollment email
self.post_enrollment_email = "" # html to render as the post-enrollment email self.post_enrollment_email = "" # html to render as the post-enrollment email
self.pre_enrollment_email_subject = "" # header of the pre_enrollment_email
self.post_enrollment_email_subject = "" # header of the post_enrollment_email
self.intro_video = None # a video pointer self.intro_video = None # a video pointer
self.effort = None # int hours/week self.effort = None # int hours/week
self.course_image_name = "" self.course_image_name = ""
self.course_image_asset_path = "" # URL of the course image self.course_image_asset_path = "" # URL of the course image
self.enable_enrollment_email = False
self.enable_default_enrollment_email = True
@classmethod @classmethod
def fetch(cls, course_key): def fetch(cls, course_key):
...@@ -45,6 +49,8 @@ class CourseDetails(object): ...@@ -45,6 +49,8 @@ class CourseDetails(object):
course_details.enrollment_end = descriptor.enrollment_end course_details.enrollment_end = descriptor.enrollment_end
course_details.course_image_name = descriptor.course_image course_details.course_image_name = descriptor.course_image
course_details.course_image_asset_path = course_image_url(descriptor) course_details.course_image_asset_path = course_image_url(descriptor)
course_details.enable_enrollment_email = descriptor.enable_enrollment_email
course_details.enable_default_enrollment_email = descriptor.enable_default_enrollment_email
temploc = course_key.make_usage_key('about', 'syllabus') temploc = course_key.make_usage_key('about', 'syllabus')
try: try:
...@@ -63,16 +69,26 @@ class CourseDetails(object): ...@@ -63,16 +69,26 @@ class CourseDetails(object):
course_details.overview = modulestore().get_item(temploc).data course_details.overview = modulestore().get_item(temploc).data
except ItemNotFoundError: except ItemNotFoundError:
pass pass
temploc = course_key.make_usage_key('about', 'pre_enrollment_email_subject')
try:
course_details.pre_enrollment_email_subject = modulestore().get_item(temploc).data
except ItemNotFoundError:
pass
temploc = course_key.make_usage_key('about', 'post_enrollment_email_subject')
try:
course_details.post_enrollment_email_subject = modulestore().get_item(temploc).data
except ItemNotFoundError:
pass
temploc = course_key.make_usage_key('about', 'pre_enrollment_email') temploc = course_key.make_usage_key('about', 'pre_enrollment_email')
try: try:
course.pre_enrollment_email = get_modulestore(temploc).get_item(temploc).data course_details.pre_enrollment_email = modulestore().get_item(temploc).data
except ItemNotFoundError: except ItemNotFoundError:
pass pass
temploc = course_key.make_usage_key('about', 'post_enrollment_email') temploc = course_key.make_usage_key('about', 'post_enrollment_email')
try: try:
course.post_enrollment_email = get_modulestore(temploc).get_item(temploc).data course_details.post_enrollment_email = modulestore().get_item(temploc).data
except ItemNotFoundError: except ItemNotFoundError:
pass pass
...@@ -124,6 +140,16 @@ class CourseDetails(object): ...@@ -124,6 +140,16 @@ class CourseDetails(object):
# into the model is nasty, convert the JSON Date to a Python date, which is what the # into the model is nasty, convert the JSON Date to a Python date, which is what the
# setter expects as input. # setter expects as input.
date = Date() date = Date()
# Added to allow admins to enable/disable default enrollment emails
if 'enable_default_enrollment_email' in jsondict:
descriptor.enable_default_enrollment_email = jsondict['enable_default_enrollment_email']
dirty = True
# Added to allow admins to enable/disable enrollment emails
if 'enable_enrollment_email' in jsondict:
descriptor.enable_enrollment_email = jsondict['enable_enrollment_email']
dirty=True
if 'start_date' in jsondict: if 'start_date' in jsondict:
converted = date.from_json(jsondict['start_date']) converted = date.from_json(jsondict['start_date'])
...@@ -169,7 +195,9 @@ class CourseDetails(object): ...@@ -169,7 +195,9 @@ class CourseDetails(object):
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
# to make faster, could compare against db or could have client send over a list of which fields changed. # to make faster, could compare against db or could have client send over a list of which fields changed.
for about_type in ['syllabus', 'overview', 'effort', 'short_description', 'pre_enrollment_email', 'post_enrollment_email']: for about_type in ['syllabus', 'overview', 'effort', 'short_description',
'pre_enrollment_email', 'post_enrollment_email', 'pre_enrollment_email_subject',
'post_enrollment_email_subject']:
cls.update_about_item(course_key, about_type, jsondict[about_type], descriptor, user) cls.update_about_item(course_key, about_type, jsondict[about_type], descriptor, user)
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video']) recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
......
...@@ -5,17 +5,18 @@ var CourseDetails = Backbone.Model.extend({ ...@@ -5,17 +5,18 @@ var CourseDetails = Backbone.Model.extend({
org : '', org : '',
course_id: '', course_id: '',
run: '', run: '',
start_date: null, // maps to 'start' start_date: null, // maps to 'start'
end_date: null, // maps to 'end' end_date: null, // maps to 'end'
enrollment_start: null, enrollment_start: null,
enrollment_end: null, enrollment_end: null,
syllabus: null, syllabus: null,
short_description: "", short_description: "",
overview: "", overview: "",
intro_video: null, intro_video: null,
effort: null, // an int or null, effort: null, // an int or null,
course_image_name: '', // the filename course_image_name: '', // the filename
course_image_asset_path: '' // the full URL (/c4x/org/course/num/asset/filename) course_image_asset_path: '', // the full URL (/c4x/org/course/num/asset/filename)
enable_enrollment_email: false
}, },
// When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset) // When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset)
......
...@@ -13,6 +13,7 @@ var DetailsView = ValidatingView.extend({ ...@@ -13,6 +13,7 @@ var DetailsView = ValidatingView.extend({
'click .remove-course-introduction-video' : "removeVideo", 'click .remove-course-introduction-video' : "removeVideo",
'focus #course-overview' : "codeMirrorize", 'focus #course-overview' : "codeMirrorize",
'click #enable-enrollment-email' : "toggleEnrollmentEmails", 'click #enable-enrollment-email' : "toggleEnrollmentEmails",
'click #enable-default-enrollment-email' : "toggleDefaultEnrollmentEmails",
'focus #pre-enrollment-email' : "codeMirrorize", 'focus #pre-enrollment-email' : "codeMirrorize",
'focus #post-enrollment-email' : "codeMirrorize", 'focus #post-enrollment-email' : "codeMirrorize",
'mouseover #timezone' : "updateTime", 'mouseover #timezone' : "updateTime",
...@@ -43,6 +44,21 @@ var DetailsView = ValidatingView.extend({ ...@@ -43,6 +44,21 @@ var DetailsView = ValidatingView.extend({
this.listenTo(this.model, 'invalid', this.handleValidationError); this.listenTo(this.model, 'invalid', this.handleValidationError);
this.listenTo(this.model, 'change', this.showNotificationBar); this.listenTo(this.model, 'change', this.showNotificationBar);
this.selectorToField = _.invert(this.fieldToSelectorMap); this.selectorToField = _.invert(this.fieldToSelectorMap);
/* Memoize html elements for enrollment emails */
this.pre_enrollment_email_elem = this.$el.find('#' + this.fieldToSelectorMap['pre_enrollment_email']);
this.pre_enrollment_email_subject_elem = this.$el.find('#' + this.fieldToSelectorMap['pre_enrollment_email_subject']);
this.pre_enrollment_email_field = this.$el.find('#field-pre-enrollment-email');
this.pre_enrollment_email_subject_field = this.$el.find('#field-pre-enrollment-email-subject');
this.post_enrollment_email_elem = this.$el.find('#' + this.fieldToSelectorMap['post_enrollment_email']);
this.post_enrollment_email_subject_elem = this.$el.find('#' + this.fieldToSelectorMap['post_enrollment_email_subject']);
this.post_enrollment_email_field = this.$el.find('#field-post-enrollment-email');
this.post_enrollment_email_subject_field = this.$el.find('#field-post-enrollment-email-subject');
this.enable_enrollment_email_box = this.$el.find('#' + this.fieldToSelectorMap['enable_enrollment_email'])[0];
this.enable_default_enrollment_email_box = this.$el.find('#' + this.fieldToSelectorMap['enable_default_enrollment_email'])[0];
}, },
render: function() { render: function() {
...@@ -54,12 +70,31 @@ var DetailsView = ValidatingView.extend({ ...@@ -54,12 +70,31 @@ var DetailsView = ValidatingView.extend({
this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview')); this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview'));
this.codeMirrorize(null, $('#course-overview')[0]); this.codeMirrorize(null, $('#course-overview')[0]);
this.$el.find('#' + this.fieldToSelectorMap['pre_enrollment_email']).val(this.model.get('pre_enrollment_email')); this.pre_enrollment_email_subject_elem.val(this.model.get('pre_enrollment_email_subject'));
this.post_enrollment_email_subject_elem.val(this.model.get('post_enrollment_email_subject'));
this.pre_enrollment_email_elem.val(this.model.get('pre_enrollment_email'));
this.codeMirrorize(null, $('#pre-enrollment-email')[0]); this.codeMirrorize(null, $('#pre-enrollment-email')[0]);
this.$el.find('#' + this.fieldToSelectorMap['post_enrollment_email']).val(this.model.get('post_enrollment_email')); this.post_enrollment_email_elem.val(this.model.get('post_enrollment_email'));
this.codeMirrorize(null, $('#post-enrollment-email')[0]); this.codeMirrorize(null, $('#post-enrollment-email')[0]);
this.enable_enrollment_email_box.checked = this.model.get('enable_enrollment_email');
this.enable_default_enrollment_email_box.checked = this.model.get('enable_default_enrollment_email');
if (this.model.get('enable_enrollment_email')) {
this.pre_enrollment_email_field.show();
this.post_enrollment_email_field.show();
this.pre_enrollment_email_subject_field.show();
this.post_enrollment_email_subject_field.show();
} else {
this.pre_enrollment_email_field.hide();
this.post_enrollment_email_field.hide();
this.pre_enrollment_email_subject_field.hide();
this.post_enrollment_email_subject_field.hide();
}
this.$el.find('#' + this.fieldToSelectorMap['short_description']).val(this.model.get('short_description')); this.$el.find('#' + this.fieldToSelectorMap['short_description']).val(this.model.get('short_description'));
this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample()); this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
...@@ -88,7 +123,11 @@ var DetailsView = ValidatingView.extend({ ...@@ -88,7 +123,11 @@ var DetailsView = ValidatingView.extend({
'short_description' : 'course-short-description', 'short_description' : 'course-short-description',
'intro_video' : 'course-introduction-video', 'intro_video' : 'course-introduction-video',
'effort' : "course-effort", 'effort' : "course-effort",
'course_image_asset_path': 'course-image-url' 'course_image_asset_path': 'course-image-url',
'enable_enrollment_email': 'enable-enrollment-email',
'pre_enrollment_email_subject' :'pre-enrollment-email-subject',
'post_enrollment_email_subject':'post-enrollment-email-subject',
'enable_default_enrollment_email':'enable-default-enrollment-email'
}, },
updateTime : function(e) { updateTime : function(e) {
...@@ -163,6 +202,12 @@ var DetailsView = ValidatingView.extend({ ...@@ -163,6 +202,12 @@ var DetailsView = ValidatingView.extend({
case 'course-effort': case 'course-effort':
this.setField(event); this.setField(event);
break; break;
case 'pre-enrollment-email-subject':
this.setField(event);
break;
case 'post-enrollment-email-subject':
this.setField(event);
break;
case 'course-short-description': case 'course-short-description':
this.setField(event); this.setField(event);
break; break;
...@@ -198,13 +243,36 @@ var DetailsView = ValidatingView.extend({ ...@@ -198,13 +243,36 @@ var DetailsView = ValidatingView.extend({
}, },
toggleEnrollmentEmails: function(event) { toggleEnrollmentEmails: function(event) {
var isChecked = this.$el.find("#enable-enrollment-email").is(':checked'); var isChecked = this.enable_enrollment_email_box.checked;
if(isChecked) { if(isChecked) {
this.$el.find('#field-pre-enrollment-email').show(); this.pre_enrollment_email_field.show();
this.$el.find('#field-post-enrollment-email').show(); this.post_enrollment_email_field.show();
this.pre_enrollment_email_subject_field.show();
this.post_enrollment_email_subject_field.show();
} else { } else {
this.$el.find('#field-pre-enrollment-email').hide(); this.pre_enrollment_email_field.hide();
this.$el.find('#field-post-enrollment-email').hide(); this.post_enrollment_email_field.hide();
this.pre_enrollment_email_subject_field.hide();
this.post_enrollment_email_subject_field.hide();
/* If enrollment email sending option is turned off, default email option should be turned off as well. */
this.enable_default_enrollment_email_box.checked = false;
this.setAndValidate(this.selectorToField['enable-default-enrollment-email'], false);
}
var field = this.selectorToField['enable-enrollment-email'];
if (this.model.get(field) != isChecked) {
this.setAndValidate(field, isChecked);
}
},
toggleDefaultEnrollmentEmails: function(event) {
var isChecked = this.enable_default_enrollment_email_box.checked;
var field = this.selectorToField['enable-default-enrollment-email'];
if (this.model.get(field) != isChecked) {
this.setAndValidate(field, isChecked);
} }
}, },
......
...@@ -204,11 +204,8 @@ ...@@ -204,11 +204,8 @@
} }
.list-actions { .list-actions {
//box-shadow: inset 0 1px 1px $shadow-l1;
//border-top: 1px solid $gray-l2;
padding-top: ($baseline/2); padding-top: ($baseline/2);
//background: $gray-l5;
.action-primary { .action-primary {
@include blue-button(); @include blue-button();
@extend %t-action3; @extend %t-action3;
......
...@@ -226,10 +226,21 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s ...@@ -226,10 +226,21 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
% endif % endif
<li class="field text" id="field-enable-enrollment-email"> <li class="field text" id="field-enable-enrollment-email">
<label for="course-email">${_("Course Enrollment Email")}</label>
<input type="checkbox" id="enable-enrollment-email"/> <input type="checkbox" id="enable-enrollment-email"/>
<label for="enable-enrollment-email">${_("Enable enrollment emails")}</label> <label for="enable-enrollment-email">${_("Enable enrollment emails")}</label>
</li> </li>
<li class="field text" id="field-enable-default-enrollment-email">
<input type="checkbox" id="enable-default-enrollment-email"/>
<label for="enable-enrollment-email">${_("Use default enrollment emails")}</label>
</li>
<li class="field text" id="field-pre-enrollment-email-subject">
<label for="pre-enrollment-email-subject">${_("Subject of the Email sent to students who enroll before the course starts")}</label>
<textarea class = 'text' id="pre-enrollment-email-subject"></textarea>
</li>
<li class="field text" id="field-pre-enrollment-email"> <li class="field text" id="field-pre-enrollment-email">
<label for="pre-enrollment-email">${_("Email sent to students who enroll before the course starts")}</label> <label for="pre-enrollment-email">${_("Email sent to students who enroll before the course starts")}</label>
<textarea class="tinymce text-editor" id="pre-enrollment-email"></textarea> <textarea class="tinymce text-editor" id="pre-enrollment-email"></textarea>
...@@ -242,6 +253,10 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s ...@@ -242,6 +253,10 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
</li> </li>
</ul> </ul>
</li> </li>
<li class="field text" id="field-post-enrollment-email-subject">
<label for="post-enrollment-email-subject">${_("Subject of the Email sent to students who enroll after the course starts")}</label>
<textarea class = 'text' id="post-enrollment-email-subject"></textarea>
</li>
<li class="field text" id="field-post-enrollment-email"> <li class="field text" id="field-post-enrollment-email">
<label for="post-enrollment-email">${_("Email sent to students who enroll after the course starts")}</label> <label for="post-enrollment-email">${_("Email sent to students who enroll after the course starts")}</label>
......
...@@ -791,6 +791,9 @@ class CourseEnrollment(models.Model): ...@@ -791,6 +791,9 @@ class CourseEnrollment(models.Model):
may include "audit", "verified_id", etc. Please don't use it may include "audit", "verified_id", etc. Please don't use it
until we have these mapped out. until we have these mapped out.
'should_send_email' is a boolean that specifies if a course enrollment
email should be sent to the given user.
It is expected that this method is called from a method which has already It is expected that this method is called from a method which has already
verified the user authentication and access. verified the user authentication and access.
""" """
......
...@@ -3,7 +3,7 @@ import django.db ...@@ -3,7 +3,7 @@ import django.db
import unittest import unittest
from student.tests.factories import UserFactory, RegistrationFactory, PendingEmailChangeFactory from student.tests.factories import UserFactory, RegistrationFactory, PendingEmailChangeFactory
from student.views import reactivation_email_for_user, change_email_request, confirm_email_change from student.views import reactivation_email_for_user, change_email_request, confirm_email_change, notify_enrollment_by_email
from student.models import UserProfile, PendingEmailChange from student.models import UserProfile, PendingEmailChange
from django.contrib.auth.models import User, AnonymousUser from django.contrib.auth.models import User, AnonymousUser
from django.test import TestCase, TransactionTestCase from django.test import TestCase, TransactionTestCase
...@@ -14,7 +14,8 @@ from django.conf import settings ...@@ -14,7 +14,8 @@ from django.conf import settings
from edxmako.shortcuts import render_to_string from edxmako.shortcuts import render_to_string
from util.request import safe_get_host from util.request import safe_get_host
from textwrap import dedent from textwrap import dedent
from microsite_configuration import microsite
from xmodule.modulestore.tests.factories import CourseFactory
class TestException(Exception): class TestException(Exception):
"""Exception used for testing that nothing will catch explicitly""" """Exception used for testing that nothing will catch explicitly"""
...@@ -58,6 +59,48 @@ class EmailTestMixin(object): ...@@ -58,6 +59,48 @@ class EmailTestMixin(object):
settings.ALLOWED_HOSTS.append(hostname) settings.ALLOWED_HOSTS.append(hostname)
self.addCleanup(settings.ALLOWED_HOSTS.pop) self.addCleanup(settings.ALLOWED_HOSTS.pop)
class EnrollmentEmailTests(TestCase):
""" Test senging automated emails to users upon course enrollment. """
def setUp(self):
# Test Contstants
COURSE_SLUG = "100"
COURSE_NAME = "test_course"
COURSE_ORG = "EDX"
self.user = UserFactory.create(username="tester", email="tester@gmail.com", password="test")
self.course = CourseFactory.create(org=COURSE_ORG, display_name=COURSE_NAME, number=COURSE_SLUG)
self.assertIsNotNone(self.course)
self.request = RequestFactory().post('random_url')
self.request.user = self.user
def send_enrollment_email(self):
""" Send enrollment email to the user and return the Json response data. """
return json.loads(notify_enrollment_by_email(self.course, self.user, self.request).content)
def test_disabled_email_case(self):
""" Make sure emails don't fire when enable_enrollment_email setting is disabled. """
self.course.enable_enrollment_email = False
email_result = self.send_enrollment_email()
self.assertIn('email_did_fire', email_result)
self.assertFalse(email_result['email_did_fire'])
def test_custom_enrollment_email_sent(self):
""" Test sending of enrollment emails when enable_default_enrollment_email setting is disabled. """
self.course.enable_enrollment_email = True
self.course.enable_default_enrollment_email = False
email_result = self.send_enrollment_email()
self.assertNotIn('email_did_fire', email_result)
self.assertIn('is_success', email_result)
def test_default_enrollment_email_sent(self):
""" Test sending of enrollment emails when enable_default_enrollment_email setting is enabled. """
self.course.enable_enrollment_email = True
self.course.enable_default_enrollment_email = True
email_result = self.send_enrollment_email()
self.assertNotIn('email_did_fire', email_result)
self.assertIn('is_success', email_result)
@patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) @patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True))
@patch('django.contrib.auth.models.User.email_user') @patch('django.contrib.auth.models.User.email_user')
......
...@@ -8,6 +8,8 @@ import uuid ...@@ -8,6 +8,8 @@ import uuid
import time import time
from collections import defaultdict from collections import defaultdict
from pytz import UTC from pytz import UTC
from pytz import timezone
import json
from django.conf import settings from django.conf import settings
from django.contrib.auth import logout, authenticate, login from django.contrib.auth import logout, authenticate, login
...@@ -57,7 +59,7 @@ from xmodule.modulestore import ModuleStoreEnum ...@@ -57,7 +59,7 @@ from xmodule.modulestore import ModuleStoreEnum
from collections import namedtuple from collections import namedtuple
from courseware.courses import get_courses, sort_by_announcement from courseware.courses import get_courses, sort_by_announcement, get_course_about_section
from courseware.access import has_access from courseware.access import has_access
from courseware.models import CoursePreference from courseware.models import CoursePreference
...@@ -682,7 +684,11 @@ def change_enrollment(request): ...@@ -682,7 +684,11 @@ def change_enrollment(request):
current_mode = available_modes[0] current_mode = available_modes[0]
CourseEnrollment.enroll(user, course.id, mode=current_mode.slug) CourseEnrollment.enroll(user, course.id, mode=current_mode.slug)
# notify the user of the enrollment via email
enrollment_email_result = json.loads(notify_enrollment_by_email(course, user, request).content)
if ('is_success' in enrollment_email_result and not enrollment_email_result['is_success']):
return HttpResponseBadRequest(_(enrollment_email_result['error']))
return HttpResponse() return HttpResponse()
elif action == "add_to_cart": elif action == "add_to_cart":
...@@ -706,6 +712,52 @@ def change_enrollment(request): ...@@ -706,6 +712,52 @@ def change_enrollment(request):
else: else:
return HttpResponseBadRequest(_("Enrollment action is invalid")) return HttpResponseBadRequest(_("Enrollment action is invalid"))
def notify_enrollment_by_email(course, user, request):
"""
Updates the user about the course enrollment by email.
If the Course has already started, use post_enrollment_email
If the Course has not yet started, use pre_enrollment_email
"""
if (not (settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING')) and course.enable_enrollment_email):
from_address = microsite.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
try:
if (not course.enable_default_enrollment_email):
# Check if the course has already started and set subject & message accordingly
if (course.has_started):
subject = get_course_about_section(course, 'post_enrollment_email_subject')
message = get_course_about_section(course, 'post_enrollment_email')
else:
subject = get_course_about_section(course, 'pre_enrollment_email_subject')
message = get_course_about_section(course, 'pre_enrollment_email')
else:
# If not default, use the default emailing template
course_url = reverse('info', args=(course.id.to_deprecated_string(),))
context = {
'course':course,
'full_name':user.profile.name,
'site_name': microsite.get_value('SITE_NAME', settings.SITE_NAME),
'course_url':request.build_absolute_uri(course_url),
}
subject = render_to_string('emails/enroll_email_enrolledsubject.txt', context)
message = render_to_string('emails/enroll_email_enrolledmessage.txt', context)
subject = ''.join(subject.splitlines())
user.email_user(subject, message, from_address)
except Exception:
log.error('unable to send course enrollment verification email to user from "{from_address}"'.format(
from_address=from_address), exc_info = True)
return JsonResponse({"is_success": False, "error": _("Could not send enrollment email to the user"),})
return JsonResponse({"is_success": True, "subject": subject, "message": message})
else:
return JsonResponse({"email_did_fire": False})
def _check_can_enroll_in_course(user, course_key, access_type="enroll"): def _check_can_enroll_in_course(user, course_key, access_type="enroll"):
""" """
......
...@@ -169,6 +169,8 @@ class CourseFields(object): ...@@ -169,6 +169,8 @@ class CourseFields(object):
default=[], scope=Scope.content) default=[], scope=Scope.content)
wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content) wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content)
enable_enrollment_email = Boolean(help="Whether to send notification email upon enrollment or not", default=False, scope=Scope.settings)
enable_default_enrollment_email = Boolean(help="Whether to use default enrollment email for enrollment notification", default=True, scope=Scope.settings)
enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings) enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings)
enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings) enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings)
start = Date(help="Start time when this module is visible", start = Date(help="Start time when this module is visible",
......
...@@ -162,6 +162,10 @@ def get_course_about_section(course, section_key): ...@@ -162,6 +162,10 @@ def get_course_about_section(course, section_key):
- faq - faq
- more_info - more_info
- ocw_links - ocw_links
- pre_enrollment_email
- post_enrollment_email
- pre_enrollment_email_subject
- post_enrollment_email_subject
""" """
# Many of these are stored as html files instead of some semantic # Many of these are stored as html files instead of some semantic
...@@ -173,7 +177,9 @@ def get_course_about_section(course, section_key): ...@@ -173,7 +177,9 @@ def get_course_about_section(course, section_key):
'course_staff_short', 'course_staff_extended', 'course_staff_short', 'course_staff_extended',
'requirements', 'syllabus', 'textbook', 'faq', 'more_info', 'requirements', 'syllabus', 'textbook', 'faq', 'more_info',
'number', 'instructors', 'overview', 'number', 'instructors', 'overview',
'effort', 'end_date', 'prerequisites', 'ocw_links']: 'effort', 'end_date', 'prerequisites', 'ocw_links',
'pre_enrollment_email', 'post_enrollment_email',
'pre_enrollment_email_subject', 'post_enrollment_email_subject']:
try: try:
......
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