Issues (2160)

main/exercise/fill_blanks.class.php (6 issues)

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
        $listWeightings = explode(',', $listDetails[0]);
828
        if (count($listDetails) < 3) {
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
            $listSizeOfInput = explode(',', $listDetails[1]);
836
            $blankSeparatorNumber = $listDetails[2];
837
        }
838
839
        $listAnswerResults['text'] = $listDoubleColon[0];
840
        $listAnswerResults['weighting'] = $listWeightings;
841
        $listAnswerResults['input_size'] = $listSizeOfInput;
842
        $listAnswerResults['switchable'] = $listArobaseSplit[1];
843
        $listAnswerResults['blank_separator_start'] = self::getStartSeparator($blankSeparatorNumber);
844
        $listAnswerResults['blank_separator_end'] = self::getEndSeparator($blankSeparatorNumber);
845
        $listAnswerResults['blank_separator_number'] = $blankSeparatorNumber;
846
847
        $blankCharStart = self::getStartSeparator($blankSeparatorNumber);
848
        $blankCharEnd = self::getEndSeparator($blankSeparatorNumber);
849
        $blankCharStartForRegexp = self::escapeForRegexp($blankCharStart);
850
        $blankCharEndForRegexp = self::escapeForRegexp($blankCharEnd);
851
852
        // Get all blanks words
853
        $listAnswerResults['words_count'] = api_preg_match_all(
854
            '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
855
            $listDoubleColon[0],
856
            $listWords
857
        );
858
859
        if ($listAnswerResults['words_count'] > 0) {
860
            $listAnswerResults['words_with_bracket'] = $listWords[0];
861
            // remove [ and ] in string
862
            array_walk(
863
                $listWords[0],
864
                function (&$value, $key, $tabBlankChar) {
865
                    $trimChars = '';
866
                    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...
867
                        $trimChars .= $tabBlankChar[$i];
868
                    }
869
                    $value = trim($value, $trimChars);
870
                },
871
                [$blankCharStart, $blankCharEnd]
872
            );
873
            $listAnswerResults['words'] = $listWords[0];
874
        }
875
876
        // Get all common words
877
        $commonWords = api_preg_replace(
878
            '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
879
            '::',
880
            $listDoubleColon[0]
881
        );
882
883
        // if student answer, the second [] is the student answer,
884
        // the third is if student scored or not
885
        $listBrackets = [];
886
        $listWords = [];
887
        if ($isStudentAnswer) {
888
            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...
889
                $listBrackets[] = $listAnswerResults['words_with_bracket'][$i];
890
                $listWords[] = $listAnswerResults['words'][$i];
891
                if ($i + 1 < count($listAnswerResults['words'])) {
892
                    // should always be
893
                    $i++;
894
                }
895
                $listAnswerResults['student_answer'][] = Security::remove_XSS($listAnswerResults['words'][$i]);
896
                if ($i + 1 < count($listAnswerResults['words'])) {
897
                    // should always be
898
                    $i++;
899
                }
900
                $listAnswerResults['student_score'][] = $listAnswerResults['words'][$i];
901
            }
902
            $listAnswerResults['words'] = $listWords;
903
            $listAnswerResults['words_with_bracket'] = $listBrackets;
904
905
            // if we are in student view, we've got 3 times :::::: for common words
906
            $commonWords = api_preg_replace("/::::::/", '::', $commonWords);
907
        }
908
        $listAnswerResults['common_words'] = explode('::', $commonWords);
909
        $listAnswerResults['words_types'] = array_map(
910
            function ($word): int {
911
                return FillBlanks::getFillTheBlankAnswerType($word);
912
            },
913
            $listAnswerResults['words']
914
        );
915
916
        return $listAnswerResults;
917
    }
