Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
aac28d08
Commit
aac28d08
authored
Jul 14, 2014
by
sjang92
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #74 from Stanford-Online/sjang92/origin/enrollment-email
Sjang92/origin/enrollment email
parents
44129cc4
496eff88
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
347 additions
and
14 deletions
+347
-14
AUTHORS
+3
-2
cms/djangoapps/contentstore/tests/test_course_settings.py
+2
-0
cms/djangoapps/models/settings/course_details.py
+43
-1
cms/static/js/models/settings/course_details.js
+5
-4
cms/static/js/views/settings/main.js
+92
-1
cms/static/sass/views/_settings.scss
+43
-0
cms/templates/settings.html
+48
-1
common/djangoapps/student/models.py
+3
-0
common/djangoapps/student/tests/test_email.py
+45
-2
common/djangoapps/student/views.py
+54
-2
common/lib/xmodule/xmodule/course_module.py
+2
-0
lms/djangoapps/courseware/courses.py
+7
-1
No files found.
AUTHORS
View file @
aac28d08
...
@@ -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>
cms/djangoapps/contentstore/tests/test_course_settings.py
View file @
aac28d08
...
@@ -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
)
...
...
cms/djangoapps/models/settings/course_details.py
View file @
aac28d08
...
@@ -24,10 +24,16 @@ class CourseDetails(object):
...
@@ -24,10 +24,16 @@ class CourseDetails(object):
self
.
syllabus
=
None
# a pdf file asset
self
.
syllabus
=
None
# a pdf file asset
self
.
short_description
=
""
self
.
short_description
=
""
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
.
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
):
...
@@ -43,6 +49,8 @@ class CourseDetails(object):
...
@@ -43,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
:
...
@@ -61,6 +69,28 @@ class CourseDetails(object):
...
@@ -61,6 +69,28 @@ 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'
)
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'
)
temploc
=
course_key
.
make_usage_key
(
'about'
,
'effort'
)
try
:
try
:
...
@@ -110,6 +140,16 @@ class CourseDetails(object):
...
@@ -110,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'
])
...
@@ -155,7 +195,9 @@ class CourseDetails(object):
...
@@ -155,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'
]:
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'
])
...
...
cms/static/js/models/settings/course_details.js
View file @
aac28d08
...
@@ -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)
...
...
cms/static/js/views/settings/main.js
View file @
aac28d08
...
@@ -12,6 +12,10 @@ var DetailsView = ValidatingView.extend({
...
@@ -12,6 +12,10 @@ var DetailsView = ValidatingView.extend({
"change textarea"
:
"updateModel"
,
"change textarea"
:
"updateModel"
,
'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-default-enrollment-email'
:
"toggleDefaultEnrollmentEmails"
,
'focus #pre-enrollment-email'
:
"codeMirrorize"
,
'focus #post-enrollment-email'
:
"codeMirrorize"
,
'mouseover #timezone'
:
"updateTime"
,
'mouseover #timezone'
:
"updateTime"
,
// would love to move to a general superclass, but event hashes don't inherit in backbone :-(
// would love to move to a general superclass, but event hashes don't inherit in backbone :-(
'focus :input'
:
"inputFocus"
,
'focus :input'
:
"inputFocus"
,
...
@@ -40,6 +44,21 @@ var DetailsView = ValidatingView.extend({
...
@@ -40,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
()
{
...
@@ -51,6 +70,31 @@ var DetailsView = ValidatingView.extend({
...
@@ -51,6 +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
.
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
(
'#'
+
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
());
...
@@ -74,10 +118,16 @@ var DetailsView = ValidatingView.extend({
...
@@ -74,10 +118,16 @@ var DetailsView = ValidatingView.extend({
'enrollment_start'
:
'enrollment-start'
,
'enrollment_start'
:
'enrollment-start'
,
'enrollment_end'
:
'enrollment-end'
,
'enrollment_end'
:
'enrollment-end'
,
'overview'
:
'course-overview'
,
'overview'
:
'course-overview'
,
'pre_enrollment_email'
:
'pre-enrollment-email'
,
'post_enrollment_email'
:
'post-enrollment-email'
,
'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
)
{
...
@@ -152,6 +202,12 @@ var DetailsView = ValidatingView.extend({
...
@@ -152,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
;
...
@@ -185,6 +241,41 @@ var DetailsView = ValidatingView.extend({
...
@@ -185,6 +241,41 @@ var DetailsView = ValidatingView.extend({
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
hide
();
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
:
{},
codeMirrors
:
{},
codeMirrorize
:
function
(
e
,
forcedTarget
)
{
codeMirrorize
:
function
(
e
,
forcedTarget
)
{
var
thisTarget
;
var
thisTarget
;
...
...
cms/static/sass/views/_settings.scss
View file @
aac28d08
...
@@ -202,6 +202,23 @@
...
@@ -202,6 +202,23 @@
display
:
inline-block
;
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
{
.field-group
{
...
@@ -404,6 +421,32 @@
...
@@ -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
// specific fields - video
#field-course-introduction-video
{
#field-course-introduction-video
{
...
...
cms/templates/settings.html
View file @
aac28d08
...
@@ -205,7 +205,7 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
...
@@ -205,7 +205,7 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
<ol
class=
"list-input"
>
<ol
class=
"list-input"
>
% if short_description_editable:
% if short_description_editable:
<li
class=
"field text"
id=
"field-course-short-description"
>
<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>
<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>
<span
class=
"tip tip-stacked"
>
${_("Appears on the course catalog page when students roll over the course name. Limit to ~150 characters")}
</span>
</li>
</li>
...
@@ -225,6 +225,53 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
...
@@ -225,6 +225,53 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
</li>
</li>
% endif
% 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"
>
<li
class=
"field image"
id=
"field-course-image"
>
<label>
${_("Course Image")}
</label>
<label>
${_("Course Image")}
</label>
<div
class=
"current current-course-image"
>
<div
class=
"current current-course-image"
>
...
...
common/djangoapps/student/models.py
View file @
aac28d08
...
@@ -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.
"""
"""
...
...
common/djangoapps/student/tests/test_email.py
View file @
aac28d08
...
@@ -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'
)
...
...
common/djangoapps/student/views.py
View file @
aac28d08
...
@@ -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"
):
"""
"""
...
...
common/lib/xmodule/xmodule/course_module.py
View file @
aac28d08
...
@@ -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"
,
...
...
lms/djangoapps/courseware/courses.py
View file @
aac28d08
...
@@ -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
:
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment