From 27bb2b914494f0931d8a5c3b7beb40f30df9579e Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Thu, 23 Jul 2020 23:17:31 +0100 Subject: [PATCH 1/5] issue/2845 Presenation component save/restore --- src/core/js/models/componentModel.js | 125 +++++++++++++++++++++- src/core/js/models/itemsComponentModel.js | 12 +++ src/core/js/models/questionModel.js | 116 +------------------- 3 files changed, 137 insertions(+), 116 deletions(-) diff --git a/src/core/js/models/componentModel.js b/src/core/js/models/componentModel.js index 67b8dc684..2757a5c4d 100644 --- a/src/core/js/models/componentModel.js +++ b/src/core/js/models/componentModel.js @@ -25,18 +25,22 @@ define([ defaults() { return AdaptModel.resultExtend('defaults', { - _isA11yComponentDescriptionEnabled: true + _isA11yComponentDescriptionEnabled: true, + _userAnswer: null, + _attemptStates: null, }); } trackable() { return AdaptModel.resultExtend('trackable', [ - '_userAnswer' + '_userAnswer', + '_attemptStates' ]); } trackableType() { return AdaptModel.resultExtend('trackableType', [ + Array, Array ]); } @@ -45,6 +49,123 @@ define([ return false; } + init() { + if (Adapt.get('_isStarted')) { + this.onAdaptInitialize(); + return; + } + this.listenToOnce(Adapt, 'adapt:initialize', this.onAdaptInitialize); + } + + onAdaptInitialize() { + this.restoreUserAnswers(); + } + + restoreUserAnswers() {} + + storeUserAnswer() {} + + resetUserAnswer() { + this.set('_userAnswer', null); + } + + reset(type, force) { + if (!this.get('_canReset') && !force) return; + this.resetUserAnswer(); + super.reset(type, force); + } + + /** + * Returns the current attempt state raw data or the raw data from the supplied attempt state object. + * @param {Object} [object] JSON object representing the component state. Defaults to current JSON. + * @returns {Array} + */ + getAttemptState(object = this.toJSON()) { + const trackables = this.trackable(); + const types = this.trackableType(); + trackables.find((name, index) => { + // Exclude _attemptStates as it's trackable but isn't needed here + if (name !== '_attemptStates') return; + trackables.splice(index, 1); + types.splice(index, 1); + return true; + }); + const values = trackables.map(n => object[n]); + const booleans = values.filter((v, i) => types[i] === Boolean).map(Boolean); + const numbers = values.filter((v, i) => types[i] === Number).map(v => Number(v) || 0); + const arrays = values.filter((v, i) => types[i] === Array); + return [ + numbers, + booleans, + arrays + ]; + } + + /** + * Returns an attempt object representing the current state or a formatted version of the raw state object supplied. + * @param {Array} [state] JSON object representing the component state, defaults to current state returned from getAttemptState(). + * @returns {Object} + */ + getAttemptObject(state = this.getAttemptState()) { + const trackables = this.trackable(); + const types = this.trackableType(); + trackables.find((name, index) => { + // Exclude _attemptStates as it's trackable but isn't needed here + if (name !== '_attemptStates') return; + trackables.splice(index, 1); + types.splice(index, 1); + return true; + }); + const numbers = (state[0] || []).slice(0); + const booleans = (state[1] || []).slice(0); + const arrays = (state[2] || []).slice(0); + const object = {}; + trackables.forEach((n, i) => { + if (n === '_id') return; + switch (types[i]) { + case Number: + object[n] = numbers.shift(); + break; + case Boolean: + object[n] = booleans.shift(); + break; + case Array: + object[n] = arrays.shift(); + break; + } + }); + return object; + } + + /** + * Sets the current attempt state from the supplied attempt state object. + * @param {Object} object JSON object representing the component state. + * @param {boolean} silent Stops change events from triggering + */ + setAttemptObject(object, silent = true) { + this.set(object, { silent }); + } + + /** + * Adds the current attempt state object or the supplied state object to the attempts store. + * @param {Object} [object] JSON object representing the component state. Defaults to current JSON. + */ + addAttemptObject(object = this.getAttemptObject()) { + const attemptStates = this.get('_attemptStates') || []; + const state = this.getAttemptState(object); + attemptStates.push(state); + this.set('_attemptStates', attemptStates); + } + + /** + * Returns an array of the previous state objects. The most recent state is last in the list. + * @returns {Array} + */ + getAttemptObjects() { + const states = this.get('_attemptStates') || []; + return states.map(state => this.getAttemptObject(state)); + } + } // This abstract model needs to registered to support deprecated view-only components diff --git a/src/core/js/models/itemsComponentModel.js b/src/core/js/models/itemsComponentModel.js index 6ed0c284f..5636e4610 100644 --- a/src/core/js/models/itemsComponentModel.js +++ b/src/core/js/models/itemsComponentModel.js @@ -18,6 +18,17 @@ define([ 'all': this.onAll, 'change:_isVisited': this.checkCompletionStatus }); + super.init(); + } + + restoreUserAnswers() { + const userAnswer = this.get('_userAnswer'); + if (!userAnswer) return; + this.getChildren().forEach((child, index) => child.set('_isVisited', userAnswer[index])); + } + + storeUserAnswer() { + this.set('_userAnswer', this.getChildren().map(child => child.get('_isVisited'))); } /** @@ -56,6 +67,7 @@ define([ } checkCompletionStatus() { + this.storeUserAnswer(); if (!this.areAllItemsCompleted()) return; this.setCompletionStatus(); } diff --git a/src/core/js/models/questionModel.js b/src/core/js/models/questionModel.js index 6ccb48c19..4ff9eab04 100644 --- a/src/core/js/models/questionModel.js +++ b/src/core/js/models/questionModel.js @@ -32,8 +32,7 @@ define([ '_isSubmitted', '_score', '_isCorrect', - '_attemptsLeft', - '_attemptStates' + '_attemptsLeft' ]); } @@ -42,8 +41,7 @@ define([ Boolean, Number, Boolean, - Number, - Array + Number ]); } @@ -109,18 +107,6 @@ define([ // Not needed as handled by model defaults, keeping to maintain API } - /// /// - // Selection restoration process - /// / - - // Used to add post-load changes to the model - onAdaptInitialize() { - this.restoreUserAnswers(); - } - - // Used to restore the user answers - restoreUserAnswers() {} - /// /// // Submit process /// / @@ -149,10 +135,6 @@ define([ }); } - // This is important for returning or showing the users answer - // This should preserve the state of the users answers - storeUserAnswer() {} - // Sets _isCorrect:true/false based upon isCorrect method below markQuestion() { @@ -308,9 +290,6 @@ define([ }); } - // Used by the question view to reset the stored user answer - resetUserAnswer() {} - refresh() { this.trigger('question:refresh'); } @@ -358,97 +337,6 @@ define([ return this.get('_isInteractionComplete'); } - /** - * Returns the current attempt state raw data or the raw data from the supplied attempt state object. - * @param {Object} [object] JSON object representing the component state. Defaults to current JSON. - * @returns {Array} - */ - getAttemptState(object = this.toJSON()) { - const trackables = this.trackable(); - const types = this.trackableType(); - trackables.find((name, index) => { - // Exclude _attemptStates as it's trackable but isn't needed here - if (name !== '_attemptStates') return; - trackables.splice(index, 1); - types.splice(index, 1); - return true; - }); - const values = trackables.map(n => object[n]); - const booleans = values.filter((v, i) => types[i] === Boolean).map(Boolean); - const numbers = values.filter((v, i) => types[i] === Number).map(v => Number(v) || 0); - const arrays = values.filter((v, i) => types[i] === Array); - return [ - numbers, - booleans, - arrays - ]; - } - - /** - * Returns an attempt object representing the current state or a formatted version of the raw state object supplied. - * @param {Array} [state] JSON object representing the component state, defaults to current state returned from getAttemptState(). - * @returns {Object} - */ - getAttemptObject(state = this.getAttemptState()) { - const trackables = this.trackable(); - const types = this.trackableType(); - trackables.find((name, index) => { - // Exclude _attemptStates as it's trackable but isn't needed here - if (name !== '_attemptStates') return; - trackables.splice(index, 1); - types.splice(index, 1); - return true; - }); - const numbers = (state[0] || []).slice(0); - const booleans = (state[1] || []).slice(0); - const arrays = (state[2] || []).slice(0); - const object = {}; - trackables.forEach((n, i) => { - if (n === '_id') return; - switch (types[i]) { - case Number: - object[n] = numbers.shift(); - break; - case Boolean: - object[n] = booleans.shift(); - break; - case Array: - object[n] = arrays.shift(); - break; - } - }); - return object; - } - - /** - * Sets the current attempt state from the supplied attempt state object. - * @param {Object} object JSON object representing the component state. - * @param {boolean} silent Stops change events from triggering - */ - setAttemptObject(object, silent = true) { - this.set(object, { silent }); - } - - /** - * Adds the current attempt state object or the supplied state object to the attempts store. - * @param {Object} [object] JSON object representing the component state. Defaults to current JSON. - */ - addAttemptObject(object = this.getAttemptObject()) { - const attemptStates = this.get('_attemptStates') || []; - const state = this.getAttemptState(object); - attemptStates.push(state); - this.set('_attemptStates', attemptStates); - } - - /** - * Returns an array of the previous state objects. The most recent state is last in the list. - * @returns {Array} - */ - getAttemptObjects() { - const states = this.get('_attemptStates') || []; - return states.map(state => this.getAttemptObject(state)); - } - } return QuestionModel; From 4e11849e011d8b61461ae676c848fd6ce7821a18 Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Thu, 23 Jul 2020 23:32:10 +0100 Subject: [PATCH 2/5] issue/2845 Added notes on _userAnswer values --- src/core/js/models/componentModel.js | 10 ++++++++++ src/core/js/models/itemsComponentModel.js | 9 +++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/core/js/models/componentModel.js b/src/core/js/models/componentModel.js index 2757a5c4d..27fe1658a 100644 --- a/src/core/js/models/componentModel.js +++ b/src/core/js/models/componentModel.js @@ -61,8 +61,18 @@ define([ this.restoreUserAnswers(); } + /** + * Restore the user's answer from the _userAnswer property. + * The _userAnswer value must be in the form of arrays containing + * numbers, booleans or arrays only. + */ restoreUserAnswers() {} + /** + * Store the user's answer in the _userAnswer property. + * The _userAnswer value must be in the form of arrays containing + * numbers, booleans or arrays only. + */ storeUserAnswer() {} resetUserAnswer() { diff --git a/src/core/js/models/itemsComponentModel.js b/src/core/js/models/itemsComponentModel.js index 5636e4610..c365bf7be 100644 --- a/src/core/js/models/itemsComponentModel.js +++ b/src/core/js/models/itemsComponentModel.js @@ -22,13 +22,14 @@ define([ } restoreUserAnswers() { - const userAnswer = this.get('_userAnswer'); - if (!userAnswer) return; - this.getChildren().forEach((child, index) => child.set('_isVisited', userAnswer[index])); + const booleanArray = this.get('_userAnswer'); + if (!booleanArray) return; + this.getChildren().forEach((child, index) => child.set('_isVisited', booleanArray[index])); } storeUserAnswer() { - this.set('_userAnswer', this.getChildren().map(child => child.get('_isVisited'))); + const booleanArray = this.getChildren().map(child => child.get('_isVisited')); + this.set('_userAnswer', booleanArray); } /** From 9b590396fbd79d5f0be61c806c2c29128bdfa403 Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Thu, 23 Jul 2020 23:33:58 +0100 Subject: [PATCH 3/5] issue/2845 Removed duplicate event handler --- src/core/js/models/questionModel.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/core/js/models/questionModel.js b/src/core/js/models/questionModel.js index 4ff9eab04..528699be7 100644 --- a/src/core/js/models/questionModel.js +++ b/src/core/js/models/questionModel.js @@ -55,11 +55,6 @@ define([ init() { this.setupDefaultSettings(); - if (Adapt.get('_isStarted')) { - this.onAdaptInitialize(); - return; - } - this.listenToOnce(Adapt, 'adapt:initialize', this.onAdaptInitialize); this.setLocking('_canSubmit', true); } From 6abdba070bb305486037273cff522de06e908169 Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Sat, 25 Jul 2020 16:36:11 +0100 Subject: [PATCH 4/5] issue/2845 Typos and missing call --- src/core/js/models/componentModel.js | 8 ++++---- src/core/js/models/questionModel.js | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/core/js/models/componentModel.js b/src/core/js/models/componentModel.js index 27fe1658a..cd6f9ead3 100644 --- a/src/core/js/models/componentModel.js +++ b/src/core/js/models/componentModel.js @@ -63,15 +63,15 @@ define([ /** * Restore the user's answer from the _userAnswer property. - * The _userAnswer value must be in the form of arrays containing - * numbers, booleans or arrays only. + * The _userAnswer value must be in the form of an array containing + * arrays of numbers, booleans or arrays only. */ restoreUserAnswers() {} /** * Store the user's answer in the _userAnswer property. - * The _userAnswer value must be in the form of arrays containing - * numbers, booleans or arrays only. + * The _userAnswer value must be in the form of an array containing + * arrays of numbers, booleans or arrays only. */ storeUserAnswer() {} diff --git a/src/core/js/models/questionModel.js b/src/core/js/models/questionModel.js index 528699be7..c33641b80 100644 --- a/src/core/js/models/questionModel.js +++ b/src/core/js/models/questionModel.js @@ -56,6 +56,7 @@ define([ init() { this.setupDefaultSettings(); this.setLocking('_canSubmit', true); + super.init(); } // Calls default methods to setup on questions From e4d4c2df8331566fd79ed105b7e9a7fb18dafc63 Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Sat, 25 Jul 2020 16:38:33 +0100 Subject: [PATCH 5/5] issue/2845 Reverted correction --- src/core/js/models/componentModel.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/js/models/componentModel.js b/src/core/js/models/componentModel.js index cd6f9ead3..27fe1658a 100644 --- a/src/core/js/models/componentModel.js +++ b/src/core/js/models/componentModel.js @@ -63,15 +63,15 @@ define([ /** * Restore the user's answer from the _userAnswer property. - * The _userAnswer value must be in the form of an array containing - * arrays of numbers, booleans or arrays only. + * The _userAnswer value must be in the form of arrays containing + * numbers, booleans or arrays only. */ restoreUserAnswers() {} /** * Store the user's answer in the _userAnswer property. - * The _userAnswer value must be in the form of an array containing - * arrays of numbers, booleans or arrays only. + * The _userAnswer value must be in the form of arrays containing + * numbers, booleans or arrays only. */ storeUserAnswer() {}