fill_blanks.class.php 44 KB

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