Commit aac28d08 by sjang92

Merge pull request #74 from Stanford-Online/sjang92/origin/enrollment-email

Sjang92/origin/enrollment email
parents 44129cc4 496eff88
......@@ -154,4 +154,6 @@ Abdallah Nassif <abdoosh00@gmail.com>
Johnny Brown <johnnybrown7@gmail.com>
Ben McMorran <bmcmorran@edx.org>
Mat Peterson <mpeterson@edx.org>
Tim Babych <tim.babych@gmail.com>
\ No newline at end of file
Tim Babych <tim.babych@gmail.com>
Se Won Jang <swjang@stanford.edu>
......@@ -42,6 +42,8 @@ class CourseDetailsTestCase(CourseTestCase):
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.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):
details = CourseDetails.fetch(self.course.id)
......
......@@ -24,10 +24,16 @@ class CourseDetails(object):
self.syllabus = None # a pdf file asset
self.short_description = ""
self.overview = "" # html to render as the overview
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.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.effort = None # int hours/week
self.course_image_name = ""
self.course_image_asset_path = "" # URL of the course image
self.enable_enrollment_email = False
self.enable_default_enrollment_email = True
@classmethod
def fetch(cls, course_key):
......@@ -43,6 +49,8 @@ class CourseDetails(object):
course_details.enrollment_end = descriptor.enrollment_end
course_details.course_image_name = descriptor.course_image
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')
try:
......@@ -61,6 +69,28 @@ class CourseDetails(object):
course_details.overview = modulestore().get_item(temploc).data
except ItemNotFoundError:
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')
try:
course_details.pre_enrollment_email = modulestore().get_item(temploc).data
except ItemNotFoundError:
pass
temploc = course_key.make_usage_key('about', 'post_enrollment_email')
try:
course_details.post_enrollment_email = modulestore().get_item(temploc).data
except ItemNotFoundError:
pass
temploc = course_key.make_usage_key('about', 'effort')
try:
......@@ -110,6 +140,16 @@ class CourseDetails(object):
# into the model is nasty, convert the JSON Date to a Python date, which is what the
# setter expects as input.
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:
converted = date.from_json(jsondict['start_date'])
......@@ -155,7 +195,9 @@ class CourseDetails(object):
# 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.
for about_type in ['syllabus', 'overview', 'effort', 'short_description']:
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)
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
......
......@@ -5,17 +5,18 @@ var CourseDetails = Backbone.Model.extend({
org : '',
course_id: '',
run: '',
start_date: null, // maps to 'start'
end_date: null, // maps to 'end'
start_date: null, // maps to 'start'
end_date: null, // maps to 'end'
enrollment_start: null,
enrollment_end: null,
syllabus: null,
short_description: "",
overview: "",
intro_video: null,
effort: null, // an int or null,
effort: null, // an int or null,
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)
......
......@@ -12,6 +12,10 @@ var DetailsView = ValidatingView.extend({
"change textarea" : "updateModel",
'click .remove-course-introduction-video' : "removeVideo",
'focus #course-overview' : "codeMirrorize",
'click #enable-enrollment-email' : "toggleEnrollmentEmails",
'click #enable-default-enrollment-email' : "toggleDefaultEnrollmentEmails",
'focus #pre-enrollment-email' : "codeMirrorize",
'focus #post-enrollment-email' : "codeMirrorize",
'mouseover #timezone' : "updateTime",
// would love to move to a general superclass, but event hashes don't inherit in backbone :-(
'focus :input' : "inputFocus",
......@@ -40,6 +44,21 @@ var DetailsView = ValidatingView.extend({
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.listenTo(this.model, 'change', this.showNotificationBar);
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() {
......@@ -51,6 +70,31 @@ var DetailsView = ValidatingView.extend({
this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview'));
this.codeMirrorize(null, $('#course-overview')[0]);
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.post_enrollment_email_elem.val(this.model.get('post_enrollment_email'));
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('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
......@@ -74,10 +118,16 @@ var DetailsView = ValidatingView.extend({
'enrollment_start' : 'enrollment-start',
'enrollment_end' : 'enrollment-end',
'overview' : 'course-overview',
'pre_enrollment_email' : 'pre-enrollment-email',
'post_enrollment_email' : 'post-enrollment-email',
'short_description' : 'course-short-description',
'intro_video' : 'course-introduction-video',
'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) {
......@@ -152,6 +202,12 @@ var DetailsView = ValidatingView.extend({
case 'course-effort':
this.setField(event);
break;
case 'pre-enrollment-email-subject':
this.setField(event);
break;
case 'post-enrollment-email-subject':
this.setField(event);
break;
case 'course-short-description':
this.setField(event);
break;
......@@ -185,6 +241,41 @@ var DetailsView = ValidatingView.extend({
this.$el.find('.remove-course-introduction-video').hide();
}
},
toggleEnrollmentEmails: function(event) {
var isChecked = this.enable_enrollment_email_box.checked;
if(isChecked) {
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();
/* 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);
}
},
codeMirrors : {},
codeMirrorize: function (e, forcedTarget) {
var thisTarget;
......
......@@ -202,6 +202,23 @@
display: inline-block;
}
}
.list-actions {
padding-top: ($baseline/2);
.action-primary {
@include blue-button();
@extend %t-action3;
font-weight: 600;
[class^="icon-"] {
@extend %t-icon5;
display: inline-block;
vertical-align: middle;
margin-top: -3px;
}
}
}
}
.field-group {
......@@ -404,6 +421,32 @@
}
}
// specific fields - pre-enrollment email
#field-pre-enrollment-email {
#pre-enrollment-email {
height: ($baseline*20);
}
//adds back in CodeMirror border removed due to Unit page styling of component editors
.CodeMirror {
border: 1px solid $gray-l2;
}
}
// specific fields - post-enrollment email
#field-post-enrollment-email {
#post-enrollment-email {
height: ($baseline*20);
}
//adds back in CodeMirror border removed due to Unit page styling of component editors
.CodeMirror {
border: 1px solid $gray-l2;
}
}
// specific fields - video
#field-course-introduction-video {
......
......@@ -205,7 +205,7 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
<ol class="list-input">
% if short_description_editable:
<li class="field text" id="field-course-short-description">
<label for="course-overview">${_("Course Short Description")}</label>
<label for="course-short-description">${_("Course Short Description")}</label>
<textarea class="text" id="course-short-description"></textarea>
<span class="tip tip-stacked">${_("Appears on the course catalog page when students roll over the course name. Limit to ~150 characters")}</span>
</li>
......@@ -225,6 +225,53 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
</li>
% endif
<li class="field text" id="field-enable-enrollment-email">
<label for="course-email">${_("Course Enrollment Email")}</label>
<input type="checkbox" id="enable-enrollment-email"/>
<label for="enable-enrollment-email">${_("Enable enrollment emails")}</label>
</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">
<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>
<span class="tip tip-stacked">${_("This email will be sent to any student who enrolls in the course before its start date")}</span>
<ul class="list-actions">
<li class="action-item">
<a title="${_('Send me a copy of this via email')}"
href="" class="action action-primary">
${_("Send me a test email")}</a>
</li>
</ul>
</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">
<label for="post-enrollment-email">${_("Email sent to students who enroll after the course starts")}</label>
<textarea class="tinymce text-editor" id="post-enrollment-email"></textarea>
<span class="tip tip-stacked">${_("This email will be sent to any student who enrolls in the course after its start date")}</span>
<ul class="list-actions">
<li class="action-item">
<a title="${_('Send me a copy of this via email')}"
href="" class="action action-primary">
${_("Send me a test email")}</a>
</li>
</ul>
</li>
<li class="field image" id="field-course-image">
<label>${_("Course Image")}</label>
<div class="current current-course-image">
......
......@@ -791,6 +791,9 @@ class CourseEnrollment(models.Model):
may include "audit", "verified_id", etc. Please don't use it
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
verified the user authentication and access.
"""
......
......@@ -3,7 +3,7 @@ import django.db
import unittest
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 django.contrib.auth.models import User, AnonymousUser
from django.test import TestCase, TransactionTestCase
......@@ -14,7 +14,8 @@ from django.conf import settings
from edxmako.shortcuts import render_to_string
from util.request import safe_get_host
from textwrap import dedent
from microsite_configuration import microsite
from xmodule.modulestore.tests.factories import CourseFactory
class TestException(Exception):
"""Exception used for testing that nothing will catch explicitly"""
......@@ -58,6 +59,48 @@ class EmailTestMixin(object):
settings.ALLOWED_HOSTS.append(hostname)
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('django.contrib.auth.models.User.email_user')
......
......@@ -8,6 +8,8 @@ import uuid
import time
from collections import defaultdict
from pytz import UTC
from pytz import timezone
import json
from django.conf import settings
from django.contrib.auth import logout, authenticate, login
......@@ -57,7 +59,7 @@ from xmodule.modulestore import ModuleStoreEnum
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.models import CoursePreference
......@@ -682,7 +684,11 @@ def change_enrollment(request):
current_mode = available_modes[0]
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()
elif action == "add_to_cart":
......@@ -706,6 +712,52 @@ def change_enrollment(request):
else:
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"):
"""
......
......@@ -169,6 +169,8 @@ class CourseFields(object):
default=[], 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_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings)
start = Date(help="Start time when this module is visible",
......
......@@ -162,6 +162,10 @@ def get_course_about_section(course, section_key):
- faq
- more_info
- 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
......@@ -173,7 +177,9 @@ def get_course_about_section(course, section_key):
'course_staff_short', 'course_staff_extended',
'requirements', 'syllabus', 'textbook', 'faq', 'more_info',
'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:
......
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