Merge pull request #12318 from edx/hotfix/2016-05-02
Patch Release 2016-05-02 WL Studio Course Details Fields
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,11 +255,17 @@ 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")
|
||||
self.assertNotContains(response, "Course Duration")
|
||||
self.assertNotContains(response, "Course Description")
|
||||
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):
|
||||
@@ -367,7 +373,8 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
|
||||
def test_regular_site_fetch(self):
|
||||
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)
|
||||
self.assertContains(response, "Course Summary Page")
|
||||
self.assertContains(response, "Send a note to students via email")
|
||||
@@ -380,11 +387,17 @@ 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")
|
||||
self.assertContains(response, "Course Description")
|
||||
self.assertContains(response, "Course Short Description")
|
||||
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
|
||||
|
||||
@@ -976,18 +976,24 @@ def settings_handler(request, course_key_string):
|
||||
'ENABLE_MKTG_SITE',
|
||||
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
|
||||
enrollment_end_editable = GlobalStaff().has_user(request.user) or not marketing_site_enabled
|
||||
short_description_editable = settings.FEATURES.get('EDITABLE_SHORT_DESCRIPTION', True)
|
||||
|
||||
self_paced_enabled = SelfPacedConfiguration.current().enabled
|
||||
|
||||
settings_context = {
|
||||
'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,
|
||||
@@ -1001,6 +1007,7 @@ def settings_handler(request, course_key_string):
|
||||
'is_prerequisite_courses_enabled': is_prerequisite_courses_enabled(),
|
||||
'is_entrance_exams_enabled': is_entrance_exams_enabled(),
|
||||
'self_paced_enabled': self_paced_enabled,
|
||||
'enable_extended_course_details': enable_extended_course_details
|
||||
}
|
||||
if is_prerequisite_courses_enabled():
|
||||
courses, in_process_course_actions = get_courses_accessible_to_user(request)
|
||||
|
||||
@@ -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 **",
|
||||
|
||||
@@ -12,6 +12,10 @@ var CourseDetails = Backbone.Model.extend({
|
||||
enrollment_start: null,
|
||||
enrollment_end: null,
|
||||
syllabus: null,
|
||||
title: "",
|
||||
subtitle: "",
|
||||
duration: "",
|
||||
description: "",
|
||||
short_description: "",
|
||||
overview: "",
|
||||
intro_video: null,
|
||||
@@ -19,9 +23,15 @@ 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'
|
||||
entrance_exam_minimum_score_pct: '50',
|
||||
learning_info: [],
|
||||
instructor_info: {}
|
||||
},
|
||||
|
||||
validate: function(newattrs) {
|
||||
@@ -32,9 +42,30 @@ var CourseDetails = Backbone.Model.extend({
|
||||
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) {
|
||||
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) {
|
||||
errors.end_date = gettext("The course end date must be later than the course start date.");
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
define([
|
||||
'jquery', 'js/models/settings/course_details', 'js/views/settings/main',
|
||||
'common/js/spec_helpers/ajax_helpers'
|
||||
], function($, CourseDetailsModel, MainView, AjaxHelpers) {
|
||||
'common/js/spec_helpers/ajax_helpers', 'common/js/spec_helpers/template_helpers',
|
||||
], function($, CourseDetailsModel, MainView, AjaxHelpers, TemplateHelpers) {
|
||||
'use strict';
|
||||
|
||||
var SELECTORS = {
|
||||
entrance_exam_min_score: '#entrance-exam-minimum-score-pct',
|
||||
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 () {
|
||||
@@ -21,24 +25,50 @@ define([
|
||||
course_id : '',
|
||||
run : '',
|
||||
syllabus : null,
|
||||
title: '',
|
||||
subtitle: '',
|
||||
duration: '',
|
||||
description: '',
|
||||
short_description : '',
|
||||
overview : '',
|
||||
intro_video : null,
|
||||
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',
|
||||
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 () {
|
||||
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.view = new MainView({
|
||||
el: $('.settings-details'),
|
||||
@@ -178,5 +208,149 @@ 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);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
92
cms/static/js/views/instructor_info.js
Normal file
92
cms/static/js/views/instructor_info.js
Normal file
@@ -0,0 +1,92 @@
|
||||
// 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;
|
||||
});
|
||||
53
cms/static/js/views/learning_info.js
Normal file
53
cms/static/js/views/learning_info.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// 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;
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
define(["js/views/validation", "codemirror", "underscore", "jquery", "jquery.ui", "js/utils/date_utils", "js/models/uploads",
|
||||
"js/views/uploads", "js/views/license", "js/models/license",
|
||||
"common/js/components/views/feedback_notification", "jquery.timepicker", "date", "gettext"],
|
||||
define(["js/views/validation", "codemirror", "underscore", "jquery", "jquery.ui", "js/utils/date_utils",
|
||||
"js/models/uploads", "js/views/uploads", "js/views/license", "js/models/license",
|
||||
"common/js/components/views/feedback_notification", "jquery.timepicker", "date", "gettext",
|
||||
"js/views/learning_info", "js/views/instructor_info"],
|
||||
function(ValidatingView, CodeMirror, _, $, ui, DateUtils, FileUploadModel,
|
||||
FileUploadDialog, LicenseView, LicenseModel, NotificationView,
|
||||
timepicker, date, gettext) {
|
||||
timepicker, date, gettext, LearningInfoView, InstructorInfoView) {
|
||||
|
||||
var DetailsView = ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseDetails
|
||||
@@ -20,7 +21,9 @@ var DetailsView = ValidatingView.extend({
|
||||
// would love to move to a general superclass, but event hashes don't inherit in backbone :-(
|
||||
'focus :input' : "inputFocus",
|
||||
'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) {
|
||||
@@ -34,10 +37,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();
|
||||
});
|
||||
|
||||
@@ -60,6 +63,16 @@ var DetailsView = ValidatingView.extend({
|
||||
closeIcon: true
|
||||
}).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() {
|
||||
@@ -71,6 +84,16 @@ var DetailsView = ValidatingView.extend({
|
||||
this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview'));
|
||||
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('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
|
||||
@@ -82,9 +105,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 : '';
|
||||
@@ -115,7 +146,9 @@ var DetailsView = ValidatingView.extend({
|
||||
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;
|
||||
},
|
||||
@@ -126,13 +159,47 @@ var DetailsView = ValidatingView.extend({
|
||||
'enrollment_start' : 'enrollment-start',
|
||||
'enrollment_end' : 'enrollment-end',
|
||||
'overview' : 'course-overview',
|
||||
'title': 'course-title',
|
||||
'subtitle': 'course-subtitle',
|
||||
'duration': 'course-duration',
|
||||
'description': 'course-description',
|
||||
'short_description' : 'course-short-description',
|
||||
'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'
|
||||
'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) {
|
||||
@@ -148,23 +215,34 @@ var DetailsView = ValidatingView.extend({
|
||||
},
|
||||
|
||||
updateModel: function(event) {
|
||||
var value;
|
||||
var index = event.currentTarget.getAttribute('data-index');
|
||||
switch (event.currentTarget.id) {
|
||||
case 'course-language':
|
||||
this.setField(event);
|
||||
case 'course-learning-info-' + index:
|
||||
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;
|
||||
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 'course-effort':
|
||||
this.setField(event);
|
||||
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")){
|
||||
@@ -183,9 +261,6 @@ var DetailsView = ValidatingView.extend({
|
||||
this.setField(event);
|
||||
}
|
||||
break;
|
||||
case 'course-short-description':
|
||||
this.setField(event);
|
||||
break;
|
||||
case 'pre-requisite-course':
|
||||
var value = $(event.currentTarget).val();
|
||||
value = value == "" ? [] : [value];
|
||||
@@ -212,11 +287,30 @@ var DetailsView = ValidatingView.extend({
|
||||
case 'course-pace-instructor-paced':
|
||||
this.model.set('self_paced', JSON.parse(event.currentTarget.value));
|
||||
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.
|
||||
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')) {
|
||||
@@ -300,8 +394,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']
|
||||
});
|
||||
@@ -309,13 +425,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);
|
||||
@@ -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
|
||||
&.advanced-policies {
|
||||
|
||||
|
||||
43
cms/templates/js/course-instructor-details.underscore
Normal file
43
cms/templates/js/course-instructor-details.underscore
Normal file
@@ -0,0 +1,43 @@
|
||||
<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>
|
||||
@@ -0,0 +1,5 @@
|
||||
<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 @@
|
||||
</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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</form>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
%>
|
||||
|
||||
<%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">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
@@ -27,7 +27,7 @@
|
||||
<script type="text/javascript">
|
||||
window.CMS = window.CMS || {};
|
||||
CMS.URL = CMS.URL || {};
|
||||
CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
|
||||
CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -292,9 +292,33 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
|
||||
<span class="tip">${_("Information for prospective students")}</span>
|
||||
</header>
|
||||
<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:
|
||||
<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>
|
||||
<span class="tip tip-stacked">${_("Appears on the course catalog page when students roll over the course name. Limit to ~150 characters")}</span>
|
||||
</li>
|
||||
@@ -315,11 +339,11 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
|
||||
% endif
|
||||
|
||||
<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">
|
||||
% 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">
|
||||
@@ -328,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
|
||||
@@ -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" />
|
||||
<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-overview">${_("Course Introduction Video")}</label>
|
||||
<label for="course-introduction-video">${_("Course Introduction Video")}</label>
|
||||
<div class="input input-existing">
|
||||
<div class="current current-course-introduction-video">
|
||||
<iframe width="618" height="350" title="${_('Course Introduction Video')}" src="" frameborder="0" allowfullscreen></iframe>
|
||||
@@ -362,10 +448,44 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
|
||||
<span class="tip tip-stacked">${_("Enter your YouTube video's ID (along with any restriction parameters)")}</span>
|
||||
</div>
|
||||
</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>
|
||||
<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>
|
||||
% endif
|
||||
|
||||
% if about_page_editable or is_prerequisite_courses_enabled or is_entrance_exams_enabled:
|
||||
<hr class="divide" />
|
||||
|
||||
|
||||
@@ -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=_(
|
||||
@@ -774,6 +794,36 @@ class CourseFields(object):
|
||||
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
|
||||
"""
|
||||
|
||||
@@ -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,53 @@ 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, 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):
|
||||
'cert_name_short',
|
||||
'certificates_display_behavior',
|
||||
'course_image',
|
||||
'banner_image',
|
||||
'video_thumbnail_image',
|
||||
'cosmetic_display_price',
|
||||
'advertised_start',
|
||||
'announcement',
|
||||
@@ -217,4 +219,6 @@ class AdvancedSettingsPage(CoursePage):
|
||||
'enable_proctored_exams',
|
||||
'enable_timed_exams',
|
||||
'enable_subsection_gating',
|
||||
'learning_info',
|
||||
'instructor_info'
|
||||
]
|
||||
|
||||
@@ -475,3 +475,36 @@ class ContentLicenseTest(StudioCourseTest):
|
||||
# The course_license text will include a bunch of screen reader text to explain
|
||||
# the selected options
|
||||
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
|
||||
# handled separately; its value maps to an alternate key name.
|
||||
ABOUT_ATTRIBUTES = [
|
||||
'syllabus',
|
||||
'title',
|
||||
'subtitle',
|
||||
'duration',
|
||||
'description',
|
||||
'short_description',
|
||||
'overview',
|
||||
'effort',
|
||||
@@ -43,6 +47,10 @@ class CourseDetails(object):
|
||||
self.enrollment_start = None
|
||||
self.enrollment_end = None
|
||||
self.syllabus = None # a pdf file asset
|
||||
self.title = ""
|
||||
self.subtitle = ""
|
||||
self.duration = ""
|
||||
self.description = ""
|
||||
self.short_description = ""
|
||||
self.overview = "" # html to render as the overview
|
||||
self.intro_video = None # a video pointer
|
||||
@@ -50,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
|
||||
@@ -58,6 +70,8 @@ class CourseDetails(object):
|
||||
'50'
|
||||
) # minimum passing score for entrance exam content module/tree,
|
||||
self.self_paced = None
|
||||
self.learning_info = []
|
||||
self.instructor_info = []
|
||||
|
||||
@classmethod
|
||||
def fetch_about_attribute(cls, course_key, attribute):
|
||||
@@ -89,9 +103,15 @@ 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
|
||||
course_details.learning_info = descriptor.learning_info
|
||||
course_details.instructor_info = descriptor.instructor_info
|
||||
|
||||
# Default course license is "All Rights Reserved"
|
||||
course_details.license = getattr(descriptor, "license", "all-rights-reserved")
|
||||
@@ -209,6 +229,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']
|
||||
@@ -218,6 +247,14 @@ class CourseDetails(object):
|
||||
descriptor.license = jsondict['license']
|
||||
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:
|
||||
descriptor.language = jsondict['language']
|
||||
dirty = True
|
||||
|
||||
@@ -95,11 +95,41 @@ 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,
|
||||
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):
|
||||
SelfPacedConfiguration(enabled=True).save()
|
||||
|
||||
@@ -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 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
|
||||
# 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