918
919
    /**
920
     * Return an array of student state answers for fill the blank questions
921
     * for each students that answered the question
922
     * -2  : didn't answer
923
     * -1  : student answer is wrong
924
     *  0  : student answer is correct
925
     * >0  : fill the blank question with choice menu, is the index of the student answer (right answer index is 0).
926
     *
927
     * @param int $testId
928
     * @param int $questionId
929
     * @param $studentsIdList
930
     * @param string $startDate
931
     * @param string $endDate
932
     * @param bool   $useLastAnsweredAttempt
933
     *
934
     * @return array
935
     *               (
936
     *               [student_id] => Array
937
     *               (
938
     *               [first fill the blank for question] => -1
939
     *               [second fill the blank for question] => 2
940
     *               [third fill the blank for question] => -1
941
     *               )
942
     *               )
943
     */
944
    public static function getFillTheBlankResult(
945
        $testId,
946
        $questionId,
947
        $studentsIdList,
948
        $startDate,
949
        $endDate,
950
        $useLastAnsweredAttempt = true
951
    ) {
952
        $tblTrackEAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
953
        $tblTrackEExercise = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
954
        $courseId = api_get_course_int_id();
955
        // If no user has answered questions, no need to go further. Return empty array.
956
        if (empty($studentsIdList)) {
957
            return [];
958
        }
959
        // request to have all the answers of student for this question
960
        // student may have doing it several time
961
        // student may have not answered the bracket id, in this case, is result of the answer is empty
962
        // we got the less recent attempt first
963
        $sql = 'SELECT * FROM '.$tblTrackEAttempt.' tea
964
                LEFT JOIN '.$tblTrackEExercise.' tee
965
                ON
966
                    tee.exe_id = tea.exe_id AND
967
                    tea.c_id = '.$courseId.' AND
968
                    exe_exo_id = '.$testId.'
969
               WHERE
970
                    tee.c_id = '.$courseId.' AND
971
                    question_id = '.$questionId.' AND
972
                    tea.user_id IN ('.implode(',', $studentsIdList).')  AND
973
                    tea.tms >= "'.$startDate.'" AND
974
                    tea.tms <= "'.$endDate.'"
975
               ORDER BY user_id, tea.exe_id;
976
        ';
977
978
        $res = Database::query($sql);
979
        $userResult = [];
980
        // foreach attempts for all students starting with his older attempt
981
        while ($data = Database::fetch_array($res)) {
982
            $answer = self::getAnswerInfo($data['answer'], true);
983
984
            // for each bracket to find in this question
985
            foreach ($answer['student_answer'] as $bracketNumber => $studentAnswer) {
986
                if ($answer['student_answer'][$bracketNumber] != '') {
987
                    // student has answered this bracket, cool
988
                    switch (self::getFillTheBlankAnswerType($answer['words'][$bracketNumber])) {
989
                        case self::FILL_THE_BLANK_MENU:
990
                            // get the indice of the choosen answer in the menu
991
                            // we know that the right answer is the first entry of the menu, ie 0
992
                            // (remember, menu entries are shuffled when taking the test)
993
                            $userResult[$data['user_id']][$bracketNumber] = self::getFillTheBlankMenuAnswerNum(
994
                                $answer['words'][$bracketNumber],
995
                                $answer['student_answer'][$bracketNumber]
996
                            );
997
                            break;
998
                        default:
999
                            if (self::isStudentAnswerGood(
1000
                                $answer['student_answer'][$bracketNumber],
1001
                                $answer['words'][$bracketNumber]
1002
                            )
1003
                            ) {
1004
                                $userResult[$data['user_id']][$bracketNumber] = 0; //  right answer
1005
                            } else {
1006
                                $userResult[$data['user_id']][$bracketNumber] = -1; // wrong answer
1007
                            }
1008
                    }
1009
                } else {
1010
                    // student didn't answer this bracket
1011
                    if ($useLastAnsweredAttempt) {
1012
                        // if we take into account the last answered attempt
1013
                        if (!isset($userResult[$data['user_id']][$bracketNumber])) {
1014
                            $userResult[$data['user_id']][$bracketNumber] = -2; // not answered
1015
                        }
1016
                    } else {
1017
                        // we take the last attempt, even if the student answer the question before
1018
                        $userResult[$data['user_id']][$bracketNumber] = -2; // not answered
1019
                    }
1020
                }
1021
            }
1022
        }
1023
1024
        return $userResult;
1025
    }
