qti2_export.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. /**
  4. * @author Claro Team <cvs@claroline.net>
  5. * @author Yannick Warnier <yannick.warnier@beeznest.com>
  6. *
  7. * @package chamilo.exercise
  8. */
  9. require __DIR__.'/qti2_classes.php';
  10. /**
  11. * An IMS/QTI item. It corresponds to a single question.
  12. * This class allows export from Claroline to IMS/QTI2.0 XML format of a single question.
  13. * It is not usable as-is, but must be subclassed, to support different kinds of questions.
  14. *
  15. * Every start_*() and corresponding end_*(), as well as export_*() methods return a string.
  16. *
  17. * note: Attached files are NOT exported.
  18. *
  19. * @package chamilo.exercise
  20. */
  21. class ImsAssessmentItem
  22. {
  23. /**
  24. * @var Ims2Question
  25. */
  26. public $question;
  27. /**
  28. * @var string
  29. */
  30. public $questionIdent;
  31. /**
  32. * @var ImsAnswerInterface
  33. */
  34. public $answer;
  35. /**
  36. * Constructor.
  37. *
  38. * @param Ims2Question $question ims2Question object we want to export
  39. */
  40. public function __construct($question)
  41. {
  42. $this->question = $question;
  43. $this->answer = $this->question->setAnswer();
  44. $this->questionIdent = "QST_".$question->id;
  45. }
  46. /**
  47. * Start the XML flow.
  48. *
  49. * This opens the <item> block, with correct attributes.
  50. */
  51. public function start_item()
  52. {
  53. $categoryTitle = '';
  54. if (!empty($this->question->category)) {
  55. $category = new TestCategory();
  56. $category = $category->getCategory($this->question->category);
  57. if ($category) {
  58. $categoryTitle = htmlspecialchars(formatExerciseQtiText($category->name));
  59. }
  60. }
  61. $string = '<assessmentItem xmlns="http://www.imsglobal.org/xsd/imsqti_v2p1"
  62. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  63. xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqti_v2p1 imsqti_v2p1.xsd"
  64. identifier="'.$this->questionIdent.'"
  65. title = "'.htmlspecialchars(formatExerciseQtiText($this->question->selectTitle())).'"
  66. category = "'.$categoryTitle.'"
  67. >'."\n";
  68. return $string;
  69. }
  70. /**
  71. * End the XML flow, closing the </item> tag.
  72. */
  73. public function end_item()
  74. {
  75. return "</assessmentItem>\n";
  76. }
  77. /**
  78. * Start the itemBody.
  79. */
  80. public function start_item_body()
  81. {
  82. return ' <itemBody>'."\n";
  83. }
  84. /**
  85. * End the itemBody part.
  86. */
  87. public function end_item_body()
  88. {
  89. return " </itemBody>\n";
  90. }
  91. /**
  92. * add the response processing template used.
  93. */
  94. public function add_response_processing()
  95. {
  96. return ' <responseProcessing template="http://www.imsglobal.org/question/qti_v2p1/rptemplates/map_correct"/>'."\n";
  97. }
  98. /**
  99. * Export the question as an IMS/QTI Item.
  100. *
  101. * This is a default behaviour, some classes may want to override this.
  102. *
  103. * @param $standalone: Boolean stating if it should be exported as a stand-alone question
  104. *
  105. * @return string string, the XML flow for an Item
  106. */
  107. public function export($standalone = false)
  108. {
  109. $head = $foot = '';
  110. if ($standalone) {
  111. $head = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'."\n";
  112. }
  113. //TODO understand why answer might be a non-object sometimes
  114. if (!is_object($this->answer)) {
  115. return $head;
  116. }
  117. $res = $head
  118. .$this->start_item()
  119. .$this->answer->imsExportResponsesDeclaration($this->questionIdent, $this->question)
  120. .$this->start_item_body()
  121. .$this->answer->imsExportResponses(
  122. $this->questionIdent,
  123. $this->question->question,
  124. $this->question->description,
  125. $this->question->getPictureFilename()
  126. )
  127. .$this->end_item_body()
  128. .$this->add_response_processing()
  129. .$this->end_item()
  130. .$foot;
  131. return $res;
  132. }
  133. }
  134. /**
  135. * This class represents an entire exercise to be exported in IMS/QTI.
  136. * It will be represented by a single <section> containing several <item>.
  137. *
  138. * Some properties cannot be exported, as IMS does not support them :
  139. * - type (one page or multiple pages)
  140. * - start_date and end_date
  141. * - max_attempts
  142. * - show_answer
  143. * - anonymous_attempts
  144. *
  145. * @author Amand Tihon <amand@alrj.org>
  146. *
  147. * @package chamilo.exercise
  148. */
  149. class ImsSection
  150. {
  151. public $exercise;
  152. /**
  153. * Constructor.
  154. *
  155. * @param Exercise $exe The Exercise instance to export
  156. *
  157. * @author Amand Tihon <amand@alrj.org>
  158. */
  159. public function __construct($exe)
  160. {
  161. $this->exercise = $exe;
  162. }
  163. public function start_section()
  164. {
  165. $out = '<section
  166. ident = "EXO_'.$this->exercise->selectId().'"
  167. title = "'.cleanAttribute(formatExerciseQtiDescription($this->exercise->selectTitle())).'"
  168. >'."\n";
  169. return $out;
  170. }
  171. public function end_section()
  172. {
  173. return "</section>\n";
  174. }
  175. public function export_duration()
  176. {
  177. if ($max_time = $this->exercise->selectTimeLimit()) {
  178. // return exercise duration in ISO8601 format.
  179. $minutes = floor($max_time / 60);
  180. $seconds = $max_time % 60;
  181. return '<duration>PT'.$minutes.'M'.$seconds."S</duration>\n";
  182. } else {
  183. return '';
  184. }
  185. }
  186. /**
  187. * Export the presentation (Exercise's description).
  188. *
  189. * @author Amand Tihon <amand@alrj.org>
  190. */
  191. public function export_presentation()
  192. {
  193. $out = "<presentation_material><flow_mat><material>\n"
  194. ." <mattext><![CDATA[".formatExerciseQtiDescription($this->exercise->selectDescription())."]]></mattext>\n"
  195. ."</material></flow_mat></presentation_material>\n";
  196. return $out;
  197. }
  198. /**
  199. * Export the ordering information.
  200. * Either sequential, through all questions, or random, with a selected number of questions.
  201. *
  202. * @author Amand Tihon <amand@alrj.org>
  203. */
  204. public function export_ordering()
  205. {
  206. $out = '';
  207. if ($n = $this->exercise->getShuffle()) {
  208. $out .= "<selection_ordering>"
  209. ." <selection>\n"
  210. ." <selection_number>".$n."</selection_number>\n"
  211. ." </selection>\n"
  212. .' <order order_type="Random" />'
  213. ."\n</selection_ordering>\n";
  214. } else {
  215. $out .= '<selection_ordering sequence_type="Normal">'."\n"
  216. ." <selection />\n"
  217. ."</selection_ordering>\n";
  218. }
  219. return $out;
  220. }
  221. /**
  222. * Export the questions, as a succession of <items>.
  223. *
  224. * @author Amand Tihon <amand@alrj.org>
  225. */
  226. public function exportQuestions()
  227. {
  228. $out = '';
  229. foreach ($this->exercise->selectQuestionList() as $q) {
  230. $out .= export_question_qti($q, false);
  231. }
  232. return $out;
  233. }
  234. /**
  235. * Export the exercise in IMS/QTI.
  236. *
  237. * @param bool $standalone wether it should include XML tag and DTD line
  238. *
  239. * @return string string containing the XML flow
  240. *
  241. * @author Amand Tihon <amand@alrj.org>
  242. */
  243. public function export($standalone)
  244. {
  245. $head = $foot = '';
  246. if ($standalone) {
  247. $head = '<?xml version = "1.0" encoding = "UTF-8" standalone = "no"?>'."\n"
  248. .'<!DOCTYPE questestinterop SYSTEM "ims_qtiasiv2p1.dtd">'."\n"
  249. ."<questestinterop>\n";
  250. $foot = "</questestinterop>\n";
  251. }
  252. $out = $head
  253. .$this->start_section()
  254. .$this->export_duration()
  255. .$this->export_presentation()
  256. .$this->export_ordering()
  257. .$this->exportQuestions()
  258. .$this->end_section()
  259. .$foot;
  260. return $out;
  261. }
  262. }
  263. /*
  264. Some quick notes on identifiers generation.
  265. The IMS format requires some blocks, like items, responses, feedbacks, to be uniquely
  266. identified.
  267. The unicity is mandatory in a single XML, of course, but it's prefered that the identifier stays
  268. coherent for an entire site.
  269. Here's the method used to generate those identifiers.
  270. Question identifier :: "QST_" + <Question Id from the DB> + "_" + <Question numeric type>
  271. Response identifier :: <Question identifier> + "_A_" + <Response Id from the DB>
  272. Condition identifier :: <Question identifier> + "_C_" + <Response Id from the DB>
  273. Feedback identifier :: <Question identifier> + "_F_" + <Response Id from the DB>
  274. */
  275. /**
  276. * Class ImsItem.
  277. *
  278. * An IMS/QTI item. It corresponds to a single question.
  279. * This class allows export from Claroline to IMS/QTI XML format.
  280. * It is not usable as-is, but must be subclassed, to support different kinds of questions.
  281. *
  282. * Every start_*() and corresponding end_*(), as well as export_*() methods return a string.
  283. *
  284. * warning: Attached files are NOT exported.
  285. *
  286. * @author Amand Tihon <amand@alrj.org>
  287. *
  288. * @package chamilo.exercise
  289. */
  290. class ImsItem
  291. {
  292. public $question;
  293. public $questionIdent;
  294. public $answer;
  295. /**
  296. * Constructor.
  297. *
  298. * @param Question $question the Question object we want to export
  299. *
  300. * @author Anamd Tihon
  301. */
  302. public function __construct($question)
  303. {
  304. $this->question = $question;
  305. $this->answer = $question->answer;
  306. $this->questionIdent = "QST_".$question->selectId();
  307. }
  308. /**
  309. * Start the XML flow.
  310. *
  311. * This opens the <item> block, with correct attributes.
  312. *
  313. * @author Amand Tihon <amand@alrj.org>
  314. */
  315. public function start_item()
  316. {
  317. return '<item title="'.cleanAttribute(formatExerciseQtiDescription($this->question->selectTitle())).'" ident="'.$this->questionIdent.'">'."\n";
  318. }
  319. /**
  320. * End the XML flow, closing the </item> tag.
  321. *
  322. * @author Amand Tihon <amand@alrj.org>
  323. */
  324. public function end_item()
  325. {
  326. return "</item>\n";
  327. }
  328. /**
  329. * Create the opening, with the question itself.
  330. *
  331. * This means it opens the <presentation> but doesn't close it, as this is the role of end_presentation().
  332. * In between, the export_responses from the subclass should have been called.
  333. *
  334. * @author Amand Tihon <amand@alrj.org>
  335. */
  336. public function start_presentation()
  337. {
  338. return '<presentation label="'.$this->questionIdent.'"><flow>'."\n"
  339. .'<material><mattext>'.formatExerciseQtiDescription($this->question->selectDescription())."</mattext></material>\n";
  340. }
  341. /**
  342. * End the </presentation> part, opened by export_header.
  343. *
  344. * @author Amand Tihon <amand@alrj.org>
  345. */
  346. public function end_presentation()
  347. {
  348. return "</flow></presentation>\n";
  349. }
  350. /**
  351. * Start the response processing, and declare the default variable, SCORE, at 0 in the outcomes.
  352. *
  353. * @author Amand Tihon <amand@alrj.org>
  354. */
  355. public function start_processing()
  356. {
  357. return '<resprocessing><outcomes><decvar vartype="Integer" defaultval="0" /></outcomes>'."\n";
  358. }
  359. /**
  360. * End the response processing part.
  361. *
  362. * @author Amand Tihon <amand@alrj.org>
  363. */
  364. public function end_processing()
  365. {
  366. return "</resprocessing>\n";
  367. }
  368. /**
  369. * Export the question as an IMS/QTI Item.
  370. *
  371. * This is a default behaviour, some classes may want to override this.
  372. *
  373. * @param $standalone: Boolean stating if it should be exported as a stand-alone question
  374. *
  375. * @return string string, the XML flow for an Item
  376. *
  377. * @author Amand Tihon <amand@alrj.org>
  378. */
  379. public function export($standalone = false)
  380. {
  381. global $charset;
  382. $head = $foot = "";
  383. if ($standalone) {
  384. $head = '<?xml version = "1.0" encoding = "'.$charset.'" standalone = "no"?>'."\n"
  385. .'<!DOCTYPE questestinterop SYSTEM "ims_qtiasiv2p1.dtd">'."\n"
  386. ."<questestinterop>\n";
  387. $foot = "</questestinterop>\n";
  388. }
  389. return $head
  390. .$this->start_item()
  391. .$this->start_presentation()
  392. .$this->answer->imsExportResponses($this->questionIdent)
  393. .$this->end_presentation()
  394. .$this->start_processing()
  395. .$this->answer->imsExportProcessing($this->questionIdent)
  396. .$this->end_processing()
  397. .$this->answer->imsExportFeedback($this->questionIdent)
  398. .$this->end_item()
  399. .$foot;
  400. }
  401. }
  402. /**
  403. * Send a complete exercise in IMS/QTI format, from its ID.
  404. *
  405. * @param int $exerciseId The exercise to export
  406. * @param bool $standalone wether it should include XML tag and DTD line
  407. *
  408. * @return string XML as a string, or an empty string if there's no exercise with given ID
  409. */
  410. function export_exercise_to_qti($exerciseId, $standalone = true)
  411. {
  412. $exercise = new Exercise();
  413. if (!$exercise->read($exerciseId)) {
  414. return '';
  415. }
  416. $ims = new ImsSection($exercise);
  417. $xml = $ims->export($standalone);
  418. return $xml;
  419. }
  420. /**
  421. * Returns the XML flow corresponding to one question.
  422. *
  423. * @param int $questionId
  424. * @param bool $standalone (ie including XML tag, DTD declaration, etc)
  425. *
  426. * @return string
  427. */
  428. function export_question_qti($questionId, $standalone = true)
  429. {
  430. $question = new Ims2Question();
  431. $qst = $question->read($questionId);
  432. if (!$qst) {
  433. return '';
  434. }
  435. $isValid = $qst instanceof UniqueAnswer || $qst instanceof MultipleAnswer || $qst instanceof FreeAnswer;
  436. if (!$isValid) {
  437. return '';
  438. }
  439. $question->id = $qst->id;
  440. $question->type = $qst->type;
  441. $question->question = $qst->question;
  442. $question->description = $qst->description;
  443. $question->weighting = $qst->weighting;
  444. $question->position = $qst->position;
  445. $question->picture = $qst->picture;
  446. $question->category = $qst->category;
  447. $ims = new ImsAssessmentItem($question);
  448. return $ims->export($standalone);
  449. }
  450. /**
  451. * Clean text like a description.
  452. */
  453. function formatExerciseQtiDescription($text)
  454. {
  455. $entities = api_html_entity_decode($text);
  456. return htmlspecialchars($entities);
  457. }
  458. /**
  459. * Clean titles.
  460. *
  461. * @param $text
  462. *
  463. * @return string
  464. */
  465. function formatExerciseQtiText($text)
  466. {
  467. return htmlspecialchars($text);
  468. }
  469. /**
  470. * @param string $text
  471. *
  472. * @return string
  473. */
  474. function cleanAttribute($text)
  475. {
  476. return $text;
  477. }