Skip to content

Commit

Permalink
New: Added _uniqueInteractionIds, testing support for cmi.interactions (
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverfoster authored Jun 14, 2023
1 parent 9a46faa commit f88877c
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 67 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ Determines whether a reset button will be available to relaunch the course and o
#### \_shouldPersistCookieLMSData (boolean):
Determines whether to persist the cookie data over browser sessions (scorm_test_harness.html only). The default is `true`.

#### \_uniqueInteractionIds (boolean):
Determines whether `cmi.interactions.n.id` will be prepended with an index, making the id unique. Some LMSes require unique ids, this will inhibit the grouping of interactions by id on the server-side.

<div float align=right><a href="#top">Back to Top</a></div>

## Notes
Expand Down
3 changes: 2 additions & 1 deletion example.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"_testOnSetValue": true,
"_silentRetryLimit": 0,
"_silentRetryDelay": 2000
}
},
"_uniqueInteractionIds": false
},
"_showCookieLmsResetButton": false,
"_shouldPersistCookieLMSData": true
Expand Down
6 changes: 5 additions & 1 deletion js/adapt-stateful-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default class StatefulSession extends Backbone.Controller {
this._shouldStoreResponses = true;
this._shouldStoreAttempts = false;
this._shouldRecordInteractions = true;
this._uniqueInteractionIds = false;
this.beginSession();
}

Expand Down Expand Up @@ -51,6 +52,7 @@ export default class StatefulSession extends Backbone.Controller {
this.scorm.initialize();
return;
}
this._uniqueInteractionIds = settings._uniqueInteractionIds || false;
this.scorm.initialize(settings);
}