1026
1027
    /**
1028
     * Return the number of student that give at leat an answer in the fill the blank test.
1029
     *
1030
     * @param array $resultList
1031
     *
1032
     * @return int
1033
     */
1034
    public static function getNbResultFillBlankAll($resultList)
1035
    {
1036
        $outRes = 0;
1037
        // for each student in group
1038
        foreach ($resultList as $list) {
1039
            $found = false;
1040
            // for each bracket, if student has at least one answer ( choice > -2) then he pass the question
1041
            foreach ($list as $choice) {
1042
                if ($choice > -2 && !$found) {
1043
                    $outRes++;
1044
                    $found = true;
1045
                }
1046
            }
1047
        }
1048
1049
        return $outRes;
1050
    }
1051
1052
    /**
1053
     * Replace the occurrence of blank word with [correct answer][student answer][answer is correct].
1054
     *
1055
     * @param array $listWithStudentAnswer
1056
     *
1057
     * @return string
1058
     */
1059
    public static function getAnswerInStudentAttempt($listWithStudentAnswer)
1060
    {
1061
        $separatorStart = $listWithStudentAnswer['blank_separator_start'];
1062
        $separatorEnd = $listWithStudentAnswer['blank_separator_end'];
1063
        // lets rebuild the sentence with [correct answer][student answer][answer is correct]
1064
        $result = '';
1065
        for ($i = 0; $i < count($listWithStudentAnswer['common_words']) - 1; $i++) {
1066
            $answerValue = null;
1067
            if (isset($listWithStudentAnswer['student_answer'][$i])) {
1068
                $answerValue = $listWithStudentAnswer['student_answer'][$i];
1069
            }
1070
            $scoreValue = null;
1071
            if (isset($listWithStudentAnswer['student_score'][$i])) {
1072
                $scoreValue = $listWithStudentAnswer['student_score'][$i];
1073
            }
1074
1075
            $result .= $listWithStudentAnswer['common_words'][$i];
1076
            $result .= $listWithStudentAnswer['words_with_bracket'][$i];
1077
            $result .= $separatorStart.$answerValue.$separatorEnd;
1078
            $result .= $separatorStart.$scoreValue.$separatorEnd;
1079
        }
1080
        $result .= $listWithStudentAnswer['common_words'][$i];
1081
        $result .= '::';
1082
        // add the system string
1083
        $result .= $listWithStudentAnswer['system_string'];
1084
1085
        return $result;
1086
    }
1087
1088
    /**
1089
     * This function is the same than the js one above getBlankSeparatorRegexp.
1090
     *
1091
     * @param string $inChar
1092
     *
1093
     * @return string
1094
     */
1095
    public static function escapeForRegexp($inChar)
1096
    {
1097
        $listChars = [
1098
            ".",
1099
            "+",
1100
            "*",
1101
            "?",
1102
            "[",
1103
            "^",
1104
            "]",
1105
            "$",
1106
            "(",
1107
            ")",
1108
            "{",
1109
            "}",
1110
            "=",
1111
            "!",
1112
            ">",
1113
            "|",
1114
            ":",
1115
            "-",
1116
            ")",
1117
        ];
1118
1119
        if (in_array($inChar, $listChars)) {
1120
            return "\\".$inChar;
1121
        } else {
1122
            return $inChar;
1123
        }
1124
    }
1125
1126
    /**
1127
     * This function must be the same than the js one getSeparatorFromNumber above.
1128
     *
1129
     * @return array
1130
     */
