Completed
Push — master ( 27e209...a08afa )
by Julito
186:04 queued 150:53
created

FillBlanks::getFillTheBlankSeveralAnswers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/* For licensing terms, see /license.txt */
3
4
/**
5
 *  Class FillBlanks
6
 *
7
 * @author Eric Marguin
8
 * @author Julio Montoya multiple fill in blank option added
9
 * @package chamilo.exercise
10
 **/
11
class FillBlanks extends Question
12
{
13
    const FILL_THE_BLANK_STANDARD = 0;
14
    const FILL_THE_BLANK_MENU = 1;
15
    const FILL_THE_BLANK_SEVERAL_ANSWER = 2;
16
17
    public static $typePicture = 'fill_in_blanks.png';
18
    public static $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
        if (!empty($this->id)) {
37
            $objectAnswer = new Answer($this->id);
38
            $answer = $objectAnswer->selectAnswer(1);
39
            $listAnswersInfo = self::getAnswerInfo($answer);
40
            if ($listAnswersInfo['switchable']) {
41
                $defaults['multiple_answer'] = 1;
42
            } else {
43
                $defaults['multiple_answer'] = 0;
44
            }
45
            // Take the complete string except after the last '::'
46
            $defaults['answer'] = $listAnswersInfo['text'];
47
            $defaults['select_separator'] = $listAnswersInfo['blank_separator_number'];
48
            $blankSeparatorNumber = $listAnswersInfo['blank_separator_number'];
49
        } else {
50
            $defaults['answer'] = get_lang('DefaultTextInBlanks');
51
            $defaults['select_separator'] = 0;
52
            $blankSeparatorNumber = 0;
53
        }
54
55
        $blankSeparatorStart = self::getStartSeparator($blankSeparatorNumber);
56
        $blankSeparatorEnd = self::getEndSeparator($blankSeparatorNumber);
57
        $setWeightAndSize = '';
58
        if (isset($listAnswersInfo) && count($listAnswersInfo['weighting']) > 0) {
59
            foreach ($listAnswersInfo['weighting'] as $i => $weighting) {
60
                $setWeightAndSize .= 'document.getElementById("weighting['.$i.']").value = "'.$weighting.'";';
61
            }
62
            foreach ($listAnswersInfo['input_size'] as $i => $sizeOfInput) {
63
                $setWeightAndSize .= 'document.getElementById("sizeofinput['.$i.']").value = "'.$sizeOfInput.'";';
64
                $setWeightAndSize .= 'document.getElementById("samplesize['.$i.']").style.width = "'.$sizeOfInput.'px";';
65
            }
66
        }
67
68
        echo '<script>            
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
                fields += "<th style=\"width:50px\">'.get_lang("QuestionWeighting").'</th>";
107
                fields += "<th>'.get_lang("BlankInputSize").'</th></tr>";
108
109
                if (blanks != null) {
110
                    for (var i=0; i < blanks.length; i++) {
111
                        // remove forbidden characters that causes bugs
112
                        blanks[i] = removeForbiddenChars(blanks[i]);                        
113
                        // trim blanks between brackets
114
                        blanks[i] = trimBlanksBetweenSeparator(blanks[i], blankSeparatorStart, blankSeparatorEnd);
115
                        
116
                        // if the word is empty []
117
                        if (blanks[i] == blankSeparatorStartRegexp+blankSeparatorEndRegexp) {
118
                            break;
119
                        }
120
                        
121
                        // get input size
122
                        var inputSize = 100;                        
123
                        var textValue = blanks[i].substr(1, blanks[i].length - 2);
124
                        var btoaValue = textValue.hashCode();
125
                                                                      
126
                        if (firstTime == false) {
127
                            var element = document.getElementById("samplesize["+i+"]");                                
128
                            if (element) {
129
                                inputSize = document.getElementById("sizeofinput["+i+"]").value;                                
130
                            }
131
                        }                                                                    
132
133
                        if (document.getElementById("weighting["+i+"]")) {
134
                            var value = document.getElementById("weighting["+i+"]").value;
135
                        } else {
136
                            var value = "1";    
137
                        }                
138
                        var blanksWithColor = trimBlanksBetweenSeparator(blanks[i], blankSeparatorStart, blankSeparatorEnd, 1);
139
                        
140
                        fields += "<tr>";
141
                        fields += "<td>"+blanksWithColor+"</td>";
142
                        fields += "<td><input class=\"form-control\" style=\"width:60px\" value=\""+value+"\" type=\"text\" id=\"weighting["+i+"]\" name=\"weighting["+i+"]\" /></td>";
143
                        fields += "<td>";
144
                        fields += "<input class=\"btn btn-default\" type=\"button\" value=\"-\" onclick=\"changeInputSize(-1, "+i+")\">&nbsp;";
145
                        fields += "<input class=\"btn btn-default\" type=\"button\" value=\"+\" onclick=\"changeInputSize(1, "+i+")\">&nbsp;";
146
                        fields += "&nbsp;&nbsp;<input class=\"sample\" id=\"samplesize["+i+"]\" data-btoa=\""+btoaValue+"\"   type=\"text\" value=\""+textValue+"\" style=\"width:"+inputSize+"px\" disabled=disabled />";
147
                        fields += "<input id=\"sizeofinput["+i+"]\" type=\"hidden\" value=\""+inputSize+"\" name=\"sizeofinput["+i+"]\"  />";
148
                        fields += "</td>";
149
                        fields += "</tr>";
150
                        
151
                        // enable the save button
152
                        $("button").removeAttr("disabled");
153
                        $("#defineoneblank").hide();
154
                    }
155
                }                         
156
                
157
                document.getElementById("blanks_weighting").innerHTML = fields + "</table></div></div>";
158
                
159
                $(originalOrder).each(function(i, data) {
160
                     if (firstTime == false) {
161
                        value = data.value;                        
162
                        var d = $("input.sample[data-btoa=\'"+value+"\']");                        
163
                        var id = d.attr("id");   
164
                        if (id) {
165
                            var sizeInputId = id.replace("samplesize", "sizeofinput");                            
166
                            var sizeInputId = sizeInputId.replace("[", "\\\[");
167
                            var sizeInputId = sizeInputId.replace("]", "\\\]");                       
168
                                                         
169
                            $("#"+sizeInputId).val(data.width);                        
170
                            d.outerWidth(data.width+"px");
171
                        }
172
                    }
173
                });
174
                
175
                updateOrder(blanks);               
176
177
                if (firstTime) {
178
                    firstTime = false;
179
                    '.$setWeightAndSize.'
180
                }
181
            }
