fill_blanks.class.php 44 KB

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