1131
    public static function getAllowedSeparator()
1132
    {
1133
        return [
1134
            ['[', ']'],
1135
            ['{', '}'],
1136
            ['(', ')'],
1137
            ['*', '*'],
1138
            ['#', '#'],
1139
            ['%', '%'],
1140
            ['$', '$'],
1141
        ];
1142
    }
1143
1144
    /**
1145
     * return the start separator for answer.
1146
     *
1147
     * @param string $number
1148
     *
1149
     * @return string
1150
     */
1151
    public static function getStartSeparator($number)
1152
    {
1153
        $listSeparators = self::getAllowedSeparator();
1154
1155
        return $listSeparators[$number][0];
1156
    }
1157
1158
    /**
1159
     * return the end separator for answer.
1160
     *
1161
     * @param string $number
1162
     *
1163
     * @return string
1164
     */
1165
    public static function getEndSeparator($number)
1166
    {
1167
        $listSeparators = self::getAllowedSeparator();
1168
1169
        return $listSeparators[$number][1];
1170
    }
1171
1172
    /**
1173
     * Return as a description text, array of allowed separators for question
1174
     * eg: array("[...]", "(...)").
1175
     *
1176
     * @return array
1177
     */
1178
    public static function getAllowedSeparatorForSelect()
1179
    {
1180
        $listResults = [];
1181
        $allowedSeparator = self::getAllowedSeparator();
1182
        foreach ($allowedSeparator as $part) {
1183
            $listResults[] = $part[0].'...'.$part[1];
1184
        }
1185
1186
        return $listResults;
1187
    }
1188
1189
    /**
1190
     * return the HTML display of the answer.
1191
     *
1192
     * @param string $answer
1193
     * @param int    $feedbackType
1194
     * @param bool   $resultsDisabled
1195
     * @param bool   $showTotalScoreAndUserChoices
1196
     *
1197
     * @return string
1198
     */
1199
    public static function getHtmlDisplayForAnswer(
1200
        $answer,
1201
        $feedbackType,
1202
        $resultsDisabled = false,
1203
        $showTotalScoreAndUserChoices = false,
1204
        $exercise
1205
    ) {
1206
        $result = '';
1207
        $listStudentAnswerInfo = self::getAnswerInfo($answer, true);
1208
1209
        // rebuild the answer with good HTML style
1210
        // this is the student answer, right or wrong
1211
        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...
1212
            if ($listStudentAnswerInfo['student_score'][$i] == 1) {
1213
                $listStudentAnswerInfo['student_answer'][$i] = self::getHtmlRightAnswer(
1214
                    $listStudentAnswerInfo['student_answer'][$i],
1215
                    $listStudentAnswerInfo['words'][$i],
1216
                    $feedbackType,
1217
                    $resultsDisabled,
1218
                    $showTotalScoreAndUserChoices,
1219
                    $exercise
1220
                );
1221
            } else {
1222
                $listStudentAnswerInfo['student_answer'][$i] = self::getHtmlWrongAnswer(
1223
                    $listStudentAnswerInfo['student_answer'][$i],
1224
                    $listStudentAnswerInfo['words'][$i],
1225
                    $feedbackType,
1226
                    $resultsDisabled,
1227
                    $showTotalScoreAndUserChoices,
1228
                    $exercise
1229
                );
1230
            }
1231
        }
1232
1233
        // rebuild the sentence with student answer inserted
1234
        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...
1235
            if ($resultsDisabled == RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK) {
1236
                if (empty($listStudentAnswerInfo['student_answer'][$i])) {
1237
                    continue;
1238
                }
1239
            }
1240
            $result .= $listStudentAnswerInfo['common_words'][$i] ?? '';
1241
            $studentLabel = $listStudentAnswerInfo['student_answer'][$i] ?? '';
1242
            $result .= $studentLabel;
1243
        }