Expand Down Expand Up @@ -177,7 +179,9 @@ export default class StatefulSession extends Backbone.Controller {
// If responseType doesn't contain any data, assume that the question
// component hasn't been set up for cmi.interaction tracking
if (_.isEmpty(responseType)) return;
const id = `${this.scorm.getInteractionCount()}-${questionModel.get('_id')}`;
const id = this._uniqueInteractionIds
? `${this.scorm.getInteractionCount()}-${questionModel.get('_id')}`
: questionModel.get('_id');
const response = (questionModel.getResponse ? questionModel.getResponse() : questionView.getResponse());
const result = (questionModel.isCorrect ? questionModel.isCorrect() : questionView.isCorrect());
const latency = (questionModel.getLatency ? questionModel.getLatency() : questionView.getLatency());
Expand Down
160 changes: 95 additions & 65 deletions js/scorm/cookieLMS.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,33 @@ export const shouldStart = (Object.prototype.hasOwnProperty.call(window, 'ISCOOK
/** Store the data in a cookie if window.ISCOOKIELMS is true, otherwise setup the API without storing data. */
export const isStoringData = (window.ISCOOKIELMS === true);

/**
* Store value nested inside object at given path
* @param {Object} object Root of hierarchy
* @param {string} path Period separated key names
* @param {*} value Value to store at final path
*/
export const set = (object, path, value) => {
const keys = path.split('.');
const initialKeys = keys.slice(0, -1);
const lastKey = keys[keys.length - 1];
const finalObject = initialKeys.reduce((object, key) => {
return (object[key] = object?.[key] || {});
}, object);
finalObject[lastKey] = value;
};

/**
* Fetch value nested inside object at given path
* @param {Object} object
* @param {string} path Period separated key names
* @returns
*/
export const get = (object, path) => {
const keys = path.split('.');
return keys.reduce((object, key) => object?.[key], object);
};

export function createResetButton() {
const resetButtonStyle = '<style id="spoor-clear-button">.spoor-reset-button { position:fixed; right:0px; bottom:0px; } </style>';
const resetButton = '<button class="spoor-reset-button btn-text">Reset</button>';
Expand Down Expand Up @@ -53,7 +80,7 @@ export function start () {

__offlineAPIWrapper: true,

store: function(force) {
store(force) {
if (!isStoringData) return;

if (!force && Cookies.get('_spoor') === undefined) return;
Expand All @@ -64,72 +91,101 @@ export function start () {
if (Cookies.get('_spoor').length !== JSON.stringify(this.data).length) postStorageWarning();
},

fetch: function() {
initialize(defaults = {}) {
if (!isStoringData) {
this.data = {};
Object.entries(defaults).forEach(([path, value]) => set(this.data, path, value));
return;
}

this.data = Cookies.getJSON('_spoor');

if (!this.data) {
this.data = {};
Object.entries(defaults).forEach(([path, value]) => set(this.data, path, value));
this.store(true);
return false;
}

const entries = Object.entries(this.data);
const isUsingLegacyKeys = (entries[0][0].includes('.'));
if (isUsingLegacyKeys) {
/**
* convert from: cmi.student_name = ''
* to: { cmi: { student_name: '' } }
*/
const reworked = {};
Object.entries(defaults).forEach(([path, value]) => set(reworked, path, value));
Object.entries(entries).forEach(([path, value]) => set(reworked, path, value));
this.data = reworked;
this.store(true);
}

return true;
}

};

// SCORM 1.2 API
window.API = {
const SCORM1_2 = window.API = {

...GenericAPI,

LMSInitialize: function() {
LMSInitialize() {
configure();
if (!this.fetch()) {
this.data['cmi.core.lesson_status'] = 'not attempted';
this.data['cmi.suspend_data'] = '';
this.data['cmi.core.student_name'] = 'Surname, Sam';
this.data['cmi.core.student_id'] = 'sam.surname@example.org';
this.store(true);
}
this.initialize({
'cmi.interactions': [],
'cmi.core.lesson_status': 'not attempted',
'cmi.suspend_data': '',
'cmi.core.student_name': 'Surname, Sam',
'cmi.core.student_id': 'sam.surname@example.org'
});
return 'true';
},

LMSFinish: function() {
LMSFinish() {
return 'true';
},

LMSGetValue: function(key) {
return this.data[key];
LMSGetValue(path) {
const value = get(this.data, path);
const keys = path.split('.');
const firstKey = keys[0];
const lastKey = keys[keys.length - 1];
if (firstKey === 'cmi' && lastKey === '_count') {
// Treat requests for cmi.*._count as an array length query
const arrayPath = keys.slice(0, -1).join('.');
return get(this.data, arrayPath)?.length ?? 0;
}
return value;
},

LMSSetValue: function(key, value) {
const str = 'cmi.interactions.';
if (key.indexOf(str) !== -1) return 'true';

this.data[key] = value;

LMSSetValue(path, value) {
const keys = path.split('.');
const firstKey = keys[0];
const lastKey = keys[keys.length - 1];
if (firstKey === 'cmi' && lastKey === '_count') {
// Fail silently
return 'true';
}
set(this.data, path, value);
this.store();
return 'true';
},

LMSCommit: function() {
LMSCommit() {
return 'true';
},

LMSGetLastError: function() {
LMSGetLastError() {
return 0;
},

LMSGetErrorString: function() {
LMSGetErrorString() {
return 'Fake error string.';
},

LMSGetDiagnostic: function() {
LMSGetDiagnostic() {
return 'Fake diagnostic information.';
}
};
Expand All @@ -139,51 +195,25 @@ export function start () {

...GenericAPI,

Initialize: function() {
Initialize() {
configure();
if (!this.fetch()) {
this.data['cmi.completion_status'] = 'not attempted';
this.data['cmi.suspend_data'] = '';
this.data['cmi.learner_name'] = 'Surname, Sam';
this.data['cmi.learner_id'] = 'sam.surname@example.org';
this.store(true);
}
this.initialize({
'cmi.interactions': [],
'cmi.completion_status': 'not attempted',
'cmi.suspend_data': '',
'cmi.learner_name': 'Surname, Sam',
'cmi.learner_id': 'sam.surname@example.org'
});
return 'true';
},

Terminate: function() {
return 'true';
},

GetValue: function(key) {
return this.data[key];
},

SetValue: function(key, value) {
const str = 'cmi.interactions.';
if (key.indexOf(str) !== -1) return 'true';

this.data[key] = value;

this.store();
return 'true';
},

Commit: function() {
return 'true';
},

GetLastError: function() {
return 0;
},

GetErrorString: function() {
return 'Fake error string.';
},

GetDiagnostic: function() {
return 'Fake diagnostic information.';
}
Terminate: SCORM1_2.LMSFinish,
GetValue: SCORM1_2.LMSGetValue,
SetValue: SCORM1_2.LMSSetValue,
Commit: SCORM1_2.LMSCommit,
GetLastError: SCORM1_2.LMSGetLastError,
GetErrorString: SCORM1_2.LMSGetErrorString,
GetDiagnostic: SCORM1_2.LMSGetDiagnostic

};
}
9 changes: 9 additions & 0 deletions properties.schema
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,15 @@
"help": "The interval in milliseconds between silent connection retries."
}
}
},
"_uniqueInteractionIds": {
"type": "boolean",
"required": false,
"default": false,
"title": "Unique Interaction Ids",
"inputType": "Checkbox",
"validators": [],
"help": "If enabled, `cmi.interactions.n.id` will be prepended with an index, making the id unique. Some LMSes require unique ids, this will inhibit the grouping of interactions by id on the server-side."
}
}
},
Expand Down
6 changes: 6 additions & 0 deletions schema/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@
"default": 1000
}
}
},
"_uniqueInteractionIds": {
"type": "boolean",
"title": "Unique Interaction Ids",
"description": "If enabled, `cmi.interactions.n.id` will be prepended with an index, making the id unique. Some LMSes require unique ids, this will inhibit the grouping of interactions by id on the server-side.",
"default": false
}
}
},
Expand Down

0 comments on commit f88877c

Please sign in to comment.