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/js/ScoringSet.js b/js/ScoringSet.js index 7df8cf2..61db130 100644 --- a/js/ScoringSet.js +++ b/js/ScoringSet.js @@ -45,8 +45,7 @@ export default class ScoringSet extends Backbone.Controller { this._title = title; this._isScoreIncluded = _isScoreIncluded; this._isCompletionRequired = _isCompletionRequired; - // only register root sets as subsets are dynamically created when required - if (!this.subsetParent) this.register(); + this.register(); this._setupListeners(); } @@ -56,6 +55,7 @@ export default class ScoringSet extends Backbone.Controller { * @fires Adapt#scoring:set:register */ register() { + if (this.subsetParent) return; Adapt.scoring.register(this); Adapt.trigger(`scoring:${this.type}:register scoring:set:register`, this); } @@ -64,6 +64,8 @@ export default class ScoringSet extends Backbone.Controller { * @protected */ _setupListeners() { + 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); } @@ -75,10 +77,14 @@ export default class ScoringSet extends Backbone.Controller { * @fires Adapt#scoring:set:restored */ restore() { + if (this.subsetParent) return; Adapt.trigger(`scoring:${this.type}:restored scoring:set:restored`, this); } init() { + this._setObjectiveStatus = _.debounce(this._setObjectiveStatus, 100); + this._wasAvailable = this.isAvailable; + this._wasIncomplete = this.isIncomplete; this._wasComplete = this.isComplete; this._wasPassed = this.isPassed; this._initializeObjective(); @@ -89,9 +95,12 @@ export default class ScoringSet extends Backbone.Controller { */ update() { const isComplete = this.isComplete; - if (isComplete && !this._wasComplete) this.onCompleted(); const isPassed = this.isPassed; - if (isPassed && !this._wasPassed) this.onPassed(); + 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; this._wasPassed = isPassed; } @@ -102,6 +111,7 @@ export default class ScoringSet extends Backbone.Controller { * @fires Adapt#scoring:set:reset */ reset() { + if (this.subsetParent) return; Adapt.trigger(`scoring:${this.type}:reset scoring:set:reset`, this); Logging.debug(`${this.id} reset`); this._resetObjective(); @@ -357,7 +367,7 @@ export default class ScoringSet extends Backbone.Controller { * @returns {boolean} */ get canReset() { - return false + return false; } /** @@ -365,7 +375,7 @@ export default class ScoringSet extends Backbone.Controller { * @returns {boolean} */ get isOptional() { - return false + return false; } /** @@ -373,7 +383,32 @@ export default class ScoringSet extends Backbone.Controller { * @returns {boolean} */ get isAvailable() { - return true + return true; + } + + /** + * Returns whether the set is started + * @returns {boolean} + */ + get isStarted() { + return this.models.some(model => model.get('_isVisited')); + } + + /** + * Returns whether the set is started and incomplete + * @returns {boolean} + */ + get isIncomplete() { + 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; } /** @@ -384,8 +419,13 @@ export default class ScoringSet extends Backbone.Controller { Logging.error(`isComplete must be overriden for ${this.constructor.name}`); } - get isIncomplete() { - return (this.isComplete === false); + /** + * 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; } /** @@ -400,18 +440,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(); } /** @@ -419,25 +466,51 @@ 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; + this._setObjectiveScore(); + 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); + this._setObjectiveScore(); + this._setObjectiveStatus(); + } + + /** + * 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 + */ + _setObjectiveStatus() { + if (this.subsetParent) return; + const isAvailable = this.isAvailable; + const isIncomplete = this.isIncomplete; + 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 = (isPassed ? COMPLETION_STATE.PASSED : COMPLETION_STATE.FAILED).asLowerCase; + } + OfflineStorage.set('objectiveStatus', this.id, completionStatus, successStatus); } /** @@ -445,6 +518,7 @@ export default class ScoringSet extends Backbone.Controller { * @fires Adapt#scoring:set:complete */ onCompleted() { + if (this.subsetParent) return; Adapt.trigger(`scoring:${this.type}:complete scoring:set:complete`, this); Logging.debug(`${this.id} completed`); this._completeObjective(); @@ -455,8 +529,19 @@ export default class ScoringSet extends Backbone.Controller { * @fires Adapt#scoring:set:passed */ onPassed() { + if (this.subsetParent) return; Adapt.trigger(`scoring:${this.type}:passed scoring:set:passed`, this); 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); + } + } 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",