Passed
Push — master ( f437d8...92f70a )
by Julito
10:14
created

FillBlanks::trimOption()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
rs 10
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
 *
10
 * @package chamilo.exercise
11
 */
12
class FillBlanks extends Question
13
{
14
    const FILL_THE_BLANK_STANDARD = 0;
15
    const FILL_THE_BLANK_MENU = 1;
16
    const FILL_THE_BLANK_SEVERAL_ANSWER = 2;
17
18
    public static $typePicture = 'fill_in_blanks.png';
19
    public static $explanationLangVar = 'FillBlanks';
20
21
    /**
22
     * Constructor.
23
     */
24
    public function __construct()
25
    {
26
        parent::__construct();
27
        $this->type = FILL_IN_BLANKS;
28
        $this->isContent = $this->getIsContent();
29
    }
30
31
    /**
32
     * {@inheritdoc}
33
     */
34
    public function createAnswersForm($form)
35
    {
36
        $defaults = [];
37
        $defaults['answer'] = get_lang('DefaultTextInBlanks');
38
        $defaults['select_separator'] = 0;
39
        $blankSeparatorNumber = 0;
40
        if (!empty($this->id)) {
41
            $objectAnswer = new Answer($this->id);
42
            $answer = $objectAnswer->selectAnswer(1);
43
            $listAnswersInfo = self::getAnswerInfo($answer);
44
            $defaults['multiple_answer'] = 0;
45
            if ($listAnswersInfo['switchable']) {
46
                $defaults['multiple_answer'] = 1;
47
            }
48
            // Take the complete string except after the last '::'
49
            $defaults['answer'] = $listAnswersInfo['text'];
50
            $defaults['select_separator'] = $listAnswersInfo['blank_separator_number'];
51
            $blankSeparatorNumber = $listAnswersInfo['blank_separator_number'];
52
        }
53
54
        $blankSeparatorStart = self::getStartSeparator($blankSeparatorNumber);
55
        $blankSeparatorEnd = self::getEndSeparator($blankSeparatorNumber);
56
        $setWeightAndSize = '';
57
        if (isset($listAnswersInfo) && count($listAnswersInfo['weighting']) > 0) {
58
            foreach ($listAnswersInfo['weighting'] as $i => $weighting) {
59
                $setWeightAndSize .= 'document.getElementById("weighting['.$i.']").value = "'.$weighting.'";';
60
            }
61
            foreach ($listAnswersInfo['input_size'] as $i => $sizeOfInput) {
62
                $setWeightAndSize .= 'document.getElementById("sizeofinput['.$i.']").value = "'.$sizeOfInput.'";';
63
                $setWeightAndSize .= 'document.getElementById("samplesize['.$i.']").style.width = "'.$sizeOfInput.'px";';
64
            }
65
        }
66
67
        echo '<script>            
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
            CKEDITOR.on("instanceCreated", function(e) {
77
                if (e.editor.name === "answer") {                  
78
                    //e.editor.on("change", updateBlanks);
79
                    e.editor.on("change", function(){
80
                        updateBlanks();
81
                    });
82
                }
83
            });
84
            
85
            function updateBlanks()
86
            {                
87
                var answer;                
88
                if (firstTime) {
89
                    var field = document.getElementById("answer");
90
                    answer = field.value;
91
                } else {
92
                    answer = CKEDITOR.instances["answer"].getData();
93
                }
94
                
95
                // disable the save button, if not blanks have been created
96
                $("button").attr("disabled", "disabled");
97
                $("#defineoneblank").show();      
98
                
99
                var blanks = answer.match(eval(blanksRegexp));             
100
                var fields = "<div class=\"form-group \">";                
101
                fields += "<label class=\"col-sm-2 control-label\"></label>";
102
                fields += "<div class=\"col-sm-8\">";
103
                fields += "<table class=\"data_table\">";
104
                fields += "<tr><th style=\"width:220px\">'.get_lang('WordTofind').'</th>";
105
                fields += "<th style=\"width:50px\">'.get_lang('QuestionWeighting').'</th>";
106
                fields += "<th>'.get_lang('BlankInputSize').'</th></tr>";
107
108
                if (blanks != null) {
109
                    for (var i=0; i < blanks.length; i++) {
110
                        // remove forbidden characters that causes bugs
111
                        blanks[i] = removeForbiddenChars(blanks[i]);                        
112
                        // trim blanks between brackets
113
                        blanks[i] = trimBlanksBetweenSeparator(blanks[i], blankSeparatorStart, blankSeparatorEnd);
114
                        
115
                        // if the word is empty []
116
                        if (blanks[i] == blankSeparatorStartRegexp+blankSeparatorEndRegexp) {
117
                            break;
118
                        }
119
                        
120
                        // get input size
121
                        var inputSize = 100;                        
122
                        var textValue = blanks[i].substr(1, blanks[i].length - 2);
123
                        var btoaValue = textValue.hashCode();
124
                                                                      
125
                        if (firstTime == false) {
126
                            var element = document.getElementById("samplesize["+i+"]");                                
127
                            if (element) {
128
                                inputSize = document.getElementById("sizeofinput["+i+"]").value;                                
129
                            }
130
                        }                                                                    
131
132
                        if (document.getElementById("weighting["+i+"]")) {
133
                            var value = document.getElementById("weighting["+i+"]").value;
134
                        } else {
135
                            var value = "1";    
136
                        }                
137
                        var blanksWithColor = trimBlanksBetweenSeparator(blanks[i], blankSeparatorStart, blankSeparatorEnd, 1);
138
                        
139
                        fields += "<tr>";
140
                        fields += "<td>"+blanksWithColor+"</td>";
141
                        fields += "<td><input class=\"form-control\" style=\"width:60px\" value=\""+value+"\" type=\"text\" id=\"weighting["+i+"]\" name=\"weighting["+i+"]\" /></td>";
142
                        fields += "<td>";
143
                        fields += "<input class=\"btn btn-default\" type=\"button\" value=\"-\" onclick=\"changeInputSize(-1, "+i+")\">&nbsp;";
144
                        fields += "<input class=\"btn btn-default\" type=\"button\" value=\"+\" onclick=\"changeInputSize(1, "+i+")\">&nbsp;";
145
                        fields += "&nbsp;&nbsp;<input class=\"sample\" id=\"samplesize["+i+"]\" data-btoa=\""+btoaValue+"\"   type=\"text\" value=\""+textValue+"\" style=\"width:"+inputSize+"px\" disabled=disabled />";
146
                        fields += "<input id=\"sizeofinput["+i+"]\" type=\"hidden\" value=\""+inputSize+"\" name=\"sizeofinput["+i+"]\"  />";
147
                        fields += "</td>";
148
                        fields += "</tr>";
149
                        
150
                        // enable the save button
151
                        $("button").removeAttr("disabled");
152
                        $("#defineoneblank").hide();
153
                    }
154
                }                         
155
                
156
                document.getElementById("blanks_weighting").innerHTML = fields + "</table></div></div>";
157
                
158
                $(originalOrder).each(function(i, data) {
159
                     if (firstTime == false) {
160
                        value = data.value;                        
161
                        var d = $("input.sample[data-btoa=\'"+value+"\']");                        
162
                        var id = d.attr("id");   
163
                        if (id) {
164
                            var sizeInputId = id.replace("samplesize", "sizeofinput");                            
165
                            var sizeInputId = sizeInputId.replace("[", "\\\[");
166
                            var sizeInputId = sizeInputId.replace("]", "\\\]");                                                         
167
                            $("#"+sizeInputId).val(data.width);                        
168
                            d.outerWidth(data.width+"px");
169
                        }
170
                    }
171
                });
172
                
173
                updateOrder(blanks);               
174
175
                if (firstTime) {
176
                    firstTime = false;
177
                    '.$setWeightAndSize.'
178
                }
179
            }
180
181
            window.onload = updateBlanks;            
182
            String.prototype.hashCode = function() {
183
                var hash = 0, i, chr, len;
184
                if (this.length === 0) return hash;
185
                for (i = 0, len = this.length; i < len; i++) {
186
                    chr   = this.charCodeAt(i);
187
                    hash  = ((hash << 5) - hash) + chr;
188
                    hash |= 0; // Convert to 32bit integer
189
                }
190
                return hash;
191
            };
192
            
193
            function updateOrder(blanks) 
194
            {
195
                originalOrder = new Array();                
196
                 if (blanks != null) {
197
                    for (var i=0; i < blanks.length; i++) {
198
                        // remove forbidden characters that causes bugs
199
                        blanks[i] = removeForbiddenChars(blanks[i]);                        
200
                        // trim blanks between brackets
201
                        blanks[i] = trimBlanksBetweenSeparator(blanks[i], blankSeparatorStart, blankSeparatorEnd);
202
                        
203
                        // if the word is empty []
204
                        if (blanks[i] == blankSeparatorStartRegexp+blankSeparatorEndRegexp) {
205
                            break;
206
                        }                        
207
                        var textValue = blanks[i].substr(1, blanks[i].length - 2);
208
                        var btoaValue = textValue.hashCode();
209
                        
210
                        if (firstTime == false) {
211
                            var element = document.getElementById("samplesize["+i+"]");                                
212
                            if (element) {
213
                                inputSize = document.getElementById("sizeofinput["+i+"]").value;
214
                                originalOrder.push({ "width" : inputSize, "value": btoaValue });                                                                               
215
                            }
216
                        }
217
                    }
218
                }
219
            }
220
            
221
            function changeInputSize(coef, inIdNum)
222
            {
223
                if (firstTime) {
224
                    var field = document.getElementById("answer");
225
                    answer = field.value;
226
                } else {
227
                    answer = CKEDITOR.instances["answer"].getData();
228
                }
229
                
230
                var blanks = answer.match(eval(blanksRegexp));
231
                var currentWidth = $("#samplesize\\\["+inIdNum+"\\\]").width();
232
                var newWidth = currentWidth + coef * 20;
233
                newWidth = Math.max(20, newWidth);
234
                newWidth = Math.min(newWidth, 600);
235
                $("#samplesize\\\["+inIdNum+"\\\]").outerWidth(newWidth);
236
                $("#sizeofinput\\\["+inIdNum+"\\\]").attr("value", newWidth);
237
                
238
                updateOrder(blanks);
239
            }
240
241
            function removeForbiddenChars(inTxt)
242
            {
243
                outTxt = inTxt;
244
                outTxt = outTxt.replace(/&quot;/g, ""); // remove the   char
245
                outTxt = outTxt.replace(/\x22/g, ""); // remove the   char
246
                outTxt = outTxt.replace(/"/g, ""); // remove the   char
247
                outTxt = outTxt.replace(/\\\\/g, ""); // remove the \ char
248
                outTxt = outTxt.replace(/&nbsp;/g, " ");
249
                outTxt = outTxt.replace(/^ +/, "");
250
                outTxt = outTxt.replace(/ +$/, "");
251
                
252
                return outTxt;
253
            }
254
255
            function changeBlankSeparator()
256
            {
257
                var separatorNumber = $("#select_separator").val();
258
                var tabSeparator = getSeparatorFromNumber(separatorNumber);
259
                blankSeparatorStart = tabSeparator[0];
260
                blankSeparatorEnd = tabSeparator[1];
261
                blankSeparatorStartRegexp = getBlankSeparatorRegexp(blankSeparatorStart);
262
                blankSeparatorEndRegexp = getBlankSeparatorRegexp(blankSeparatorEnd);
263
                blanksRegexp = "/"+blankSeparatorStartRegexp+"[^"+blankSeparatorStartRegexp+"]*"+blankSeparatorEndRegexp+"/g";
264
                updateBlanks();
265
            }
266
267
            // this function is the same than the PHP one
268
            // if modify it modify the php one escapeForRegexp
269
            function getBlankSeparatorRegexp(inTxt)
270
            {
271
                var tabSpecialChar = new Array(".", "+", "*", "?", "[", "^", "]", "$", "(", ")",
272
                    "{", "}", "=", "!", "<", ">", "|", ":", "-", ")");
273
                for (var i=0; i < tabSpecialChar.length; i++) {
274
                    if (inTxt == tabSpecialChar[i]) {
275
                        return "\\\"+inTxt;
276
                    }
277
                }
278
                return inTxt;
279
            }
280
281
            // this function is the same than the PHP one
282
            // if modify it modify the php one getAllowedSeparator
283
            function getSeparatorFromNumber(number)
284
            {
285
                var separator = new Array();
286
                separator[0] = new Array("[", "]");
287
                separator[1] = new Array("{", "}");
288
                separator[2] = new Array("(", ")");
289
                separator[3] = new Array("*", "*");
290
                separator[4] = new Array("#", "#");
291
                separator[5] = new Array("%", "%");
292
                separator[6] = new Array("$", "$");
293
                return separator[number];
294
            }
295
296
            function trimBlanksBetweenSeparator(inTxt, inSeparatorStart, inSeparatorEnd, addColor)
297
            {
298
                var result = inTxt
299
                result = result.replace(inSeparatorStart, "");
300
                result = result.replace(inSeparatorEnd, "");
301
                result = result.trim();
302
                
303
                if (addColor == 1) {
304
                    var resultParts = result.split("|");
305
                    var partsToString = "";                    
306
                    resultParts.forEach(function(item, index) {                        
307
                        if (index == 0) {
308
                            item = "<b><font style=\"color:green\"> " + item +"</font></b>";
309
                        }
310
                        if (index < resultParts.length - 1) {
311
                            item  = item + " | ";
312
                        }
313
                        partsToString += item;
314
                    });
315
                    result = partsToString;
316
                }
317
                
318
                return inSeparatorStart+result+inSeparatorEnd;
319
            }
320
            
321
        </script>';
322
323
        // answer
324
        $form->addLabel(
325
            null,
326
            get_lang('TypeTextBelow').', '.get_lang('And').' '.get_lang('UseTagForBlank')
327
        );
328
        $form->addElement(
329
            'html_editor',
330
            'answer',
331
            Display::return_icon('fill_field.png'),
332
            ['id' => 'answer'],
333
            ['ToolbarSet' => 'TestQuestionDescription']
334
        );
335
        $form->addRule('answer', get_lang('GiveText'), 'required');
336
337
        //added multiple answers
338
        $form->addElement('checkbox', 'multiple_answer', '', get_lang('FillInBlankSwitchable'));
339
        $form->addElement(
340
            'select',
341
            'select_separator',
342
            get_lang('SelectFillTheBlankSeparator'),
343
            self::getAllowedSeparatorForSelect(),
344
            ' id="select_separator" style="width:150px" class="selectpicker" onchange="changeBlankSeparator()" '
345
        );
346
        $form->addLabel(
347
            null,
348
            '<input type="button" onclick="updateBlanks()" value="'.get_lang('RefreshBlanks').'" class="btn btn-default" />'
349
        );
350
351
        $form->addHtml('<div id="blanks_weighting"></div>');
352
353
        global $text;
354
        // setting the save button here and not in the question class.php
355
        $form->addHtml('<div id="defineoneblank" style="color:#D04A66; margin-left:160px">'.get_lang('DefineBlanks').'</div>');
356
        $form->addButtonSave($text, 'submitQuestion');
357
358
        if (!empty($this->id)) {
359
            $form->setDefaults($defaults);
360
        } else {
361
            if ($this->isContent == 1) {
362
                $form->setDefaults($defaults);
363
            }
364
        }
365
    }
366
367
    /**
368
     * {@inheritdoc}
369
     */
370
    public function processAnswersCreation($form, $exercise)
371
    {
372
        $answer = $form->getSubmitValue('answer');
373
        // Due the ckeditor transform the elements to their HTML value
374
375
        //$answer = api_html_entity_decode($answer, ENT_QUOTES, $charset);
376
        //$answer = htmlentities(api_utf8_encode($answer));
377
378
        // remove the "::" eventually written by the user
379
        $answer = str_replace('::', '', $answer);
380
381
        // remove starting and ending space and &nbsp;
382
        $answer = api_preg_replace("/\xc2\xa0/", " ", $answer);
383
384
        // start and end separator
385
        $blankStartSeparator = self::getStartSeparator($form->getSubmitValue('select_separator'));
386
        $blankEndSeparator = self::getEndSeparator($form->getSubmitValue('select_separator'));
387
        $blankStartSeparatorRegexp = self::escapeForRegexp($blankStartSeparator);
388
        $blankEndSeparatorRegexp = self::escapeForRegexp($blankEndSeparator);
389
390
        // remove spaces at the beginning and the end of text in square brackets
391
        $answer = preg_replace_callback(
392
            "/".$blankStartSeparatorRegexp."[^]]+".$blankEndSeparatorRegexp."/",
393
            function ($matches) use ($blankStartSeparator, $blankEndSeparator) {
394
                $matchingResult = $matches[0];
395
                $matchingResult = trim($matchingResult, $blankStartSeparator);
396
                $matchingResult = trim($matchingResult, $blankEndSeparator);
397
                $matchingResult = trim($matchingResult);
398
                // remove forbidden chars
399
                $matchingResult = str_replace("/\\/", "", $matchingResult);
400
                $matchingResult = str_replace('/"/', "", $matchingResult);
401
402
                return $blankStartSeparator.$matchingResult.$blankEndSeparator;
403
            },
404
            $answer
405
        );
406
407
        // get the blanks weightings
408
        $nb = preg_match_all(
409
            '/'.$blankStartSeparatorRegexp.'[^'.$blankStartSeparatorRegexp.']*'.$blankEndSeparatorRegexp.'/',
410
            $answer,
411
            $blanks
412
        );
413
414
        if (isset($_GET['editQuestion'])) {
415
            $this->weighting = 0;
416
        }
417
418
        /* if we have some [tobefound] in the text
419
        build the string to save the following in the answers table
420
        <p>I use a [computer] and a [pen].</p>
421
        becomes
422
        <p>I use a [computer] and a [pen].</p>::100,50:100,50@1
423
            ++++++++-------**
424
            --- -- --- -- -
425
            A B  (C) (D)(E)
426
        +++++++ : required, weighting of each words
427
        ------- : optional, input width to display, 200 if not present
428
        ** : equal @1 if "Allow answers order switches" has been checked, @ otherwise
429
        A : weighting for the word [computer]
430
        B : weighting for the word [pen]
431
        C : input width for the word [computer]
432
        D : input width for the word [pen]
433
        E : equal @1 if "Allow answers order switches" has been checked, @ otherwise
434
        */
435
        if ($nb > 0) {
436
            $answer .= '::';
437
            // weighting
438
            for ($i = 0; $i < $nb; $i++) {
439
                // enter the weighting of word $i
440
                $answer .= $form->getSubmitValue('weighting['.$i.']');
441
                // not the last word, add ","
442
                if ($i != $nb - 1) {
443
                    $answer .= ',';
444
                }
445
                // calculate the global weighting for the question
446
                $this->weighting += (float) $form->getSubmitValue('weighting['.$i.']');
447
            }
448
449
            // input width
450
            $answer .= ":";
451
            for ($i = 0; $i < $nb; $i++) {
452
                // enter the width of input for word $i
453
                $answer .= $form->getSubmitValue('sizeofinput['.$i.']');
454
                // not the last word, add ","
455
                if ($i != $nb - 1) {
456
                    $answer .= ',';
457
                }
458
            }
459
        }
460
461
        // write the blank separator code number
462
        // see function getAllowedSeparator
463
        /*
464
            0 [...]
465
            1 {...}
466
            2 (...)
467
            3 *...*
468
            4 #...#
469
            5 %...%
470
            6 $...$
471
         */
472
        $answer .= ':'.$form->getSubmitValue('select_separator');
473
474
        // Allow answers order switches
475
        $is_multiple = $form->getSubmitValue('multiple_answer');
476
        $answer .= '@'.$is_multiple;
477
478
        $this->save($exercise);
479
        $objAnswer = new Answer($this->id);
480
        $objAnswer->createAnswer($answer, 0, '', 0, 1);
481
        $objAnswer->save();
482
    }
483
484
    /**
485
     * {@inheritdoc}
486
     */
487
    public function return_header($exercise, $counter = null, $score = null)
488
    {
489
        $header = parent::return_header($exercise, $counter, $score);
490
        $header .= '<table class="'.$this->question_table_class.'">
491
            <tr>
492
                <th>'.get_lang('Answer').'</th>
493
            </tr>';
494
495
        return $header;
496
    }
497
498
    /**
499
     * @param int    $currentQuestion
500
     * @param int    $questionId
501
     * @param string $correctItem
502
     * @param array  $attributes
503
     * @param string $answer
504
     * @param array  $listAnswersInfo
505
     * @param bool   $displayForStudent
506
     * @param int    $inBlankNumber
507
     * @param string $labelId
508
     *
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
        $labelId = ''
521
    ) {
522
        $inTabTeacherSolution = $listAnswersInfo['words'];
523
        $inTeacherSolution = $inTabTeacherSolution[$inBlankNumber];
524
525
        if (empty($labelId)) {
526
            $labelId = 'choice_id_'.$currentQuestion.'_'.$inBlankNumber;
527
        }
528
529
        switch (self::getFillTheBlankAnswerType($inTeacherSolution)) {
530
            case self::FILL_THE_BLANK_MENU:
531
                $selected = '';
532
                // the blank menu
533
                // display a menu from answer separated with |
534
                // if display for student, shuffle the correct answer menu
535
                $listMenu = self::getFillTheBlankMenuAnswers(
536
                    $inTeacherSolution,
537
                    $displayForStudent
538
                );
539
540
                $resultOptions = ['' => '--'];
541
                foreach ($listMenu as $item) {
542
                    $resultOptions[sha1($item)] = $item;
543
                }
544
545
                foreach ($resultOptions as $key => $value) {
546
                    if ($correctItem == $value) {
547
                        $selected = $key;
548
549
                        break;
550
                    }
551
                }
552
                $width = '';
553
                if (!empty($attributes['style'])) {
554
                    $width = str_replace('width:', '', $attributes['style']);
555
                }
556
557
                $result = Display::select(
558
                    "choice[$questionId][]",
559
                    $resultOptions,
560
                    $selected,
561
                    [
562
                        'class' => 'selectpicker',
563
                        'data-width' => $width,
564
                        'id' => $labelId,
565
                    ],
566
                    false
567
                );
568
                break;
569
            case self::FILL_THE_BLANK_SEVERAL_ANSWER:
570
            case self::FILL_THE_BLANK_STANDARD:
571
            default:
572
                $attributes['id'] = $labelId;
573
                $attributes['class'] = 'form-control';
574
                $result = Display::input(
575
                    'text',
576
                    "choice[$questionId][]",
577
                    $correctItem,
578
                    $attributes
579
                );
580
                break;
581
        }
582
583
        return $result;
584
    }
585
586
    /**
587
     * Return an array with the different choices available
588
     * when the answers between bracket show as a menu.
589
     *
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
     *
613
     * @param string $correctAnswer the menu Choice1|Choice2|Choice3
614
     * @param string $studentAnswer the student answer must be Choice1 or Choice2 or Choice3
615
     *
616
     * @return int in the example 0 1 or 2 depending of the choice of the student
617
     */
618
    public static function getFillTheBlankMenuAnswerNum($correctAnswer, $studentAnswer)
619
    {
620
        $listChoices = self::getFillTheBlankMenuAnswers($correctAnswer, false);
621
        foreach ($listChoices as $num => $value) {
622
            if ($value == $studentAnswer) {
623
                return $num;
624
            }
625
        }
626
627
        // should not happened, because student choose the answer in a menu of possible answers
628
        return -1;
629
    }
630
631
    /**
632
     * Return the possible answer if the answer between brackets is a multiple choice menu.
633
     *
634
     * @param string $correctAnswer
635
     *
636
     * @return array
637
     */
638
    public static function getFillTheBlankSeveralAnswers($correctAnswer)
639
    {
640
        // is answer||Answer||response||Response , mean answer or Answer ...
641
        $listSeveral = api_preg_split("/\|\|/", $correctAnswer);
642
643
        return $listSeveral;
644
    }
645
646
    /**
647
     * Return true if student answer is right according to the correctAnswer
648
     * it is not as simple as equality, because of the type of Fill The Blank question
649
     * eg : studentAnswer = 'Un' and correctAnswer = 'Un||1||un'.
650
     *
651
     * @param string $studentAnswer [student_answer] of the info array of the answer field
652
     * @param string $correctAnswer [words] of the info array of the answer field
653
     * @param bool   $fromDatabase
654
     *
655
     * @return bool
656
     */
657
    public static function isStudentAnswerGood($studentAnswer, $correctAnswer, $fromDatabase = false)
658
    {
659
        $result = false;
660
        switch (self::getFillTheBlankAnswerType($correctAnswer)) {
661
            case self::FILL_THE_BLANK_MENU:
662
                $listMenu = self::getFillTheBlankMenuAnswers($correctAnswer, false);
663
                if ($studentAnswer != '' && isset($listMenu[0])) {
664
                    // First item is always the correct one.
665
                    $item = $listMenu[0];
666
                    if (!$fromDatabase) {
667
                        $item = sha1($item);
668
                        $studentAnswer = sha1($studentAnswer);
669
                    }
670
                    if ($item === $studentAnswer) {
671
                        $result = true;
672
                    }
673
                }
674
                break;
675
            case self::FILL_THE_BLANK_SEVERAL_ANSWER:
676
                // the answer must be one of the choice made
677
                $listSeveral = self::getFillTheBlankSeveralAnswers($correctAnswer);
678
                $listSeveral = array_map(
679
                    function ($item) {
680
                        return self::trimOption(api_html_entity_decode($item));
681
                    },
682
                    $listSeveral
683
                );
684
                //$studentAnswer = htmlspecialchars($studentAnswer);
685
                $result = in_array($studentAnswer, $listSeveral);
686
                break;
687
            case self::FILL_THE_BLANK_STANDARD:
688
            default:
689
                $correctAnswer = api_html_entity_decode($correctAnswer);
690
                //$studentAnswer = htmlspecialchars($studentAnswer);
691
                $result = $studentAnswer == self::trimOption($correctAnswer);
692
                break;
693
        }
694
695
        return $result;
696
    }
697
698
    /**
699
     * @param string $correctAnswer
700
     *
701
     * @return int
702
     */
703
    public static function getFillTheBlankAnswerType($correctAnswer)
704
    {
705
        $type = self::FILL_THE_BLANK_STANDARD;
706
        if (api_strpos($correctAnswer, '|') && !api_strpos($correctAnswer, '||')) {
707
            $type = self::FILL_THE_BLANK_MENU;
708
        } elseif (api_strpos($correctAnswer, '||')) {
709
            $type = self::FILL_THE_BLANK_SEVERAL_ANSWER;
710
        }
711
712
        return $type;
713
    }
714
715
    /**
716
     * Return information about the answer.
717
     *
718
     * @param string $userAnswer      the text of the answer of the question
719
     * @param bool   $isStudentAnswer true if it's a student answer false the empty question model
720
     *
721
     * @return array of information about the answer
722
     */
723
    public static function getAnswerInfo($userAnswer = '', $isStudentAnswer = false)
724
    {
725
        $listAnswerResults = [];
726
        $listAnswerResults['text'] = '';
727
        $listAnswerResults['words_count'] = 0;
728
        $listAnswerResults['words_with_bracket'] = [];
729
        $listAnswerResults['words'] = [];
730
        $listAnswerResults['weighting'] = [];
731
        $listAnswerResults['input_size'] = [];
732
        $listAnswerResults['switchable'] = '';
733
        $listAnswerResults['student_answer'] = [];
734
        $listAnswerResults['student_score'] = [];
735
        $listAnswerResults['blank_separator_number'] = 0;
736
        $listDoubleColon = [];
737
738
        api_preg_match("/(.*)::(.*)$/s", $userAnswer, $listResult);
739
740
        if (count($listResult) < 2) {
741
            $listDoubleColon[] = '';
742
            $listDoubleColon[] = '';
743
        } else {
744
            $listDoubleColon[] = $listResult[1];
745
            $listDoubleColon[] = $listResult[2];
746
        }
747
748
        $listAnswerResults['system_string'] = $listDoubleColon[1];
749
750
        // Make sure we only take the last bit to find special marks
751
        $listArobaseSplit = explode('@', $listDoubleColon[1]);
752
753
        if (count($listArobaseSplit) < 2) {
754
            $listArobaseSplit[1] = '';
755
        }
756
757
        // Take the complete string except after the last '::'
758
        $listDetails = explode(':', $listArobaseSplit[0]);
759
760
        // < number of item after the ::[score]:[size]:[separator_id]@ , here there are 3
761
        if (count($listDetails) < 3) {
762
            $listWeightings = explode(',', $listDetails[0]);
763
            $listSizeOfInput = [];
764
            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...
765
                $listSizeOfInput[] = 200;
766
            }
767
            $blankSeparatorNumber = 0; // 0 is [...]
768
        } else {
769
            $listWeightings = explode(',', $listDetails[0]);
770
            $listSizeOfInput = explode(',', $listDetails[1]);
771
            $blankSeparatorNumber = $listDetails[2];
772
        }
773
774
        $listAnswerResults['text'] = $listDoubleColon[0];
775
        $listAnswerResults['weighting'] = $listWeightings;
776
        $listAnswerResults['input_size'] = $listSizeOfInput;
777
        $listAnswerResults['switchable'] = $listArobaseSplit[1];
778
        $listAnswerResults['blank_separator_start'] = self::getStartSeparator($blankSeparatorNumber);
779
        $listAnswerResults['blank_separator_end'] = self::getEndSeparator($blankSeparatorNumber);
780
        $listAnswerResults['blank_separator_number'] = $blankSeparatorNumber;
781
782
        $blankCharStart = self::getStartSeparator($blankSeparatorNumber);
783
        $blankCharEnd = self::getEndSeparator($blankSeparatorNumber);
784
        $blankCharStartForRegexp = self::escapeForRegexp($blankCharStart);
785
        $blankCharEndForRegexp = self::escapeForRegexp($blankCharEnd);
786
787
        // Get all blanks words
788
        $listAnswerResults['words_count'] = api_preg_match_all(
789
            '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
790
            $listDoubleColon[0],
791
            $listWords
792
        );
793
794
        if ($listAnswerResults['words_count'] > 0) {
795
            $listAnswerResults['words_with_bracket'] = $listWords[0];
796
            // remove [ and ] in string
797
            array_walk(
798
                $listWords[0],
799
                function (&$value, $key, $tabBlankChar) {
800
                    $trimChars = '';
801
                    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...
802
                        $trimChars .= $tabBlankChar[$i];
803
                    }
804
                    $value = trim($value, $trimChars);
805
                },
806
                [$blankCharStart, $blankCharEnd]
807
            );
808
            $listAnswerResults['words'] = $listWords[0];
809
        }
810
811
        // Get all common words
812
        $commonWords = api_preg_replace(
813
            '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
814
            "::",
815
            $listDoubleColon[0]
816
        );
817
818
        // if student answer, the second [] is the student answer,
819
        // the third is if student scored or not
820
        $listBrackets = [];
821
        $listWords = [];
822
        if ($isStudentAnswer) {
823
            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...
824
                $listBrackets[] = $listAnswerResults['words_with_bracket'][$i];
825
                $listWords[] = $listAnswerResults['words'][$i];
826
                if ($i + 1 < count($listAnswerResults['words'])) {
827
                    // should always be
828
                    $i++;
829
                }
830
                $listAnswerResults['student_answer'][] = $listAnswerResults['words'][$i];
831
                if ($i + 1 < count($listAnswerResults['words'])) {
832
                    // should always be
833
                    $i++;
834
                }
835
                $listAnswerResults['student_score'][] = $listAnswerResults['words'][$i];
836
            }
837
            $listAnswerResults['words'] = $listWords;
838
            $listAnswerResults['words_with_bracket'] = $listBrackets;
839
840
            // if we are in student view, we've got 3 times :::::: for common words
841
            $commonWords = api_preg_replace("/::::::/", '::', $commonWords);
842
        }
843
        $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

843
        $listAnswerResults['common_words'] = explode('::', /** @scrutinizer ignore-type */ $commonWords);
Loading history...
844
845
        return $listAnswerResults;
846
    }
847
848
    /**
849
     * Return an array of student state answers for fill the blank questions
850
     * for each students that answered the question
851
     * -2  : didn't answer
852
     * -1  : student answer is wrong
853
     *  0  : student answer is correct
854
     * >0  : fill the blank question with choice menu, is the index of the student answer (right answer index is 0).
855
     *
856
     * @param int $testId
857
     * @param int $questionId
858
     * @param $studentsIdList
859
     * @param string $startDate
860
     * @param string $endDate
861
     * @param bool   $useLastAnsweredAttempt
862
     *
863
     * @return array
864
     *               (
865
     *               [student_id] => Array
866
     *               (
867
     *               [first fill the blank for question] => -1
868
     *               [second fill the blank for question] => 2
869
     *               [third fill the blank for question] => -1
870
     *               )
871
     *               )
872
     */
873
    public static function getFillTheBlankResult(
874
        $testId,
875
        $questionId,
876
        $studentsIdList,
877
        $startDate,
878
        $endDate,
879
        $useLastAnsweredAttempt = true
880
    ) {
881
        $tblTrackEAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
882
        $tblTrackEExercise = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
883
        $courseId = api_get_course_int_id();
884
        // If no user has answered questions, no need to go further. Return empty array.
885
        if (empty($studentsIdList)) {
886
            return [];
887
        }
888
        // request to have all the answers of student for this question
889
        // student may have doing it several time
890
        // student may have not answered the bracket id, in this case, is result of the answer is empty
891
        // we got the less recent attempt first
892
        $sql = 'SELECT * FROM '.$tblTrackEAttempt.' tea
893
                LEFT JOIN '.$tblTrackEExercise.' tee
894
                ON 
895
                    tee.exe_id = tea.exe_id AND 
896
                    tea.c_id = '.$courseId.' AND 
897
                    exe_exo_id = '.$testId.'    
898
               WHERE 
899
                    tee.c_id = '.$courseId.' AND 
900
                    question_id = '.$questionId.' AND 
901
                    tea.user_id IN ('.implode(',', $studentsIdList).')  AND 
902
                    tea.tms >= "'.$startDate.'" AND 
903
                    tea.tms <= "'.$endDate.'"
904
               ORDER BY user_id, tea.exe_id;
905
        ';
906
907
        $res = Database::query($sql);
908
        $userResult = [];
909
        // foreach attempts for all students starting with his older attempt
910
        while ($data = Database::fetch_array($res)) {
911
            $answer = self::getAnswerInfo($data['answer'], true);
912
913
            // for each bracket to find in this question
914
            foreach ($answer['student_answer'] as $bracketNumber => $studentAnswer) {
915
                if ($answer['student_answer'][$bracketNumber] != '') {
916
                    // student has answered this bracket, cool
917
                    switch (self::getFillTheBlankAnswerType($answer['words'][$bracketNumber])) {
918
                        case self::FILL_THE_BLANK_MENU:
919
                            // get the indice of the choosen answer in the menu
920
                            // we know that the right answer is the first entry of the menu, ie 0
921
                            // (remember, menu entries are shuffled when taking the test)
922
                            $userResult[$data['user_id']][$bracketNumber] = self::getFillTheBlankMenuAnswerNum(
923
                                $answer['words'][$bracketNumber],
924
                                $answer['student_answer'][$bracketNumber]
925
                            );
926
                            break;
927
                        default:
928
                            if (self::isStudentAnswerGood(
929
                                $answer['student_answer'][$bracketNumber],
930
                                $answer['words'][$bracketNumber]
931
                            )
932
                            ) {
933
                                $userResult[$data['user_id']][$bracketNumber] = 0; //  right answer
934
                            } else {
935
                                $userResult[$data['user_id']][$bracketNumber] = -1; // wrong answer
936
                            }
937
                    }
938
                } else {
939
                    // student didn't answer this bracket
940
                    if ($useLastAnsweredAttempt) {
941
                        // if we take into account the last answered attempt
942
                        if (!isset($userResult[$data['user_id']][$bracketNumber])) {
943
                            $userResult[$data['user_id']][$bracketNumber] = -2; // not answered
944
                        }
945
                    } else {
946
                        // we take the last attempt, even if the student answer the question before
947
                        $userResult[$data['user_id']][$bracketNumber] = -2; // not answered
948
                    }
949
                }
950
            }
951
        }
952
953
        return $userResult;
954
    }
955
956
    /**
957
     * Return the number of student that give at leat an answer in the fill the blank test.
958
     *
959
     * @param array $resultList
960
     *
961
     * @return int
962
     */
963
    public static function getNbResultFillBlankAll($resultList)
964
    {
965
        $outRes = 0;
966
        // for each student in group
967
        foreach ($resultList as $list) {
968
            $found = false;
969
            // for each bracket, if student has at least one answer ( choice > -2) then he pass the question
970
            foreach ($list as $choice) {
971
                if ($choice > -2 && !$found) {
972
                    $outRes++;
973
                    $found = true;
974
                }
975
            }
976
        }
977
978
        return $outRes;
979
    }
980
981
    /**
982
     * Replace the occurrence of blank word with [correct answer][student answer][answer is correct].
983
     *
984
     * @param array $listWithStudentAnswer
985
     *
986
     * @return string
987
     */
988
    public static function getAnswerInStudentAttempt($listWithStudentAnswer)
989
    {
990
        $separatorStart = $listWithStudentAnswer['blank_separator_start'];
991
        $separatorEnd = $listWithStudentAnswer['blank_separator_end'];
992
        // lets rebuild the sentence with [correct answer][student answer][answer is correct]
993
        $result = '';
994
        for ($i = 0; $i < count($listWithStudentAnswer['common_words']) - 1; $i++) {
995
            $answerValue = null;
996
            if (isset($listWithStudentAnswer['student_answer'][$i])) {
997
                $answerValue = $listWithStudentAnswer['student_answer'][$i];
998
            }
999
            $scoreValue = null;
1000
            if (isset($listWithStudentAnswer['student_score'][$i])) {
1001
                $scoreValue = $listWithStudentAnswer['student_score'][$i];
1002
            }
1003
1004
            $result .= $listWithStudentAnswer['common_words'][$i];
1005
            $result .= $listWithStudentAnswer['words_with_bracket'][$i];
1006
            $result .= $separatorStart.$answerValue.$separatorEnd;
1007
            $result .= $separatorStart.$scoreValue.$separatorEnd;
1008
        }
1009
        $result .= $listWithStudentAnswer['common_words'][$i];
1010
        $result .= '::';
1011
        // add the system string
1012
        $result .= $listWithStudentAnswer['system_string'];
1013
1014
        return $result;
1015
    }
1016
1017
    /**
1018
     * This function is the same than the js one above getBlankSeparatorRegexp.
1019
     *
1020
     * @param string $inChar
1021
     *
1022
     * @return string
1023
     */
1024
    public static function escapeForRegexp($inChar)
1025
    {
1026
        $listChars = [
1027
            ".",
1028
            "+",
1029
            "*",
1030
            "?",
1031
            "[",
1032
            "^",
1033
            "]",
1034
            "$",
1035
            "(",
1036
            ")",
1037
            "{",
1038
            "}",
1039
            "=",
1040
            "!",
1041
            ">",
1042
            "|",
1043
            ":",
1044
            "-",
1045
            ")",
1046
        ];
1047
1048
        if (in_array($inChar, $listChars)) {
1049
            return "\\".$inChar;
1050
        } else {
1051
            return $inChar;
1052
        }
1053
    }
1054
1055
    /**
1056
     * return $text protected for use in regexp.
1057
     *
1058
     * @param string $text
1059
     *
1060
     * @return string
1061
     */
1062
    public static function getRegexpProtected($text)
1063
    {
1064
        $listRegexpCharacters = [
1065
            "/",
1066
            ".",
1067
            "+",
1068
            "*",
1069
            "?",
1070
            "[",
1071
            "^",
1072
            "]",
1073
            "$",
1074
            "(",
1075
            ")",
1076
            "{",
1077
            "}",
1078
            "=",
1079
            "!",
1080
            ">",
1081
            "|",
1082
            ":",
1083
            "-",
1084
            ")",
1085
        ];
1086
        $result = $text;
1087
        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...
1088
            $result = str_replace($listRegexpCharacters[$i], "\\".$listRegexpCharacters[$i], $result);
1089
        }
1090
1091
        return $result;
1092
    }
1093
1094
    /**
1095
     * This function must be the same than the js one getSeparatorFromNumber above.
1096
     *
1097
     * @return array
1098
     */
1099
    public static function getAllowedSeparator()
1100
    {
1101
        return [
1102
            ['[', ']'],
1103
            ['{', '}'],
1104
            ['(', ')'],
1105
            ['*', '*'],
1106
            ['#', '#'],
1107
            ['%', '%'],
1108
            ['$', '$'],
1109
        ];
1110
    }
1111
1112
    /**
1113
     * return the start separator for answer.
1114
     *
1115
     * @param string $number
1116
     *
1117
     * @return string
1118
     */
1119
    public static function getStartSeparator($number)
1120
    {
1121
        $listSeparators = self::getAllowedSeparator();
1122
1123
        return $listSeparators[$number][0];
1124
    }
1125
1126
    /**
1127
     * return the end separator for answer.
1128
     *
1129
     * @param string $number
1130
     *
1131
     * @return string
1132
     */
1133
    public static function getEndSeparator($number)
1134
    {
1135
        $listSeparators = self::getAllowedSeparator();
1136
1137
        return $listSeparators[$number][1];
1138
    }
1139
1140
    /**
1141
     * Return as a description text, array of allowed separators for question
1142
     * eg: array("[...]", "(...)").
1143
     *
1144
     * @return array
1145
     */
1146
    public static function getAllowedSeparatorForSelect()
1147
    {
1148
        $listResults = [];
1149
        $allowedSeparator = self::getAllowedSeparator();
1150
        foreach ($allowedSeparator as $part) {
1151
            $listResults[] = $part[0].'...'.$part[1];
1152
        }
1153
1154
        return $listResults;
1155
    }
1156
1157
    /**
1158
     * return the code number of the separator for the question.
1159
     *
1160
     * @param string $startSeparator
1161
     * @param string $endSeparator
1162
     *
1163
     * @return int
1164
     */
1165
    public function getDefaultSeparatorNumber($startSeparator, $endSeparator)
1166
    {
1167
        $listSeparators = self::getAllowedSeparator();
1168
        $result = 0;
1169
        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...
1170
            if ($listSeparators[$i][0] == $startSeparator &&
1171
                $listSeparators[$i][1] == $endSeparator
1172
            ) {
1173
                $result = $i;
1174
            }
1175
        }
1176
1177
        return $result;
1178
    }
1179
1180
    /**
1181
     * return the HTML display of the answer.
1182
     *
1183
     * @param string $answer
1184
     * @param int    $feedbackType
1185
     * @param bool   $resultsDisabled
1186
     * @param bool   $showTotalScoreAndUserChoices
1187
     *
1188
     * @return string
1189
     */
1190
    public static function getHtmlDisplayForAnswer(
1191
        $answer,
1192
        $feedbackType,
1193
        $resultsDisabled = false,
1194
        $showTotalScoreAndUserChoices = false
1195
    ) {
1196
        $result = '';
1197
        $listStudentAnswerInfo = self::getAnswerInfo($answer, true);
1198
1199
        if (in_array($resultsDisabled, [
1200
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
1201
            RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
1202
            ]
1203
        )
1204
        ) {
1205
            $resultsDisabled = true;
1206
            if ($showTotalScoreAndUserChoices) {
1207
                $resultsDisabled = false;
1208
            }
1209
        }
1210
1211
        // rebuild the answer with good HTML style
1212
        // this is the student answer, right or wrong
1213
        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...
1214
            if ($listStudentAnswerInfo['student_score'][$i] == 1) {
1215
                $listStudentAnswerInfo['student_answer'][$i] = self::getHtmlRightAnswer(
1216
                    $listStudentAnswerInfo['student_answer'][$i],
1217
                    $listStudentAnswerInfo['words'][$i],
1218
                    $feedbackType,
1219
                    $resultsDisabled,
1220
                    $showTotalScoreAndUserChoices
1221
                );
1222
            } else {
1223
                $listStudentAnswerInfo['student_answer'][$i] = self::getHtmlWrongAnswer(
1224
                    $listStudentAnswerInfo['student_answer'][$i],
1225
                    $listStudentAnswerInfo['words'][$i],
1226
                    $feedbackType,
1227
                    $resultsDisabled,
1228
                    $showTotalScoreAndUserChoices
1229
                );
1230
            }
1231
        }
1232
1233
        // rebuild the sentence with student answer inserted
1234
        for ($i = 0; $i < count($listStudentAnswerInfo['common_words']); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
1235
            if ($resultsDisabled == RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER) {
1236
                if (isset($listStudentAnswerInfo['student_score'][$i]) &&
1237
                    $listStudentAnswerInfo['student_score'][$i] != 1) {
1238
                    continue;
1239
                }
1240
            }
1241
            $result .= isset($listStudentAnswerInfo['common_words'][$i]) ? $listStudentAnswerInfo['common_words'][$i] : '';
1242
            $result .= isset($listStudentAnswerInfo['student_answer'][$i]) ? $listStudentAnswerInfo['student_answer'][$i] : '';
1243
        }
1244
1245
        // the last common word (should be </p>)
1246
        $result .= isset($listStudentAnswerInfo['common_words'][$i]) ? $listStudentAnswerInfo['common_words'][$i] : '';
1247
1248
        return $result;
1249
    }
1250
1251
    /**
1252
     * return the HTML code of answer for correct and wrong answer.
1253
     *
1254
     * @param string $answer
1255
     * @param string $correct
1256
     * @param string $right
1257
     * @param int    $feedbackType
1258
     * @param bool   $resultsDisabled
1259
     * @param bool   $showTotalScoreAndUserChoices
1260
     *
1261
     * @return string
1262
     */
1263
    public static function getHtmlAnswer(
1264
        $answer,
1265
        $correct,
1266
        $right,
1267
        $feedbackType,
1268
        $resultsDisabled = false,
1269
        $showTotalScoreAndUserChoices = false
1270
    ) {
1271
        $hideExpectedAnswer = false;
1272
        switch ($resultsDisabled) {
1273
            case RESULT_DISABLE_SHOW_SCORE_ONLY:
1274
                if ($feedbackType == 0) {
1275
                    $hideExpectedAnswer = true;
1276
                }
1277
                break;
1278
            case RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK:
1279
            case RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT:
1280
                $hideExpectedAnswer = true;
1281
                if ($showTotalScoreAndUserChoices) {
1282
                    $hideExpectedAnswer = false;
1283
                }
1284
                break;
1285
        }
1286
1287
        $style = 'feedback-green';
1288
        $iconAnswer = Display::return_icon('attempt-check.png', get_lang('Correct'), null, ICON_SIZE_SMALL);
1289
        if (!$right) {
1290
            $style = 'feedback-red';
1291
            $iconAnswer = Display::return_icon('attempt-nocheck.png', get_lang('Incorrect'), null, ICON_SIZE_SMALL);
1292
        }
1293
1294
        $correctAnswerHtml = '';
1295
        $type = self::getFillTheBlankAnswerType($correct);
1296
        switch ($type) {
1297
            case self::FILL_THE_BLANK_MENU:
1298
                $listPossibleAnswers = self::getFillTheBlankMenuAnswers($correct, false);
1299
                $correctAnswerHtml .= "<span class='correct-answer'><strong>".$listPossibleAnswers[0]."</strong>";
1300
                $correctAnswerHtml .= ' (';
1301
                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...
1302
                    $correctAnswerHtml .= $listPossibleAnswers[$i];
1303
                    if ($i != count($listPossibleAnswers) - 1) {
1304
                        $correctAnswerHtml .= ' | ';
1305
                    }
1306
                }
1307
                $correctAnswerHtml .= ")</span>";
1308
                break;
1309
            case self::FILL_THE_BLANK_SEVERAL_ANSWER:
1310
                $listCorrects = explode('||', $correct);
1311
                $firstCorrect = $correct;
1312
                if (count($listCorrects) > 0) {
1313
                    $firstCorrect = $listCorrects[0];
1314
                }
1315
                $correctAnswerHtml = "<span class='correct-answer'>".$firstCorrect."</span>";
1316
                break;
1317
            case self::FILL_THE_BLANK_STANDARD:
1318
            default:
1319
                $correctAnswerHtml = "<span class='correct-answer'>".$correct."</span>";
1320
        }
1321
1322
        if ($hideExpectedAnswer) {
1323
            $correctAnswerHtml = "<span 
1324
                class='feedback-green' 
1325
                title='".get_lang('ExerciseWithFeedbackWithoutCorrectionComment')."'> &#8212; </span>";
1326
        }
1327
1328
        $result = "<span class='feedback-question'>";
1329
        $result .= $iconAnswer."<span class='$style'>".$answer."</span>";
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
        return self::getHtmlAnswer(
1384
            $answer,
1385
            $correct,
1386
            false,
1387
            $feedbackType,
1388
            $resultsDisabled,
1389
            $showTotalScoreAndUserChoices
1390
        );
1391
    }
1392
1393
    /**
1394
     * Check if a answer is correct by its text.
1395
     *
1396
     * @param string $answerText
1397
     *
1398
     * @return bool
1399
     */
1400
    public static function isCorrect($answerText)
1401
    {
1402
        $answerInfo = self::getAnswerInfo($answerText, true);
1403
        $correctAnswerList = $answerInfo['words'];
1404
        $studentAnswer = $answerInfo['student_answer'];
1405
        $isCorrect = true;
1406
1407
        foreach ($correctAnswerList as $i => $correctAnswer) {
1408
            $value = self::isStudentAnswerGood($studentAnswer[$i], $correctAnswer);
1409
            $isCorrect = $isCorrect && $value;
1410
        }
1411
1412
        return $isCorrect;
1413
    }
1414
1415
    /**
1416
     * Clear the answer entered by student.
1417
     *
1418
     * @param string $answer
1419
     *
1420
     * @return string
1421
     */
1422
    public static function clearStudentAnswer($answer)
1423
    {
1424
        $answer = htmlentities(api_utf8_encode($answer), ENT_QUOTES);
1425
        $answer = str_replace('&#039;', '&#39;', $answer); // fix apostrophe
1426
        $answer = api_preg_replace('/\s\s+/', ' ', $answer); // replace excess white spaces
1427
        $answer = strtr($answer, array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES)));
1428
1429
        return trim($answer);
1430
    }
1431
1432
    /**
1433
     * Removes double spaces between words.
1434
     *
1435
     * @param string $text
1436
     *
1437
     * @return string
1438
     */
1439
    private static function trimOption($text)
1440
    {
1441
        $text = trim($text);
1442
        $text = preg_replace("/\s+/", ' ', $text);
1443
1444
        return $text;
1445
    }
1446
}
1447