1 | <?php |
||
2 | |||
3 | /* For licensing terms, see /license.txt */ |
||
4 | |||
5 | /** |
||
6 | * Class FillBlanks. |
||
7 | * |
||
8 | * @author Eric Marguin |
||
9 | * @author Julio Montoya multiple fill in blank option added. |
||
10 | */ |
||
11 | class FillBlanks extends Question |
||
12 | { |
||
13 | const FILL_THE_BLANK_STANDARD = 0; |
||
14 | const FILL_THE_BLANK_MENU = 1; |
||
15 | const FILL_THE_BLANK_SEVERAL_ANSWER = 2; |
||
16 | |||
17 | public $typePicture = 'fill_in_blanks.png'; |
||
18 | public $explanationLangVar = 'FillBlanks'; |
||
19 | |||
20 | /** |
||
21 | * Constructor. |
||
22 | */ |
||
23 | public function __construct() |
||
24 | { |
||
25 | parent::__construct(); |
||
26 | $this->type = FILL_IN_BLANKS; |
||
27 | $this->isContent = $this->getIsContent(); |
||
28 | } |
||
29 | |||
30 | /** |
||
31 | * {@inheritdoc} |
||
32 | */ |
||
33 | public function createAnswersForm($form) |
||
34 | { |
||
35 | $defaults = []; |
||
36 | $defaults['answer'] = get_lang('DefaultTextInBlanks'); |
||
37 | $defaults['select_separator'] = 0; |
||
38 | $blankSeparatorNumber = 0; |
||
39 | if (!empty($this->iid)) { |
||
40 | $objectAnswer = new Answer($this->iid); |
||
41 | $answer = $objectAnswer->selectAnswer(1); |
||
42 | $listAnswersInfo = self::getAnswerInfo($answer); |
||
43 | $defaults['multiple_answer'] = 0; |
||
44 | if ($listAnswersInfo['switchable']) { |
||
45 | $defaults['multiple_answer'] = 1; |
||
46 | } |
||
47 | // Take the complete string except after the last '::' |
||
48 | $defaults['answer'] = $listAnswersInfo['text']; |
||
49 | $defaults['select_separator'] = $listAnswersInfo['blank_separator_number']; |
||
50 | $blankSeparatorNumber = $listAnswersInfo['blank_separator_number']; |
||
51 | } |
||
52 | |||
53 | $blankSeparatorStart = self::getStartSeparator($blankSeparatorNumber); |
||
54 | $blankSeparatorEnd = self::getEndSeparator($blankSeparatorNumber); |
||
55 | $setWeightAndSize = ''; |
||
56 | if (isset($listAnswersInfo) && count($listAnswersInfo['weighting']) > 0) { |
||
57 | foreach ($listAnswersInfo['weighting'] as $i => $weighting) { |
||
58 | $setWeightAndSize .= 'document.getElementById("weighting['.$i.']").value = "'.$weighting.'";'; |
||
59 | } |
||
60 | foreach ($listAnswersInfo['input_size'] as $i => $sizeOfInput) { |
||
61 | $setWeightAndSize .= 'document.getElementById("sizeofinput['.$i.']").value = "'.$sizeOfInput.'";'; |
||
62 | $setWeightAndSize .= 'document.getElementById("samplesize['.$i.']").style.width = "'.$sizeOfInput.'px";'; |
||
63 | } |
||
64 | } |
||
65 | |||
66 | echo '<script> |
||
67 | var firstTime = true; |
||
68 | var originalOrder = new Array(); |
||
69 | var blankSeparatorStart = "'.$blankSeparatorStart.'"; |
||
70 | var blankSeparatorEnd = "'.$blankSeparatorEnd.'"; |
||
71 | var blankSeparatorStartRegexp = getBlankSeparatorRegexp(blankSeparatorStart); |
||
72 | var blankSeparatorEndRegexp = getBlankSeparatorRegexp(blankSeparatorEnd); |
||
73 | var blanksRegexp = "/"+blankSeparatorStartRegexp+"[^"+blankSeparatorStartRegexp+"]*"+blankSeparatorEndRegexp+"/g"; |
||
74 | |||
75 | CKEDITOR.on("instanceCreated", function(e) { |
||
76 | if (e.editor.name === "answer") { |
||
77 | //e.editor.on("change", updateBlanks); |
||
78 | e.editor.on("change", function(){ |
||
79 | updateBlanks(); |
||
80 | }); |
||
81 | } |
||
82 | }); |
||
83 | |||
84 | function updateBlanks() |
||
85 | { |
||
86 | var answer; |
||
87 | if (firstTime) { |
||
88 | var field = document.getElementById("answer"); |
||
89 | answer = field.value; |
||
90 | } else { |
||
91 | answer = CKEDITOR.instances["answer"].getData(); |
||
92 | } |
||
93 | |||
94 | // disable the save button, if not blanks have been created |
||
95 | $("button").attr("disabled", "disabled"); |
||
96 | $("#defineoneblank").show(); |
||
97 | |||
98 | var blanks = answer.match(eval(blanksRegexp)); |
||
99 | var fields = "<div class=\"form-group \">"; |
||
100 | fields += "<label class=\"col-sm-2 control-label\"></label>"; |
||
101 | fields += "<div class=\"col-sm-8\">"; |
||
102 | fields += "<table class=\"data_table\">"; |
||
103 | fields += "<tr><th style=\"width:220px\">'.get_lang('WordTofind').'</th>"; |
||
104 | fields += "<th style=\"width:50px\">'.get_lang('QuestionWeighting').'</th>"; |
||
105 | fields += "<th>'.get_lang('BlankInputSize').'</th></tr>"; |
||
106 | |||
107 | if (blanks != null) { |
||
108 | for (var i=0; i < blanks.length; i++) { |
||
109 | // remove forbidden characters that causes bugs |
||
110 | blanks[i] = removeForbiddenChars(blanks[i]); |
||
111 | // trim blanks between brackets |
||
112 | blanks[i] = trimBlanksBetweenSeparator(blanks[i], blankSeparatorStart, blankSeparatorEnd); |
||
113 | |||
114 | // if the word is empty [] |
||
115 | if (blanks[i] == blankSeparatorStartRegexp+blankSeparatorEndRegexp) { |
||
116 | break; |
||
117 | } |
||
118 | |||
119 | // get input size |
||
120 | var inputSize = 100; |
||
121 | var textValue = blanks[i].substr(1, blanks[i].length - 2); |
||
122 | var btoaValue = textValue.hashCode(); |
||
123 | |||
124 | if (firstTime == false) { |
||
125 | var element = document.getElementById("samplesize["+i+"]"); |
||
126 | if (element) { |
||
127 | inputSize = document.getElementById("sizeofinput["+i+"]").value; |
||
128 | } |
||
129 | } |
||
130 | |||
131 | if (document.getElementById("weighting["+i+"]")) { |
||
132 | var value = document.getElementById("weighting["+i+"]").value; |
||
133 | } else { |
||
134 | var value = "1"; |
||
135 | } |
||
136 | var blanksWithColor = trimBlanksBetweenSeparator(blanks[i], blankSeparatorStart, blankSeparatorEnd, 1); |
||
137 | |||
138 | fields += "<tr>"; |
||
139 | fields += "<td>"+blanksWithColor+"</td>"; |
||
140 | fields += "<td><input class=\"form-control\" style=\"width:60px\" value=\""+value+"\" type=\"text\" id=\"weighting["+i+"]\" name=\"weighting["+i+"]\" /></td>"; |
||
141 | fields += "<td>"; |
||
142 | fields += "<input class=\"btn btn-default\" type=\"button\" value=\"-\" onclick=\"changeInputSize(-1, "+i+")\"> "; |
||
143 | fields += "<input class=\"btn btn-default\" type=\"button\" value=\"+\" onclick=\"changeInputSize(1, "+i+")\"> "; |
||
144 | fields += " <input class=\"sample\" id=\"samplesize["+i+"]\" data-btoa=\""+btoaValue+"\" type=\"text\" value=\""+textValue+"\" style=\"width:"+inputSize+"px\" disabled=disabled />"; |
||
145 | fields += "<input id=\"sizeofinput["+i+"]\" type=\"hidden\" value=\""+inputSize+"\" name=\"sizeofinput["+i+"]\" />"; |
||
146 | fields += "</td>"; |
||
147 | fields += "</tr>"; |
||
148 | |||
149 | // enable the save button |
||
150 | $("button").removeAttr("disabled"); |
||
151 | $("#defineoneblank").hide(); |
||
152 | } |
||
153 | } |
||
154 | |||
155 | document.getElementById("blanks_weighting").innerHTML = fields + "</table></div></div>"; |
||
156 | |||
157 | $(originalOrder).each(function(i, data) { |
||
158 | if (firstTime == false) { |
||
159 | value = data.value; |
||
160 | var d = $("input.sample[data-btoa=\'"+value+"\']"); |
||
161 | var id = d.attr("id"); |
||
162 | if (id) { |
||
163 | var sizeInputId = id.replace("samplesize", "sizeofinput"); |
||
164 | var sizeInputId = sizeInputId.replace("[", "\\\["); |
||
165 | var sizeInputId = sizeInputId.replace("]", "\\\]"); |
||
166 | $("#"+sizeInputId).val(data.width); |
||
167 | d.outerWidth(data.width+"px"); |
||
168 | } |
||
169 | } |
||
170 | }); |
||
171 | |||
172 | updateOrder(blanks); |
||
173 | |||
174 | if (firstTime) { |
||
175 | firstTime = false; |
||
176 | '.$setWeightAndSize.' |
||
177 | } |
||
178 | } |
||
179 | |||
180 | window.onload = updateBlanks; |
||
181 | String.prototype.hashCode = function() { |
||
182 | var hash = 0, i, chr, len; |
||
183 | if (this.length === 0) return hash; |
||
184 | for (i = 0, len = this.length; i < len; i++) { |
||
185 | chr = this.charCodeAt(i); |
||
186 | hash = ((hash << 5) - hash) + chr; |
||
187 | hash |= 0; // Convert to 32bit integer |
||
188 | } |
||
189 | return hash; |
||
190 | }; |
||
191 | |||
192 | function updateOrder(blanks) |
||
193 | { |
||
194 | originalOrder = new Array(); |
||
195 | if (blanks != null) { |
||
196 | for (var i=0; i < blanks.length; i++) { |
||
197 | // remove forbidden characters that causes bugs |
||
198 | blanks[i] = removeForbiddenChars(blanks[i]); |
||
199 | // trim blanks between brackets |
||
200 | blanks[i] = trimBlanksBetweenSeparator(blanks[i], blankSeparatorStart, blankSeparatorEnd); |
||
201 | |||
202 | // if the word is empty [] |
||
203 | if (blanks[i] == blankSeparatorStartRegexp+blankSeparatorEndRegexp) { |
||
204 | break; |
||
205 | } |
||
206 | var textValue = blanks[i].substr(1, blanks[i].length - 2); |
||
207 | var btoaValue = textValue.hashCode(); |
||
208 | |||
209 | if (firstTime == false) { |
||
210 | var element = document.getElementById("samplesize["+i+"]"); |
||
211 | if (element) { |
||
212 | inputSize = document.getElementById("sizeofinput["+i+"]").value; |
||
213 | originalOrder.push({ "width" : inputSize, "value": btoaValue }); |
||
214 | } |
||
215 | } |
||
216 | } |
||
217 | } |
||
218 | } |
||
219 | |||
220 | function changeInputSize(coef, inIdNum) |
||
221 | { |
||
222 | if (firstTime) { |
||
223 | var field = document.getElementById("answer"); |
||
224 | answer = field.value; |
||
225 | } else { |
||
226 | answer = CKEDITOR.instances["answer"].getData(); |
||
227 | } |
||
228 | |||
229 | var blanks = answer.match(eval(blanksRegexp)); |
||
230 | var currentWidth = $("#samplesize\\\["+inIdNum+"\\\]").width(); |
||
231 | var newWidth = currentWidth + coef * 20; |
||
232 | newWidth = Math.max(20, newWidth); |
||
233 | newWidth = Math.min(newWidth, 600); |
||
234 | $("#samplesize\\\["+inIdNum+"\\\]").outerWidth(newWidth); |
||
235 | $("#sizeofinput\\\["+inIdNum+"\\\]").attr("value", newWidth); |
||
236 | |||
237 | updateOrder(blanks); |
||
238 | } |
||
239 | |||
240 | function removeForbiddenChars(inTxt) |
||
241 | { |
||
242 | outTxt = inTxt; |
||
243 | outTxt = outTxt.replace(/"/g, ""); // remove the char |
||
244 | outTxt = outTxt.replace(/\x22/g, ""); // remove the char |
||
245 | outTxt = outTxt.replace(/"/g, ""); // remove the char |
||
246 | outTxt = outTxt.replace(/\\\\/g, ""); // remove the \ char |
||
247 | outTxt = outTxt.replace(/ /g, " "); |
||
248 | outTxt = outTxt.replace(/^ +/, ""); |
||
249 | outTxt = outTxt.replace(/ +$/, ""); |
||
250 | |||
251 | return outTxt; |
||
252 | } |
||
253 | |||
254 | function changeBlankSeparator() |
||
255 | { |
||
256 | /* get current select blank type and replaced into #defineoneblank */ |
||
257 | var definedSeparator = $("[name=select_separator] option:selected").text(); |
||
258 | $("[name=select_separator] option").each(function (index, value) { |
||
259 | $("#defineoneblank").html($("#defineoneblank").html().replace($(value).html(), definedSeparator)) |
||
260 | }); |
||
261 | var separatorNumber = $("#select_separator").val(); |
||
262 | var tabSeparator = getSeparatorFromNumber(separatorNumber); |
||
263 | blankSeparatorStart = tabSeparator[0]; |
||
264 | blankSeparatorEnd = tabSeparator[1]; |
||
265 | blankSeparatorStartRegexp = getBlankSeparatorRegexp(blankSeparatorStart); |
||
266 | blankSeparatorEndRegexp = getBlankSeparatorRegexp(blankSeparatorEnd); |
||
267 | blanksRegexp = "/"+blankSeparatorStartRegexp+"[^"+blankSeparatorStartRegexp+"]*"+blankSeparatorEndRegexp+"/g"; |
||
268 | updateBlanks(); |
||
269 | } |
||
270 | |||
271 | // this function is the same than the PHP one |
||
272 | // if modify it modify the php one escapeForRegexp |
||
273 | function getBlankSeparatorRegexp(inTxt) |
||
274 | { |
||
275 | var tabSpecialChar = new Array(".", "+", "*", "?", "[", "^", "]", "$", "(", ")", |
||
276 | "{", "}", "=", "!", "<", ">", "|", ":", "-", ")"); |
||
277 | for (var i=0; i < tabSpecialChar.length; i++) { |
||
278 | if (inTxt == tabSpecialChar[i]) { |
||
279 | return "\\\"+inTxt; |
||
280 | } |
||
281 | } |
||
282 | return inTxt; |
||
283 | } |
||
284 | |||
285 | // this function is the same than the PHP one |
||
286 | // if modify it modify the php one getAllowedSeparator |
||
287 | function getSeparatorFromNumber(number) |
||
288 | { |
||
289 | var separator = new Array(); |
||
290 | separator[0] = new Array("[", "]"); |
||
291 | separator[1] = new Array("{", "}"); |
||
292 | separator[2] = new Array("(", ")"); |
||
293 | separator[3] = new Array("*", "*"); |
||
294 | separator[4] = new Array("#", "#"); |
||
295 | separator[5] = new Array("%", "%"); |
||
296 | separator[6] = new Array("$", "$"); |
||
297 | return separator[number]; |
||
298 | } |
||
299 | |||
300 | function trimBlanksBetweenSeparator(inTxt, inSeparatorStart, inSeparatorEnd, addColor) |
||
301 | { |
||
302 | var result = inTxt |
||
303 | result = result.replace(inSeparatorStart, ""); |
||
304 | result = result.replace(inSeparatorEnd, ""); |
||
305 | result = result.trim(); |
||
306 | |||
307 | if (addColor == 1) { |
||
308 | var resultParts = result.split("|"); |
||
309 | var partsToString = ""; |
||
310 | resultParts.forEach(function(item, index) { |
||
311 | if (index == 0) { |
||
312 | item = "<b><font style=\"color:green\"> " + item +"</font></b>"; |
||
313 | } |
||
314 | if (index < resultParts.length - 1) { |
||
315 | item = item + " | "; |
||
316 | } |
||
317 | partsToString += item; |
||
318 | }); |
||
319 | result = partsToString; |
||
320 | } |
||
321 | |||
322 | return inSeparatorStart+result+inSeparatorEnd; |
||
323 | } |
||
324 | |||
325 | </script>'; |
||
326 | |||
327 | // answer |
||
328 | $form->addLabel( |
||
329 | null, |
||
330 | get_lang('TypeTextBelow').', '.get_lang('And').' '.get_lang('UseTagForBlank') |
||
331 | ); |
||
332 | $form->addElement( |
||
333 | 'html_editor', |
||
334 | 'answer', |
||
335 | Display::return_icon('fill_field.png'), |
||
336 | ['id' => 'answer'], |
||
337 | ['ToolbarSet' => 'TestQuestionDescription'] |
||
338 | ); |
||
339 | $form->addRule('answer', get_lang('GiveText'), 'required'); |
||
340 | |||
341 | //added multiple answers |
||
342 | $form->addElement('checkbox', 'multiple_answer', '', get_lang('FillInBlankSwitchable')); |
||
343 | $form->addElement( |
||
344 | 'select', |
||
345 | 'select_separator', |
||
346 | get_lang('SelectFillTheBlankSeparator'), |
||
347 | self::getAllowedSeparatorForSelect(), |
||
348 | ' id="select_separator" style="width:150px" class="selectpicker" onchange="changeBlankSeparator()" ' |
||
349 | ); |
||
350 | $form->addLabel( |
||
351 | null, |
||
352 | '<input type="button" onclick="updateBlanks()" value="'.get_lang('RefreshBlanks').'" class="btn btn-default" />' |
||
353 | ); |
||
354 | |||
355 | $form->addHtml('<div id="blanks_weighting"></div>'); |
||
356 | |||
357 | global $text; |
||
358 | // setting the save button here and not in the question class.php |
||
359 | $form->addHtml('<div id="defineoneblank" style="color:#D04A66; margin-left:160px">'.get_lang('DefineBlanks').'</div>'); |
||
360 | $form->addButtonSave($text, 'submitQuestion'); |
||
361 | |||
362 | if (!empty($this->iid)) { |
||
363 | $form->setDefaults($defaults); |
||
364 | } else { |
||
365 | if ($this->isContent == 1) { |
||
366 | $form->setDefaults($defaults); |
||
367 | } |
||
368 | } |
||
369 | } |
||
370 | |||
371 | /** |
||
372 | * {@inheritdoc} |
||
373 | */ |
||
374 | public function processAnswersCreation($form, $exercise) |
||
375 | { |
||
376 | $answer = $form->getSubmitValue('answer'); |
||
377 | // Due the ckeditor transform the elements to their HTML value |
||
378 | |||
379 | //$answer = api_html_entity_decode($answer, ENT_QUOTES, $charset); |
||
380 | //$answer = htmlentities(api_utf8_encode($answer)); |
||
381 | |||
382 | // remove the "::" eventually written by the user |
||
383 | $answer = str_replace('::', '', $answer); |
||
384 | |||
385 | // remove starting and ending space and |
||
386 | $answer = api_preg_replace("/\xc2\xa0/", " ", $answer); |
||
387 | |||
388 | // start and end separator |
||
389 | $blankStartSeparator = self::getStartSeparator($form->getSubmitValue('select_separator')); |
||
390 | $blankEndSeparator = self::getEndSeparator($form->getSubmitValue('select_separator')); |
||
391 | $blankStartSeparatorRegexp = self::escapeForRegexp($blankStartSeparator); |
||
392 | $blankEndSeparatorRegexp = self::escapeForRegexp($blankEndSeparator); |
||
393 | |||
394 | // remove spaces at the beginning and the end of text in square brackets |
||
395 | $answer = preg_replace_callback( |
||
396 | "/".$blankStartSeparatorRegexp."[^]]+".$blankEndSeparatorRegexp."/", |
||
397 | function ($matches) use ($blankStartSeparator, $blankEndSeparator) { |
||
398 | $matchingResult = $matches[0]; |
||
399 | $matchingResult = trim($matchingResult, $blankStartSeparator); |
||
400 | $matchingResult = trim($matchingResult, $blankEndSeparator); |
||
401 | $matchingResult = trim($matchingResult); |
||
402 | // remove forbidden chars |
||
403 | $matchingResult = str_replace("/\\/", "", $matchingResult); |
||
404 | $matchingResult = str_replace('/"/', "", $matchingResult); |
||
405 | |||
406 | return $blankStartSeparator.$matchingResult.$blankEndSeparator; |
||
407 | }, |
||
408 | $answer |
||
409 | ); |
||
410 | |||
411 | // get the blanks weightings |
||
412 | $nb = preg_match_all( |
||
413 | '/'.$blankStartSeparatorRegexp.'[^'.$blankStartSeparatorRegexp.']*'.$blankEndSeparatorRegexp.'/', |
||
414 | $answer, |
||
415 | $blanks |
||
416 | ); |
||
417 | |||
418 | if (isset($_GET['editQuestion'])) { |
||
419 | $this->weighting = 0; |
||
420 | } |
||
421 | |||
422 | /* if we have some [tobefound] in the text |
||
423 | build the string to save the following in the answers table |
||
424 | <p>I use a [computer] and a [pen].</p> |
||
425 | becomes |
||
426 | <p>I use a [computer] and a [pen].</p>::100,50:100,50@1 |
||
427 | ++++++++-------** |
||
428 | --- -- --- -- - |
||
429 | A B (C) (D)(E) |
||
430 | +++++++ : required, weighting of each words |
||
431 | ------- : optional, input width to display, 200 if not present |
||
432 | ** : equal @1 if "Allow answers order switches" has been checked, @ otherwise |
||
433 | A : weighting for the word [computer] |
||
434 | B : weighting for the word [pen] |
||
435 | C : input width for the word [computer] |
||
436 | D : input width for the word [pen] |
||
437 | E : equal @1 if "Allow answers order switches" has been checked, @ otherwise |
||
438 | */ |
||
439 | if ($nb > 0) { |
||
440 | $answer .= '::'; |
||
441 | // weighting |
||
442 | for ($i = 0; $i < $nb; $i++) { |
||
443 | // enter the weighting of word $i |
||
444 | $answer .= $form->getSubmitValue('weighting['.$i.']'); |
||
445 | // not the last word, add "," |
||
446 | if ($i != $nb - 1) { |
||
447 | $answer .= ','; |
||
448 | } |
||
449 | // calculate the global weighting for the question |
||
450 | $this->weighting += (float) $form->getSubmitValue('weighting['.$i.']'); |
||
451 | } |
||
452 | |||
453 | // input width |
||
454 | $answer .= ':'; |
||
455 | for ($i = 0; $i < $nb; $i++) { |
||
456 | // enter the width of input for word $i |
||
457 | $answer .= $form->getSubmitValue('sizeofinput['.$i.']'); |
||
458 | // not the last word, add "," |
||
459 | if ($i != $nb - 1) { |
||
460 | $answer .= ','; |
||
461 | } |
||
462 | } |
||
463 | } |
||
464 | |||
465 | // write the blank separator code number |
||
466 | // see function getAllowedSeparator |
||
467 | /* |
||
468 | 0 [...] |
||
469 | 1 {...} |
||
470 | 2 (...) |
||
471 | 3 *...* |
||
472 | 4 #...# |
||
473 | 5 %...% |
||
474 | 6 $...$ |
||
475 | */ |
||
476 | $answer .= ':'.$form->getSubmitValue('select_separator'); |
||
477 | |||
478 | // Allow answers order switches |
||
479 | $is_multiple = $form->getSubmitValue('multiple_answer'); |
||
480 | $answer .= '@'.$is_multiple; |
||
481 | |||
482 | $this->save($exercise); |
||
483 | $objAnswer = new Answer($this->iid); |
||
484 | $objAnswer->createAnswer($answer, 0, '', 0, 1); |
||
485 | $objAnswer->save(); |
||
486 | } |
||
487 | |||
488 | /** |
||
489 | * {@inheritdoc} |
||
490 | */ |
||
491 | public function return_header(Exercise $exercise, $counter = null, $score = []) |
||
492 | { |
||
493 | $header = parent::return_header($exercise, $counter, $score); |
||
494 | $header .= '<table class="'.$this->question_table_class.'"> |
||
495 | <tr> |
||
496 | <th>'.get_lang('Answer').'</th> |
||
497 | </tr>'; |
||
498 | |||
499 | return $header; |
||
500 | } |
||
501 | |||
502 | /** |
||
503 | * @param int $currentQuestion |
||
504 | * @param int $questionId |
||
505 | * @param string $correctItem |
||
506 | * @param array $attributes |
||
507 | * @param string $answer |
||
508 | * @param array $listAnswersInfo |
||
509 | * @param bool $displayForStudent |
||
510 | * @param int $inBlankNumber |
||
511 | * @param string $labelId |
||
512 | * |
||
513 | * @return string |
||
514 | */ |
||
515 | public static function getFillTheBlankHtml( |
||
516 | $currentQuestion, |
||
517 | $questionId, |
||
518 | $correctItem, |
||
519 | $attributes, |
||
520 | $answer, |
||
521 | $listAnswersInfo, |
||
522 | $displayForStudent, |
||
523 | $inBlankNumber, |
||
524 | $labelId = '' |
||
525 | ) { |
||
526 | $inTabTeacherSolution = $listAnswersInfo['words']; |
||
527 | $inTeacherSolution = $inTabTeacherSolution[$inBlankNumber]; |
||
528 | |||
529 | if (empty($labelId)) { |
||
530 | $labelId = 'choice_id_'.$currentQuestion.'_'.$inBlankNumber; |
||
531 | } |
||
532 | |||
533 | switch (self::getFillTheBlankAnswerType($inTeacherSolution)) { |
||
534 | case self::FILL_THE_BLANK_MENU: |
||
535 | $selected = ''; |
||
536 | // the blank menu |
||
537 | // display a menu from answer separated with | |
||
538 | // if display for student, shuffle the correct answer menu |
||
539 | $listMenu = self::getFillTheBlankMenuAnswers( |
||
540 | $inTeacherSolution, |
||
541 | $displayForStudent |
||
542 | ); |
||
543 | |||
544 | $resultOptions = ['' => '--']; |
||
545 | foreach ($listMenu as $item) { |
||
546 | $resultOptions[sha1($item)] = $item; |
||
547 | } |
||
548 | |||
549 | foreach ($resultOptions as $key => $value) { |
||
550 | if ($correctItem == $value) { |
||
551 | $selected = $key; |
||
552 | |||
553 | break; |
||
554 | } |
||
555 | } |
||
556 | $width = ''; |
||
557 | if (!empty($attributes['style'])) { |
||
558 | $width = str_replace('width:', '', $attributes['style']); |
||
559 | } |
||
560 | |||
561 | $result = Display::select( |
||
562 | "choice[$questionId][]", |
||
563 | $resultOptions, |
||
564 | $selected, |
||
565 | [ |
||
566 | 'class' => 'selectpicker', |
||
567 | 'data-width' => $width, |
||
568 | 'id' => $labelId, |
||
569 | ], |
||
570 | false |
||
571 | ); |
||
572 | break; |
||
573 | case self::FILL_THE_BLANK_SEVERAL_ANSWER: |
||
574 | case self::FILL_THE_BLANK_STANDARD: |
||
575 | default: |
||
576 | $attributes['id'] = $labelId; |
||
577 | $result = Display::input( |
||
578 | 'text', |
||
579 | "choice[$questionId][]", |
||
580 | $correctItem, |
||
581 | $attributes |
||
582 | ); |
||
583 | break; |
||
584 | } |
||
585 | |||
586 | return $result; |
||
587 | } |
||
588 | |||
589 | /** |
||
590 | * Return an array with the different choices available |
||
591 | * when the answers between bracket show as a menu. |
||
592 | * |
||
593 | * @param string $correctAnswer |
||
594 | * @param bool $displayForStudent true if we want to shuffle the choices of the menu for students |
||
595 | * |
||
596 | * @return array |
||
597 | */ |
||
598 | public static function getFillTheBlankMenuAnswers($correctAnswer, $displayForStudent) |
||
599 | { |
||
600 | $list = api_preg_split("/\|/", $correctAnswer); |
||
601 | foreach ($list as &$item) { |
||
602 | $item = self::trimOption($item); |
||
603 | $item = api_html_entity_decode($item); |
||
604 | } |
||
605 | // The list is always in the same order, there's no option to allow or disable shuffle options. |
||
606 | if ($displayForStudent) { |
||
607 | shuffle_assoc($list); |
||
608 | } |
||
609 | |||
610 | return $list; |
||
611 | } |
||
612 | |||
613 | /** |
||
614 | * Return the array index of the student answer. |
||
615 | * |
||
616 | * @param string $correctAnswer the menu Choice1|Choice2|Choice3 |
||
617 | * @param string $studentAnswer the student answer must be Choice1 or Choice2 or Choice3 |
||
618 | * |
||
619 | * @return int in the example 0 1 or 2 depending of the choice of the student |
||
620 | */ |
||
621 | public static function getFillTheBlankMenuAnswerNum($correctAnswer, $studentAnswer) |
||
622 | { |
||
623 | $listChoices = self::getFillTheBlankMenuAnswers($correctAnswer, false); |
||
624 | foreach ($listChoices as $num => $value) { |
||
625 | if ($value == $studentAnswer) { |
||
626 | return $num; |
||
627 | } |
||
628 | } |
||
629 | |||
630 | // should not happened, because student choose the answer in a menu of possible answers |
||
631 | return -1; |
||
632 | } |
||
633 | |||
634 | /** |
||
635 | * Return the possible answer if the answer between brackets is a multiple choice menu. |
||
636 | * |
||
637 | * @param string $correctAnswer |
||
638 | * |
||
639 | * @return array |
||
640 | */ |
||
641 | public static function getFillTheBlankSeveralAnswers($correctAnswer) |
||
642 | { |
||
643 | // is answer||Answer||response||Response , mean answer or Answer ... |
||
644 | return api_preg_split("/\|\|/", $correctAnswer); |
||
645 | } |
||
646 | |||
647 | /** |
||
648 | * Return true if student answer is right according to the correctAnswer |
||
649 | * it is not as simple as equality, because of the type of Fill The Blank question |
||
650 | * eg : studentAnswer = 'Un' and correctAnswer = 'Un||1||un'. |
||
651 | * |
||
652 | * @param string $studentAnswer [student_answer] of the info array of the answer field |
||
653 | * @param string $correctAnswer [words] of the info array of the answer field |
||
654 | * @param bool $fromDatabase |
||
655 | * |
||
656 | * @return bool |
||
657 | */ |
||
658 | public static function isStudentAnswerGood($studentAnswer, $correctAnswer, $fromDatabase = false) |
||
659 | { |
||
660 | $result = false; |
||
661 | switch (self::getFillTheBlankAnswerType($correctAnswer)) { |
||
662 | case self::FILL_THE_BLANK_MENU: |
||
663 | $listMenu = self::getFillTheBlankMenuAnswers($correctAnswer, false); |
||
664 | if ($studentAnswer != '' && isset($listMenu[0])) { |
||
665 | // First item is always the correct one. |
||
666 | $item = $listMenu[0]; |
||
667 | if (!$fromDatabase) { |
||
668 | $item = sha1($item); |
||
669 | $studentAnswer = sha1($studentAnswer); |
||
670 | } |
||
671 | if ($item === $studentAnswer) { |
||
672 | $result = true; |
||
673 | } |
||
674 | } |
||
675 | break; |
||
676 | case self::FILL_THE_BLANK_SEVERAL_ANSWER: |
||
677 | // the answer must be one of the choice made |
||
678 | $listSeveral = self::getFillTheBlankSeveralAnswers($correctAnswer); |
||
679 | $listSeveral = array_map( |
||
680 | function ($item) { |
||
681 | return self::trimOption(api_html_entity_decode($item)); |
||
682 | }, |
||
683 | $listSeveral |
||
684 | ); |
||
685 | //$studentAnswer = htmlspecialchars($studentAnswer); |
||
686 | $result = in_array($studentAnswer, $listSeveral); |
||
687 | break; |
||
688 | case self::FILL_THE_BLANK_STANDARD: |
||
689 | default: |
||
690 | $correctAnswer = api_html_entity_decode($correctAnswer); |
||
691 | //$studentAnswer = htmlspecialchars($studentAnswer); |
||
692 | $result = $studentAnswer == self::trimOption($correctAnswer); |
||
693 | break; |
||
694 | } |
||
695 | |||
696 | return $result; |
||
697 | } |
||
698 | |||
699 | /** |
||
700 | * @param string $correctAnswer |
||
701 | * |
||
702 | * @return int |
||
703 | */ |
||
704 | public static function getFillTheBlankAnswerType($correctAnswer) |
||
705 | { |
||
706 | $type = self::FILL_THE_BLANK_STANDARD; |
||
707 | if (api_strpos($correctAnswer, '|') && !api_strpos($correctAnswer, '||')) { |
||
708 | $type = self::FILL_THE_BLANK_MENU; |
||
709 | } elseif (api_strpos($correctAnswer, '||')) { |
||
710 | $type = self::FILL_THE_BLANK_SEVERAL_ANSWER; |
||
711 | } |
||
712 | |||
713 | return $type; |
||
714 | } |
||
715 | |||
716 | /** |
||
717 | * Return information about the answer. |
||
718 | * |
||
719 | * @param string $userAnswer the text of the answer of the question |
||
720 | * @param bool $isStudentAnswer true if it's a student answer false the empty question model |
||
721 | * |
||
722 | * @return array of information about the answer |
||
723 | */ |
||
724 | public static function getAnswerInfo($userAnswer = '', $isStudentAnswer = false) |
||
725 | { |
||
726 | $listAnswerResults = []; |
||
727 | $listAnswerResults['text'] = ''; |
||
728 | $listAnswerResults['words_count'] = 0; |
||
729 | $listAnswerResults['words_with_bracket'] = []; |
||
730 | $listAnswerResults['words'] = []; |
||
731 | $listAnswerResults['weighting'] = []; |
||
732 | $listAnswerResults['input_size'] = []; |
||
733 | $listAnswerResults['switchable'] = ''; |
||
734 | $listAnswerResults['student_answer'] = []; |
||
735 | $listAnswerResults['student_score'] = []; |
||
736 | $listAnswerResults['blank_separator_number'] = 0; |
||
737 | $listDoubleColon = []; |
||
738 | |||
739 | api_preg_match("/(.*)::(.*)$/s", $userAnswer, $listResult); |
||
740 | |||
741 | if (count($listResult) < 2) { |
||
742 | $listDoubleColon[] = ''; |
||
743 | $listDoubleColon[] = ''; |
||
744 | } else { |
||
745 | $listDoubleColon[] = $listResult[1]; |
||
746 | $listDoubleColon[] = $listResult[2]; |
||
747 | } |
||
748 | |||
749 | $listAnswerResults['system_string'] = $listDoubleColon[1]; |
||
750 | |||
751 | // Make sure we only take the last bit to find special marks |
||
752 | $listArobaseSplit = explode('@', $listDoubleColon[1]); |
||
753 | |||
754 | if (count($listArobaseSplit) < 2) { |
||
755 | $listArobaseSplit[1] = ''; |
||
756 | } |
||
757 | |||
758 | // Take the complete string except after the last '::' |
||
759 | $listDetails = explode(':', $listArobaseSplit[0]); |
||
760 | |||
761 | // < number of item after the ::[score]:[size]:[separator_id]@ , here there are 3 |
||
762 | if (count($listDetails) < 3) { |
||
763 | $listWeightings = explode(',', $listDetails[0]); |
||
764 | $listSizeOfInput = []; |
||
765 | for ($i = 0; $i < count($listWeightings); $i++) { |
||
0 ignored issues
–
show
|
|||
766 | $listSizeOfInput[] = 200; |
||
767 | } |
||
768 | $blankSeparatorNumber = 0; // 0 is [...] |
||
769 | } else { |
||
770 | $listWeightings = explode(',', $listDetails[0]); |
||
771 | $listSizeOfInput = explode(',', $listDetails[1]); |
||
772 | $blankSeparatorNumber = $listDetails[2]; |
||
773 | } |
||
774 | |||
775 | $listAnswerResults['text'] = $listDoubleColon[0]; |
||
776 | $listAnswerResults['weighting'] = $listWeightings; |
||
777 | $listAnswerResults['input_size'] = $listSizeOfInput; |
||
778 | $listAnswerResults['switchable'] = $listArobaseSplit[1]; |
||
779 | $listAnswerResults['blank_separator_start'] = self::getStartSeparator($blankSeparatorNumber); |
||
780 | $listAnswerResults['blank_separator_end'] = self::getEndSeparator($blankSeparatorNumber); |
||
781 | $listAnswerResults['blank_separator_number'] = $blankSeparatorNumber; |
||
782 | |||
783 | $blankCharStart = self::getStartSeparator($blankSeparatorNumber); |
||
784 | $blankCharEnd = self::getEndSeparator($blankSeparatorNumber); |
||
785 | $blankCharStartForRegexp = self::escapeForRegexp($blankCharStart); |
||
786 | $blankCharEndForRegexp = self::escapeForRegexp($blankCharEnd); |
||
787 | |||
788 | // Get all blanks words |
||
789 | $listAnswerResults['words_count'] = api_preg_match_all( |
||
790 | '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/', |
||
791 | $listDoubleColon[0], |
||
792 | $listWords |
||
793 | ); |
||
794 | |||
795 | if ($listAnswerResults['words_count'] > 0) { |
||
796 | $listAnswerResults['words_with_bracket'] = $listWords[0]; |
||
797 | // remove [ and ] in string |
||
798 | array_walk( |
||
799 | $listWords[0], |
||
800 | function (&$value, $key, $tabBlankChar) { |
||
801 | $trimChars = ''; |
||
802 | for ($i = 0; $i < count($tabBlankChar); $i++) { |
||
0 ignored issues
–
show
It seems like you are calling the size function
count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.
If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration: for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}
// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
|
|||
803 | $trimChars .= $tabBlankChar[$i]; |
||
804 | } |
||
805 | $value = trim($value, $trimChars); |
||
806 | }, |
||
807 | [$blankCharStart, $blankCharEnd] |
||
808 | ); |
||
809 | $listAnswerResults['words'] = $listWords[0]; |
||
810 | } |
||
811 | |||
812 | // Get all common words |
||
813 | $commonWords = api_preg_replace( |
||
814 | '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/', |
||
815 | '::', |
||
816 | $listDoubleColon[0] |
||
817 | ); |
||
818 | |||
819 | // if student answer, the second [] is the student answer, |
||
820 | // the third is if student scored or not |
||
821 | $listBrackets = []; |
||
822 | $listWords = []; |
||
823 | if ($isStudentAnswer) { |
||
824 | for ($i = 0; $i < count($listAnswerResults['words']); $i++) { |
||
825 | $listBrackets[] = $listAnswerResults['words_with_bracket'][$i]; |
||
826 | $listWords[] = $listAnswerResults['words'][$i]; |
||
827 | if ($i + 1 < count($listAnswerResults['words'])) { |
||
828 | // should always be |
||
829 | $i++; |
||
830 | } |
||
831 | $listAnswerResults['student_answer'][] = $listAnswerResults['words'][$i]; |
||
832 | if ($i + 1 < count($listAnswerResults['words'])) { |
||
833 | // should always be |
||
834 | $i++; |
||
835 | } |
||
836 | $listAnswerResults['student_score'][] = $listAnswerResults['words'][$i]; |
||
837 | } |
||
838 | $listAnswerResults['words'] = $listWords; |
||
839 | $listAnswerResults['words_with_bracket'] = $listBrackets; |
||
840 | |||
841 | // if we are in student view, we've got 3 times :::::: for common words |
||
842 | $commonWords = api_preg_replace("/::::::/", '::', $commonWords); |
||
843 | } |
||
844 | $listAnswerResults['common_words'] = explode('::', $commonWords); |
||
845 | |||
846 | return $listAnswerResults; |
||
847 | } |
||
848 | |||
849 | /** |
||
850 | * Return an array of student state answers for fill the blank questions |
||
851 | * for each students that answered the question |
||
852 | * -2 : didn't answer |
||
853 | * -1 : student answer is wrong |
||
854 | * 0 : student answer is correct |
||
855 | * >0 : fill the blank question with choice menu, is the index of the student answer (right answer index is 0). |
||
856 | * |
||
857 | * @param int $testId |
||
858 | * @param int $questionId |
||
859 | * @param $studentsIdList |
||
860 | * @param string $startDate |
||
861 | * @param string $endDate |
||
862 | * @param bool $useLastAnsweredAttempt |
||
863 | * |
||
864 | * @return array |
||
865 | * ( |
||
866 | * [student_id] => Array |
||
867 | * ( |
||
868 | * [first fill the blank for question] => -1 |
||
869 | * [second fill the blank for question] => 2 |
||
870 | * [third fill the blank for question] => -1 |
||
871 | * ) |
||
872 | * ) |
||
873 | */ |
||
874 | public static function getFillTheBlankResult( |
||
875 | $testId, |
||
876 | $questionId, |
||
877 | $studentsIdList, |
||
878 | $startDate, |
||
879 | $endDate, |
||
880 | $useLastAnsweredAttempt = true |
||
881 | ) { |
||
882 | $tblTrackEAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT); |
||
883 | $tblTrackEExercise = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); |
||
884 | $courseId = api_get_course_int_id(); |
||
885 | // If no user has answered questions, no need to go further. Return empty array. |
||
886 | if (empty($studentsIdList)) { |
||
887 | return []; |
||
888 | } |
||
889 | // request to have all the answers of student for this question |
||
890 | // student may have doing it several time |
||
891 | // student may have not answered the bracket id, in this case, is result of the answer is empty |
||
892 | // we got the less recent attempt first |
||
893 | $sql = 'SELECT * FROM '.$tblTrackEAttempt.' tea |
||
894 | LEFT JOIN '.$tblTrackEExercise.' tee |
||
895 | ON |
||
896 | tee.exe_id = tea.exe_id AND |
||
897 | tea.c_id = '.$courseId.' AND |
||
898 | exe_exo_id = '.$testId.' |
||
899 | WHERE |
||
900 | tee.c_id = '.$courseId.' AND |
||
901 | question_id = '.$questionId.' AND |
||
902 | tea.user_id IN ('.implode(',', $studentsIdList).') AND |
||
903 | tea.tms >= "'.$startDate.'" AND |
||
904 | tea.tms <= "'.$endDate.'" |
||
905 | ORDER BY user_id, tea.exe_id; |
||
906 | '; |
||
907 | |||
908 | $res = Database::query($sql); |
||
909 | $userResult = []; |
||
910 | // foreach attempts for all students starting with his older attempt |
||
911 | while ($data = Database::fetch_array($res)) { |
||
912 | $answer = self::getAnswerInfo($data['answer'], true); |
||
913 | |||
914 | // for each bracket to find in this question |
||
915 | foreach ($answer['student_answer'] as $bracketNumber => $studentAnswer) { |
||
916 | if ($answer['student_answer'][$bracketNumber] != '') { |
||
917 | // student has answered this bracket, cool |
||
918 | switch (self::getFillTheBlankAnswerType($answer['words'][$bracketNumber])) { |
||
919 | case self::FILL_THE_BLANK_MENU: |
||
920 | // get the indice of the choosen answer in the menu |
||
921 | // we know that the right answer is the first entry of the menu, ie 0 |
||
922 | // (remember, menu entries are shuffled when taking the test) |
||
923 | $userResult[$data['user_id']][$bracketNumber] = self::getFillTheBlankMenuAnswerNum( |
||
924 | $answer['words'][$bracketNumber], |
||
925 | $answer['student_answer'][$bracketNumber] |
||
926 | ); |
||
927 | break; |
||
928 | default: |
||
929 | if (self::isStudentAnswerGood( |
||
930 | $answer['student_answer'][$bracketNumber], |
||
931 | $answer['words'][$bracketNumber] |
||
932 | ) |
||
933 | ) { |
||
934 | $userResult[$data['user_id']][$bracketNumber] = 0; // right answer |
||
935 | } else { |
||
936 | $userResult[$data['user_id']][$bracketNumber] = -1; // wrong answer |
||
937 | } |
||
938 | } |
||
939 | } else { |
||
940 | // student didn't answer this bracket |
||
941 | if ($useLastAnsweredAttempt) { |
||
942 | // if we take into account the last answered attempt |
||
943 | if (!isset($userResult[$data['user_id']][$bracketNumber])) { |
||
944 | $userResult[$data['user_id']][$bracketNumber] = -2; // not answered |
||
945 | } |
||
946 | } else { |
||
947 | // we take the last attempt, even if the student answer the question before |
||
948 | $userResult[$data['user_id']][$bracketNumber] = -2; // not answered |
||
949 | } |
||
950 | } |
||
951 | } |
||
952 | } |
||
953 | |||
954 | return $userResult; |
||
955 | } |
||
956 | |||
957 | /** |
||
958 | * Return the number of student that give at leat an answer in the fill the blank test. |
||
959 | * |
||
960 | * @param array $resultList |
||
961 | * |
||
962 | * @return int |
||
963 | */ |
||
964 | public static function getNbResultFillBlankAll($resultList) |
||
965 | { |
||
966 | $outRes = 0; |
||
967 | // for each student in group |
||
968 | foreach ($resultList as $list) { |
||
969 | $found = false; |
||
970 | // for each bracket, if student has at least one answer ( choice > -2) then he pass the question |
||
971 | foreach ($list as $choice) { |
||
972 | if ($choice > -2 && !$found) { |
||
973 | $outRes++; |
||
974 | $found = true; |
||
975 | } |
||
976 | } |
||
977 | } |
||
978 | |||
979 | return $outRes; |
||
980 | } |
||
981 | |||
982 | /** |
||
983 | * Replace the occurrence of blank word with [correct answer][student answer][answer is correct]. |
||
984 | * |
||
985 | * @param array $listWithStudentAnswer |
||
986 | * |
||
987 | * @return string |
||
988 | */ |
||
989 | public static function getAnswerInStudentAttempt($listWithStudentAnswer) |
||
990 | { |
||
991 | $separatorStart = $listWithStudentAnswer['blank_separator_start']; |
||
992 | $separatorEnd = $listWithStudentAnswer['blank_separator_end']; |
||
993 | // lets rebuild the sentence with [correct answer][student answer][answer is correct] |
||
994 | $result = ''; |
||
995 | for ($i = 0; $i < count($listWithStudentAnswer['common_words']) - 1; $i++) { |
||
996 | $answerValue = null; |
||
997 | if (isset($listWithStudentAnswer['student_answer'][$i])) { |
||
998 | $answerValue = $listWithStudentAnswer['student_answer'][$i]; |
||
999 | } |
||
1000 | $scoreValue = null; |
||
1001 | if (isset($listWithStudentAnswer['student_score'][$i])) { |
||
1002 | $scoreValue = $listWithStudentAnswer['student_score'][$i]; |
||
1003 | } |
||
1004 | |||
1005 | $result .= $listWithStudentAnswer['common_words'][$i]; |
||
1006 | $result .= $listWithStudentAnswer['words_with_bracket'][$i]; |
||
1007 | $result .= $separatorStart.$answerValue.$separatorEnd; |
||
1008 | $result .= $separatorStart.$scoreValue.$separatorEnd; |
||
1009 | } |
||
1010 | $result .= $listWithStudentAnswer['common_words'][$i]; |
||
1011 | $result .= '::'; |
||
1012 | // add the system string |
||
1013 | $result .= $listWithStudentAnswer['system_string']; |
||
1014 | |||
1015 | return $result; |
||
1016 | } |
||
1017 | |||
1018 | /** |
||
1019 | * This function is the same than the js one above getBlankSeparatorRegexp. |
||
1020 | * |
||
1021 | * @param string $inChar |
||
1022 | * |
||
1023 | * @return string |
||
1024 | */ |
||
1025 | public static function escapeForRegexp($inChar) |
||
1026 | { |
||
1027 | $listChars = [ |
||
1028 | ".", |
||
1029 | "+", |
||
1030 | "*", |
||
1031 | "?", |
||
1032 | "[", |
||
1033 | "^", |
||
1034 | "]", |
||
1035 | "$", |
||
1036 | "(", |
||
1037 | ")", |
||
1038 | "{", |
||
1039 | "}", |
||
1040 | "=", |
||
1041 | "!", |
||
1042 | ">", |
||
1043 | "|", |
||
1044 | ":", |
||
1045 | "-", |
||
1046 | ")", |
||
1047 | ]; |
||
1048 | |||
1049 | if (in_array($inChar, $listChars)) { |
||
1050 | return "\\".$inChar; |
||
1051 | } else { |
||
1052 | return $inChar; |
||
1053 | } |
||
1054 | } |
||
1055 | |||
1056 | /** |
||
1057 | * This function must be the same than the js one getSeparatorFromNumber above. |
||
1058 | * |
||
1059 | * @return array |
||
1060 | */ |
||
1061 | public static function getAllowedSeparator() |
||
1062 | { |
||
1063 | return [ |
||
1064 | ['[', ']'], |
||
1065 | ['{', '}'], |
||
1066 | ['(', ')'], |
||
1067 | ['*', '*'], |
||
1068 | ['#', '#'], |
||
1069 | ['%', '%'], |
||
1070 | ['$', '$'], |
||
1071 | ]; |
||
1072 | } |
||
1073 | |||
1074 | /** |
||
1075 | * return the start separator for answer. |
||
1076 | * |
||
1077 | * @param string $number |
||
1078 | * |
||
1079 | * @return string |
||
1080 | */ |
||
1081 | public static function getStartSeparator($number) |
||
1082 | { |
||
1083 | $listSeparators = self::getAllowedSeparator(); |
||
1084 | |||
1085 | return $listSeparators[$number][0]; |
||
1086 | } |
||
1087 | |||
1088 | /** |
||
1089 | * return the end separator for answer. |
||
1090 | * |
||
1091 | * @param string $number |
||
1092 | * |
||
1093 | * @return string |
||
1094 | */ |
||
1095 | public static function getEndSeparator($number) |
||
1096 | { |
||
1097 | $listSeparators = self::getAllowedSeparator(); |
||
1098 | |||
1099 | return $listSeparators[$number][1]; |
||
1100 | } |
||
1101 | |||
1102 | /** |
||
1103 | * Return as a description text, array of allowed separators for question |
||
1104 | * eg: array("[...]", "(...)"). |
||
1105 | * |
||
1106 | * @return array |
||
1107 | */ |
||
1108 | public static function getAllowedSeparatorForSelect() |
||
1109 | { |
||
1110 | $listResults = []; |
||
1111 | $allowedSeparator = self::getAllowedSeparator(); |
||
1112 | foreach ($allowedSeparator as $part) { |
||
1113 | $listResults[] = $part[0].'...'.$part[1]; |
||
1114 | } |
||
1115 | |||
1116 | return $listResults; |
||
1117 | } |
||
1118 | |||
1119 | /** |
||
1120 | * return the HTML display of the answer. |
||
1121 | * |
||
1122 | * @param string $answer |
||
1123 | * @param int $feedbackType |
||
1124 | * @param bool $resultsDisabled |
||
1125 | * @param bool $showTotalScoreAndUserChoices |
||
1126 | * |
||
1127 | * @return string |
||
1128 | */ |
||
1129 | public static function getHtmlDisplayForAnswer( |
||
1130 | $answer, |
||
1131 | $feedbackType, |
||
1132 | $resultsDisabled = false, |
||
1133 | $showTotalScoreAndUserChoices = false |
||
1134 | ) { |
||
1135 | $result = ''; |
||
1136 | $listStudentAnswerInfo = self::getAnswerInfo($answer, true); |
||
1137 | |||
1138 | // rebuild the answer with good HTML style |
||
1139 | // this is the student answer, right or wrong |
||
1140 | for ($i = 0; $i < count($listStudentAnswerInfo['student_answer']); $i++) { |
||
1141 | if ($listStudentAnswerInfo['student_score'][$i] == 1) { |
||
1142 | $listStudentAnswerInfo['student_answer'][$i] = self::getHtmlRightAnswer( |
||
1143 | $listStudentAnswerInfo['student_answer'][$i], |
||
1144 | $listStudentAnswerInfo['words'][$i], |
||
1145 | $feedbackType, |
||
1146 | $resultsDisabled, |
||
1147 | $showTotalScoreAndUserChoices |
||
1148 | ); |
||
1149 | } else { |
||
1150 | $listStudentAnswerInfo['student_answer'][$i] = self::getHtmlWrongAnswer( |
||
1151 | $listStudentAnswerInfo['student_answer'][$i], |
||
1152 | $listStudentAnswerInfo['words'][$i], |
||
1153 | $feedbackType, |
||
1154 | $resultsDisabled, |
||
1155 | $showTotalScoreAndUserChoices |
||
1156 | ); |
||
1157 | } |
||
1158 | } |
||
1159 | |||
1160 | // rebuild the sentence with student answer inserted |
||
1161 | for ($i = 0; $i < count($listStudentAnswerInfo['common_words']); $i++) { |
||
1162 | if ($resultsDisabled == RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK) { |
||
1163 | if (empty($listStudentAnswerInfo['student_answer'][$i])) { |
||
1164 | continue; |
||
1165 | } |
||
1166 | } |
||
1167 | $result .= isset($listStudentAnswerInfo['common_words'][$i]) ? $listStudentAnswerInfo['common_words'][$i] : ''; |
||
1168 | $studentLabel = isset($listStudentAnswerInfo['student_answer'][$i]) ? $listStudentAnswerInfo['student_answer'][$i] : ''; |
||
1169 | $result .= $studentLabel; |
||
1170 | } |
||
1171 | |||
1172 | // the last common word (should be </p>) |
||
1173 | $result .= isset($listStudentAnswerInfo['common_words'][$i]) ? $listStudentAnswerInfo['common_words'][$i] : ''; |
||
1174 | |||
1175 | return $result; |
||
1176 | } |
||
1177 | |||
1178 | /** |
||
1179 | * return the HTML code of answer for correct and wrong answer. |
||
1180 | * |
||
1181 | * @param string $answer |
||
1182 | * @param string $correct |
||
1183 | * @param string $right |
||
1184 | * @param int $feedbackType |
||
1185 | * @param bool $resultsDisabled |
||
1186 | * @param bool $showTotalScoreAndUserChoices |
||
1187 | * |
||
1188 | * @return string |
||
1189 | */ |
||
1190 | public static function getHtmlAnswer( |
||
1191 | $answer, |
||
1192 | $correct, |
||
1193 | $right, |
||
1194 | $feedbackType, |
||
1195 | $resultsDisabled = false, |
||
1196 | $showTotalScoreAndUserChoices = false |
||
1197 | ) { |
||
1198 | $hideExpectedAnswer = false; |
||
1199 | $hideUserSelection = false; |
||
1200 | switch ($resultsDisabled) { |
||
1201 | case RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING: |
||
1202 | case RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER: |
||
1203 | $hideUserSelection = true; |
||
1204 | break; |
||
1205 | case RESULT_DISABLE_SHOW_SCORE_ONLY: |
||
1206 | if (0 == $feedbackType) { |
||
1207 | $hideExpectedAnswer = true; |
||
1208 | } |
||
1209 | break; |
||
1210 | case RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK: |
||
1211 | case RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK: |
||
1212 | case RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT: |
||
1213 | $hideExpectedAnswer = true; |
||
1214 | if ($showTotalScoreAndUserChoices) { |
||
1215 | $hideExpectedAnswer = false; |
||
1216 | } |
||
1217 | break; |
||
1218 | } |
||
1219 | |||
1220 | $style = 'feedback-green'; |
||
1221 | $iconAnswer = Display::return_icon('attempt-check.png', get_lang('Correct'), null, ICON_SIZE_SMALL); |
||
1222 | if (!$right) { |
||
1223 | $style = 'feedback-red'; |
||
1224 | $iconAnswer = Display::return_icon('attempt-nocheck.png', get_lang('Incorrect'), null, ICON_SIZE_SMALL); |
||
1225 | } |
||
1226 | |||
1227 | $correctAnswerHtml = ''; |
||
1228 | $type = self::getFillTheBlankAnswerType($correct); |
||
1229 | switch ($type) { |
||
1230 | case self::FILL_THE_BLANK_MENU: |
||
1231 | $listPossibleAnswers = self::getFillTheBlankMenuAnswers($correct, false); |
||
1232 | $correctAnswerHtml .= "<span class='correct-answer'><strong>".$listPossibleAnswers[0]."</strong>"; |
||
1233 | $correctAnswerHtml .= ' ('; |
||
1234 | for ($i = 1; $i < count($listPossibleAnswers); $i++) { |
||
1235 | $correctAnswerHtml .= $listPossibleAnswers[$i]; |
||
1236 | if ($i != count($listPossibleAnswers) - 1) { |
||
1237 | $correctAnswerHtml .= ' | '; |
||
1238 | } |
||
1239 | } |
||
1240 | $correctAnswerHtml .= ")</span>"; |
||
1241 | break; |
||
1242 | case self::FILL_THE_BLANK_SEVERAL_ANSWER: |
||
1243 | $listCorrects = explode('||', $correct); |
||
1244 | $firstCorrect = $correct; |
||
1245 | if (count($listCorrects) > 0) { |
||
1246 | $firstCorrect = $listCorrects[0]; |
||
1247 | } |
||
1248 | $correctAnswerHtml = "<span class='correct-answer'>".$firstCorrect."</span>"; |
||
1249 | break; |
||
1250 | case self::FILL_THE_BLANK_STANDARD: |
||
1251 | default: |
||
1252 | $correctAnswerHtml = "<span class='correct-answer'>".$correct."</span>"; |
||
1253 | } |
||
1254 | |||
1255 | if ($hideExpectedAnswer) { |
||
1256 | $correctAnswerHtml = "<span |
||
1257 | class='feedback-green' |
||
1258 | title='".get_lang('ExerciseWithFeedbackWithoutCorrectionComment')."'> — </span>"; |
||
1259 | } |
||
1260 | |||
1261 | $result = "<span class='feedback-question'>"; |
||
1262 | if ($hideUserSelection === false) { |
||
1263 | $result .= $iconAnswer."<span class='$style'>".$answer."</span>"; |
||
1264 | } |
||
1265 | $result .= "<span class='feedback-separator'>|</span>"; |
||
1266 | $result .= $correctAnswerHtml; |
||
1267 | $result .= '</span>'; |
||
1268 | |||
1269 | return $result; |
||
1270 | } |
||
1271 | |||
1272 | /** |
||
1273 | * return HTML code for correct answer. |
||
1274 | * |
||
1275 | * @param string $answer |
||
1276 | * @param string $correct |
||
1277 | * @param string $feedbackType |
||
1278 | * @param bool $resultsDisabled |
||
1279 | * @param bool $showTotalScoreAndUserChoices |
||
1280 | * |
||
1281 | * @return string |
||
1282 | */ |
||
1283 | public static function getHtmlRightAnswer( |
||
1284 | $answer, |
||
1285 | $correct, |
||
1286 | $feedbackType, |
||
1287 | $resultsDisabled = false, |
||
1288 | $showTotalScoreAndUserChoices = false |
||
1289 | ) { |
||
1290 | return self::getHtmlAnswer( |
||
1291 | $answer, |
||
1292 | $correct, |
||
1293 | true, |
||
1294 | $feedbackType, |
||
1295 | $resultsDisabled, |
||
1296 | $showTotalScoreAndUserChoices |
||
1297 | ); |
||
1298 | } |
||
1299 | |||
1300 | /** |
||
1301 | * return HTML code for wrong answer. |
||
1302 | * |
||
1303 | * @param string $answer |
||
1304 | * @param string $correct |
||
1305 | * @param string $feedbackType |
||
1306 | * @param bool $resultsDisabled |
||
1307 | * @param bool $showTotalScoreAndUserChoices |
||
1308 | * |
||
1309 | * @return string |
||
1310 | */ |
||
1311 | public static function getHtmlWrongAnswer( |
||
1312 | $answer, |
||
1313 | $correct, |
||
1314 | $feedbackType, |
||
1315 | $resultsDisabled = false, |
||
1316 | $showTotalScoreAndUserChoices = false |
||
1317 | ) { |
||
1318 | if ($resultsDisabled == RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK) { |
||
1319 | return ''; |
||
1320 | } |
||
1321 | |||
1322 | return self::getHtmlAnswer( |
||
1323 | $answer, |
||
1324 | $correct, |
||
1325 | false, |
||
1326 | $feedbackType, |
||
1327 | $resultsDisabled, |
||
1328 | $showTotalScoreAndUserChoices |
||
1329 | ); |
||
1330 | } |
||
1331 | |||
1332 | /** |
||
1333 | * Check if a answer is correct by its text. |
||
1334 | * |
||
1335 | * @param string $answerText |
||
1336 | * |
||
1337 | * @return bool |
||
1338 | */ |
||
1339 | public static function isCorrect($answerText) |
||
1340 | { |
||
1341 | $answerInfo = self::getAnswerInfo($answerText, true); |
||
1342 | $correctAnswerList = $answerInfo['words']; |
||
1343 | $studentAnswer = $answerInfo['student_answer']; |
||
1344 | $isCorrect = true; |
||
1345 | |||
1346 | foreach ($correctAnswerList as $i => $correctAnswer) { |
||
1347 | $value = self::isStudentAnswerGood($studentAnswer[$i], $correctAnswer); |
||
1348 | $isCorrect = $isCorrect && $value; |
||
1349 | } |
||
1350 | |||
1351 | return $isCorrect; |
||
1352 | } |
||
1353 | |||
1354 | /** |
||
1355 | * Clear the answer entered by student. |
||
1356 | * |
||
1357 | * @param string $answer |
||
1358 | * |
||
1359 | * @return string |
||
1360 | */ |
||
1361 | public static function clearStudentAnswer($answer) |
||
1362 | { |
||
1363 | $answer = htmlentities(api_utf8_encode($answer), ENT_QUOTES); |
||
1364 | $answer = str_replace(''', ''', $answer); // fix apostrophe |
||
1365 | $answer = api_preg_replace('/\s\s+/', ' ', $answer); // replace excess white spaces |
||
1366 | $answer = strtr($answer, array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES))); |
||
1367 | |||
1368 | return trim($answer); |
||
1369 | } |
||
1370 | |||
1371 | /** |
||
1372 | * Removes double spaces between words. |
||
1373 | * |
||
1374 | * @param string $text |
||
1375 | * |
||
1376 | * @return string |
||
1377 | */ |
||
1378 | private static function trimOption($text) |
||
1379 | { |
||
1380 | $text = trim($text); |
||
1381 | |||
1382 | return preg_replace("/\s+/", ' ', $text); |
||
1383 | } |
||
1384 | } |
||
1385 |
If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration: