Working on the saving of data

This commit is contained in:
Jonathan Putney
2019-11-15 17:24:30 -05:00
parent 51da89f737
commit 8e6f6c47e9
11 changed files with 559 additions and 75 deletions

View File

@@ -13,9 +13,16 @@ import {
export default class AICC extends Scorm12API { export default class AICC extends Scorm12API {
/** /**
* Constructor to create AICC API object * Constructor to create AICC API object
* @param {object} settings
*/ */
constructor() { constructor(settings: {}) {
super(); const finalSettings = {
...{
mastery_override: false,
}, ...settings,
};
super(finalSettings);
this.cmi = new CMI(); this.cmi = new CMI();
this.nav = new NAV(); this.nav = new NAV();

View File

@@ -1,19 +1,8 @@
// @flow // @flow
import {CMIArray} from './cmi/common'; import {CMIArray} from './cmi/common';
import {ValidationError} from './exceptions'; import {ValidationError} from './exceptions';
import {scorm12_error_codes} from './constants/error_codes';
const api_constants = { import {global_constants} from './constants/api_constants';
SCORM_TRUE: 'true',
SCORM_FALSE: 'false',
STATE_NOT_INITIALIZED: 0,
STATE_INITIALIZED: 1,
STATE_TERMINATED: 2,
LOG_LEVEL_DEBUG: 1,
LOG_LEVEL_INFO: 2,
LOG_LEVEL_WARNING: 3,
LOG_LEVEL_ERROR: 4,
LOG_LEVEL_NONE: 5,
};
/** /**
* Base API class for AICC, SCORM 1.2, and SCORM 2004. Should be considered * Base API class for AICC, SCORM 1.2, and SCORM 2004. Should be considered
@@ -22,24 +11,34 @@ const api_constants = {
export default class BaseAPI { export default class BaseAPI {
#timeout; #timeout;
#error_codes; #error_codes;
#settings = {
autocommit: false,
autocommitSeconds: 60,
lmsCommitUrl: false,
dataCommitFormat: 'json', // valid formats are 'json' or 'flattened', 'params'
};
cmi; cmi;
startingData: {};
/** /**
* Constructor for Base API class. Sets some shared API fields, as well as * Constructor for Base API class. Sets some shared API fields, as well as
* sets up options for the API. * sets up options for the API.
* @param {object} error_codes * @param {object} error_codes
* @param {object} settings
*/ */
constructor(error_codes) { constructor(error_codes, settings) {
if (new.target === BaseAPI) { if (new.target === BaseAPI) {
throw new TypeError('Cannot construct BaseAPI instances directly'); throw new TypeError('Cannot construct BaseAPI instances directly');
} }
this.currentState = api_constants.STATE_NOT_INITIALIZED; this.currentState = global_constants.STATE_NOT_INITIALIZED;
this.apiLogLevel = api_constants.LOG_LEVEL_ERROR; this.apiLogLevel = global_constants.LOG_LEVEL_ERROR;
this.lastErrorCode = 0; this.lastErrorCode = 0;
this.listenerArray = []; this.listenerArray = [];
this.#timeout = null; this.#timeout = null;
this.#error_codes = error_codes; this.#error_codes = error_codes;
this.settings = settings;
} }
/** /**
@@ -53,26 +52,42 @@ export default class BaseAPI {
callbackName: String, callbackName: String,
initializeMessage?: String, initializeMessage?: String,
terminationMessage?: String) { terminationMessage?: String) {
let returnValue = api_constants.SCORM_FALSE; let returnValue = global_constants.SCORM_FALSE;
if (this.isInitialized()) { if (this.isInitialized()) {
this.throwSCORMError(this.#error_codes.INITIALIZED, initializeMessage); this.throwSCORMError(this.#error_codes.INITIALIZED, initializeMessage);
} else if (this.isTerminated()) { } else if (this.isTerminated()) {
this.throwSCORMError(this.#error_codes.TERMINATED, terminationMessage); this.throwSCORMError(this.#error_codes.TERMINATED, terminationMessage);
} else { } else {
this.currentState = api_constants.STATE_INITIALIZED; this.currentState = global_constants.STATE_INITIALIZED;
this.lastErrorCode = 0; this.lastErrorCode = 0;
returnValue = api_constants.SCORM_TRUE; returnValue = global_constants.SCORM_TRUE;
this.processListeners(callbackName); this.processListeners(callbackName);
} }
this.apiLog(callbackName, null, 'returned: ' + returnValue, this.apiLog(callbackName, null, 'returned: ' + returnValue,
api_constants.LOG_LEVEL_INFO); global_constants.LOG_LEVEL_INFO);
this.clearSCORMError(returnValue); this.clearSCORMError(returnValue);
return returnValue; return returnValue;
} }
/**
* Getter for #settings
* @return {object}
*/
get settings() {
return this.#settings;
}
/**
* Setter for #settings
* @param {object} settings
*/
set settings(settings: Object) {
this.#settings = {...this.#settings, ...settings};
}
/** /**
* Terminates the current run of the API * Terminates the current run of the API
* @param {string} callbackName * @param {string} callbackName
@@ -82,19 +97,19 @@ export default class BaseAPI {
terminate( terminate(
callbackName: String, callbackName: String,
checkTerminated: boolean) { checkTerminated: boolean) {
let returnValue = api_constants.SCORM_FALSE; let returnValue = global_constants.SCORM_FALSE;
if (this.checkState(checkTerminated, if (this.checkState(checkTerminated,
this.#error_codes.TERMINATION_BEFORE_INIT, this.#error_codes.TERMINATION_BEFORE_INIT,
this.#error_codes.MULTIPLE_TERMINATION)) { this.#error_codes.MULTIPLE_TERMINATION)) {
if (checkTerminated) this.lastErrorCode = 0; if (checkTerminated) this.lastErrorCode = 0;
this.currentState = api_constants.STATE_TERMINATED; this.currentState = global_constants.STATE_TERMINATED;
returnValue = api_constants.SCORM_TRUE; returnValue = global_constants.SCORM_TRUE;
this.processListeners(callbackName); this.processListeners(callbackName);
} }
this.apiLog(callbackName, null, 'returned: ' + returnValue, this.apiLog(callbackName, null, 'returned: ' + returnValue,
api_constants.LOG_LEVEL_INFO); global_constants.LOG_LEVEL_INFO);
this.clearSCORMError(returnValue); this.clearSCORMError(returnValue);
return returnValue; return returnValue;
@@ -123,7 +138,7 @@ export default class BaseAPI {
} }
this.apiLog(callbackName, CMIElement, ': returned: ' + returnValue, this.apiLog(callbackName, CMIElement, ': returned: ' + returnValue,
api_constants.LOG_LEVEL_INFO); global_constants.LOG_LEVEL_INFO);
this.clearSCORMError(returnValue); this.clearSCORMError(returnValue);
return returnValue; return returnValue;
@@ -143,7 +158,7 @@ export default class BaseAPI {
checkTerminated: boolean, checkTerminated: boolean,
CMIElement, CMIElement,
value) { value) {
let returnValue = api_constants.SCORM_FALSE; let returnValue = global_constants.SCORM_FALSE;
if (this.checkState(checkTerminated, this.#error_codes.STORE_BEFORE_INIT, if (this.checkState(checkTerminated, this.#error_codes.STORE_BEFORE_INIT,
this.#error_codes.STORE_AFTER_TERM)) { this.#error_codes.STORE_AFTER_TERM)) {
@@ -153,7 +168,7 @@ export default class BaseAPI {
} catch (e) { } catch (e) {
if (e instanceof ValidationError) { if (e instanceof ValidationError) {
this.lastErrorCode = e.errorCode; this.lastErrorCode = e.errorCode;
returnValue = api_constants.SCORM_FALSE; returnValue = global_constants.SCORM_FALSE;
} else { } else {
this.throwSCORMError(this.#error_codes.GENERAL); this.throwSCORMError(this.#error_codes.GENERAL);
} }
@@ -162,12 +177,20 @@ export default class BaseAPI {
} }
if (returnValue === undefined) { if (returnValue === undefined) {
returnValue = api_constants.SCORM_FALSE; returnValue = global_constants.SCORM_FALSE;
}
// If we didn't have any errors while setting the data, go ahead and
// schedule a commit, if autocommit is turned on
if (String(this.lastErrorCode) === '0') {
if (this.#settings.autocommit && this.#timeout === undefined) {
this.scheduleCommit(this.#settings.autocommitSeconds * 1000);
}
} }
this.apiLog(callbackName, CMIElement, this.apiLog(callbackName, CMIElement,
': ' + value + ': result: ' + returnValue, ': ' + value + ': result: ' + returnValue,
api_constants.LOG_LEVEL_INFO); global_constants.LOG_LEVEL_INFO);
this.clearSCORMError(returnValue); this.clearSCORMError(returnValue);
return returnValue; return returnValue;
@@ -182,17 +205,29 @@ export default class BaseAPI {
commit( commit(
callbackName: String, callbackName: String,
checkTerminated: boolean) { checkTerminated: boolean) {
let returnValue = api_constants.SCORM_FALSE; this.clearScheduledCommit();
let returnValue = global_constants.SCORM_FALSE;
if (this.checkState(checkTerminated, this.#error_codes.COMMIT_BEFORE_INIT, if (this.checkState(checkTerminated, this.#error_codes.COMMIT_BEFORE_INIT,
this.#error_codes.COMMIT_AFTER_TERM)) { this.#error_codes.COMMIT_AFTER_TERM)) {
const result = this.storeData(false);
if (result.errorCode && result.errorCode > 0) {
this.throwSCORMError(result.errorCode);
}
returnValue = result.result ?
result.result : global_constants.SCORM_FALSE;
this.apiLog(callbackName, 'HttpRequest', ' Result: ' + returnValue,
global_constants.LOG_LEVEL_DEBUG);
if (checkTerminated) this.lastErrorCode = 0; if (checkTerminated) this.lastErrorCode = 0;
returnValue = api_constants.SCORM_TRUE;
this.processListeners(callbackName); this.processListeners(callbackName);
} }
this.apiLog(callbackName, null, 'returned: ' + returnValue, this.apiLog(callbackName, null, 'returned: ' + returnValue,
api_constants.LOG_LEVEL_INFO); global_constants.LOG_LEVEL_INFO);
this.clearSCORMError(returnValue); this.clearSCORMError(returnValue);
return returnValue; return returnValue;
@@ -209,7 +244,7 @@ export default class BaseAPI {
this.processListeners(callbackName); this.processListeners(callbackName);
this.apiLog(callbackName, null, 'returned: ' + returnValue, this.apiLog(callbackName, null, 'returned: ' + returnValue,
api_constants.LOG_LEVEL_INFO); global_constants.LOG_LEVEL_INFO);
return returnValue; return returnValue;
} }
@@ -230,7 +265,7 @@ export default class BaseAPI {
} }
this.apiLog(callbackName, null, 'returned: ' + returnValue, this.apiLog(callbackName, null, 'returned: ' + returnValue,
api_constants.LOG_LEVEL_INFO); global_constants.LOG_LEVEL_INFO);
return returnValue; return returnValue;
} }
@@ -251,7 +286,7 @@ export default class BaseAPI {
} }
this.apiLog(callbackName, null, 'returned: ' + returnValue, this.apiLog(callbackName, null, 'returned: ' + returnValue,
api_constants.LOG_LEVEL_INFO); global_constants.LOG_LEVEL_INFO);
return returnValue; return returnValue;
} }
@@ -296,13 +331,13 @@ export default class BaseAPI {
if (messageLevel >= this.apiLogLevel) { if (messageLevel >= this.apiLogLevel) {
switch (messageLevel) { switch (messageLevel) {
case api_constants.LOG_LEVEL_ERROR: case global_constants.LOG_LEVEL_ERROR:
console.error(logMessage); console.error(logMessage);
break; break;
case api_constants.LOG_LEVEL_WARNING: case global_constants.LOG_LEVEL_WARNING:
console.warn(logMessage); console.warn(logMessage);
break; break;
case api_constants.LOG_LEVEL_INFO: case global_constants.LOG_LEVEL_INFO:
console.info(logMessage); console.info(logMessage);
break; break;
} }
@@ -426,12 +461,12 @@ export default class BaseAPI {
_commonSetCMIValue( _commonSetCMIValue(
methodName: String, scorm2004: boolean, CMIElement, value) { methodName: String, scorm2004: boolean, CMIElement, value) {
if (!CMIElement || CMIElement === '') { if (!CMIElement || CMIElement === '') {
return api_constants.SCORM_FALSE; return global_constants.SCORM_FALSE;
} }
const structure = CMIElement.split('.'); const structure = CMIElement.split('.');
let refObject = this; let refObject = this;
let returnValue = api_constants.SCORM_FALSE; let returnValue = global_constants.SCORM_FALSE;
let foundFirstIndex = false; let foundFirstIndex = false;
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`; const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
@@ -455,7 +490,7 @@ export default class BaseAPI {
if (!scorm2004 || this.lastErrorCode === 0) { if (!scorm2004 || this.lastErrorCode === 0) {
refObject[attribute] = value; refObject[attribute] = value;
returnValue = api_constants.SCORM_TRUE; returnValue = global_constants.SCORM_TRUE;
} }
} }
} else { } else {
@@ -496,10 +531,10 @@ export default class BaseAPI {
} }
} }
if (returnValue === api_constants.SCORM_FALSE) { if (returnValue === global_constants.SCORM_FALSE) {
this.apiLog(methodName, null, this.apiLog(methodName, null,
`There was an error setting the value for: ${CMIElement}, value of: ${value}`, `There was an error setting the value for: ${CMIElement}, value of: ${value}`,
api_constants.LOG_LEVEL_WARNING); global_constants.LOG_LEVEL_WARNING);
} }
return returnValue; return returnValue;
@@ -604,9 +639,9 @@ export default class BaseAPI {
if (refObject === null || refObject === undefined) { if (refObject === null || refObject === undefined) {
if (!scorm2004) { if (!scorm2004) {
if (attribute === '_children') { if (attribute === '_children') {
this.throwSCORMError(202); this.throwSCORMError(scorm12_error_codes.CHILDREN_ERROR);
} else if (attribute === '_count') { } else if (attribute === '_count') {
this.throwSCORMError(203); this.throwSCORMError(scorm12_error_codes.COUNT_ERROR);
} }
} }
} else { } else {
@@ -620,7 +655,7 @@ export default class BaseAPI {
* @return {boolean} * @return {boolean}
*/ */
isInitialized() { isInitialized() {
return this.currentState === api_constants.STATE_INITIALIZED; return this.currentState === global_constants.STATE_INITIALIZED;
} }
/** /**
@@ -629,7 +664,7 @@ export default class BaseAPI {
* @return {boolean} * @return {boolean}
*/ */
isNotInitialized() { isNotInitialized() {
return this.currentState === api_constants.STATE_NOT_INITIALIZED; return this.currentState === global_constants.STATE_NOT_INITIALIZED;
} }
/** /**
@@ -638,7 +673,7 @@ export default class BaseAPI {
* @return {boolean} * @return {boolean}
*/ */
isTerminated() { isTerminated() {
return this.currentState === api_constants.STATE_TERMINATED; return this.currentState === global_constants.STATE_TERMINATED;
} }
/** /**
@@ -702,7 +737,7 @@ export default class BaseAPI {
} }
this.apiLog('throwSCORMError', null, errorNumber + ': ' + message, this.apiLog('throwSCORMError', null, errorNumber + ': ' + message,
api_constants.LOG_LEVEL_ERROR); global_constants.LOG_LEVEL_ERROR);
this.lastErrorCode = String(errorNumber); this.lastErrorCode = String(errorNumber);
} }
@@ -713,11 +748,24 @@ export default class BaseAPI {
* @param {string} success * @param {string} success
*/ */
clearSCORMError(success: String) { clearSCORMError(success: String) {
if (success !== undefined && success !== api_constants.SCORM_FALSE) { if (success !== undefined && success !== global_constants.SCORM_FALSE) {
this.lastErrorCode = 0; this.lastErrorCode = 0;
} }
} }
/**
* Attempts to store the data to the LMS, logs data if no LMS configured
* APIs that inherit BaseAPI should override this function
*
* @param {boolean} _calculateTotalTime
* @return {string}
* @abstract
*/
storeData(_calculateTotalTime) {
throw new Error(
'The storeData method has not been implemented');
}
/** /**
* Loads CMI data from a JSON object. * Loads CMI data from a JSON object.
* *
@@ -733,6 +781,8 @@ export default class BaseAPI {
CMIElement = CMIElement || 'cmi'; CMIElement = CMIElement || 'cmi';
this.startingData = json;
for (const key in json) { for (const key in json) {
if ({}.hasOwnProperty.call(json, key) && json[key]) { if ({}.hasOwnProperty.call(json, key) && json[key]) {
const currentCMIElement = CMIElement + '.' + key; const currentCMIElement = CMIElement + '.' + key;
@@ -769,10 +819,22 @@ export default class BaseAPI {
* @return {object} * @return {object}
*/ */
renderCMIToJSONObject() { renderCMIToJSONObject() {
const cmi = this.cmi;
// Do we want/need to return fields that have no set value? // Do we want/need to return fields that have no set value?
// return JSON.stringify({ cmi }, (k, v) => v === undefined ? null : v, 2); // return JSON.stringify({ cmi }, (k, v) => v === undefined ? null : v, 2);
return JSON.parse(JSON.stringify(cmi)); return JSON.parse(this.renderCMIToJSONString());
}
/**
* Render the cmi object to the proper format for LMS commit
* APIs that inherit BaseAPI should override this function
*
* @param {boolean} _terminateCommit
* @return {*}
* @abstract
*/
renderCommitCMI(_terminateCommit) {
throw new Error(
'The storeData method has not been implemented');
} }
/** /**
@@ -783,6 +845,33 @@ export default class BaseAPI {
this.cmi.getCurrentTotalTime(); this.cmi.getCurrentTotalTime();
} }
/**
* Send the request to the LMS
* @param {string} url
* @param {object|Array} params
* @return {object}
*/
processHttpRequest(url: String, params) {
const httpReq = new XMLHttpRequest();
httpReq.open('POST', url, false);
httpReq.setRequestHeader('Content-Type',
'application/x-www-form-urlencoded');
try {
if (params instanceof Array) {
httpReq.send(params.join('&'));
} else {
httpReq.send(params);
}
} catch (e) {
return {
'result': global_constants.SCORM_FALSE,
'errorCode': this.#error_codes.GENERAL,
};
}
return JSON.parse(httpReq.responseText);
}
/** /**
* Throws a SCORM error * Throws a SCORM error
* *

View File

@@ -8,9 +8,8 @@ import {
CMIObjectivesObject, CMIObjectivesObject,
} from './cmi/scorm12_cmi'; } from './cmi/scorm12_cmi';
import * as Utilities from './utilities'; import * as Utilities from './utilities';
import {scorm12_constants} from './constants/api_constants'; import {global_constants, scorm12_constants} from './constants/api_constants';
import {scorm12_error_codes} from './constants/error_codes'; import {scorm12_error_codes} from './constants/error_codes';
import {scorm12_regex} from './constants/regex';
const constants = scorm12_constants; const constants = scorm12_constants;
@@ -20,9 +19,16 @@ const constants = scorm12_constants;
export default class Scorm12API extends BaseAPI { export default class Scorm12API extends BaseAPI {
/** /**
* Constructor for SCORM 1.2 API * Constructor for SCORM 1.2 API
* @param {object} settings
*/ */
constructor() { constructor(settings: {}) {
super(scorm12_error_codes); const finalSettings = {
...{
mastery_override: false,
}, ...settings,
};
super(scorm12_error_codes, finalSettings);
this.cmi = new CMI(); this.cmi = new CMI();
// Rename functions to match 1.2 Spec and expose to modules // Rename functions to match 1.2 Spec and expose to modules
@@ -149,9 +155,11 @@ export default class Scorm12API extends BaseAPI {
if (this.stringMatches(CMIElement, 'cmi\\.objectives\\.\\d')) { if (this.stringMatches(CMIElement, 'cmi\\.objectives\\.\\d')) {
newChild = new CMIObjectivesObject(); newChild = new CMIObjectivesObject();
} else if (foundFirstIndex && this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d\\.correct_responses\\.\\d')) { } else if (foundFirstIndex && this.stringMatches(CMIElement,
'cmi\\.interactions\\.\\d\\.correct_responses\\.\\d')) {
newChild = new CMIInteractionsCorrectResponsesObject(); newChild = new CMIInteractionsCorrectResponsesObject();
} else if (foundFirstIndex && this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d\\.objectives\\.\\d')) { } else if (foundFirstIndex && this.stringMatches(CMIElement,
'cmi\\.interactions\\.\\d\\.objectives\\.\\d')) {
newChild = new CMIInteractionsObjectivesObject(); newChild = new CMIInteractionsObjectivesObject();
} else if (this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d')) { } else if (this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d')) {
newChild = new CMIInteractionsObject(); newChild = new CMIInteractionsObject();
@@ -201,4 +209,86 @@ export default class Scorm12API extends BaseAPI {
// Data Model // Data Model
this.cmi = newAPI.cmi; this.cmi = newAPI.cmi;
} }
/**
* Render the cmi object to the proper format for LMS commit
*
* @param {boolean} terminateCommit
* @return {object|Array}
*/
renderCommitCMI(terminateCommit: boolean) {
const cmiExport = this.renderCMIToJSONObject();
if (terminateCommit) {
cmiExport.cmi.core.total_time = this.getCurrentTotalTime();
}
const result = [];
const flattened = Utilities.flatten(cmiExport);
switch (this.settings.dataCommitFormat) {
case 'flattened':
return Utilities.flatten(cmiExport);
case 'params':
for (const item in flattened) {
if ({}.hasOwnProperty.call(flattened, item)) {
result.push(`${item}=${flattened[item]}`);
}
}
return result;
case 'json':
default:
return cmiExport;
}
}
/**
* Attempts to store the data to the LMS
*
* @param {boolean} terminateCommit
* @return {string}
*/
storeData(terminateCommit: boolean) {
if (terminateCommit) {
const originalStatus = this.cmi.core.lesson_status;
if (originalStatus === 'not attempted') {
this.cmi.core.lesson_status = 'completed';
}
if (this.cmi.core.lesson_mode === 'normal') {
if (this.cmi.core.credit === 'credit') {
if (this.settings.mastery_override &&
this.cmi.student_data.mastery_score !== '' &&
this.cmi.core.score.raw !== '') {
if (parseFloat(this.cmi.core.score.raw) >=
parseFloat(this.cmi.student_data.mastery_score)) {
this.cmi.core.lesson_status = 'passed';
} else {
this.cmi.core.lesson_status = 'failed';
}
}
}
} else if (this.cmi.core.lesson_mode === 'browse') {
if ((this.startingData?.cmi?.core?.lesson_status || '') === '' &&
originalStatus === 'not attempted') {
this.cmi.core.lesson_status = 'browsed';
}
}
}
const commitObject = this.renderCommitCMI(terminateCommit);
if (this.settings.lmsCommitUrl) {
if (this.apiLogLevel === global_constants.LOG_LEVEL_DEBUG) {
console.debug('Commit (terminated: ' +
(terminateCommit ? 'yes' : 'no') + '): ');
console.debug(commitObject);
}
return this.processHttpRequest(this.settings.lmsCommitUrl, commitObject);
} else {
console.log('Commit (terminated: ' +
(terminateCommit ? 'yes' : 'no') + '): ');
console.log(commitObject);
return global_constants.SCORM_TRUE;
}
}
} }

View File

@@ -9,8 +9,8 @@ import {
CMIInteractionsObjectivesObject, CMIInteractionsObjectivesObject,
CMIObjectivesObject, CMIObjectivesObject,
} from './cmi/scorm2004_cmi'; } from './cmi/scorm2004_cmi';
import * as Util from './utilities'; import * as Utilities from './utilities';
import {scorm2004_constants} from './constants/api_constants'; import {global_constants, scorm2004_constants} from './constants/api_constants';
import {scorm2004_error_codes} from './constants/error_codes'; import {scorm2004_error_codes} from './constants/error_codes';
import {correct_responses} from './constants/response_constants'; import {correct_responses} from './constants/response_constants';
import {valid_languages} from './constants/language_constants'; import {valid_languages} from './constants/language_constants';
@@ -26,9 +26,16 @@ export default class Scorm2004API extends BaseAPI {
/** /**
* Constructor for SCORM 2004 API * Constructor for SCORM 2004 API
* @param {object} settings
*/ */
constructor() { constructor(settings: {}) {
super(scorm2004_error_codes); const finalSettings = {
...{
mastery_override: false,
}, ...settings,
};
super(scorm2004_error_codes, finalSettings);
this.cmi = new CMI(); this.cmi = new CMI();
this.adl = new ADL(); this.adl = new ADL();
@@ -146,7 +153,8 @@ export default class Scorm2004API extends BaseAPI {
if (this.stringMatches(CMIElement, 'cmi\\.objectives\\.\\d')) { if (this.stringMatches(CMIElement, 'cmi\\.objectives\\.\\d')) {
newChild = new CMIObjectivesObject(); newChild = new CMIObjectivesObject();
} else if (foundFirstIndex && this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d\\.correct_responses\\.\\d')) { } else if (foundFirstIndex && this.stringMatches(CMIElement,
'cmi\\.interactions\\.\\d\\.correct_responses\\.\\d')) {
const parts = CMIElement.split('.'); const parts = CMIElement.split('.');
const index = Number(parts[2]); const index = Number(parts[2]);
const interaction = this.cmi.interactions.childArray[index]; const interaction = this.cmi.interactions.childArray[index];
@@ -183,13 +191,16 @@ export default class Scorm2004API extends BaseAPI {
if (this.lastErrorCode === 0) { if (this.lastErrorCode === 0) {
newChild = new CMIInteractionsCorrectResponsesObject(); newChild = new CMIInteractionsCorrectResponsesObject();
} }
} else if (foundFirstIndex && this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d\\.objectives\\.\\d')) { } else if (foundFirstIndex && this.stringMatches(CMIElement,
'cmi\\.interactions\\.\\d\\.objectives\\.\\d')) {
newChild = new CMIInteractionsObjectivesObject(); newChild = new CMIInteractionsObjectivesObject();
} else if (this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d')) { } else if (this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d')) {
newChild = new CMIInteractionsObject(); newChild = new CMIInteractionsObject();
} else if (this.stringMatches(CMIElement, 'cmi\\.comments_from_learner\\.\\d')) { } else if (this.stringMatches(CMIElement,
'cmi\\.comments_from_learner\\.\\d')) {
newChild = new CMICommentsObject(); newChild = new CMICommentsObject();
} else if (this.stringMatches(CMIElement, 'cmi\\.comments_from_lms\\.\\d')) { } else if (this.stringMatches(CMIElement,
'cmi\\.comments_from_lms\\.\\d')) {
newChild = new CMICommentsObject(true); newChild = new CMICommentsObject(true);
} }
@@ -420,4 +431,94 @@ export default class Scorm2004API extends BaseAPI {
this.cmi = newAPI.cmi; this.cmi = newAPI.cmi;
this.adl = newAPI.adl; this.adl = newAPI.adl;
} }
/**
* Render the cmi object to the proper format for LMS commit
*
* @param {boolean} terminateCommit
* @return {object|Array}
*/
renderCommitCMI(terminateCommit: boolean) {
const cmi = this.renderCMIToJSONObject();
if (terminateCommit) {
cmi.total_time = this.getCurrentTotalTime();
}
const result = [];
const flattened = Utilities.flatten(cmi);
switch (this.settings.dataCommitFormat) {
case 'flattened':
return Utilities.flatten(cmi);
case 'params':
for (const item in flattened) {
if ({}.hasOwnProperty.call(flattened, item)) {
result.push(`${item}=${flattened[item]}`);
}
}
return result;
case 'json':
default:
return cmi;
}
}
/**
* Attempts to store the data to the LMS
*
* @param {boolean} terminateCommit
* @return {string}
*/
storeData(terminateCommit: boolean) {
if (terminateCommit) {
if (this.cmi.lesson_mode === 'normal') {
if (this.cmi.credit === 'credit') {
if (this.cmi.completion_threshold && this.cmi.progress_measure) {
if (this.cmi.progress_measure >= this.cmi.completion_threshold) {
this.cmi.completion_status = 'completed';
} else {
this.cmi.completion_status = 'incomplete';
}
}
if (this.cmi.scaled_passing_score !== null &&
this.cmi.score.scaled !== '') {
if (this.cmi.score.scaled >= this.cmi.scaled_passing_score) {
this.cmi.success_status = 'passed';
} else {
this.cmi.success_status = 'failed';
}
}
}
}
}
let navRequest = false;
if (this.adl.nav.request !== (this.startingData?.adl?.nav?.request || '')) {
this.adl.nav.request = encodeURIComponent(this.adl.nav.request);
navRequest = true;
}
const commitObject = this.renderCommitCMI(terminateCommit);
if (this.settings.lmsCommitUrl) {
if (this.apiLogLevel === global_constants.LOG_LEVEL_DEBUG) {
console.debug('Commit (terminated: ' +
(terminateCommit ? 'yes' : 'no') + '): ');
console.debug(commitObject);
}
const result = this.processHttpRequest(this.settings.lmsCommitUrl,
commitObject);
// check if this is a sequencing call, and then call the necessary JS
if (navRequest && result.navRequest !== undefined &&
result.navRequest !== '') {
Function(`"use strict";(() => { ${result.navRequest} })()`)();
}
return result;
} else {
console.log('Commit (terminated: ' +
(terminateCommit ? 'yes' : 'no') + '): ');
console.log(commitObject);
return global_constants.SCORM_TRUE;
}
}
} }

View File

@@ -290,7 +290,7 @@ class CMICore extends BaseCMI {
#student_name = ''; #student_name = '';
#lesson_location = ''; #lesson_location = '';
#credit = ''; #credit = '';
#lesson_status = ''; #lesson_status = 'not attempted';
#entry = ''; #entry = '';
#total_time = ''; #total_time = '';
#lesson_mode = 'normal'; #lesson_mode = 'normal';

View File

@@ -1,4 +1,18 @@
// @flow // @flow
export const global_constants = {
SCORM_TRUE: 'true',
SCORM_FALSE: 'false',
STATE_NOT_INITIALIZED: 0,
STATE_INITIALIZED: 1,
STATE_TERMINATED: 2,
LOG_LEVEL_DEBUG: 1,
LOG_LEVEL_INFO: 2,
LOG_LEVEL_WARNING: 3,
LOG_LEVEL_ERROR: 4,
LOG_LEVEL_NONE: 5,
};
export const scorm12_constants = { export const scorm12_constants = {
// Children lists // Children lists
cmi_children: 'core,suspend_data,launch_data,comments,objectives,student_data,student_preference,interactions', cmi_children: 'core,suspend_data,launch_data,comments,objectives,student_data,student_preference,interactions',

View File

@@ -157,3 +157,66 @@ export function addHHMMSSTimeStrings(
const secondSeconds = getTimeAsSeconds(second, timeRegex); const secondSeconds = getTimeAsSeconds(second, timeRegex);
return getSecondsAsHHMMSS(firstSeconds + secondSeconds); return getSecondsAsHHMMSS(firstSeconds + secondSeconds);
} }
/**
* Flatten a JSON object down to string paths for each values
* @param {object} data
* @return {object}
*/
export function flatten(data) {
const result = {};
/**
* Recurse through the object
* @param {*} cur
* @param {*} prop
*/
function recurse(cur, prop) {
if (Object(cur) !== cur) {
result[prop] = cur;
} else if (Array.isArray(cur)) {
for (let i = 0, l = cur.length; i < l; i++) {
recurse(cur[i], prop + '[' + i + ']');
if (l === 0) result[prop] = [];
}
} else {
let isEmpty = true;
for (const p in cur) {
if ({}.hasOwnProperty.call(cur, p)) {
isEmpty = false;
recurse(cur[p], prop ? prop + '.' + p : p);
}
}
if (isEmpty && prop) result[prop] = {};
}
}
recurse(data, '');
return result;
}
/**
* Un-flatten a flat JSON object
* @param {object} data
* @return {object}
*/
export function unflatten(data) {
'use strict';
if (Object(data) !== data || Array.isArray(data)) return data;
const regex = /\.?([^.[\]]+)|\[(\d+)]/g;
const result = {};
for (const p in data) {
if ({}.hasOwnProperty.call(data, p)) {
let cur = result;
let prop = '';
let m = regex.exec(p);
while (m) {
cur = cur[prop] || (cur[prop] = (m[2] ? [] : {}));
prop = m[2] || m[1];
m = regex.exec(p);
}
cur[prop] = data[p];
}
}
return result[''] || result;
}

View File

@@ -4,13 +4,13 @@ import Scorm12API from '../src/Scorm12API';
import * as h from './api_helpers'; import * as h from './api_helpers';
import {scorm12_error_codes} from '../src/constants/error_codes'; import {scorm12_error_codes} from '../src/constants/error_codes';
const api = () => { const api = (settings = {}) => {
const API = new Scorm12API(); const API = new Scorm12API(settings);
API.apiLogLevel = 1; API.apiLogLevel = 1;
return API; return API;
}; };
const apiInitialized = () => { const apiInitialized = (settings = {}) => {
const API = api(); const API = api(settings);
API.lmsInitialize(); API.lmsInitialize();
return API; return API;
}; };
@@ -271,4 +271,64 @@ describe('SCORM 1.2 API Tests', () => {
firstAPI.cmi.core.student_id, firstAPI.cmi.core.student_id,
).to.equal('student_2'); ).to.equal('student_2');
}); });
describe('storeData()', () => {
it('should set cmi.core.lesson_status to "completed"', () => {
const scorm12API = api();
scorm12API.storeData(true);
expect(scorm12API.cmi.core.lesson_status).to.equal('completed');
});
it('should set cmi.core.lesson_status to "browsed"', () => {
const scorm12API = api();
scorm12API.cmi.core.lesson_mode = 'browse';
scorm12API.storeData(true);
expect(scorm12API.cmi.core.lesson_status).to.equal('browsed');
});
it('should set cmi.core.lesson_status to "browsed" - Initial Status',
() => {
const scorm12API = api();
scorm12API.startingData = {'cmi': {'core': {'lesson_status': ''}}};
scorm12API.cmi.core.lesson_mode = 'browse';
scorm12API.storeData(true);
expect(scorm12API.cmi.core.lesson_status).to.equal('browsed');
});
it('should set cmi.core.lesson_status to "passed" - mastery_override: true',
() => {
const scorm12API = api({mastery_override: true});
scorm12API.cmi.core.credit = 'credit';
scorm12API.cmi.student_data.mastery_score = '60.0';
scorm12API.cmi.core.score.raw = '75.0';
scorm12API.storeData(true);
expect(scorm12API.cmi.core.lesson_status).to.equal('passed');
});
it('should set cmi.core.lesson_status to "failed" - mastery_override: true',
() => {
const scorm12API = api({mastery_override: true});
scorm12API.cmi.core.credit = 'credit';
scorm12API.cmi.student_data.mastery_score = '60.0';
scorm12API.cmi.core.score.raw = '55.0';
scorm12API.storeData(true);
expect(scorm12API.cmi.core.lesson_status).to.equal('failed');
});
it('should set cmi.core.lesson_status to "passed" - mastery_override: false',
() => {
const scorm12API = api({mastery_override: false});
scorm12API.cmi.core.lesson_status = 'failed'; // module author wanted the user to pass, so we don't override
scorm12API.cmi.core.credit = 'credit';
scorm12API.cmi.student_data.mastery_score = '60.0';
scorm12API.cmi.core.score.raw = '75.0';
scorm12API.storeData(true);
expect(scorm12API.cmi.core.lesson_status).to.equal('failed');
});
it('should set cmi.core.lesson_status to "failed" - mastery_override: false',
() => {
const scorm12API = api({mastery_override: false});
scorm12API.cmi.core.lesson_status = 'passed'; // module author wanted the user to pass, so we don't override
scorm12API.cmi.core.credit = 'credit';
scorm12API.cmi.student_data.mastery_score = '60.0';
scorm12API.cmi.core.score.raw = '55.0';
scorm12API.storeData(true);
expect(scorm12API.cmi.core.lesson_status).to.equal('passed');
});
});
}); });