182
183
            window.onload = updateBlanks;
184
            
185
            String.prototype.hashCode = function() {
186
                var hash = 0, i, chr, len;
187
                if (this.length === 0) return hash;
188
                for (i = 0, len = this.length; i < len; i++) {
189
                    chr   = this.charCodeAt(i);
190
                    hash  = ((hash << 5) - hash) + chr;
191
                    hash |= 0; // Convert to 32bit integer
192
                }
193
                return hash;
194
            };
195
            
196
            function updateOrder(blanks) 
197
            {
198
                originalOrder = new Array();                
199
                 if (blanks != null) {
200
                    for (var i=0; i < blanks.length; i++) {
201
                        // remove forbidden characters that causes bugs
202
                        blanks[i] = removeForbiddenChars(blanks[i]);                        
203
                        // trim blanks between brackets
204
                        blanks[i] = trimBlanksBetweenSeparator(blanks[i], blankSeparatorStart, blankSeparatorEnd);
205
                        
206
                        // if the word is empty []
207
                        if (blanks[i] == blankSeparatorStartRegexp+blankSeparatorEndRegexp) {
208
                            break;
209
                        }                        
210
                        var textValue = blanks[i].substr(1, blanks[i].length - 2);
211
                        var btoaValue = textValue.hashCode();
212
                        
213
                        if (firstTime == false) {
214
                            var element = document.getElementById("samplesize["+i+"]");                                
215
                            if (element) {
216
                                inputSize = document.getElementById("sizeofinput["+i+"]").value;
217
                                originalOrder.push({ "width" : inputSize, "value": btoaValue });                                                                               
218
                            }
219
                        }
220
                    }
221
                }
222
            }