1244
1245
        // the last common word (should be </p>)
1246
        $result .= $listStudentAnswerInfo['common_words'][$i] ?? '';
1247
1248
        return $result;
1249
    }
1250
1251
    /**
1252
     * return the HTML code of answer for correct and wrong answer.
1253
     *
1254
     * @param string $answer
1255
     * @param string $correct
1256
     * @param string $right
1257
     * @param int    $feedbackType
1258
     * @param bool   $resultsDisabled
1259
     * @param bool   $showTotalScoreAndUserChoices
1260
     */
1261
    public static function getHtmlAnswer(
1262
        $answer,
1263
        $correct,
1264
        $right,
1265
        $feedbackType,
1266
        $resultsDisabled = false,
1267
        $showTotalScoreAndUserChoices = false,
1268
        $exercise
1269
    ): string {
1270
        $hideExpectedAnswer = false;
1271
        $hideUserSelection = false;
1272
        if (!$exercise->showExpectedChoiceColumn()) {
1273
            $hideExpectedAnswer = true;
1274
        }
1275
        switch ($resultsDisabled) {
1276
            case RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING:
1277
            case RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER:
1278
                $hideUserSelection = true;
1279
                break;
1280
            case RESULT_DISABLE_SHOW_SCORE_ONLY:
1281
                if (0 == $feedbackType) {
1282
                    $hideExpectedAnswer = true;
1283
                }
1284
                break;
1285
            case RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK:
1286
            case RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK:
1287
            case RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT:
1288
                $hideExpectedAnswer = true;
1289
                if ($showTotalScoreAndUserChoices) {
1290
                    $hideExpectedAnswer = false;
1291
                }
1292
                break;
1293
        }
1294
1295
        $style = 'feedback-green';
1296
        $iconAnswer = Display::return_icon('attempt-check.png', get_lang('Correct'), null, ICON_SIZE_SMALL);
1297
        if (!$right) {
1298
            $style = 'feedback-red';
1299
            $iconAnswer = Display::return_icon('attempt-nocheck.png', get_lang('Incorrect'), null, ICON_SIZE_SMALL);
1300
        }
1301
1302
        $correctAnswerHtml = '';
1303
        $type = self::getFillTheBlankAnswerType($correct);
1304
        switch ($type) {
1305
            case self::FILL_THE_BLANK_MENU:
1306
                $listPossibleAnswers = self::getFillTheBlankMenuAnswers($correct, false);
1307
                $correctAnswerHtml .= "<span class='correct-answer'><strong>".$listPossibleAnswers[0]."</strong>";
1308
                $correctAnswerHtml .= ' (';
1309
                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...
1310
                    $correctAnswerHtml .= $listPossibleAnswers[$i];
1311
                    if ($i != count($listPossibleAnswers) - 1) {
1312
                        $correctAnswerHtml .= ' | ';
1313
                    }
1314
                }
1315
                $correctAnswerHtml .= ")</span>";
1316
                break;
1317
            case self::FILL_THE_BLANK_SEVERAL_ANSWER:
1318
                $listCorrects = explode('||', $correct);
1319
                $firstCorrect = $correct;
1320
                if (count($listCorrects) > 0) {
1321
                    $firstCorrect = $listCorrects[0];
1322
                }
1323
                $correctAnswerHtml = "<span class='correct-answer'>".$firstCorrect."</span>";
1324
                break;
1325
            case self::FILL_THE_BLANK_STANDARD:
1326
            default:
1327
                $correctAnswerHtml = "<span class='correct-answer'>".$correct."</span>";
1328
        }
1329
1330
        if ($hideExpectedAnswer) {
1331
            $correctAnswerHtml = "<span
1332
                class='feedback-green'
1333
                title='".get_lang('ExerciseWithFeedbackWithoutCorrectionComment')."'> &#8212; </span>";
1334
        }
