Revert "feat: Reimagine certificate_availability_date and certificates_display_behavior"
This commit is contained in:
@@ -41,7 +41,6 @@ class CourseMetadata:
|
||||
'enrollment_start',
|
||||
'enrollment_end',
|
||||
'certificate_available_date',
|
||||
'certificates_display_behavior',
|
||||
'tabs',
|
||||
'graceperiod',
|
||||
'show_timezone',
|
||||
|
||||
@@ -13,12 +13,6 @@ define([
|
||||
$('label').removeClass('is-focused');
|
||||
});
|
||||
|
||||
// Toggle collapsibles when trigger is clicked
|
||||
$(".collapsible .collapsible-trigger").click(function() {
|
||||
const contentId = this.id.replace("-trigger", "-content")
|
||||
$(`#${contentId}`).toggleClass("collapsed")
|
||||
})
|
||||
|
||||
model = new CourseDetailsModel();
|
||||
model.urlRoot = detailsUrl;
|
||||
model.showCertificateAvailableDate = showCertificateAvailableDate;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
define(['backbone', 'underscore', 'gettext', 'js/models/validation_helpers', 'js/utils/date_utils',
|
||||
'edx-ui-toolkit/js/utils/string-utils'
|
||||
],
|
||||
function (Backbone, _, gettext, ValidationHelpers, DateUtils, StringUtils) {
|
||||
function(Backbone, _, gettext, ValidationHelpers, DateUtils, StringUtils) {
|
||||
'use strict';
|
||||
var CourseDetails = Backbone.Model.extend({
|
||||
defaults: {
|
||||
@@ -11,7 +11,6 @@ define(['backbone', 'underscore', 'gettext', 'js/models/validation_helpers', 'js
|
||||
language: '',
|
||||
start_date: null, // maps to 'start'
|
||||
end_date: null, // maps to 'end'
|
||||
certificates_display_behavior: "",
|
||||
certificate_available_date: null,
|
||||
enrollment_start: null,
|
||||
enrollment_end: null,
|
||||
@@ -39,16 +38,10 @@ define(['backbone', 'underscore', 'gettext', 'js/models/validation_helpers', 'js
|
||||
self_paced: null
|
||||
},
|
||||
|
||||
validate: function (newattrs) {
|
||||
// Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs
|
||||
// A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
|
||||
validate: function(newattrs) {
|
||||
// Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs
|
||||
// A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
|
||||
var errors = {};
|
||||
const CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS = {
|
||||
END: "end",
|
||||
END_WITH_DATE: "end_with_date",
|
||||
EARLY_NO_INFO: "early_no_info"
|
||||
};
|
||||
|
||||
newattrs = DateUtils.convertDateStringsToObjects(
|
||||
newattrs,
|
||||
['start_date', 'end_date', 'certificate_available_date', 'enrollment_start', 'enrollment_end']
|
||||
@@ -62,15 +55,15 @@ define(['backbone', 'underscore', 'gettext', 'js/models/validation_helpers', 'js
|
||||
errors.end_date = gettext('The course end date must be later than the course start date.');
|
||||
}
|
||||
if (newattrs.start_date && newattrs.enrollment_start &&
|
||||
newattrs.start_date < newattrs.enrollment_start) {
|
||||
newattrs.start_date < newattrs.enrollment_start) {
|
||||
errors.enrollment_start = gettext(
|
||||
'The course start date must be later than the enrollment start date.'
|
||||
'The course start date must be later than the enrollment start date.'
|
||||
);
|
||||
}
|
||||
if (newattrs.enrollment_start && newattrs.enrollment_end &&
|
||||
newattrs.enrollment_start >= newattrs.enrollment_end) {
|
||||
newattrs.enrollment_start >= newattrs.enrollment_end) {
|
||||
errors.enrollment_end = gettext(
|
||||
'The enrollment start date cannot be after the enrollment end date.'
|
||||
'The enrollment start date cannot be after the enrollment end date.'
|
||||
);
|
||||
}
|
||||
if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) {
|
||||
@@ -82,43 +75,11 @@ define(['backbone', 'underscore', 'gettext', 'js/models/validation_helpers', 'js
|
||||
'The certificate available date must be later than the course end date.'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
newattrs.certificates_display_behavior
|
||||
&& !(Object.values(CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS).includes(newattrs.certificates_display_behavior))
|
||||
) {
|
||||
|
||||
errors.certificates_display_behavior = StringUtils.interpolate(
|
||||
gettext(
|
||||
"The certificate display behavior must be one of: {behavior_options}"
|
||||
),
|
||||
{
|
||||
behavior_options: Object.values(CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS).join(', ')
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Throw error if there's a value for certificate_available_date
|
||||
if(
|
||||
(newattrs.certificate_available_date && newattrs.certificates_display_behavior != CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS.END_WITH_DATE)
|
||||
|| (!newattrs.certificate_available_date && newattrs.certificates_display_behavior == CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS.END_WITH_DATE)
|
||||
){
|
||||
errors.certificates_display_behavior = StringUtils.interpolate(
|
||||
gettext(
|
||||
"The certificates display behavior must be {valid_option} if certificate available date is set."
|
||||
),
|
||||
{
|
||||
valid_option: CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS.END_WITH_DATE
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) {
|
||||
if (this._videokey_illegal_chars.exec(newattrs.intro_video)) {
|
||||
errors.intro_video = gettext('Key should only contain letters, numbers, _, or -');
|
||||
}
|
||||
// TODO check if key points to a real video using google's youtube api
|
||||
// TODO check if key points to a real video using google's youtube api
|
||||
}
|
||||
if (_.has(newattrs, 'entrance_exam_minimum_score_pct')) {
|
||||
var range = {
|
||||
@@ -127,37 +88,37 @@ define(['backbone', 'underscore', 'gettext', 'js/models/validation_helpers', 'js
|
||||
};
|
||||
if (!ValidationHelpers.validateIntegerRange(newattrs.entrance_exam_minimum_score_pct, range)) {
|
||||
errors.entrance_exam_minimum_score_pct = StringUtils.interpolate(gettext(
|
||||
'Please enter an integer between %(min)s and %(max)s.'
|
||||
'Please enter an integer between %(min)s and %(max)s.'
|
||||
), range, true);
|
||||
}
|
||||
}
|
||||
if (!_.isEmpty(errors)) return errors;
|
||||
// NOTE don't return empty errors as that will be interpreted as an error state
|
||||
// NOTE don't return empty errors as that will be interpreted as an error state
|
||||
},
|
||||
|
||||
_videokey_illegal_chars: /[^a-zA-Z0-9_-]/g,
|
||||
|
||||
set_videosource: function (newsource) {
|
||||
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
|
||||
// returns the videosource for the preview which iss the key whose speed is closest to 1
|
||||
set_videosource: function(newsource) {
|
||||
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
|
||||
// returns the videosource for the preview which iss the key whose speed is closest to 1
|
||||
if (_.isEmpty(newsource) &&
|
||||
!_.isEmpty(this.get('intro_video'))) this.set({ intro_video: null }, { validate: true });
|
||||
// TODO remove all whitespace w/in string
|
||||
!_.isEmpty(this.get('intro_video'))) this.set({intro_video: null}, {validate: true});
|
||||
// TODO remove all whitespace w/in string
|
||||
else {
|
||||
if (this.get('intro_video') !== newsource) this.set('intro_video', newsource, { validate: true });
|
||||
if (this.get('intro_video') !== newsource) this.set('intro_video', newsource, {validate: true});
|
||||
}
|
||||
|
||||
return this.videosourceSample();
|
||||
},
|
||||
|
||||
videosourceSample: function () {
|
||||
videosourceSample: function() {
|
||||
if (this.has('intro_video')) return '//www.youtube.com/embed/' + this.get('intro_video');
|
||||
else return '';
|
||||
},
|
||||
|
||||
// Whether or not the course pacing can be toggled. If the course
|
||||
// has already started, returns false; otherwise, returns true.
|
||||
canTogglePace: function () {
|
||||
// Whether or not the course pacing can be toggled. If the course
|
||||
// has already started, returns false; otherwise, returns true.
|
||||
canTogglePace: function() {
|
||||
return new Date() <= new Date(this.get('start_date'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -21,8 +21,7 @@ define([
|
||||
end_date: '2014-11-05T20:00:00Z',
|
||||
enrollment_start: '2014-10-00T00:00:00Z',
|
||||
enrollment_end: '2014-11-05T00:00:00Z',
|
||||
certificates_display_behavior: 'end',
|
||||
certificate_available_date: null,
|
||||
certificate_available_date: '2014-11-05T20:00:00Z',
|
||||
org: '',
|
||||
course_id: '',
|
||||
run: '',
|
||||
@@ -352,8 +351,6 @@ define([
|
||||
});
|
||||
it('should disallow save with a certificate available date before end date', function() {
|
||||
this.model.showCertificateAvailableDate = true;
|
||||
$('#certificates-display-behavior').val('end_with_date').trigger('change');
|
||||
$('#certificate-available-date').val('01/01/2020').trigger('change');
|
||||
$('#course-end-date').val('01/01/2030').trigger('change');
|
||||
expect(this.view.$('.message-error')).toExist();
|
||||
});
|
||||
|
||||
@@ -2,503 +2,469 @@ define(['js/views/validation', 'codemirror', 'underscore', 'jquery', 'jquery.ui'
|
||||
'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', 'edx-ui-toolkit/js/utils/string-utils'],
|
||||
function (ValidatingView, CodeMirror, _, $, ui, DateUtils, FileUploadModel,
|
||||
FileUploadDialog, LicenseView, LicenseModel, NotificationView,
|
||||
timepicker, date, gettext, LearningInfoView, InstructorInfoView, StringUtils) {
|
||||
var DetailsView = ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseDetails
|
||||
events: {
|
||||
'input input': 'updateModel',
|
||||
'input textarea': 'updateModel',
|
||||
// Leaving change in as fallback for older browsers
|
||||
'change input': 'updateModel',
|
||||
'change textarea': 'updateModel',
|
||||
'change select': 'updateModel',
|
||||
'click .remove-course-introduction-video': 'removeVideo',
|
||||
'focus #course-overview': 'codeMirrorize',
|
||||
'focus #course-about-sidebar-html': 'codeMirrorize',
|
||||
'mouseover .timezone': 'updateTime',
|
||||
// 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 .add-course-learning-info': 'addLearningFields',
|
||||
'click .add-course-instructor-info': 'addInstructorFields'
|
||||
},
|
||||
function(ValidatingView, CodeMirror, _, $, ui, DateUtils, FileUploadModel,
|
||||
FileUploadDialog, LicenseView, LicenseModel, NotificationView,
|
||||
timepicker, date, gettext, LearningInfoView, InstructorInfoView, StringUtils) {
|
||||
var DetailsView = ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseDetails
|
||||
events: {
|
||||
'input input': 'updateModel',
|
||||
'input textarea': 'updateModel',
|
||||
// Leaving change in as fallback for older browsers
|
||||
'change input': 'updateModel',
|
||||
'change textarea': 'updateModel',
|
||||
'change select': 'updateModel',
|
||||
'click .remove-course-introduction-video': 'removeVideo',
|
||||
'focus #course-overview': 'codeMirrorize',
|
||||
'focus #course-about-sidebar-html': 'codeMirrorize',
|
||||
'mouseover .timezone': 'updateTime',
|
||||
// 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 .add-course-learning-info': 'addLearningFields',
|
||||
'click .add-course-instructor-info': 'addInstructorFields'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
options = options || {};
|
||||
// fill in fields
|
||||
this.$el.find('#course-language').val(this.model.get('language'));
|
||||
this.$el.find('#course-organization').val(this.model.get('org'));
|
||||
this.$el.find('#course-number').val(this.model.get('course_id'));
|
||||
this.$el.find('#course-name').val(this.model.get('run'));
|
||||
this.$el.find('.set-date').datepicker({ dateFormat: 'm/d/yy' });
|
||||
this.$el.find("#certificates-display-behavior").val(this.model.get("certificates_display_behavior"));
|
||||
this.updateCertificatesDisplayBehavior();
|
||||
initialize: function(options) {
|
||||
options = options || {};
|
||||
// fill in fields
|
||||
this.$el.find('#course-language').val(this.model.get('language'));
|
||||
this.$el.find('#course-organization').val(this.model.get('org'));
|
||||
this.$el.find('#course-number').val(this.model.get('course_id'));
|
||||
this.$el.find('#course-name').val(this.model.get('run'));
|
||||
this.$el.find('.set-date').datepicker({dateFormat: 'm/d/yy'});
|
||||
|
||||
// Avoid showing broken image on mistyped/nonexistent image
|
||||
this.$el.find('img').error(function () {
|
||||
$(this).hide();
|
||||
});
|
||||
this.$el.find('img').load(function () {
|
||||
$(this).show();
|
||||
});
|
||||
// Avoid showing broken image on mistyped/nonexistent image
|
||||
this.$el.find('img').error(function() {
|
||||
$(this).hide();
|
||||
});
|
||||
this.$el.find('img').load(function() {
|
||||
$(this).show();
|
||||
});
|
||||
|
||||
this.listenTo(this.model, 'invalid', this.handleValidationError);
|
||||
this.listenTo(this.model, 'change', this.showNotificationBar);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
// handle license separately, to avoid reimplementing view logic
|
||||
this.licenseModel = new LicenseModel({ asString: this.model.get('license') });
|
||||
this.licenseView = new LicenseView({
|
||||
model: this.licenseModel,
|
||||
el: this.$('#course-license-selector').get(),
|
||||
showPreview: true
|
||||
});
|
||||
this.listenTo(this.licenseModel, 'change', this.handleLicenseChange);
|
||||
this.listenTo(this.model, 'invalid', this.handleValidationError);
|
||||
this.listenTo(this.model, 'change', this.showNotificationBar);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
// handle license separately, to avoid reimplementing view logic
|
||||
this.licenseModel = new LicenseModel({asString: this.model.get('license')});
|
||||
this.licenseView = new LicenseView({
|
||||
model: this.licenseModel,
|
||||
el: this.$('#course-license-selector').get(),
|
||||
showPreview: true
|
||||
});
|
||||
this.listenTo(this.licenseModel, 'change', this.handleLicenseChange);
|
||||
|
||||
if (options.showMinGradeWarning || false) {
|
||||
new NotificationView.Warning({
|
||||
title: gettext('Course Credit Requirements'),
|
||||
message: gettext('The minimum grade for course credit is not set.'),
|
||||
closeIcon: true
|
||||
}).show();
|
||||
}
|
||||
if (options.showMinGradeWarning || false) {
|
||||
new NotificationView.Warning({
|
||||
title: gettext('Course Credit Requirements'),
|
||||
message: gettext('The minimum grade for course credit is not set.'),
|
||||
closeIcon: true
|
||||
}).show();
|
||||
}
|
||||
|
||||
this.learning_info_view = new LearningInfoView({
|
||||
el: $('.course-settings-learning-fields'),
|
||||
model: this.model
|
||||
});
|
||||
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
|
||||
});
|
||||
},
|
||||
this.instructor_info_view = new InstructorInfoView({
|
||||
el: $('.course-instructor-details-fields'),
|
||||
model: this.model
|
||||
});
|
||||
},
|
||||
|
||||
render: function () {
|
||||
// Clear any image preview timeouts set in this.updateImagePreview
|
||||
clearTimeout(this.imageTimer);
|
||||
render: function() {
|
||||
// Clear any image preview timeouts set in this.updateImagePreview
|
||||
clearTimeout(this.imageTimer);
|
||||
|
||||
DateUtils.setupDatePicker('start_date', this);
|
||||
DateUtils.setupDatePicker('end_date', this);
|
||||
DateUtils.setupDatePicker('certificate_available_date', this);
|
||||
DateUtils.setupDatePicker('enrollment_start', this);
|
||||
DateUtils.setupDatePicker('enrollment_end', this);
|
||||
DateUtils.setupDatePicker('upgrade_deadline', this);
|
||||
DateUtils.setupDatePicker('start_date', this);
|
||||
DateUtils.setupDatePicker('end_date', this);
|
||||
DateUtils.setupDatePicker('certificate_available_date', this);
|
||||
DateUtils.setupDatePicker('enrollment_start', this);
|
||||
DateUtils.setupDatePicker('enrollment_end', this);
|
||||
DateUtils.setupDatePicker('upgrade_deadline', this);
|
||||
|
||||
this.$el.find('#' + this.fieldToSelectorMap.overview).val(this.model.get('overview'));
|
||||
this.codeMirrorize(null, $('#course-overview')[0]);
|
||||
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'));
|
||||
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.about_sidebar_html).val(
|
||||
this.model.get('about_sidebar_html')
|
||||
);
|
||||
this.codeMirrorize(null, $('#course-about-sidebar-html')[0]);
|
||||
this.$el.find('#' + this.fieldToSelectorMap.short_description).val(this.model.get('short_description'));
|
||||
this.$el.find('#' + this.fieldToSelectorMap.about_sidebar_html).val(
|
||||
this.model.get('about_sidebar_html')
|
||||
);
|
||||
this.codeMirrorize(null, $('#course-about-sidebar-html')[0]);
|
||||
|
||||
this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
|
||||
this.$el.find('#' + this.fieldToSelectorMap.intro_video).val(this.model.get('intro_video') || '');
|
||||
if (this.model.has('intro_video')) {
|
||||
this.$el.find('.remove-course-introduction-video').show();
|
||||
} else this.$el.find('.remove-course-introduction-video').hide();
|
||||
this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
|
||||
this.$el.find('#' + this.fieldToSelectorMap.intro_video).val(this.model.get('intro_video') || '');
|
||||
if (this.model.has('intro_video')) {
|
||||
this.$el.find('.remove-course-introduction-video').show();
|
||||
} else this.$el.find('.remove-course-introduction-video').hide();
|
||||
|
||||
this.$el.find('#' + this.fieldToSelectorMap.effort).val(this.model.get('effort'));
|
||||
this.$el.find("#" + this.fieldToSelectorMap.certificates_display_behavior).val(this.model.get('certificates_display_behavior'));
|
||||
this.updateCertificatesDisplayBehavior();
|
||||
this.$el.find('#' + this.fieldToSelectorMap.effort).val(this.model.get('effort'));
|
||||
|
||||
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 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 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 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 : '';
|
||||
this.$el.find('#' + this.fieldToSelectorMap.pre_requisite_courses).val(pre_requisite_courses);
|
||||
var pre_requisite_courses = this.model.get('pre_requisite_courses');
|
||||
pre_requisite_courses = pre_requisite_courses.length > 0 ? pre_requisite_courses : '';
|
||||
this.$el.find('#' + this.fieldToSelectorMap.pre_requisite_courses).val(pre_requisite_courses);
|
||||
|
||||
if (this.model.get('entrance_exam_enabled') == 'true') {
|
||||
this.$('#' + this.fieldToSelectorMap.entrance_exam_enabled).attr('checked', this.model.get('entrance_exam_enabled'));
|
||||
this.$('.div-grade-requirements').show();
|
||||
} else {
|
||||
this.$('#' + this.fieldToSelectorMap.entrance_exam_enabled).removeAttr('checked');
|
||||
this.$('.div-grade-requirements').hide();
|
||||
}
|
||||
this.$('#' + this.fieldToSelectorMap.entrance_exam_minimum_score_pct).val(this.model.get('entrance_exam_minimum_score_pct'));
|
||||
if (this.model.get('entrance_exam_enabled') == 'true') {
|
||||
this.$('#' + this.fieldToSelectorMap.entrance_exam_enabled).attr('checked', this.model.get('entrance_exam_enabled'));
|
||||
this.$('.div-grade-requirements').show();
|
||||
} else {
|
||||
this.$('#' + this.fieldToSelectorMap.entrance_exam_enabled).removeAttr('checked');
|
||||
this.$('.div-grade-requirements').hide();
|
||||
}
|
||||
this.$('#' + this.fieldToSelectorMap.entrance_exam_minimum_score_pct).val(this.model.get('entrance_exam_minimum_score_pct'));
|
||||
|
||||
var selfPacedButton = this.$('#course-pace-self-paced'),
|
||||
instructorPacedButton = this.$('#course-pace-instructor-paced'),
|
||||
paceToggleTip = this.$('#course-pace-toggle-tip');
|
||||
(this.model.get('self_paced') ? selfPacedButton : instructorPacedButton).attr('checked', true);
|
||||
if (this.model.canTogglePace()) {
|
||||
selfPacedButton.removeAttr('disabled');
|
||||
instructorPacedButton.removeAttr('disabled');
|
||||
paceToggleTip.text('');
|
||||
} else {
|
||||
selfPacedButton.attr('disabled', true);
|
||||
instructorPacedButton.attr('disabled', true);
|
||||
paceToggleTip.text(gettext('Course pacing cannot be changed once a course has started.'));
|
||||
}
|
||||
var selfPacedButton = this.$('#course-pace-self-paced'),
|
||||
instructorPacedButton = this.$('#course-pace-instructor-paced'),
|
||||
paceToggleTip = this.$('#course-pace-toggle-tip');
|
||||
(this.model.get('self_paced') ? selfPacedButton : instructorPacedButton).attr('checked', true);
|
||||
if (this.model.canTogglePace()) {
|
||||
selfPacedButton.removeAttr('disabled');
|
||||
instructorPacedButton.removeAttr('disabled');
|
||||
paceToggleTip.text('');
|
||||
} else {
|
||||
selfPacedButton.attr('disabled', true);
|
||||
instructorPacedButton.attr('disabled', true);
|
||||
paceToggleTip.text(gettext('Course pacing cannot be changed once a course has started.'));
|
||||
}
|
||||
|
||||
this.licenseView.render();
|
||||
this.learning_info_view.render();
|
||||
this.instructor_info_view.render();
|
||||
this.licenseView.render();
|
||||
this.learning_info_view.render();
|
||||
this.instructor_info_view.render();
|
||||
|
||||
return this;
|
||||
},
|
||||
fieldToSelectorMap: {
|
||||
language: 'course-language',
|
||||
start_date: 'course-start',
|
||||
end_date: 'course-end',
|
||||
enrollment_start: 'enrollment-start',
|
||||
enrollment_end: 'enrollment-end',
|
||||
upgrade_deadline: 'upgrade-deadline',
|
||||
certificate_available_date: 'certificate-available',
|
||||
certificates_display_behavior: 'certificates-display-behavior',
|
||||
overview: 'course-overview',
|
||||
title: 'course-title',
|
||||
subtitle: 'course-subtitle',
|
||||
duration: 'course-duration',
|
||||
description: 'course-description',
|
||||
about_sidebar_html: 'course-about-sidebar-html',
|
||||
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',
|
||||
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'
|
||||
},
|
||||
return this;
|
||||
},
|
||||
fieldToSelectorMap: {
|
||||
language: 'course-language',
|
||||
start_date: 'course-start',
|
||||
end_date: 'course-end',
|
||||
enrollment_start: 'enrollment-start',
|
||||
enrollment_end: 'enrollment-end',
|
||||
upgrade_deadline: 'upgrade-deadline',
|
||||
certificate_available_date: 'certificate-available',
|
||||
overview: 'course-overview',
|
||||
title: 'course-title',
|
||||
subtitle: 'course-subtitle',
|
||||
duration: 'course-duration',
|
||||
description: 'course-description',
|
||||
about_sidebar_html: 'course-about-sidebar-html',
|
||||
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',
|
||||
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);
|
||||
},
|
||||
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 });
|
||||
},
|
||||
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) {
|
||||
var now = new Date(),
|
||||
hours = now.getUTCHours(),
|
||||
minutes = now.getUTCMinutes(),
|
||||
currentTimeText = StringUtils.interpolate(
|
||||
gettext('{hours}:{minutes} (current UTC time)'),
|
||||
{
|
||||
hours: hours,
|
||||
minutes: minutes
|
||||
}
|
||||
);
|
||||
updateTime: function(e) {
|
||||
var now = new Date(),
|
||||
hours = now.getUTCHours(),
|
||||
minutes = now.getUTCMinutes(),
|
||||
currentTimeText = StringUtils.interpolate(
|
||||
gettext('{hours}:{minutes} (current UTC time)'),
|
||||
{
|
||||
hours: hours,
|
||||
minutes: minutes
|
||||
}
|
||||
);
|
||||
|
||||
$(e.currentTarget).attr('title', currentTimeText);
|
||||
},
|
||||
updateModel: function (event) {
|
||||
var value;
|
||||
var index = event.currentTarget.getAttribute('data-index');
|
||||
switch (event.currentTarget.id) {
|
||||
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-instructor-image-' + index:
|
||||
instructors = this.model.get('instructor_info').instructors.slice(0);
|
||||
instructors[index].image = $(event.currentTarget).val();
|
||||
this.model.set('instructor_info', { instructors: instructors });
|
||||
this.showNotificationBar();
|
||||
this.updateImagePreview(event.currentTarget, '#course-instructor-image-preview-' + index);
|
||||
break;
|
||||
case 'course-image-url':
|
||||
this.updateImageField(event, 'course_image_name', '#course-image');
|
||||
break;
|
||||
case 'banner-image-url':
|
||||
this.updateImageField(event, 'banner_image_name', '#banner-image');
|
||||
break;
|
||||
case 'video-thumbnail-image-url':
|
||||
this.updateImageField(event, 'video_thumbnail_image_name', '#video-thumbnail-image');
|
||||
break;
|
||||
case 'entrance-exam-enabled':
|
||||
if ($(event.currentTarget).is(':checked')) {
|
||||
this.$('.div-grade-requirements').show();
|
||||
} else {
|
||||
this.$('.div-grade-requirements').hide();
|
||||
}
|
||||
this.setField(event);
|
||||
break;
|
||||
case 'entrance-exam-minimum-score-pct':
|
||||
// If the val is an empty string then update model with default value.
|
||||
if ($(event.currentTarget).val() === '') {
|
||||
this.model.set('entrance_exam_minimum_score_pct', this.model.defaults.entrance_exam_minimum_score_pct);
|
||||
} else {
|
||||
this.setField(event);
|
||||
}
|
||||
break;
|
||||
case 'pre-requisite-course':
|
||||
var value = $(event.currentTarget).val();
|
||||
value = value == '' ? [] : [value];
|
||||
this.model.set('pre_requisite_courses', value);
|
||||
break;
|
||||
// Don't make the user reload the page to check the Youtube ID.
|
||||
// Wait for a second to load the video, avoiding egregious AJAX calls.
|
||||
case 'course-introduction-video':
|
||||
this.clearValidationErrors();
|
||||
var previewsource = this.model.set_videosource($(event.currentTarget).val());
|
||||
clearTimeout(this.videoTimer);
|
||||
this.videoTimer = setTimeout(_.bind(function () {
|
||||
this.$el.find('.current-course-introduction-video iframe').attr('src', previewsource);
|
||||
if (this.model.has('intro_video')) {
|
||||
this.$el.find('.remove-course-introduction-video').show();
|
||||
} else {
|
||||
this.$el.find('.remove-course-introduction-video').hide();
|
||||
}
|
||||
}, this), 1000);
|
||||
break;
|
||||
case 'course-pace-self-paced':
|
||||
// Fallthrough to handle both radio buttons
|
||||
case 'course-pace-instructor-paced':
|
||||
this.model.set('self_paced', JSON.parse(event.currentTarget.value));
|
||||
break;
|
||||
case 'certificates-display-behavior':
|
||||
this.setField(event);
|
||||
this.updateCertificatesDisplayBehavior();
|
||||
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('/'));
|
||||
// If image path is entered directly, we need to strip the asset prefix
|
||||
image_name = _.last(image_name.split('block@'));
|
||||
this.model.set(image_field, image_name);
|
||||
this.updateImagePreview(event.currentTarget, selector);
|
||||
},
|
||||
updateImagePreview: function (imagePathInputElement, previewSelector) {
|
||||
// Wait to set the image src until the user stops typing
|
||||
clearTimeout(this.imageTimer);
|
||||
this.imageTimer = setTimeout(function () {
|
||||
$(previewSelector).attr('src', $(imagePathInputElement).val());
|
||||
}, 1000);
|
||||
},
|
||||
removeVideo: function (event) {
|
||||
event.preventDefault();
|
||||
if (this.model.has('intro_video')) {
|
||||
this.model.set_videosource(null);
|
||||
this.$el.find('.current-course-introduction-video iframe').attr('src', '');
|
||||
this.$el.find('#' + this.fieldToSelectorMap.intro_video).val('');
|
||||
this.$el.find('.remove-course-introduction-video').hide();
|
||||
}
|
||||
},
|
||||
codeMirrors: {},
|
||||
codeMirrorize: function (e, forcedTarget) {
|
||||
var thisTarget, cachethis, field, cmTextArea;
|
||||
if (forcedTarget) {
|
||||
thisTarget = forcedTarget;
|
||||
thisTarget.id = $(thisTarget).attr('id');
|
||||
} else if (e !== null) {
|
||||
thisTarget = e.currentTarget;
|
||||
} else {
|
||||
// e and forcedTarget can be null so don't deference it
|
||||
// This is because in cases where we have a marketing site
|
||||
// we don't display the codeMirrors for editing the marketing
|
||||
// materials, except we do need to show the 'set course image'
|
||||
// workflow. So in this case e = forcedTarget = null.
|
||||
return;
|
||||
}
|
||||
$(e.currentTarget).attr('title', currentTimeText);
|
||||
},
|
||||
updateModel: function(event) {
|
||||
var value;
|
||||
var index = event.currentTarget.getAttribute('data-index');
|
||||
switch (event.currentTarget.id) {
|
||||
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-instructor-image-' + index:
|
||||
instructors = this.model.get('instructor_info').instructors.slice(0);
|
||||
instructors[index].image = $(event.currentTarget).val();
|
||||
this.model.set('instructor_info', {instructors: instructors});
|
||||
this.showNotificationBar();
|
||||
this.updateImagePreview(event.currentTarget, '#course-instructor-image-preview-' + index);
|
||||
break;
|
||||
case 'course-image-url':
|
||||
this.updateImageField(event, 'course_image_name', '#course-image');
|
||||
break;
|
||||
case 'banner-image-url':
|
||||
this.updateImageField(event, 'banner_image_name', '#banner-image');
|
||||
break;
|
||||
case 'video-thumbnail-image-url':
|
||||
this.updateImageField(event, 'video_thumbnail_image_name', '#video-thumbnail-image');
|
||||
break;
|
||||
case 'entrance-exam-enabled':
|
||||
if ($(event.currentTarget).is(':checked')) {
|
||||
this.$('.div-grade-requirements').show();
|
||||
} else {
|
||||
this.$('.div-grade-requirements').hide();
|
||||
}
|
||||
this.setField(event);
|
||||
break;
|
||||
case 'entrance-exam-minimum-score-pct':
|
||||
// If the val is an empty string then update model with default value.
|
||||
if ($(event.currentTarget).val() === '') {
|
||||
this.model.set('entrance_exam_minimum_score_pct', this.model.defaults.entrance_exam_minimum_score_pct);
|
||||
} else {
|
||||
this.setField(event);
|
||||
}
|
||||
break;
|
||||
case 'pre-requisite-course':
|
||||
var value = $(event.currentTarget).val();
|
||||
value = value == '' ? [] : [value];
|
||||
this.model.set('pre_requisite_courses', value);
|
||||
break;
|
||||
// Don't make the user reload the page to check the Youtube ID.
|
||||
// Wait for a second to load the video, avoiding egregious AJAX calls.
|
||||
case 'course-introduction-video':
|
||||
this.clearValidationErrors();
|
||||
var previewsource = this.model.set_videosource($(event.currentTarget).val());
|
||||
clearTimeout(this.videoTimer);
|
||||
this.videoTimer = setTimeout(_.bind(function() {
|
||||
this.$el.find('.current-course-introduction-video iframe').attr('src', previewsource);
|
||||
if (this.model.has('intro_video')) {
|
||||
this.$el.find('.remove-course-introduction-video').show();
|
||||
} else {
|
||||
this.$el.find('.remove-course-introduction-video').hide();
|
||||
}
|
||||
}, this), 1000);
|
||||
break;
|
||||
case 'course-pace-self-paced':
|
||||
// Fallthrough to handle both radio buttons
|
||||
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('/'));
|
||||
// If image path is entered directly, we need to strip the asset prefix
|
||||
image_name = _.last(image_name.split('block@'));
|
||||
this.model.set(image_field, image_name);
|
||||
this.updateImagePreview(event.currentTarget, selector);
|
||||
},
|
||||
updateImagePreview: function(imagePathInputElement, previewSelector) {
|
||||
// Wait to set the image src until the user stops typing
|
||||
clearTimeout(this.imageTimer);
|
||||
this.imageTimer = setTimeout(function() {
|
||||
$(previewSelector).attr('src', $(imagePathInputElement).val());
|
||||
}, 1000);
|
||||
},
|
||||
removeVideo: function(event) {
|
||||
event.preventDefault();
|
||||
if (this.model.has('intro_video')) {
|
||||
this.model.set_videosource(null);
|
||||
this.$el.find('.current-course-introduction-video iframe').attr('src', '');
|
||||
this.$el.find('#' + this.fieldToSelectorMap.intro_video).val('');
|
||||
this.$el.find('.remove-course-introduction-video').hide();
|
||||
}
|
||||
},
|
||||
codeMirrors: {},
|
||||
codeMirrorize: function(e, forcedTarget) {
|
||||
var thisTarget, cachethis, field, cmTextArea;
|
||||
if (forcedTarget) {
|
||||
thisTarget = forcedTarget;
|
||||
thisTarget.id = $(thisTarget).attr('id');
|
||||
} else if (e !== null) {
|
||||
thisTarget = e.currentTarget;
|
||||
} else {
|
||||
// e and forcedTarget can be null so don't deference it
|
||||
// This is because in cases where we have a marketing site
|
||||
// we don't display the codeMirrors for editing the marketing
|
||||
// materials, except we do need to show the 'set course image'
|
||||
// workflow. So in this case e = forcedTarget = null.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.codeMirrors[thisTarget.id]) {
|
||||
cachethis = this;
|
||||
field = this.selectorToField[thisTarget.id];
|
||||
this.codeMirrors[thisTarget.id] = CodeMirror.fromTextArea(thisTarget, {
|
||||
mode: 'text/html', lineNumbers: true, lineWrapping: true
|
||||
});
|
||||
this.codeMirrors[thisTarget.id].on('change', function (mirror) {
|
||||
mirror.save();
|
||||
cachethis.clearValidationErrors();
|
||||
var newVal = mirror.getValue();
|
||||
if (cachethis.model.get(field) != newVal) {
|
||||
cachethis.setAndValidate(field, newVal);
|
||||
}
|
||||
});
|
||||
cmTextArea = this.codeMirrors[thisTarget.id].getInputField();
|
||||
cmTextArea.setAttribute('id', thisTarget.id + '-cm-textarea');
|
||||
}
|
||||
},
|
||||
if (!this.codeMirrors[thisTarget.id]) {
|
||||
cachethis = this;
|
||||
field = this.selectorToField[thisTarget.id];
|
||||
this.codeMirrors[thisTarget.id] = CodeMirror.fromTextArea(thisTarget, {
|
||||
mode: 'text/html', lineNumbers: true, lineWrapping: true});
|
||||
this.codeMirrors[thisTarget.id].on('change', function(mirror) {
|
||||
mirror.save();
|
||||
cachethis.clearValidationErrors();
|
||||
var newVal = mirror.getValue();
|
||||
if (cachethis.model.get(field) != newVal) {
|
||||
cachethis.setAndValidate(field, newVal);
|
||||
}
|
||||
});
|
||||
cmTextArea = this.codeMirrors[thisTarget.id].getInputField();
|
||||
cmTextArea.setAttribute('id', thisTarget.id + '-cm-textarea');
|
||||
}
|
||||
},
|
||||
|
||||
updateCertificatesDisplayBehavior: function() {
|
||||
/*
|
||||
Hides and clears the certificate available date field if a display behavior that doesn't use it is
|
||||
chosen. Because we are clearing it, toggling back to "end_with_date" will require re-entering the date
|
||||
*/
|
||||
console.info("IN UPDATER");
|
||||
let showDatepicker = this.model.get("certificates_display_behavior") == "end_with_date";
|
||||
let datepicker = this.$el.find('#certificate-available-date');
|
||||
let certificateAvailableDateField = this.$el.find('#field-certificate-available-date');
|
||||
revertView: function() {
|
||||
// Make sure that the CodeMirror instance has the correct
|
||||
// data from its corresponding textarea
|
||||
var self = this;
|
||||
this.model.fetch({
|
||||
success: function() {
|
||||
self.render();
|
||||
_.each(self.codeMirrors, function(mirror) {
|
||||
var ele = mirror.getTextArea();
|
||||
var field = self.selectorToField[ele.id];
|
||||
mirror.setValue(self.model.get(field));
|
||||
});
|
||||
self.licenseModel.setFromString(self.model.get('license'), {silent: true});
|
||||
self.licenseView.render();
|
||||
},
|
||||
reset: true,
|
||||
silent: true});
|
||||
},
|
||||
setAndValidate: function(attr, value) {
|
||||
// If we call model.set() with {validate: true}, model fields
|
||||
// will not be set if validation fails. This puts the UI and
|
||||
// the model in an inconsistent state, and causes us to not
|
||||
// see the right validation errors the next time validate() is
|
||||
// called on the model. So we set *without* validating, then
|
||||
// call validate ourselves.
|
||||
this.model.set(attr, value);
|
||||
this.model.isValid();
|
||||
},
|
||||
|
||||
if (showDatepicker) {
|
||||
console.info("Value is end_with_date");
|
||||
datepicker.prop('disabled', false);
|
||||
certificateAvailableDateField.removeClass("hidden");
|
||||
} else {
|
||||
console.info("Value is NOT end_with_date");
|
||||
datepicker.prop('disabled', true);
|
||||
datepicker.val(null);
|
||||
this.clearValidationErrors();
|
||||
this.setAndValidate("certificate_available_date", null)
|
||||
certificateAvailableDateField.addClass("hidden");
|
||||
}
|
||||
},
|
||||
revertView: function () {
|
||||
// Make sure that the CodeMirror instance has the correct
|
||||
// data from its corresponding textarea
|
||||
var self = this;
|
||||
this.model.fetch({
|
||||
success: function () {
|
||||
self.render();
|
||||
_.each(self.codeMirrors, function (mirror) {
|
||||
var ele = mirror.getTextArea();
|
||||
var field = self.selectorToField[ele.id];
|
||||
mirror.setValue(self.model.get(field));
|
||||
});
|
||||
self.licenseModel.setFromString(self.model.get('license'), { silent: true });
|
||||
self.licenseView.render();
|
||||
},
|
||||
reset: true,
|
||||
silent: true
|
||||
});
|
||||
},
|
||||
setAndValidate: function (attr, value) {
|
||||
// If we call model.set() with {validate: true}, model fields
|
||||
// will not be set if validation fails. This puts the UI and
|
||||
// the model in an inconsistent state, and causes us to not
|
||||
// see the right validation errors the next time validate() is
|
||||
// called on the model. So we set *without* validating, then
|
||||
// call validate ourselves.
|
||||
this.model.set(attr, value);
|
||||
this.model.isValid();
|
||||
},
|
||||
showNotificationBar: function() {
|
||||
// We always call showNotificationBar with the same args, just
|
||||
// delegate to superclass
|
||||
ValidatingView.prototype.showNotificationBar.call(this,
|
||||
this.save_message,
|
||||
_.bind(this.saveView, this),
|
||||
_.bind(this.revertView, this));
|
||||
},
|
||||
|
||||
showNotificationBar: function () {
|
||||
// We always call showNotificationBar with the same args, just
|
||||
// delegate to superclass
|
||||
ValidatingView.prototype.showNotificationBar.call(this,
|
||||
this.save_message,
|
||||
_.bind(this.saveView, this),
|
||||
_.bind(this.revertView, this));
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
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: title,
|
||||
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) {
|
||||
var options = {};
|
||||
options[image_key] = response.asset.display_name;
|
||||
options[image_path_key] = response.asset.url;
|
||||
self.model.set(options);
|
||||
self.render();
|
||||
$(selector).attr('src', self.model.get(image_path_key));
|
||||
}
|
||||
});
|
||||
modal.show();
|
||||
},
|
||||
|
||||
var upload = new FileUploadModel({
|
||||
title: title,
|
||||
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) {
|
||||
var options = {};
|
||||
options[image_key] = response.asset.display_name;
|
||||
options[image_path_key] = response.asset.url;
|
||||
self.model.set(options);
|
||||
self.render();
|
||||
$(selector).attr('src', self.model.get(image_path_key));
|
||||
}
|
||||
});
|
||||
modal.show();
|
||||
},
|
||||
handleLicenseChange: function() {
|
||||
this.showNotificationBar();
|
||||
this.model.set('license', this.licenseModel.toString());
|
||||
}
|
||||
});
|
||||
|
||||
handleLicenseChange: function () {
|
||||
this.showNotificationBar();
|
||||
this.model.set('license', this.licenseModel.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return DetailsView;
|
||||
}); // end define()
|
||||
return DetailsView;
|
||||
}); // end define()
|
||||
|
||||
@@ -157,29 +157,6 @@
|
||||
color: $red;
|
||||
}
|
||||
|
||||
.collapsible {
|
||||
.collapsible-trigger{
|
||||
color: theme-color("primary");
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsible-content{
|
||||
font-size: 12px;
|
||||
padding-top: ($baseline/2);
|
||||
.collapsible-description-heading {
|
||||
font-weight: bold;
|
||||
margin-top: $baseline;
|
||||
}
|
||||
|
||||
&.collapsed{
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buttons
|
||||
.remove-item {
|
||||
@include white-button;
|
||||
@@ -245,14 +222,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label,
|
||||
input,
|
||||
textarea {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
label {
|
||||
@@ -269,8 +242,7 @@
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
textarea {
|
||||
@extend %t-copy-base;
|
||||
|
||||
@include placeholder($gray-l4);
|
||||
@@ -314,10 +286,6 @@
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.datepicker-icon {
|
||||
margin-left: -($baseline * 1.5);
|
||||
}
|
||||
}
|
||||
|
||||
.field-group {
|
||||
@@ -510,7 +478,7 @@
|
||||
.field {
|
||||
@include float(left);
|
||||
|
||||
width: flex-grid(4, 9);
|
||||
width: flex-grid(3, 9);
|
||||
margin-bottom: ($baseline/4);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
@@ -223,7 +223,6 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
|
||||
<div class="field date" id="field-course-start-date">
|
||||
<label for="course-start-date">${_("Course Start Date")}</label>
|
||||
<input type="text" class="start-date date start datepicker" id="course-start-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="icon icon-inline fa fa-calendar-check-o datepicker-icon" aria-hidden="true"></span>
|
||||
<span class="tip tip-stacked">${_("First day the course begins")}</span>
|
||||
</div>
|
||||
|
||||
@@ -238,7 +237,6 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
|
||||
<div class="field date" id="field-course-end-date">
|
||||
<label for="course-end-date">${_("Course End Date")}</label>
|
||||
<input type="text" class="end-date date end" id="course-end-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="icon icon-inline fa fa-calendar-check-o datepicker-icon" aria-hidden="true"></span>
|
||||
<span class="tip tip-stacked">${_("Last day your course is active")}</span>
|
||||
</div>
|
||||
|
||||
@@ -253,45 +251,10 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
|
||||
% if can_show_certificate_available_date_field(context_course):
|
||||
<ol class="list-input">
|
||||
<li class="field-group field-group-certificate-available" id="certificate-available">
|
||||
<div class="field date" id="field-certificates-display-behavior">
|
||||
<label for="certificates-display-behavior">${_("Certificates Display Behavior")}</label>
|
||||
<select id="certificates-display-behavior">
|
||||
<option value="early_no_info">${_("Immediately upon passing")}</option>
|
||||
<option value="end">${_("End date of course")}</option>
|
||||
<option value="end_with_date">${_("A date after the course end date")}</option>
|
||||
</select>
|
||||
<span class="tip tip-stacked">${_("Certificates are awarded at the end of a course run")}</span>
|
||||
|
||||
<!-- Collapsible -->
|
||||
<div class="collapsible">
|
||||
<div id="certificate-display-behavior-collapsible-trigger" class="collapsible-trigger" role="button" tabindex="0" aria-expanded="false">
|
||||
<span>
|
||||
<span class="icon icon-inline fa fa-info-circle" aria-hidden="true"></span>
|
||||
Read more about this setting
|
||||
</span>
|
||||
</div>
|
||||
<div id="certificate-display-behavior-collapsible-content" class="collapsible-content collapsed">
|
||||
<p>In all configurations of this setting, certificates are generated for learners as soon as they achieve the passing threshold in the course (which can occur before a final assignment based on course design)</p>
|
||||
<div>
|
||||
<div class="collapsible-description-heading">Immediately upon passing</div>
|
||||
<div class="collapsible-description-description">Learners can access their certificate as soon as they achieve a passing grade above the course grade threshold. Note: learners can achieve a passing grade before encountering all assignments in some course configurations.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="collapsible-description-heading">On course end date</div>
|
||||
<div class="collapsible-description-description">Learners with passing grades can access their certificate once the end date of the course has elapsed.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="collapsible-description-heading">A date after the course end date</div>
|
||||
<div class="collapsible-description-description">Learners with passing grades can access their certificate after the date that you set has elapsed.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field date hidden" id="field-certificate-available-date" >
|
||||
<div class="field date" id="field-certificate-available-date">
|
||||
<label for="certificate-available-date">${_("Certificates Available Date")}</label>
|
||||
<input type="text" class="certificate-available-date date start datepicker" id="certificate-available-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="icon icon-inline fa fa-calendar-check-o datepicker-icon" aria-hidden="true"></span>
|
||||
<span class="tip tip-stacked">${_("By default, 48 hours after course end date")}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
@@ -302,7 +265,6 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
|
||||
<div class="field date" id="field-enrollment-start-date">
|
||||
<label for="course-enrollment-start-date">${_("Enrollment Start Date")}</label>
|
||||
<input type="text" class="start-date date start" id="course-enrollment-start-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="icon icon-inline fa fa-calendar-check-o datepicker-icon" aria-hidden="true"></span>
|
||||
<span class="tip tip-stacked">${_("First day students can enroll")}</span>
|
||||
</div>
|
||||
|
||||
@@ -320,7 +282,6 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
|
||||
<div class="field date ${enrollment_end_editable_class}" id="field-enrollment-end-date">
|
||||
<label for="course-enrollment-end-date">${_("Enrollment End Date")}</label>
|
||||
<input type="text" class="end-date date end" id="course-enrollment-end-date" placeholder="MM/DD/YYYY" autocomplete="off" ${enrollment_end_readonly} />
|
||||
<span class="icon icon-inline fa fa-calendar-check-o datepicker-icon" aria-hidden="true"></span>
|
||||
<span class="tip tip-stacked">
|
||||
${_("Last day students can enroll.")}
|
||||
% if not enrollment_end_editable:
|
||||
|
||||
Reference in New Issue
Block a user