From 6cc8cfafe758a1ed238a656aaccd56011382d465 Mon Sep 17 00:00:00 2001 From: danielghost Date: Wed, 2 Jul 2025 14:32:24 +0100 Subject: [PATCH 1/3] Update: Use contextActivities API and update unavailable logic for objectives (fixes #20). --- js/ScoringSet.js | 117 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 28 deletions(-) diff --git a/js/ScoringSet.js b/js/ScoringSet.js index 78d4669..77305df 100644 --- a/js/ScoringSet.js +++ b/js/ScoringSet.js @@ -64,7 +64,8 @@ export default class ScoringSet extends Backbone.Controller { * @protected */ _setupListeners() { - if (this.subsetParent) return; + if (this.subsetParent || this.type === 'adapt') return; + this.listenTo(Adapt, 'questionView:submitted', this.onQuestionSubmitted); if (OfflineStorage.ready) return this.restore(); this.listenTo(Adapt, 'offlineStorage:ready', this.restore); } @@ -81,6 +82,8 @@ export default class ScoringSet extends Backbone.Controller { } init() { + this._wasAvailable = this.isAvailable; + this._wasIncomplete = this.isIncomplete; this._wasComplete = this.isComplete; this._wasPassed = this.isPassed; this._initializeObjective(); @@ -91,9 +94,13 @@ export default class ScoringSet extends Backbone.Controller { */ update() { const isComplete = this.isComplete; - if (isComplete && !this._wasComplete) this.onCompleted(); const isPassed = this.isPassed; + if (isComplete && !this._wasComplete) this.onCompleted(); if (isPassed && !this._wasPassed) this.onPassed(); + // don't change the status of a currently completed objective - `isObjectiveComplete` should be controlled accordingly by each set + if (this.hasStatusChanged && !this.isObjectiveComplete) this._setObjectiveStatus(); + this._wasAvailable = this.isAvailable; + this._wasIncomplete = this.isIncomplete; this._wasComplete = isComplete; this._wasPassed = isPassed; } @@ -360,7 +367,7 @@ export default class ScoringSet extends Backbone.Controller { * @returns {boolean} */ get canReset() { - return false + return false; } /** @@ -368,7 +375,7 @@ export default class ScoringSet extends Backbone.Controller { * @returns {boolean} */ get isOptional() { - return false + return false; } /** @@ -376,19 +383,40 @@ export default class ScoringSet extends Backbone.Controller { * @returns {boolean} */ get isAvailable() { - return true + return true; } /** - * Returns whether the set is completed + * Returns whether the set is started * @returns {boolean} */ - get isComplete() { - Logging.error(`isComplete must be overriden for ${this.constructor.name}`); + get isStarted() { + return this.models.some(model => model.get('_isVisited')); } + /** + * Returns whether the set is started and incomplete + * @returns {boolean} + */ get isIncomplete() { - return (this.isComplete === false); + return this.isStarted && !this.isComplete; + } + + /** + * Returns whether the objective for the set is completed. + * Depending on the set logic, this can differ to `_isComplete`. + * @returns {boolean} + */ + get isObjectiveComplete() { + return this.isComplete; + } + + /** + * Returns whether the set is completed + * @returns {boolean} + */ + get isComplete() { + Logging.error(`isComplete must be overriden for ${this.constructor.name}`); } /** @@ -403,18 +431,25 @@ export default class ScoringSet extends Backbone.Controller { return (this.isPassed === false); } + /** + * Check whether the status has changed since the last `update` + * @returns {boolean} + */ + get hasStatusChanged() { + return this.isAvailable !== this._wasAvailable || + this.isIncomplete !== this._wasIncomplete || + this.isComplete !== this._wasComplete || + this.isPassed !== this._wasPassed; + } + /** * Define the objective for reporting purposes * @protected */ _initializeObjective() { - if (this.subsetParent) return; - const id = this.id; - const description = this.title; - const completionStatus = COMPLETION_STATE.NOTATTEMPTED.asLowerCase; - OfflineStorage.set('objectiveDescription', id, description); - if (this.isComplete) return; - OfflineStorage.set('objectiveStatus', id, completionStatus); + if (this.subsetParent || this.isStarted) return; + OfflineStorage.set('objectiveDescription', this.id, this.title); + this._setObjectiveStatus(); } /** @@ -422,25 +457,41 @@ export default class ScoringSet extends Backbone.Controller { * @protected */ _resetObjective() { - if (this.subsetParent || this.isComplete) return; - const id = this.id; - const completionStatus = COMPLETION_STATE.INCOMPLETE.asLowerCase; - OfflineStorage.set('objectiveScore', id, this.score, this.minScore, this.maxScore); - OfflineStorage.set('objectiveStatus', id, completionStatus); + if (this.subsetParent || this.isObjectiveComplete || !this.hasStatusChanged) return; + OfflineStorage.set('objectiveScore', this.id, this.score, this.minScore, this.maxScore); + this._setObjectiveStatus(); } /** - * Complete the objective - * @todo Always updates to latest data - is this desired? + * Complete the objective. + * Will update to the latest data/attempt unless overriden in a subset. * @protected */ _completeObjective() { if (this.subsetParent) return; - const id = this.id; - const completionStatus = COMPLETION_STATE.COMPLETED.asLowerCase; - const successStatus = (this.isPassed ? COMPLETION_STATE.PASSED : COMPLETION_STATE.FAILED).asLowerCase; - OfflineStorage.set('objectiveScore', id, this.score, this.minScore, this.maxScore); - OfflineStorage.set('objectiveStatus', id, completionStatus, successStatus); + OfflineStorage.set('objectiveScore', this.id, this.score, this.minScore, this.maxScore); + this._setObjectiveStatus(); + } + + /** + * Set the appropriate objective completion and success status + * Will update to the latest data/attempt, unless controlled accordingly in a subset. + * @protected + */ + _setObjectiveStatus() { + if (this.subsetParent) return; + const isAvailable = this.isAvailable; + const isIncomplete = this.isIncomplete; + const isComplete = this.isComplete; + let completionStatus = COMPLETION_STATE.UNKNOWN.asLowerCase; + let successStatus = COMPLETION_STATE.UNKNOWN.asLowerCase; + if (isAvailable && !isIncomplete) completionStatus = COMPLETION_STATE.NOTATTEMPTED.asLowerCase; + if (isAvailable && isIncomplete) completionStatus = COMPLETION_STATE.INCOMPLETE.asLowerCase; + if (isAvailable && isComplete) { + completionStatus = COMPLETION_STATE.COMPLETED.asLowerCase; + if (this.passmark.isEnabled) successStatus = (this.isPassed ? COMPLETION_STATE.PASSED : COMPLETION_STATE.FAILED).asLowerCase; + } + OfflineStorage.set('objectiveStatus', this.id, completionStatus, successStatus); } /** @@ -464,4 +515,14 @@ export default class ScoringSet extends Backbone.Controller { Logging.debug(`${this.id} passed`); } + /** + * @param {QuestionView} view + * @listens Adapt#questionView:submitted + */ + onQuestionSubmitted(view) { + const model = view.model; + if (!this.questions.includes(model)) return; + model.addContextActivity(this.id, this.type, this.title); + } + } From 4ddfb4e9574018a10f0debe3fbbd4b507d6f02b2 Mon Sep 17 00:00:00 2001 From: danielghost Date: Wed, 2 Jul 2025 14:36:32 +0100 Subject: [PATCH 2/3] Updated framework version. --- bower.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bower.json b/bower.json index dfa7ca4..878d21f 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "adapt-contrib-scoring", "version": "1.1.0", - "framework": ">=5.31.31", + "framework": ">=5.48.2", "homepage": "https://github.com/adaptlearning/adapt-contrib-scoring", "issues": "https://github.com/adaptlearning/adapt-contrib-scoring/issues/new", "extension": "scoring", diff --git a/package.json b/package.json index dfa7ca4..878d21f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "adapt-contrib-scoring", "version": "1.1.0", - "framework": ">=5.31.31", + "framework": ">=5.48.2", "homepage": "https://github.com/adaptlearning/adapt-contrib-scoring", "issues": "https://github.com/adaptlearning/adapt-contrib-scoring/issues/new", "extension": "scoring", From aa53969cb1d2b3e36525e8aa7e09908eaf58613d Mon Sep 17 00:00:00 2001 From: danielghost Date: Wed, 2 Jul 2025 23:30:32 +0100 Subject: [PATCH 3/3] Amended objective logic to account for multiple availability changes to the same set (e.g. true > false > true) without losing data for the current state. --- js/ScoringSet.js | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/js/ScoringSet.js b/js/ScoringSet.js index 77305df..61db130 100644 --- a/js/ScoringSet.js +++ b/js/ScoringSet.js @@ -82,6 +82,7 @@ export default class ScoringSet extends Backbone.Controller { } init() { + this._setObjectiveStatus = _.debounce(this._setObjectiveStatus, 100); this._wasAvailable = this.isAvailable; this._wasIncomplete = this.isIncomplete; this._wasComplete = this.isComplete; @@ -95,10 +96,9 @@ export default class ScoringSet extends Backbone.Controller { update() { const isComplete = this.isComplete; const isPassed = this.isPassed; - if (isComplete && !this._wasComplete) this.onCompleted(); - if (isPassed && !this._wasPassed) this.onPassed(); - // don't change the status of a currently completed objective - `isObjectiveComplete` should be controlled accordingly by each set - if (this.hasStatusChanged && !this.isObjectiveComplete) this._setObjectiveStatus(); + if (isComplete && !this._wasComplete && this._wasAvailable) this.onCompleted(); + if (isPassed && !this._wasPassed && this._wasAvailable) this.onPassed(); + if (this.hasStatusChanged) this._setObjectiveStatus(); this._wasAvailable = this.isAvailable; this._wasIncomplete = this.isIncomplete; this._wasComplete = isComplete; @@ -419,6 +419,15 @@ export default class ScoringSet extends Backbone.Controller { Logging.error(`isComplete must be overriden for ${this.constructor.name}`); } + /** + * Returns whether the objective for the set is passed. + * Depending on the set logic, this can differ to `isPassed`. + * @returns {boolean} + */ + get isObjectivePassed() { + return this.isPassed; + } + /** * Returns whether the configured passmark has been achieved * @returns {boolean} @@ -458,7 +467,7 @@ export default class ScoringSet extends Backbone.Controller { */ _resetObjective() { if (this.subsetParent || this.isObjectiveComplete || !this.hasStatusChanged) return; - OfflineStorage.set('objectiveScore', this.id, this.score, this.minScore, this.maxScore); + this._setObjectiveScore(); this._setObjectiveStatus(); } @@ -469,12 +478,21 @@ export default class ScoringSet extends Backbone.Controller { */ _completeObjective() { if (this.subsetParent) return; - OfflineStorage.set('objectiveScore', this.id, this.score, this.minScore, this.maxScore); + this._setObjectiveScore(); this._setObjectiveStatus(); } /** - * Set the appropriate objective completion and success status + * Set the objective score + * @protected + */ + _setObjectiveScore() { + if (this.subsetParent) return; + OfflineStorage.set('objectiveScore', this.id, this.score, this.minScore, this.maxScore); + } + + /** + * Set the appropriate objective completion and success status. * Will update to the latest data/attempt, unless controlled accordingly in a subset. * @protected */ @@ -482,14 +500,15 @@ export default class ScoringSet extends Backbone.Controller { if (this.subsetParent) return; const isAvailable = this.isAvailable; const isIncomplete = this.isIncomplete; - const isComplete = this.isComplete; + const isComplete = this.isObjectiveComplete; + const isPassed = this.isObjectivePassed; let completionStatus = COMPLETION_STATE.UNKNOWN.asLowerCase; let successStatus = COMPLETION_STATE.UNKNOWN.asLowerCase; if (isAvailable && !isIncomplete) completionStatus = COMPLETION_STATE.NOTATTEMPTED.asLowerCase; if (isAvailable && isIncomplete) completionStatus = COMPLETION_STATE.INCOMPLETE.asLowerCase; if (isAvailable && isComplete) { completionStatus = COMPLETION_STATE.COMPLETED.asLowerCase; - if (this.passmark.isEnabled) successStatus = (this.isPassed ? COMPLETION_STATE.PASSED : COMPLETION_STATE.FAILED).asLowerCase; + if (this.passmark.isEnabled) successStatus = (isPassed ? COMPLETION_STATE.PASSED : COMPLETION_STATE.FAILED).asLowerCase; } OfflineStorage.set('objectiveStatus', this.id, completionStatus, successStatus); }