Merge pull request #12256 from edx/ibrahimahmed443/WL-398-image-fields-in-studio
WL-398 Add hero image and thumbnail image fields to Studio schedule and details settings page for WL site courses
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -132,37 +132,6 @@ def test_change_course_overview(_step):
|
||||
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 ####################
|
||||
def set_date_or_time(css, date_or_time):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 **",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -105,6 +105,96 @@
|
||||
</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>
|
||||
</li>
|
||||
|
||||
</ol>
|
||||
</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>
|
||||
|
||||
@@ -297,17 +297,17 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'
|
||||
<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 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 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 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 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 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 class="field text" id="field-course-description">
|
||||
<label for="course-description">${_("Course Description")}</label>
|
||||
@@ -339,11 +339,11 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'
|
||||
% endif
|
||||
|
||||
<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">
|
||||
% if context_course.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 class="msg msg-help">
|
||||
@@ -352,7 +352,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'
|
||||
|
||||
% else:
|
||||
<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 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
|
||||
@@ -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" />
|
||||
<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">${_("Upload Course Image")}</button>
|
||||
<button type="button" class="action action-upload-image" id="upload-course-image">${_("Upload Course Card Image")}</button>
|
||||
</div>
|
||||
</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 & 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 & 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:
|
||||
<li class="field video" id="field-course-introduction-video">
|
||||
<label for="course-introduction-video">${_("Course Introduction Video")}</label>
|
||||
|
||||
@@ -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=_(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user