feat: reimagine certificate display settings The course settings `certificate_available_date` (CAD) and `certificates_display_behavior` (CDB) were previously acting indedependantly of one another. They now work in tandem. This change: - limits CDB to a dropdown - removes "early_with_info" and adds "end_with_date" - only takes CAD into account if "end_with_date" is selected - Moves CDB to the main course schedule settings page - updates CourseOverview model and CourseDetails objects to validate these fields and choose sane defaults if they aren't expected values This work was previously done in bd9e7dd (complete with bugs), so this version is toggleable via the ENABLE_V2_CERT_DISPLAY_SETTINGS setting
168 lines
8.1 KiB
JavaScript
168 lines
8.1 KiB
JavaScript
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) {
|
|
'use strict';
|
|
var CourseDetails = Backbone.Model.extend({
|
|
defaults: {
|
|
org: '',
|
|
course_id: '',
|
|
run: '',
|
|
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,
|
|
syllabus: null,
|
|
title: '',
|
|
subtitle: '',
|
|
duration: '',
|
|
description: '',
|
|
short_description: '',
|
|
overview: '',
|
|
intro_video: null,
|
|
effort: null, // an int or null,
|
|
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',
|
|
learning_info: [],
|
|
instructor_info: {},
|
|
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.
|
|
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']
|
|
);
|
|
|
|
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.');
|
|
}
|
|
if (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.'
|
|
);
|
|
}
|
|
if (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.'
|
|
);
|
|
}
|
|
if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) {
|
|
errors.enrollment_end = gettext('The enrollment end date cannot be after the course end date.');
|
|
}
|
|
if (this.showCertificateAvailableDate && newattrs.end_date && newattrs.certificate_available_date &&
|
|
newattrs.certificate_available_date < newattrs.end_date) {
|
|
errors.certificate_available_date = gettext(
|
|
'The certificate available date must be later than the course end date.'
|
|
);
|
|
}
|
|
|
|
if (this.useV2CertDisplaySettings){
|
|
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
|
|
}
|
|
if (_.has(newattrs, 'entrance_exam_minimum_score_pct')) {
|
|
var range = {
|
|
min: 1,
|
|
max: 100
|
|
};
|
|
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.'
|
|
), range, true);
|
|
}
|
|
}
|
|
if (!_.isEmpty(errors)) return errors;
|
|
// 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
|
|
if (_.isEmpty(newsource) &&
|
|
!_.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});
|
|
}
|
|
|
|
return this.videosourceSample();
|
|
},
|
|
|
|
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() {
|
|
return new Date() <= new Date(this.get('start_date'));
|
|
}
|
|
});
|
|
|
|
return CourseDetails;
|
|
}); // end define()
|