Commit 2d6604a0 by Douglas Hall

Merge pull request #12318 from edx/hotfix/2016-05-02

Patch Release 2016-05-02 WL Studio Course Details Fields
parents 7d7d5a16 59f70cf8
...@@ -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,11 +255,17 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): ...@@ -255,11 +255,17 @@ 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 Subtitle")
self.assertNotContains(response, "Course Duration")
self.assertNotContains(response, "Course Description")
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):
...@@ -367,7 +373,8 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): ...@@ -367,7 +373,8 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
def test_regular_site_fetch(self): def test_regular_site_fetch(self):
settings_details_url = get_url(self.course.id) settings_details_url = get_url(self.course.id)
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}): with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False,
'ENABLE_EXTENDED_COURSE_DETAILS': True}):
response = self.client.get_html(settings_details_url) response = self.client.get_html(settings_details_url)
self.assertContains(response, "Course Summary Page") self.assertContains(response, "Course Summary Page")
self.assertContains(response, "Send a note to students via email") self.assertContains(response, "Send a note to students via email")
...@@ -380,11 +387,17 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): ...@@ -380,11 +387,17 @@ 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 Subtitle")
self.assertContains(response, "Course Duration")
self.assertContains(response, "Course Description")
self.assertContains(response, "Course Short Description") self.assertContains(response, "Course Short Description")
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
......
...@@ -976,18 +976,24 @@ def settings_handler(request, course_key_string): ...@@ -976,18 +976,24 @@ def settings_handler(request, course_key_string):
'ENABLE_MKTG_SITE', 'ENABLE_MKTG_SITE',
settings.FEATURES.get('ENABLE_MKTG_SITE', False) settings.FEATURES.get('ENABLE_MKTG_SITE', False)
) )
enable_extended_course_details = microsite.get_value_for_org(
course_module.location.org,
'ENABLE_EXTENDED_COURSE_DETAILS',
settings.FEATURES.get('ENABLE_EXTENDED_COURSE_DETAILS', False)
)
about_page_editable = not marketing_site_enabled about_page_editable = not marketing_site_enabled
enrollment_end_editable = GlobalStaff().has_user(request.user) or not marketing_site_enabled enrollment_end_editable = GlobalStaff().has_user(request.user) or not marketing_site_enabled
short_description_editable = settings.FEATURES.get('EDITABLE_SHORT_DESCRIPTION', True) short_description_editable = settings.FEATURES.get('EDITABLE_SHORT_DESCRIPTION', True)
self_paced_enabled = SelfPacedConfiguration.current().enabled self_paced_enabled = SelfPacedConfiguration.current().enabled
settings_context = { settings_context = {
'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,
...@@ -1001,6 +1007,7 @@ def settings_handler(request, course_key_string): ...@@ -1001,6 +1007,7 @@ def settings_handler(request, course_key_string):
'is_prerequisite_courses_enabled': is_prerequisite_courses_enabled(), 'is_prerequisite_courses_enabled': is_prerequisite_courses_enabled(),
'is_entrance_exams_enabled': is_entrance_exams_enabled(), 'is_entrance_exams_enabled': is_entrance_exams_enabled(),
'self_paced_enabled': self_paced_enabled, 'self_paced_enabled': self_paced_enabled,
'enable_extended_course_details': enable_extended_course_details
} }
if is_prerequisite_courses_enabled(): if is_prerequisite_courses_enabled():
courses, in_process_course_actions = get_courses_accessible_to_user(request) courses, in_process_course_actions = get_courses_accessible_to_user(request)
......
...@@ -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 **",
......
...@@ -12,6 +12,10 @@ var CourseDetails = Backbone.Model.extend({ ...@@ -12,6 +12,10 @@ var CourseDetails = Backbone.Model.extend({
enrollment_start: null, enrollment_start: null,
enrollment_end: null, enrollment_end: null,
syllabus: null, syllabus: null,
title: "",
subtitle: "",
duration: "",
description: "",
short_description: "", short_description: "",
overview: "", overview: "",
intro_video: null, intro_video: null,
...@@ -19,9 +23,15 @@ var CourseDetails = Backbone.Model.extend({ ...@@ -19,9 +23,15 @@ 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',
learning_info: [],
instructor_info: {}
}, },
validate: function(newattrs) { validate: function(newattrs) {
...@@ -32,9 +42,30 @@ var CourseDetails = Backbone.Model.extend({ ...@@ -32,9 +42,30 @@ var CourseDetails = Backbone.Model.extend({
newattrs, ["start_date", "end_date", "enrollment_start", "enrollment_end"] newattrs, ["start_date", "end_date", "enrollment_start", "enrollment_end"]
); );
if (newattrs.title.length > 50) {
errors.title = gettext("The title field must be limited to 50 characters.");
}
if (newattrs.subtitle.length > 150) {
errors.subtitle = gettext("The subtitle field must be limited to 150 characters.");
}
if (newattrs.duration.length > 50) {
errors.duration = gettext("The duration field must be limited to 50 characters.");
}
if (newattrs.short_description.length > 150) {
errors.short_description = gettext("The short description field must be limited to 150 characters.");
}
if (newattrs.description.length > 1000) {
errors.description = gettext("The description field must be limited to 1000 characters.");
}
if (newattrs.start_date === null) { if (newattrs.start_date === null) {
errors.start_date = gettext("The course must have an assigned start date."); errors.start_date = gettext("The course must have an assigned start date.");
} }
if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) { if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
errors.end_date = gettext("The course end date must be later than the course start date."); errors.end_date = gettext("The course end date must be later than the course start date.");
} }
......
define([ define([
'jquery', 'js/models/settings/course_details', 'js/views/settings/main', 'jquery', 'js/models/settings/course_details', 'js/views/settings/main',
'common/js/spec_helpers/ajax_helpers' 'common/js/spec_helpers/ajax_helpers', 'common/js/spec_helpers/template_helpers',
], function($, CourseDetailsModel, MainView, AjaxHelpers) { ], function($, CourseDetailsModel, MainView, AjaxHelpers, TemplateHelpers) {
'use strict'; 'use strict';
var SELECTORS = { var SELECTORS = {
entrance_exam_min_score: '#entrance-exam-minimum-score-pct', entrance_exam_min_score: '#entrance-exam-minimum-score-pct',
entrance_exam_enabled_field: '#entrance-exam-enabled', entrance_exam_enabled_field: '#entrance-exam-enabled',
grade_requirement_div: '.div-grade-requirements div' grade_requirement_div: '.div-grade-requirements div',
add_course_learning_info: '.add-course-learning-info',
delete_course_learning_info: '.delete-course-learning-info',
add_course_instructor_info: '.add-course-instructor-info',
remove_instructor_data: '.remove-instructor-data'
}; };
describe('Settings/Main', function () { describe('Settings/Main', function () {
...@@ -21,24 +25,50 @@ define([ ...@@ -21,24 +25,50 @@ define([
course_id : '', course_id : '',
run : '', run : '',
syllabus : null, syllabus : null,
title: '',
subtitle: '',
duration: '',
description: '',
short_description : '', short_description : '',
overview : '', overview : '',
intro_video : null, intro_video : null,
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',
license: null, license: null,
language: '' language: '',
learning_info: [''],
instructor_info: {
'instructors': [{"name": "","title": "","organization": "","image": "","bio": ""}]
}
}, },
mockSettingsPage = readFixtures('mock/mock-settings-page.underscore');
mockSettingsPage = readFixtures('mock/mock-settings-page.underscore'),
learningInfoTpl = readFixtures('course-settings-learning-fields.underscore'),
instructorInfoTpl = readFixtures('course-instructor-details.underscore');
beforeEach(function () { beforeEach(function () {
setFixtures(mockSettingsPage); TemplateHelpers.installTemplates(['course-settings-learning-fields', 'course-instructor-details'], true);
appendSetFixtures(mockSettingsPage);
appendSetFixtures(
$("<script>", { id: "basic-learning-info-tpl", type: "text/template" }).text(learningInfoTpl)
);
appendSetFixtures(
$("<script>", { id: "basic-instructor-info-tpl", type: "text/template" }).text(instructorInfoTpl)
);
this.model = new CourseDetailsModel(modelData, {parse: true});
this.model = new CourseDetailsModel($.extend(true, {}, modelData, {
instructor_info: {
'instructors': [{"name": "","title": "","organization": "","image": "","bio": ""}]
}}), {parse: true});
this.model.urlRoot = urlRoot; this.model.urlRoot = urlRoot;
this.view = new MainView({ this.view = new MainView({
el: $('.settings-details'), el: $('.settings-details'),
...@@ -178,5 +208,149 @@ define([ ...@@ -178,5 +208,149 @@ 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);
});
it('can add learning information', function () {
this.view.$(SELECTORS.add_course_learning_info).click();
expect('click').not.toHaveBeenPreventedOn(SELECTORS.add_course_learning_info);
expect(this.model.get('learning_info').length).toEqual(2);
this.view.$(SELECTORS.add_course_learning_info).click();
expect(this.model.get('learning_info').length).toEqual(3);
});
it('can delete learning information', function () {
for (var i = 0 ; i < 2; i++) {
this.view.$(SELECTORS.add_course_learning_info).click();
}
expect(this.model.get('learning_info').length).toEqual(3);
expect(this.view.$(SELECTORS.delete_course_learning_info)).toExist();
this.view.$(SELECTORS.delete_course_learning_info).click();
expect(this.model.get('learning_info').length).toEqual(2);
});
it('can save learning information', function () {
expect(this.model.get('learning_info').length).toEqual(1);
var requests = AjaxHelpers.requests(this),
expectedJson = $.extend(true, {}, modelData, {
learning_info: ['testing info']
});
// Input some value.
this.view.$("#course-learning-info-0").val('testing info');
this.view.$("#course-learning-info-0").trigger('change');
this.view.saveView();
AjaxHelpers.expectJsonRequest(
requests, 'POST', urlRoot, expectedJson
);
AjaxHelpers.respondWithJson(requests, expectedJson);
});
it('can add instructor information', function () {
this.view.$(SELECTORS.add_course_instructor_info).click();
expect(this.model.get('instructor_info').instructors.length).toEqual(2);
this.view.$(SELECTORS.add_course_instructor_info).click();
expect(this.model.get('instructor_info').instructors.length).toEqual(3);
});
it('can delete instructor information', function () {
this.view.$(SELECTORS.add_course_instructor_info).click();
expect(this.model.get('instructor_info').instructors.length).toEqual(2);
expect(this.view.$(SELECTORS.remove_instructor_data)).toExist();
this.view.$(SELECTORS.remove_instructor_data).click();
expect(this.model.get('instructor_info').instructors.length).toEqual(1);
});
it('can save instructor information', function () {
var requests = AjaxHelpers.requests(this),
expectedJson = $.extend(true, {}, modelData, {
instructor_info: {
instructors:
[{
"name": "test_name",
"title": "test_title",
"organization": "test_org",
"image": "",
"bio": "test_bio"
}]
}
});
// Input some value.
this.view.$("#course-instructor-name-0").val('test_name').trigger('change');
this.view.$("#course-instructor-title-0").val('test_title').trigger('change');
this.view.$("#course-instructor-organization-0").val('test_org').trigger('change');
this.view.$("#course-instructor-bio-0").val('test_bio').trigger('change');
this.view.saveView();
AjaxHelpers.expectJsonRequest(
requests, 'POST', urlRoot, expectedJson
);
AjaxHelpers.respondWithJson(requests, expectedJson);
});
}); });
}); });
// Backbone Application View: Instructor Information
define([ // jshint ignore:line
'jquery',
'underscore',
'backbone',
'gettext',
'js/utils/templates',
"js/models/uploads",
"js/views/uploads"
],
function ($, _, Backbone, gettext, TemplateUtils, FileUploadModel, FileUploadDialog) {
'use strict';
var InstructorInfoView = Backbone.View.extend({
events : {
'click .remove-instructor-data': 'removeInstructor',
'click .action-upload-instructor-image': "uploadInstructorImage"
},
initialize: function() {
// Set up the initial state of the attributes set for this model instance
_.bindAll(this, 'render');
this.template = this.loadTemplate('course-instructor-details');
this.listenTo(this.model, 'change:instructor_info', this.render);
},
loadTemplate: function(name) {
// Retrieve the corresponding template for this model
return TemplateUtils.loadTemplate(name);
},
render: function() {
// Assemble the render view for this model.
$(".course-instructor-details-fields").empty();
var self = this;
$.each(this.model.get('instructor_info').instructors, function( index, data ) {
$(self.el).append(self.template({
data: data,
index: index
}));
});
// Avoid showing broken image on mistyped/nonexistent image
this.$el.find('img').error(function() {
$(this).hide();
});
this.$el.find('img').load(function() {
$(this).show();
});
},
removeInstructor: function(event) {
/*
* Remove course Instructor fields.
* */
event.preventDefault();
var index = event.currentTarget.getAttribute('data-index'),
instructors = this.model.get('instructor_info').instructors.slice(0);
instructors.splice(index, 1);
this.model.set('instructor_info', {instructors: instructors});
},
uploadInstructorImage: function(event) {
/*
* Upload instructor image.
* */
event.preventDefault();
var index = event.currentTarget.getAttribute('data-index'),
instructors = this.model.get('instructor_info').instructors.slice(0),
instructor = instructors[index];
var upload = new FileUploadModel({
title: gettext("Upload instructor image."),
message: gettext("Files must be in JPEG or PNG format."),
mimeTypes: ['image/jpeg', 'image/png']
});
var self = this;
var modal = new FileUploadDialog({
model: upload,
onSuccess: function(response) {
instructor.image = response.asset.url;
self.model.set('instructor_info', {instructors: instructors});
self.model.trigger('change', self.model);
self.model.trigger('change:instructor_info', self.model);
}
});
modal.show();
}
});
return InstructorInfoView;
});
// Backbone Application View: Course Learning Information
define([ // jshint ignore:line
'jquery',
'underscore',
'backbone',
'gettext',
'js/utils/templates'
],
function ($, _, Backbone, gettext, TemplateUtils) {
'use strict';
var LearningInfoView = Backbone.View.extend({
events: {
'click .delete-course-learning-info': "removeLearningInfo"
},
initialize: function() {
// Set up the initial state of the attributes set for this model instance
_.bindAll(this, 'render');
this.template = this.loadTemplate('course-settings-learning-fields');
this.listenTo(this.model, 'change:learning_info', this.render);
},
loadTemplate: function(name) {
// Retrieve the corresponding template for this model
return TemplateUtils.loadTemplate(name);
},
render: function() {
// rendering for this model
$("li.course-settings-learning-fields").empty();
var self = this;
var learning_information = this.model.get('learning_info');
$.each(learning_information, function( index, info ) {
$(self.el).append(self.template({index: index, info: info, info_count: learning_information.length }));
});
},
removeLearningInfo: function(event) {
/*
* Remove course learning fields.
* */
event.preventDefault();
var index = event.currentTarget.getAttribute('data-index'),
existing_info = _.clone(this.model.get('learning_info'));
existing_info.splice(index, 1);
this.model.set('learning_info', existing_info);
}
});
return LearningInfoView;
});
define(["js/views/validation", "codemirror", "underscore", "jquery", "jquery.ui", "js/utils/date_utils", "js/models/uploads", define(["js/views/validation", "codemirror", "underscore", "jquery", "jquery.ui", "js/utils/date_utils",
"js/views/uploads", "js/views/license", "js/models/license", "js/models/uploads", "js/views/uploads", "js/views/license", "js/models/license",
"common/js/components/views/feedback_notification", "jquery.timepicker", "date", "gettext"], "common/js/components/views/feedback_notification", "jquery.timepicker", "date", "gettext",
"js/views/learning_info", "js/views/instructor_info"],
function(ValidatingView, CodeMirror, _, $, ui, DateUtils, FileUploadModel, function(ValidatingView, CodeMirror, _, $, ui, DateUtils, FileUploadModel,
FileUploadDialog, LicenseView, LicenseModel, NotificationView, FileUploadDialog, LicenseView, LicenseModel, NotificationView,
timepicker, date, gettext) { timepicker, date, gettext, LearningInfoView, InstructorInfoView) {
var DetailsView = ValidatingView.extend({ var DetailsView = ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseDetails // Model class is CMS.Models.Settings.CourseDetails
...@@ -20,7 +21,9 @@ var DetailsView = ValidatingView.extend({ ...@@ -20,7 +21,9 @@ var DetailsView = ValidatingView.extend({
// would love to move to a general superclass, but event hashes don't inherit in backbone :-( // would love to move to a general superclass, but event hashes don't inherit in backbone :-(
'focus :input' : "inputFocus", 'focus :input' : "inputFocus",
'blur :input' : "inputUnfocus", 'blur :input' : "inputUnfocus",
'click .action-upload-image': "uploadImage" 'click .action-upload-image': "uploadImage",
'click .add-course-learning-info': "addLearningFields",
'click .add-course-instructor-info': "addInstructorFields"
}, },
initialize : function(options) { initialize : function(options) {
...@@ -34,10 +37,10 @@ var DetailsView = ValidatingView.extend({ ...@@ -34,10 +37,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();
}); });
...@@ -60,6 +63,16 @@ var DetailsView = ValidatingView.extend({ ...@@ -60,6 +63,16 @@ var DetailsView = ValidatingView.extend({
closeIcon: true closeIcon: true
}).show(); }).show();
} }
this.learning_info_view = new LearningInfoView({
el: $(".course-settings-learning-fields"),
model: this.model
});
this.instructor_info_view = new InstructorInfoView({
el: $(".course-instructor-details-fields"),
model: this.model
});
}, },
render: function() { render: function() {
...@@ -71,6 +84,16 @@ var DetailsView = ValidatingView.extend({ ...@@ -71,6 +84,16 @@ var DetailsView = ValidatingView.extend({
this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview')); this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview'));
this.codeMirrorize(null, $('#course-overview')[0]); this.codeMirrorize(null, $('#course-overview')[0]);
if (this.model.get('title') !== '') {
this.$el.find('#' + this.fieldToSelectorMap.title).val(this.model.get('title'));
} else {
var displayName = this.$el.find('#' + this.fieldToSelectorMap.title).attr('data-display-name');
this.$el.find('#' + this.fieldToSelectorMap.title).val(displayName);
}
this.$el.find('#' + this.fieldToSelectorMap.subtitle).val(this.model.get('subtitle'));
this.$el.find('#' + this.fieldToSelectorMap.duration).val(this.model.get('duration'));
this.$el.find('#' + this.fieldToSelectorMap.description).val(this.model.get('description'));
this.$el.find('#' + this.fieldToSelectorMap['short_description']).val(this.model.get('short_description')); this.$el.find('#' + this.fieldToSelectorMap['short_description']).val(this.model.get('short_description'));
this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample()); this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
...@@ -82,9 +105,17 @@ var DetailsView = ValidatingView.extend({ ...@@ -82,9 +105,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 : '';
...@@ -115,7 +146,9 @@ var DetailsView = ValidatingView.extend({ ...@@ -115,7 +146,9 @@ var DetailsView = ValidatingView.extend({
paceToggleTip.text(gettext('Course pacing cannot be changed once a course has started.')); paceToggleTip.text(gettext('Course pacing cannot be changed once a course has started.'));
} }
this.licenseView.render() this.licenseView.render();
this.learning_info_view.render();
this.instructor_info_view.render();
return this; return this;
}, },
...@@ -126,13 +159,47 @@ var DetailsView = ValidatingView.extend({ ...@@ -126,13 +159,47 @@ var DetailsView = ValidatingView.extend({
'enrollment_start' : 'enrollment-start', 'enrollment_start' : 'enrollment-start',
'enrollment_end' : 'enrollment-end', 'enrollment_end' : 'enrollment-end',
'overview' : 'course-overview', 'overview' : 'course-overview',
'title': 'course-title',
'subtitle': 'course-subtitle',
'duration': 'course-duration',
'description': 'course-description',
'short_description' : 'course-short-description', 'short_description' : 'course-short-description',
'intro_video' : 'course-introduction-video', 'intro_video' : 'course-introduction-video',
'effort' : "course-effort", 'effort' : "course-effort",
'course_image_asset_path': 'course-image-url', 'course_image_asset_path': 'course-image-url',
'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',
'course_settings_learning_fields': 'course-settings-learning-fields',
'add_course_learning_info': 'add-course-learning-info',
'add_course_instructor_info': 'add-course-instructor-info',
'course_learning_info': 'course-learning-info'
},
addLearningFields: function() {
/*
* Add new course learning fields.
* */
var existingInfo = _.clone(this.model.get('learning_info'));
existingInfo.push('');
this.model.set('learning_info', existingInfo);
},
addInstructorFields: function() {
/*
* Add new course instructor fields.
* */
var instructors = this.model.get('instructor_info').instructors.slice(0);
instructors.push({
name: '',
title: '',
organization: '',
image: '',
bio: ''
});
this.model.set('instructor_info', {instructors: instructors});
}, },
updateTime : function(e) { updateTime : function(e) {
...@@ -148,23 +215,34 @@ var DetailsView = ValidatingView.extend({ ...@@ -148,23 +215,34 @@ var DetailsView = ValidatingView.extend({
}, },
updateModel: function(event) { updateModel: function(event) {
var value;
var index = event.currentTarget.getAttribute('data-index');
switch (event.currentTarget.id) { switch (event.currentTarget.id) {
case 'course-language': case 'course-learning-info-' + index:
this.setField(event); value = $(event.currentTarget).val();
var learningInfo = this.model.get('learning_info');
learningInfo[index] = value;
this.showNotificationBar();
break;
case 'course-instructor-name-' + index:
case 'course-instructor-title-' + index:
case 'course-instructor-organization-' + index:
case 'course-instructor-bio-' + index:
value = $(event.currentTarget).val();
var field = event.currentTarget.getAttribute('data-field'),
instructors = this.model.get('instructor_info').instructors.slice(0);
instructors[index][field] = value;
this.model.set('instructor_info', {instructors: instructors});
this.showNotificationBar();
break; break;
case 'course-image-url': case 'course-image-url':
this.setField(event); this.updateImageField(event, 'course_image_name', '#course-image');
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);
break; break;
case 'course-effort': case 'banner-image-url':
this.setField(event); 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; break;
case 'entrance-exam-enabled': case 'entrance-exam-enabled':
if($(event.currentTarget).is(":checked")){ if($(event.currentTarget).is(":checked")){
...@@ -183,9 +261,6 @@ var DetailsView = ValidatingView.extend({ ...@@ -183,9 +261,6 @@ var DetailsView = ValidatingView.extend({
this.setField(event); this.setField(event);
} }
break; break;
case 'course-short-description':
this.setField(event);
break;
case 'pre-requisite-course': case 'pre-requisite-course':
var value = $(event.currentTarget).val(); var value = $(event.currentTarget).val();
value = value == "" ? [] : [value]; value = value == "" ? [] : [value];
...@@ -212,11 +287,30 @@ var DetailsView = ValidatingView.extend({ ...@@ -212,11 +287,30 @@ var DetailsView = ValidatingView.extend({
case 'course-pace-instructor-paced': case 'course-pace-instructor-paced':
this.model.set('self_paced', JSON.parse(event.currentTarget.value)); this.model.set('self_paced', JSON.parse(event.currentTarget.value));
break; break;
case 'course-language':
case 'course-effort':
case 'course-title':
case 'course-subtitle':
case 'course-duration':
case 'course-description':
case 'course-short-description':
this.setField(event);
break;
default: // Everything else is handled by datepickers and CodeMirror. default: // Everything else is handled by datepickers and CodeMirror.
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')) {
...@@ -300,8 +394,30 @@ var DetailsView = ValidatingView.extend({ ...@@ -300,8 +394,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']
}); });
...@@ -309,13 +425,12 @@ var DetailsView = ValidatingView.extend({ ...@@ -309,13 +425,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);
...@@ -838,6 +838,142 @@ ...@@ -838,6 +838,142 @@
} }
} }
&.course-learning-info {
.list-input {
margin-bottom: $baseline;
.course-settings-learning-fields {
.field {
.input-learning-info {
width: flex-grid(10, 12);
display: inline-block;
}
}
}
}
.actions {
width: flex-grid(9, 9);
.new-button {
@extend %btn-primary-green;
}
.delete-button {
margin: 0;
}
}
}
&.instructor-types {
.list-input {
.course-instructor-details-fields {
.field {
width: flex-grid(2, 6);
&.field-course-instructor-bio {
width: flex-grid(6, 6);
}
&.current-instructor-image {
width: flex-grid(6, 6);
text-align: left;
padding: 0;
.wrapper-instructor-image {
margin: 15px auto;
}
}
}
}
&:last-child {
margin-bottom: 0;
}
}
.field-group {
@include clearfix();
width: flex-grid(9, 9);
margin-bottom: ($baseline*1.5);
border-bottom: 1px solid $gray-l5;
padding-bottom: ($baseline*1.5);
&:last-child {
border: none;
padding-bottom: 0;
}
.field {
display: inline-block;
vertical-align: top;
width: flex-grid(3, 6);
margin-bottom: ($baseline/2);
margin-right: flex-gutter();
}
// specific fields - course image
.field-course-instructor-image {
margin-bottom: ($baseline/2);
padding: ($baseline/2) $baseline;
background: $gray-l5;
text-align: left;
.wrapper-instructor-image {
display: block;
width: 375px;
height: 200px;
overflow: hidden;
margin: 0 auto;
border: 1px solid $gray-l4;
box-shadow: 0 1px 1px $shadow-l1;
padding: ($baseline/2);
background: $white;
}
.instructor-image {
display: block;
width: 100%;
min-height: 100%;
}
.msg {
@extend %t-copy-sub2;
display: block;
margin-top: ($baseline/2);
color: $gray-l3;
}
}
.wrapper-input {
@include clearfix();
width: flex-grid(9,9);
.input {
float: left;
width: flex-grid(6,9);
margin-right: flex-gutter();
}
.action-upload-instructor-image {
@extend %ui-btn-flat-outline;
float: right;
width: flex-grid(2,9);
margin-top: ($baseline/4);
padding: ($baseline/2) $baseline;
}
}
}
.actions {
width: flex-grid(9, 9);
.new-button {
@extend %btn-primary-green;
}
.delete-button {
margin: 0;
}
}
}
// specific fields - advanced settings // specific fields - advanced settings
&.advanced-policies { &.advanced-policies {
......
<li class="field-group">
<div class="field text field-course-instructor-name">
<label for="course-instructor-name-<%- index %>"><%- gettext("Name") %></label>
<input type="text" class="long" id="course-instructor-name-<%- index %>" value="<%- data['name'] %>" data-index=<%- index %> data-field="name" placeholder="<%- gettext('Instructor Name') %>" />
<span class="tip tip-stacked"><%- gettext("Please add the instructor's name")%></span>
</div>
<div class="field text field-course-instructor-title">
<label for="course-instructor-title-<%- index %>"><%- gettext("Title") %></label>
<input type="text" class="long" id="course-instructor-title-<%- index %>" value="<%- data['title'] %>" data-index=<%- index %> data-field="title" placeholder="<%- gettext('Instructor Title') %>" />
<span class="tip tip-stacked"><%- gettext("Please add the instructor's title")%></span>
</div>
<div class="field text field-course-instructor-organization">
<label for="course-instructor-organization-<%- index %>"><%- gettext("Organization") %></label>
<input type="text" class="long" id="course-instructor-organization-<%- index %>" value = "<%- data['organization'] %>" data-index=<%- index %> data-field="organization" placeholder="<%- gettext('Organization Name') %>" />
<span class="tip tip-stacked"><%- gettext("Please add the institute where the instructor is associated")%></span>
</div>
<div class="field text field-course-instructor-bio">
<label for="course-instructor-bio-<%- index %>"><%- gettext("Biography") %></label>
<textarea class="short text" id="course-instructor-bio-<%- index %>" data-index=<%- index %> data-field="bio" placeholder="<%- gettext('Instructor Biography') %>" ><%- data['bio'] %></textarea>
<span class="tip tip-stacked"><%- gettext("Please add the instructor's biography")%></span>
</div>
<div class="field image field-course-instructor-image current-instructor-image">
<label for="course-instructor-image-<%- index %>"><%- gettext("Photo") %></label>
<span class="wrapper-instructor-image">
<img class="instructor-image" src="<%- data['image']%>" alt="<%- gettext('Instructor Photo') %>" />
</span>
<div class="wrapper-input">
<div class="input">
<input type="text" dir="ltr" class="long new-instructor-image-url" id="course-instructor-image-<%- index %>" value="<%- data['image'] %>" data-field="image" placeholder="<%- gettext('Instructor Photo URL') %>" autocomplete="off" />
<span class="tip tip-stacked"><%- gettext("Please add a photo of the instructor (Note: only JPEG or PNG format supported)")%></span>
</div>
<button type="button" class="action action-upload-instructor-image" data-index=<%- index %>><%- gettext("Upload Photo") %></button>
</div>
</div>
<div class="actions">
<button type="button" class="button delete-button standard remove-item remove-instructor-data" data-index=<%- index %>><%- gettext("Delete") %></button>
</div>
</li>
<div class="field text" id="fields-course-learning-info-<%- index %>">
<label for= "course-learning-info-<%- index %>"><%- gettext("Learning Outcome") %> <%- index + 1 %></label>
<input type="text" class="input-learning-info" id="course-learning-info-<%- index %>" value="<%- info %>" data-index="<%- index %>" placeholder="<%- gettext('Add a learning outcome here') %>">
<button type="button" class="button delete-button standard remove-item delete-course-learning-info" data-index="<%- index %>"><%- gettext("Delete") %></button>
</div>
...@@ -105,6 +105,125 @@ ...@@ -105,6 +105,125 @@
</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>
</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>
<section class="group-settings course-learning-info">
<header>
<h2 class="title-2">Learning Outcomes</h2>
<span class="tip">Add the learning outcomes for this course</span>
</header>
<ol class="list-input enum">
<li class="course-settings-learning-fields"></li>
</ol>
<div class="actions">
<button type="button" class="action action-primary button new-button add-course-learning-info">
<i class="icon fa fa-plus icon-inline"></i>Add Learning Outcome
</button>
</div>
</section>
<section class="group-settings instructor-types">
<header>
<h2 class="title-2">Instructors</h2>
<span class="tip">Add details about the instructors for this course</span>
</header>
<ol class="list-input enum">
<li class="course-instructor-details-fields"></li>
</ol> </ol>
<div class="actions">
<button type="button" class="action action-primary button new-button add-course-instructor-info">
<i class="icon fa fa-plus icon-inline"></i>Add Instructor
</button>
</div>
</section> </section>
</form> </form>
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
%> %>
<%block name="header_extras"> <%block name="header_extras">
% for template_name in ["basic-modal", "modal-button", "upload-dialog", "license-selector"]: % for template_name in ["basic-modal", "modal-button", "upload-dialog", "license-selector", "course-settings-learning-fields", "course-instructor-details"]:
<script type="text/template" id="${template_name}-tpl"> <script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" /> <%static:include path="js/${template_name}.underscore" />
</script> </script>
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
<script type="text/javascript"> <script type="text/javascript">
window.CMS = window.CMS || {}; window.CMS = window.CMS || {};
CMS.URL = CMS.URL || {}; CMS.URL = CMS.URL || {};
CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'; CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'
</script> </script>
</%block> </%block>
...@@ -292,9 +292,33 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'; ...@@ -292,9 +292,33 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
<span class="tip">${_("Information for prospective students")}</span> <span class="tip">${_("Information for prospective students")}</span>
</header> </header>
<ol class="list-input"> <ol class="list-input">
% if enable_extended_course_details:
<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>
% endif
% if short_description_editable: % if short_description_editable:
<li class="field text" id="field-course-short-description"> <li class="field text" id="field-course-short-description">
<label for="course-overview">${_("Course Short Description")}</label> <label for="course-short-description">${_("Course Short Description")}</label>
<textarea class="text" id="course-short-description"></textarea> <textarea class="text" id="course-short-description"></textarea>
<span class="tip tip-stacked">${_("Appears on the course catalog page when students roll over the course name. Limit to ~150 characters")}</span> <span class="tip tip-stacked">${_("Appears on the course catalog page when students roll over the course name. Limit to ~150 characters")}</span>
</li> </li>
...@@ -315,11 +339,11 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'; ...@@ -315,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>${_("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">
...@@ -328,7 +352,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'; ...@@ -328,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
...@@ -340,13 +364,75 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'; ...@@ -340,13 +364,75 @@ 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-overview">${_("Course Introduction Video")}</label> <label for="course-introduction-video">${_("Course Introduction Video")}</label>
<div class="input input-existing"> <div class="input input-existing">
<div class="current current-course-introduction-video"> <div class="current current-course-introduction-video">
<iframe width="618" height="350" title="${_('Course Introduction Video')}" src="" frameborder="0" allowfullscreen></iframe> <iframe width="618" height="350" title="${_('Course Introduction Video')}" src="" frameborder="0" allowfullscreen></iframe>
...@@ -362,9 +448,43 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'; ...@@ -362,9 +448,43 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
<span class="tip tip-stacked">${_("Enter your YouTube video's ID (along with any restriction parameters)")}</span> <span class="tip tip-stacked">${_("Enter your YouTube video's ID (along with any restriction parameters)")}</span>
</div> </div>
</li> </li>
% endif % endif
</ol>
</section>
% if enable_extended_course_details:
<hr class="divide" />
<section class="group-settings course-learning-info">
<header>
<h2 class="title-2">${_("Learning Outcomes")}</h2>
<span class="tip">${_("Add the learning outcomes for this course")}</span>
</header>
<ol class="list-input enum">
<li class="course-settings-learning-fields"></li>
</ol>
<div class="actions">
<button type="button" class="action action-primary button new-button add-course-learning-info">
<i class="icon fa fa-plus icon-inline"></i>${_("Add Learning Outcome")}
</button>
</div>
</section>
<hr class="divide" />
<section class="group-settings instructor-types">
<header>
<h2 class="title-2">${_("Instructors")}</h2>
<span class="tip">${_("Add details about the instructors for this course")}</span>
</header>
<ol class="list-input enum">
<li class="course-instructor-details-fields"></li>
</ol> </ol>
<div class="actions">
<button type="button" class="action action-primary button new-button add-course-instructor-info">
<i class="icon fa fa-plus icon-inline"></i>${_("Add Instructor")}
</button>
</div>
</section> </section>
% endif
% if about_page_editable or is_prerequisite_courses_enabled or is_entrance_exams_enabled: % if about_page_editable or is_prerequisite_courses_enabled or is_entrance_exams_enabled:
<hr class="divide" /> <hr class="divide" />
......
...@@ -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=_(
...@@ -774,6 +794,36 @@ class CourseFields(object): ...@@ -774,6 +794,36 @@ class CourseFields(object):
scope=Scope.settings scope=Scope.settings
) )
learning_info = List(
display_name=_("Course Learning Information"),
help=_("Specify what student can learn from the course."),
default=[],
scope=Scope.settings
)
"""
instructor_info dict structure:
{
"instructors": [
{
"name": "",
"title": "",
"organization": "",
"image": "",
"bio": ""
}
]
}
"""
instructor_info = Dict(
display_name=_("Course Instructor"),
help=_("Enter the details for Course Instructor"),
default={
"instructors": []
},
scope=Scope.settings
)
class CourseModule(CourseFields, SequenceModule): # pylint: disable=abstract-method class CourseModule(CourseFields, SequenceModule): # pylint: disable=abstract-method
""" """
......
...@@ -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,53 @@ class SettingsPage(CoursePage): ...@@ -234,3 +238,53 @@ 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, upload_btn_selector, file_to_upload):
"""
Upload image specified by image_selector and file_to_upload
"""
# wait for upload button
self.wait_for_element_presence(upload_btn_selector, 'upload button is present')
self.q(css=upload_btn_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',
...@@ -217,4 +219,6 @@ class AdvancedSettingsPage(CoursePage): ...@@ -217,4 +219,6 @@ class AdvancedSettingsPage(CoursePage):
'enable_proctored_exams', 'enable_proctored_exams',
'enable_timed_exams', 'enable_timed_exams',
'enable_subsection_gating', 'enable_subsection_gating',
'learning_info',
'instructor_info'
] ]
...@@ -475,3 +475,36 @@ class ContentLicenseTest(StudioCourseTest): ...@@ -475,3 +475,36 @@ class ContentLicenseTest(StudioCourseTest):
# The course_license text will include a bunch of screen reader text to explain # The course_license text will include a bunch of screen reader text to explain
# the selected options # the selected options
self.assertIn("Some Rights Reserved", self.lms_courseware.course_license) self.assertIn("Some Rights Reserved", self.lms_courseware.course_license)
@attr('a11y')
class StudioSettingsA11yTest(StudioCourseTest):
"""
Class to test Studio pages accessibility.
"""
def setUp(self): # pylint: disable=arguments-differ
super(StudioSettingsA11yTest, self).setUp()
self.settings_page = SettingsPage(self.browser, self.course_info['org'], self.course_info['number'],
self.course_info['run'])
def test_studio_settings_page_a11y(self):
"""
Check accessibility of SettingsPage.
"""
self.settings_page.visit()
self.settings_page.wait_for_page()
# There are several existing color contrast errors on this page,
# we will ignore this error in the test until we fix them.
self.settings_page.a11y_audit.config.set_rules({
"ignore": [
'color-contrast', # TODO: AC-225
'link-href', # TODO: AC-226
'nav-aria-label', # TODO: AC-227
'icon-aria-hidden', # TODO: AC-229
],
})
self.settings_page.a11y_audit.check_for_accessibility_errors()
...@@ -18,6 +18,10 @@ from xmodule.modulestore.django import modulestore ...@@ -18,6 +18,10 @@ from xmodule.modulestore.django import modulestore
# handled separately; its value maps to an alternate key name. # handled separately; its value maps to an alternate key name.
ABOUT_ATTRIBUTES = [ ABOUT_ATTRIBUTES = [
'syllabus', 'syllabus',
'title',
'subtitle',
'duration',
'description',
'short_description', 'short_description',
'overview', 'overview',
'effort', 'effort',
...@@ -43,6 +47,10 @@ class CourseDetails(object): ...@@ -43,6 +47,10 @@ class CourseDetails(object):
self.enrollment_start = None self.enrollment_start = None
self.enrollment_end = None self.enrollment_end = None
self.syllabus = None # a pdf file asset self.syllabus = None # a pdf file asset
self.title = ""
self.subtitle = ""
self.duration = ""
self.description = ""
self.short_description = "" self.short_description = ""
self.overview = "" # html to render as the overview self.overview = "" # html to render as the overview
self.intro_video = None # a video pointer self.intro_video = None # a video pointer
...@@ -50,6 +58,10 @@ class CourseDetails(object): ...@@ -50,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
...@@ -58,6 +70,8 @@ class CourseDetails(object): ...@@ -58,6 +70,8 @@ class CourseDetails(object):
'50' '50'
) # minimum passing score for entrance exam content module/tree, ) # minimum passing score for entrance exam content module/tree,
self.self_paced = None self.self_paced = None
self.learning_info = []
self.instructor_info = []
@classmethod @classmethod
def fetch_about_attribute(cls, course_key, attribute): def fetch_about_attribute(cls, course_key, attribute):
...@@ -89,9 +103,15 @@ class CourseDetails(object): ...@@ -89,9 +103,15 @@ 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
course_details.learning_info = descriptor.learning_info
course_details.instructor_info = descriptor.instructor_info
# Default course license is "All Rights Reserved" # Default course license is "All Rights Reserved"
course_details.license = getattr(descriptor, "license", "all-rights-reserved") course_details.license = getattr(descriptor, "license", "all-rights-reserved")
...@@ -209,6 +229,15 @@ class CourseDetails(object): ...@@ -209,6 +229,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']
...@@ -218,6 +247,14 @@ class CourseDetails(object): ...@@ -218,6 +247,14 @@ class CourseDetails(object):
descriptor.license = jsondict['license'] descriptor.license = jsondict['license']
dirty = True dirty = True
if 'learning_info' in jsondict:
descriptor.learning_info = jsondict['learning_info']
dirty = True
if 'instructor_info' in jsondict:
descriptor.instructor_info = jsondict['instructor_info']
dirty = True
if 'language' in jsondict and jsondict['language'] != descriptor.language: if 'language' in jsondict and jsondict['language'] != descriptor.language:
descriptor.language = jsondict['language'] descriptor.language = jsondict['language']
dirty = True dirty = True
......
...@@ -95,11 +95,41 @@ class CourseDetailsTestCase(ModuleStoreTestCase): ...@@ -95,11 +95,41 @@ 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,
jsondetails.language jsondetails.language
) )
jsondetails.learning_info = ["test", "test"]
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).learning_info,
jsondetails.learning_info
)
jsondetails.instructor_info = {
"instructors": [
{
"name": "test",
"title": "test",
"organization": "test",
"image": "test",
"bio": "test"
}
]
}
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).instructor_info,
jsondetails.instructor_info
)
def test_toggle_pacing_during_course_run(self): def test_toggle_pacing_during_course_run(self):
SelfPacedConfiguration(enabled=True).save() SelfPacedConfiguration(enabled=True).save()
......
...@@ -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 or modulestore().get_modulestore_type(course.id) == ModuleStoreEnum.Type.xml: if course.static_asset_path or modulestore().get_modulestore_type(course.id) == ModuleStoreEnum.Type.xml:
# 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