fill_blanks.class.php 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032
  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. global $charset;
  257. $answer = $form->getSubmitValue('answer');
  258. // Due the ckeditor transform the elements to their HTML value
  259. //$answer = api_html_entity_decode($answer, ENT_QUOTES, $charset);
  260. //$answer = htmlentities(api_utf8_encode($answer));
  261. // remove the :: eventually written by the user
  262. $answer = str_replace('::', '', $answer);
  263. // remove starting and ending space and &nbsp;
  264. $answer = api_preg_replace("/\xc2\xa0/", " ", $answer);
  265. // start and end separator
  266. $blankStartSeparator = self::getStartSeparator($form->getSubmitValue('select_separator'));
  267. $blankEndSeparator = self::getEndSeparator($form->getSubmitValue('select_separator'));
  268. $blankStartSeparatorRegexp = self::escapeForRegexp($blankStartSeparator);
  269. $blankEndSeparatorRegexp = self::escapeForRegexp($blankEndSeparator);
  270. // remove spaces at the beginning and the end of text in square brackets
  271. $answer = preg_replace_callback(
  272. "/".$blankStartSeparatorRegexp."[^]]+".$blankEndSeparatorRegexp."/",
  273. function ($matches) use ($blankStartSeparator, $blankEndSeparator) {
  274. $matchingResult = $matches[0];
  275. $matchingResult = trim($matchingResult, $blankStartSeparator);
  276. $matchingResult = trim($matchingResult, $blankEndSeparator);
  277. $matchingResult = trim($matchingResult);
  278. // remove forbidden chars
  279. $matchingResult = str_replace("/\\/", "", $matchingResult);
  280. $matchingResult = str_replace('/"/', "", $matchingResult);
  281. return $blankStartSeparator.$matchingResult.$blankEndSeparator;
  282. },
  283. $answer
  284. );
  285. // get the blanks weightings
  286. $nb = preg_match_all(
  287. '/'.$blankStartSeparatorRegexp.'[^'.$blankStartSeparatorRegexp.']*'.$blankEndSeparatorRegexp.'/',
  288. $answer,
  289. $blanks
  290. );
  291. if (isset($_GET['editQuestion'])) {
  292. $this->weighting = 0;
  293. }
  294. /* if we have some [tobefound] in the text
  295. build the string to save the following in the answers table
  296. <p>I use a [computer] and a [pen].</p>
  297. becomes
  298. <p>I use a [computer] and a [pen].</p>::100,50:100,50@1
  299. ++++++++-------**
  300. --- -- --- -- -
  301. A B (C) (D)(E)
  302. +++++++ : required, weighting of each words
  303. ------- : optional, input width to display, 200 if not present
  304. ** : equal @1 if "Allow answers order switches" has been checked, @ otherwise
  305. A : weighting for the word [computer]
  306. B : weighting for the word [pen]
  307. C : input width for the word [computer]
  308. D : input width for the word [pen]
  309. E : equal @1 if "Allow answers order switches" has been checked, @ otherwise
  310. */
  311. if ($nb > 0) {
  312. $answer .= '::';
  313. // weighting
  314. for ($i=0; $i < $nb; ++$i) {
  315. // enter the weighting of word $i
  316. $answer .= $form->getSubmitValue('weighting['.$i.']');
  317. // not the last word, add ","
  318. if ($i != $nb - 1) {
  319. $answer .= ",";
  320. }
  321. // calculate the global weighting for the question
  322. $this -> weighting += $form->getSubmitValue('weighting['.$i.']');
  323. }
  324. // input width
  325. $answer .= ":";
  326. for ($i=0; $i < $nb; ++$i) {
  327. // enter the width of input for word $i
  328. $answer .= $form->getSubmitValue('sizeofinput['.$i.']');
  329. // not the last word, add ","
  330. if ($i != $nb - 1) {
  331. $answer .= ",";
  332. }
  333. }
  334. }
  335. // write the blank separator code number
  336. // see function getAllowedSeparator
  337. /*
  338. 0 [...]
  339. 1 {...}
  340. 2 (...)
  341. 3 *...*
  342. 4 #...#
  343. 5 %...%
  344. 6 $...$
  345. */
  346. $answer .= ":".$form->getSubmitValue('select_separator');
  347. // Allow answers order switches
  348. $is_multiple = $form -> getSubmitValue('multiple_answer');
  349. $answer.= '@'.$is_multiple;
  350. $this->save();
  351. $objAnswer = new Answer($this->id);
  352. $objAnswer->createAnswer($answer, 0, '', 0, 1);
  353. $objAnswer->save();
  354. }
  355. /**
  356. * @param null $feedback_type
  357. * @param null $counter
  358. * @param null $score
  359. * @return null|string
  360. */
  361. public function return_header($feedback_type = null, $counter = null, $score = null)
  362. {
  363. $header = parent::return_header($feedback_type, $counter, $score);
  364. $header .= '<table class="'.$this->question_table_class .'">
  365. <tr>
  366. <th>'.get_lang("Answer").'</th>
  367. </tr>';
  368. return $header;
  369. }
  370. /**
  371. * @param $separatorStartRegexp
  372. * @param $separatorEndRegexp
  373. * @param $correctItemRegexp
  374. * @param $questionId
  375. * @param $correctItem
  376. * @param $attributes
  377. * @param $answer
  378. * @param $listAnswersInfo
  379. * @param $displayForStudent
  380. * @return string
  381. */
  382. public static function getFillTheBlankHtml(
  383. $separatorStartRegexp,
  384. $separatorEndRegexp,
  385. $correctItemRegexp,
  386. $questionId,
  387. $correctItem,
  388. $attributes,
  389. $answer,
  390. $listAnswersInfo,
  391. $displayForStudent,
  392. $inBlankNumber
  393. ) {
  394. $result = "";
  395. $inTabTeacherSolution = $listAnswersInfo['tabwords'];
  396. $inTeacherSolution = $inTabTeacherSolution[$inBlankNumber];
  397. switch (self::getFillTheBlankAnswerType($inTeacherSolution)) {
  398. case self::FILL_THE_BLANK_MENU:
  399. $selected = "";
  400. // the blank menu
  401. $optionMenu = "";
  402. // display a menu from answer separated with |
  403. // if display for student, shuffle the correct answer menu
  404. $listMenu = self::getFillTheBlankMenuAnswers($inTeacherSolution, $displayForStudent);
  405. $result .= '<select name="choice['.$questionId.'][]">';
  406. for ($k=0; $k < count($listMenu); $k++) {
  407. $selected = "";
  408. if ($correctItem == $listMenu[$k]) {
  409. $selected = " selected=selected ";
  410. }
  411. // if in teacher view, display the first item by default, which is the right answer
  412. if ($k==0 && !$displayForStudent) {
  413. $selected = " selected=selected ";
  414. }
  415. $optionMenu .= '<option '.$selected.' value="'.$listMenu[$k].'">'.$listMenu[$k].'</option>';
  416. }
  417. if ($selected == "") {
  418. // no good answer have been found...
  419. $selected = " selected=selected ";
  420. }
  421. $result .= "<option $selected value=''>--</option>";
  422. $result .= $optionMenu;
  423. $result .= '</select>';
  424. break;
  425. case self::FILL_THE_BLANK_SEVERAL_ANSWER:
  426. //no break
  427. case self::FILL_THE_BLANK_STANDARD:
  428. default:
  429. $result = Display::input('text', "choice[$questionId][]", $correctItem, $attributes);
  430. break;
  431. }
  432. return $result;
  433. }
  434. /**
  435. * Return an array with the different choices available
  436. * when the answers between bracket show as a menu
  437. * @param string $correctAnswer
  438. * @param bool $displayForStudent true if we want to shuffle the choices of the menu for students
  439. *
  440. * @return array
  441. */
  442. public static function getFillTheBlankMenuAnswers($correctAnswer, $displayForStudent)
  443. {
  444. // if $inDisplayForStudent, then shuffle the result array
  445. $listChoises = api_preg_split("/\|/", $correctAnswer);
  446. if ($displayForStudent) {
  447. shuffle($listChoises);
  448. }
  449. return $listChoises;
  450. }
  451. /**
  452. * Return the array index of the student answer
  453. * @param string $correctAnswer the menu Choice1|Choice2|Choice3
  454. * @param string $studentAnswer the student answer must be Choice1 or Choice2 or Choice3
  455. *
  456. * @return int in the example 0 1 or 2 depending of the choice of the student
  457. */
  458. public static function getFillTheBlankMenuAnswerNum($correctAnswer, $studentAnswer)
  459. {
  460. $listChoices = self::getFillTheBlankMenuAnswers($correctAnswer, false);
  461. foreach ($listChoices as $num => $value) {
  462. if ($value == $studentAnswer) {
  463. return $num;
  464. }
  465. }
  466. // should not happened, because student choose the answer in a menu of possible answers
  467. return -1;
  468. }
  469. /**
  470. * Return the possible answer if the answer between brackets is a multiple choice menu
  471. * @param string $correctAnswer
  472. *
  473. * @return array
  474. */
  475. public static function getFillTheBlankSeveralAnswers($correctAnswer)
  476. {
  477. // is answer||Answer||response||Response , mean answer or Answer ...
  478. $listSeveral = api_preg_split("/\|\|/", $correctAnswer);
  479. return $listSeveral;
  480. }
  481. /**
  482. * Return true if student answer is right according to the correctAnswer
  483. * it is not as simple as equality, because of the type of Fill The Blank question
  484. * eg : studentAnswer = 'Un' and correctAnswer = 'Un||1||un'
  485. * @param string $studentAnswer [studentanswer] of the info array of the answer field
  486. * @param string $correctAnswer [tabwords] of the info array of the answer field
  487. *
  488. * @return bool
  489. */
  490. public static function isGoodStudentAnswer($studentAnswer, $correctAnswer)
  491. {
  492. switch (self::getFillTheBlankAnswerType($correctAnswer)) {
  493. case self::FILL_THE_BLANK_MENU:
  494. $listMenu = self::getFillTheBlankMenuAnswers($correctAnswer, false);
  495. $result = $listMenu[0] == $studentAnswer;
  496. break;
  497. case self::FILL_THE_BLANK_SEVERAL_ANSWER:
  498. // the answer must be one of the choice made
  499. $listSeveral = self::getFillTheBlankSeveralAnswers($correctAnswer);
  500. $result = in_array($studentAnswer, $listSeveral);
  501. break;
  502. case self::FILL_THE_BLANK_STANDARD:
  503. default:
  504. $result = $studentAnswer == $correctAnswer;
  505. break;
  506. }
  507. return $result;
  508. }
  509. /**
  510. * @param string $correctAnswer
  511. *
  512. * @return int
  513. */
  514. public static function getFillTheBlankAnswerType($correctAnswer)
  515. {
  516. if (api_strpos($correctAnswer, "|") && !api_strpos($correctAnswer, "||")) {
  517. return self::FILL_THE_BLANK_MENU;
  518. } elseif (api_strpos($correctAnswer, "||")) {
  519. return self::FILL_THE_BLANK_SEVERAL_ANSWER;
  520. } else {
  521. return self::FILL_THE_BLANK_STANDARD;
  522. }
  523. }
  524. /**
  525. * Return information about the answer
  526. * @param string $userAnswer the text of the answer of the question
  527. * @param bool $isStudentAnswer true if it's a student answer false the empty question model
  528. *
  529. * @return array of information about the answer
  530. */
  531. public static function getAnswerInfo($userAnswer = "", $isStudentAnswer = false)
  532. {
  533. $listAnswerResults = array();
  534. $listAnswerResults['text'] = "";
  535. $listAnswerResults['wordsCount'] = 0;
  536. $listAnswerResults['tabwordsbracket'] = array();
  537. $listAnswerResults['tabwords'] = array();
  538. $listAnswerResults['tabweighting'] = array();
  539. $listAnswerResults['tabinputsize'] = array();
  540. $listAnswerResults['switchable'] = "";
  541. $listAnswerResults['studentanswer'] = array();
  542. $listAnswerResults['studentscore'] = array();
  543. $listAnswerResults['blankseparatornumber'] = 0;
  544. $listDoubleColon = array();
  545. api_preg_match("/(.*)::(.*)$/s", $userAnswer, $listResult);
  546. if (count($listResult) < 2) {
  547. $listDoubleColon[] = '';
  548. $listDoubleColon[] = '';
  549. } else {
  550. $listDoubleColon[] = $listResult[1];
  551. $listDoubleColon[] = $listResult[2];
  552. }
  553. $listAnswerResults['systemstring'] = $listDoubleColon[1];
  554. // make sure we only take the last bit to find special marks
  555. $listArobaseSplit = explode('@', $listDoubleColon[1]);
  556. if (count($listArobaseSplit) < 2) {
  557. $listArobaseSplit[1] = "";
  558. }
  559. // take the complete string except after the last '::'
  560. $listDetails = explode(":", $listArobaseSplit[0]);
  561. // < number of item after the ::[score]:[size]:[separator_id]@ , here there are 3
  562. if (count($listDetails) < 3) {
  563. $listWeightings = explode(',', $listDetails[0]);
  564. $listSizeOfInput = array();
  565. for ($i=0; $i < count($listWeightings); $i++) {
  566. $listSizeOfInput[] = 200;
  567. }
  568. $blankSeparatorNumber = 0; // 0 is [...]
  569. } else {
  570. $listWeightings = explode(',', $listDetails[0]);
  571. $listSizeOfInput = explode(',', $listDetails[1]);
  572. $blankSeparatorNumber = $listDetails[2];
  573. }
  574. $listAnswerResults['text'] = $listDoubleColon[0];
  575. $listAnswerResults['tabweighting'] = $listWeightings;
  576. $listAnswerResults['tabinputsize'] = $listSizeOfInput;
  577. $listAnswerResults['switchable'] = $listArobaseSplit[1];
  578. $listAnswerResults['blankseparatorstart'] = self::getStartSeparator($blankSeparatorNumber);
  579. $listAnswerResults['blankseparatorend'] = self::getEndSeparator($blankSeparatorNumber);
  580. $listAnswerResults['blankseparatornumber'] = $blankSeparatorNumber;
  581. $blankCharStart = self::getStartSeparator($blankSeparatorNumber);
  582. $blankCharEnd = self::getEndSeparator($blankSeparatorNumber);
  583. $blankCharStartForRegexp = self::escapeForRegexp($blankCharStart);
  584. $blankCharEndForRegexp = self::escapeForRegexp($blankCharEnd);
  585. // get all blanks words
  586. $listAnswerResults['wordsCount'] = api_preg_match_all(
  587. '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
  588. $listDoubleColon[0],
  589. $listWords
  590. );
  591. if ($listAnswerResults['wordsCount'] > 0) {
  592. $listAnswerResults['tabwordsbracket'] = $listWords[0];
  593. // remove [ and ] in string
  594. array_walk(
  595. $listWords[0],
  596. function (&$value, $key, $tabBlankChar) {
  597. $trimChars = "";
  598. for ($i=0; $i < count($tabBlankChar); $i++) {
  599. $trimChars .= $tabBlankChar[$i];
  600. }
  601. $value = trim($value, $trimChars);
  602. },
  603. array($blankCharStart, $blankCharEnd)
  604. );
  605. $listAnswerResults['tabwords'] = $listWords[0];
  606. }
  607. // get all common words
  608. $commonWords = api_preg_replace(
  609. '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
  610. "::",
  611. $listDoubleColon[0]
  612. );
  613. // if student answer, the second [] is the student answer,
  614. // the third is if student scored or not
  615. $listBrackets = array();
  616. $listWords = array();
  617. if ($isStudentAnswer) {
  618. for ($i=0; $i < count($listAnswerResults['tabwords']); $i++) {
  619. $listBrackets[] = $listAnswerResults['tabwordsbracket'][$i];
  620. $listWords[] = $listAnswerResults['tabwords'][$i];
  621. if ($i+1 < count($listAnswerResults['tabwords'])) {
  622. // should always be
  623. $i++;
  624. }
  625. $listAnswerResults['studentanswer'][] = $listAnswerResults['tabwords'][$i];
  626. if ($i+1 < count($listAnswerResults['tabwords'])) {
  627. // should always be
  628. $i++;
  629. }
  630. $listAnswerResults['studentscore'][] = $listAnswerResults['tabwords'][$i];
  631. }
  632. $listAnswerResults['tabwords'] = $listWords;
  633. $listAnswerResults['tabwordsbracket'] = $listBrackets;
  634. // if we are in student view, we've got 3 times :::::: for common words
  635. $commonWords = api_preg_replace("/::::::/", "::", $commonWords);
  636. }
  637. $listAnswerResults['commonwords'] = explode("::", $commonWords);
  638. return $listAnswerResults;
  639. }
  640. /**
  641. * Replace the occurrence of blank word with [correct answer][student answer][answer is correct]
  642. * @param array $listWithStudentAnswer
  643. *
  644. * @return string
  645. */
  646. public static function getAnswerInStudentAttempt($listWithStudentAnswer)
  647. {
  648. $separatorStart = $listWithStudentAnswer['blankseparatorstart'];
  649. $separatorEnd = $listWithStudentAnswer['blankseparatorend'];
  650. // lets rebuild the sentence with [correct answer][student answer][answer is correct]
  651. $result = "";
  652. for ($i=0; $i < count($listWithStudentAnswer['commonwords']) - 1; $i++) {
  653. $result .= $listWithStudentAnswer['commonwords'][$i];
  654. $result .= $listWithStudentAnswer['tabwordsbracket'][$i];
  655. $result .= $separatorStart.$listWithStudentAnswer['studentanswer'][$i].$separatorEnd;
  656. $result .= $separatorStart.$listWithStudentAnswer['studentscore'][$i].$separatorEnd;
  657. }
  658. $result .= $listWithStudentAnswer['commonwords'][$i];
  659. $result .= "::";
  660. // add the system string
  661. $result .= $listWithStudentAnswer['systemstring'];
  662. return $result;
  663. }
  664. /**
  665. * This function is the same than the js one above getBlankSeparatorRegexp
  666. * @param string $inChar
  667. *
  668. * @return string
  669. */
  670. public static function escapeForRegexp($inChar)
  671. {
  672. $listChars = [
  673. ".",
  674. "+",
  675. "*",
  676. "?",
  677. "[",
  678. "^",
  679. "]",
  680. "$",
  681. "(",
  682. ")",
  683. "{",
  684. "}",
  685. "=",
  686. "!",
  687. ">",
  688. "|",
  689. ":",
  690. "-",
  691. ")",
  692. ];
  693. if (in_array($inChar, $listChars)) {
  694. return "\\".$inChar;
  695. } else {
  696. return $inChar;
  697. }
  698. }
  699. /**
  700. * return $text protected for use in regexp
  701. * @param string $text
  702. *
  703. * @return mixed
  704. */
  705. public static function getRegexpProtected($text)
  706. {
  707. $listRegexpCharacters = [
  708. "/",
  709. ".",
  710. "+",
  711. "*",
  712. "?",
  713. "[",
  714. "^",
  715. "]",
  716. "$",
  717. "(",
  718. ")",
  719. "{",
  720. "}",
  721. "=",
  722. "!",
  723. ">",
  724. "|",
  725. ":",
  726. "-",
  727. ")",
  728. ];
  729. $result = $text;
  730. for ($i=0; $i < count($listRegexpCharacters); $i++) {
  731. $result = str_replace($listRegexpCharacters[$i], "\\".$listRegexpCharacters[$i], $result);
  732. }
  733. return $result;
  734. }
  735. /**
  736. * This function must be the same than the js one getSeparatorFromNumber above
  737. * @return array
  738. */
  739. public static function getAllowedSeparator()
  740. {
  741. $fillBlanksAllowedSeparator = array(
  742. array('[', ']'),
  743. array('{', '}'),
  744. array('(', ')'),
  745. array('*', '*'),
  746. array('#', '#'),
  747. array('%', '%'),
  748. array('$', '$'),
  749. );
  750. return $fillBlanksAllowedSeparator;
  751. }
  752. /**
  753. * return the start separator for answer
  754. * @param string $number
  755. *
  756. * @return mixed
  757. */
  758. public static function getStartSeparator($number)
  759. {
  760. $listSeparators = self::getAllowedSeparator();
  761. return $listSeparators[$number][0];
  762. }
  763. /**
  764. * return the end separator for answer
  765. * @param string $number
  766. *
  767. * @return mixed
  768. */
  769. public static function getEndSeparator($number)
  770. {
  771. $listSeparators = self::getAllowedSeparator();
  772. return $listSeparators[$number][1];
  773. }
  774. /**
  775. * Return as a description text, array of allowed separators for question
  776. * eg: array("[...]", "(...)")
  777. * @return array
  778. */
  779. public static function getAllowedSeparatorForSelect()
  780. {
  781. $listResults = array();
  782. $fillBlanksAllowedSeparator = self::getAllowedSeparator();
  783. for ($i=0; $i < count($fillBlanksAllowedSeparator); $i++) {
  784. $listResults[] = $fillBlanksAllowedSeparator[$i][0]."...".$fillBlanksAllowedSeparator[$i][1];
  785. }
  786. return $listResults;
  787. }
  788. /**
  789. * return the code number of the separator for the question
  790. * @param string $startSeparator
  791. * @param string $endSeparator
  792. *
  793. * @return int
  794. */
  795. public function getDefaultSeparatorNumber($startSeparator, $endSeparator)
  796. {
  797. $listSeparators = self::getAllowedSeparator();
  798. $result = 0;
  799. for ($i=0; $i < count($listSeparators); $i++) {
  800. if ($listSeparators[$i][0] == $startSeparator &&
  801. $listSeparators[$i][1] == $endSeparator
  802. ) {
  803. $result = $i;
  804. }
  805. }
  806. return $result;
  807. }
  808. /**
  809. * return the HTML display of the answer
  810. * @param string $answer
  811. * @param bool $resultsDisabled
  812. *
  813. * @return string
  814. */
  815. public static function getHtmlDisplayForAnswer($answer, $resultsDisabled = false)
  816. {
  817. $result = "";
  818. $listStudentAnswerInfo = self::getAnswerInfo($answer, true);
  819. // rebuild the answer with good HTML style
  820. // this is the student answer, right or wrong
  821. for ($i=0; $i < count($listStudentAnswerInfo['studentanswer']); $i++) {
  822. if ($listStudentAnswerInfo['studentscore'][$i] == 1) {
  823. $listStudentAnswerInfo['studentanswer'][$i] = self::getHtmlRightAnswer(
  824. $listStudentAnswerInfo['studentanswer'][$i],
  825. $listStudentAnswerInfo['tabwords'][$i],
  826. $resultsDisabled
  827. );
  828. } else {
  829. $listStudentAnswerInfo['studentanswer'][$i] = self::getHtmlWrongAnswer(
  830. $listStudentAnswerInfo['studentanswer'][$i],
  831. $listStudentAnswerInfo['tabwords'][$i],
  832. $resultsDisabled
  833. );
  834. }
  835. }
  836. // rebuild the sentence with student answer inserted
  837. for ($i=0; $i < count($listStudentAnswerInfo['commonwords']); $i++) {
  838. $result .= isset($listStudentAnswerInfo['commonwords'][$i]) ? $listStudentAnswerInfo['commonwords'][$i] : '';
  839. $result .= isset($listStudentAnswerInfo['studentanswer'][$i]) ? $listStudentAnswerInfo['studentanswer'][$i] : '';
  840. }
  841. // the last common word (should be </p>)
  842. $result .= isset($listStudentAnswerInfo['commonwords'][$i]) ? $listStudentAnswerInfo['commonwords'][$i] : '';
  843. return $result;
  844. }
  845. /**
  846. * return the HTML code of answer for correct and wrong answer
  847. * @param string $answer
  848. * @param string $correct
  849. * @param string $right
  850. * @param bool $resultsDisabled
  851. *
  852. * @return string
  853. */
  854. public static function getHtmlAnswer($answer, $correct, $right, $resultsDisabled = false)
  855. {
  856. $style = "color: green";
  857. if (!$right) {
  858. $style = "color: red; text-decoration: line-through;";
  859. }
  860. $type = FillBlanks::getFillTheBlankAnswerType($correct);
  861. switch ($type) {
  862. case self::FILL_THE_BLANK_MENU:
  863. $correctAnswerHtml = "";
  864. $listPossibleAnswers = FillBlanks::getFillTheBlankMenuAnswers($correct, false);
  865. $correctAnswerHtml .= "<span style='color: green'>".$listPossibleAnswers[0]."</span>";
  866. $correctAnswerHtml .= " <span style='font-weight:normal'>(";
  867. for ($i=1; $i < count($listPossibleAnswers); $i++) {
  868. $correctAnswerHtml .= $listPossibleAnswers[$i];
  869. if ($i != count($listPossibleAnswers) - 1) {
  870. $correctAnswerHtml .= " | ";
  871. }
  872. }
  873. $correctAnswerHtml .= ")</span>";
  874. break;
  875. case self::FILL_THE_BLANK_SEVERAL_ANSWER:
  876. $listCorrects = explode("||", $correct);
  877. $firstCorrect = $correct;
  878. if (count($listCorrects) > 0) {
  879. $firstCorrect = $listCorrects[0];
  880. }
  881. $correctAnswerHtml = "<span style='color: green'>".$firstCorrect."</span>";
  882. break;
  883. case self::FILL_THE_BLANK_STANDARD:
  884. default:
  885. $correctAnswerHtml = "<span style='color: green'>".$correct."</span>";
  886. }
  887. if ($resultsDisabled) {
  888. $correctAnswerHtml = "<span title='".get_lang("ExerciseWithFeedbackWithoutCorrectionComment")."'> - </span>";
  889. }
  890. $result = "<span style='border:1px solid black; border-radius:5px; padding:2px; font-weight:bold;'>";
  891. $result .= "<span style='$style'>".$answer."</span>";
  892. $result .= "&nbsp;<span style='font-size:120%;'>/</span>&nbsp;";
  893. $result .= $correctAnswerHtml;
  894. $result .= "</span>";
  895. return $result;
  896. }
  897. /**
  898. * return HTML code for correct answer
  899. * @param string $answer
  900. * @param string $correct
  901. * @param bool $resultsDisabled
  902. *
  903. * @return string
  904. */
  905. public static function getHtmlRightAnswer($answer, $correct, $resultsDisabled = false)
  906. {
  907. return self::getHtmlAnswer($answer, $correct, true, $resultsDisabled);
  908. }
  909. /**
  910. * return HTML code for wrong answer
  911. * @param string $answer
  912. * @param string $correct
  913. * @param bool $resultsDisabled
  914. *
  915. * @return string
  916. */
  917. public static function getHtmlWrongAnswer($answer, $correct, $resultsDisabled = false)
  918. {
  919. return self::getHtmlAnswer($answer, $correct, false, $resultsDisabled);
  920. }
  921. }