Skip to content

Commit

Permalink
Fix: Added cmi.objectives _recordObjectives and id existence check (f…
Browse files Browse the repository at this point in the history
…ixes #316) (#317)
  • Loading branch information
oliverfoster authored Apr 29, 2024
1 parent df90b8d commit 71e88df
Show file tree
Hide file tree
Showing 7 changed files with 60 additions and 9 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ Determines whether the history of the user's responses to questions should be pe

Determines whether the user's responses to questions should be tracked to the `cmi.interactions` fields of the SCORM data model or not. Acceptable values are `true` or `false`. The default is `true`. Note that not all SCORM 1.2 conformant Learning Management Systems support `cmi.interactions`. The code will attempt to detect whether support is implemented or not and, if not, will fail gracefully. Occasionally the code is unable to detect when `cmi.interactions` are not supported, in those (rare) instances you can switch off interaction tracking using this property so as to avoid 'not supported' errors. You can also switch off interaction tracking for any individual question using the `_recordInteraction` property of question components. All core question components support recording of interactions, community components will not necessarily do so.

##### \_shouldRecordObjectives (boolean)

Determines whether the user's content objects and their statuses should be tracked to the `cmi.objectives` fields of the SCORM data model or not. Acceptable values are `true` or `false`. The default is `true`. Note that not all SCORM 1.2 conformant Learning Management Systems support `cmi.objectives`. The code will attempt to detect whether support is implemented or not and, if not, will fail gracefully. Occasionally the code is unable to detect when `cmi.objectives` are not supported, in those (rare) instances you can switch off objectives using this property so as to avoid 'not supported' errors.

##### \_shouldCompress (boolean)

Allow variable LZMA compress on component state data. The default is `false`.
Expand Down
1 change: 1 addition & 0 deletions example.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"_shouldStoreResponses": true,
"_shouldStoreAttempts": false,
"_shouldRecordInteractions": true,
"_shouldRecordObjectives": true,
"_shouldCompress": false
},
"_reporting": {
Expand Down
4 changes: 4 additions & 0 deletions js/adapt-offlineStorage-scorm.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,16 @@ export default class OfflineStorageScorm extends Backbone.Controller {

switch (name.toLowerCase()) {
case 'interaction':
if (!this.statefulSession.shouldRecordInteractions) return;
return this.scorm.recordInteraction(...args);
case 'objectivedescription':
if (!this.statefulSession.shouldRecordObjectives) return;
return this.scorm.recordObjectiveDescription(...args);
case 'objectivestatus':
if (!this.statefulSession.shouldRecordObjectives) return;
return this.scorm.recordObjectiveStatus(...args);
case 'objectivescore':
if (!this.statefulSession.shouldRecordObjectives) return;
return this.scorm.recordObjectiveScore(...args);
case 'location':
return this.scorm.setLessonLocation(...args);
Expand Down
17 changes: 16 additions & 1 deletion js/adapt-stateful-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,19 @@ export default class StatefulSession extends Backbone.Controller {
this._shouldStoreResponses = true;
this._shouldStoreAttempts = false;
this._shouldRecordInteractions = true;
this._shouldRecordObjectives = true;
this._uniqueInteractionIds = false;
this.beginSession();
}

get shouldRecordInteractions() {
return this._shouldRecordInteractions;
}

get shouldRecordObjectives() {
return this._shouldRecordObjectives;
}

beginSession() {
this.listenTo(Adapt, {
'app:dataReady': this.restoreSession,
Expand All @@ -45,6 +54,9 @@ export default class StatefulSession extends Backbone.Controller {
if (tracking?._shouldRecordInteractions === false) {
this._shouldRecordInteractions = false;
}
if (tracking?._shouldRecordObjectives === false) {
this._shouldRecordObjectives = false;
}
const settings = config._advancedSettings;
if (!settings) {
// force use of SCORM 1.2 by default - some LMSes (SABA/Kallidus for instance)
Expand Down Expand Up @@ -166,6 +178,7 @@ export default class StatefulSession extends Backbone.Controller {
}

initializeContentObjectives() {
if (!this.shouldRecordObjectives) return;
Adapt.contentObjects.forEach(model => {
if (model.isTypeGroup('course')) return;
const id = model.get('_id');
Expand Down Expand Up @@ -195,6 +208,7 @@ export default class StatefulSession extends Backbone.Controller {
}

onPageViewReady(view) {
if (!this.shouldRecordObjectives) return;
const model = view.model;
if (model.get('_isComplete')) return;
const id = model.get('_id');
Expand All @@ -203,7 +217,7 @@ export default class StatefulSession extends Backbone.Controller {
}

onQuestionRecordInteraction(questionView) {
if (!this._shouldRecordInteractions) return;
if (!this.shouldRecordInteractions) return;
if (!this.scorm.isSupported('cmi.interactions._count')) return;
// View functions are deprecated: getResponseType, getResponse, isCorrect, getLatency
const questionModel = questionView.model;
Expand All @@ -221,6 +235,7 @@ export default class StatefulSession extends Backbone.Controller {
}

onContentObjectCompleteChange(model) {
if (!this.shouldRecordObjectives) return;
if (model.isTypeGroup('course')) return;
const id = model.get('_id');
const completionStatus = (model.get('_isComplete') ? COMPLETION_STATE.COMPLETED : COMPLETION_STATE.INCOMPLETE).asLowerCase;
Expand Down
28 changes: 20 additions & 8 deletions js/scorm/wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class ScormWrapper {
this.finishCalled = false;
this.logger = Logger.getInstance();
this.scorm = pipwerks.SCORM;
this.maxCharLimitOverride = null
this.maxCharLimitOverride = null;
/**
* Prevent the Pipwerks SCORM API wrapper's handling of the exit status
*/
Expand Down Expand Up @@ -612,7 +612,7 @@ class ScormWrapper {
const validate = (attribute, value) => {
const isValid = value >= 0 && score <= 100;
if (!isValid) this.logger.warn(`${attribute} must be between 0-100.`);
}
};
validate(`${cmiPrefix}.score.raw`, score);
validate(`${cmiPrefix}.score.min`, minScore);
validate(`${cmiPrefix}.score.max`, maxScore);
Expand Down Expand Up @@ -687,6 +687,15 @@ class ScormWrapper {
return count === '' ? 0 : count;
}

hasObjectiveById(id) {
const count = this.getObjectiveCount();
for (let i = 0; i < count; i++) {
const storedId = this.getValue(`cmi.objectives.${i}.id`);
if (storedId === id) return true;
}
return false;
}

getObjectiveIndexById(id) {
const count = this.getObjectiveCount();
for (let i = 0; i < count; i++) {
Expand All @@ -699,18 +708,20 @@ class ScormWrapper {
recordObjectiveDescription(id, description) {
if (!this.isSCORM2004() || !description) return;
id = id.trim();
const hasObjective = this.hasObjectiveById(id);
const index = this.getObjectiveIndexById(id);
const cmiPrefix = `cmi.objectives.${index}`;
this.setValue(`${cmiPrefix}.id`, id);
if (!hasObjective) this.setValue(`${cmiPrefix}.id`, id);
this.setValue(`${cmiPrefix}.description`, description);
}

recordObjectiveScore(id, score, minScore = 0, maxScore = 100, isPercentageBased = true) {
if (!this.isChildSupported('cmi.objectives.n.id') || !this.isSupported('cmi.objectives._count')) return;
id = id.trim();
const hasObjective = this.hasObjectiveById(id);
const index = this.getObjectiveIndexById(id);
const cmiPrefix = `cmi.objectives.${index}`;
this.setValue(`${cmiPrefix}.id`, id);
if (!hasObjective) this.setValue(`${cmiPrefix}.id`, id);
this.recordScore(cmiPrefix, score, minScore, maxScore, isPercentageBased);
}

Expand All @@ -725,9 +736,10 @@ class ScormWrapper {
return;
}
id = id.trim();
const hasObjective = this.hasObjectiveById(id);
const index = this.getObjectiveIndexById(id);
const cmiPrefix = `cmi.objectives.${index}`;
this.setValue(`${cmiPrefix}.id`, id);
if (!hasObjective) this.setValue(`${cmiPrefix}.id`, id);
if (this.isSCORM2004()) {
this.setValue(`${cmiPrefix}.completion_status`, completionStatus);
this.setValue(`${cmiPrefix}.success_status`, successStatus);
Expand All @@ -741,7 +753,7 @@ class ScormWrapper {
isValidCompletionStatus(status) {
status = status.toLowerCase(); // workaround for some LMSs (e.g. Arena) not adhering to the all-lowercase rule
if (this.isSCORM2004()) {
switch(status) {
switch (status) {
case COMPLETION_STATE.UNKNOWN.asLowerCase:
case COMPLETION_STATE.NOTATTEMPTED.asLowerCase:
case COMPLETION_STATE.NOT_ATTEMPTED.asLowerCase: // mentioned in SCORM 2004 spec - mapped to 'not attempted'
Expand All @@ -750,7 +762,7 @@ class ScormWrapper {
return true;
}
} else {
switch(status) {
switch (status) {
case COMPLETION_STATE.NOTATTEMPTED.asLowerCase:
case COMPLETION_STATE.BROWSED.asLowerCase:
case COMPLETION_STATE.INCOMPLETE.asLowerCase:
Expand All @@ -766,7 +778,7 @@ class ScormWrapper {
isValidSuccessStatus(status) {
status = status.toLowerCase(); // workaround for some LMSs (e.g. Arena) not adhering to the all-lowercase rule
if (this.isSCORM2004()) {
switch(status) {
switch (status) {
case SUCCESS_STATE.UNKNOWN.asLowerCase:
case SUCCESS_STATE.PASSED.asLowerCase:
case SUCCESS_STATE.FAILED.asLowerCase:
Expand Down
9 changes: 9 additions & 0 deletions properties.schema
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@
"validators": [],
"help": "If enabled, the course will record the user's responses to questions to the cmi.interactions SCORM data fields."
},
"_shouldRecordObjectives": {
"type": "boolean",
"required": false,
"default": true,
"title": "Record objectives",
"inputType": "Checkbox",
"validators": [],
"help": "If enabled, the course will be able to record the status and scores of the course objectives to the cmi.objectives SCORM data fields."
},
"_shouldCompress": {
"type": "boolean",
"required": false,
Expand Down
6 changes: 6 additions & 0 deletions schema/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
"title": "Record interactions",
"description": "If enabled, the course will record the user's responses to questions to the cmi.interactions SCORM data fields",
"default": true
},
"_shouldRecordObjectives": {
"type": "boolean",
"title": "Record objectives",
"description": "If enabled, the course will be able to record the status and scores of the course objectives to the cmi.objectives SCORM data fields",
"default": true
}
}
},
Expand Down

0 comments on commit 71e88df

Please sign in to comment.