Commit f7f281b6 by Ibrahim Committed by Douglas Hall

WL-398 Add Course Background Image and Video Thumbnail Image Fields to Studio

parent c283afcc
...@@ -79,7 +79,6 @@ Feature: CMS.Course Settings ...@@ -79,7 +79,6 @@ Feature: CMS.Course Settings
| Course Start Time | 11:00 | | Course Start Time | 11:00 |
| Course Introduction Video | 4r7wHMg5Yjg | | Course Introduction Video | 4r7wHMg5Yjg |
| Course Effort | 200:00 | | Course Effort | 200:00 |
| Course Image URL | image.jpg |
# Special case because we have to type in code mirror # Special case because we have to type in code mirror
Scenario: Changes in Course Overview show a confirmation Scenario: Changes in Course Overview show a confirmation
...@@ -94,11 +93,3 @@ Feature: CMS.Course Settings ...@@ -94,11 +93,3 @@ Feature: CMS.Course Settings
When I select Schedule and Details When I select Schedule and Details
And I change the "Course Start Date" field to "" And I change the "Course Start Date" field to ""
Then the save notification button is disabled 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
...@@ -132,37 +132,6 @@ def test_change_course_overview(_step): ...@@ -132,37 +132,6 @@ def test_change_course_overview(_step):
type_in_codemirror(0, "<h1>Overview</h1>") type_in_codemirror(0, "<h1>Overview</h1>")
@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 #################### ############### HELPER METHODS ####################
def set_date_or_time(css, date_or_time): def set_date_or_time(css, date_or_time):
""" """
......
...@@ -255,7 +255,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): ...@@ -255,7 +255,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
self.assertContains(response, "not the dates shown on your course summary page") self.assertContains(response, "not the dates shown on your course summary page")
self.assertContains(response, "Introducing Your Course") self.assertContains(response, "Introducing Your Course")
self.assertContains(response, "Course Image") self.assertContains(response, "Course Card Image")
self.assertContains(response, "Course Short Description") self.assertContains(response, "Course Short Description")
self.assertNotContains(response, "Course Title") self.assertNotContains(response, "Course Title")
self.assertNotContains(response, "Course Subtitle") self.assertNotContains(response, "Course Subtitle")
...@@ -264,6 +264,8 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): ...@@ -264,6 +264,8 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
self.assertNotContains(response, "Course Overview") self.assertNotContains(response, "Course Overview")
self.assertNotContains(response, "Course Introduction Video") self.assertNotContains(response, "Course Introduction Video")
self.assertNotContains(response, "Requirements") 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) @unittest.skipUnless(settings.FEATURES.get('ENTRANCE_EXAMS', False), True)
def test_entrance_exam_created_updated_and_deleted_successfully(self): def test_entrance_exam_created_updated_and_deleted_successfully(self):
...@@ -385,7 +387,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): ...@@ -385,7 +387,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
self.assertNotContains(response, "not the dates shown on your course summary page") self.assertNotContains(response, "not the dates shown on your course summary page")
self.assertContains(response, "Introducing Your Course") 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 Title")
self.assertContains(response, "Course Subtitle") self.assertContains(response, "Course Subtitle")
self.assertContains(response, "Course Duration") self.assertContains(response, "Course Duration")
...@@ -394,6 +396,8 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): ...@@ -394,6 +396,8 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
self.assertContains(response, "Course Overview") self.assertContains(response, "Course Overview")
self.assertContains(response, "Course Introduction Video") self.assertContains(response, "Course Introduction Video")
self.assertContains(response, "Requirements") self.assertContains(response, "Requirements")
self.assertContains(response, "Course Banner Image")
self.assertContains(response, "Course Video Thumbnail Image")
@ddt.ddt @ddt.ddt
......
...@@ -991,7 +991,9 @@ def settings_handler(request, course_key_string): ...@@ -991,7 +991,9 @@ def settings_handler(request, course_key_string):
'context_course': course_module, 'context_course': course_module,
'course_locator': course_key, 'course_locator': course_key,
'lms_link_for_about_page': utils.get_lms_link_for_about_page(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), 'details_url': reverse_course_url('settings_handler', course_key),
'about_page_editable': about_page_editable, 'about_page_editable': about_page_editable,
'short_description_editable': short_description_editable, 'short_description_editable': short_description_editable,
......
...@@ -76,7 +76,8 @@ ...@@ -76,7 +76,8 @@
"ALLOW_ALL_ADVANCED_COMPONENTS": true, "ALLOW_ALL_ADVANCED_COMPONENTS": true,
"ENABLE_CONTENT_LIBRARIES": true, "ENABLE_CONTENT_LIBRARIES": true,
"ENABLE_SPECIAL_EXAMS": true, "ENABLE_SPECIAL_EXAMS": true,
"SHOW_LANGUAGE_SELECTOR": true "SHOW_LANGUAGE_SELECTOR": true,
"ENABLE_EXTENDED_COURSE_DETAILS": true
}, },
"FEEDBACK_SUBMISSION_EMAIL": "", "FEEDBACK_SUBMISSION_EMAIL": "",
"GITHUB_REPO_ROOT": "** OVERRIDDEN **", "GITHUB_REPO_ROOT": "** OVERRIDDEN **",
......
...@@ -23,6 +23,10 @@ var CourseDetails = Backbone.Model.extend({ ...@@ -23,6 +23,10 @@ var CourseDetails = Backbone.Model.extend({
license: null, license: 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)
banner_image_name: '',
banner_image_asset_path: '',
video_thumbnail_image_name: '',
video_thumbnail_image_asset_path: '',
pre_requisite_courses: [], pre_requisite_courses: [],
entrance_exam_enabled : '', entrance_exam_enabled : '',
entrance_exam_minimum_score_pct: '50' entrance_exam_minimum_score_pct: '50'
......
...@@ -31,6 +31,10 @@ define([ ...@@ -31,6 +31,10 @@ define([
effort : null, effort : null,
course_image_name : '', course_image_name : '',
course_image_asset_path : '', course_image_asset_path : '',
banner_image_name : '',
banner_image_asset_path : '',
video_thumbnail_image_name : '',
video_thumbnail_image_asset_path : '',
pre_requisite_courses : [], pre_requisite_courses : [],
entrance_exam_enabled : '', entrance_exam_enabled : '',
entrance_exam_minimum_score_pct: '50', entrance_exam_minimum_score_pct: '50',
...@@ -182,5 +186,69 @@ define([ ...@@ -182,5 +186,69 @@ define([
AjaxHelpers.expectJsonRequest(requests, 'POST', urlRoot, modelData); 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);
});
}); });
}); });
...@@ -34,10 +34,10 @@ var DetailsView = ValidatingView.extend({ ...@@ -34,10 +34,10 @@ var DetailsView = ValidatingView.extend({
this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' }); this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' });
// Avoid showing broken image on mistyped/nonexistent image // 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).hide();
}); });
this.$el.find('img.course-image').load(function() { this.$el.find('img').load(function() {
$(this).show(); $(this).show();
}); });
...@@ -92,9 +92,17 @@ var DetailsView = ValidatingView.extend({ ...@@ -92,9 +92,17 @@ var DetailsView = ValidatingView.extend({
this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort')); this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort'));
var imageURL = this.model.get('course_image_asset_path'); var courseImageURL = this.model.get('course_image_asset_path');
this.$el.find('#course-image-url').val(imageURL); this.$el.find('#course-image-url').val(courseImageURL);
this.$el.find('#course-image').attr('src', imageURL); 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'); var pre_requisite_courses = this.model.get('pre_requisite_courses');
pre_requisite_courses = pre_requisite_courses.length > 0 ? pre_requisite_courses : ''; pre_requisite_courses = pre_requisite_courses.length > 0 ? pre_requisite_courses : '';
...@@ -144,6 +152,8 @@ var DetailsView = ValidatingView.extend({ ...@@ -144,6 +152,8 @@ var DetailsView = ValidatingView.extend({
'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',
'banner_image_asset_path': 'banner-image-url',
'video_thumbnail_image_asset_path': 'video-thumbnail-image-url',
'pre_requisite_courses': 'pre-requisite-course', 'pre_requisite_courses': 'pre-requisite-course',
'entrance_exam_enabled': 'entrance-exam-enabled', 'entrance_exam_enabled': 'entrance-exam-enabled',
'entrance_exam_minimum_score_pct': 'entrance-exam-minimum-score-pct' 'entrance_exam_minimum_score_pct': 'entrance-exam-minimum-score-pct'
...@@ -166,15 +176,13 @@ var DetailsView = ValidatingView.extend({ ...@@ -166,15 +176,13 @@ var DetailsView = ValidatingView.extend({
updateModel: function(event) { updateModel: function(event) {
switch (event.currentTarget.id) { switch (event.currentTarget.id) {
case 'course-image-url': case 'course-image-url':
this.setField(event); this.updateImageField(event, 'course_image_name', '#course-image');
var url = $(event.currentTarget).val(); break;
var image_name = _.last(url.split('/')); case 'banner-image-url':
this.model.set('course_image_name', image_name); this.updateImageField(event, 'banner_image_name', '#banner-image');
// Wait to set the image src until the user stops typing break;
clearTimeout(this.imageTimer); case 'video-thumbnail-image-url':
this.imageTimer = setTimeout(function() { this.updateImageField(event, 'video_thumbnail_image_name', '#video-thumbnail-image');
$('#course-image').attr('src', $(event.currentTarget).val());
}, 1000);
break; break;
case 'entrance-exam-enabled': case 'entrance-exam-enabled':
if($(event.currentTarget).is(":checked")){ if($(event.currentTarget).is(":checked")){
...@@ -232,7 +240,17 @@ var DetailsView = ValidatingView.extend({ ...@@ -232,7 +240,17 @@ var DetailsView = ValidatingView.extend({
break; 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) { removeVideo: function(event) {
event.preventDefault(); event.preventDefault();
if (this.model.has('intro_video')) { if (this.model.has('intro_video')) {
...@@ -316,8 +334,30 @@ var DetailsView = ValidatingView.extend({ ...@@ -316,8 +334,30 @@ var DetailsView = ValidatingView.extend({
uploadImage: function(event) { uploadImage: function(event) {
event.preventDefault(); 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({ var upload = new FileUploadModel({
title: gettext("Upload your course image."), title: title,
message: gettext("Files must be in JPEG or PNG format."), message: gettext("Files must be in JPEG or PNG format."),
mimeTypes: ['image/jpeg', 'image/png'] mimeTypes: ['image/jpeg', 'image/png']
}); });
...@@ -325,13 +365,12 @@ var DetailsView = ValidatingView.extend({ ...@@ -325,13 +365,12 @@ var DetailsView = ValidatingView.extend({
var modal = new FileUploadDialog({ var modal = new FileUploadDialog({
model: upload, model: upload,
onSuccess: function(response) { onSuccess: function(response) {
var options = { var options = {};
'course_image_name': response.asset.display_name, options[image_key] = response.asset.display_name;
'course_image_asset_path': response.asset.url options[image_path_key] = response.asset.url;
};
self.model.set(options); self.model.set(options);
self.render(); self.render();
$('#course-image').attr('src', self.model.get('course_image_asset_path')); $(selector).attr('src', self.model.get(image_path_key));
} }
}); });
modal.show(); modal.show();
......
...@@ -516,7 +516,7 @@ ...@@ -516,7 +516,7 @@
} }
// specific fields - course image // specific fields - course image
#field-course-image { #field-course-image, #field-banner-image, #field-video-thumbnail-image {
.current-course-image { .current-course-image {
margin-bottom: ($baseline/2); margin-bottom: ($baseline/2);
......
...@@ -105,6 +105,96 @@ ...@@ -105,6 +105,96 @@
</select> </select>
<span class="tip tip-stacked">Identify the course language here. This is used to assist users find courses that are taught in a specific language.</span> <span class="tip tip-stacked">Identify the course language here. This is used to assist users find courses that are taught in a specific language.</span>
</li> </li>
</ol> </ol>
</section> </section>
<section class="group-settings marketing">
<header>
<h2 class="title-2">Introducing Your Course</h2>
<span class="tip">Information for prospective students</span>
</header>
<ol class="list-input">
<li class="field text" id="field-course-title">
<label for="course-title">Course Title</label>
<input type="text" id="course-title" data-display-name="${context_course.display_name}">
<span class="tip tip-stacked">Displayed as title on the course details page. Limit to 50 characters.</span>
</li>
<li class="field text" id="field-course-subtitle">
<label for="course-subtitle">Course Subtitle</label>
<input type="text" id="course-subtitle">
<span class="tip tip-stacked">Displayed as subtitle on the course details page. Limit to 150 characters.</span>
</li>
<li class="field text" id="field-course-duration">
<label for="course-duration">Course Duration</label>
<input type="text" id="course-duration">
<span class="tip tip-stacked">Displayed on the course details page. Limit to 50 characters.</span>
</li>
<li class="field text" id="field-course-description">
<label for="course-description">Course Description</label>
<textarea class="text" id="course-description"></textarea>
<span class="tip tip-stacked">Displayed on the course details page. Limit to 1000 characters.</span>
</li>
<li class="field image" id="field-course-image">
<label for="course-image-url">Course Card Image</label>
<div class="current current-course-image">
<span class="wrapper-course-image">
<img class="course-image placeholder" id="course-image" src="${course_image_url}" alt="Course Image"/>
</span>
<span class="msg msg-empty">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)</span>
</div>
<div class="wrapper-input">
<div class="input">
## Translators: This is the placeholder text for a field that requests the URL for a course image
<input type="text" dir="ltr" class="long new-course-image-url" id="course-image-url" value="" placeholder="Your course image URL" autocomplete="off" />
<span class="tip tip-stacked">Please provide a valid path and name to your course image (Note: only JPEG or PNG format supported)</span>
</div>
<button type="button" class="action action-upload-image" id="upload-course-image">Upload Course Card Image</button>
</div>
</li>
<li class="field image" id="field-banner-image">
<label for="banner-image-url">Course Banner Image</label>
<div class="current current-course-image">
<span class="wrapper-course-image">
<img class="course-image placeholder" id="banner-image" src="${banner_image_url}" alt="Course Banner Image"/>
</span>
<span class="msg msg-empty">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)</span>
</div>
<div class="wrapper-input">
<div class="input">
## Translators: This is the placeholder text for a field that requests the URL for a course banner image
<input type="text" dir="ltr" class="long new-course-image-url" id="banner-image-url" value="" placeholder="Your banner image URL" autocomplete="off" />
<span class="tip tip-stacked">Please provide a valid path and name to your banner image (Note: only JPEG or PNG format supported)</span>
</div>
<button type="button" class="action action-upload-image" id="upload-banner-image">Upload Course Banner Image</button>
</div>
</li>
<li class="field image" id="field-video-thumbnail-image">
<label for="video-thumbnail-image-url">Course Video Thumbnail Image</label>
<div class="current current-course-image">
<span class="wrapper-course-image">
<img class="course-image placeholder" id="video-thumbnail-image" src="${video_thumbnail_image_url}" alt="Video Thumbnail Image"/>
</span>
<span class="msg msg-empty">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)</span>
</div>
<div class="wrapper-input">
<div class="input">
## Translators: This is the placeholder text for a field that requests the URL for a course video thumbnail image
<input type="text" dir="ltr" class="long new-course-image-url" id="video-thumbnail-image-url" value="" placeholder="Your video thumbnail image URL" autocomplete="off" />
<span class="tip tip-stacked">Please provide a valid path and name to your video thumbnail image (Note: only JPEG or PNG format supported)</span>
</div>
<button type="button" class="action action-upload-image" id="upload-video-thumbnail-image">Upload Video Thumbnail Image</button>
</div>
</li>
</ol>
</section>
</form> </form>
...@@ -297,17 +297,17 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}' ...@@ -297,17 +297,17 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'
<li class="field text" id="field-course-title"> <li class="field text" id="field-course-title">
<label for="course-title">${_("Course Title")}</label> <label for="course-title">${_("Course Title")}</label>
<input type="text" id="course-title" data-display-name="${context_course.display_name}"> <input type="text" id="course-title" data-display-name="${context_course.display_name}">
<span class="tip tip-stacked">${_("Displayed as hero image overlay on the course details page. Limit to 50 characters.")}</span> <span class="tip tip-stacked">${_("Displayed as title on the course details page. Limit to 50 characters.")}</span>
</li> </li>
<li class="field text" id="field-course-subtitle"> <li class="field text" id="field-course-subtitle">
<label for="course-subtitle">${_("Course Subtitle")}</label> <label for="course-subtitle">${_("Course Subtitle")}</label>
<input type="text" id="course-subtitle"> <input type="text" id="course-subtitle">
<span class="tip tip-stacked">${_("Displayed as hero image overlay on the course details page below the Course Title in a smaller font. Limit to 150 characters.")}</span> <span class="tip tip-stacked">${_("Displayed as subtitle on the course details page. Limit to 150 characters.")}</span>
</li> </li>
<li class="field text" id="field-course-duration"> <li class="field text" id="field-course-duration">
<label for="course-duration">${_("Course Duration")}</label> <label for="course-duration">${_("Course Duration")}</label>
<input type="text" id="course-duration"> <input type="text" id="course-duration">
<span class="tip tip-stacked">${_("Displayed on the course details page below the hero image. Limit to 50 characters.")}</span> <span class="tip tip-stacked">${_("Displayed on the course details page. Limit to 50 characters.")}</span>
</li> </li>
<li class="field text" id="field-course-description"> <li class="field text" id="field-course-description">
<label for="course-description">${_("Course Description")}</label> <label for="course-description">${_("Course Description")}</label>
...@@ -339,11 +339,11 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}' ...@@ -339,11 +339,11 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'
% endif % endif
<li class="field image" id="field-course-image"> <li class="field image" id="field-course-image">
<label for="course-image-url">${_("Course Image")}</label> <label for="course-image-url">${_("Course Card Image")}</label>
<div class="current current-course-image"> <div class="current current-course-image">
% if context_course.course_image: % if context_course.course_image:
<span class="wrapper-course-image"> <span class="wrapper-course-image">
<img class="course-image" id="course-image" src="${course_image_url}" alt="${_('Course Image')}"/> <img class="course-image" id="course-image" src="${course_image_url}" alt="${_('Course Card Image')}"/>
</span> </span>
<span class="msg msg-help"> <span class="msg msg-help">
...@@ -352,7 +352,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}' ...@@ -352,7 +352,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'
% else: % else:
<span class="wrapper-course-image"> <span class="wrapper-course-image">
<img class="course-image placeholder" id="course-image" src="${course_image_url}" alt="${_('Course Image')}"/> <img class="course-image placeholder" id="course-image" src="${course_image_url}" alt="${_('Course Card Image')}"/>
</span> </span>
<span class="msg msg-empty">${_("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)")}</span> <span class="msg msg-empty">${_("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)")}</span>
% endif % endif
...@@ -364,10 +364,72 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}' ...@@ -364,10 +364,72 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'
<input type="text" dir="ltr" class="long new-course-image-url" id="course-image-url" value="" placeholder="${_("Your course image URL")}" autocomplete="off" /> <input type="text" dir="ltr" class="long new-course-image-url" id="course-image-url" value="" placeholder="${_("Your course image URL")}" autocomplete="off" />
<span class="tip tip-stacked">${_("Please provide a valid path and name to your course image (Note: only JPEG or PNG format supported)")}</span> <span class="tip tip-stacked">${_("Please provide a valid path and name to your course image (Note: only JPEG or PNG format supported)")}</span>
</div> </div>
<button type="button" class="action action-upload-image">${_("Upload Course Image")}</button> <button type="button" class="action action-upload-image" id="upload-course-image">${_("Upload Course Card Image")}</button>
</div> </div>
</li> </li>
% if enable_extended_course_details:
<li class="field image" id="field-banner-image">
<label for="banner-image-url">${_("Course Banner Image")}</label>
<div class="current current-course-image">
% if context_course.banner_image:
<span class="wrapper-course-image">
<img class="course-image" id="banner-image" src="${banner_image_url}" alt="${_('Course Banner Image')}"/>
</span>
<span class="msg msg-help">
${_("You can manage this image along with all of your other <a href='{}'>files &amp; uploads</a>").format(upload_asset_url)}
</span>
% else:
<span class="wrapper-course-image">
<img class="course-image placeholder" id="banner-image" src="${banner_image_url}" alt="${_('Course Banner Image')}"/>
</span>
<span class="msg msg-empty">${_("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)")}</span>
% endif
</div>
<div class="wrapper-input">
<div class="input">
## Translators: This is the placeholder text for a field that requests the URL for a course banner image
<input type="text" dir="ltr" class="long new-course-image-url" id="banner-image-url" value="" placeholder="${_("Your banner image URL")}" autocomplete="off" />
<span class="tip tip-stacked">${_("Please provide a valid path and name to your banner image (Note: only JPEG or PNG format supported)")}</span>
</div>
<button type="button" class="action action-upload-image" id="upload-banner-image">${_("Upload Course Banner Image")}</button>
</div>
</li>
<li class="field image" id="field-video-thumbnail-image">
<label for="video-thumbnail-image-url">${_("Course Video Thumbnail Image")}</label>
<div class="current current-course-image">
% if context_course.video_thumbnail_image:
<span class="wrapper-course-image">
<img class="course-image" id="video-thumbnail-image" src="${video_thumbnail_image_url}" alt="${_('Video Thumbnail Image')}"/>
</span>
<span class="msg msg-help">
${_("You can manage this image along with all of your other <a href='{}'>files &amp; uploads</a>").format(upload_asset_url)}
</span>
% else:
<span class="wrapper-course-image">
<img class="course-image placeholder" id="video-thumbnail-image" src="${video_thumbnail_image_url}" alt="${_('Video Thumbnail Image')}"/>
</span>
<span class="msg msg-empty">${_("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)")}</span>
% endif
</div>
<div class="wrapper-input">
<div class="input">
## Translators: This is the placeholder text for a field that requests the URL for a course video thumbnail image
<input type="text" dir="ltr" class="long new-course-image-url" id="video-thumbnail-image-url" value="" placeholder="${_("Your video thumbnail image URL")}" autocomplete="off" />
<span class="tip tip-stacked">${_("Please provide a valid path and name to your video thumbnail image (Note: only JPEG or PNG format supported)")}</span>
</div>
<button type="button" class="action action-upload-image" id="upload-video-thumbnail-image">${_("Upload Video Thumbnail Image")}</button>
</div>
</li>
% endif
% if about_page_editable: % if about_page_editable:
<li class="field video" id="field-course-introduction-video"> <li class="field video" id="field-course-introduction-video">
<label for="course-introduction-video">${_("Course Introduction Video")}</label> <label for="course-introduction-video">${_("Course Introduction Video")}</label>
......
...@@ -475,6 +475,26 @@ class CourseFields(object): ...@@ -475,6 +475,26 @@ class CourseFields(object):
# Ensure that courses imported from XML keep their image # Ensure that courses imported from XML keep their image
default="images_course_image.jpg" 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( issue_badges = Boolean(
display_name=_("Issue Open Badges"), display_name=_("Issue Open Badges"),
help=_( help=_(
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
Course Schedule and Details Settings page. Course Schedule and Details Settings page.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import os
from bok_choy.promise import EmptyPromise from bok_choy.promise import EmptyPromise
from bok_choy.javascript import requirejs from bok_choy.javascript import requirejs
...@@ -17,6 +18,9 @@ class SettingsPage(CoursePage): ...@@ -17,6 +18,9 @@ class SettingsPage(CoursePage):
""" """
url_path = "settings/details" 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 # Helpers
...@@ -234,3 +238,50 @@ class SettingsPage(CoursePage): ...@@ -234,3 +238,50 @@ class SettingsPage(CoursePage):
).fulfill() ).fulfill()
self.wait_for_require_js() self.wait_for_require_js()
self.wait_for_ajax() 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]
...@@ -172,6 +172,8 @@ class AdvancedSettingsPage(CoursePage): ...@@ -172,6 +172,8 @@ class AdvancedSettingsPage(CoursePage):
'cert_name_short', 'cert_name_short',
'certificates_display_behavior', 'certificates_display_behavior',
'course_image', 'course_image',
'banner_image',
'video_thumbnail_image',
'cosmetic_display_price', 'cosmetic_display_price',
'advertised_start', 'advertised_start',
'announcement', 'announcement',
......
...@@ -571,3 +571,42 @@ class StudioSubsectionSettingsA11yTest(StudioCourseTest): ...@@ -571,3 +571,42 @@ class StudioSubsectionSettingsA11yTest(StudioCourseTest):
include=['section.edit-settings-timed-examination'] include=['section.edit-settings-timed-examination']
) )
self.course_outline.a11y_audit.check_for_accessibility_errors() 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'))
...@@ -58,6 +58,10 @@ class CourseDetails(object): ...@@ -58,6 +58,10 @@ class CourseDetails(object):
self.license = "all-rights-reserved" # default course license is all rights reserved self.license = "all-rights-reserved" # default course license is all rights reserved
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.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.pre_requisite_courses = [] # pre-requisite courses
self.entrance_exam_enabled = "" # is entrance exam enabled self.entrance_exam_enabled = "" # is entrance exam enabled
self.entrance_exam_id = "" # the content location for the entrance exam self.entrance_exam_id = "" # the content location for the entrance exam
...@@ -97,7 +101,11 @@ class CourseDetails(object): ...@@ -97,7 +101,11 @@ class CourseDetails(object):
course_details.enrollment_end = descriptor.enrollment_end course_details.enrollment_end = descriptor.enrollment_end
course_details.pre_requisite_courses = descriptor.pre_requisite_courses course_details.pre_requisite_courses = descriptor.pre_requisite_courses
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_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.language = descriptor.language
course_details.self_paced = descriptor.self_paced course_details.self_paced = descriptor.self_paced
...@@ -217,6 +225,15 @@ class CourseDetails(object): ...@@ -217,6 +225,15 @@ class CourseDetails(object):
descriptor.course_image = jsondict['course_image_name'] descriptor.course_image = jsondict['course_image_name']
dirty = True 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 \ if 'pre_requisite_courses' in jsondict \
and sorted(jsondict['pre_requisite_courses']) != sorted(descriptor.pre_requisite_courses): and sorted(jsondict['pre_requisite_courses']) != sorted(descriptor.pre_requisite_courses):
descriptor.pre_requisite_courses = jsondict['pre_requisite_courses'] descriptor.pre_requisite_courses = jsondict['pre_requisite_courses']
......
...@@ -95,6 +95,16 @@ class CourseDetailsTestCase(ModuleStoreTestCase): ...@@ -95,6 +95,16 @@ class CourseDetailsTestCase(ModuleStoreTestCase):
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).course_image_name, CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).course_image_name,
jsondetails.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" jsondetails.language = "hr"
self.assertEqual( self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).language, CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).language,
......
...@@ -10,24 +10,25 @@ from xmodule.modulestore.django import modulestore ...@@ -10,24 +10,25 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore import ModuleStoreEnum 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, """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 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 # set different than the default, return that path so that
# courses can use custom course image paths, otherwise just # courses can use custom course image paths, otherwise just
# return the default static path. # return the default static path.
url = '/static/' + (course.static_asset_path or getattr(course, 'data_dir', '')) 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: if hasattr(course, image_key) and getattr(course, image_key) != course.fields[image_key].default:
url += '/' + course.course_image url += '/' + getattr(course, image_key)
else: else:
url += '/images/course_image.jpg' url += '/images/' + image_key + '.jpg'
elif not course.course_image: elif not getattr(course, image_key):
# if course_image is empty, use the default image url from settings # if image_key is empty, use the default image url from settings
url = settings.STATIC_URL + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL url = settings.STATIC_URL + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL
else: 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) url = StaticContent.serialize_asset_key_with_slash(loc)
return url return url
......
...@@ -65,3 +65,21 @@ class CourseImageTestCase(ModuleStoreTestCase): ...@@ -65,3 +65,21 @@ class CourseImageTestCase(ModuleStoreTestCase):
'static/test.png', 'static/test.png',
course_image_url(course), 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')
)
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