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:
|
||||
|
||||
@@ -49,7 +49,6 @@ from lms.djangoapps.verify_student.utils import is_verification_expiring_soon, v
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.theming.helpers import get_themes
|
||||
from openedx.core.djangoapps.user_authn.utils import is_safe_login_or_logout_redirect
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
|
||||
# Enumeration of per-course verification statuses
|
||||
# we display on the student dashboard.
|
||||
@@ -512,17 +511,14 @@ def _cert_info(user, course_overview, cert_status):
|
||||
is_hidden_status = status in ('processing', 'generating', 'notpassing', 'auditing')
|
||||
|
||||
if (
|
||||
not certificates_viewable_for_course(course_overview)
|
||||
and CertificateStatuses.is_passing_status(status)
|
||||
and course_overview.certificates_display_behavior in (
|
||||
CertificatesDisplayBehaviors.END_WITH_DATE,
|
||||
CertificatesDisplayBehaviors.END
|
||||
)
|
||||
not certificates_viewable_for_course(course_overview) and
|
||||
CertificateStatuses.is_passing_status(status) and
|
||||
course_overview.certificate_available_date
|
||||
):
|
||||
status = certificate_earned_but_not_available_status
|
||||
|
||||
if (
|
||||
course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.EARLY_NO_INFO and
|
||||
course_overview.certificates_display_behavior == 'early_no_info' and
|
||||
is_hidden_status
|
||||
):
|
||||
return default_info
|
||||
|
||||
@@ -22,7 +22,6 @@ from lms.djangoapps.certificates.tests.factories import (
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
|
||||
# pylint: disable=no-member
|
||||
|
||||
@@ -41,7 +40,7 @@ class CertificateDisplayTestBase(SharedModuleStoreTestCase):
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.course = CourseFactory()
|
||||
cls.course.certificates_display_behavior = CertificatesDisplayBehaviors.EARLY_NO_INFO
|
||||
cls.course.certificates_display_behavior = "early_with_info"
|
||||
|
||||
with cls.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, cls.course.id):
|
||||
cls.store.update_item(cls.course, cls.USERNAME)
|
||||
@@ -117,54 +116,40 @@ class CertificateDashboardMessageDisplayTest(CertificateDisplayTestBase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.course.certificates_display_behavior = CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
cls.course.certificates_display_behavior = "end"
|
||||
cls.course.save()
|
||||
cls.store.update_item(cls.course, cls.USERNAME)
|
||||
|
||||
def _check_message(self, visible_date): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
def _check_message(self, certificate_available_date): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
test_message = 'Your grade and certificate will be ready after'
|
||||
|
||||
is_past = visible_date < datetime.datetime.now(UTC)
|
||||
|
||||
if is_past:
|
||||
if certificate_available_date is None:
|
||||
self.assertNotContains(response, test_message)
|
||||
self.assertNotContains(response, "View Test_Certificate")
|
||||
self._check_can_download_certificate()
|
||||
|
||||
else:
|
||||
elif datetime.datetime.now(UTC) < certificate_available_date:
|
||||
self.assertContains(response, test_message)
|
||||
self.assertNotContains(response, "View Test_Certificate")
|
||||
else:
|
||||
self._check_can_download_certificate()
|
||||
|
||||
@ddt.data(
|
||||
(CertificatesDisplayBehaviors.END, True),
|
||||
(CertificatesDisplayBehaviors.END, False),
|
||||
(CertificatesDisplayBehaviors.END_WITH_DATE, True),
|
||||
(CertificatesDisplayBehaviors.END_WITH_DATE, False)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_certificate_available_date(self, certificates_display_behavior, past_date):
|
||||
@ddt.data(True, False, None)
|
||||
def test_certificate_available_date(self, past_certificate_available_date):
|
||||
cert = self._create_certificate('verified')
|
||||
cert.status = CertificateStatuses.downloadable
|
||||
cert.save()
|
||||
|
||||
self.course.certificates_display_behavior = certificates_display_behavior
|
||||
|
||||
if certificates_display_behavior == CertificatesDisplayBehaviors.END:
|
||||
if past_date:
|
||||
self.course.end = PAST_DATE
|
||||
else:
|
||||
self.course.end = FUTURE_DATE
|
||||
if certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE:
|
||||
if past_date:
|
||||
self.course.certificate_available_date = PAST_DATE
|
||||
else:
|
||||
self.course.certificate_available_date = FUTURE_DATE
|
||||
if past_certificate_available_date is None:
|
||||
certificate_available_date = None
|
||||
elif past_certificate_available_date:
|
||||
certificate_available_date = PAST_DATE
|
||||
elif not past_certificate_available_date:
|
||||
certificate_available_date = FUTURE_DATE
|
||||
|
||||
self.course.certificate_available_date = certificate_available_date
|
||||
self.course.save()
|
||||
self.store.update_item(self.course, self.USERNAME)
|
||||
|
||||
self._check_message(PAST_DATE if past_date else FUTURE_DATE)
|
||||
self._check_message(certificate_available_date)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
|
||||
@@ -40,7 +40,6 @@ from openedx.core.djangoapps.content.course_overviews.tests.factories import Cou
|
||||
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context
|
||||
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
||||
from openedx.features.course_experience.tests.views.helpers import add_course_mode
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
@@ -228,7 +227,6 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
id=course_key,
|
||||
end_date=THREE_YEARS_AGO,
|
||||
certificate_available_date=TOMORROW,
|
||||
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE,
|
||||
lowest_passing_grade=0.3
|
||||
)
|
||||
CourseEnrollmentFactory(course_id=course.id, user=self.user)
|
||||
@@ -244,7 +242,6 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
id=course_key,
|
||||
end_date=TOMORROW,
|
||||
certificate_available_date=TOMORROW,
|
||||
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE,
|
||||
lowest_passing_grade=0.3
|
||||
)
|
||||
CourseEnrollmentFactory(course_id=course.id, user=self.user)
|
||||
@@ -260,7 +257,6 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
id=course_key,
|
||||
end_date=ONE_WEEK_AGO,
|
||||
certificate_available_date=now(),
|
||||
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE,
|
||||
lowest_passing_grade=0.3
|
||||
)
|
||||
CourseEnrollmentFactory(course_id=course.id, user=self.user)
|
||||
|
||||
@@ -49,8 +49,6 @@ from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreEnum, ModuleStoreTestCase, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -77,8 +75,7 @@ class CourseEndingTest(ModuleStoreTestCase):
|
||||
survey_url = "http://a_survey.com"
|
||||
course = CourseOverviewFactory.create(
|
||||
end_of_course_survey_url=survey_url,
|
||||
certificates_display_behavior=CertificatesDisplayBehaviors.END,
|
||||
end=datetime.now(pytz.UTC) - timedelta(days=2)
|
||||
certificates_display_behavior='end',
|
||||
)
|
||||
cert = GeneratedCertificateFactory.create(
|
||||
user=user,
|
||||
@@ -141,7 +138,7 @@ class CourseEndingTest(ModuleStoreTestCase):
|
||||
'can_unenroll': True}
|
||||
|
||||
# test when the display is unavailable or notpassing, we get the correct results out
|
||||
course2.certificates_display_behavior = CertificatesDisplayBehaviors.EARLY_NO_INFO
|
||||
course2.certificates_display_behavior = 'early_no_info'
|
||||
cert_status = {'status': 'unavailable', 'mode': 'honor', 'uuid': None}
|
||||
assert _cert_info(user, course2, cert_status) == {'status': 'processing', 'show_survey_button': False,
|
||||
'can_unenroll': True}
|
||||
@@ -176,8 +173,7 @@ class CourseEndingTest(ModuleStoreTestCase):
|
||||
survey_url = "http://a_survey.com"
|
||||
course = CourseOverviewFactory.create(
|
||||
end_of_course_survey_url=survey_url,
|
||||
certificates_display_behavior=CertificatesDisplayBehaviors.END,
|
||||
end=datetime.now(pytz.UTC) - timedelta(days=2),
|
||||
certificates_display_behavior='end',
|
||||
)
|
||||
|
||||
if cert_grade is not None:
|
||||
@@ -202,8 +198,7 @@ class CourseEndingTest(ModuleStoreTestCase):
|
||||
survey_url = "http://a_survey.com"
|
||||
course = CourseOverviewFactory.create(
|
||||
end_of_course_survey_url=survey_url,
|
||||
certificates_display_behavior=CertificatesDisplayBehaviors.END,
|
||||
end=datetime.now(pytz.UTC) - timedelta(days=2),
|
||||
certificates_display_behavior='end',
|
||||
)
|
||||
cert_status = {'status': 'generating', 'mode': 'honor', 'uuid': None}
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ from math import exp
|
||||
import dateutil.parser
|
||||
from pytz import utc
|
||||
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
|
||||
DEFAULT_START_DATE = datetime(2030, 1, 1, tzinfo=utc)
|
||||
|
||||
"""
|
||||
@@ -158,18 +156,15 @@ def may_certify_for_course(
|
||||
self_paced (bool): Whether the course is self-paced.
|
||||
"""
|
||||
show_early = (
|
||||
certificates_display_behavior == CertificatesDisplayBehaviors.EARLY_NO_INFO
|
||||
certificates_display_behavior in ('early_with_info', 'early_no_info')
|
||||
or certificates_show_before_end
|
||||
)
|
||||
past_available_date = (
|
||||
certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
and certificate_available_date
|
||||
certificate_available_date
|
||||
and certificate_available_date < datetime.now(utc)
|
||||
)
|
||||
ended_without_available_date = (
|
||||
certificates_display_behavior == CertificatesDisplayBehaviors.END
|
||||
and has_ended
|
||||
)
|
||||
ended_without_available_date = (certificate_available_date is None) and has_ended
|
||||
|
||||
return any((self_paced, show_early, past_available_date, ended_without_available_date))
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Django module container for classes and operations related to the "Course Module
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from io import BytesIO
|
||||
|
||||
import dateutil.parser
|
||||
@@ -22,7 +22,6 @@ from openedx.core.lib.license import LicenseMixin
|
||||
from openedx.core.lib.teams_config import TeamsConfig # lint-amnesty, pylint: disable=unused-import
|
||||
from xmodule import course_metadata_utils
|
||||
from xmodule.course_metadata_utils import DEFAULT_GRADING_POLICY, DEFAULT_START_DATE
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.graders import grader_from_conf
|
||||
from xmodule.seq_module import SequenceBlock
|
||||
from xmodule.tabs import CourseTabList, InvalidTabsException
|
||||
@@ -556,15 +555,15 @@ class CourseFields: # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
certificates_display_behavior = String(
|
||||
display_name=_("Certificates Display Behavior"),
|
||||
help=_(
|
||||
"Enter end, end_with_date, or early_no_info. After certificate generation, students who passed see a "
|
||||
"Enter end, early_with_info, or early_no_info. After certificate generation, students who passed see a "
|
||||
"link to their certificates on the dashboard and students who did not pass see information about the "
|
||||
"grading configuration. The default is end, which displays this certificate information to all students "
|
||||
"after the course end date. To display the certificate information to all students at a date after the "
|
||||
"course end date, use end_with_date and add a certificate_available_date. To display only the links to "
|
||||
"passing students as soon as certificates are generated, enter early_no_info."
|
||||
"after the course end date. To display this certificate information to all students as soon as "
|
||||
"certificates are generated, enter early_with_info. To display only the links to passing students as "
|
||||
"soon as certificates are generated, enter early_no_info."
|
||||
),
|
||||
scope=Scope.settings,
|
||||
default=CertificatesDisplayBehaviors.END,
|
||||
default="end"
|
||||
)
|
||||
course_image = String(
|
||||
display_name=_("Course About Page Image"),
|
||||
@@ -1062,6 +1061,8 @@ class CourseBlock(
|
||||
except InvalidTabsException as err:
|
||||
raise type(err)(f'{str(err)} For course: {str(self.id)}') # lint-amnesty, pylint: disable=line-too-long
|
||||
|
||||
self.set_default_certificate_available_date()
|
||||
|
||||
def set_grading_policy(self, course_policy):
|
||||
"""
|
||||
The JSON object can have the keys GRADER and GRADE_CUTOFFS. If either is
|
||||
@@ -1087,6 +1088,10 @@ class CourseBlock(
|
||||
self.raw_grader = grading_policy['GRADER'] # used for cms access
|
||||
self.grade_cutoffs = grading_policy['GRADE_CUTOFFS']
|
||||
|
||||
def set_default_certificate_available_date(self):
|
||||
if (not self.certificate_available_date) and self.end:
|
||||
self.certificate_available_date = self.end + timedelta(days=2)
|
||||
|
||||
@classmethod
|
||||
def read_grading_policy(cls, paths, system):
|
||||
"""Load a grading policy from the specified paths, in order, if it exists."""
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""
|
||||
Public data structures for this app.
|
||||
|
||||
See OEP-49 for details
|
||||
"""
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class CertificatesDisplayBehaviors(str, Enum):
|
||||
"""
|
||||
Options for the certificates_display_behavior field of a course
|
||||
|
||||
end: Certificates are available at the end of the course
|
||||
end_with_date: Certificates are available after the certificate_available_date (post course end)
|
||||
early_no_info: Certificates are available immediately after earning them.
|
||||
|
||||
Only in affect for instructor based courses.
|
||||
"""
|
||||
END = "end"
|
||||
END_WITH_DATE = "end_with_date"
|
||||
EARLY_NO_INFO = "early_no_info"
|
||||
|
||||
@classmethod
|
||||
def includes_value(cls, value):
|
||||
return value in set(item.value for item in cls)
|
||||
@@ -23,7 +23,6 @@ from xmodule.course_metadata_utils import (
|
||||
may_certify_for_course,
|
||||
number_for_course_location
|
||||
)
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.modulestore.tests.utils import (
|
||||
MixedModulestoreBuilder,
|
||||
MongoModulestoreBuilder,
|
||||
@@ -164,28 +163,16 @@ class CourseMetadataUtilsTestCase(TestCase):
|
||||
TestScenario((DEFAULT_START_DATE, None), True),
|
||||
]),
|
||||
FunctionTest(may_certify_for_course, [
|
||||
# Test certificates_show_before_end
|
||||
TestScenario((CertificatesDisplayBehaviors.EARLY_NO_INFO, True, False, test_datetime, False), True),
|
||||
TestScenario((CertificatesDisplayBehaviors.END, True, False, test_datetime, False), True),
|
||||
TestScenario((CertificatesDisplayBehaviors.END_WITH_DATE, True, False, _NEXT_WEEK, False), True),
|
||||
|
||||
# Test that EARLY_NO_INFO
|
||||
TestScenario((CertificatesDisplayBehaviors.EARLY_NO_INFO, True, True, test_datetime, False), True),
|
||||
TestScenario((CertificatesDisplayBehaviors.EARLY_NO_INFO, False, False, test_datetime, False), True),
|
||||
|
||||
# Test END_WITH_DATE
|
||||
TestScenario((CertificatesDisplayBehaviors.END_WITH_DATE, False, False, test_datetime, False), True),
|
||||
TestScenario((CertificatesDisplayBehaviors.END_WITH_DATE, False, False, _LAST_WEEK, False), True),
|
||||
TestScenario((CertificatesDisplayBehaviors.END_WITH_DATE, False, False, _NEXT_WEEK, False), False),
|
||||
TestScenario((CertificatesDisplayBehaviors.END_WITH_DATE, False, False, None, False), False),
|
||||
|
||||
# Test END
|
||||
TestScenario((CertificatesDisplayBehaviors.END, False, False, test_datetime, False), False),
|
||||
TestScenario((CertificatesDisplayBehaviors.END, False, True, test_datetime, False), True),
|
||||
|
||||
# Test self_paced
|
||||
TestScenario((CertificatesDisplayBehaviors.END, False, False, test_datetime, False), False),
|
||||
TestScenario((CertificatesDisplayBehaviors.END, False, False, test_datetime, True), True),
|
||||
TestScenario(('early_with_info', True, True, test_datetime, False), True),
|
||||
TestScenario(('early_no_info', False, False, test_datetime, False), True),
|
||||
TestScenario(('end', True, False, test_datetime, False), True),
|
||||
TestScenario(('end', False, True, test_datetime, False), True),
|
||||
TestScenario(('end', False, False, _NEXT_WEEK, False), False),
|
||||
TestScenario(('end', False, False, _LAST_WEEK, False), True),
|
||||
TestScenario(('end', False, False, None, False), False),
|
||||
TestScenario(('early_with_info', False, False, None, False), True),
|
||||
TestScenario(('end', False, False, _NEXT_WEEK, False), False),
|
||||
TestScenario(('end', False, False, _NEXT_WEEK, True), True),
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ from xblock.runtime import DictKeyValueStore, KvsFieldData
|
||||
|
||||
from openedx.core.lib.teams_config import TeamsConfig, DEFAULT_COURSE_RUN_MAX_TEAM_SIZE
|
||||
import xmodule.course_module
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
|
||||
from xmodule.modulestore.exceptions import InvalidProctoringProvider
|
||||
|
||||
@@ -106,41 +105,36 @@ class HasEndedMayCertifyTestCase(unittest.TestCase):
|
||||
super().setUp()
|
||||
|
||||
system = DummySystem(load_error_modules=True) # lint-amnesty, pylint: disable=unused-variable
|
||||
|
||||
#sample_xml = """
|
||||
# <course org="{org}" course="{course}" display_organization="{org}_display" display_coursenumber="{course}_display" # lint-amnesty, pylint: disable=line-too-long
|
||||
# graceperiod="1 day" url_name="test"
|
||||
# start="2012-01-01T12:00"
|
||||
# {end}
|
||||
# certificates_show_before_end={cert}>
|
||||
# <chapter url="hi" url_name="ch" display_name="CH">
|
||||
# <html url_name="h" display_name="H">Two houses, ...</html>
|
||||
# </chapter>
|
||||
# </course>
|
||||
#""".format(org=ORG, course=COURSE)
|
||||
past_end = (datetime.now() - timedelta(days=12)).strftime("%Y-%m-%dT%H:%M:00")
|
||||
future_end = (datetime.now() + timedelta(days=12)).strftime("%Y-%m-%dT%H:%M:00")
|
||||
self.past_show_certs = get_dummy_course(
|
||||
"2012-01-01T12:00",
|
||||
end=past_end,
|
||||
certs=CertificatesDisplayBehaviors.EARLY_NO_INFO
|
||||
)
|
||||
self.past_show_certs_no_info = get_dummy_course(
|
||||
"2012-01-01T12:00",
|
||||
end=past_end,
|
||||
certs=CertificatesDisplayBehaviors.EARLY_NO_INFO
|
||||
)
|
||||
self.past_noshow_certs = get_dummy_course(
|
||||
"2012-01-01T12:00",
|
||||
end=past_end,
|
||||
certs=CertificatesDisplayBehaviors.END
|
||||
)
|
||||
|
||||
self.future_show_certs_no_info = get_dummy_course(
|
||||
"2012-01-01T12:00",
|
||||
end=future_end,
|
||||
certs=CertificatesDisplayBehaviors.EARLY_NO_INFO
|
||||
)
|
||||
self.future_noshow_certs = get_dummy_course(
|
||||
"2012-01-01T12:00",
|
||||
end=future_end,
|
||||
certs=CertificatesDisplayBehaviors.END
|
||||
)
|
||||
self.past_show_certs = get_dummy_course("2012-01-01T12:00", end=past_end, certs='early_with_info')
|
||||
self.past_show_certs_no_info = get_dummy_course("2012-01-01T12:00", end=past_end, certs='early_no_info')
|
||||
self.past_noshow_certs = get_dummy_course("2012-01-01T12:00", end=past_end, certs='end')
|
||||
self.future_show_certs = get_dummy_course("2012-01-01T12:00", end=future_end, certs='early_with_info')
|
||||
self.future_show_certs_no_info = get_dummy_course("2012-01-01T12:00", end=future_end, certs='early_no_info')
|
||||
self.future_noshow_certs = get_dummy_course("2012-01-01T12:00", end=future_end, certs='end')
|
||||
#self.past_show_certs = system.process_xml(sample_xml.format(end=past_end, cert=True))
|
||||
#self.past_noshow_certs = system.process_xml(sample_xml.format(end=past_end, cert=False))
|
||||
#self.future_show_certs = system.process_xml(sample_xml.format(end=future_end, cert=True))
|
||||
#self.future_noshow_certs = system.process_xml(sample_xml.format(end=future_end, cert=False))
|
||||
|
||||
def test_has_ended(self):
|
||||
"""Check that has_ended correctly tells us when a course is over."""
|
||||
assert self.past_show_certs.has_ended()
|
||||
assert self.past_show_certs_no_info.has_ended()
|
||||
assert self.past_noshow_certs.has_ended()
|
||||
assert not self.future_show_certs.has_ended()
|
||||
assert not self.future_show_certs_no_info.has_ended()
|
||||
assert not self.future_noshow_certs.has_ended()
|
||||
|
||||
@@ -149,6 +143,7 @@ class HasEndedMayCertifyTestCase(unittest.TestCase):
|
||||
assert self.past_show_certs.may_certify()
|
||||
assert self.past_noshow_certs.may_certify()
|
||||
assert self.past_show_certs_no_info.may_certify()
|
||||
assert self.future_show_certs.may_certify()
|
||||
assert self.future_show_certs_no_info.may_certify()
|
||||
assert not self.future_noshow_certs.may_certify()
|
||||
|
||||
@@ -416,6 +411,14 @@ class CourseBlockTestCase(unittest.TestCase):
|
||||
"""
|
||||
assert self.course.number == COURSE
|
||||
|
||||
def test_set_default_certificate_available_date(self):
|
||||
"""
|
||||
The certificate_available_date field should default to two days
|
||||
after the course end date.
|
||||
"""
|
||||
expected_certificate_available_date = self.course.end + timedelta(days=2)
|
||||
assert expected_certificate_available_date == self.course.certificate_available_date
|
||||
|
||||
|
||||
class ProctoringProviderTestCase(unittest.TestCase):
|
||||
"""
|
||||
|
||||
@@ -47,7 +47,6 @@ from lms.djangoapps.certificates.utils import (
|
||||
has_html_certificates_enabled as _has_html_certificates_enabled
|
||||
)
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
|
||||
log = logging.getLogger("edx.certificate")
|
||||
User = get_user_model()
|
||||
@@ -268,7 +267,6 @@ def certificate_downloadable_status(student, course_key):
|
||||
if (
|
||||
not certificates_viewable_for_course(course_overview) and
|
||||
CertificateStatuses.is_passing_status(current_status['status']) and
|
||||
course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE and
|
||||
course_overview.certificate_available_date
|
||||
):
|
||||
response_data['earned_but_not_available'] = True
|
||||
@@ -596,18 +594,17 @@ def certificates_viewable_for_course(course):
|
||||
if course.self_paced:
|
||||
return True
|
||||
if (
|
||||
course.certificates_display_behavior == CertificatesDisplayBehaviors.EARLY_NO_INFO
|
||||
course.certificates_display_behavior in ('early_with_info', 'early_no_info')
|
||||
or course.certificates_show_before_end
|
||||
):
|
||||
return True
|
||||
if (
|
||||
course.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
and course.certificate_available_date
|
||||
course.certificate_available_date
|
||||
and course.certificate_available_date <= datetime.now(UTC)
|
||||
):
|
||||
return True
|
||||
if (
|
||||
course.certificates_display_behavior == CertificatesDisplayBehaviors.END
|
||||
course.certificate_available_date is None
|
||||
and course.has_ended()
|
||||
):
|
||||
return True
|
||||
|
||||
@@ -19,7 +19,6 @@ from freezegun import freeze_time
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from testfixtures import LogCapture
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
@@ -225,31 +224,19 @@ class CertificateDownloadableStatusTests(WebCertificateTestMixin, ModuleStoreTes
|
||||
'uuid': cert_status['uuid']}
|
||||
|
||||
@ddt.data(
|
||||
(True, timedelta(days=2), CertificatesDisplayBehaviors.END_WITH_DATE, True, None),
|
||||
(False, -timedelta(days=2), CertificatesDisplayBehaviors.EARLY_NO_INFO, True, None),
|
||||
(False, timedelta(days=2), CertificatesDisplayBehaviors.EARLY_NO_INFO, True, None),
|
||||
(False, -timedelta(days=2), CertificatesDisplayBehaviors.END, True, None),
|
||||
(False, timedelta(days=2), CertificatesDisplayBehaviors.END, True, None),
|
||||
(False, -timedelta(days=2), CertificatesDisplayBehaviors.END_WITH_DATE, True, None),
|
||||
(False, timedelta(days=2), CertificatesDisplayBehaviors.END_WITH_DATE, False, True),
|
||||
(False, timedelta(days=2), False, True),
|
||||
(False, -timedelta(days=2), True, None),
|
||||
(True, timedelta(days=2), True, None)
|
||||
)
|
||||
@ddt.unpack
|
||||
@patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True})
|
||||
def test_cert_api_return(
|
||||
self,
|
||||
self_paced,
|
||||
cert_avail_delta,
|
||||
certificates_display_behavior,
|
||||
cert_downloadable_status,
|
||||
earned_but_not_available
|
||||
):
|
||||
def test_cert_api_return(self, self_paced, cert_avail_delta, cert_downloadable_status, earned_but_not_available):
|
||||
"""
|
||||
Test 'downloadable status'
|
||||
"""
|
||||
cert_avail_date = datetime.now(pytz.UTC) + cert_avail_delta
|
||||
self.course.self_paced = self_paced
|
||||
self.course.certificate_available_date = cert_avail_date
|
||||
self.course.certificates_display_behavior = certificates_display_behavior
|
||||
self.course.save()
|
||||
|
||||
self._setup_course_certificate()
|
||||
|
||||
@@ -24,7 +24,6 @@ from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFact
|
||||
from lms.djangoapps.certificates.utils import get_certificate_url
|
||||
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
@@ -212,8 +211,7 @@ class CertificatesViewsSiteTests(ModuleStoreTestCase):
|
||||
org='testorg',
|
||||
number='run1',
|
||||
display_name='refundable course',
|
||||
certificate_available_date=datetime.datetime.today() - datetime.timedelta(days=1),
|
||||
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
certificate_available_date=datetime.datetime.today() - datetime.timedelta(days=1)
|
||||
)
|
||||
self.course.cert_html_view_enabled = True
|
||||
self.course.save()
|
||||
|
||||
@@ -50,7 +50,6 @@ from openedx.core.djangoapps.site_configuration.tests.test_util import (
|
||||
from openedx.core.djangolib.js_utils import js_escaped_string
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
from openedx.core.lib.tests.assertions.events import assert_event_matches
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
@@ -82,7 +81,6 @@ class CommonCertificatesTestCase(ModuleStoreTestCase):
|
||||
number='run1',
|
||||
display_name='refundable course',
|
||||
certificate_available_date=datetime.datetime.today() - datetime.timedelta(days=1),
|
||||
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
)
|
||||
self.course_id = self.course.location.course_key
|
||||
self.user = UserFactory.create(
|
||||
|
||||
@@ -50,7 +50,6 @@ from openedx.core.djangoapps.lang_pref.api import get_closest_released_language
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.lib.courses import course_image_url
|
||||
from openedx.core.lib.courses import get_course_by_id
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
_ = translation.ugettext
|
||||
@@ -333,14 +332,8 @@ def _get_user_certificate(request, user, course_key, course, preview_mode=None):
|
||||
if preview_mode:
|
||||
# certificate is being previewed from studio
|
||||
if request.user.has_perm(PREVIEW_CERTIFICATES, course):
|
||||
if (
|
||||
course.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
and course.certificate_available_date
|
||||
and not course.self_paced
|
||||
):
|
||||
if course.certificate_available_date and not course.self_paced:
|
||||
modified_date = course.certificate_available_date
|
||||
elif course.certificates_display_behavior == CertificatesDisplayBehaviors.END:
|
||||
modified_date = course.end
|
||||
else:
|
||||
modified_date = datetime.now().date()
|
||||
user_certificate = GeneratedCertificate(
|
||||
|
||||
@@ -99,7 +99,6 @@ from common.djangoapps.util.tests.test_date_utils import fake_pgettext, fake_uge
|
||||
from common.djangoapps.util.url import reload_django_url_config
|
||||
from common.djangoapps.util.views import ensure_valid_course_key
|
||||
from xmodule.course_module import COURSE_VISIBILITY_PRIVATE, COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.graders import ShowCorrectness
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -1289,7 +1288,6 @@ class ProgressPageBaseTests(ModuleStoreTestCase):
|
||||
grade_cutoffs={'çü†øƒƒ': 0.75, 'Pass': 0.5},
|
||||
end=datetime.now(),
|
||||
certificate_available_date=datetime.now(UTC),
|
||||
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE,
|
||||
**options
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.utils.translation import ugettext as _
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from lms.djangoapps.certificates.data import CertificateStatuses
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
%>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
@@ -37,12 +36,7 @@ else:
|
||||
<div class="message message-status ${status_css_class} is-shown">
|
||||
<p class="message-copy">
|
||||
<%
|
||||
if course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE:
|
||||
certificate_available_date_string = course_overview.certificate_available_date.strftime('%Y-%m-%dT%H:%M:%S%z')
|
||||
elif course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END:
|
||||
certificate_available_date_string = course_overview.end.strftime('%Y-%m-%dT%H:%M:%S%z')
|
||||
else:
|
||||
raise Exception(course_overview.certificate_available_date, course_overview.certificates_display_behavior)
|
||||
certificate_available_date_string = course_overview.certificate_available_date.strftime('%Y-%m-%dT%H:%M:%S%z')
|
||||
container_string = _("Your grade and certificate will be ready after {date}.")
|
||||
format = 'shortDate'
|
||||
%>
|
||||
|
||||
@@ -11,7 +11,6 @@ from lms.djangoapps.certificates import api as certs_api
|
||||
from lms.djangoapps.certificates.data import CertificateStatuses
|
||||
from openedx.core.djangoapps.certificates.config import waffle
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -57,10 +56,7 @@ def can_show_certificate_available_date_field(course):
|
||||
|
||||
|
||||
def _course_uses_available_date(course):
|
||||
return (
|
||||
can_show_certificate_available_date_field(course)
|
||||
and course.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
)
|
||||
return can_show_certificate_available_date_field(course) and course.certificate_available_date
|
||||
|
||||
|
||||
def available_date_for_certificate(course, certificate, certificate_available_date=None):
|
||||
|
||||
@@ -11,11 +11,10 @@ from edx_toggles.toggles import LegacyWaffleSwitch
|
||||
from edx_toggles.toggles.testutils import override_waffle_switch
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from openedx.core.djangoapps.certificates import api
|
||||
from openedx.core.djangoapps.certificates.config import waffle as certs_waffle
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
|
||||
|
||||
# TODO: Copied from lms.djangoapps.certificates.models,
|
||||
@@ -168,7 +167,6 @@ class CertificatesApiTestCase(TestCase):
|
||||
|
||||
# With an available date set in the past, both return the available date (if configured)
|
||||
self.course.certificate_available_date = datetime(2017, 2, 1, tzinfo=pytz.UTC)
|
||||
self.course.certificates_display_behavior = CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
maybe_avail = self.course.certificate_available_date if uses_avail_date else self.certificate.modified_date
|
||||
assert maybe_avail == api.available_date_for_certificate(self.course, self.certificate)
|
||||
assert maybe_avail == api.display_date_for_certificate(self.course, self.certificate)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 2.2.24 on 2021-06-15 17:06
|
||||
|
||||
from django.db import migrations, models
|
||||
import xmodule.data
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('course_overviews', '0025_auto_20210702_1602'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='courseoverview',
|
||||
name='certificates_display_behavior',
|
||||
field=models.TextField(choices=[('end', 'END'), ('end_with_date', 'END_WITH_DATE'), ('early_no_info', 'EARLY_NO_INFO')], default=xmodule.data.CertificatesDisplayBehaviors('end'), null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalcourseoverview',
|
||||
name='certificates_display_behavior',
|
||||
field=models.TextField(choices=[('end', 'END'), ('end_with_date', 'END_WITH_DATE'), ('early_no_info', 'EARLY_NO_INFO')], default=xmodule.data.CertificatesDisplayBehaviors('end'), null=True),
|
||||
),
|
||||
]
|
||||
@@ -32,11 +32,11 @@ from openedx.core.lib.cache_utils import request_cached, RequestCache
|
||||
from common.djangoapps.static_replace.models import AssetBaseUrlConfig
|
||||
from xmodule import block_metadata_utils, course_metadata_utils
|
||||
from xmodule.course_module import DEFAULT_START_DATE, CourseBlock
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.error_module import ErrorBlock
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.tabs import CourseTab
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -56,10 +56,6 @@ class CourseOverview(TimeStampedModel):
|
||||
course catalog (courses to enroll in)
|
||||
course about (meta data about the course)
|
||||
|
||||
When you bump the VERSION you will invalidate all existing course overviews. This
|
||||
will cause a slew of modulestore reads as each course needs to be re-cached into
|
||||
the course overview.
|
||||
|
||||
.. no_pii:
|
||||
"""
|
||||
|
||||
@@ -67,7 +63,7 @@ class CourseOverview(TimeStampedModel):
|
||||
app_label = 'course_overviews'
|
||||
|
||||
# IMPORTANT: Bump this whenever you modify this model and/or add a migration.
|
||||
VERSION = 14
|
||||
VERSION = 13
|
||||
|
||||
# Cache entry versioning.
|
||||
version = IntegerField()
|
||||
@@ -100,11 +96,7 @@ class CourseOverview(TimeStampedModel):
|
||||
end_of_course_survey_url = TextField(null=True)
|
||||
|
||||
# Certification data
|
||||
certificates_display_behavior = TextField(
|
||||
null=True,
|
||||
choices=[(choice.value, choice.name) for choice in CertificatesDisplayBehaviors],
|
||||
default=CertificatesDisplayBehaviors.END
|
||||
)
|
||||
certificates_display_behavior = TextField(null=True)
|
||||
certificates_show_before_end = BooleanField(default=False)
|
||||
cert_html_view_enabled = BooleanField(default=False)
|
||||
has_any_active_web_certificate = BooleanField(default=False)
|
||||
@@ -219,15 +211,13 @@ class CourseOverview(TimeStampedModel):
|
||||
course_overview.course_image_url = course_image_url(course)
|
||||
course_overview.social_sharing_url = course.social_sharing_url
|
||||
|
||||
updated_display_behavior, updated_available_date = cls.validate_certificate_settings(course)
|
||||
|
||||
course_overview.certificates_display_behavior = updated_display_behavior
|
||||
course_overview.certificate_available_date = updated_available_date
|
||||
course_overview.certificates_display_behavior = course.certificates_display_behavior
|
||||
course_overview.certificates_show_before_end = course.certificates_show_before_end
|
||||
course_overview.cert_html_view_enabled = course.cert_html_view_enabled
|
||||
course_overview.has_any_active_web_certificate = (get_active_web_certificate(course) is not None)
|
||||
course_overview.cert_name_short = course.cert_name_short
|
||||
course_overview.cert_name_long = course.cert_name_long
|
||||
course_overview.certificate_available_date = course.certificate_available_date
|
||||
course_overview.lowest_passing_grade = lowest_passing_grade
|
||||
course_overview.end_of_course_survey_url = course.end_of_course_survey_url
|
||||
|
||||
@@ -906,35 +896,6 @@ class CourseOverview(TimeStampedModel):
|
||||
"""
|
||||
return self._original_course.edxnotes_visibility
|
||||
|
||||
@staticmethod
|
||||
def validate_certificate_settings(course):
|
||||
"""
|
||||
Take a course and returns validated certificate display settings
|
||||
|
||||
Arguments:
|
||||
course (CourseBlock): any course descriptor object
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: updated certificates_display_behavior, updated certificate_available_date
|
||||
None
|
||||
"""
|
||||
# Backwards compatibility for existing courses that set availability date, didn't set behavior,
|
||||
# and expect availability date to be used
|
||||
certificates_display_behavior = course.certificates_display_behavior
|
||||
certificate_available_date = course.certificate_available_date
|
||||
|
||||
if certificates_display_behavior == "" and certificate_available_date:
|
||||
certificates_display_behavior = CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
|
||||
if not CertificatesDisplayBehaviors.includes_value(certificates_display_behavior):
|
||||
certificates_display_behavior = CertificatesDisplayBehaviors.END
|
||||
|
||||
# Null the date if it's not going to be used
|
||||
if certificates_display_behavior != CertificatesDisplayBehaviors.END_WITH_DATE:
|
||||
certificate_available_date = None
|
||||
|
||||
return (certificates_display_behavior, certificate_available_date)
|
||||
|
||||
def __str__(self):
|
||||
"""Represent ourselves with the course key."""
|
||||
return str(self.id)
|
||||
|
||||
@@ -2,21 +2,16 @@
|
||||
|
||||
import datetime
|
||||
from unittest.mock import patch
|
||||
from collections import namedtuple
|
||||
|
||||
import pytest
|
||||
import ddt
|
||||
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
|
||||
|
||||
from ..models import CourseOverview
|
||||
|
||||
# represents a change of a course overview field. Used to avoid confusing indicies
|
||||
Change = namedtuple("Change", ["field_name", "initial_value", "changed_value"])
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CourseOverviewSignalsTestCase(ModuleStoreTestCase):
|
||||
@@ -76,12 +71,8 @@ class CourseOverviewSignalsTestCase(ModuleStoreTestCase):
|
||||
self.store.delete_course(course.id, ModuleStoreEnum.UserID.test)
|
||||
CourseOverview.get_from_id(course.id)
|
||||
|
||||
def assert_changed_signal_sent(self, changes, mock_signal): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
|
||||
course = CourseFactory.create(
|
||||
emit_signals=True,
|
||||
**{change.field_name: change.initial_value for change in changes}
|
||||
)
|
||||
def assert_changed_signal_sent(self, field_name, initial_value, changed_value, mock_signal): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
course = CourseFactory.create(emit_signals=True, **{field_name: initial_value})
|
||||
|
||||
# changing display name doesn't fire the signal
|
||||
course.display_name = course.display_name + 'changed'
|
||||
@@ -89,27 +80,18 @@ class CourseOverviewSignalsTestCase(ModuleStoreTestCase):
|
||||
assert not mock_signal.called
|
||||
|
||||
# changing the given field fires the signal
|
||||
for change in changes:
|
||||
setattr(course, change.field_name, change.changed_value)
|
||||
setattr(course, field_name, changed_value)
|
||||
self.store.update_item(course, ModuleStoreEnum.UserID.test)
|
||||
assert mock_signal.called
|
||||
|
||||
@patch('openedx.core.djangoapps.content.course_overviews.signals.COURSE_START_DATE_CHANGED.send')
|
||||
def test_start_changed(self, mock_signal):
|
||||
self.assert_changed_signal_sent([Change('start', self.TODAY, self.NEXT_WEEK)], mock_signal)
|
||||
self.assert_changed_signal_sent('start', self.TODAY, self.NEXT_WEEK, mock_signal)
|
||||
|
||||
@patch('openedx.core.djangoapps.content.course_overviews.signals.COURSE_PACING_CHANGED.send')
|
||||
def test_pacing_changed(self, mock_signal):
|
||||
self.assert_changed_signal_sent([Change('self_paced', True, False)], mock_signal)
|
||||
self.assert_changed_signal_sent('self_paced', True, False, mock_signal)
|
||||
|
||||
@patch('openedx.core.djangoapps.content.course_overviews.signals.COURSE_CERT_DATE_CHANGE.send_robust')
|
||||
def test_cert_date_changed(self, mock_signal):
|
||||
changes = [
|
||||
Change("certificate_available_date", self.TODAY, self.NEXT_WEEK),
|
||||
Change(
|
||||
"certificates_display_behavior",
|
||||
CertificatesDisplayBehaviors.END,
|
||||
CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
)
|
||||
]
|
||||
self.assert_changed_signal_sent(changes, mock_signal)
|
||||
self.assert_changed_signal_sent('certificate_available_date', self.TODAY, self.NEXT_WEEK, mock_signal)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Tests for courseware API
|
||||
"""
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlencode
|
||||
from typing import Optional
|
||||
|
||||
@@ -39,7 +39,6 @@ from common.djangoapps.student.roles import CourseInstructorRole
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentCelebrationFactory, UserFactory
|
||||
from openedx.core.djangoapps.agreements.api import create_integrity_signature
|
||||
from openedx.core.djangoapps.agreements.toggles import ENABLE_INTEGRITY_SIGNATURE
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import ItemFactory, ToyCourseFactory
|
||||
@@ -47,8 +46,6 @@ from xmodule.modulestore.tests.factories import ItemFactory, ToyCourseFactory
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
_NEXT_WEEK = datetime.now() + timedelta(days=7)
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class BaseCoursewareTests(SharedModuleStoreTestCase):
|
||||
@@ -67,8 +64,6 @@ class BaseCoursewareTests(SharedModuleStoreTestCase):
|
||||
enrollment_end=datetime(2028, 1, 1, 1, 1, 1),
|
||||
emit_signals=True,
|
||||
modulestore=cls.store,
|
||||
certificate_available_date=_NEXT_WEEK,
|
||||
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
)
|
||||
cls.chapter = ItemFactory(parent=cls.course, category='chapter')
|
||||
cls.sequence = ItemFactory(parent=cls.chapter, category='sequential', display_name='sequence')
|
||||
@@ -152,7 +147,6 @@ class CourseApiTestViews(BaseCoursewareTests, MasqueradeMixin):
|
||||
CourseEnrollment.enroll(self.user, self.course.id, enrollment_mode)
|
||||
|
||||
response = self.client.get(self.url)
|
||||
|
||||
assert response.status_code == 200
|
||||
if enrollment_mode:
|
||||
enrollment = response.data['enrollment']
|
||||
|
||||
@@ -48,8 +48,6 @@ class CourseDetails:
|
||||
self.end_date = None # 'end'
|
||||
self.enrollment_start = None
|
||||
self.enrollment_end = None
|
||||
self.certificate_available_date = None
|
||||
self.certificates_display_behavior = None
|
||||
self.syllabus = None # a pdf file asset
|
||||
self.title = ""
|
||||
self.subtitle = ""
|
||||
@@ -110,8 +108,7 @@ class CourseDetails:
|
||||
course_details = cls(course_key.org, course_key.course, course_key.run)
|
||||
course_details.start_date = course_descriptor.start
|
||||
course_details.end_date = course_descriptor.end
|
||||
course_details.certificate_available_date = course_descriptor.certificate_available_date
|
||||
course_details.certificates_display_behavior = course_descriptor.certificates_display_behavior
|
||||
course_details.certificate_available_date = course_descriptor.certificate_available_date # lint-amnesty, pylint: disable=attribute-defined-outside-init
|
||||
course_details.enrollment_start = course_descriptor.enrollment_start
|
||||
course_details.enrollment_end = course_descriptor.enrollment_end
|
||||
course_details.pre_requisite_courses = course_descriptor.pre_requisite_courses
|
||||
@@ -247,13 +244,6 @@ class CourseDetails:
|
||||
dirty = True
|
||||
descriptor.certificate_available_date = converted
|
||||
|
||||
if (
|
||||
'certificates_display_behavior' in jsondict
|
||||
and jsondict['certificates_display_behavior'] != descriptor.certificates_display_behavior
|
||||
):
|
||||
descriptor.certificates_display_behavior = jsondict['certificates_display_behavior']
|
||||
dirty = True
|
||||
|
||||
if 'course_image_name' in jsondict and jsondict['course_image_name'] != descriptor.course_image:
|
||||
descriptor.course_image = jsondict['course_image_name']
|
||||
dirty = True
|
||||
|
||||
@@ -30,7 +30,6 @@ from openedx.core.djangoapps.oauth_dispatch.tests.factories import ApplicationFa
|
||||
from openedx.core.djangoapps.programs import tasks
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -521,7 +520,6 @@ class AwardCourseCertificatesTestCase(CredentialsApiConfigMixin, TestCase):
|
||||
self.course = CourseOverviewFactory.create(
|
||||
self_paced=True, # Any option to allow the certificate to be viewable for the course
|
||||
certificate_available_date=self.available_date,
|
||||
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
)
|
||||
self.student = UserFactory.create(username='test-student')
|
||||
# Instantiate the Certificate first so that the config doesn't execute issuance
|
||||
|
||||
@@ -51,7 +51,6 @@ from openedx.core.djangoapps.site_configuration.tests.factories import SiteFacto
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from common.djangoapps.student.tests.factories import AnonymousUserFactory, CourseEnrollmentFactory, UserFactory
|
||||
from common.djangoapps.util.date_utils import strftime_localized
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase, SharedModuleStoreTestCase, TEST_DATA_SPLIT_MODULESTORE
|
||||
@@ -500,7 +499,7 @@ class TestProgramProgressMeter(ModuleStoreTestCase):
|
||||
end=two_days_ago,
|
||||
self_paced=False,
|
||||
certificate_available_date=tomorrow,
|
||||
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
certificates_display_behavior='end'
|
||||
)
|
||||
third_course_run_key = str(course3.id)
|
||||
|
||||
@@ -608,7 +607,6 @@ class TestProgramProgressMeter(ModuleStoreTestCase):
|
||||
# 3 certs, all available, program cert in the past/now
|
||||
course3_overview = CourseOverview.get_from_id(course3.id)
|
||||
course3_overview.certificate_available_date = yesterday
|
||||
course3_overview.certificates_display_behavior = CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
course3_overview.save()
|
||||
meter = ProgramProgressMeter(self.site, self.user)
|
||||
self._assert_progress(
|
||||
@@ -624,7 +622,7 @@ class TestProgramProgressMeter(ModuleStoreTestCase):
|
||||
def test_old_course_runs(self, mock_get_programs):
|
||||
"""
|
||||
Test that old course runs may exist for a program which do not exist in LMS.
|
||||
In that case, continue considering the course run to have been failed by the learner
|
||||
In that case, continue considering the course run to've been failed by the learner
|
||||
"""
|
||||
course_run = CourseRunFactory.create()
|
||||
course = CourseFactory.create(course_runs=[course_run])
|
||||
|
||||
@@ -21,7 +21,6 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
|
||||
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
|
||||
from openedx.features.learner_profile.toggles import REDIRECT_TO_PROFILE_MICROFRONTEND
|
||||
from openedx.features.learner_profile.views.learner_profile import learner_profile_context
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
@@ -223,8 +222,7 @@ class LearnerProfileViewTest(SiteMixin, UrlResetMixin, ModuleStoreTestCase):
|
||||
"""
|
||||
# add new course with certificate_available_date is future date.
|
||||
course = CourseFactory.create(
|
||||
certificate_available_date=datetime.datetime.now() + datetime.timedelta(days=5),
|
||||
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
certificate_available_date=datetime.datetime.now() + datetime.timedelta(days=5)
|
||||
)
|
||||
|
||||
cert = self._create_certificate(course_key=course.id)
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -579,7 +579,7 @@
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
|
||||
"integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo="
|
||||
},
|
||||
"are-we-there-yet": {
|
||||
"version": "1.1.5",
|
||||
|
||||
Reference in New Issue
Block a user