fill_blanks.class.php 37 KB

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