unserver-unify
Version:
786 lines (776 loc) • 30.2 kB
JavaScript
angular.module('bamboo.common').service('Scorm2004', function($timeout, ApiService, $localStorage) {
var Utl = SCOBotUtil,
defaults = {
version: "4.1.6",
createdate: "07/17/2010 08:15AM",
moddate: "03/04/2016 12:24PM",
prefix: "SCOBot_API_1484_11",
errorCode: 0,
diagnostic: '',
initialized: 0,
terminated: 0,
cmi: null,
adl: null,
// CMI is the Computer Managed Instruction object (the student attempt). Edit these values as you see fit.
CMI: {
_version: "Local 1.0",
comments_from_learner: {
_children: "comment,location,timestamp",
_count: "0"
},
comments_from_lms: {
_children: "comment,location,timestamp",
_count: "0"
},
completion_status: "unknown",
completion_threshold: "0.7",
credit: "no-credit",
entry: "ab-initio",
exit: "",
interactions: {
_children: "id,type,objectives,timestamp,correct_responses,weighting,learner_response,result,latency,description",
_count: "0"
},
launch_data: "?name1=value1&name2=value2&name3=value3", // {\"name1\": \"value1\", \"name2\": \"value2\", \"name3\": \"value3\"} or ?name1=value1&name2=value2&name3=value3
learner_id: "100",
learner_name: "Simulated User",
learner_preference: {
_children: "audio_level,language,delivery_speed,audio_captioning",
audio_level: "1",
language: "",
delivery_speed: "1",
audio_captioning: "0"
},
location: "",
max_time_allowed: "", // PT26.4S for 26.4 Seconds
mode: "normal",
objectives: {
_children: "id,score,success_status,completion_status,description",
_count: "0"
},
progress_measure: "",
scaled_passing_score: "0.7",
score: {
_children: "scaled,raw,min,max",
scaled: "",
raw: "",
min: "",
max: ""
},
session_time: "PT0H0M0S",
success_status: "unknown",
suspend_data: "",
time_limit_action: "", // exit, no message or continue, message etc ...
total_time: "PT0H0M0S"
},
ADL: {
nav: {
request: "_none_",
request_valid: {
choice: {}, // "{target=<STRING>} - keep in mind you have no lesson structure in standalone mode"
continue: "false",
previous: "false"
}
}
}
},
// Settings merged with defaults and extended options */
settings={},
cmi = {},
adl = {},
/**
* Completion Status's that are allowed
*/
completion_status = "|completed|incomplete|not attempted|unknown|",
/**
Read Only values -
The hash following could of been much simpler had certain name spaces always been read-only in all areas.
This would of allowed me to just evaluate the last item and perform that rule globally. The following are issues -
id - This is read-only under adl.data.n.id, and read/write everywhere else
comments_from_lms are entirely read-only (global rule)
timestamp is RO for comments from LMS
*/
read_only = "|_version|completion_threshold|credit|entry|launch_data|learner_id|learner_name|_children|_count|mode|maximum_time_allowed|scaled_passing_score|time_limit_action|total_time|comment|",
/**
* Write Only values
*/
write_only = "|exit|session_time|",
exit = "|time-out|suspend|logout|normal||",
nav_states = "|_none_|continue|previous|choice|exit|exitAll|abandon|abandonAll|suspendAll|jump|",
errors = {
0: "No error",
101: "General exception",
102: "General Initialization Failure",
103: "Already Initialized",
104: "Content Instance Terminated",
111: "General Termination Failure",
112: "Termination Before Initialization",
113: "Termination After Termination",
122: "Retrieve Data Before Initialization",
123: "Retrieve Data After Termination",
132: "Store Data Before Initialization",
133: "Store Data After Termination",
142: "Commit Before Initialization",
143: "Commit After Termination",
201: "General Argument Error",
301: "General Get Failure",
351: "General Set Failure",
391: "General Commit Failure",
401: "Undefined Data Model",
402: "Unimplemented Data Model Element",
403: "Data Model Element Value Not Initialized",
404: "Data Model Element Is Read Only",
405: "Data Model Element Is Write Only",
406: "Data Model Element Type Mismatch",
407: "Data Model Element Value Out Of Range",
408: "Data Model Dependency Not Established"
},
self = this;
// Private
/**
* Throw Vocabulary Error
* This sets the errorCode and Diagnostic for the key and value attempted.
* @param k {String} key
* @param v {String} value
* @returns {String} 'false'
*/
function throwVocabError(k, v) {
settings.diganostic = "The " + k + " of " + v + " must be a proper vocabulary element.";
settings.errorCode = 406;
return 'false';
}
/**
* Throw Unimplemented Error
* 402 data model doesn't exist yet.
* @param key {String}
* @returns {String} 'false'
*/
function throwUnimplemented(key) {
settings.errorCode = 402;
settings.diagnostic = 'The value for key ' + key + ' has not been created yet.';
return 'false';
}
/**
* Throw General Set Error
* This sets the errorCode and Diagnostic for the key and value attempted.
* Note, messages differ too much for this to be genericized. I think the SCORM Error, Message and Diagnostic needs to be bundled better.
* @param k {String} key
* @param v {String} value
* @param o {String} optional
* @returns {String} 'false'
*/
function throwGeneralSetError(k, v, o) {
settings.errorCode = "351";
settings.diagnostic = "The " + k + " element must be unique. The value '" + v + "' has already been set in #" + o;
return 'false';
}
/**
* Set Data (Private)
* This covers setting key's values against a object even when there are numbers as objects
* It will chase thru the Object dot syntax to locate the key you request. This worked out
* better than doing a eval(param); which breaks when numbers are introduced.
* @param key {String} Location of value in object
* @param val {String} Value of the Key
* @param obj {Object} Object to search and set
*/
var timer;
function setData(key, val, obj) {
//if (!obj) { obj = data;} //outside (non-recursive) call, use "data" as our base object
var ka = key.split(/\./);
//split the key by the dots
if (ka.length < 2) {
obj[ka[0]] = val;
//only one part (no dots) in key, just set value
} else {
if (!obj[ka[0]]) {
obj[ka[0]] = {};
} //create our "new" base obj if it doesn't exist
obj = obj[ka.shift()];
//remove the new "base" obj from string array, and hold actual object for recursive call
setData(ka.join("."), val, obj);
//join the remaining parts back up with dots, and recursively set data on our new "base" obj
}
console.log(settings);
console.log('------------- write here ------');
if (timer) {
$timeout.cancel(timer);
}
timer = $timeout(updatelms, 2000);
}
var courseId, userId;
var lmsId;
var passed;
var mvId;
var successCallBack;
this.scorm2004Local = function(cid,mvid, rid, _successCallBack, callback) {
// console.log(user);
courseId = cid;
lmsId = rid;
mvId=mvid;
successCallBack=_successCallBack;
getlmsrecord(function(r) {
console.log(r);
if(r&&r.cmi&&r.cmi.success_status=='passed'){
console.log("-- passed --");
passed=true;
}
var data={};
if (!r) {
settings = angular.copy(defaults);
}
if (!r || {}.toString.call(r) != '[object Object]' || r._flush || r._expireTime === 0) {
settings = angular.copy(defaults)
// } else if (r['cmi.completion_status'] == 'passed') { // never expire for passed records
// data = r;
// } else if (r._expireTime !== -1 && time() >= (r._expireTime || Number.POSITIVE_INIFINITY)) {
// data = {};
} else {
settings = r;
}
//settings = Utl.extend(defaults, data);
if (callback) {
callback();
}
})
}
/**
* Get Data (Private)
* This covers getting key's values against a object even when there are numbers as objects
* It will chase thru the Object dot syntax to locate the key you request. This worked out
* better than doing a eval(param); which breaks when numbers are introduced.
* @param key {String} Location of value in object
* @param obj {Object} Object to search
* @returns {String}
*/
function getData(key, obj) {
//if (!obj) { obj = data;} //outside (non-recursive) call, use "data" as our base object
//console.log(settings.prefix + ": GetData Checking " + key, 4);
var ka = key.split(/\./),
v;
//split the key by the dots
if (ka.length < 2) {
try {
//console.log(settings.prefix + ": getData returning - key:" + ka[0] + " value:" + obj[ka[0]], 4);
return obj[ka[0]];
} catch (e) {
throwUnimplemented(key);
return 'false';
}
//only one part (no dots) in key, just set value
} else {
v = ka.shift();
if (obj[v]) {
return String(getData(ka.join("."), obj[v])); // just in case its undefined
}
return throwUnimplemented(key);
//join the remaining parts back up with dots, and recursively set data on our new "base" obj
}
}
/**
* CMI Get Value (Private)
* This covers getting CMI Keys and returning there values.
* It will have mild error control against the CMI object for Write Only values.
* @param key {String} Location of value in object
* @returns {String}
*/
function cmiGetValue(key) {
var r = "false";
switch (key) {
//Write Only
case "cmi.exit":
case "cmi.session_time":
settings.errorCode = 405;
settings.diagnostic = "Sorry, this has been specified as a read-only value for " + key;
break;
default:
r = getData(key.substr(4, key.length), cmi);
//console.log(settings.prefix + ": cmiGetValue got " + r, 4);
// Filter
if (r === 'undefined') {
settings.errorCode = 401;
settings.diagnostic = "Sorry, there was a undefined response from " + key;
r = "false";
}
break;
}
// console.log(settings.prefix + ": GetValue " + key + " = " + r, 4);
return r;
}
/**
* ADL Get Value (Private)
* This covers the ADL Sequence and Navigation object present in SCORM 2004
* @param key {String}
* @returns {String}
*/
function adlGetValue(key) {
var r = "false";
// Stop a jump navigation request since there is no navigation in this scenario.
if (key.indexOf('adl.nav.request_valid.choice') >= 0) {
settings.errorCode = 301;
console.log(settings.prefix + "Sorry, targeted 'choice' requests not allowed by this API since there is no navigation.", 2);
} else {
r = getData(key.substr(4, key.length), adl);
// Filter
if (r === 'undefined') {
settings.errorCode = 401;
settings.diagnostic = "Sorry, there was a undefined response from " + key;
r = "false";
}
}
console.log(settings.prefix + ": GetValue " + key + " = " + r, 4);
return r;
}
/**
* Is Read Only?
* I've placed several of the read-only items in a delimited string. This is used to compare
* the key, to known read-only values to keep you from changing something your not supposed to.
* @param key {String} like cmi.location
* @returns {Boolean} true or false
*/
function isReadOnly(key) {
// See note above about read-only
var tiers = key.split('.'),
v = tiers[tiers.length - 1]; // last value
if (tiers[2] === "request_valid" || tiers[4] === 'id') {
return true;
}
if (tiers[1] === 'comments_from_lms') { // entirely read only
return true;
}
if (tiers[1] === 'comments_from_learner') { // Condition where comment in this case is allowed.
return false;
}
return read_only.indexOf('|' + v + '|') >= 0;
}
/**
* Is Write Only?
* I've placed several write-only items in a delimited string. This is used to compare
* the key, to known write-only values to keep you from reading things your not suppose to.
* @param key {String}
* @returns {Boolean} true or false
*/
function isWriteOnly(key) {
var tiers = key.split("."),
v = tiers[tiers.length - 1]; // last value
return write_only.indexOf('|' + v + '|') >= 0;
}
/**
* Round Value
* Rounds to 2 decimal places
* @param v {Number}
* @returns {Number}
*/
function roundVal(v) {
var dec = 2;
return Math.round(v * Math.pow(10, dec)) / Math.pow(10, dec);
}
/**
* Get Object Length
* @param obj {Object}
* returns {Number}
*/
function getObjLength(obj) {
var name,
length = 0;
for (name in obj) {
if (obj.hasOwnProperty(name)) {
length += 1;
}
}
return length;
}
function checkExitType() {
if (cmi.exit === "suspend") {
cmi.entry = "resume";
}
}
function getlmsrecord(callback) {
var info = {
action: "getlmsrecord",
cid: courseId,
rid: lmsId,
mvId:mvId,
}
mvApi(info, callback)
}
function updatelms() {
timer = null;
console.log(passed);
var needToRefresh;
if(passed){
if(!settings||!settings.cmi||settings.cmi.success_status!='passed'){
console.log("-- can not overwrite pass result --");
return;
}
}else if(settings&&settings.cmi&&settings.cmi.success_status=='passed'){
// passed, block future unpassed results
passed=true;
needToRefresh=true;
}
delete settings.CMI;
delete settings.ADL;
var info = {
action: "updatelms",
cid: courseId,
rid: lmsId,
mvId:mvId,
object: settings,
}
console.log(info);
mvApi(info,function(){
if(needToRefresh){
successCallBack(true);
}
})
}
function mvApi(info, callback) {
ApiService.post('/mvsubjects', info).then(function(result) {
console.log(result);
if (result.data.success) {
if (!callback) {
console.log('success');
} else {
callback(result.data.data);
}
} else {
console.log("Error");
console.log(result.data);
}
});
}
/**
* Update Suspend Data Usage Statistics
* Will update settings.suspend_date_usage with current % level
*/
function suspendDataUsageStatistic() {
return roundVal((cmi.suspend_data.length / 64000) * 100) + "%";
}
this.API_1484_11 = {
// imports the API methods to another object
isRunning: function() {
return settings.initialized === 1 && settings.terminated === 0;
},
// TODO: check if return booleans should be strings
Initialize: function() {
console.log(settings.prefix + ": Initializing...", 3);
// Computer Managed Instruction
if (settings.cmi !== null) {
cmi = settings.cmi;
checkExitType();
} else {
cmi = settings.CMI;
}
// ADL - Sequence and Navigation
if (settings.adl !== null) {
adl = settings.adl;
} else {
adl = settings.ADL;
}
// setting.adl=adl;
// Clean CMI Object
settings.initialized = 1;
settings.terminated = 0;
return 'true';
},
Terminate: function(emptyString) {
if (emptyString !== "") {
return "false";
} else {
return "true";
}
},
// TODO: check if return booleans should be strings
// TODO: validate arguments
GetValue: function(key) {
// console.log(settings.prefix + ": Running: " + this.isRunning() + " GetValue: " + key + "...", 4);
settings.errorCode = 0;
var r = "false",
k = key.toString(), // ensure string
tiers = [];
if (this.isRunning()) {
if (isWriteOnly(k)) {
console.log(settings.prefix + ": This " + k + " is write only", 4);
settings.errorCode = 405;
return "false";
}
tiers = k.toLowerCase().split(".");
switch (tiers[0]) {
case "cmi":
r = cmiGetValue(k);
break;
case "ssp":
break;
case "adl":
r = adlGetValue(k);
break;
}
// Responding with 403 if empty
if (r === "") {
settings.errorCode = 403;
}
return r;
}
settings.errorCode = 123;
return r;
},
// TODO: check if return booleans should be strings
// TODO: validate arguments
SetValue: function(key, value) {
console.log(settings.prefix + ": SetValue: " + key + " = " + value, 4);
settings.errorCode = 0;
settings.cmi=cmi;
settings.adl=adl;
var tiers = [],
k = key.toString(), // ensure string
v = value.toString(), // ensure string
z = 0,
count = 0,
arr = [];
if (this.isRunning()) {
if (isReadOnly(k)) {
console.log(settings.prefix + ": This " + k + " is read only", 4);
settings.errorCode = 404;
settings.diagnostic = "This namespace is read-only.";
return "false";
}
tiers = k.split(".");
//console.log(settings.prefix + ": Tiers " + tiers[1], 4);
switch (tiers[0]) {
case "cmi":
switch (key) {
case "cmi.location":
if (v.length > 1000) {
console.log(settings.prefix + ": Some LMS's might truncate your bookmark as you've passed " + v.length + " characters of bookmarking data", 2);
}
break;
case "cmi.completion_status":
if (completion_status.indexOf('|' + v + '|') === -1) {
// Invalid value
return throwVocabError(key, v);
}
break;
case "cmi.exit":
if (exit.indexOf('|' + v + '|') === -1) {
// Invalid value
return throwVocabError(key, v);
}
break;
default:
// Need to dig in to some of these lower level values
switch (tiers[1]) {
case "comments_from_lms":
settings.errorCode = "404";
settings.diagnostic = "The cmi.comments_from_lms element is entirely read only.";
return 'false';
case "comments_from_learner":
// Validate
if (cmi.comments_from_learner._children.indexOf(tiers[3]) === -1) {
return throwVocabError(key, v);
}
setData(k.substr(4, k.length), v, cmi);
cmi.comments_from_learner._count = (getObjLength(cmi.comments_from_learner) - 2).toString(); // Why -1? _count and _children
return 'true';
case "interactions":
// Validate
if (cmi.interactions._children.indexOf(tiers[3]) === -1) {
return throwVocabError(key, v);
}
//console.log(settings.prefix + ": Checking Interactions .... " + getObjLength(cmi.interactions), 4);
cmi.interactions._count = (getObjLength(cmi.interactions) - 2).toString(); // Why -2? _count and _children
// Check interactions.n.objectives._count
// This one is tricky because if a id is added at tier[3] this means the objective count needs to increase for this interaction.
// Interactions array values may not exist yet, which is why its important to build these out ahead of time.
// this should work (Subtract _count, and _children)
if (isNaN(parseInt(tiers[2], 10))) {
return 'false';
}
// Interactions uses objectives and correct_repsponses that need to be constructed.
// Legal build of interaction array item
if (!Utl.isPlainObject(cmi.interactions[tiers[2]])) {
if (tiers[3] === "id") {
cmi.interactions[tiers[2]] = {};
setData(k.substr(4, k.length), v, cmi);
cmi.interactions._count = (getObjLength(cmi.interactions) - 2).toString(); // Why -2? _count and _children
if (!Utl.isPlainObject(cmi.interactions[tiers[2]].objectives)) {
// Setup Objectives for the first time
console.log(settings.prefix + ": Constructing objectives object for new interaction", 4);
cmi.interactions[tiers[2]].objectives = {};
cmi.interactions[tiers[2]].objectives._count = "-1";
}
// Wait, before you go trying set a count on a undefined object, lets make sure it exists...
if (!Utl.isPlainObject(cmi.interactions[tiers[2]].correct_responses)) {
// Setup Objectives for the first time
console.log(settings.prefix + ": Constructing correct responses object for new interaction", 4);
cmi.interactions[tiers[2]].correct_responses = {};
cmi.interactions[tiers[2]].correct_responses._count = "-1";
}
return 'true';
}
console.log("Can't add interaction without ID first!", 3);
return 'false';
// throw error code
}
// Manage Objectives
if (tiers[3] === 'objectives') { // cmi.interactions.n.objectives
// Objectives require a unique ID
if (tiers[5] === "id") {
count = parseInt(cmi.interactions[tiers[2]].objectives._count, 10);
z = count;
//for (z = 0; z < count; z += 1) {
while (z < count) {
if (cmi.interactions[tiers[2]].objectives[z].id === v) {
return throwGeneralSetError(key, v, z);
//settings.errorCode = "351";
//settings.diagnostic = "The objectives.id element must be unique. The value '" + v + "' has already been set in objective #" + z;
}
z += 1;
}
} else {
return throwVocabError(key, v);
}
setData(k.substr(4, k.length), v, cmi);
cmi.interactions[tiers[2]].objectives._count = (getObjLength(cmi.interactions[tiers[2]].objectives) - 1).toString(); // Why -1? _count
return 'true';
}
// Manage Correct Responses
if (tiers[3] === 'correct_responses') {
// Validate Correct response patterns
setData(k.substr(4, k.length), v, cmi);
cmi.interactions[tiers[2]].correct_responses._count = (getObjLength(cmi.interactions[tiers[2]].correct_responses) - 1).toString(); // Why -1? _count
}
setData(k.substr(4, k.length), v, cmi);
cmi.interactions._count = (getObjLength(cmi.interactions) - 2).toString(); // Why -2? _count and _children
return 'true';
//break;
case "objectives":
// Objectives require a unique ID, which to me contradicts journaling
if (tiers[3] === "id") {
count = parseInt(cmi.objectives._count, 10);
//for (z = 0; z < count; z += 1) {
while (z < count) {
if (cmi.objectives[z].id === v) {
settings.errorCode = "351";
settings.diagnostic = "The objectives.id element must be unique. The value '" + v + "' has already been set in objective #" + z;
return 'false';
}
z += 1;
}
}
// End Unique ID Check
// Now Verify the objective in question even has a ID yet, if not throw error.
if (tiers[3] !== "id") {
arr = parseInt(tiers[2], 10);
if (cmi.objectives[arr] === undefined) {
settings.errorCode = "408";
settings.diagnostic = "The objectives.id element must be set before other elements can be set";
return 'false';
}
}
// END ID CHeck
if (isNaN(parseInt(tiers[2], 10))) {
return 'false';
// throw error code
}
setData(k.substr(4, k.length), v, cmi);
cmi.objectives._count = (getObjLength(cmi.objectives) - 2).toString(); // Why -2? _count and _children
return 'true';
}
break;
// More reinforcement to come ...
}
// Rip off 'cmi.' before we add this to the model
setData(k.substr(4, k.length), v, cmi);
break;
case "ssp":
// Unless local storage was used, persisting would be difficult.
break;
case "adl":
// This value is dynamic since it commonly includes a {target=STRING}
if (key.indexOf('adl.nav.request_valid.choice') >= 0) {
settings.errorCode = "404";
settings.diagnostic = "The requested namespace is read-only.";
return 'false';
}
// Check the rest
switch (key) {
case "adl.nav.request":
/* if (nav_states.indexOf('|' + v + '|') === -1) {
console.log(v);
console.log(nav_states);
settings.errorCode = "406";
settings.diagnostic = "The requested namespace value did not match any allowed states.";
console.log("reject 1");
return 'false';
} */
break;
// This should get caught by isReadOnly above
case "adl.nav.request_valid.continue":
case "adl.nav.request_valid.previous":
settings.errorCode = "404";
settings.diagnostic = "The requested namespace is read-only.";
console.log("reject 2");
return 'false';
default:
// Further evaluation?
if (tiers[1] !== "nav") {
settings.errorCode = "351";
settings.diagnostic = "The requested namespace does not exist.";
console.log("reject 3");
return 'false';
}
break;
}
setData(k.substr(4, k.length), v, adl);
break;
}
return "true";
}
// Determine Error Code
if (settings.terminated) {
settings.errorCode = 133;
} else {
settings.errorCode = 132;
}
console.log("return false");
return "false";
},
// TODO: check if return booleans should be strings
Commit: function() {
console.log(settings.prefix + ": Commit called.\nSuspend Data Usage " + suspendDataUsageStatistic(), 4);
//$(self).triggerHandler({
Utl.triggerEvent(self, 'StoreData', {
name: 'StoreData',
runtimedata: cmi,
sequence: adl
});
return 'true';
},
Terminate: function() {
// Could do things here like a LMS
//self.Commit(); // Force commit?
// self.API_1484_11.Commit();
console.log("Terminate");
settings.terminated = 1;
settings.initialized = 0;
return 'true';
},
GetLastError: function() {
return settings.errorCode;
},
GetErrorString: function(param) {
if (param !== "") {
// console.log(param);
var nparam = parseInt(param, 10);
// console.log(nparam);
if (errors[nparam] !== undefined) {
return errors[nparam];
}
}
return "";
},
// TODO: improve diagnostics
GetDiagnostic: function(code) {
return settings.diagnostic;
}
};
});