Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: Added _uniqueInteractionIds, testing support for cmi.interactions #278

Merged
merged 7 commits into from
Jun 14, 2023
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
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');
oliverfoster marked this conversation as resolved.
Show resolved Hide resolved
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