Passed
Pull Request — master (#6637)
by
unknown
14:37 queued 06:29
created

FillBlanks::createAnswersForm()   C

Complexity

Conditions 11
Paths 54

Size

Total Lines 386
Code Lines 70

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 70
nc 54
nop 1
dl 0
loc 386
rs 6.5077
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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