Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue/2845 Presentation component save+restore #2846

Merged
merged 6 commits into from
Jul 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 133 additions & 2 deletions src/core/js/models/componentModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
]);
}
Expand All @@ -45,6 +49,133 @@ define([
return false;
}

init() {
if (Adapt.get('_isStarted')) {
this.onAdaptInitialize();
return;
}
this.listenToOnce(Adapt, 'adapt:initialize', this.onAdaptInitialize);
}

onAdaptInitialize() {
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() {
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
Expand Down
13 changes: 13 additions & 0 deletions src/core/js/models/itemsComponentModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ define([
'all': this.onAll,
'change:_isVisited': this.checkCompletionStatus
});
super.init();
}

restoreUserAnswers() {
const booleanArray = this.get('_userAnswer');
if (!booleanArray) return;
this.getChildren().forEach((child, index) => child.set('_isVisited', booleanArray[index]));
}

storeUserAnswer() {
const booleanArray = this.getChildren().map(child => child.get('_isVisited'));
this.set('_userAnswer', booleanArray);
}

/**
Expand Down Expand Up @@ -56,6 +68,7 @@ define([
}

checkCompletionStatus() {
this.storeUserAnswer();
if (!this.areAllItemsCompleted()) return;
this.setCompletionStatus();
}
Expand Down
122 changes: 3 additions & 119 deletions src/core/js/models/questionModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ define([
'_isSubmitted',
'_score',
'_isCorrect',
'_attemptsLeft',
'_attemptStates'
'_attemptsLeft'
]);
}

Expand All @@ -42,8 +41,7 @@ define([
Boolean,
Number,
Boolean,
Number,
Array
Number
]);
}

Expand All @@ -58,11 +56,7 @@ define([
init() {
this.setupDefaultSettings();
this.setLocking('_canSubmit', true);
if (Adapt.get('_isStarted')) {
this.onAdaptInitialize();
return;
}
this.listenToOnce(Adapt, 'adapt:initialize', this.onAdaptInitialize);
super.init();
}

// Calls default methods to setup on questions
Expand Down Expand Up @@ -109,18 +103,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
/// /
Expand Down Expand Up @@ -149,10 +131,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() {

Expand Down Expand Up @@ -308,9 +286,6 @@ define([
});
}

// Used by the question view to reset the stored user answer
resetUserAnswer() {}

refresh() {
this.trigger('question:refresh');
}
Expand Down Expand Up @@ -358,97 +333,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;
Expand Down