223
            
224
            function changeInputSize(coef, inIdNum)
225
            {
226
                if (firstTime) {
227
                    var field = document.getElementById("answer");
228
                    answer = field.value;
229
                } else {
230
                    answer = CKEDITOR.instances["answer"].getData();
231
                }
232
                
233
                var blanks = answer.match(eval(blanksRegexp));
234
                var currentWidth = $("#samplesize\\\["+inIdNum+"\\\]").width();
235
                var newWidth = currentWidth + coef * 20;
236
                newWidth = Math.max(20, newWidth);
237
                newWidth = Math.min(newWidth, 600);
238
                $("#samplesize\\\["+inIdNum+"\\\]").outerWidth(newWidth);
239
                $("#sizeofinput\\\["+inIdNum+"\\\]").attr("value", newWidth);
240
                
241
                updateOrder(blanks);
242
            }
243
244
            function removeForbiddenChars(inTxt)
245
            {
246
                outTxt = inTxt;
247
                outTxt = outTxt.replace(/&quot;/g, ""); // remove the   char
248
                outTxt = outTxt.replace(/\x22/g, ""); // remove the   char
249
                outTxt = outTxt.replace(/"/g, ""); // remove the   char
250
                outTxt = outTxt.replace(/\\\\/g, ""); // remove the \ char
251
                outTxt = outTxt.replace(/&nbsp;/g, " ");
252
                outTxt = outTxt.replace(/^ +/, "");
253
                outTxt = outTxt.replace(/ +$/, "");
254
                return outTxt;
255
            }
256
257
            function changeBlankSeparator()
258
            {
259
                var separatorNumber = $("#select_separator").val();
260
                var tabSeparator = getSeparatorFromNumber(separatorNumber);
261
                blankSeparatorStart = tabSeparator[0];
262
                blankSeparatorEnd = tabSeparator[1];
263
                blankSeparatorStartRegexp = getBlankSeparatorRegexp(blankSeparatorStart);
264
                blankSeparatorEndRegexp = getBlankSeparatorRegexp(blankSeparatorEnd);
265
                blanksRegexp = "/"+blankSeparatorStartRegexp+"[^"+blankSeparatorStartRegexp+"]*"+blankSeparatorEndRegexp+"/g";
266
                updateBlanks();
267
            }
268
269
            // this function is the same than the PHP one
270
            // if modify it modify the php one escapeForRegexp
271
            function getBlankSeparatorRegexp(inTxt)
272
            {
273
                var tabSpecialChar = new Array(".", "+", "*", "?", "[", "^", "]", "$", "(", ")",
274
                    "{", "}", "=", "!", "<", ">", "|", ":", "-", ")");
275
                for (var i=0; i < tabSpecialChar.length; i++) {
276
                    if (inTxt == tabSpecialChar[i]) {
277
                        return "\\\"+inTxt;
278
                    }
279
                }
280
                return inTxt;
281
            }
282
283
            // this function is the same than the PHP one
284
            // if modify it modify the php one getAllowedSeparator
285
            function getSeparatorFromNumber(number)
286
            {
287
                var separator = new Array();
288
                separator[0] = new Array("[", "]");
289
                separator[1] = new Array("{", "}");
290
                separator[2] = new Array("(", ")");
291
                separator[3] = new Array("*", "*");
292
                separator[4] = new Array("#", "#");
293
                separator[5] = new Array("%", "%");
294
                separator[6] = new Array("$", "$");
295
                return separator[number];
296
            }
297
298
            function trimBlanksBetweenSeparator(inTxt, inSeparatorStart, inSeparatorEnd, addColor)
299
            {
300
                var result = inTxt
301
                result = result.replace(inSeparatorStart, "");
302
                result = result.replace(inSeparatorEnd, "");
303
                result = result.trim();
304
                
305
                if (addColor == 1) {
306
                    var resultParts = result.split("|");
307
                    var partsToString = "";                    
308
                    resultParts.forEach(function(item, index) {                        
309
                        if (index == 0) {
310
                            item = "<b><font style=\"color:green\"> " + item +"</font></b>";
311
                        }
312
                        if (index < resultParts.length - 1) {
313
                            item  = item + " | ";
314
                        }
315
                        partsToString += item;
316
                    });
317
                    result = partsToString;
318
                }
319
                
320
                return inSeparatorStart+result+inSeparatorEnd;
321
            }
322
            
323
        </script>';
324
325
        // answer
326
        $form->addLabel(
327
            null,
328
            get_lang('TypeTextBelow').', '.get_lang('And').' '.get_lang('UseTagForBlank')
329
        );
330
        $form->addElement(
331
            'html_editor',
332
            'answer',
333
            Display::return_icon('fill_field.png'),
334
            ['id' => 'answer'],
335
            ['ToolbarSet' => 'TestQuestionDescription']
336
        );
337
        $form->addRule('answer', get_lang('GiveText'), 'required');
338
339
        //added multiple answers
340
        $form->addElement('checkbox', 'multiple_answer', '', get_lang('FillInBlankSwitchable'));
341
        $form->addElement(
342
            'select',
343
            'select_separator',
344
            get_lang('SelectFillTheBlankSeparator'),
345
            self::getAllowedSeparatorForSelect(),
346
            ' id="select_separator" style="width:150px" class="selectpicker" onchange="changeBlankSeparator()" '
347
        );
348
        $form->addLabel(
349
            null,
350
            '<input type="button" onclick="updateBlanks()" value="'.get_lang('RefreshBlanks').'" class="btn btn-default" />'
351
        );
352
353
        $form->addHtml('<div id="blanks_weighting"></div>');
354
355
        global $text;
356
        // setting the save button here and not in the question class.php
357
        $form->addHtml('<div id="defineoneblank" style="color:#D04A66; margin-left:160px">'.get_lang('DefineBlanks').'</div>');
358
        $form->addButtonSave($text, 'submitQuestion');
359
360
        if (!empty($this->id)) {
361
            $form->setDefaults($defaults);
362
        } else {
363
            if ($this->isContent == 1) {
364
                $form->setDefaults($defaults);
365
            }
366
        }
367
    }
368
369
    /**
370
     * @inheritdoc
371
     */
372
    public function processAnswersCreation($form, $exercise)
373
    {
374
        $answer = $form->getSubmitValue('answer');
375
        // Due the ckeditor transform the elements to their HTML value
376
377
        //$answer = api_html_entity_decode($answer, ENT_QUOTES, $charset);
378
        //$answer = htmlentities(api_utf8_encode($answer));
379
380
        // remove the "::" eventually written by the user
381
        $answer = str_replace('::', '', $answer);
382
383
        // remove starting and ending space and &nbsp;
384
        $answer = api_preg_replace("/\xc2\xa0/", " ", $answer);
385
386
        // start and end separator
387
        $blankStartSeparator = self::getStartSeparator($form->getSubmitValue('select_separator'));
388
        $blankEndSeparator = self::getEndSeparator($form->getSubmitValue('select_separator'));
389
        $blankStartSeparatorRegexp = self::escapeForRegexp($blankStartSeparator);
390
        $blankEndSeparatorRegexp = self::escapeForRegexp($blankEndSeparator);
391
392
        // remove spaces at the beginning and the end of text in square brackets
393
        $answer = preg_replace_callback(
394
            "/".$blankStartSeparatorRegexp."[^]]+".$blankEndSeparatorRegexp."/",
395
            function ($matches) use ($blankStartSeparator, $blankEndSeparator) {
396
                $matchingResult = $matches[0];
397
                $matchingResult = trim($matchingResult, $blankStartSeparator);
398
                $matchingResult = trim($matchingResult, $blankEndSeparator);
399
                $matchingResult = trim($matchingResult);
400
                // remove forbidden chars
401
                $matchingResult = str_replace("/\\/", "", $matchingResult);
402
                $matchingResult = str_replace('/"/', "", $matchingResult);
403
404
                return $blankStartSeparator.$matchingResult.$blankEndSeparator;
405
            },
406
            $answer
407
        );
408
409
        // get the blanks weightings
410
        $nb = preg_match_all(
411
            '/'.$blankStartSeparatorRegexp.'[^'.$blankStartSeparatorRegexp.']*'.$blankEndSeparatorRegexp.'/',
412
            $answer,
413
            $blanks
414
        );
415
416
        if (isset($_GET['editQuestion'])) {
417
            $this->weighting = 0;
418
        }
419
420
        /* if we have some [tobefound] in the text
421
        build the string to save the following in the answers table
422
        <p>I use a [computer] and a [pen].</p>
423
        becomes
424
        <p>I use a [computer] and a [pen].</p>::100,50:100,50@1
425
            ++++++++-------**
426
            --- -- --- -- -
427
            A B  (C) (D)(E)
428
        +++++++ : required, weighting of each words
429
        ------- : optional, input width to display, 200 if not present
430
        ** : equal @1 if "Allow answers order switches" has been checked, @ otherwise
431
        A : weighting for the word [computer]
432
        B : weighting for the word [pen]
433
        C : input width for the word [computer]
434
        D : input width for the word [pen]
435
        E : equal @1 if "Allow answers order switches" has been checked, @ otherwise
436
        */
437
        if ($nb > 0) {
438
            $answer .= '::';
439
            // weighting
440
            for ($i = 0; $i < $nb; ++$i) {
441
                // enter the weighting of word $i
442
                $answer .= $form->getSubmitValue('weighting['.$i.']');
443
                // not the last word, add ","
444
                if ($i != $nb - 1) {
445
                    $answer .= ",";
446
                }
447
                // calculate the global weighting for the question
448
                $this->weighting += (float) $form->getSubmitValue('weighting['.$i.']');
449
            }
450
451
            // input width
452
            $answer .= ":";
453
            for ($i = 0; $i < $nb; ++$i) {
454
                // enter the width of input for word $i
455
                $answer .= $form->getSubmitValue('sizeofinput['.$i.']');
456
                // not the last word, add ","
457
                if ($i != $nb - 1) {
458
                    $answer .= ",";
459
                }
460
            }
461
        }
462
463
        // write the blank separator code number
464
        // see function getAllowedSeparator
465
        /*
466
            0 [...]
467
            1 {...}
468
            2 (...)
469
            3 *...*
470
            4 #...#
471
            5 %...%
472
            6 $...$
473
         */
474
        $answer .= ":".$form->getSubmitValue('select_separator');
475
476
        // Allow answers order switches
477
        $is_multiple = $form -> getSubmitValue('multiple_answer');
478
        $answer .= '@'.$is_multiple;
479
480
        $this->save($exercise);
481
        $objAnswer = new Answer($this->id);
0 ignored issues
show
Bug introduced by
It seems like $this->id can also be of type false; however, parameter $questionId of Answer::__construct() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

481
        $objAnswer = new Answer(/** @scrutinizer ignore-type */ $this->id);
Loading history...
482
        $objAnswer->createAnswer($answer, 0, '', 0, 1);
483
        $objAnswer->save();
484
    }
485
486
    /**
487
     * @inheritdoc
488
     */
489
    public function return_header($exercise, $counter = null, $score = null)
490
    {
491
        $header = parent::return_header($exercise, $counter, $score);
492
        $header .= '<table class="'.$this->question_table_class.'">
493
            <tr>
494
                <th>'.get_lang("Answer").'</th>
495
            </tr>';
496
497
        return $header;
498
    }
499
500
    /**
501
     * @param int $currentQuestion
502
     * @param int $questionId
503
     * @param string $correctItem
504
     * @param array $attributes
505
     * @param string $answer
506
     * @param array $listAnswersInfo
507
     * @param boolean $displayForStudent
508
     * @param int $inBlankNumber
509
     * @return string
510
     */
511
    public static function getFillTheBlankHtml(
512
        $currentQuestion,
513
        $questionId,
514
        $correctItem,
515
        $attributes,
516
        $answer,
517
        $listAnswersInfo,
518
        $displayForStudent,
519
        $inBlankNumber
520
    ) {
521
        $inTabTeacherSolution = $listAnswersInfo['words'];
522
        $inTeacherSolution = $inTabTeacherSolution[$inBlankNumber];
523
524
        switch (self::getFillTheBlankAnswerType($inTeacherSolution)) {
525
            case self::FILL_THE_BLANK_MENU:
526
                $selected = '';
527
                // the blank menu
528
                // display a menu from answer separated with |
529
                // if display for student, shuffle the correct answer menu
530
                $listMenu = self::getFillTheBlankMenuAnswers(
531
                    $inTeacherSolution,
532
                    $displayForStudent
533
                );
534
535
                $resultOptions = ['' => '--'];
536
                foreach ($listMenu as $item) {
537
                    $resultOptions[sha1($item)] = $item;
538
                }
539
540
                //var_dump($resultOptions, $correctItem);
541
542
                foreach ($resultOptions as $key => $value) {
543
                    if ($correctItem == $value) {
544
                        $selected = $key;
545
546
                        break;
547
                    }
548
                }
549
550
                $result = Display::select(
551
                    "choice[$questionId][]",
552
                    $resultOptions,
553
                    $selected,
554
                    ['class' => 'selectpicker'],
555
                    false
556
                );
557
                break;
558
            case self::FILL_THE_BLANK_SEVERAL_ANSWER:
559
            case self::FILL_THE_BLANK_STANDARD:
560
            default:
561
                $attributes['id'] = 'choice_id_'.$currentQuestion.'_'.$inBlankNumber;
562
                $result = Display::input(
563
                    'text',
564
                    "choice[$questionId][]",
565
                    $correctItem,
566
                    $attributes
567
                );
568
                break;
569
        }
570
571
        return $result;
572
    }
573
574
    /**
575
     * Removes double spaces between words
576
     * @param string $text
577
     * @return string
578
     */
579
    private static function trimOption($text)
580
    {
581
        $text = trim($text);
582
        $text = preg_replace("/\s+/", " ", $text);
583
584
        return $text;
585
    }
586
587
    /**
588
     * Return an array with the different choices available
589
     * when the answers between bracket show as a menu
590
     * @param string $correctAnswer
591
     * @param bool $displayForStudent true if we want to shuffle the choices of the menu for students
592
     *
593
     * @return array
594
     */
595
    public static function getFillTheBlankMenuAnswers($correctAnswer, $displayForStudent)
596
    {
597
        $list = api_preg_split("/\|/", $correctAnswer);
598
        foreach ($list as &$item) {
599
            $item = self::trimOption($item);
600
            $item = api_html_entity_decode($item);
601
        }
602
        // The list is always in the same order, there's no option to allow or disable shuffle options.
603
        if ($displayForStudent) {
604
            shuffle_assoc($list);
605
        }
606
607
        return $list;
608
    }
609
610
    /**
611
     * Return the array index of the student answer
612
     * @param string $correctAnswer the menu Choice1|Choice2|Choice3
613
     * @param string $studentAnswer the student answer must be Choice1 or Choice2 or Choice3
614
     *
615
     * @return int  in the example 0 1 or 2 depending of the choice of the student
616
     */
617
    public static function getFillTheBlankMenuAnswerNum($correctAnswer, $studentAnswer)
618
    {
619
        $listChoices = self::getFillTheBlankMenuAnswers($correctAnswer, false);
620
        foreach ($listChoices as $num => $value) {
621
            if ($value == $studentAnswer) {
622
                return $num;
623
            }
624
        }
625
626
        // should not happened, because student choose the answer in a menu of possible answers
627
        return -1;
628
    }
629
630
631
    /**
632
     * Return the possible answer if the answer between brackets is a multiple choice menu
633
     * @param string $correctAnswer
634
     *
635
     * @return array
636
     */
637
    public static function getFillTheBlankSeveralAnswers($correctAnswer)
638
    {
639
        // is answer||Answer||response||Response , mean answer or Answer ...
640
        $listSeveral = api_preg_split("/\|\|/", $correctAnswer);
641
642
        return $listSeveral;
643
    }
644
645
    /**
646
     * Return true if student answer is right according to the correctAnswer
647
     * it is not as simple as equality, because of the type of Fill The Blank question
648
     * eg : studentAnswer = 'Un' and correctAnswer = 'Un||1||un'
649
     * @param string $studentAnswer [student_answer] of the info array of the answer field
650
     * @param string $correctAnswer [words] of the info array of the answer field
651
     * @param bool $fromDatabase
652
     * @return bool
653
     */
654
    public static function isStudentAnswerGood($studentAnswer, $correctAnswer, $fromDatabase = false)
655
    {
656
        $result = false;
657
        switch (self::getFillTheBlankAnswerType($correctAnswer)) {
658
            case self::FILL_THE_BLANK_MENU:
659
                $listMenu = self::getFillTheBlankMenuAnswers($correctAnswer, false);
660
                if ($studentAnswer != '' && isset($listMenu[0])) {
661
                    // First item is always the correct one.
662
                    $item = $listMenu[0];
663
                    if (!$fromDatabase) {
664
                        $item = sha1($item);
665
                    }
666
                    if ($item === $studentAnswer) {
667
                        $result = true;
668
                    }
669
                }
670
                break;
671
            case self::FILL_THE_BLANK_SEVERAL_ANSWER:
672
                // the answer must be one of the choice made
673
                $listSeveral = self::getFillTheBlankSeveralAnswers($correctAnswer);
674
                $listSeveral = array_map(
675
                    function ($item) {
676
                        return self::trimOption($item);
677
                    },
678
                    $listSeveral
679
                );
680
                $result = in_array($studentAnswer, $listSeveral);
681
                break;
682
            case self::FILL_THE_BLANK_STANDARD:
683
            default:
684
                $correctAnswer = api_html_entity_decode($correctAnswer);
685
                $studentAnswer = htmlspecialchars($studentAnswer);
686
                $result = $studentAnswer == self::trimOption($correctAnswer);
687
688
689
                break;
690
        }
691
        //var_dump($result);
692
693
        return $result;
694
    }
695
696
    /**
697
     * @param string $correctAnswer
698
     *
699
     * @return int
700
     */
701
    public static function getFillTheBlankAnswerType($correctAnswer)
702
    {
703
        if (api_strpos($correctAnswer, '|') && !api_strpos($correctAnswer, '||')) {
704
            return self::FILL_THE_BLANK_MENU;
705
        } elseif (api_strpos($correctAnswer, '||')) {
706
            return self::FILL_THE_BLANK_SEVERAL_ANSWER;
707
        } else {
708
            return self::FILL_THE_BLANK_STANDARD;
709
        }
710
    }
711
712
    /**
713
     * Return information about the answer
714
     * @param string $userAnswer the text of the answer of the question
715
     * @param bool   $isStudentAnswer true if it's a student answer false the empty question model
716
     *
717
     * @return array of information about the answer
718
     */
719
    public static function getAnswerInfo($userAnswer = '', $isStudentAnswer = false)
720
    {
721
        $listAnswerResults = [];
722
        $listAnswerResults['text'] = '';
723
        $listAnswerResults['words_count'] = 0;
724
        $listAnswerResults['words_with_bracket'] = [];
725
        $listAnswerResults['words'] = [];
726
        $listAnswerResults['weighting'] = [];
727
        $listAnswerResults['input_size'] = [];
728
        $listAnswerResults['switchable'] = '';
729
        $listAnswerResults['student_answer'] = [];
730
        $listAnswerResults['student_score'] = [];
731
        $listAnswerResults['blank_separator_number'] = 0;
732
        $listDoubleColon = [];
733
734
        api_preg_match("/(.*)::(.*)$/s", $userAnswer, $listResult);
735
736
        if (count($listResult) < 2) {
737
            $listDoubleColon[] = '';
738
            $listDoubleColon[] = '';
739
        } else {
740
            $listDoubleColon[] = $listResult[1];
741
            $listDoubleColon[] = $listResult[2];
742
        }
743
744
        $listAnswerResults['system_string'] = $listDoubleColon[1];
745
746
        // Make sure we only take the last bit to find special marks
747
        $listArobaseSplit = explode('@', $listDoubleColon[1]);
748
749
        if (count($listArobaseSplit) < 2) {
750
            $listArobaseSplit[1] = '';
751
        }
752
753
        // Take the complete string except after the last '::'
754
        $listDetails = explode(":", $listArobaseSplit[0]);
755
756
        // < number of item after the ::[score]:[size]:[separator_id]@ , here there are 3
757
        if (count($listDetails) < 3) {
758
            $listWeightings = explode(',', $listDetails[0]);
759
            $listSizeOfInput = [];
760
            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...
761
                $listSizeOfInput[] = 200;
762
            }
763
            $blankSeparatorNumber = 0; // 0 is [...]
764
        } else {
765
            $listWeightings = explode(',', $listDetails[0]);
766
            $listSizeOfInput = explode(',', $listDetails[1]);
767
            $blankSeparatorNumber = $listDetails[2];
768
        }