1335
1336
        $result = "<span class='feedback-question'>";
1337
        if ($hideUserSelection === false) {
1338
            $result .= $iconAnswer."<span class='$style'>".$answer."</span>";
1339
        }
1340
        $result .= "<span class='feedback-separator'>|</span>";
1341
        $result .= $correctAnswerHtml;
1342
        $result .= '</span>';
1343
1344
        return $result;
1345
    }
1346
1347
    /**
1348
     * return HTML code for correct answer.
1349
     *
1350
     * @param string $answer
1351
     * @param string $correct
1352
     * @param string $feedbackType
1353
     * @param bool   $resultsDisabled
1354
     * @param bool   $showTotalScoreAndUserChoices
1355
     *
1356
     * @return string
1357
     */
1358
    public static function getHtmlRightAnswer(
1359
        $answer,
1360
        $correct,
1361
        $feedbackType,
1362
        $resultsDisabled = false,
1363
        $showTotalScoreAndUserChoices = false,
1364
        $exercise
1365
    ) {
1366
        return self::getHtmlAnswer(
1367
            $answer,
1368
            $correct,
1369
            true,
1370
            $feedbackType,
1371
            $resultsDisabled,
1372
            $showTotalScoreAndUserChoices,
1373
            $exercise
1374
        );
1375
    }
1376
1377
    /**
1378
     * return HTML code for wrong answer.
1379
     *
1380
     * @param string $answer
1381
     * @param string $correct
1382
     * @param string $feedbackType
1383
     * @param bool   $resultsDisabled
1384
     * @param bool   $showTotalScoreAndUserChoices
1385
     *
1386
     * @return string
1387
     */
1388
    public static function getHtmlWrongAnswer(
1389
        $answer,
1390
        $correct,
1391
        $feedbackType,
1392
        $resultsDisabled = false,
1393
        $showTotalScoreAndUserChoices = false,
1394
        $exercise
1395
    ) {
1396
        return self::getHtmlAnswer(
1397
            $answer,
1398
            $correct,
1399
            false,
1400
            $feedbackType,
1401
            $resultsDisabled,
1402
            $showTotalScoreAndUserChoices,
1403
            $exercise
1404
        );
1405
    }
1406
1407
    /**
1408
     * Check if a answer is correct by its text.
1409
     *
1410
     * @param string $answerText
1411
     */
1412
    public static function isCorrect($answerText): bool
1413
    {
1414
        $answerInfo = self::getAnswerInfo($answerText, true);
1415
        $correctAnswerList = $answerInfo['words'];
1416
        $studentAnswer = $answerInfo['student_answer'];
1417
        $isCorrect = true;
1418
1419
        foreach ($correctAnswerList as $i => $correctAnswer) {
1420
            $value = self::isStudentAnswerGood($studentAnswer[$i], $correctAnswer);
1421
            $isCorrect = $isCorrect && $value;
1422
        }
1423
1424
        return $isCorrect;
1425
    }
1426
1427
    /**
1428
     * Clear the answer entered by student.
1429
     *
1430
     * @param string $answer
1431
     */
1432
    public static function clearStudentAnswer($answer): string
1433
    {
1434
        $answer = htmlentities(api_utf8_encode($answer), ENT_QUOTES);
1435
        $answer = str_replace('&#039;', '&#39;', $answer); // fix apostrophe
1436
        $answer = api_preg_replace('/\s\s+/', ' ', $answer); // replace excess white spaces
1437
        $answer = strtr($answer, array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES)));
1438
1439
        return trim($answer);
1440
    }
1441
1442
    /**
1443
     * Removes double spaces between words.
1444
     *
1445
     * @param string $text
1446
     *
1447
     * @return string
1448
     */
1449
    private static function trimOption($text)
1450
    {
1451
        $text = trim($text);
1452
1453
        return preg_replace("/\s+/", ' ', $text);
1454
    }
1455
}
1456