qti2_classes.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. /**
  4. * Interface ImsAnswerInterface.
  5. */
  6. interface ImsAnswerInterface
  7. {
  8. /**
  9. * @param $questionIdent
  10. * @param $questionStatment
  11. *
  12. * @return mixed
  13. */
  14. public function imsExportResponses($questionIdent, $questionStatment);
  15. /**
  16. * @param $questionIdent
  17. * @param Question|null $question
  18. *
  19. * @return mixed
  20. */
  21. public function imsExportResponsesDeclaration($questionIdent, Question $question = null);
  22. }
  23. /**
  24. * @author Claro Team <cvs@claroline.net>
  25. * @author Yannick Warnier <yannick.warnier@beeznest.com> -
  26. * updated ImsAnswerHotspot to match QTI norms
  27. *
  28. * @package chamilo.exercise
  29. */
  30. class Ims2Question extends Question
  31. {
  32. /**
  33. * Include the correct answer class and create answer.
  34. *
  35. * @return Answer
  36. */
  37. public function setAnswer()
  38. {
  39. switch ($this->type) {
  40. case MCUA:
  41. $answer = new ImsAnswerMultipleChoice($this->id);
  42. return $answer;
  43. case MCMA:
  44. $answer = new ImsAnswerMultipleChoice($this->id);
  45. return $answer;
  46. case TF:
  47. $answer = new ImsAnswerMultipleChoice($this->id);
  48. return $answer;
  49. case FIB:
  50. $answer = new ImsAnswerFillInBlanks($this->id);
  51. return $answer;
  52. case MATCHING:
  53. case MATCHING_DRAGGABLE:
  54. $answer = new ImsAnswerMatching($this->id);
  55. return $answer;
  56. case FREE_ANSWER:
  57. $answer = new ImsAnswerFree($this->id);
  58. return $answer;
  59. case HOT_SPOT:
  60. $answer = new ImsAnswerHotspot($this->id);
  61. return $answer;
  62. default:
  63. $answer = null;
  64. break;
  65. }
  66. return $answer;
  67. }
  68. public function createAnswersForm($form)
  69. {
  70. return true;
  71. }
  72. /**
  73. * {@inheritdoc}
  74. */
  75. public function processAnswersCreation($form, $exercise)
  76. {
  77. return true;
  78. }
  79. }
  80. /**
  81. * Class.
  82. *
  83. * @package chamilo.exercise
  84. */
  85. class ImsAnswerMultipleChoice extends Answer implements ImsAnswerInterface
  86. {
  87. /**
  88. * Return the XML flow for the possible answers.
  89. */
  90. public function imsExportResponses($questionIdent, $questionStatment)
  91. {
  92. // @todo getAnswersList() converts the answers using api_html_entity_decode()
  93. $this->answerList = $this->getAnswersList(true);
  94. $out = ' <choiceInteraction responseIdentifier="'.$questionIdent.'" >'."\n";
  95. $out .= ' <prompt><![CDATA['.formatExerciseQtiText($questionStatment).']]></prompt>'."\n";
  96. if (is_array($this->answerList)) {
  97. foreach ($this->answerList as $current_answer) {
  98. $out .= '<simpleChoice identifier="answer_'.$current_answer['id'].'" fixed="false">
  99. <![CDATA['.formatExerciseQtiText($current_answer['answer']).']]>';
  100. if (isset($current_answer['comment']) && $current_answer['comment'] != '') {
  101. $out .= '<feedbackInline identifier="answer_'.$current_answer['id'].'">
  102. <![CDATA['.formatExerciseQtiText($current_answer['comment']).']]>
  103. </feedbackInline>';
  104. }
  105. $out .= '</simpleChoice>'."\n";
  106. }
  107. }
  108. $out .= ' </choiceInteraction>'."\n";
  109. return $out;
  110. }
  111. /**
  112. * Return the XML flow of answer ResponsesDeclaration.
  113. */
  114. public function imsExportResponsesDeclaration($questionIdent, Question $question = null)
  115. {
  116. $this->answerList = $this->getAnswersList(true);
  117. $type = $this->getQuestionType();
  118. if ($type == MCMA) {
  119. $cardinality = 'multiple';
  120. } else {
  121. $cardinality = 'single';
  122. }
  123. $out = ' <responseDeclaration identifier="'.$questionIdent.'" cardinality="'.$cardinality.'" baseType="identifier">'."\n";
  124. // Match the correct answers.
  125. if (is_array($this->answerList)) {
  126. $out .= ' <correctResponse>'."\n";
  127. foreach ($this->answerList as $current_answer) {
  128. if ($current_answer['correct']) {
  129. $out .= ' <value>answer_'.$current_answer['id'].'</value>'."\n";
  130. }
  131. }
  132. $out .= ' </correctResponse>'."\n";
  133. }
  134. // Add the grading
  135. if (is_array($this->answerList)) {
  136. $out .= ' <mapping>'."\n";
  137. foreach ($this->answerList as $current_answer) {
  138. if (isset($current_answer['grade'])) {
  139. $out .= ' <mapEntry mapKey="answer_'.$current_answer['id'].'" mappedValue="'.$current_answer['grade'].'" />'."\n";
  140. }
  141. }
  142. $out .= ' </mapping>'."\n";
  143. }
  144. $out .= ' </responseDeclaration>'."\n";
  145. return $out;
  146. }
  147. }
  148. /**
  149. * Class.
  150. *
  151. * @package chamilo.exercise
  152. */
  153. class ImsAnswerFillInBlanks extends Answer implements ImsAnswerInterface
  154. {
  155. private $answerList = [];
  156. private $gradeList = [];
  157. /**
  158. * Export the text with missing words.
  159. */
  160. public function imsExportResponses($questionIdent, $questionStatment)
  161. {
  162. $this->answerList = $this->getAnswersList(true);
  163. $text = isset($this->answerText) ? $this->answerText : '';
  164. if (is_array($this->answerList)) {
  165. foreach ($this->answerList as $key => $answer) {
  166. $key = $answer['id'];
  167. $answer = $answer['answer'];
  168. $len = api_strlen($answer);
  169. $text = str_replace('['.$answer.']', '<textEntryInteraction responseIdentifier="fill_'.$key.'" expectedLength="'.api_strlen($answer).'"/>', $text);
  170. }
  171. }
  172. return $text;
  173. }
  174. public function imsExportResponsesDeclaration($questionIdent, Question $question = null)
  175. {
  176. $this->answerList = $this->getAnswersList(true);
  177. $this->gradeList = $this->getGradesList();
  178. $out = '';
  179. if (is_array($this->answerList)) {
  180. foreach ($this->answerList as $answer) {
  181. $answerKey = $answer['id'];
  182. $answer = $answer['answer'];
  183. $out .= ' <responseDeclaration identifier="fill_'.$answerKey.'" cardinality="single" baseType="identifier">'."\n";
  184. $out .= ' <correctResponse>'."\n";
  185. $out .= ' <value><![CDATA['.formatExerciseQtiText($answer).']]></value>'."\n";
  186. $out .= ' </correctResponse>'."\n";
  187. if (isset($this->gradeList[$answerKey])) {
  188. $out .= ' <mapping>'."\n";
  189. $out .= ' <mapEntry mapKey="'.$answer.'" mappedValue="'.$this->gradeList[$answerKey].'"/>'."\n";
  190. $out .= ' </mapping>'."\n";
  191. }
  192. $out .= ' </responseDeclaration>'."\n";
  193. }
  194. }
  195. return $out;
  196. }
  197. }
  198. /**
  199. * Class.
  200. *
  201. * @package chamilo.exercise
  202. */
  203. class ImsAnswerMatching extends Answer implements ImsAnswerInterface
  204. {
  205. public $leftList = [];
  206. public $rightList = [];
  207. private $answerList = [];
  208. /**
  209. * Export the question part as a matrix-choice, with only one possible answer per line.
  210. */
  211. public function imsExportResponses($questionIdent, $questionStatment)
  212. {
  213. $this->answerList = $this->getAnswersList(true);
  214. $maxAssociation = max(count($this->leftList), count($this->rightList));
  215. $out = '<matchInteraction responseIdentifier="'.$questionIdent.'" maxAssociations="'.$maxAssociation.'">'."\n";
  216. $out .= $questionStatment;
  217. //add left column
  218. $out .= ' <simpleMatchSet>'."\n";
  219. if (is_array($this->leftList)) {
  220. foreach ($this->leftList as $leftKey => $leftElement) {
  221. $out .= '
  222. <simpleAssociableChoice identifier="left_'.$leftKey.'" >
  223. <![CDATA['.formatExerciseQtiText($leftElement['answer']).']]>
  224. </simpleAssociableChoice>'."\n";
  225. }
  226. }
  227. $out .= ' </simpleMatchSet>'."\n";
  228. //add right column
  229. $out .= ' <simpleMatchSet>'."\n";
  230. $i = 0;
  231. if (is_array($this->rightList)) {
  232. foreach ($this->rightList as $rightKey => $rightElement) {
  233. $out .= '<simpleAssociableChoice identifier="right_'.$i.'" >
  234. <![CDATA['.formatExerciseQtiText($rightElement['answer']).']]>
  235. </simpleAssociableChoice>'."\n";
  236. $i++;
  237. }
  238. }
  239. $out .= ' </simpleMatchSet>'."\n";
  240. $out .= '</matchInteraction>'."\n";
  241. return $out;
  242. }
  243. public function imsExportResponsesDeclaration($questionIdent, Question $question = null)
  244. {
  245. $this->answerList = $this->getAnswersList(true);
  246. $out = ' <responseDeclaration identifier="'.$questionIdent.'" cardinality="single" baseType="identifier">'."\n";
  247. $out .= ' <correctResponse>'."\n";
  248. $gradeArray = [];
  249. if (isset($this->leftList) && is_array($this->leftList)) {
  250. foreach ($this->leftList as $leftKey => $leftElement) {
  251. $i = 0;
  252. foreach ($this->rightList as $rightKey => $rightElement) {
  253. if (($leftElement['match'] == $rightElement['code'])) {
  254. $out .= ' <value>left_'.$leftKey.' right_'.$i.'</value>'."\n";
  255. $gradeArray['left_'.$leftKey.' right_'.$i] = $leftElement['grade'];
  256. }
  257. $i++;
  258. }
  259. }
  260. }
  261. $out .= ' </correctResponse>'."\n";
  262. if (is_array($gradeArray)) {
  263. $out .= ' <mapping>'."\n";
  264. foreach ($gradeArray as $gradeKey => $grade) {
  265. $out .= ' <mapEntry mapKey="'.$gradeKey.'" mappedValue="'.$grade.'"/>'."\n";
  266. }
  267. $out .= ' </mapping>'."\n";
  268. }
  269. $out .= ' </responseDeclaration>'."\n";
  270. return $out;
  271. }
  272. }
  273. /**
  274. * Class.
  275. *
  276. * @package chamilo.exercise
  277. */
  278. class ImsAnswerHotspot extends Answer implements ImsAnswerInterface
  279. {
  280. private $answerList = [];
  281. private $gradeList = [];
  282. /**
  283. * TODO update this to match hot spots instead of copying matching
  284. * Export the question part as a matrix-choice, with only one possible answer per line.
  285. */
  286. public function imsExportResponses($questionIdent, $questionStatment, $questionDesc = '', $questionMedia = '')
  287. {
  288. $this->answerList = $this->getAnswersList(true);
  289. $mediaFilePath = api_get_course_path().'/document/images/'.$questionMedia;
  290. $sysQuestionMediaPath = api_get_path(SYS_COURSE_PATH).$mediaFilePath;
  291. $questionMedia = api_get_path(WEB_COURSE_PATH).$mediaFilePath;
  292. $mimetype = mime_content_type($sysQuestionMediaPath);
  293. if (empty($mimetype)) {
  294. $mimetype = 'image/jpeg';
  295. }
  296. $text = ' <p>'.$questionStatment.'</p>'."\n";
  297. $text .= ' <graphicOrderInteraction responseIdentifier="hotspot_'.$questionIdent.'">'."\n";
  298. $text .= ' <prompt>'.$questionDesc.'</prompt>'."\n";
  299. $text .= ' <object type="'.$mimetype.'" width="250" height="230" data="'.$questionMedia.'">-</object>'."\n";
  300. if (is_array($this->answerList)) {
  301. foreach ($this->answerList as $key => $answer) {
  302. $key = $answer['id'];
  303. $answerTxt = $answer['answer'];
  304. $len = api_strlen($answerTxt);
  305. //coords are transformed according to QTIv2 rules here: http://www.imsproject.org/question/qtiv2p1pd/imsqti_infov2p1pd.html#element10663
  306. $coords = '';
  307. $type = 'default';
  308. switch ($answer['hotspot_type']) {
  309. case 'square':
  310. $type = 'rect';
  311. $res = [];
  312. $coords = preg_match('/^\s*(\d+);(\d+)\|(\d+)\|(\d+)\s*$/', $answer['hotspot_coord'], $res);
  313. $coords = $res[1].','.$res[2].','.((int) $res[1] + (int) $res[3]).",".((int) $res[2] + (int) $res[4]);
  314. break;
  315. case 'circle':
  316. $type = 'circle';
  317. $res = [];
  318. $coords = preg_match('/^\s*(\d+);(\d+)\|(\d+)\|(\d+)\s*$/', $answer['hotspot_coord'], $res);
  319. $coords = $res[1].','.$res[2].','.sqrt(pow(($res[1] - $res[3]), 2) + pow(($res[2] - $res[4])));
  320. break;
  321. case 'poly':
  322. $type = 'poly';
  323. $coords = str_replace([';', '|'], [',', ','], $answer['hotspot_coord']);
  324. break;
  325. case 'delineation':
  326. $type = 'delineation';
  327. $coords = str_replace([';', '|'], [',', ','], $answer['hotspot_coord']);
  328. break;
  329. }
  330. $text .= ' <hotspotChoice shape="'.$type.'" coords="'.$coords.'" identifier="'.$key.'"/>'."\n";
  331. }
  332. }
  333. $text .= ' </graphicOrderInteraction>'."\n";
  334. return $text;
  335. }
  336. public function imsExportResponsesDeclaration($questionIdent, Question $question = null)
  337. {
  338. $this->answerList = $this->getAnswersList(true);
  339. $this->gradeList = $this->getGradesList();
  340. $out = ' <responseDeclaration identifier="hotspot_'.$questionIdent.'" cardinality="ordered" baseType="identifier">'."\n";
  341. if (is_array($this->answerList)) {
  342. $out .= ' <correctResponse>'."\n";
  343. foreach ($this->answerList as $answerKey => $answer) {
  344. $answerKey = $answer['id'];
  345. $answer = $answer['answer'];
  346. $out .= '<value><![CDATA['.formatExerciseQtiText($answerKey).']]></value>';
  347. }
  348. $out .= ' </correctResponse>'."\n";
  349. }
  350. $out .= ' </responseDeclaration>'."\n";
  351. return $out;
  352. }
  353. }
  354. /**
  355. * Class.
  356. *
  357. * @package chamilo.exercise
  358. */
  359. class ImsAnswerFree extends Answer implements ImsAnswerInterface
  360. {
  361. /**
  362. * TODO implement
  363. * Export the question part as a matrix-choice, with only one possible answer per line.
  364. */
  365. public function imsExportResponses(
  366. $questionIdent,
  367. $questionStatment,
  368. $questionDesc = '',
  369. $questionMedia = ''
  370. ) {
  371. $questionDesc = formatExerciseQtiText($questionDesc);
  372. return '<extendedTextInteraction responseIdentifier="'.$questionIdent.'" >
  373. <prompt>
  374. '.$questionDesc.'
  375. </prompt>
  376. </extendedTextInteraction>';
  377. }
  378. public function imsExportResponsesDeclaration($questionIdent, Question $question = null)
  379. {
  380. $out = ' <responseDeclaration identifier="'.$questionIdent.'" cardinality="single" baseType="string">';
  381. $out .= '<outcomeDeclaration identifier="SCORE" cardinality="single" baseType="float">
  382. <defaultValue><value>'.$question->weighting.'</value></defaultValue></outcomeDeclaration>';
  383. $out .= ' </responseDeclaration>'."\n";
  384. return $out;
  385. }
  386. }