diff --git a/openassessment/xblock/static/js/spec/studio/oa_edit_fields.js b/openassessment/xblock/static/js/spec/studio/oa_edit_fields.js index dfb88c73b7..246e04fcf5 100644 --- a/openassessment/xblock/static/js/spec/studio/oa_edit_fields.js +++ b/openassessment/xblock/static/js/spec/studio/oa_edit_fields.js @@ -102,6 +102,46 @@ describe("OpenAssessment.DatetimeControl", function() { datetimeControl.clearValidationErrors(); expect(datetimeControl.validationErrors()).toEqual([]); }); + + describe("stash and pop", function() { + const dates = ["2024-09-09", "2030-03-02", "2031-03-05"] + const times = ["05:28", "01:01", "02:13"] + + beforeEach(function() { + datetimeControl.datetime(dates[0], times[0]); + expect(datetimeControl.describe()).toEqual(`${dates[0]}T${times[0]}`); + }) + + it('update values correctly', function() { + datetimeControl.stash(dates[1], times[1]); + expect(datetimeControl.describe()).toEqual(`${dates[1]}T${times[1]}`); + + datetimeControl.pop() + expect(datetimeControl.describe()).toEqual(`${dates[0]}T${times[0]}`); + }); + + it('stashing multiple times does nothing until popping', function() { + datetimeControl.stash(dates[1], times[1]); + expect(datetimeControl.describe()).toEqual(`${dates[1]}T${times[1]}`); + datetimeControl.stash(dates[2], times[2]); + expect(datetimeControl.describe()).toEqual(`${dates[1]}T${times[1]}`); + + datetimeControl.pop() + expect(datetimeControl.describe()).toEqual(`${dates[0]}T${times[0]}`); + }); + + it('popping multiple times does nothing', function() { + datetimeControl.stash(dates[1], times[1]); + expect(datetimeControl.describe()).toEqual(`${dates[1]}T${times[1]}`); + + datetimeControl.pop() + expect(datetimeControl.describe()).toEqual(`${dates[0]}T${times[0]}`); + + datetimeControl.pop() + expect(datetimeControl.describe()).toEqual(`${dates[0]}T${times[0]}`); + }); + }) + }); describe("OpenAssessment.ToggleControl", function() { diff --git a/openassessment/xblock/static/js/spec/studio/oa_edit_schedule.js b/openassessment/xblock/static/js/spec/studio/oa_edit_schedule.js index 894da5f349..678f1b968d 100644 --- a/openassessment/xblock/static/js/spec/studio/oa_edit_schedule.js +++ b/openassessment/xblock/static/js/spec/studio/oa_edit_schedule.js @@ -77,6 +77,66 @@ describe('OpenAssessment.EditScheduleView', function() { expect(courseEndEl.prop('disabled')).toBe(true); }); + it('stashes and restores manual dates on date config change', function() { + const dateConfigRadios = { + manual: $('input#manual_date_config_type', this.element), + subsection: $('input#subsection_date_config_type', this.element), + course_end: $('input#course_end_date_config_type', this.element), + } + function expectSelectedRadio(expectedValue) { + // Helper to cleanly assert the currently selected radio button + expect($('input[name="date_config_type"][type="radio"]:checked').val()).toEqual(expectedValue); + } + function getCurrentValues() { + // Helper to get the current state of the controls as an object + return { + submission: { + start: view.submissionStart(), + due: view.submissionDue(), + }, + self: { + start: assessmentViews.oa_self_assessment_editor.startDatetimeControl.datetime(), + due: assessmentViews.oa_self_assessment_editor.dueDatetimeControl.datetime(), + }, + peer: { + start: assessmentViews.oa_peer_assessment_editor.startDatetimeControl.datetime(), + due: assessmentViews.oa_peer_assessment_editor.dueDatetimeControl.datetime(), + } + } + } + // If we select a non-manual setting, the current manual values should be stashed, and the + // values of the controls should be set to a default, to avoid any backend validation issues. + const startingValues = getCurrentValues(); + expectSelectedRadio('manual') + dateConfigRadios.subsection.trigger('click'); + expectSelectedRadio('subsection'); + + const expectedPostStashValues = { + submission: { + start: "2001-01-01", + due: "2099-12-31", + }, + self: { + start: "2001-01-01", + due: "2099-12-31", + }, + peer: { + start: "2001-01-01", + due: "2099-12-31", + }, + } + + expect(getCurrentValues()).toEqual(expectedPostStashValues); + // clicking course end now should have no effect + dateConfigRadios.course_end.trigger('click'); + expectSelectedRadio('course_end'); + expect(getCurrentValues()).toEqual(expectedPostStashValues); + // clicking manual will pop the stashed values back into the controls + dateConfigRadios.manual.trigger('click'); + expectSelectedRadio('manual'); + expect(getCurrentValues()).toEqual(startingValues); + }); + it('validates submission start datetime fields', function() { testValidateDate( view.startDatetimeControl, diff --git a/openassessment/xblock/static/js/src/studio/oa_edit_fields.js b/openassessment/xblock/static/js/src/studio/oa_edit_fields.js index 31ae9fdc59..f2b8d842f2 100644 --- a/openassessment/xblock/static/js/src/studio/oa_edit_fields.js +++ b/openassessment/xblock/static/js/src/studio/oa_edit_fields.js @@ -194,6 +194,8 @@ export class DatetimeControl { this.element = element; this.datePicker = datePicker; this.timePicker = timePicker; + this.stashedDate = null; + this.stashedTime = null; } /** @@ -234,6 +236,30 @@ export class DatetimeControl { return `${datePickerSel.val()}T${timePickerSel.val()}`; } + hasStash() { + return this.stashedDate !== null && this.stashedTime !== null; + } + + stash(dateString) { + if (this.hasStash()) { return; } + const datePickerSel = $(this.datePicker, this.element); + const timePickerSel = $(this.timePicker, this.element); + this.stashedDate = datePickerSel.val(); + this.stashedTime = timePickerSel.val(); + datePickerSel.val(dateString); + timePickerSel.val('00:00'); + } + + pop() { + if (!this.hasStash()) { return; } + const datePickerSel = $(this.datePicker, this.element); + const timePickerSel = $(this.timePicker, this.element); + datePickerSel.val(this.stashedDate); + timePickerSel.val(this.stashedTime); + this.stashedDate = null; + this.stashedTime = null; + } + /** Mark validation errors. diff --git a/openassessment/xblock/static/js/src/studio/oa_edit_schedule.js b/openassessment/xblock/static/js/src/studio/oa_edit_schedule.js index 876262c137..fe84c9e769 100644 --- a/openassessment/xblock/static/js/src/studio/oa_edit_schedule.js +++ b/openassessment/xblock/static/js/src/studio/oa_edit_schedule.js @@ -1,5 +1,4 @@ import { DatetimeControl, SelectControl } from './oa_edit_fields'; -import Notifier from './oa_edit_notifier'; /** Editing interface for OpenAssessment schedule settings. @@ -30,23 +29,11 @@ export class EditScheduleView { '#openassessment_submission_due_time', ).install(); - const configSettingsList = $('.schedule_setting_list ', this.element); - this.selectedDateConfigType = $('input[name="date_config_type"][type="radio"]:checked', this.element).val(); new SelectControl( $('input[name="date_config_type"][type="radio"]', this.element), - (value) => { - configSettingsList.each((_, el) => { - if (el.id !== `${value}_schedule_settings_list`) { - $(el).addClass('is--hidden'); - } else { - $(el).removeClass('is--hidden'); - } - }); - this.selectedDateConfigType = value; - }, - new Notifier([]), + this.handleDateTimeConfigChanged, ).install(); } @@ -85,12 +72,96 @@ export class EditScheduleView { } /** + * Handles the date config selection being changed. + * "Stashes" the current start and due dates and displays the correct date settings panel. + * + * @param {string} value The currently selected date config type, one of [ manual, subsection, course_end ] + */ + handleDateTimeConfigChanged(value) { + if (value === 'manual') { + this.popManualDates(); + } else { + this.stashManualDates(); + } + this.showSelectedDateConfigSettings(value); + this.selectedDateConfigType = value; + } + + /** + * Displays the schedule_setting_list entry for the associated date config type and hides the others * + * @param {string} value The currently selected date config type, one of [ manual, subsection, course_end ] + */ + showSelectedDateConfigSettings(value) { + $('.schedule_setting_list ', this.element).each((_, el) => { + if (el.id !== `${value}_schedule_settings_list`) { + $(el).addClass('is--hidden'); + } else { + $(el).removeClass('is--hidden'); + } + }); + } + + /** + * "Stashes" the current values of all manual start and due date settings and replaces the value of the disabled + * input elements with default date values. + * + * Calling this function once will do the stash, and calling it repeatedly after that will have no effect until + * `popManualDates` is called + * + * This allows us to essentially ignore all date validation for non-manual date config on the backend + */ + stashManualDates() { + const defaultStartDate = '2001-01-01'; + const defaultDueDate = '2099-12-31'; + + this.startDatetimeControl.stash(defaultStartDate); + this.dueDatetimeControl.stash(defaultDueDate); + + const peerStep = this.assessmentViews.oa_peer_assessment_editor; + peerStep.startDatetimeControl.stash(defaultStartDate); + peerStep.dueDatetimeControl.stash(defaultDueDate); + + const selfStep = this.assessmentViews.oa_self_assessment_editor; + selfStep.startDatetimeControl.stash(defaultStartDate); + selfStep.dueDatetimeControl.stash(defaultDueDate); + } + + /** + * "Pops" the stashed values of all manual start and due date settings back into the actual input controls. + * + * Calling this function once will perform the pop action. + * Calling it repeatedly after that will have no effect until`stashManualDates` is called again, + * and if `stashManualDates` has never been called, this function will have no effect. + */ + + popManualDates() { + this.startDatetimeControl.pop(); + this.dueDatetimeControl.pop(); + + const peerStep = this.assessmentViews.oa_peer_assessment_editor; + peerStep.startDatetimeControl.pop(); + peerStep.dueDatetimeControl.pop(); + + const selfStep = this.assessmentViews.oa_self_assessment_editor; + selfStep.startDatetimeControl.pop(); + selfStep.dueDatetimeControl.pop(); + } + + /** + * Returns the current date config type */ dateConfigType() { return this.selectedDateConfigType; } + /** + * Returns true if the current date config type is manual + */ + isManualDateConfig() { + return this.dateConfigType() === 'manual'; + } + /** Mark validation errors. @@ -99,6 +170,12 @@ export class EditScheduleView { * */ validate() { + // If we are using a non-manual date config type, don't validate + // the hidden fields + if (!this.isManualDateConfig()) { + return true; + } + // Validate the start and due datetime controls let isValid = true;