exercise_import.inc.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. <?php
  2. /**
  3. * @copyright (c) 2001-2006 Universite catholique de Louvain (UCL)
  4. *
  5. * @license http://www.gnu.org/copyleft/gpl.html (GPL) GENERAL PUBLIC LICENSE
  6. *
  7. * @package chamilo.exercise
  8. *
  9. * @author claro team <cvs@claroline.net>
  10. * @author Guillaume Lederer <guillaume@claroline.net>
  11. */
  12. /**
  13. * Security check
  14. */
  15. if (count(get_included_files()) == 1) {
  16. die('---');
  17. }
  18. /**
  19. * function to create a temporary directory (SAME AS IN MODULE ADMIN)
  20. */
  21. function tempdir($dir, $prefix = 'tmp', $mode = 0777)
  22. {
  23. if (substr($dir, -1) != '/') {
  24. $dir .= '/';
  25. }
  26. do {
  27. $path = $dir.$prefix.mt_rand(0, 9999999);
  28. } while (!mkdir($path, $mode));
  29. return $path;
  30. }
  31. /**
  32. * @return the path of the temporary directory where the exercise was uploaded and unzipped
  33. */
  34. function get_and_unzip_uploaded_exercise($baseWorkDir, $uploadPath)
  35. {
  36. global $_user;
  37. $_course = api_get_course_info();
  38. //Check if the file is valid (not to big and exists)
  39. if (!isset ($_FILES['userFile']) || !is_uploaded_file($_FILES['userFile']['tmp_name'])) {
  40. // upload failed
  41. return false;
  42. }
  43. if (preg_match('/.zip$/i', $_FILES['userFile']['name']) && FileManager::handle_uploaded_document(
  44. $_course,
  45. $_FILES['userFile'],
  46. $baseWorkDir,
  47. $uploadPath,
  48. $_user['user_id'],
  49. 0,
  50. null,
  51. 1
  52. )
  53. ) {
  54. if (!function_exists('gzopen')) {
  55. //claro_delete_file($uploadPath);
  56. return false;
  57. }
  58. // upload successfull
  59. return true;
  60. } else {
  61. //claro_delete_file($uploadPath);
  62. return false;
  63. }
  64. }
  65. /**
  66. * main function to import an exercise,
  67. *
  68. * @return an array as a backlog of what was really imported, and error or debug messages to display
  69. */
  70. function import_exercise($file)
  71. {
  72. global $exercise_info;
  73. global $element_pile;
  74. global $non_HTML_tag_to_avoid;
  75. global $record_item_body;
  76. // used to specify the question directory where files could be found in relation in any question
  77. global $questionTempDir;
  78. $archive_path = api_get_path(SYS_ARCHIVE_PATH).'qti2';
  79. $baseWorkDir = $archive_path;
  80. if (!is_dir($baseWorkDir)) {
  81. mkdir($baseWorkDir, api_get_permissions_for_new_directories(), true);
  82. }
  83. $uploadPath = '/';
  84. // set some default values for the new exercise
  85. $exercise_info = array();
  86. $exercise_info['name'] = preg_replace('/.zip$/i', '', $file);
  87. $exercise_info['question'] = array();
  88. $element_pile = array();
  89. // create parser and array to retrieve info from manifest
  90. $element_pile = array(); //pile to known the depth in which we are
  91. //$module_info = array (); //array to store the info we need
  92. // if file is not a .zip, then we cancel all
  93. if (!preg_match('/.zip$/i', $file)) {
  94. Display :: display_error_message(get_lang('You must upload a zip file'));
  95. return false;
  96. }
  97. // unzip the uploaded file in a tmp directory
  98. if (!get_and_unzip_uploaded_exercise($baseWorkDir, $uploadPath)) {
  99. Display :: display_error_message(get_lang('You must upload a zip file'));
  100. return false;
  101. }
  102. // find the different manifests for each question and parse them.
  103. $exerciseHandle = opendir($baseWorkDir);
  104. //$question_number = 0;
  105. $file_found = false;
  106. $operation = false;
  107. $result = false;
  108. // parse every subdirectory to search xml question files
  109. while (false !== ($file = readdir($exerciseHandle))) {
  110. if (is_dir($baseWorkDir.'/'.$file) && $file != "." && $file != "..") {
  111. //find each manifest for each question repository found
  112. $questionHandle = opendir($baseWorkDir.'/'.$file);
  113. while (false !== ($questionFile = readdir($questionHandle))) {
  114. if (preg_match('/.xml$/i', $questionFile)) {
  115. $result = parse_file($baseWorkDir, $file, $questionFile);
  116. $file_found = true;
  117. }
  118. }
  119. } elseif (preg_match('/.xml$/i', $file)) {
  120. $result = parse_file($baseWorkDir, '', $file);
  121. $file_found = true;
  122. } // else ignore file
  123. }
  124. if (!$file_found) {
  125. Display :: display_error_message(get_lang('No XML file found in the zip'));
  126. return false;
  127. }
  128. if ($result == false) {
  129. return false;
  130. }
  131. //add exercise in tool
  132. //1.create exercise
  133. $exercise = new Exercise();
  134. $exercise->exercise = $exercise_info['name'];
  135. $exercise->save();
  136. $last_exercise_id = $exercise->selectId();
  137. if (!empty($last_exercise_id)) {
  138. //For each question found...
  139. foreach ($exercise_info['question'] as $key => $question_array) {
  140. //2.create question
  141. $question = new Ims2Question();
  142. $question->type = $question_array['type'];
  143. $question->setAnswer();
  144. $question->updateTitle($question_array['title']); // question ...
  145. $type = $question->selectType();
  146. $question->type = constant($type); // type ...
  147. $question->save($last_exercise_id); // save computed grade
  148. $last_question_id = $question->selectId();
  149. //3.create answer
  150. $answer = new Answer($last_question_id);
  151. $answer->new_nbrAnswers = count($question_array['answer']);
  152. foreach ($question_array['answer'] as $key => $answers) {
  153. $split = explode('_', $key);
  154. $i = $split[1];
  155. $answer->new_answer[$i] = $answers['value']; // answer ...
  156. $answer->new_comment[$i] = $answers['feedback']; // comment ...
  157. $answer->new_position[$i] = $i; // position ...
  158. // correct answers ...
  159. if (in_array($key, $question_array['correct_answers'])) {
  160. $answer->new_correct[$i] = 1;
  161. } else {
  162. $answer->new_correct[$i] = 0;
  163. }
  164. $answer->new_weighting[$i] = $question_array['weighting'][$key];
  165. }
  166. $answer->save();
  167. }
  168. // delete the temp dir where the exercise was unzipped
  169. FileManager::my_delete($baseWorkDir.$uploadPath);
  170. $operation = true;
  171. }
  172. return $operation;
  173. }
  174. function parse_file($exercisePath, $file, $questionFile)
  175. {
  176. global $exercise_info;
  177. global $element_pile;
  178. global $non_HTML_tag_to_avoid;
  179. global $record_item_body;
  180. global $questionTempDir;
  181. $questionTempDir = $exercisePath.'/'.$file.'/';
  182. $questionFilePath = $questionTempDir.$questionFile;
  183. if (!($fp = @ fopen($questionFilePath, 'r'))) {
  184. Display :: display_error_message(get_lang('Error opening question\'s XML file'));
  185. return false;
  186. } else {
  187. $data = fread($fp, filesize($questionFilePath));
  188. }
  189. //parse XML question file
  190. $data = str_replace(array('<p>', '</p>', '<front>', '</front>'), '', $data);
  191. //used global variable start values declaration :
  192. $record_item_body = false;
  193. $non_HTML_tag_to_avoid = array(
  194. "SIMPLECHOICE",
  195. "CHOICEINTERACTION",
  196. "INLINECHOICEINTERACTION",
  197. "INLINECHOICE",
  198. "SIMPLEMATCHSET",
  199. "SIMPLEASSOCIABLECHOICE",
  200. "TEXTENTRYINTERACTION",
  201. "FEEDBACKINLINE",
  202. "MATCHINTERACTION",
  203. "ITEMBODY",
  204. "BR",
  205. "IMG"
  206. );
  207. //this array to detect tag not supported by claroline import in the xml file to warn the user.
  208. $non_supported_content_in_question = array(
  209. "GAPMATCHINTERACTION",
  210. "EXTENDEDTEXTINTERACTION",
  211. "HOTTEXTINTERACTION",
  212. "HOTSPOTINTERACTION",
  213. "SELECTPOINTINTERACTION",
  214. "GRAPHICORDERINTERACTION",
  215. "GRAPHICASSOCIATIONINTERACTION",
  216. "GRAPHICGAPMATCHINTERACTION",
  217. "POSITIONOBJECTINTERACTION",
  218. "SLIDERINTERACTION",
  219. "DRAWINGINTERACTION",
  220. "UPLOADINTERACTION",
  221. "RESPONSECONDITION",
  222. "RESPONSEIF"
  223. );
  224. $question_format_supported = true;
  225. $xml_parser = xml_parser_create();
  226. xml_parser_set_option($xml_parser, XML_OPTION_SKIP_WHITE, false);
  227. xml_set_element_handler($xml_parser, 'startElement', 'endElement');
  228. xml_set_character_data_handler($xml_parser, 'elementData');
  229. if (!xml_parse($xml_parser, $data, feof($fp))) {
  230. // if reading of the xml file in not successfull :
  231. // set errorFound, set error msg, break while statement
  232. Display :: display_error_message(get_lang('Error reading XML file'));
  233. return false;
  234. }
  235. //close file
  236. fclose($fp);
  237. if (!$question_format_supported) {
  238. Display :: display_error_message(
  239. get_lang(
  240. 'Unknown question format in file %file',
  241. array(
  242. '%file' => $questionFile
  243. )
  244. )
  245. );
  246. return false;
  247. }
  248. return true;
  249. }
  250. /**
  251. * Function used by the SAX xml parser when the parser meets a opening tag
  252. *
  253. * @param unknown_type $parser xml parser created with "xml_parser_create()"
  254. * @param unknown_type $name name of the element
  255. * @param unknown_type $attributes
  256. */
  257. function startElement($parser, $name, $attributes)
  258. {
  259. global $element_pile;
  260. global $exercise_info;
  261. global $current_question_ident;
  262. global $current_answer_id;
  263. global $current_match_set;
  264. global $currentAssociableChoice;
  265. global $current_question_item_body;
  266. global $record_item_body;
  267. global $non_HTML_tag_to_avoid;
  268. global $current_inlinechoice_id;
  269. global $cardinality;
  270. global $questionTempDir;
  271. array_push($element_pile, $name);
  272. $current_element = end($element_pile);
  273. if (sizeof($element_pile) >= 2) {
  274. $parent_element = $element_pile[sizeof($element_pile) - 2];
  275. } else {
  276. $parent_element = "";
  277. }
  278. if (sizeof($element_pile) >= 3) {
  279. $grant_parent_element = $element_pile[sizeof($element_pile) - 3];
  280. } else {
  281. $grant_parent_element = "";
  282. }
  283. if ($record_item_body) {
  284. if ((!in_array($current_element, $non_HTML_tag_to_avoid))) {
  285. $current_question_item_body .= "<".$name;
  286. foreach ($attributes as $attribute_name => $attribute_value) {
  287. $current_question_item_body .= " ".$attribute_name."=\"".$attribute_value."\"";
  288. }
  289. $current_question_item_body .= ">";
  290. } else {
  291. //in case of FIB question, we replace the IMS-QTI tag b y the correct answer between "[" "]",
  292. //we first save with claroline tags ,then when the answer will be parsed, the claroline tags will be replaced
  293. if ($current_element == 'INLINECHOICEINTERACTION') {
  294. $current_question_item_body .= "**claroline_start**".$attributes['RESPONSEIDENTIFIER']."**claroline_end**";
  295. }
  296. if ($current_element == 'TEXTENTRYINTERACTION') {
  297. $correct_answer_value = $exercise_info['question'][$current_question_ident]['correct_answers'][$current_answer_id];
  298. $current_question_item_body .= "[".$correct_answer_value."]";
  299. }
  300. if ($current_element == 'BR') {
  301. $current_question_item_body .= "<BR/>";
  302. }
  303. }
  304. }
  305. switch ($current_element) {
  306. case 'ASSESSMENTITEM' :
  307. {
  308. //retrieve current question
  309. $current_question_ident = $attributes['IDENTIFIER'];
  310. $exercise_info['question'][$current_question_ident] = array();
  311. $exercise_info['question'][$current_question_ident]['answer'] = array();
  312. $exercise_info['question'][$current_question_ident]['correct_answers'] = array();
  313. $exercise_info['question'][$current_question_ident]['title'] = $attributes['TITLE'];
  314. $exercise_info['question'][$current_question_ident]['tempdir'] = $questionTempDir;
  315. }
  316. break;
  317. case 'SECTION' :
  318. {
  319. //retrieve exercise name
  320. $exercise_info['name'] = $attributes['TITLE'];
  321. }
  322. break;
  323. case 'RESPONSEDECLARATION' :
  324. {
  325. //retrieve question type
  326. if ("multiple" == $attributes['CARDINALITY']) {
  327. $exercise_info['question'][$current_question_ident]['type'] = 'MCMA';
  328. $cardinality = 'multiple';
  329. }
  330. if ("single" == $attributes['CARDINALITY']) {
  331. $exercise_info['question'][$current_question_ident]['type'] = 'MCUA';
  332. $cardinality = 'single';
  333. }
  334. //needed for FIB
  335. $current_answer_id = $attributes['IDENTIFIER'];
  336. }
  337. break;
  338. case 'INLINECHOICEINTERACTION' :
  339. {
  340. $exercise_info['question'][$current_question_ident]['type'] = 'FIB';
  341. $exercise_info['question'][$current_question_ident]['subtype'] = 'LISTBOX_FILL';
  342. $current_answer_id = $attributes['RESPONSEIDENTIFIER'];
  343. }
  344. break;
  345. case 'INLINECHOICE' :
  346. {
  347. $current_inlinechoice_id = $attributes['IDENTIFIER'];
  348. }
  349. break;
  350. case 'TEXTENTRYINTERACTION' :
  351. {
  352. $exercise_info['question'][$current_question_ident]['type'] = 'FIB';
  353. $exercise_info['question'][$current_question_ident]['subtype'] = 'TEXTFIELD_FILL';
  354. $exercise_info['question'][$current_question_ident]['response_text'] = $current_question_item_body;
  355. //replace claroline tags
  356. }
  357. break;
  358. case 'MATCHINTERACTION' :
  359. {
  360. $exercise_info['question'][$current_question_ident]['type'] = 'MATCHING';
  361. }
  362. break;
  363. case 'SIMPLEMATCHSET' :
  364. {
  365. if (!isset ($current_match_set)) {
  366. $current_match_set = 1;
  367. } else {
  368. $current_match_set++;
  369. }
  370. $exercise_info['question'][$current_question_ident]['answer'][$current_match_set] = array();
  371. }
  372. break;
  373. case 'SIMPLEASSOCIABLECHOICE' :
  374. {
  375. $currentAssociableChoice = $attributes['IDENTIFIER'];
  376. }
  377. break;
  378. //retrieve answers id for MCUA and MCMA questions
  379. case 'SIMPLECHOICE' :
  380. $current_answer_id = $attributes['IDENTIFIER'];
  381. if (!isset($exercise_info['question'][$current_question_ident]['answer'][$current_answer_id])) {
  382. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id] = array();
  383. }
  384. break;
  385. case 'MAPENTRY' :
  386. {
  387. if ($parent_element == "MAPPING") {
  388. $answer_id = $attributes['MAPKEY'];
  389. if (!isset ($exercise_info['question'][$current_question_ident]['weighting'])) {
  390. $exercise_info['question'][$current_question_ident]['weighting'] = array();
  391. }
  392. $exercise_info['question'][$current_question_ident]['weighting'][$answer_id] = $attributes['MAPPEDVALUE'];
  393. }
  394. }
  395. break;
  396. case 'MAPPING' :
  397. {
  398. if (isset ($attributes['DEFAULTVALUE'])) {
  399. $exercise_info['question'][$current_question_ident]['default_weighting'] = $attributes['DEFAULTVALUE'];
  400. }
  401. }
  402. case 'ITEMBODY' :
  403. {
  404. $record_item_body = true;
  405. $current_question_item_body = '';
  406. }
  407. break;
  408. case 'IMG' :
  409. {
  410. $exercise_info['question'][$current_question_ident]['attached_file_url'] = $attributes['SRC'];
  411. }
  412. break;
  413. }
  414. }
  415. /**
  416. * Function used by the SAX xml parser when the parser meets a closing tag
  417. *
  418. * @param $parser xml parser created with "xml_parser_create()"
  419. * @param $name name of the element
  420. */
  421. function endElement($parser, $name)
  422. {
  423. global $element_pile;
  424. global $exercise_info;
  425. global $current_question_ident;
  426. global $record_item_body;
  427. global $current_question_item_body;
  428. global $non_HTML_tag_to_avoid;
  429. global $cardinality;
  430. $current_element = end($element_pile);
  431. //treat the record of the full content of itembody tag :
  432. if ($record_item_body && (!in_array($current_element, $non_HTML_tag_to_avoid))) {
  433. $current_question_item_body .= "</".$name.">";
  434. }
  435. switch ($name) {
  436. case 'ITEMBODY' :
  437. {
  438. $record_item_body = false;
  439. if ($exercise_info['question'][$current_question_ident]['type'] == 'FIB') {
  440. $exercise_info['question'][$current_question_ident]['response_text'] = $current_question_item_body;
  441. } else {
  442. $exercise_info['question'][$current_question_ident]['statement'] = $current_question_item_body;
  443. }
  444. }
  445. break;
  446. }
  447. array_pop($element_pile);
  448. }
  449. function elementData($parser, $data)
  450. {
  451. global $element_pile;
  452. global $exercise_info;
  453. global $current_question_ident;
  454. global $current_answer_id;
  455. global $current_match_set;
  456. global $currentAssociableChoice;
  457. global $current_question_item_body;
  458. global $record_item_body;
  459. global $non_HTML_tag_to_avoid;
  460. global $current_inlinechoice_id;
  461. global $cardinality;
  462. $current_element = end($element_pile);
  463. if (sizeof($element_pile) >= 2) {
  464. $parent_element = $element_pile[sizeof($element_pile) - 2];
  465. } else {
  466. $parent_element = "";
  467. }
  468. if (sizeof($element_pile) >= 3) {
  469. $grant_parent_element = $element_pile[sizeof($element_pile) - 3];
  470. } else {
  471. $grant_parent_element = "";
  472. }
  473. //treat the record of the full content of itembody tag (needed for question statment and/or FIB text:
  474. if ($record_item_body && (!in_array($current_element, $non_HTML_tag_to_avoid))) {
  475. $current_question_item_body .= $data;
  476. }
  477. switch ($current_element) {
  478. case 'SIMPLECHOICE' :
  479. if (!isset ($exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['value'])) {
  480. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['value'] = trim(
  481. $data
  482. );
  483. } else {
  484. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['value'] .= ''.trim(
  485. $data
  486. );
  487. }
  488. break;
  489. case 'FEEDBACKINLINE' :
  490. {
  491. if (!isset ($exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['feedback'])) {
  492. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['feedback'] = trim(
  493. $data
  494. );
  495. } else {
  496. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['feedback'] .= ' '.trim(
  497. $data
  498. );
  499. }
  500. }
  501. break;
  502. case 'SIMPLEASSOCIABLECHOICE' :
  503. {
  504. $exercise_info['question'][$current_question_ident]['answer'][$current_match_set][$currentAssociableChoice] = trim(
  505. $data
  506. );
  507. }
  508. break;
  509. case 'VALUE' :
  510. {
  511. if ($parent_element == "CORRECTRESPONSE") {
  512. if ($cardinality == "single") {
  513. $exercise_info['question'][$current_question_ident]['correct_answers'][$current_answer_id] = $data;
  514. } else {
  515. $exercise_info['question'][$current_question_ident]['correct_answers'][] = $data;
  516. }
  517. }
  518. }
  519. break;
  520. case 'ITEMBODY' :
  521. {
  522. $current_question_item_body .= $data;
  523. }
  524. break;
  525. case 'INLINECHOICE' :
  526. {
  527. // if this is the right answer, then we must replace the claroline tags in the FIB text bye the answer between "[" and "]" :
  528. $answer_identifier = $exercise_info['question'][$current_question_ident]['correct_answers'][$current_answer_id];
  529. if ($current_inlinechoice_id == $answer_identifier) {
  530. $current_question_item_body = str_replace(
  531. "**claroline_start**".$current_answer_id."**claroline_end**",
  532. "[".$data."]",
  533. $current_question_item_body
  534. );
  535. } else // save wrong answers in an array
  536. {
  537. if (!isset ($exercise_info['question'][$current_question_ident]['wrong_answers'])) {
  538. $exercise_info['question'][$current_question_ident]['wrong_answers'] = array();
  539. }
  540. $exercise_info['question'][$current_question_ident]['wrong_answers'][] = $data;
  541. }
  542. }
  543. break;
  544. }
  545. }