FillBlanks::getNbResultFillBlankAll()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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