feat: AA-885 show offsets in studio self paced course outline for a subsection

feat: AA-883 basic prototype for custom pacing pls in studio

refactor: merge with basic prototype for self paced courses from AA-844

feat: add due date estimate message in self paced courses studio modal

refactor: merge with main that has up to date self paced custom pls editor and tests

fix: only display projected date if start date exists

fix: tests to check grading date in outline

fix: only one warning message show at a time

fix: do not show projected date when it is before the start date
This commit is contained in:
Sofia Yoon
2021-06-23 16:04:07 -04:00
parent 139e7ba48b
commit 898684287a
13 changed files with 236 additions and 116 deletions

View File

@@ -69,13 +69,15 @@ REDIRECT_TO_LIBRARY_AUTHORING_MICROFRONTEND = LegacyWaffleFlag(
)
# .. toggle_name: studio.custom_pls
# .. toggle_name: studio.custom_relative_dates
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: Waffle flag to enable custom pacing for PLS
# .. toggle_description: Waffle flag to enable custom pacing input for Personalized Learner Schedule (PLS).
# .. This flag guards an input in Studio for a self paced course, where the user can enter date offsets
# .. for a subsection.
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2021-07-12
# .. toggle_target_removal_date: 2021-12-31
# .. toggle_warnings: For this flag to be active, add flag 'studio.custom_pls' in Django Admin
# .. toggle_warnings: Flag course_experience.relative_dates should also be active for relative dates functionalities to work.
# .. toggle_tickets: https://openedx.atlassian.net/browse/AA-844
CUSTOM_PLS = CourseWaffleFlag(WAFFLE_NAMESPACE, 'custom_pls', module_name=__name__,)
CUSTOM_RELATIVE_DATES = CourseWaffleFlag(WAFFLE_NAMESPACE, 'custom_relative_dates', module_name=__name__,)

View File

@@ -1230,7 +1230,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
'graded': xblock.graded,
'due_date': get_default_time_display(xblock.due),
'due': xblock.fields['due'].to_json(xblock.due),
'due_num_weeks': xblock.due_num_weeks,
'relative_weeks_due': xblock.relative_weeks_due,
'format': xblock.format,
'course_graders': [grader.get('type') for grader in graders],
'has_changes': has_changes,

View File

@@ -204,7 +204,7 @@ describe('CourseOutlinePage', function() {
setSelfPacedCustomPLS = function() {
setSelfPaced();
course.set('is_custom_pls_active', true);
course.set('is_custom_relative_dates_active', true);
}
createCourseOutlinePage = function(test, courseJSON, createOnly) {
@@ -1009,7 +1009,7 @@ describe('CourseOutlinePage', function() {
describe('Subsection', function() {
var getDisplayNameWrapper, setEditModalValues, setEditModalValuesForCustomPacing, setContentVisibility, mockServerValuesJson,
mockCustomPacingServerValuesJson, selectDisableSpecialExams, selectTimedExam, selectProctoredExam, selectPracticeExam,
selectPrerequisite, selectLastPrerequisiteSubsection, selectDueNumWeeksSubsection, checkOptionFieldVisibility,
selectPrerequisite, selectLastPrerequisiteSubsection, selectRelativeWeeksSubsection, checkOptionFieldVisibility,
defaultModalSettings, modalSettingsWithExamReviewRules, getMockNoPrereqOrExamsCourseJSON, expectShowCorrectness;
getDisplayNameWrapper = function() {
@@ -2134,12 +2134,12 @@ describe('CourseOutlinePage', function() {
setSelfPacedCustomPLS();
});
setEditModalValuesForCustomPacing = function(due_in, grading_type) {
$('#due_in').val(due_in);
setEditModalValuesForCustomPacing = function(grading_type, due_in) {
$('#grading_type').val(grading_type);
$('#due_in').val(due_in);
};
selectDueNumWeeksSubsection = function(weeks) {
selectRelativeWeeksSubsection = function(weeks) {
$('#due_in').val(weeks).trigger('keyup');
}
@@ -2148,7 +2148,8 @@ describe('CourseOutlinePage', function() {
}, [
createMockSubsectionJSON({
graded: true,
due_num_weeks: 3,
relative_weeks_due: 3,
start: '2014-07-09T00:00:00Z',
format: 'Lab',
has_explicit_staff_lock: true,
staff_only_message: true,
@@ -2178,14 +2179,14 @@ describe('CourseOutlinePage', function() {
it('can be edited when custom pacing for self paced course is active', function() {
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValuesForCustomPacing('3', 'Lab');
selectAdvancedSettings();
setEditModalValuesForCustomPacing('Lab', '3');
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', {
graderType: 'Lab',
isPrereq: false,
metadata: {
due_num_weeks: 3,
relative_weeks_due: 3,
is_time_limited: false,
is_practice_exam: false,
is_proctored_enabled: false,
@@ -2200,6 +2201,10 @@ describe('CourseOutlinePage', function() {
AjaxHelpers.respondWithJson(requests, mockCustomPacingServerValuesJson);
AjaxHelpers.expectNoRequests(requests);
expect($('.outline-subsection .status-custom-grading-date').text().trim()).toEqual(
'Custom due date: 3 weeks from enrollment'
);
expect($('.outline-subsection .status-grading-value')).toContainText(
'Lab'
);
@@ -2209,6 +2214,9 @@ describe('CourseOutlinePage', function() {
expect($('.outline-item .outline-subsection .status-grading-value')).toContainText('Lab');
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
expect($('#relative_date_input').css('display')).not.toBe('none');
expect($('#relative_weeks_due_projected.message').text().trim()).toEqual('If a learner starts on Jul 09, 2014, this subsection will be due on Jul 30, 2014.');
expect($('#due_in').val()).toBe('3');
expect($('#grading_type').val()).toBe('Lab');
expect($('input[name=content-visibility][value=staff_only]').is(':checked')).toBe(true);
@@ -2219,32 +2227,45 @@ describe('CourseOutlinePage', function() {
expectShowCorrectness('never');
});
it('shows validation error on due number of weeks', function() {
it ('does not show relative date input when assignment is not graded', function() {
outlinePage.$('.outline-subsection .configure-button').click();
$('#grading_type').val('Lab').trigger('change');
$('#due_in').val('').trigger('change');
expect($('#relative_date_input').css('display')).not.toBe('none');
$('#grading_type').val('notgraded').trigger('change');
$('#due_in').val('').trigger('change');
expect($('#relative_date_input').css('display')).toBe('none');
})
it('shows validation error on relative date', function() {
outlinePage.$('.outline-subsection .configure-button').click();
// when due number of weeks goes over 18
selectDueNumWeeksSubsection('19');
expect($('#due-num-weeks-warning-max').css('display')).not.toBe('none');
selectRelativeWeeksSubsection('19');
expect($('#relative_weeks_due_warning_max').css('display')).not.toBe('none');
expect($('#relative_weeks_due_warning_max')).toContainText('The maximum number of weeks this subsection can be due in is 18 weeks from the learner enrollment date.');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
// when due number of weeks is less than 1
selectDueNumWeeksSubsection('-1');
expect($('#due-num-weeks-warning-min').css('display')).not.toBe('none');
selectRelativeWeeksSubsection('-1');
expect($('#relative_weeks_due_warning_min').css('display')).not.toBe('none');
expect($('#relative_weeks_due_warning_min')).toContainText('The minimum number of weeks this subsection can be due in is 1 week from the learner enrollment date.');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
// when no validation error should show up
selectDueNumWeeksSubsection('10');
expect($('#due-num-weeks-warning-max').css('display')).toBe('none');
expect($('#due-num-weeks-warning-min').css('display')).toBe('none');
selectRelativeWeeksSubsection('10');
expect($('#relative_weeks_due_warning_max').css('display')).toBe('none');
expect($('#relative_weeks_due_warning_min').css('display')).toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(false);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(false);
});
it('due num weeks (due_in) can be cleared.', function() {
it('outline with assignment type and date are cleared when relative date input is cleared.', function() {
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
setEditModalValuesForCustomPacing('3', 'Lab');
setEditModalValuesForCustomPacing('Lab', '3');
setContentVisibility('staff_only');
$('.wrapper-modal-window .action-save').click();
@@ -2253,6 +2274,10 @@ describe('CourseOutlinePage', function() {
// This is the response for the subsequent fetch operation.
AjaxHelpers.respondWithJson(requests, mockCustomPacingServerValuesJson);
expect($('.outline-subsection .status-custom-grading-date').text().trim()).toEqual(
'Custom due date: 3 weeks from enrollment'
);
expect($('.outline-subsection .status-grading-value')).toContainText(
'Lab'
);
@@ -2261,12 +2286,12 @@ describe('CourseOutlinePage', function() {
);
outlinePage.$('.outline-subsection .configure-button').click();
expect($('#relative_weeks_due_projected.message').text().trim()).toEqual('If a learner starts on Jul 09, 2014, this subsection will be due on Jul 30, 2014.');
expect($('#due_in').val()).toBe('3');
expect($('#grading_type').val()).toBe('Lab');
expect($('input[name=content-visibility][value=staff_only]').is(':checked')).toBe(true);
$('.wrapper-modal-window .due-date-input .action-clear').click();
expect($('#due_in').val()).toBe('');
$('#due_in').val('');
$('#grading_type').val('notgraded');
setContentVisibility('visible');
@@ -2280,6 +2305,7 @@ describe('CourseOutlinePage', function() {
createMockSectionJSON({}, [createMockSubsectionJSON()])
);
expect($('.outline-subsection .status-custom-grading-date')).not.toExist();
expect($('.outline-subsection .status-grading-value')).not.toExist();
expect($('.outline-subsection .status-message-copy')).not.toContainText(
'Contains staff only content'

View File

@@ -390,12 +390,11 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
});
SelfPacedDueDateEditor = AbstractEditor.extend({
fieldName: 'due_num_weeks',
fieldName: 'relative_weeks_due',
templateName: 'self-paced-due-date-editor',
className: 'modal-section-content has-actions due-date-input grading-due-date',
events: {
'click .clear-date': 'clearValue',
'change #due_in': 'validateDueIn',
'keyup #due_in': 'validateDueIn',
'blur #due_in': 'validateDueIn',
},
@@ -404,44 +403,72 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
return parseInt(this.$('#due_in').val());
},
showProjectedDate: function() {
if (!this.getValue()) return;
var startDate = new Date(this.model.get('start'));
// The value returned by toUTCString() is a string in the form Www, dd Mmm yyyy hh:mm:ss GMT
var startDateList = startDate.toUTCString().split(' ')
// This text will look like Mmm dd, yyyy (i.e. Jul 26, 2021)
this.$("#relative_weeks_due_start_date").text(startDateList[2] + ' ' + startDateList[1] + ', ' + startDateList[3]);
var projectedDate = new Date(startDate)
projectedDate.setDate(projectedDate.getDate() + this.getValue()*7);
var projectedDateList = projectedDate.toUTCString().split(' ');
this.$("#relative_weeks_due_projected_due_in").text(projectedDateList[2] + ' ' + projectedDateList[1] + ', ' + projectedDateList[3]);
this.$('#relative_weeks_due_projected').show();
},
validateDueIn: function() {
this.$('#relative_weeks_due_projected').hide();
if (this.getValue() > 18){
this.$('#due-num-weeks-warning-max').show();
this.$('#relative_weeks_due_warning_max').show();
BaseModal.prototype.disableActionButton.call(this.parent, 'save');
}
else if (this.getValue() < 1){
this.$('#due-num-weeks-warning-min').show()
this.$('#relative_weeks_due_warning_min').show()
BaseModal.prototype.disableActionButton.call(this.parent, 'save');
}
else {
this.$('#due-num-weeks-warning-max').hide();
this.$('#due-num-weeks-warning-min').hide();
this.$('#relative_weeks_due_warning_max').hide();
this.$('#relative_weeks_due_warning_min').hide();
if (this.model.get('start')){
this.showProjectedDate();
}
BaseModal.prototype.enableActionButton.call(this.parent, 'save');
}
},
clearValue: function(event) {
event.preventDefault();
this.$('#due_in').val('');
},
afterRender: function() {
AbstractEditor.prototype.afterRender.call(this);
this.$('.field-due-in input').val(this.model.get('due_num_weeks'));
if (this.model.get('graded')) {
this.$('#relative_date_input').show()
}
else {
this.$('#relative_date_input').hide()
}
this.$('.field-due-in input').val(this.model.get('relative_weeks_due'));
this.$('#relative_weeks_due_projected').hide();
if (this.getValue() && this.model.get('start')){
this.showProjectedDate();
}
},
getRequestData: function() {
if (this.getValue() < 19 && this.getValue() > 0) {
if (this.getValue() < 19 && this.getValue() > 0 && $('#grading_type').val() !== 'notgraded') {
return {
metadata: {
due_num_weeks: this.getValue()
relative_weeks_due: this.getValue()
}
};
} else {
return {
metadata: {
relative_weeks_due: null
}
}
}
}
},
});
ReleaseDateEditor = BaseDateEditor.extend({
fieldName: 'start',
templateName: 'release-date-editor',
@@ -692,6 +719,18 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
GradingEditor = AbstractEditor.extend({
templateName: 'grading-editor',
className: 'edit-settings-grading',
events: {
'change #grading_type': 'handleGradingSelect',
},
handleGradingSelect: function(event) {
event.preventDefault();
if (this.$('#grading_type').val() !== 'notgraded' && course.get('self_paced') && course.get('is_custom_relative_dates_active')) {
$('#relative_date_input').show();
} else {
$('#relative_date_input').hide();
}
},
afterRender: function() {
AbstractEditor.prototype.afterRender.call(this);
@@ -1130,10 +1169,9 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
} else if (xblockInfo.isSequential()) {
tabs[0].editors = [ReleaseDateEditor, GradingEditor, DueDateEditor];
tabs[1].editors = [ContentVisibilityEditor, ShowCorrectnessEditor];
if (course.get('self_paced') && course.get('is_custom_pls_active')) {
if (course.get('self_paced') && course.get('is_custom_relative_dates_active')) {
tabs[0].editors.push(SelfPacedDueDateEditor);
}
if (options.enable_proctored_exams || options.enable_timed_exams) {
advancedTab.editors.push(TimedExaminationPreferenceEditor);
}

View File

@@ -652,7 +652,8 @@ $outline-indent-width: $baseline;
}
.status-grading-value,
.status-proctored-exam-value {
.status-proctored-exam-value,
.status-custom-grading-date {
display: inline-block;
vertical-align: middle;
}

View File

@@ -10,7 +10,7 @@
<%!
from django.utils.translation import ugettext as _
from cms.djangoapps.contentstore.config.waffle import CUSTOM_PLS
from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES
from lms.djangoapps.branding import api as branding_api
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
from openedx.core.djangolib.js_utils import (
@@ -157,7 +157,7 @@ from openedx.core.release import RELEASE_LINE
display_course_number: "${context_course.display_coursenumber | n, js_escaped_string}",
revision: "${context_course.location.branch | n, js_escaped_string}",
self_paced: ${ context_course.self_paced | n, dump_js_escaped_json },
is_custom_pls_active: ${CUSTOM_PLS.is_enabled(context_course.id) | n, dump_js_escaped_json}
is_custom_relative_dates_active: ${CUSTOM_RELATIVE_DATES.is_enabled(context_course.id) | n, dump_js_escaped_json}
});
</script>
% endif

View File

@@ -246,6 +246,50 @@ if (is_proctored_exam) {
<% } %>
</p>
</div>
<% if (course.get('self_paced') && course.get('is_custom_relative_dates_active') && xblockInfo.get('relative_weeks_due')) { %>
<div class="status-grading">
<p>
<span class="icon fa fa-calendar" aria-hidden="true"></span>
<span class="status-custom-grading-date">
<%- edx.StringUtils.interpolate(
ngettext(
'Custom due date: {relativeWeeks} week from enrollment',
'Custom due date: {relativeWeeks} weeks from enrollment',
xblockInfo.get('relative_weeks_due')),
{
relativeWeeks: xblockInfo.get('relative_weeks_due')
}
)
%>
</span>
<p>
</div>
<% } %>
<% } else if (course.get('self_paced') && course.get('is_custom_relative_dates_active') && xblockInfo.get('relative_weeks_due')) { %>
<div class="status-grading">
<p>
<span class="sr status-grading-label"> <%- gettext('Graded as:') %> </span>
<span class="icon fa fa-check" aria-hidden="true"></span>
<span class="status-grading-value"> <%- gradingType %> </span>
</p>
</div>
<div class="status-grading">
<p>
<span class="icon fa fa-calendar" aria-hidden="true"></span>
<span class="status-custom-grading-date">
<%- edx.StringUtils.interpolate(
ngettext(
'Custom due date: {relativeWeeks} week from enrollment',
'Custom due date: {relativeWeeks} weeks from enrollment',
xblockInfo.get('relative_weeks_due')),
{
relativeWeeks: xblockInfo.get('relative_weeks_due')
}
)
%>
</span>
<p>
</div>
<% } %>
<div class="status-hide-after-due">
<p>

View File

@@ -1,24 +1,30 @@
<ul class="list-fields list-input date-setter">
<li class="field field-text field-due-in">
<label for="due_in"><%- gettext('Due in:') %></label>
<input type="number" id="due_in" name="due_in" value=""
placeholder="" autocomplete="off" min="1" max="18"/> weeks
</li>
</ul>
<div id="relative_date_input">
<ul class="list-fields list-input date-setter">
<li class="field field-text field-due-in">
<!-- Translators: Please use a generic pluralization that makes sense in most contexts for the second half of the sentence "weeks from learner enrollment date" which is preceded by "Due in: [input entry]" -->
<label for="due_in"><%- gettext('Due in:') %></label>
<input type="number" id="due_in" name="due_in" value=""
placeholder="" autocomplete="off" min="1" max="18" style="width:20%"/>
<%- gettext('weeks from learner enrollment date')%>
</li>
</ul>
<ul class="list-actions">
<li class="action-item">
<a href="#" data-tooltip="<%- gettext('Clear Due Date') %>" class="clear-date action-button action-clear">
<span class="icon fa fa-undo" aria-hidden="true"></span>
<span class="sr"><%- gettext('Clear Due Date') %></span>
</a>
</li>
</ul>
<div id="relative_weeks_due_projected" class="message">
<%= edx.HtmlUtils.interpolateHtml(
gettext('If a learner starts on {startDate}, this subsection will be due on {projectedDueIn}.'),
{
startDate: edx.HtmlUtils.HTML('<span id="relative_weeks_due_start_date"></span>'),
projectedDueIn: edx.HtmlUtils.HTML('<span id="relative_weeks_due_projected_due_in"></span>')
})
%>
</div>
<div id="due-num-weeks-warning-max" class="message-status error">
<%- gettext('The maximum number of weeks this subsection can be due in is 18 weeks.') %>
<div id="relative_weeks_due_warning_max" class="message-status error">
<%- gettext('The maximum number of weeks this subsection can be due in is 18 weeks from the learner enrollment date.') %>
</div>
<div id="relative_weeks_due_warning_min" class="message-status error">
<%- gettext('The minimum number of weeks this subsection can be due in is 1 week from the learner enrollment date.') %>
</div>
</div>
<div id="due-num-weeks-warning-min" class="message-status error">
<%- gettext('The minimum number of weeks this subsection can be due in is 1 week.') %>
</div>