diff --git a/cms/djangoapps/contentstore/features/course-settings.feature b/cms/djangoapps/contentstore/features/course-settings.feature
index 0aeb95dbd9..ca8b3ec4cb 100644
--- a/cms/djangoapps/contentstore/features/course-settings.feature
+++ b/cms/djangoapps/contentstore/features/course-settings.feature
@@ -79,7 +79,6 @@ Feature: CMS.Course Settings
| Course Start Time | 11:00 |
| Course Introduction Video | 4r7wHMg5Yjg |
| Course Effort | 200:00 |
- | Course Image URL | image.jpg |
# Special case because we have to type in code mirror
Scenario: Changes in Course Overview show a confirmation
@@ -94,11 +93,3 @@ Feature: CMS.Course Settings
When I select Schedule and Details
And I change the "Course Start Date" field to ""
Then the save notification button is disabled
-
- Scenario: User can upload course image
- Given I have opened a new course in Studio
- When I select Schedule and Details
- And I click the "Upload Course Image" button
- And I upload a new course image
- Then I should see the new course image
- And the image URL should be present in the field
diff --git a/cms/djangoapps/contentstore/features/course-settings.py b/cms/djangoapps/contentstore/features/course-settings.py
index 4e11b393d1..350eb0392e 100644
--- a/cms/djangoapps/contentstore/features/course-settings.py
+++ b/cms/djangoapps/contentstore/features/course-settings.py
@@ -132,37 +132,6 @@ def test_change_course_overview(_step):
type_in_codemirror(0, "
Overview
")
-@step('I click the "Upload Course Image" button')
-def click_upload_button(_step):
- button_css = '.action-upload-image'
- world.css_click(button_css)
-
-
-@step('I upload a new course image$')
-def upload_new_course_image(_step):
- upload_file('image.jpg', sub_path="uploads")
-
-
-@step('I should see the new course image$')
-def i_see_new_course_image(_step):
- img_css = '#course-image'
- images = world.css_find(img_css)
- assert len(images) == 1
- img = images[0]
- expected_src = 'image.jpg'
-
- # Don't worry about the domain in the URL
- success_func = lambda _: img['src'].endswith(expected_src)
- world.wait_for(success_func)
-
-
-@step('the image URL should be present in the field')
-def image_url_present(_step):
- field_css = '#course-image-url'
- expected_value = 'image.jpg'
- assert world.css_value(field_css).endswith(expected_value)
-
-
############### HELPER METHODS ####################
def set_date_or_time(css, date_or_time):
"""
diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py
index 5d5c6a5d7c..5171d59059 100644
--- a/cms/djangoapps/contentstore/tests/test_course_settings.py
+++ b/cms/djangoapps/contentstore/tests/test_course_settings.py
@@ -255,7 +255,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
self.assertContains(response, "not the dates shown on your course summary page")
self.assertContains(response, "Introducing Your Course")
- self.assertContains(response, "Course Image")
+ self.assertContains(response, "Course Card Image")
self.assertContains(response, "Course Short Description")
self.assertNotContains(response, "Course Title")
self.assertNotContains(response, "Course Subtitle")
@@ -264,6 +264,8 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
self.assertNotContains(response, "Course Overview")
self.assertNotContains(response, "Course Introduction Video")
self.assertNotContains(response, "Requirements")
+ self.assertNotContains(response, "Course Banner Image")
+ self.assertNotContains(response, "Course Video Thumbnail Image")
@unittest.skipUnless(settings.FEATURES.get('ENTRANCE_EXAMS', False), True)
def test_entrance_exam_created_updated_and_deleted_successfully(self):
@@ -385,7 +387,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
self.assertNotContains(response, "not the dates shown on your course summary page")
self.assertContains(response, "Introducing Your Course")
- self.assertContains(response, "Course Image")
+ self.assertContains(response, "Course Card Image")
self.assertContains(response, "Course Title")
self.assertContains(response, "Course Subtitle")
self.assertContains(response, "Course Duration")
@@ -394,6 +396,8 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
self.assertContains(response, "Course Overview")
self.assertContains(response, "Course Introduction Video")
self.assertContains(response, "Requirements")
+ self.assertContains(response, "Course Banner Image")
+ self.assertContains(response, "Course Video Thumbnail Image")
@ddt.ddt
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index 0fcf6714c5..4450031b62 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -991,7 +991,9 @@ def settings_handler(request, course_key_string):
'context_course': course_module,
'course_locator': course_key,
'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_key),
- 'course_image_url': course_image_url(course_module),
+ 'course_image_url': course_image_url(course_module, 'course_image'),
+ 'banner_image_url': course_image_url(course_module, 'banner_image'),
+ 'video_thumbnail_image_url': course_image_url(course_module, 'video_thumbnail_image'),
'details_url': reverse_course_url('settings_handler', course_key),
'about_page_editable': about_page_editable,
'short_description_editable': short_description_editable,
diff --git a/cms/envs/bok_choy.env.json b/cms/envs/bok_choy.env.json
index 14115e3612..2e9c6086b6 100644
--- a/cms/envs/bok_choy.env.json
+++ b/cms/envs/bok_choy.env.json
@@ -76,7 +76,8 @@
"ALLOW_ALL_ADVANCED_COMPONENTS": true,
"ENABLE_CONTENT_LIBRARIES": true,
"ENABLE_SPECIAL_EXAMS": true,
- "SHOW_LANGUAGE_SELECTOR": true
+ "SHOW_LANGUAGE_SELECTOR": true,
+ "ENABLE_EXTENDED_COURSE_DETAILS": true
},
"FEEDBACK_SUBMISSION_EMAIL": "",
"GITHUB_REPO_ROOT": "** OVERRIDDEN **",
diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js
index 8742a8fe67..2e192e066b 100644
--- a/cms/static/js/models/settings/course_details.js
+++ b/cms/static/js/models/settings/course_details.js
@@ -23,6 +23,10 @@ var CourseDetails = Backbone.Model.extend({
license: null,
course_image_name: '', // the filename
course_image_asset_path: '', // the full URL (/c4x/org/course/num/asset/filename)
+ banner_image_name: '',
+ banner_image_asset_path: '',
+ video_thumbnail_image_name: '',
+ video_thumbnail_image_asset_path: '',
pre_requisite_courses: [],
entrance_exam_enabled : '',
entrance_exam_minimum_score_pct: '50'
diff --git a/cms/static/js/spec/views/settings/main_spec.js b/cms/static/js/spec/views/settings/main_spec.js
index 202ff4cf0d..5c686f4ac9 100644
--- a/cms/static/js/spec/views/settings/main_spec.js
+++ b/cms/static/js/spec/views/settings/main_spec.js
@@ -31,6 +31,10 @@ define([
effort : null,
course_image_name : '',
course_image_asset_path : '',
+ banner_image_name : '',
+ banner_image_asset_path : '',
+ video_thumbnail_image_name : '',
+ video_thumbnail_image_asset_path : '',
pre_requisite_courses : [],
entrance_exam_enabled : '',
entrance_exam_minimum_score_pct: '50',
@@ -182,5 +186,69 @@ define([
AjaxHelpers.expectJsonRequest(requests, 'POST', urlRoot, modelData);
});
+ it('should save title', function(){
+ var requests = AjaxHelpers.requests(this),
+ expectedJson = $.extend(true, {}, modelData, {
+ title: 'course title'
+ });
+
+ // Input some value.
+ this.view.$("#course-title").val('course title');
+ this.view.$("#course-title").trigger('change');
+ this.view.saveView();
+ AjaxHelpers.expectJsonRequest(
+ requests, 'POST', urlRoot, expectedJson
+ );
+ AjaxHelpers.respondWithJson(requests, expectedJson);
+ });
+
+ it('should save subtitle', function(){
+ var requests = AjaxHelpers.requests(this),
+ expectedJson = $.extend(true, {}, modelData, {
+ subtitle: 'course subtitle'
+ });
+
+ // Input some value.
+ this.view.$("#course-subtitle").val('course subtitle');
+ this.view.$("#course-subtitle").trigger('change');
+ this.view.saveView();
+ AjaxHelpers.expectJsonRequest(
+ requests, 'POST', urlRoot, expectedJson
+ );
+ AjaxHelpers.respondWithJson(requests, expectedJson);
+ });
+
+ it('should save duration', function(){
+ var requests = AjaxHelpers.requests(this),
+ expectedJson = $.extend(true, {}, modelData, {
+ duration: '8 weeks'
+ });
+
+ // Input some value.
+ this.view.$("#course-duration").val('8 weeks');
+ this.view.$("#course-duration").trigger('change');
+ this.view.saveView();
+ AjaxHelpers.expectJsonRequest(
+ requests, 'POST', urlRoot, expectedJson
+ );
+ AjaxHelpers.respondWithJson(requests, expectedJson);
+ });
+
+ it('should save description', function(){
+ var requests = AjaxHelpers.requests(this),
+ expectedJson = $.extend(true, {}, modelData, {
+ description: 'course description'
+ });
+
+ // Input some value.
+ this.view.$("#course-description").val('course description');
+ this.view.$("#course-description").trigger('change');
+ this.view.saveView();
+ AjaxHelpers.expectJsonRequest(
+ requests, 'POST', urlRoot, expectedJson
+ );
+ AjaxHelpers.respondWithJson(requests, expectedJson);
+ });
+
});
});
diff --git a/cms/static/js/views/settings/main.js b/cms/static/js/views/settings/main.js
index 81221969d6..063e5ec47f 100644
--- a/cms/static/js/views/settings/main.js
+++ b/cms/static/js/views/settings/main.js
@@ -34,10 +34,10 @@ var DetailsView = ValidatingView.extend({
this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' });
// Avoid showing broken image on mistyped/nonexistent image
- this.$el.find('img.course-image').error(function() {
+ this.$el.find('img').error(function() {
$(this).hide();
});
- this.$el.find('img.course-image').load(function() {
+ this.$el.find('img').load(function() {
$(this).show();
});
@@ -92,9 +92,17 @@ var DetailsView = ValidatingView.extend({
this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort'));
- var imageURL = this.model.get('course_image_asset_path');
- this.$el.find('#course-image-url').val(imageURL);
- this.$el.find('#course-image').attr('src', imageURL);
+ var courseImageURL = this.model.get('course_image_asset_path');
+ this.$el.find('#course-image-url').val(courseImageURL);
+ this.$el.find('#course-image').attr('src', courseImageURL);
+
+ var bannerImageURL = this.model.get('banner_image_asset_path');
+ this.$el.find('#banner-image-url').val(bannerImageURL);
+ this.$el.find('#banner-image').attr('src', bannerImageURL);
+
+ var videoThumbnailImageURL = this.model.get('video_thumbnail_image_asset_path');
+ this.$el.find('#video-thumbnail-image-url').val(videoThumbnailImageURL);
+ this.$el.find('#video-thumbnail-image').attr('src', videoThumbnailImageURL);
var pre_requisite_courses = this.model.get('pre_requisite_courses');
pre_requisite_courses = pre_requisite_courses.length > 0 ? pre_requisite_courses : '';
@@ -144,6 +152,8 @@ var DetailsView = ValidatingView.extend({
'intro_video' : 'course-introduction-video',
'effort' : "course-effort",
'course_image_asset_path': 'course-image-url',
+ 'banner_image_asset_path': 'banner-image-url',
+ 'video_thumbnail_image_asset_path': 'video-thumbnail-image-url',
'pre_requisite_courses': 'pre-requisite-course',
'entrance_exam_enabled': 'entrance-exam-enabled',
'entrance_exam_minimum_score_pct': 'entrance-exam-minimum-score-pct'
@@ -166,15 +176,13 @@ var DetailsView = ValidatingView.extend({
updateModel: function(event) {
switch (event.currentTarget.id) {
case 'course-image-url':
- this.setField(event);
- var url = $(event.currentTarget).val();
- var image_name = _.last(url.split('/'));
- this.model.set('course_image_name', image_name);
- // Wait to set the image src until the user stops typing
- clearTimeout(this.imageTimer);
- this.imageTimer = setTimeout(function() {
- $('#course-image').attr('src', $(event.currentTarget).val());
- }, 1000);
+ this.updateImageField(event, 'course_image_name', '#course-image');
+ break;
+ case 'banner-image-url':
+ this.updateImageField(event, 'banner_image_name', '#banner-image');
+ break;
+ case 'video-thumbnail-image-url':
+ this.updateImageField(event, 'video_thumbnail_image_name', '#video-thumbnail-image');
break;
case 'entrance-exam-enabled':
if($(event.currentTarget).is(":checked")){
@@ -232,7 +240,17 @@ var DetailsView = ValidatingView.extend({
break;
}
},
-
+ updateImageField: function(event, image_field, selector) {
+ this.setField(event);
+ var url = $(event.currentTarget).val();
+ var image_name = _.last(url.split('/'));
+ this.model.set(image_field, image_name);
+ // Wait to set the image src until the user stops typing
+ clearTimeout(this.imageTimer);
+ this.imageTimer = setTimeout(function() {
+ $(selector).attr('src', $(event.currentTarget).val());
+ }, 1000);
+ },
removeVideo: function(event) {
event.preventDefault();
if (this.model.has('intro_video')) {
@@ -316,8 +334,30 @@ var DetailsView = ValidatingView.extend({
uploadImage: function(event) {
event.preventDefault();
+ var title = "", selector = "", image_key = "", image_path_key = "";
+ switch (event.currentTarget.id) {
+ case 'upload-course-image':
+ title = gettext("Upload your course image.");
+ selector = "#course-image";
+ image_key = 'course_image_name';
+ image_path_key = 'course_image_asset_path';
+ break;
+ case 'upload-banner-image':
+ title = gettext("Upload your banner image.");
+ selector = "#banner-image";
+ image_key = 'banner_image_name';
+ image_path_key = 'banner_image_asset_path';
+ break;
+ case 'upload-video-thumbnail-image':
+ title = gettext("Upload your video thumbnail image.");
+ selector = "#video-thumbnail-image";
+ image_key = 'video_thumbnail_image_name';
+ image_path_key = 'video_thumbnail_image_asset_path';
+ break;
+ }
+
var upload = new FileUploadModel({
- title: gettext("Upload your course image."),
+ title: title,
message: gettext("Files must be in JPEG or PNG format."),
mimeTypes: ['image/jpeg', 'image/png']
});
@@ -325,13 +365,12 @@ var DetailsView = ValidatingView.extend({
var modal = new FileUploadDialog({
model: upload,
onSuccess: function(response) {
- var options = {
- 'course_image_name': response.asset.display_name,
- 'course_image_asset_path': response.asset.url
- };
+ var options = {};
+ options[image_key] = response.asset.display_name;
+ options[image_path_key] = response.asset.url;
self.model.set(options);
self.render();
- $('#course-image').attr('src', self.model.get('course_image_asset_path'));
+ $(selector).attr('src', self.model.get(image_path_key));
}
});
modal.show();
diff --git a/cms/static/sass/views/_settings.scss b/cms/static/sass/views/_settings.scss
index f753a42d20..0a56fec268 100644
--- a/cms/static/sass/views/_settings.scss
+++ b/cms/static/sass/views/_settings.scss
@@ -516,7 +516,7 @@
}
// specific fields - course image
- #field-course-image {
+ #field-course-image, #field-banner-image, #field-video-thumbnail-image {
.current-course-image {
margin-bottom: ($baseline/2);
diff --git a/cms/templates/js/mock/mock-settings-page.underscore b/cms/templates/js/mock/mock-settings-page.underscore
index 1422279fb9..cc92b82f9e 100644
--- a/cms/templates/js/mock/mock-settings-page.underscore
+++ b/cms/templates/js/mock/mock-settings-page.underscore
@@ -105,6 +105,96 @@
Identify the course language here. This is used to assist users find courses that are taught in a specific language.
+
+
+
+
+
Introducing Your Course
+ Information for prospective students
+
+
+
+
+
+
+ Displayed as title on the course details page. Limit to 50 characters.
+
+
+
+
+ Displayed as subtitle on the course details page. Limit to 150 characters.
+
+
+
+
+ Displayed on the course details page. Limit to 50 characters.
+
+
+
+
+ Displayed on the course details page. Limit to 1000 characters.
+
+
+
+
+
+
+
+
+ Your course currently does not have an image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 375px wide by 200px tall)
+
+
+
+
+ ## Translators: This is the placeholder text for a field that requests the URL for a course image
+
+ Please provide a valid path and name to your course image (Note: only JPEG or PNG format supported)
+
+
+
+
+
+
+
+
+
+
+
+ Your course currently does not have an image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 1440px wide by 400px tall)
+
+
+
+
+ ## Translators: This is the placeholder text for a field that requests the URL for a course banner image
+
+ Please provide a valid path and name to your banner image (Note: only JPEG or PNG format supported)
+
+
+
+
+
+
+
+
+
+
+
+ Your course currently does not have a video thumbnail image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 375px wide by 200px tall)
+
+
+
+
+ ## Translators: This is the placeholder text for a field that requests the URL for a course video thumbnail image
+
+ Please provide a valid path and name to your video thumbnail image (Note: only JPEG or PNG format supported)
+
- ${_("Displayed as hero image overlay on the course details page. Limit to 50 characters.")}
+ ${_("Displayed as title on the course details page. Limit to 50 characters.")}
- ${_("Displayed as hero image overlay on the course details page below the Course Title in a smaller font. Limit to 150 characters.")}
+ ${_("Displayed as subtitle on the course details page. Limit to 150 characters.")}
- ${_("Displayed on the course details page below the hero image. Limit to 50 characters.")}
+ ${_("Displayed on the course details page. Limit to 50 characters.")}
% if context_course.course_image:
-
+
@@ -352,7 +352,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'
% else:
-
+ ${_("Your course currently does not have an image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 375px wide by 200px tall)")}
% endif
@@ -364,10 +364,72 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'
${_("Please provide a valid path and name to your course image (Note: only JPEG or PNG format supported)")}
-
+
+ % if enable_extended_course_details:
+
+
+
+ % if context_course.banner_image:
+
+
+
+
+
+ ${_("You can manage this image along with all of your other files & uploads").format(upload_asset_url)}
+
+
+ % else:
+
+
+
+ ${_("Your course currently does not have an image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 1440px wide by 400px tall)")}
+ % endif
+
+
+
+
+ ## Translators: This is the placeholder text for a field that requests the URL for a course banner image
+
+ ${_("Please provide a valid path and name to your banner image (Note: only JPEG or PNG format supported)")}
+
+
+
+
+
+
+
+
+ % if context_course.video_thumbnail_image:
+
+
+
+
+
+ ${_("You can manage this image along with all of your other files & uploads").format(upload_asset_url)}
+
+
+ % else:
+
+
+
+ ${_("Your course currently does not have a video thumbnail image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 375px wide by 200px tall)")}
+ % endif
+
+
+
+
+ ## Translators: This is the placeholder text for a field that requests the URL for a course video thumbnail image
+
+ ${_("Please provide a valid path and name to your video thumbnail image (Note: only JPEG or PNG format supported)")}
+
+
+
+
+ % endif
+
% if about_page_editable:
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 9f15a46de9..6ed3385032 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -475,6 +475,26 @@ class CourseFields(object):
# Ensure that courses imported from XML keep their image
default="images_course_image.jpg"
)
+ banner_image = String(
+ display_name=_("Course Banner Image"),
+ help=_(
+ "Edit the name of the banner image file. "
+ "You can set the banner image on the Settings & Details page."
+ ),
+ scope=Scope.settings,
+ # Ensure that courses imported from XML keep their image
+ default="images_course_image.jpg"
+ )
+ video_thumbnail_image = String(
+ display_name=_("Course Video Thumbnail Image"),
+ help=_(
+ "Edit the name of the video thumbnail image file. "
+ "You can set the video thumbnail image on the Settings & Details page."
+ ),
+ scope=Scope.settings,
+ # Ensure that courses imported from XML keep their image
+ default="images_course_image.jpg"
+ )
issue_badges = Boolean(
display_name=_("Issue Open Badges"),
help=_(
diff --git a/common/test/acceptance/pages/studio/settings.py b/common/test/acceptance/pages/studio/settings.py
index 53e2102597..adb012f95d 100644
--- a/common/test/acceptance/pages/studio/settings.py
+++ b/common/test/acceptance/pages/studio/settings.py
@@ -3,6 +3,7 @@
Course Schedule and Details Settings page.
"""
from __future__ import unicode_literals
+import os
from bok_choy.promise import EmptyPromise
from bok_choy.javascript import requirejs
@@ -17,6 +18,9 @@ class SettingsPage(CoursePage):
"""
url_path = "settings/details"
+ upload_image_browse_button_selector = 'form.upload-dialog input[type=file]'
+ upload_image_upload_button_selector = '.modal-actions li:nth-child(1) a'
+ upload_image_popup_window_selector = '.assetupload-modal'
################
# Helpers
@@ -234,3 +238,50 @@ class SettingsPage(CoursePage):
).fulfill()
self.wait_for_require_js()
self.wait_for_ajax()
+
+ @staticmethod
+ def get_asset_path(file_name):
+ """
+ Returns the full path of the file to upload.
+ These files have been placed in edx-platform/common/test/data/uploads/
+ """
+
+ # Separate the list of folders in the path reaching to the current file,
+ # e.g. '... common/test/acceptance/pages/lms/instructor_dashboard.py' will result in
+ # [..., 'common', 'test', 'acceptance', 'pages', 'lms', 'instructor_dashboard.py']
+ folders_list_in_path = __file__.split(os.sep)
+
+ # Get rid of the last 4 elements: 'acceptance', 'pages', 'lms', and 'instructor_dashboard.py'
+ # to point to the 'test' folder, a shared point in the path's tree.
+ folders_list_in_path = folders_list_in_path[:-4]
+
+ # Append the folders in the asset's path
+ folders_list_in_path.extend(['data', 'uploads', file_name])
+
+ # Return the joined path of the required asset.
+ return os.sep.join(folders_list_in_path)
+
+ def upload_image(self, image_selector, file_to_upload):
+ """
+ Upload image specified by image_selector and file_to_upload
+ """
+
+ self.q(css=image_selector).results[0].click()
+
+ # wait for popup
+ self.wait_for_element_presence(self.upload_image_popup_window_selector, 'upload dialog is present')
+
+ # upload image
+ filepath = SettingsPage.get_asset_path(file_to_upload)
+ self.q(css=self.upload_image_browse_button_selector).results[0].send_keys(filepath)
+ self.q(css=self.upload_image_upload_button_selector).results[0].click()
+
+ # wait for popup closed
+ self.wait_for_element_absence(self.upload_image_popup_window_selector, 'upload dialog is hidden')
+
+ def get_uploaded_image_path(self, image_selector):
+ """
+ Returns the uploaded image path
+ """
+
+ return self.q(css=image_selector).attrs('src')[0]
diff --git a/common/test/acceptance/pages/studio/settings_advanced.py b/common/test/acceptance/pages/studio/settings_advanced.py
index e209207189..7f189a4cd9 100644
--- a/common/test/acceptance/pages/studio/settings_advanced.py
+++ b/common/test/acceptance/pages/studio/settings_advanced.py
@@ -172,6 +172,8 @@ class AdvancedSettingsPage(CoursePage):
'cert_name_short',
'certificates_display_behavior',
'course_image',
+ 'banner_image',
+ 'video_thumbnail_image',
'cosmetic_display_price',
'advertised_start',
'announcement',
diff --git a/common/test/acceptance/tests/studio/test_studio_settings.py b/common/test/acceptance/tests/studio/test_studio_settings.py
index 3e63ee7167..9b67b5dcd4 100644
--- a/common/test/acceptance/tests/studio/test_studio_settings.py
+++ b/common/test/acceptance/tests/studio/test_studio_settings.py
@@ -571,3 +571,42 @@ class StudioSubsectionSettingsA11yTest(StudioCourseTest):
include=['section.edit-settings-timed-examination']
)
self.course_outline.a11y_audit.check_for_accessibility_errors()
+
+
+class StudioSettingsImageUploadTest(StudioCourseTest):
+
+ """
+ Class to test course settings image uploads.
+ """
+
+ def setUp(self): # pylint: disable=arguments-differ
+ super(StudioSettingsImageUploadTest, self).setUp()
+ self.settings_page = SettingsPage(self.browser, self.course_info['org'], self.course_info['number'],
+ self.course_info['run'])
+
+ def test_upload_course_card_image(self):
+ self.settings_page.visit()
+ self.settings_page.wait_for_page()
+
+ # upload image
+ file_to_upload = 'image.jpg'
+ self.settings_page.upload_image('#upload-course-image', file_to_upload)
+ self.assertIn(file_to_upload, self.settings_page.get_uploaded_image_path('#course-image'))
+
+ def test_upload_course_banner_image(self):
+ self.settings_page.visit()
+ self.settings_page.wait_for_page()
+
+ # upload image
+ file_to_upload = 'image.jpg'
+ self.settings_page.upload_image('#upload-banner-image', file_to_upload)
+ self.assertIn(file_to_upload, self.settings_page.get_uploaded_image_path('#banner-image'))
+
+ def test_upload_course_video_thumbnail_image(self):
+ self.settings_page.visit()
+ self.settings_page.wait_for_page()
+
+ # upload image
+ file_to_upload = 'image.jpg'
+ self.settings_page.upload_image('#upload-video-thumbnail-image', file_to_upload)
+ self.assertIn(file_to_upload, self.settings_page.get_uploaded_image_path('#video-thumbnail-image'))
diff --git a/openedx/core/djangoapps/models/course_details.py b/openedx/core/djangoapps/models/course_details.py
index 980484d57a..c818642def 100644
--- a/openedx/core/djangoapps/models/course_details.py
+++ b/openedx/core/djangoapps/models/course_details.py
@@ -58,6 +58,10 @@ class CourseDetails(object):
self.license = "all-rights-reserved" # default course license is all rights reserved
self.course_image_name = ""
self.course_image_asset_path = "" # URL of the course image
+ self.banner_image_name = ""
+ self.banner_image_asset_path = ""
+ self.video_thumbnail_image_name = ""
+ self.video_thumbnail_image_asset_path = ""
self.pre_requisite_courses = [] # pre-requisite courses
self.entrance_exam_enabled = "" # is entrance exam enabled
self.entrance_exam_id = "" # the content location for the entrance exam
@@ -97,7 +101,11 @@ class CourseDetails(object):
course_details.enrollment_end = descriptor.enrollment_end
course_details.pre_requisite_courses = descriptor.pre_requisite_courses
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_image')
+ course_details.banner_image_name = descriptor.banner_image
+ course_details.banner_image_asset_path = course_image_url(descriptor, 'banner_image')
+ course_details.video_thumbnail_image_name = descriptor.video_thumbnail_image
+ course_details.video_thumbnail_image_asset_path = course_image_url(descriptor, 'video_thumbnail_image')
course_details.language = descriptor.language
course_details.self_paced = descriptor.self_paced
@@ -217,6 +225,15 @@ class CourseDetails(object):
descriptor.course_image = jsondict['course_image_name']
dirty = True
+ if 'banner_image_name' in jsondict and jsondict['banner_image_name'] != descriptor.banner_image:
+ descriptor.banner_image = jsondict['banner_image_name']
+ dirty = True
+
+ if 'video_thumbnail_image_name' in jsondict \
+ and jsondict['video_thumbnail_image_name'] != descriptor.video_thumbnail_image:
+ descriptor.video_thumbnail_image = jsondict['video_thumbnail_image_name']
+ dirty = True
+
if 'pre_requisite_courses' in jsondict \
and sorted(jsondict['pre_requisite_courses']) != sorted(descriptor.pre_requisite_courses):
descriptor.pre_requisite_courses = jsondict['pre_requisite_courses']
diff --git a/openedx/core/djangoapps/models/tests/test_course_details.py b/openedx/core/djangoapps/models/tests/test_course_details.py
index 46f39982cf..fd778ee10a 100644
--- a/openedx/core/djangoapps/models/tests/test_course_details.py
+++ b/openedx/core/djangoapps/models/tests/test_course_details.py
@@ -95,6 +95,16 @@ class CourseDetailsTestCase(ModuleStoreTestCase):
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).course_image_name,
jsondetails.course_image_name
)
+ jsondetails.banner_image_name = "an_image.jpg"
+ self.assertEqual(
+ CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).banner_image_name,
+ jsondetails.banner_image_name
+ )
+ jsondetails.video_thumbnail_image_name = "an_image.jpg"
+ self.assertEqual(
+ CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).video_thumbnail_image_name,
+ jsondetails.video_thumbnail_image_name
+ )
jsondetails.language = "hr"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).language,
diff --git a/openedx/core/lib/courses.py b/openedx/core/lib/courses.py
index ca5d3f13d8..77f3920b10 100644
--- a/openedx/core/lib/courses.py
+++ b/openedx/core/lib/courses.py
@@ -10,24 +10,25 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore import ModuleStoreEnum
-def course_image_url(course):
+def course_image_url(course, image_key='course_image'):
"""Try to look up the image url for the course. If it's not found,
- log an error and return the dead link"""
+ log an error and return the dead link.
+ image_key can be one of the three: 'course_image', 'hero_image', 'thumbnail_image' """
if course.static_asset_path:
- # If we are a static course with the course_image attribute
+ # If we are a static course with the image_key attribute
# set different than the default, return that path so that
# courses can use custom course image paths, otherwise just
# return the default static path.
url = '/static/' + (course.static_asset_path or getattr(course, 'data_dir', ''))
- if hasattr(course, 'course_image') and course.course_image != course.fields['course_image'].default:
- url += '/' + course.course_image
+ if hasattr(course, image_key) and getattr(course, image_key) != course.fields[image_key].default:
+ url += '/' + getattr(course, image_key)
else:
- url += '/images/course_image.jpg'
- elif not course.course_image:
- # if course_image is empty, use the default image url from settings
+ url += '/images/' + image_key + '.jpg'
+ elif not getattr(course, image_key):
+ # if image_key is empty, use the default image url from settings
url = settings.STATIC_URL + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL
else:
- loc = StaticContent.compute_location(course.id, course.course_image)
+ loc = StaticContent.compute_location(course.id, getattr(course, image_key))
url = StaticContent.serialize_asset_key_with_slash(loc)
return url
diff --git a/openedx/core/lib/tests/test_courses.py b/openedx/core/lib/tests/test_courses.py
index 696f9e503b..45a8b47cdb 100644
--- a/openedx/core/lib/tests/test_courses.py
+++ b/openedx/core/lib/tests/test_courses.py
@@ -65,3 +65,21 @@ class CourseImageTestCase(ModuleStoreTestCase):
'static/test.png',
course_image_url(course),
)
+
+ def test_get_banner_image_url(self):
+ """Test banner image URL formatting."""
+ banner_image = u'banner_image.jpg'
+ course = CourseFactory.create(banner_image=banner_image)
+ self.verify_url(
+ unicode(course.id.make_asset_key('asset', banner_image)),
+ course_image_url(course, 'banner_image')
+ )
+
+ def test_get_video_thumbnail_image_url(self):
+ """Test video thumbnail image URL formatting."""
+ thumbnail_image = u'thumbnail_image.jpg'
+ course = CourseFactory.create(video_thumbnail_image=thumbnail_image)
+ self.verify_url(
+ unicode(course.id.make_asset_key('asset', thumbnail_image)),
+ course_image_url(course, 'video_thumbnail_image')
+ )