View File

@@ -91,6 +91,7 @@ describe('AICC CMI Tests', () => {
h.checkRead({ h.checkRead({
cmi: cmi(), cmi: cmi(),
fieldName: 'cmi.core.lesson_status', fieldName: 'cmi.core.lesson_status',
expectedValue: 'not attempted',
}); });
h.checkValidValues({ h.checkValidValues({
cmi: cmi(), cmi: cmi(),
@@ -334,6 +335,7 @@ describe('AICC CMI Tests', () => {
h.checkRead({ h.checkRead({
cmi: cmi(), cmi: cmi(),
fieldName: 'cmi.core.lesson_status', fieldName: 'cmi.core.lesson_status',
expectedValue: 'not attempted',
}); });
h.checkValidValues({ h.checkValidValues({
cmi: cmi(), cmi: cmi(),
@@ -517,7 +519,7 @@ describe('AICC CMI Tests', () => {
). ).
to. to.
equal( equal(
'{"suspend_data":"","launch_data":"","comments":"","comments_from_lms":"","core":{"student_id":"","student_name":"","lesson_location":"","credit":"","lesson_status":"","entry":"","total_time":"","lesson_mode":"normal","exit":"","session_time":"00:00:00","score":{"raw":"","min":"","max":"100"}},"objectives":{"0":{"id":"","status":"","score":{"raw":"","min":"","max":"100"}}},"student_data":{"mastery_score":"","max_time_allowed":"","time_limit_action":"","tries":{"0":{"status":"","time":"","score":{"raw":"","min":"","max":"100"}}}},"student_preference":{"audio":"","language":"","speed":"","text":""},"interactions":{"0":{"id":"","time":"","type":"","weighting":"","student_response":"","result":"","latency":"","objectives":{},"correct_responses":{}}},"evaluation":{"comments":{"0":{"content":"","location":"","time":""}}}}'); '{"suspend_data":"","launch_data":"","comments":"","comments_from_lms":"","core":{"student_id":"","student_name":"","lesson_location":"","credit":"","lesson_status":"not attempted","entry":"","total_time":"","lesson_mode":"normal","exit":"","session_time":"00:00:00","score":{"raw":"","min":"","max":"100"}},"objectives":{"0":{"id":"","status":"","score":{"raw":"","min":"","max":"100"}}},"student_data":{"mastery_score":"","max_time_allowed":"","time_limit_action":"","tries":{"0":{"status":"","time":"","score":{"raw":"","min":"","max":"100"}}}},"student_preference":{"audio":"","language":"","speed":"","text":""},"interactions":{"0":{"id":"","time":"","type":"","weighting":"","student_response":"","result":"","latency":"","objectives":{},"correct_responses":{}}},"evaluation":{"comments":{"0":{"content":"","location":"","time":""}}}}');
}); });
}); });

View File

@@ -155,6 +155,7 @@ describe('SCORM 1.2 CMI Tests', () => {
h.checkRead({ h.checkRead({
cmi: cmi(), cmi: cmi(),
fieldName: 'cmi.core.lesson_status', fieldName: 'cmi.core.lesson_status',
expectedValue: 'not attempted',
}); });
h.checkValidValues({ h.checkValidValues({
cmi: cmi(), cmi: cmi(),
@@ -415,6 +416,7 @@ describe('SCORM 1.2 CMI Tests', () => {
h.checkRead({ h.checkRead({
cmi: cmiInitialized(), cmi: cmiInitialized(),
fieldName: 'cmi.core.lesson_status', fieldName: 'cmi.core.lesson_status',
expectedValue: 'not attempted',
}); });
h.checkValidValues({ h.checkValidValues({
cmi: cmiInitialized(), cmi: cmiInitialized(),
@@ -590,7 +592,7 @@ describe('SCORM 1.2 CMI Tests', () => {
). ).
to. to.
equal( equal(
'{"suspend_data":"","launch_data":"","comments":"","comments_from_lms":"","core":{"student_id":"","student_name":"","lesson_location":"","credit":"","lesson_status":"","entry":"","total_time":"","lesson_mode":"normal","exit":"","session_time":"00:00:00","score":{"raw":"","min":"","max":"100"}},"objectives":{"0":{"id":"","status":"","score":{"raw":"","min":"","max":"100"}}},"student_data":{"mastery_score":"","max_time_allowed":"","time_limit_action":""},"student_preference":{"audio":"","language":"","speed":"","text":""},"interactions":{"0":{"id":"","time":"","type":"","weighting":"","student_response":"","result":"","latency":"","objectives":{},"correct_responses":{}}}}'); '{"suspend_data":"","launch_data":"","comments":"","comments_from_lms":"","core":{"student_id":"","student_name":"","lesson_location":"","credit":"","lesson_status":"not attempted","entry":"","total_time":"","lesson_mode":"normal","exit":"","session_time":"00:00:00","score":{"raw":"","min":"","max":"100"}},"objectives":{"0":{"id":"","status":"","score":{"raw":"","min":"","max":"100"}}},"student_data":{"mastery_score":"","max_time_allowed":"","time_limit_action":""},"student_preference":{"audio":"","language":"","speed":"","text":""},"interactions":{"0":{"id":"","time":"","type":"","weighting":"","student_response":"","result":"","latency":"","objectives":{},"correct_responses":{}}}}');
}); });
}); });

View File

@@ -274,4 +274,60 @@ describe('Utility Tests', () => {
).to.equal('01:05:30.5'); ).to.equal('01:05:30.5');
}); });
}); });
describe('flatten()', () => {
it('Should return flattened object', () => {
expect(
Utilities.flatten({
'cmi': {
'core': {
'learner_id': 'jputney',
'learner_name': 'Jonathan',
},
'objectives': {
'0': {
'id': 'AAA',
},
'1': {
'id': 'BBB',
},
},
},
}),
).to.eql({
'cmi.core.learner_id': 'jputney',
'cmi.core.learner_name': 'Jonathan',
'cmi.objectives.0.id': 'AAA',
'cmi.objectives.1.id': 'BBB',
});
});
});
describe('unflatten()', () => {
it('Should return flattened object', () => {
expect(
Utilities.unflatten({
'cmi.core.learner_id': 'jputney',
'cmi.core.learner_name': 'Jonathan',
'cmi.objectives.0.id': 'AAA',
'cmi.objectives.1.id': 'BBB',
}),
).to.eql({
'cmi': {
'core': {
'learner_id': 'jputney',
'learner_name': 'Jonathan',
},
'objectives': {
'0': {
'id': 'AAA',
},
'1': {
'id': 'BBB',
},
},
},
});
});
});
}); });