Revert "feat: Reimagine certificate_availability_date and certificates_display_behavior"

This commit is contained in:
Matt Tuchfarber
2021-07-07 16:53:05 -04:00
committed by GitHub
parent b4df37d48e
commit 63cb6a97ff
34 changed files with 572 additions and 933 deletions

View File

@@ -41,7 +41,6 @@ class CourseMetadata:
'enrollment_start',
'enrollment_end',
'certificate_available_date',
'certificates_display_behavior',
'tabs',
'graceperiod',
'show_timezone',

View File

@@ -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;

View File

@@ -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'));
}
});

View File

@@ -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();
});

View File

@@ -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()

View File

@@ -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();
}

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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}

View File

@@ -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))

View File

@@ -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."""

View File

@@ -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)

View File

@@ -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),
]),
]

View File

@@ -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):
"""

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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(

View File

@@ -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(

View File

@@ -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
)

View File

@@ -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'
%>

View File

@@ -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):

View File

@@ -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)

View File

@@ -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),
),
]

View File

@@ -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)

View File

@@ -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)

View File

@@ -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']

View File

@@ -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

View File

@@ -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

View File

@@ -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])

View File

@@ -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
View File

@@ -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",