fill_blanks.class.php 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. /**
  4. * Class FillBlanks
  5. *
  6. * @author Eric Marguin
  7. * @author Julio Montoya multiple fill in blank option added
  8. * @package chamilo.exercise
  9. **/
  10. class FillBlanks extends Question
  11. {
  12. static $typePicture = 'fill_in_blanks.png';
  13. static $explanationLangVar = 'FillBlanks';
  14. const FILL_THE_BLANK_STANDARD = 0;
  15. const FILL_THE_BLANK_MENU = 1;
  16. const FILL_THE_BLANK_SEVERAL_ANSWER = 2;
  17. /**
  18. * Constructor
  19. */
  20. public function __construct()
  21. {
  22. parent::__construct();
  23. $this->type = FILL_IN_BLANKS;
  24. $this->isContent = $this->getIsContent();
  25. }
  26. /**
  27. * function which redefines Question::createAnswersForm
  28. * @param FormValidator $form
  29. */
  30. public function createAnswersForm($form)
  31. {
  32. $defaults = array();
  33. if (!empty($this->id)) {
  34. $objectAnswer = new Answer($this->id);
  35. $answer = $objectAnswer->selectAnswer(1);
  36. $listAnswersInfo = FillBlanks::getAnswerInfo($answer);
  37. if ($listAnswersInfo["switchable"]) {
  38. $defaults['multiple_answer'] = 1;
  39. } else {
  40. $defaults['multiple_answer'] = 0;
  41. }
  42. //take the complete string except after the last '::'
  43. $defaults['answer'] = $listAnswersInfo["text"];
  44. $defaults['select_separator'] = $listAnswersInfo["blankseparatornumber"];
  45. $blanksepartornumber = $listAnswersInfo["blankseparatornumber"];
  46. } else {
  47. $defaults['answer'] = get_lang('DefaultTextInBlanks');
  48. $defaults['select_separator'] = 0;
  49. $blanksepartornumber = 0;
  50. }
  51. $blankSeparatorStart = self::getStartSeparator($blanksepartornumber);
  52. $blankSeparatorEnd = self::getEndSeparator($blanksepartornumber);
  53. $setValues = null;
  54. if (isset($a_weightings) && count($a_weightings) > 0) {
  55. foreach ($a_weightings as $i => $weighting) {
  56. $setValues .= 'document.getElementById("weighting['.$i.']").value = "'.$weighting.'";';
  57. }
  58. }
  59. // javascript
  60. echo '<script>
  61. var blankSeparatortStart = "'.$blankSeparatorStart.'";
  62. var blankSeparatortEnd = "'.$blankSeparatorEnd.'";
  63. var blankSeparatortStartRegexp = getBlankSeparatorRegexp(blankSeparatortStart);
  64. var blankSeparatortEndRegexp = getBlankSeparatorRegexp(blankSeparatortEnd);
  65. CKEDITOR.on("instanceCreated", function(e) {
  66. if (e.editor.name === "answer") {
  67. e.editor.on("change", updateBlanks);
  68. }
  69. });
  70. var firstTime = true;
  71. function updateBlanks()
  72. {
  73. if (firstTime) {
  74. var field = document.getElementById("answer");
  75. var answer = field.value;
  76. } else {
  77. var answer = CKEDITOR.instances["answer"].getData();
  78. }
  79. // disable the save button, if not blanks have been created
  80. $("button").attr("disabled", "disabled");
  81. $("#defineoneblank").show();
  82. var blanksRegexp = "/"+blankSeparatortStartRegexp+"[^"+blankSeparatortStartRegexp+"]*"+blankSeparatortEndRegexp+"/g";
  83. var blanks = answer.match(eval(blanksRegexp));
  84. var fields = "<div class=\"form-group \">";
  85. fields += "<label class=\"col-sm-2 control-label\">'.get_lang('Weighting').'</label>";
  86. fields += "<div class=\"col-sm-8\">";
  87. fields += "<table>";
  88. fields += "<tr><th style=\"padding:0 20px\">'.get_lang("WordTofind").'</th><th style=\"padding:0 20px\">'.get_lang("QuestionWeighting").'</th><th style=\"padding:0 20px\">'.get_lang("BlankInputSize").'</th></tr>";
  89. if (blanks != null) {
  90. for (var i=0 ; i < blanks.length ; i++){
  91. // remove forbidden characters that causes bugs
  92. blanks[i] = removeForbiddenChars(blanks[i]);
  93. // trim blanks between brackets
  94. blanks[i] = trimBlanksBetweenSeparator(blanks[i], blankSeparatortStart, blankSeparatortEnd);
  95. // if the word is empty []
  96. if (blanks[i] == blankSeparatortStartRegexp+blankSeparatortEndRegexp) {
  97. break;
  98. }
  99. // get input size
  100. var lainputsize = 200;
  101. if ($("#samplesize\\\["+i+"\\\]").width()) {
  102. lainputsize = $("#samplesize\\\["+i+"\\\]").width();
  103. }
  104. if (document.getElementById("weighting["+i+"]")) {
  105. var value = document.getElementById("weighting["+i+"]").value;
  106. } else {
  107. var value = "10";
  108. }
  109. fields += "<tr>";
  110. fields += "<td>"+blanks[i]+"</td>";
  111. fields += "<td><input style=\"width:35px\" value=\""+value+"\" type=\"text\" id=\"weighting["+i+"]\" name=\"weighting["+i+"]\" /></td>";
  112. fields += "<td>";
  113. fields += "<input type=\"button\" value=\"-\" onclick=\"changeInputSize(-1, "+i+")\">";
  114. fields += "<input type=\"button\" value=\"+\" onclick=\"changeInputSize(1, "+i+")\">";
  115. fields += "<input value=\""+blanks[i].substr(1, blanks[i].length - 2)+"\" style=\"width:"+lainputsize+"px\" disabled=disabled id=\"samplesize["+i+"]\"/>";
  116. fields += "<input type=\"hidden\" id=\"sizeofinput["+i+"]\" name=\"sizeofinput["+i+"]\" value=\""+lainputsize+"\" \"/>";
  117. fields += "</td>";
  118. fields += "</tr>";
  119. // enable the save button
  120. $("button").removeAttr("disabled");
  121. $("#defineoneblank").hide();
  122. }
  123. }
  124. document.getElementById("blanks_weighting").innerHTML = fields + "</table></div></div>";
  125. if (firstTime) {
  126. firstTime = false;
  127. ';
  128. if (isset($listAnswersInfo) && count($listAnswersInfo["tabweighting"]) > 0) {
  129. foreach ($listAnswersInfo["tabweighting"] as $i => $weighting) {
  130. if (!empty($i)) {
  131. echo 'document.getElementById("weighting['.$i.']").value = "'.$weighting.'";';
  132. }
  133. }
  134. foreach ($listAnswersInfo["tabinputsize"] as $i => $sizeOfInput) {
  135. if (!empty($i)) {
  136. echo 'document.getElementById("sizeofinput['.$i.']").value = "'.$sizeOfInput.'";';
  137. echo '$("#samplesize\\\['.$i.'\\\]").width('.$sizeOfInput.');';
  138. }
  139. }
  140. }
  141. echo '}
  142. }
  143. window.onload = updateBlanks;
  144. function getInputSize() {
  145. var outTabSize = new Array();
  146. $("input").each(function() {
  147. if ($(this).attr("id") && $(this).attr("id").match(/samplesize/)) {
  148. var tabidnum = $(this).attr("id").match(/\d+/);
  149. var idnum = tabidnum[0];
  150. var thewidth = $(this).next().attr("value");
  151. tabInputSize[idnum] = thewidth;
  152. }
  153. });
  154. }
  155. function changeInputSize(inCoef, inIdNum)
  156. {
  157. var currentWidth = $("#samplesize\\\["+inIdNum+"\\\]").width();
  158. var newWidth = currentWidth + inCoef * 20;
  159. newWidth = Math.max(20, newWidth);
  160. newWidth = Math.min(newWidth, 600);
  161. $("#samplesize\\\["+inIdNum+"\\\]").width(newWidth);
  162. $("#sizeofinput\\\["+inIdNum+"\\\]").attr("value", newWidth);
  163. }
  164. function removeForbiddenChars(inTxt) {
  165. outTxt = inTxt;
  166. outTxt = outTxt.replace(/&quot;/g, ""); // remove the char
  167. outTxt = outTxt.replace(/\x22/g, ""); // remove the char
  168. outTxt = outTxt.replace(/"/g, ""); // remove the char
  169. outTxt = outTxt.replace(/\\\\/g, ""); // remove the \ char
  170. outTxt = outTxt.replace(/&nbsp;/g, " ");
  171. outTxt = outTxt.replace(/^ +/, "");
  172. outTxt = outTxt.replace(/ +$/, "");
  173. return outTxt;
  174. }
  175. function changeBlankSeparator()
  176. {
  177. var separatorNumber = $("#select_separator").val();
  178. var tabSeparator = getSeparatorFromNumber(separatorNumber);
  179. blankSeparatortStart = tabSeparator[0];
  180. blankSeparatortEnd = tabSeparator[1];
  181. blankSeparatortStartRegexp = getBlankSeparatorRegexp(blankSeparatortStart);
  182. blankSeparatortEndRegexp = getBlankSeparatorRegexp(blankSeparatortEnd);
  183. updateBlanks();
  184. }
  185. // this function is the same than the PHP one
  186. // if modify it modify the php one escapeForRegexp
  187. function getBlankSeparatorRegexp(inTxt)
  188. {
  189. var tabSpecialChar = new Array(".", "+", "*", "?", "[", "^", "]", "$", "(", ")",
  190. "{", "}", "=", "!", "<", ">", "|", ":", "-", ")");
  191. for (var i=0; i < tabSpecialChar.length; i++) {
  192. if (inTxt == tabSpecialChar[i]) {
  193. return "\\\"+inTxt;
  194. }
  195. }
  196. return inTxt;
  197. }
  198. // this function is the same than the PHP one
  199. // if modify it modify the php one getAllowedSeparator
  200. function getSeparatorFromNumber(innumber)
  201. {
  202. tabSeparator = new Array();
  203. tabSeparator[0] = new Array("[", "]");
  204. tabSeparator[1] = new Array("{", "}");
  205. tabSeparator[2] = new Array("(", ")");
  206. tabSeparator[3] = new Array("*", "*");
  207. tabSeparator[4] = new Array("#", "#");
  208. tabSeparator[5] = new Array("%", "%");
  209. tabSeparator[6] = new Array("$", "$");
  210. return tabSeparator[innumber];
  211. }
  212. function trimBlanksBetweenSeparator(inTxt, inSeparatorStart, inSeparatorEnd)
  213. {
  214. // blankSeparatortStartRegexp
  215. // blankSeparatortEndRegexp
  216. var result = inTxt
  217. result = result.replace(inSeparatorStart, "");
  218. result = result.replace(inSeparatorEnd, "");
  219. result = result.trim();
  220. return inSeparatorStart+result+inSeparatorEnd;
  221. }
  222. </script>';
  223. // answer
  224. $form->addElement('label', null, '<br /><br />'.get_lang('TypeTextBelow').', '.get_lang('And').' '.get_lang('UseTagForBlank'));
  225. $form->addElement(
  226. 'html_editor',
  227. 'answer',
  228. '<img src="../img/fill_field.png">',
  229. ['id' => 'answer', 'onkeyup' => "javascript: updateBlanks(this);"],
  230. array('ToolbarSet' => 'TestQuestionDescription')
  231. );
  232. $form->addRule('answer',get_lang('GiveText'),'required');
  233. //added multiple answers
  234. $form->addElement('checkbox','multiple_answer','', get_lang('FillInBlankSwitchable'));
  235. $form->addElement('select', 'select_separator', get_lang("SelectFillTheBlankSeparator"), self::getAllowedSeparatorForSelect(), ' id="select_separator" style="width:150px" onchange="changeBlankSeparator()" ');
  236. $form->addElement('label', null, '<input type="button" onclick="updateBlanks()" value="'.get_lang('RefreshBlanks').'" class="btn" />');
  237. $form->addElement('html','<div id="blanks_weighting"></div>');
  238. global $text;
  239. // setting the save button here and not in the question class.php
  240. $form->addElement('html','<div id="defineoneblank" style="color:#D04A66; margin-left:160px">'.get_lang('DefineBlanks').'</div>');
  241. $form->addButtonSave($text, 'submitQuestion');
  242. if (!empty($this->id)) {
  243. $form->setDefaults($defaults);
  244. } else {
  245. if ($this->isContent == 1) {
  246. $form->setDefaults($defaults);
  247. }
  248. }
  249. }
  250. /**
  251. * Function which creates the form to create/edit the answers of the question
  252. * @param FormValidator $form
  253. */
  254. public function processAnswersCreation($form)
  255. {
  256. $answer = $form->getSubmitValue('answer');
  257. // Due the ckeditor transform the elements to their HTML value
  258. //$answer = api_html_entity_decode($answer, ENT_QUOTES, $charset);
  259. //$answer = htmlentities(api_utf8_encode($answer));
  260. // remove the :: eventually written by the user
  261. $answer = str_replace('::', '', $answer);
  262. // remove starting and ending space and &nbsp;
  263. $answer = api_preg_replace("/\xc2\xa0/", " ", $answer);
  264. // start and end separator
  265. $blankStartSeparator = self::getStartSeparator($form->getSubmitValue('select_separator'));
  266. $blankEndSeparator = self::getEndSeparator($form->getSubmitValue('select_separator'));
  267. $blankStartSeparatorRegexp = self::escapeForRegexp($blankStartSeparator);
  268. $blankEndSeparatorRegexp = self::escapeForRegexp($blankEndSeparator);
  269. // remove spaces at the beginning and the end of text in square brackets
  270. $answer = preg_replace_callback(
  271. "/".$blankStartSeparatorRegexp."[^]]+".$blankEndSeparatorRegexp."/",
  272. function ($matches) use ($blankStartSeparator, $blankEndSeparator) {
  273. $matchingResult = $matches[0];
  274. $matchingResult = trim($matchingResult, $blankStartSeparator);
  275. $matchingResult = trim($matchingResult, $blankEndSeparator);
  276. $matchingResult = trim($matchingResult);
  277. // remove forbidden chars
  278. $matchingResult = str_replace("/\\/", "", $matchingResult);
  279. $matchingResult = str_replace('/"/', "", $matchingResult);
  280. return $blankStartSeparator.$matchingResult.$blankEndSeparator;
  281. },
  282. $answer
  283. );
  284. // get the blanks weightings
  285. $nb = preg_match_all(
  286. '/'.$blankStartSeparatorRegexp.'[^'.$blankStartSeparatorRegexp.']*'.$blankEndSeparatorRegexp.'/',
  287. $answer,
  288. $blanks
  289. );
  290. if (isset($_GET['editQuestion'])) {
  291. $this->weighting = 0;
  292. }
  293. /* if we have some [tobefound] in the text
  294. build the string to save the following in the answers table
  295. <p>I use a [computer] and a [pen].</p>
  296. becomes
  297. <p>I use a [computer] and a [pen].</p>::100,50:100,50@1
  298. ++++++++-------**
  299. --- -- --- -- -
  300. A B (C) (D)(E)
  301. +++++++ : required, weighting of each words
  302. ------- : optional, input width to display, 200 if not present
  303. ** : equal @1 if "Allow answers order switches" has been checked, @ otherwise
  304. A : weighting for the word [computer]
  305. B : weighting for the word [pen]
  306. C : input width for the word [computer]
  307. D : input width for the word [pen]
  308. E : equal @1 if "Allow answers order switches" has been checked, @ otherwise
  309. */
  310. if ($nb > 0) {
  311. $answer .= '::';
  312. // weighting
  313. for ($i=0; $i < $nb; ++$i) {
  314. // enter the weighting of word $i
  315. $answer .= $form->getSubmitValue('weighting['.$i.']');
  316. // not the last word, add ","
  317. if ($i != $nb - 1) {
  318. $answer .= ",";
  319. }
  320. // calculate the global weighting for the question
  321. $this -> weighting += $form->getSubmitValue('weighting['.$i.']');
  322. }
  323. // input width
  324. $answer .= ":";
  325. for ($i=0; $i < $nb; ++$i) {
  326. // enter the width of input for word $i
  327. $answer .= $form->getSubmitValue('sizeofinput['.$i.']');
  328. // not the last word, add ","
  329. if ($i != $nb - 1) {
  330. $answer .= ",";
  331. }
  332. }
  333. }
  334. // write the blank separator code number
  335. // see function getAllowedSeparator
  336. /*
  337. 0 [...]
  338. 1 {...}
  339. 2 (...)
  340. 3 *...*
  341. 4 #...#
  342. 5 %...%
  343. 6 $...$
  344. */
  345. $answer .= ":".$form->getSubmitValue('select_separator');
  346. // Allow answers order switches
  347. $is_multiple = $form -> getSubmitValue('multiple_answer');
  348. $answer.= '@'.$is_multiple;
  349. $this->save();
  350. $objAnswer = new Answer($this->id);
  351. $objAnswer->createAnswer($answer, 0, '', 0, 1);
  352. $objAnswer->save();
  353. }
  354. /**
  355. * @param null $feedback_type
  356. * @param null $counter
  357. * @param null $score
  358. * @return null|string
  359. */
  360. public function return_header($feedback_type = null, $counter = null, $score = null)
  361. {
  362. $header = parent::return_header($feedback_type, $counter, $score);
  363. $header .= '<table class="'.$this->question_table_class .'">
  364. <tr>
  365. <th>'.get_lang("Answer").'</th>
  366. </tr>';
  367. return $header;
  368. }
  369. /**
  370. * @param $separatorStartRegexp
  371. * @param $separatorEndRegexp
  372. * @param $correctItemRegexp
  373. * @param $questionId
  374. * @param $correctItem
  375. * @param $attributes
  376. * @param $answer
  377. * @param $listAnswersInfo
  378. * @param $displayForStudent
  379. * @return string
  380. */
  381. public static function getFillTheBlankHtml(
  382. $separatorStartRegexp,
  383. $separatorEndRegexp,
  384. $correctItemRegexp,
  385. $questionId,
  386. $correctItem,
  387. $attributes,
  388. $answer,
  389. $listAnswersInfo,
  390. $displayForStudent,
  391. $inBlankNumber
  392. ) {
  393. $result = "";
  394. $inTabTeacherSolution = $listAnswersInfo['tabwords'];
  395. $inTeacherSolution = $inTabTeacherSolution[$inBlankNumber];
  396. switch (self::getFillTheBlankAnswerType($inTeacherSolution)) {
  397. case self::FILL_THE_BLANK_MENU:
  398. $selected = "";
  399. // the blank menu
  400. $optionMenu = "";
  401. // display a menu from answer separated with |
  402. // if display for student, shuffle the correct answer menu
  403. $listMenu = self::getFillTheBlankMenuAnswers($inTeacherSolution, $displayForStudent);
  404. $result .= '<select name="choice['.$questionId.'][]">';
  405. for ($k=0; $k < count($listMenu); $k++) {
  406. $selected = "";
  407. if ($correctItem == $listMenu[$k]) {
  408. $selected = " selected=selected ";
  409. }
  410. // if in teacher view, display the first item by default, which is the right answer
  411. if ($k==0 && !$displayForStudent) {
  412. $selected = " selected=selected ";
  413. }
  414. $optionMenu .= '<option '.$selected.' value="'.$listMenu[$k].'">'.$listMenu[$k].'</option>';
  415. }
  416. if ($selected == "") {
  417. // no good answer have been found...
  418. $selected = " selected=selected ";
  419. }
  420. $result .= "<option $selected value=''>--</option>";
  421. $result .= $optionMenu;
  422. $result .= '</select>';
  423. break;
  424. case self::FILL_THE_BLANK_SEVERAL_ANSWER:
  425. //no break
  426. case self::FILL_THE_BLANK_STANDARD:
  427. default:
  428. $result = Display::input('text', "choice[$questionId][]", $correctItem, $attributes);
  429. break;
  430. }
  431. return $result;
  432. }
  433. /**
  434. * Return an array with the different choices available
  435. * when the answers between bracket show as a menu
  436. * @param string $correctAnswer
  437. * @param bool $displayForStudent true if we want to shuffle the choices of the menu for students
  438. *
  439. * @return array
  440. */
  441. public static function getFillTheBlankMenuAnswers($correctAnswer, $displayForStudent)
  442. {
  443. // if $inDisplayForStudent, then shuffle the result array
  444. $listChoises = api_preg_split("/\|/", $correctAnswer);
  445. if ($displayForStudent) {
  446. shuffle($listChoises);
  447. }
  448. return $listChoises;
  449. }
  450. /**
  451. * Return the array index of the student answer
  452. * @param string $correctAnswer the menu Choice1|Choice2|Choice3
  453. * @param string $studentAnswer the student answer must be Choice1 or Choice2 or Choice3
  454. *
  455. * @return int in the example 0 1 or 2 depending of the choice of the student
  456. */
  457. public static function getFillTheBlankMenuAnswerNum($correctAnswer, $studentAnswer)
  458. {
  459. $listChoices = self::getFillTheBlankMenuAnswers($correctAnswer, false);
  460. foreach ($listChoices as $num => $value) {
  461. if ($value == $studentAnswer) {
  462. return $num;
  463. }
  464. }
  465. // should not happened, because student choose the answer in a menu of possible answers
  466. return -1;
  467. }
  468. /**
  469. * Return the possible answer if the answer between brackets is a multiple choice menu
  470. * @param string $correctAnswer
  471. *
  472. * @return array
  473. */
  474. public static function getFillTheBlankSeveralAnswers($correctAnswer)
  475. {
  476. // is answer||Answer||response||Response , mean answer or Answer ...
  477. $listSeveral = api_preg_split("/\|\|/", $correctAnswer);
  478. return $listSeveral;
  479. }
  480. /**
  481. * Return true if student answer is right according to the correctAnswer
  482. * it is not as simple as equality, because of the type of Fill The Blank question
  483. * eg : studentAnswer = 'Un' and correctAnswer = 'Un||1||un'
  484. * @param string $studentAnswer [studentanswer] of the info array of the answer field
  485. * @param string $correctAnswer [tabwords] of the info array of the answer field
  486. *
  487. * @return bool
  488. */
  489. public static function isGoodStudentAnswer($studentAnswer, $correctAnswer)
  490. {
  491. switch (self::getFillTheBlankAnswerType($correctAnswer)) {
  492. case self::FILL_THE_BLANK_MENU:
  493. $listMenu = self::getFillTheBlankMenuAnswers($correctAnswer, false);
  494. $result = $listMenu[0] == $studentAnswer;
  495. break;
  496. case self::FILL_THE_BLANK_SEVERAL_ANSWER:
  497. // the answer must be one of the choice made
  498. $listSeveral = self::getFillTheBlankSeveralAnswers($correctAnswer);
  499. $result = in_array($studentAnswer, $listSeveral);
  500. break;
  501. case self::FILL_THE_BLANK_STANDARD:
  502. default:
  503. $result = $studentAnswer == $correctAnswer;
  504. break;
  505. }
  506. return $result;
  507. }
  508. /**
  509. * @param string $correctAnswer
  510. *
  511. * @return int
  512. */
  513. public static function getFillTheBlankAnswerType($correctAnswer)
  514. {
  515. if (api_strpos($correctAnswer, "|") && !api_strpos($correctAnswer, "||")) {
  516. return self::FILL_THE_BLANK_MENU;
  517. } elseif (api_strpos($correctAnswer, "||")) {
  518. return self::FILL_THE_BLANK_SEVERAL_ANSWER;
  519. } else {
  520. return self::FILL_THE_BLANK_STANDARD;
  521. }
  522. }
  523. /**
  524. * Return information about the answer
  525. * @param string $userAnswer the text of the answer of the question
  526. * @param bool $isStudentAnswer true if it's a student answer false the empty question model
  527. *
  528. * @return array of information about the answer
  529. */
  530. public static function getAnswerInfo($userAnswer = "", $isStudentAnswer = false)
  531. {
  532. $listAnswerResults = array();
  533. $listAnswerResults['text'] = "";
  534. $listAnswerResults['wordsCount'] = 0;
  535. $listAnswerResults['tabwordsbracket'] = array();
  536. $listAnswerResults['tabwords'] = array();
  537. $listAnswerResults['tabweighting'] = array();
  538. $listAnswerResults['tabinputsize'] = array();
  539. $listAnswerResults['switchable'] = "";
  540. $listAnswerResults['studentanswer'] = array();
  541. $listAnswerResults['studentscore'] = array();
  542. $listAnswerResults['blankseparatornumber'] = 0;
  543. $listDoubleColon = array();
  544. api_preg_match("/(.*)::(.*)$/s", $userAnswer, $listResult);
  545. if (count($listResult) < 2) {
  546. $listDoubleColon[] = '';
  547. $listDoubleColon[] = '';
  548. } else {
  549. $listDoubleColon[] = $listResult[1];
  550. $listDoubleColon[] = $listResult[2];
  551. }
  552. $listAnswerResults['systemstring'] = $listDoubleColon[1];
  553. // make sure we only take the last bit to find special marks
  554. $listArobaseSplit = explode('@', $listDoubleColon[1]);
  555. if (count($listArobaseSplit) < 2) {
  556. $listArobaseSplit[1] = "";
  557. }
  558. // take the complete string except after the last '::'
  559. $listDetails = explode(":", $listArobaseSplit[0]);
  560. // < number of item after the ::[score]:[size]:[separator_id]@ , here there are 3
  561. if (count($listDetails) < 3) {
  562. $listWeightings = explode(',', $listDetails[0]);
  563. $listSizeOfInput = array();
  564. for ($i=0; $i < count($listWeightings); $i++) {
  565. $listSizeOfInput[] = 200;
  566. }
  567. $blankSeparatorNumber = 0; // 0 is [...]
  568. } else {
  569. $listWeightings = explode(',', $listDetails[0]);
  570. $listSizeOfInput = explode(',', $listDetails[1]);
  571. $blankSeparatorNumber = $listDetails[2];
  572. }
  573. $listAnswerResults['text'] = $listDoubleColon[0];
  574. $listAnswerResults['tabweighting'] = $listWeightings;
  575. $listAnswerResults['tabinputsize'] = $listSizeOfInput;
  576. $listAnswerResults['switchable'] = $listArobaseSplit[1];
  577. $listAnswerResults['blankseparatorstart'] = self::getStartSeparator($blankSeparatorNumber);
  578. $listAnswerResults['blankseparatorend'] = self::getEndSeparator($blankSeparatorNumber);
  579. $listAnswerResults['blankseparatornumber'] = $blankSeparatorNumber;
  580. $blankCharStart = self::getStartSeparator($blankSeparatorNumber);
  581. $blankCharEnd = self::getEndSeparator($blankSeparatorNumber);
  582. $blankCharStartForRegexp = self::escapeForRegexp($blankCharStart);
  583. $blankCharEndForRegexp = self::escapeForRegexp($blankCharEnd);
  584. // get all blanks words
  585. $listAnswerResults['wordsCount'] = api_preg_match_all(
  586. '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
  587. $listDoubleColon[0],
  588. $listWords
  589. );
  590. if ($listAnswerResults['wordsCount'] > 0) {
  591. $listAnswerResults['tabwordsbracket'] = $listWords[0];
  592. // remove [ and ] in string
  593. array_walk(
  594. $listWords[0],
  595. function (&$value, $key, $tabBlankChar) {
  596. $trimChars = "";
  597. for ($i=0; $i < count($tabBlankChar); $i++) {
  598. $trimChars .= $tabBlankChar[$i];
  599. }
  600. $value = trim($value, $trimChars);
  601. },
  602. array($blankCharStart, $blankCharEnd)
  603. );
  604. $listAnswerResults['tabwords'] = $listWords[0];
  605. }
  606. // get all common words
  607. $commonWords = api_preg_replace(
  608. '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
  609. "::",
  610. $listDoubleColon[0]
  611. );
  612. // if student answer, the second [] is the student answer,
  613. // the third is if student scored or not
  614. $listBrackets = array();
  615. $listWords = array();
  616. if ($isStudentAnswer) {
  617. for ($i=0; $i < count($listAnswerResults['tabwords']); $i++) {
  618. $listBrackets[] = $listAnswerResults['tabwordsbracket'][$i];
  619. $listWords[] = $listAnswerResults['tabwords'][$i];
  620. if ($i+1 < count($listAnswerResults['tabwords'])) {
  621. // should always be
  622. $i++;
  623. }
  624. $listAnswerResults['studentanswer'][] = $listAnswerResults['tabwords'][$i];
  625. if ($i+1 < count($listAnswerResults['tabwords'])) {
  626. // should always be
  627. $i++;
  628. }
  629. $listAnswerResults['studentscore'][] = $listAnswerResults['tabwords'][$i];
  630. }
  631. $listAnswerResults['tabwords'] = $listWords;
  632. $listAnswerResults['tabwordsbracket'] = $listBrackets;
  633. // if we are in student view, we've got 3 times :::::: for common words
  634. $commonWords = api_preg_replace("/::::::/", "::", $commonWords);
  635. }
  636. $listAnswerResults['commonwords'] = explode("::", $commonWords);
  637. return $listAnswerResults;
  638. }
  639. /**
  640. * Replace the occurrence of blank word with [correct answer][student answer][answer is correct]
  641. * @param array $listWithStudentAnswer
  642. *
  643. * @return string
  644. */
  645. public static function getAnswerInStudentAttempt($listWithStudentAnswer)
  646. {
  647. $separatorStart = $listWithStudentAnswer['blankseparatorstart'];
  648. $separatorEnd = $listWithStudentAnswer['blankseparatorend'];
  649. // lets rebuild the sentence with [correct answer][student answer][answer is correct]
  650. $result = "";
  651. for ($i=0; $i < count($listWithStudentAnswer['commonwords']) - 1; $i++) {
  652. $result .= $listWithStudentAnswer['commonwords'][$i];
  653. $result .= $listWithStudentAnswer['tabwordsbracket'][$i];
  654. $result .= $separatorStart.$listWithStudentAnswer['studentanswer'][$i].$separatorEnd;
  655. $result .= $separatorStart.$listWithStudentAnswer['studentscore'][$i].$separatorEnd;
  656. }
  657. $result .= $listWithStudentAnswer['commonwords'][$i];
  658. $result .= "::";
  659. // add the system string
  660. $result .= $listWithStudentAnswer['systemstring'];
  661. return $result;
  662. }
  663. /**
  664. * This function is the same than the js one above getBlankSeparatorRegexp
  665. * @param string $inChar
  666. *
  667. * @return string
  668. */
  669. public static function escapeForRegexp($inChar)
  670. {
  671. $listChars = [
  672. ".",
  673. "+",
  674. "*",
  675. "?",
  676. "[",
  677. "^",
  678. "]",
  679. "$",
  680. "(",
  681. ")",
  682. "{",
  683. "}",
  684. "=",
  685. "!",
  686. ">",
  687. "|",
  688. ":",
  689. "-",
  690. ")",
  691. ];
  692. if (in_array($inChar, $listChars)) {
  693. return "\\".$inChar;
  694. } else {
  695. return $inChar;
  696. }
  697. }
  698. /**
  699. * return $text protected for use in regexp
  700. * @param string $text
  701. *
  702. * @return mixed
  703. */
  704. public static function getRegexpProtected($text)
  705. {
  706. $listRegexpCharacters = [
  707. "/",
  708. ".",
  709. "+",
  710. "*",
  711. "?",
  712. "[",
  713. "^",
  714. "]",
  715. "$",
  716. "(",
  717. ")",
  718. "{",
  719. "}",
  720. "=",
  721. "!",
  722. ">",
  723. "|",
  724. ":",
  725. "-",
  726. ")",
  727. ];
  728. $result = $text;
  729. for ($i=0; $i < count($listRegexpCharacters); $i++) {
  730. $result = str_replace($listRegexpCharacters[$i], "\\".$listRegexpCharacters[$i], $result);
  731. }
  732. return $result;
  733. }
  734. /**
  735. * This function must be the same than the js one getSeparatorFromNumber above
  736. * @return array
  737. */
  738. public static function getAllowedSeparator()
  739. {
  740. $fillBlanksAllowedSeparator = array(
  741. array('[', ']'),
  742. array('{', '}'),
  743. array('(', ')'),
  744. array('*', '*'),
  745. array('#', '#'),
  746. array('%', '%'),
  747. array('$', '$'),
  748. );
  749. return $fillBlanksAllowedSeparator;
  750. }
  751. /**
  752. * return the start separator for answer
  753. * @param string $number
  754. *
  755. * @return mixed
  756. */
  757. public static function getStartSeparator($number)
  758. {
  759. $listSeparators = self::getAllowedSeparator();
  760. return $listSeparators[$number][0];
  761. }
  762. /**
  763. * return the end separator for answer
  764. * @param string $number
  765. *
  766. * @return mixed
  767. */
  768. public static function getEndSeparator($number)
  769. {
  770. $listSeparators = self::getAllowedSeparator();
  771. return $listSeparators[$number][1];
  772. }
  773. /**
  774. * Return as a description text, array of allowed separators for question
  775. * eg: array("[...]", "(...)")
  776. * @return array
  777. */
  778. public static function getAllowedSeparatorForSelect()
  779. {
  780. $listResults = array();
  781. $fillBlanksAllowedSeparator = self::getAllowedSeparator();
  782. for ($i=0; $i < count($fillBlanksAllowedSeparator); $i++) {
  783. $listResults[] = $fillBlanksAllowedSeparator[$i][0]."...".$fillBlanksAllowedSeparator[$i][1];
  784. }
  785. return $listResults;
  786. }
  787. /**
  788. * return the code number of the separator for the question
  789. * @param string $startSeparator
  790. * @param string $endSeparator
  791. *
  792. * @return int
  793. */
  794. public function getDefaultSeparatorNumber($startSeparator, $endSeparator)
  795. {
  796. $listSeparators = self::getAllowedSeparator();
  797. $result = 0;
  798. for ($i=0; $i < count($listSeparators); $i++) {
  799. if ($listSeparators[$i][0] == $startSeparator &&
  800. $listSeparators[$i][1] == $endSeparator
  801. ) {
  802. $result = $i;
  803. }
  804. }
  805. return $result;
  806. }
  807. /**
  808. * return the HTML display of the answer
  809. * @param string $answer
  810. * @param bool $resultsDisabled
  811. *
  812. * @return string
  813. */
  814. public static function getHtmlDisplayForAnswer($answer, $resultsDisabled = false)
  815. {
  816. $result = "";
  817. $listStudentAnswerInfo = self::getAnswerInfo($answer, true);
  818. // rebuild the answer with good HTML style
  819. // this is the student answer, right or wrong
  820. for ($i=0; $i < count($listStudentAnswerInfo['studentanswer']); $i++) {
  821. if ($listStudentAnswerInfo['studentscore'][$i] == 1) {
  822. $listStudentAnswerInfo['studentanswer'][$i] = self::getHtmlRightAnswer(
  823. $listStudentAnswerInfo['studentanswer'][$i],
  824. $listStudentAnswerInfo['tabwords'][$i],
  825. $resultsDisabled
  826. );
  827. } else {
  828. $listStudentAnswerInfo['studentanswer'][$i] = self::getHtmlWrongAnswer(
  829. $listStudentAnswerInfo['studentanswer'][$i],
  830. $listStudentAnswerInfo['tabwords'][$i],
  831. $resultsDisabled
  832. );
  833. }
  834. }
  835. // rebuild the sentence with student answer inserted
  836. for ($i=0; $i < count($listStudentAnswerInfo['commonwords']); $i++) {
  837. $result .= isset($listStudentAnswerInfo['commonwords'][$i]) ? $listStudentAnswerInfo['commonwords'][$i] : '';
  838. $result .= isset($listStudentAnswerInfo['studentanswer'][$i]) ? $listStudentAnswerInfo['studentanswer'][$i] : '';
  839. }
  840. // the last common word (should be </p>)
  841. $result .= isset($listStudentAnswerInfo['commonwords'][$i]) ? $listStudentAnswerInfo['commonwords'][$i] : '';
  842. return $result;
  843. }
  844. /**
  845. * return the HTML code of answer for correct and wrong answer
  846. * @param string $answer
  847. * @param string $correct
  848. * @param string $right
  849. * @param bool $resultsDisabled
  850. *
  851. * @return string
  852. */
  853. public static function getHtmlAnswer($answer, $correct, $right, $resultsDisabled = false)
  854. {
  855. $style = "color: green";
  856. if (!$right) {
  857. $style = "color: red; text-decoration: line-through;";
  858. }
  859. $type = FillBlanks::getFillTheBlankAnswerType($correct);
  860. switch ($type) {
  861. case self::FILL_THE_BLANK_MENU:
  862. $correctAnswerHtml = "";
  863. $listPossibleAnswers = FillBlanks::getFillTheBlankMenuAnswers($correct, false);
  864. $correctAnswerHtml .= "<span style='color: green'>".$listPossibleAnswers[0]."</span>";
  865. $correctAnswerHtml .= " <span style='font-weight:normal'>(";
  866. for ($i=1; $i < count($listPossibleAnswers); $i++) {
  867. $correctAnswerHtml .= $listPossibleAnswers[$i];
  868. if ($i != count($listPossibleAnswers) - 1) {
  869. $correctAnswerHtml .= " | ";
  870. }
  871. }
  872. $correctAnswerHtml .= ")</span>";
  873. break;
  874. case self::FILL_THE_BLANK_SEVERAL_ANSWER:
  875. $listCorrects = explode("||", $correct);
  876. $firstCorrect = $correct;
  877. if (count($listCorrects) > 0) {
  878. $firstCorrect = $listCorrects[0];
  879. }
  880. $correctAnswerHtml = "<span style='color: green'>".$firstCorrect."</span>";
  881. break;
  882. case self::FILL_THE_BLANK_STANDARD:
  883. default:
  884. $correctAnswerHtml = "<span style='color: green'>".$correct."</span>";
  885. }
  886. if ($resultsDisabled) {
  887. $correctAnswerHtml = "<span title='".get_lang("ExerciseWithFeedbackWithoutCorrectionComment")."'> - </span>";
  888. }
  889. $result = "<span style='border:1px solid black; border-radius:5px; padding:2px; font-weight:bold;'>";
  890. $result .= "<span style='$style'>".$answer."</span>";
  891. $result .= "&nbsp;<span style='font-size:120%;'>/</span>&nbsp;";
  892. $result .= $correctAnswerHtml;
  893. $result .= "</span>";
  894. return $result;
  895. }
  896. /**
  897. * return HTML code for correct answer
  898. * @param string $answer
  899. * @param string $correct
  900. * @param bool $resultsDisabled
  901. *
  902. * @return string
  903. */
  904. public static function getHtmlRightAnswer($answer, $correct, $resultsDisabled = false)
  905. {
  906. return self::getHtmlAnswer($answer, $correct, true, $resultsDisabled);
  907. }
  908. /**
  909. * return HTML code for wrong answer
  910. * @param string $answer
  911. * @param string $correct
  912. * @param bool $resultsDisabled
  913. *
  914. * @return string
  915. */
  916. public static function getHtmlWrongAnswer($answer, $correct, $resultsDisabled = false)
  917. {
  918. return self::getHtmlAnswer($answer, $correct, false, $resultsDisabled);
  919. }
  920. }