Skip to content
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
10 changes: 10 additions & 0 deletions js/LifecycleSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Adapt from 'core/js/adapt';
import Logging from 'core/js/logging';
import State from './State';
import IntersectionSet from './IntersectionSet';
import LifecycleUpdateJournal from './LifecycleUpdateJournal';

/**
* Set at which intersections and queries can be performed.
Expand All @@ -20,6 +21,15 @@ export default class LifecycleSet extends IntersectionSet {
return (this._state = this._state || new State({ set: this }));
}

/**
* The journal for recording the updates to the set.
* @returns {LifecycleUpdateJournal}
*/
get journal() {
if (this.isIntersectedSet) return;
return (this._journal = this._journal || new LifecycleUpdateJournal({ set: this }));
}

/**
* Signifies if onRestore returned true/false.
* @returns {boolean}
Expand Down
141 changes: 14 additions & 127 deletions js/LifecycleUpdateJournal.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import Logging from 'core/js/logging';
import {
filterModelsByIntersectingModels,
isModelAvailableInHierarchy
} from './utils/models';
import _ from 'underscore';
/** @typedef {import("./ScoringSet").default} ScoringSet */

/**
* A journal for recording the lifecycle updates to a set.
* A journal for recording the models and sets that triggered set updates in the current lifecycle.
*/
export default class LifecycleUpdateJournal {

Expand All @@ -17,140 +11,33 @@ export default class LifecycleUpdateJournal {
*/
constructor({ set } = {}) {
this.set = set;
this._pendingUpdateModels = new Set();
this._pendingUpdateSets = new Set();
this._pendingUpdateModifiers = [];
this.pendingUpdateModels = new Set();
this.pendingUpdateSets = new Set();
}

/**
* Add the model and intersected sets having triggered this set's next update.
* @param {Backbone.Model} model
* @param {ScoringSet[]} [sets]
* Add the model and intersecting sets which caused the set update to be triggered.
* @param {Backbone.Model} model Source model
* @param {ScoringSet[]} [sets] Intersecting sets
*/
addPendingUpdate(model, sets) {
this._pendingUpdateModels.add(model);
sets?.forEach(set => this._pendingUpdateSets.add(set));
this.pendingUpdateModels.add(model);
sets?.forEach(set => this.pendingUpdateSets.add(set));
}

/**
* Update the journal for the models pending updates.
* Update lifecycle phase has ended
*/
update() {
this._pendingUpdateModels.forEach(model => this._addUpdateModifiers(model));
this._write();
this._pendingUpdateModels.clear();
this._pendingUpdateSets.clear();
this._pendingUpdateModifiers = [];
this.clear();
}

/**
* Returns the minimum score for the specified model.
* @param {Backbone.Model} model
* @returns {number}
* Clear for next pending updates.
*/
getMinScoreByModel(model) {
if (!this.set.questions.includes(model)) return 0;
return model.minScore;
}

/**
* Returns the maximum score for the specified model.
* @param {Backbone.Model} model
* @returns {number}
*/
getMaxScoreByModel(model) {
if (!this.set.questions.includes(model)) return 0;
return model.maxScore;
}

/**
* Returns the score for the specified model.
* @param {Backbone.Model} model
* @returns {number}
*/
getScoreByModel(model) {
if (!this.set.questions.includes(model)) return 0;
return model.score;
}

/**
* Returns the set data to log.
* @returns {object}
*/
get setData() {
return {
id: this.set.id,
type: this.set.type,
minScore: this.set.minScore,
maxScore: this.set.maxScore,
score: this.set.score,
scaledScore: this.set.scaledScore,
isComplete: this.set.isComplete,
isPassed: this.set.isPassed
};
}

/**
* Add modifier details for how the set has been updated.
* @protected
* @param {Backbone.Model} model
*/
_addUpdateModifiers(model) {
const isAvailabilityChange = Object.hasOwn(model.changed, '_isAvailable');
if (isAvailabilityChange) {
this._addAvailabilityModifiers(model);
return;
}
this._addCompletionModifiers(model);
}

/**
* Add modifier details for how the set has been updated by availability changes.
* @protected
* @param {Backbone.Model} model
*/
_addAvailabilityModifiers(model) {
const models = model.hasManagedChildren ? model.getChildren() : [model];
const questions = filterModelsByIntersectingModels(this.set.questions, models);
questions.forEach(questionModel => {
const isAvailable = isModelAvailableInHierarchy(questionModel);
const minScore = this.getMinScoreByModel(questionModel);
const maxScore = this.getMaxScoreByModel(questionModel);
const score = this.getScoreByModel(questionModel);
const data = {
modelId: questionModel.get('_id'),
minScore: isAvailable ? minScore : -minScore,
maxScore: isAvailable ? maxScore : -maxScore
};
if (questionModel.get('_isSubmitted')) data.score = isAvailable ? score : -score;
this._pendingUpdateModifiers.push(data);
});
}

/**
* Add modifier details for how the set has been updated by completion changes.
* @protected
* @param {Backbone.Model} model
*/
_addCompletionModifiers(model) {
this._pendingUpdateModifiers.push({
modelId: model.get('_id'),
score: this.getScoreByModel(model)
});
}

/**
* Write the current state to the log if it has changed since the last update.
* @protected
*/
_write() {
const setData = this.setData;
const hasSetDataChanged = !(_.isEqual(this._lastSetData, setData));
if (!hasSetDataChanged) return;
const data = { ...setData };
if (this._pendingUpdateModifiers.length) data.modifiers = this._pendingUpdateModifiers;
Logging.info('scoring:update', JSON.stringify(data));
this._lastSetData = setData;
clear() {
this.pendingUpdateModels.clear();
this.pendingUpdateSets.clear();
}

}
6 changes: 3 additions & 3 deletions js/ScoringSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Adapt from 'core/js/adapt';
import Logging from 'core/js/logging';
import LifecycleSet from './LifecycleSet';
import Objective from './Objective';
import LifecycleUpdateJournal from './LifecycleUpdateJournal';
import ScoringUpdateJournal from './ScoringUpdateJournal';
import {
getScaledScoreFromMinMax
} from './utils/scoring';
Expand Down Expand Up @@ -243,11 +243,11 @@ export default class ScoringSet extends LifecycleSet {

/**
* The journal for recording the updates to the set.
* @returns {LifecycleUpdateJournal}
* @returns {ScoringUpdateJournal}
*/
get journal() {
if (this.isIntersectedSet) return;
return (this._journal = this._journal || new LifecycleUpdateJournal({ set: this }));
return (this._journal = this._journal || new ScoringUpdateJournal({ set: this }));
}

/**
Expand Down
126 changes: 126 additions & 0 deletions js/ScoringUpdateJournal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import LifecycleUpdateJournal from './LifecycleUpdateJournal';
import {
filterModelsByIntersectingModels,
isModelAvailableInHierarchy
} from './utils/models';
import _ from 'underscore';
import Logging from 'core/js/logging';
/** @typedef {import("./ScoringSet").default} ScoringSet */

/**
* A journal for recording the models and sets that triggered set updates in the current lifecycle.
*/
export default class ScoringUpdateJournal extends LifecycleUpdateJournal {

/**
* Log the updates to the set based on the pending update models and sets, then clear the pending updates.
* @override
*/
update() {
this.log();
this.clear();
}

/**
* Log the updates to the set based on the pending update models and sets, then clear the pending updates.
*/
log() {
const setData = this.setData;
const hasSetDataChanged = !(_.isEqual(this._lastSetData, setData));
if (!hasSetDataChanged) return;
const data = { ...setData };
const sources = this.sourceData;
if (sources.length) {
data.sources = sources;
}
Logging.info('scoring:update', JSON.stringify(data));
this._lastSetData = setData;
}

/**
* Returns the set data to log.
* @returns {object}
*/
get setData() {
return {
id: this.set.id,
type: this.set.type,
minScore: this.set.minScore,
maxScore: this.set.maxScore,
score: this.set.score,
scaledScore: this.set.scaledScore,
isComplete: this.set.isComplete,
isPassed: this.set.isPassed
};
}

/**
* Returns the pending update models score data for logging.
* For availability changes, all intersecting set questions are included with scores negated if now unavailable.
* @returns {{ modelId: string, minScore?: number, maxScore?: number, score?: number }[]}
*/
get sourceData() {
const sources = [];
for (const model of this.pendingUpdateModels) {
const isAvailabilityChange = Object.hasOwn(model.changed, '_isAvailable');
if (isAvailabilityChange) {
// If the parent availability has changed, we log the score
// changes for all current child questions in the set.
const models = model.hasManagedChildren ? model.getChildren() : [model];
const modelSetQuestions = filterModelsByIntersectingModels(this.set.questions, models);
modelSetQuestions.forEach(questionModel => {
const isAvailable = isModelAvailableInHierarchy(questionModel);
const minScore = this.getMinScoreByModel(questionModel);
const maxScore = this.getMaxScoreByModel(questionModel);
const score = this.getScoreByModel(questionModel);
// The score changes are logged as negative values
// if the question is now unavailable.
const data = {
modelId: questionModel.get('_id'),
minScore: isAvailable ? minScore : -minScore,
maxScore: isAvailable ? maxScore : -maxScore
};
if (questionModel.get('_isSubmitted')) data.score = isAvailable ? score : -score;
sources.push(data);
});
continue;
}
sources.push({
modelId: model.get('_id'),
score: this.getScoreByModel(model)
});
}
return sources;
}

/**
* Returns the minimum score for the specified model.
* @param {Backbone.Model} model
* @returns {number}
*/
getMinScoreByModel(model) {
if (!this.set.questions.includes(model)) return 0;
return model.minScore;
}

/**
* Returns the maximum score for the specified model.
* @param {Backbone.Model} model
* @returns {number}
*/
getMaxScoreByModel(model) {
if (!this.set.questions.includes(model)) return 0;
return model.maxScore;
}

/**
* Returns the score for the specified model.
* @param {Backbone.Model} model
* @returns {number}
*/
getScoreByModel(model) {
if (!this.set.questions.includes(model)) return 0;
return model.score;
}

}
Loading