api_wrapper.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. /**
  2. * Wrapper to the SCORM API provided by Chamilo
  3. * The complete set of functions and variables are in this file to avoid unnecessary file
  4. * accesses.
  5. * Only event triggers and answer data are inserted into the final document.
  6. * @author Yannick Warnier - inspired by the ADLNet documentation on SCORM content-side API
  7. * @package scorm.js
  8. */
  9. /**
  10. * Initialisation of the SCORM API section.
  11. * Find the SCO functions (startTimer, computeTime, etc in the second section)
  12. * Find the Chamilo-proper functions (checkAnswers, etc in the third section)
  13. */
  14. var _debug = true;
  15. var findAPITries = 0;
  16. var _apiHandle = null; //private variable
  17. var errMsgLocate = "Unable to locate the LMS's API implementation";
  18. var _NoError = 0;
  19. var _GeneralException = 101;
  20. var _ServerBusy = 102;
  21. var _InvalidArgumentError = 201;
  22. var _ElementCannotHaveChildren = 202;
  23. var _ElementIsNotAnArray = 203;
  24. var _NotInitialized = 301;
  25. var _NotImplementedError = 401;
  26. var _InvalidSetValue = 402;
  27. var _ElementIsReadOnly = 403;
  28. var _ElementIsWriteOnly = 404;
  29. var _IncorrectDataType = 405;
  30. /**
  31. * Gets the API handle right into the local API object and ensure there is only one.
  32. * Using the singleton pattern to ensure there's only one API object.
  33. * @return object The API object as given by the LMS
  34. */
  35. var API = new function()
  36. {
  37. if (_apiHandle == null) {
  38. _apiHandle = getAPI();
  39. }
  40. return _apiHandle;
  41. }
  42. /**
  43. * Finds the API on the LMS side or gives up giving an error message
  44. * @param object The window/frame object in which we are searching for the SCORM API
  45. * @return object The API object recovered from the LMS's implementation of the SCORM API
  46. */
  47. function findAPI(win)
  48. {
  49. while((win.API == null) && (win.parent != null) && (win.parent != win)) {
  50. findAPITries++;
  51. if (findAPITries>10) {
  52. alert("Error finding API - too deeply nested");
  53. return null;
  54. }
  55. win = win.parent
  56. }
  57. return win.API;
  58. }
  59. /**
  60. * Gets the API from the current window/frame or from parent objects if not found
  61. * @return object The API object recovered from the LMS's implementation of the SCORM API
  62. */
  63. function getAPI()
  64. {
  65. //window is the global/root object of the current window/frame
  66. var MyAPI = findAPI(window);
  67. //look through parents if any
  68. if ((MyAPI == null) && (window.opener != null) && (typeof(window.opener) != "undefined")) {
  69. MyAPI = findAPI(window.opener);
  70. }
  71. //still not found? error message
  72. if (MyAPI == null) {
  73. alert("Unable to find SCORM API adapter.\nPlease check your LMS is considering this page as SCORM and providing the right JavaScript interface.")
  74. }
  75. return MyAPI;
  76. }
  77. /**
  78. * Handles error codes (prints the error if it has a description)
  79. * @return int Error code from LMS's API
  80. */
  81. function ErrorHandler()
  82. {
  83. if (API == null) {
  84. alert("Unable to locate the LMS's API. Cannot determine LMS error code");
  85. return;
  86. }
  87. var errCode = API.LMSGetLastError().toString();
  88. if (errCode != _NoError) {
  89. if (errCode == _NotImplementedError) {
  90. var errDescription = "The LMS doesn't support this feature";
  91. if (_debug) {
  92. errDescription += "\n";
  93. errDescription += api.LMSGetDiagnostic(null);
  94. }
  95. console.log(errDescription);
  96. } else {
  97. var errDescription = API.LMSGetErrorString(errCode);
  98. if (_debug) {
  99. errDescription += "\n";
  100. errDescription += api.LMSGetDiagnostic(null);
  101. }
  102. console.log(errDescription);
  103. }
  104. }
  105. return errCode;
  106. }
  107. /**
  108. * Calls the LMSInitialize method of the LMS's API object
  109. * @return string The string value of the LMS returned value or false if error (should be "true" otherwise)
  110. */
  111. function doLMSInitialize()
  112. {
  113. if (API == null) {
  114. alert(errMsgLocate + "\nLMSInitialize failed");
  115. return false;
  116. }
  117. var result = API.LMSInitialize("");
  118. if (result.toString() != "true") {
  119. var err = ErrorHandler();
  120. }
  121. return result.toString();
  122. }
  123. /**
  124. * Calls the LMSFinish method of the LMS's API object
  125. * @return string The string value of the LMS return value, or false if error (should be "true" otherwise)
  126. */
  127. function doLMSFinish()
  128. {
  129. if (API == null) {
  130. alert(errMsgLocate + "\nLMSFinish failed");
  131. return false;
  132. } else {
  133. var result = API.LMSFinish('');
  134. if (result.toString() != "true") {
  135. var err = ErrorHandler();
  136. }
  137. }
  138. return result.toString();
  139. }
  140. /**
  141. * Calls the LMSGetValue method
  142. * @param string The name of the SCORM parameter to get
  143. * @return string The value returned by the LMS
  144. */
  145. function doLMSGetValue(name)
  146. {
  147. if (API == null) {
  148. alert(errMsgLocate + "\nLMSGetValue was not successful.");
  149. return "";
  150. } else {
  151. var value = API.LMSGetValue(name);
  152. var errCode = API.LMSGetLastError().toString();
  153. if (errCode != _NoError) {
  154. // an error was encountered so display the error description
  155. var errDescription = API.LMSGetErrorString(errCode);
  156. alert("LMSGetValue(" + name + ") failed. \n" + errDescription);
  157. return "";
  158. }
  159. }
  160. }
  161. /**
  162. * Calls the LMSSetValue method of the API object
  163. * @param string The name of the SCORM parameter to set
  164. * @param string The value to set the parameter to
  165. * @return void
  166. */
  167. function doLMSSetValue(name, value)
  168. {
  169. if (API == null) {
  170. alert("Unable to locate the LMS's API Implementation.\nLMSSetValue was not successful.");
  171. return;
  172. } else {
  173. var result = API.LMSSetValue(name, value);
  174. if (result.toString() != "true") {
  175. var err = ErrorHandler();
  176. }
  177. }
  178. return;
  179. }
  180. /**
  181. * Calls the LMSCommit method
  182. */
  183. function doLMSCommit()
  184. {
  185. if (API == null) {
  186. alert(errMsgLocate + "\nLMSCommit was not successful.");
  187. return "false";
  188. } else {
  189. var result = API.LMSCommit("");
  190. if (result != "true") {
  191. var err = ErrorHandler();
  192. }
  193. }
  194. return result.toString();
  195. }
  196. /**
  197. * Calls GetLastError()
  198. */
  199. function doLMSGetLastError()
  200. {
  201. if (API == null) {
  202. alert(errMsgLocate + "\nLMSGetLastError was not successful.");
  203. //since we can't get the error code from the LMS, return a general error
  204. return _GeneralError;
  205. }
  206. return API.LMSGetLastError().toString();
  207. }
  208. /**
  209. * Calls LMSGetErrorString()
  210. */
  211. function doLMSGetErrorString(errorCode)
  212. {
  213. if (API == null) {
  214. alert(errMsgLocate + "\nLMSGetErrorString was not successful.");
  215. }
  216. return API.LMSGetErrorString(errorCode).toString();
  217. }
  218. /**
  219. * Calls LMSGetDiagnostic()
  220. */
  221. function doLMSGetDiagnostic(errorCode)
  222. {
  223. if (API == null) {
  224. alert(errMsgLocate + "\nLMSGetDiagnostic was not successful.");
  225. }
  226. return API.LMSGetDiagnostic(errorCode).toString();
  227. }
  228. /**
  229. * Second section. The SCO functions are located here (handle time and score messaging to SCORM API)
  230. * Initialisation
  231. */
  232. var startTime;
  233. var exitPageStatus;
  234. /**
  235. * Initialise page values
  236. */
  237. function loadPage()
  238. {
  239. var result = doLMSInitialize();
  240. if (result) {
  241. var status = doLMSGetValue("cmi.core.lesson_status");
  242. if (status == "not attempted") {
  243. doLMSSetValue("cmi.core.lesson_status", "incomplete");
  244. }
  245. exitPageStatus = false;
  246. startTimer();
  247. }
  248. }
  249. /**
  250. * Starts the local timer
  251. */
  252. function startTimer()
  253. {
  254. startTime = new Date().getTime();
  255. }
  256. /**
  257. * Calculates the total time and sends the result to the LMS
  258. */
  259. function computeTime()
  260. {
  261. if (startTime != 0) {
  262. var currentDate = new Date().getTime();
  263. var elapsedSeconds = ( (currentDate - startTime) / 1000 );
  264. var formattedTime = convertTotalSeconds(elapsedSeconds);
  265. } else {
  266. formattedTime = "00:00:00.0";
  267. }
  268. doLMSSetValue( "cmi.core.session_time", formattedTime );
  269. }
  270. /**
  271. * Formats the time in a SCORM time format
  272. */
  273. function convertTotalSeconds(ts)
  274. {
  275. var sec = (ts % 60);
  276. ts -= sec;
  277. var tmp = (ts % 3600); //# of seconds in the total # of minutes
  278. ts -= tmp; //# of seconds in the total # of hours
  279. // convert seconds to conform to CMITimespan type (e.g. SS.00)
  280. sec = Math.round(sec*100)/100;
  281. var strSec = new String(sec);
  282. var strWholeSec = strSec;
  283. var strFractionSec = "";
  284. if (strSec.indexOf(".") != -1) {
  285. strWholeSec = strSec.substring(0, strSec.indexOf("."));
  286. strFractionSec = strSec.substring(strSec.indexOf(".") + 1, strSec.length);
  287. }
  288. if (strWholeSec.length < 2) {
  289. strWholeSec = "0" + strWholeSec;
  290. }
  291. strSec = strWholeSec;
  292. if (strFractionSec.length) {
  293. strSec = strSec + "." + strFractionSec;
  294. }
  295. if ((ts % 3600) != 0)
  296. var hour = 0;
  297. else var hour = (ts / 3600);
  298. if ((tmp % 60) != 0)
  299. var min = 0;
  300. else var min = (tmp / 60);
  301. if ((new String(hour)).length < 2)
  302. hour = "0" + hour;
  303. if ((new String(min)).length < 2)
  304. min = "0" + min;
  305. var rtnVal = hour + ":" + min + ":" + strSec;
  306. return rtnVal
  307. }
  308. /**
  309. * Handles the use of the back button (saves data and closes SCO)
  310. */
  311. function doBack()
  312. {
  313. checkAnswers(true);
  314. doLMSSetValue( "cmi.core.exit", "suspend" );
  315. computeTime();
  316. exitPageStatus = true;
  317. var result;
  318. result = doLMSCommit();
  319. result = doLMSFinish();
  320. }
  321. /**
  322. * Handles the closure of the current SCO before an interruption. This is only useful if the LMS
  323. * deals with the cmi.core.exit, cmi.core.lesson_status and cmi.core.lesson_mode *and* the SCO
  324. * sends some kind of value for cmi.core.exit, which is not the case here (yet).
  325. */
  326. function doContinue(status)
  327. {
  328. // Reinitialize Exit to blank
  329. doLMSSetValue( "cmi.core.exit", "" );
  330. var mode = doLMSGetValue( "cmi.core.lesson_mode" );
  331. if ( mode != "review" && mode != "browse" )
  332. {
  333. doLMSSetValue( "cmi.core.lesson_status", status );
  334. }
  335. computeTime();
  336. exitPageStatus = true;
  337. var result;
  338. result = doLMSCommit();
  339. result = doLMSFinish();
  340. }
  341. /**
  342. * handles the recording of everything on a normal shutdown
  343. */
  344. function doQuit()
  345. {
  346. checkAnswers();
  347. computeTime();
  348. exitPageStatus = true;
  349. var result;
  350. result = doLMSCommit();
  351. result = doLMSFinish();
  352. }
  353. /**
  354. * Called upon unload event from body element
  355. */
  356. function unloadPage(status)
  357. {
  358. if (!exitPageStatus)
  359. {
  360. // doQuit( status );
  361. }
  362. }
  363. /**
  364. * Third section - depending on Chamilo - check answers and set score
  365. */
  366. var questions = new Array();
  367. var questions_answers = new Array();
  368. var questions_answers_correct = new Array();
  369. var questions_types = new Array();
  370. var questions_score_max = new Array();
  371. var questions_answers_ponderation = new Array();
  372. /**
  373. * Checks the answers on the test formular page
  374. */
  375. function checkAnswers(interrupted)
  376. {
  377. var tmpScore = 0;
  378. var status = 'not attempted';
  379. var scoreMax = 0;
  380. if (_debug) {
  381. console.log('questions_answers_correct:');
  382. console.log(questions_answers_correct);
  383. }
  384. for (var i=0; i < questions.length; i++) {
  385. if (questions[i] != undefined && questions[i] != null){
  386. var idQuestion = questions[i];
  387. var type = questions_types[idQuestion];
  388. var interactionScore = 0;
  389. var interactionAnswers = '';
  390. var interactionCorrectResponses = '';
  391. var interactionType = '';
  392. if (_debug) {
  393. console.log('Type: ' +type);
  394. console.log('idQuestion: ' +idQuestion);
  395. console.log('questions_answers: ');
  396. console.log(questions_answers[idQuestion]);
  397. console.log('questions_answers_ponderation: ');
  398. console.log(questions_answers_ponderation[idQuestion]);
  399. console.log('questions_answers_correct: ');
  400. console.log(questions_answers_correct[idQuestion]);
  401. }
  402. if (type == 'mcma') {
  403. interactionType = 'choice';
  404. var myScore = 0;
  405. for(var j=0; j<questions_answers[idQuestion].length;j++) {
  406. var idAnswer = questions_answers[idQuestion][j];
  407. var answer = document.getElementById('question_'+(idQuestion)+'_multiple_'+(idAnswer));
  408. if (answer.checked) {
  409. interactionAnswers += idAnswer+'__|';// changed by isaac flores
  410. myScore += questions_answers_ponderation[idQuestion][idAnswer];
  411. }
  412. }
  413. interactionScore = myScore;
  414. scoreMax += questions_score_max[idQuestion];
  415. if (_debug) {
  416. console.log("Score: "+myScore);
  417. }
  418. } else if (type == 'mcua') {
  419. interactionType = 'choice';
  420. var myScore = 0;
  421. for (var j=0; j<questions_answers[idQuestion].length;j++) {
  422. var idAnswer = questions_answers[idQuestion][j];
  423. var answer = document.getElementById('question_'+(idQuestion)+'_unique_'+(idAnswer));
  424. if (answer.checked) {
  425. interactionAnswers += idAnswer;
  426. if (_debug) {
  427. console.log("idAnswer: "+idAnswer);
  428. console.log("questions_answers_correct: "+questions_answers_correct[idQuestion][idAnswer]);
  429. }
  430. if (questions_answers_correct[idQuestion][idAnswer] == idAnswer) {
  431. if (questions_answers_ponderation[idQuestion][idAnswer]) {
  432. myScore += questions_answers_ponderation[idQuestion][idAnswer];
  433. } else {
  434. myScore++;
  435. }
  436. }
  437. }
  438. }
  439. if (_debug) {
  440. console.log("Score: "+myScore);
  441. }
  442. interactionScore = myScore;
  443. scoreMax += questions_score_max[idQuestion];
  444. } else if (type == 'tf') {
  445. interactionType = 'true-false';
  446. var myScore = 0;
  447. for (var j = 0; j < questions_answers[idQuestion].length; j++) {
  448. var idAnswer = questions_answers[idQuestion][j];
  449. var answer = document.getElementById('question_' + idQuestion + '_tf_' + (idAnswer));
  450. if (answer.checked.value) {
  451. interactionAnswers += idAnswer;
  452. for (k = 0; k < questions_answers_correct[idQuestion].length; k++) {
  453. if (questions_answers_correct[idQuestion][k] == idAnswer) {
  454. if (questions_answers_ponderation[idQuestion][idAnswer]) {
  455. myScore += questions_answers_ponderation[idQuestion][idAnswer];
  456. } else {
  457. myScore++;
  458. }
  459. }
  460. }
  461. }
  462. }
  463. if (_debug) {
  464. console.log("Score: "+myScore);
  465. }
  466. interactionScore = myScore;
  467. scoreMax += questions_score_max[idQuestion];
  468. } else if (type == 'fib') {
  469. interactionType = 'fill-in';
  470. var myScore = 0;
  471. for (var j = 0; j < questions_answers[idQuestion].length; j++) {
  472. var idAnswer = questions_answers[idQuestion][j];
  473. var answer = document.getElementById('question_'+(idQuestion)+'_fib_'+(idAnswer));
  474. if (answer.value) {
  475. interactionAnswers += answer.value + '__|';//changed by isaac flores
  476. for (k = 0; k < questions_answers_correct[idQuestion].length; k++) {
  477. if (questions_answers_correct[idQuestion][k] == answer.value) {
  478. if (questions_answers_ponderation[idQuestion][idAnswer]) {
  479. myScore += questions_answers_ponderation[idQuestion][idAnswer];
  480. } else {
  481. myScore++;
  482. }
  483. }
  484. }
  485. }
  486. }
  487. if (_debug) {
  488. console.log("Score: "+myScore);
  489. }
  490. interactionScore = myScore;
  491. scoreMax += questions_score_max[idQuestion];
  492. } else if (type == 'matching') {
  493. interactionType = 'matching';
  494. var myScore = 0;
  495. for (var j = 0; j < questions_answers[idQuestion].length; j++) {
  496. var idAnswer = questions_answers[idQuestion][j];
  497. var answer = document.getElementById('question_' + (idQuestion) + '_matching_' + (idAnswer));
  498. if (answer && answer.value) {
  499. interactionAnswers += answer.value + '__|';//changed by isaac flores
  500. for (k = 0; k < questions_answers_correct[idQuestion].length; k++) {
  501. var left = questions_answers_correct[idQuestion][k][0];
  502. var right = questions_answers_correct[idQuestion][k][1];
  503. if (left == idAnswer && right == answer.value) {
  504. if (questions_answers_ponderation[idQuestion][idAnswer]) {
  505. myScore += questions_answers_ponderation[idQuestion][idAnswer];
  506. } else {
  507. myScore++;
  508. }
  509. }
  510. }
  511. }
  512. }
  513. if (_debug) {
  514. console.log("Score: "+myScore);
  515. }
  516. interactionScore = myScore;
  517. scoreMax += questions_score_max[idQuestion];
  518. } else if (type == 'free') {
  519. //ignore for now as a score cannot be given
  520. interactionType = 'free';
  521. var answer = document.getElementById('question_'+(idQuestion)+'_free');
  522. if (answer && answer.value) {
  523. interactionAnswers += answer.value
  524. }
  525. //interactionScore = questions_score_max[idQuestion];
  526. interactionScore = 0;
  527. scoreMax += questions_score_max[idQuestion];
  528. //interactionAnswers = document.getElementById('question_'+(idQuestion)+'_free').value;
  529. //correct responses work by pattern, see SCORM Runtime Env Doc
  530. //interactionCorrectResponses += questions_answers_correct[idQuestion].toString();
  531. } else if (type == 'hotspot') {
  532. interactionType = 'sequencing';
  533. interactionScore = 0;
  534. //if(question_score && question_score[idQuestion]){
  535. // interactionScore = question_score[idQuestion];
  536. //} //else, 0
  537. //interactionAnswers = document.getElementById('question_'+(idQuestion)+'_free').innerHTML;
  538. //correct responses work by pattern, see SCORM Runtime Env Doc
  539. //for(k=0;k<questions_answers_correct[idQuestion].length;k++)
  540. //{
  541. // interactionCorrectResponses += questions_answers_correct[idQuestion][k].toString()+',';
  542. //}
  543. } else if (type == 'exact') {
  544. interactionType = 'exact';
  545. interactionScore = 0;
  546. var real_answers = new Array();
  547. for (var j = 0; j < questions_answers[idQuestion].length; j++) {
  548. var idAnswer = questions_answers[idQuestion][j];
  549. var answer = document.getElementById('question_' + (idQuestion) + '_exact_' + (idAnswer));
  550. if (answer.checked == true) {
  551. interactionAnswers += idAnswer+', ';
  552. if (questions_answers_correct[idQuestion][idAnswer] != 0) {
  553. real_answers[j] = true;
  554. } else {
  555. real_answers[j] = false;
  556. }
  557. } else {
  558. if (questions_answers_correct[idQuestion][idAnswer] != 0) {
  559. real_answers[j] = false;
  560. } else {
  561. real_answers[j] = true;
  562. }
  563. }
  564. }
  565. var final_answer = true;
  566. for (var z = 0; z < real_answers.length; z++) {
  567. if (real_answers[z] == false) {
  568. final_answer = false;
  569. }
  570. }
  571. interactionScore = 0;
  572. console.log(real_answers);
  573. if (final_answer) {
  574. //getting only the first score where we save the weight of all the question
  575. interactionScore = questions_answers_ponderation[idQuestion][1];
  576. }
  577. if (_debug) {
  578. console.log("Score: "+interactionScore);
  579. }
  580. scoreMax += questions_score_max[idQuestion];
  581. }
  582. tmpScore += interactionScore;
  583. doLMSSetValue('cmi.interactions.'+idQuestion+'.id', 'Q'+idQuestion);
  584. doLMSSetValue('cmi.interactions.'+idQuestion+'.type', interactionType);
  585. doLMSSetValue('cmi.interactions.'+idQuestion+'.student_response', interactionAnswers);
  586. doLMSSetValue('cmi.interactions.'+idQuestion+'.result', interactionScore);
  587. }
  588. }
  589. doLMSSetValue('cmi.core.score.min', 0);
  590. doLMSSetValue('cmi.core.score.max', scoreMax);
  591. doLMSSetValue('cmi.core.score.raw', tmpScore);
  592. //get status
  593. var mastery_score = doLMSGetValue('cmi.student_data.mastery_score');
  594. if (mastery_score <= 0) {
  595. mastery_score = (scoreMax * 0.80);
  596. }
  597. if (tmpScore > mastery_score) {
  598. status = 'passed';
  599. } else {
  600. status = 'failed';
  601. }
  602. if (_debug) {
  603. console.log('student_score: ' + tmpScore);
  604. console.log('mastery_score: ' + mastery_score);
  605. console.log('cmi.core.score.max: ' + scoreMax);
  606. console.log('cmi.core.lesson_status: ' + status);
  607. }
  608. doLMSSetValue('cmi.core.lesson_status', status);
  609. if (interrupted && (status != 'completed') && (status != 'passed')) {
  610. doLMSSetValue('cmi.core.exit', 'suspended');
  611. }
  612. return false; //do not submit the form
  613. }