769
770
        $listAnswerResults['text'] = $listDoubleColon[0];
771
        $listAnswerResults['weighting'] = $listWeightings;
772
        $listAnswerResults['input_size'] = $listSizeOfInput;
773
        $listAnswerResults['switchable'] = $listArobaseSplit[1];
774
        $listAnswerResults['blank_separator_start'] = self::getStartSeparator($blankSeparatorNumber);
775
        $listAnswerResults['blank_separator_end'] = self::getEndSeparator($blankSeparatorNumber);
776
        $listAnswerResults['blank_separator_number'] = $blankSeparatorNumber;
777
778
        $blankCharStart = self::getStartSeparator($blankSeparatorNumber);
779
        $blankCharEnd = self::getEndSeparator($blankSeparatorNumber);
780
        $blankCharStartForRegexp = self::escapeForRegexp($blankCharStart);
781
        $blankCharEndForRegexp = self::escapeForRegexp($blankCharEnd);
782
783
        // Get all blanks words
784
        $listAnswerResults['words_count'] = api_preg_match_all(
785
            '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
786
            $listDoubleColon[0],
787
            $listWords
788
        );
789
790
        if ($listAnswerResults['words_count'] > 0) {
791
            $listAnswerResults['words_with_bracket'] = $listWords[0];
792
            // remove [ and ] in string
793
            array_walk(
794
                $listWords[0],
795
                function (&$value, $key, $tabBlankChar) {
796
                    $trimChars = '';
797
                    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...
798
                        $trimChars .= $tabBlankChar[$i];
799
                    }
800
                    $value = trim($value, $trimChars);
801
                },
802
                [$blankCharStart, $blankCharEnd]
803
            );
804
            $listAnswerResults['words'] = $listWords[0];
805
        }
806
807
        // Get all common words
808
        $commonWords = api_preg_replace(
809
            '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
810
            "::",
811
            $listDoubleColon[0]
812
        );
813
814
        // if student answer, the second [] is the student answer,
815
        // the third is if student scored or not
816
        $listBrackets = [];
817
        $listWords = [];
818
        if ($isStudentAnswer) {
819
            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...
820
                $listBrackets[] = $listAnswerResults['words_with_bracket'][$i];
821
                $listWords[] = $listAnswerResults['words'][$i];
822
                if ($i + 1 < count($listAnswerResults['words'])) {
823
                    // should always be
824
                    $i++;
825
                }
826
                $listAnswerResults['student_answer'][] = $listAnswerResults['words'][$i];
827
                if ($i + 1 < count($listAnswerResults['words'])) {
828
                    // should always be
829
                    $i++;
830
                }
831
                $listAnswerResults['student_score'][] = $listAnswerResults['words'][$i];
832
            }
833
            $listAnswerResults['words'] = $listWords;
834
            $listAnswerResults['words_with_bracket'] = $listBrackets;
835
836
            // if we are in student view, we've got 3 times :::::: for common words
837
            $commonWords = api_preg_replace("/::::::/", "::", $commonWords);
838
        }
839
840
        $listAnswerResults['common_words'] = explode("::", $commonWords);
0 ignored issues
show
Bug introduced by
It seems like $commonWords can also be of type array; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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