Completed
Push — master ( 9b8b24...6e1754 )
by Julito
58:58
created

FillBlanks   F

Complexity

Total Complexity 113

Size/Duplication

Total Lines 1368
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1368
rs 0.6314
c 0
b 0
f 0
wmc 113

28 Methods

Rating   Name   Duplication   Size   Complexity  
A isCorrect() 0 13 3
A return_header() 0 9 1
A getFillTheBlankAnswerType() 0 8 4
B getRegexpProtected() 0 30 2
C getHtmlDisplayForAnswer() 0 49 9
A getAllowedSeparatorForSelect() 0 9 2
B getNbResultFillBlankAll() 0 16 5
A trimOption() 0 6 1
C getHtmlAnswer() 0 64 13
A getFillTheBlankMenuAnswers() 0 13 3
A getAnswerInStudentAttempt() 0 18 2
C isStudentAnswerGood() 0 40 8
A getAllowedSeparator() 0 10 1
C getFillTheBlankTabResult() 0 81 9
A getHtmlRightAnswer() 0 14 1
B getFillTheBlankHtml() 0 62 7
A clearStudentAnswer() 0 8 1
A getEndSeparator() 0 5 1
A getFillTheBlankMenuAnswerNum() 0 11 3
C getAnswerInfo() 0 124 11
A getHtmlWrongAnswer() 0 14 1
A getStartSeparator() 0 5 1
C processAnswersCreation() 0 112 7
D createAnswersForm() 0 332 9
A getFillTheBlankSeveralAnswers() 0 6 1
B escapeForRegexp() 0 28 2
A getDefaultSeparatorNumber() 0 13 4
A __construct() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like FillBlanks often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FillBlanks, and based on these observations, apply Extract Interface, too.

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
            array('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
                //no break
560
            case self::FILL_THE_BLANK_STANDARD:
561
            default:
562
                $attributes['id'] = 'choice_id_'.$currentQuestion.'_'.$inBlankNumber;
563
                $result = Display::input(
564
                    'text',
565
                    "choice[$questionId][]",
566
                    $correctItem,
567
                    $attributes
568
                );
569
                break;
570
        }
571
572
        return $result;
573
    }
574
575
    /**
576
     * Removes double spaces between words
577
     * @param string $text
578
     * @return string
579
     */
580
    private static function trimOption($text)
581
    {
582
        $text = trim($text);
583
        $text = preg_replace("/\s+/", " ", $text);
584
585
        return $text;
586
    }
587
588
    /**
589
     * Return an array with the different choices available
590
     * when the answers between bracket show as a menu
591
     * @param string $correctAnswer
592
     * @param bool $displayForStudent true if we want to shuffle the choices of the menu for students
593
     *
594
     * @return array
595
     */
596
    public static function getFillTheBlankMenuAnswers($correctAnswer, $displayForStudent)
597
    {
598
        $list = api_preg_split("/\|/", $correctAnswer);
599
        foreach ($list as &$item) {
600
            $item = self::trimOption($item);
601
            $item = api_html_entity_decode($item);
602
        }
603
        // The list is always in the same order, there's no option to allow or disable shuffle options.
604
        if ($displayForStudent) {
605
            shuffle_assoc($list);
606
        }
607
608
        return $list;
609
    }
610
611
    /**
612
     * Return the array index of the student answer
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
    /**
633
     * Return the possible answer if the answer between brackets is a multiple choice menu
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
     * @param string $studentAnswer [student_answer] of the info array of the answer field
651
     * @param string $correctAnswer [words] of the info array of the answer field
652
     * @param bool $fromDatabase
653
     * @return bool
654
     */
655
    public static function isStudentAnswerGood($studentAnswer, $correctAnswer, $fromDatabase = false)
656
    {
657
        $result = false;
658
        switch (self::getFillTheBlankAnswerType($correctAnswer)) {
659
            case self::FILL_THE_BLANK_MENU:
660
                $listMenu = self::getFillTheBlankMenuAnswers($correctAnswer, false);
661
                if ($studentAnswer != '' && isset($listMenu[0])) {
662
                    // First item is always the correct one.
663
                    $item = $listMenu[0];
664
                    if (!$fromDatabase) {
665
                        $item = sha1($item);
666
                    }
667
                    if ($item === $studentAnswer) {
668
                        $result = true;
669
                    }
670
                }
671
                break;
672
            case self::FILL_THE_BLANK_SEVERAL_ANSWER:
673
                // the answer must be one of the choice made
674
                $listSeveral = self::getFillTheBlankSeveralAnswers($correctAnswer);
675
                $listSeveral = array_map(
676
                    function ($item) {
677
                        return self::trimOption($item);
678
                    },
679
                    $listSeveral
680
                );
681
                $result = in_array($studentAnswer, $listSeveral);
682
                break;
683
            case self::FILL_THE_BLANK_STANDARD:
684
            default:
685
                $correctAnswer = api_html_entity_decode($correctAnswer);
686
                $studentAnswer = htmlspecialchars($studentAnswer);
687
                $result = $studentAnswer == self::trimOption($correctAnswer);
688
689
690
                break;
691
        }
692
        //var_dump($result);
693
694
        return $result;
695
    }
696
697
    /**
698
     * @param string $correctAnswer
699
     *
700
     * @return int
701
     */
702
    public static function getFillTheBlankAnswerType($correctAnswer)
703
    {
704
        if (api_strpos($correctAnswer, '|') && !api_strpos($correctAnswer, '||')) {
705
            return self::FILL_THE_BLANK_MENU;
706
        } elseif (api_strpos($correctAnswer, '||')) {
707
            return self::FILL_THE_BLANK_SEVERAL_ANSWER;
708
        } else {
709
            return self::FILL_THE_BLANK_STANDARD;
710
        }
711
    }
712
713
    /**
714
     * Return information about the answer
715
     * @param string $userAnswer the text of the answer of the question
716
     * @param bool   $isStudentAnswer true if it's a student answer false the empty question model
717
     *
718
     * @return array of information about the answer
719
     */
720
    public static function getAnswerInfo($userAnswer = '', $isStudentAnswer = false)
721
    {
722
        $listAnswerResults = [];
723
        $listAnswerResults['text'] = '';
724
        $listAnswerResults['words_count'] = 0;
725
        $listAnswerResults['words_with_bracket'] = [];
726
        $listAnswerResults['words'] = [];
727
        $listAnswerResults['weighting'] = [];
728
        $listAnswerResults['input_size'] = [];
729
        $listAnswerResults['switchable'] = '';
730
        $listAnswerResults['student_answer'] = [];
731
        $listAnswerResults['student_score'] = [];
732
        $listAnswerResults['blank_separator_number'] = 0;
733
        $listDoubleColon = [];
734
735
        api_preg_match("/(.*)::(.*)$/s", $userAnswer, $listResult);
736
737
        if (count($listResult) < 2) {
738
            $listDoubleColon[] = '';
739
            $listDoubleColon[] = '';
740
        } else {
741
            $listDoubleColon[] = $listResult[1];
742
            $listDoubleColon[] = $listResult[2];
743
        }
744
745
        $listAnswerResults['system_string'] = $listDoubleColon[1];
746
747
        // Make sure we only take the last bit to find special marks
748
        $listArobaseSplit = explode('@', $listDoubleColon[1]);
749
750
        if (count($listArobaseSplit) < 2) {
751
            $listArobaseSplit[1] = '';
752
        }
753
754
        // Take the complete string except after the last '::'
755
        $listDetails = explode(":", $listArobaseSplit[0]);
756
757
        // < number of item after the ::[score]:[size]:[separator_id]@ , here there are 3
758
        if (count($listDetails) < 3) {
759
            $listWeightings = explode(',', $listDetails[0]);
760
            $listSizeOfInput = [];
761
            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...
762
                $listSizeOfInput[] = 200;
763
            }
764
            $blankSeparatorNumber = 0; // 0 is [...]
765
        } else {
766
            $listWeightings = explode(',', $listDetails[0]);
767
            $listSizeOfInput = explode(',', $listDetails[1]);
768
            $blankSeparatorNumber = $listDetails[2];
769
        }
770
771
        $listAnswerResults['text'] = $listDoubleColon[0];
772
        $listAnswerResults['weighting'] = $listWeightings;
773
        $listAnswerResults['input_size'] = $listSizeOfInput;
774
        $listAnswerResults['switchable'] = $listArobaseSplit[1];
775
        $listAnswerResults['blank_separator_start'] = self::getStartSeparator($blankSeparatorNumber);
776
        $listAnswerResults['blank_separator_end'] = self::getEndSeparator($blankSeparatorNumber);
777
        $listAnswerResults['blank_separator_number'] = $blankSeparatorNumber;
778
779
        $blankCharStart = self::getStartSeparator($blankSeparatorNumber);
780
        $blankCharEnd = self::getEndSeparator($blankSeparatorNumber);
781
        $blankCharStartForRegexp = self::escapeForRegexp($blankCharStart);
782
        $blankCharEndForRegexp = self::escapeForRegexp($blankCharEnd);
783
784
        // Get all blanks words
785
        $listAnswerResults['words_count'] = api_preg_match_all(
786
            '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
787
            $listDoubleColon[0],
788
            $listWords
789
        );
790
791
        if ($listAnswerResults['words_count'] > 0) {
792
            $listAnswerResults['words_with_bracket'] = $listWords[0];
793
            // remove [ and ] in string
794
            array_walk(
795
                $listWords[0],
796
                function (&$value, $key, $tabBlankChar) {
797
                    $trimChars = '';
798
                    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...
799
                        $trimChars .= $tabBlankChar[$i];
800
                    }
801
                    $value = trim($value, $trimChars);
802
                },
803
                array($blankCharStart, $blankCharEnd)
804
            );
805
            $listAnswerResults['words'] = $listWords[0];
806
        }
807
808
        // Get all common words
809
        $commonWords = api_preg_replace(
810
            '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
811
            "::",
812
            $listDoubleColon[0]
813
        );
814
815
        // if student answer, the second [] is the student answer,
816
        // the third is if student scored or not
817
        $listBrackets = [];
818
        $listWords = [];
819
        if ($isStudentAnswer) {
820
            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...
821
                $listBrackets[] = $listAnswerResults['words_with_bracket'][$i];
822
                $listWords[] = $listAnswerResults['words'][$i];
823
                if ($i + 1 < count($listAnswerResults['words'])) {
824
                    // should always be
825
                    $i++;
826
                }
827
                $listAnswerResults['student_answer'][] = $listAnswerResults['words'][$i];
828
                if ($i + 1 < count($listAnswerResults['words'])) {
829
                    // should always be
830
                    $i++;
831
                }
832
                $listAnswerResults['student_score'][] = $listAnswerResults['words'][$i];
833
            }
834
            $listAnswerResults['words'] = $listWords;
835
            $listAnswerResults['words_with_bracket'] = $listBrackets;
836
837
            // if we are in student view, we've got 3 times :::::: for common words
838
            $commonWords = api_preg_replace("/::::::/", "::", $commonWords);
839
        }
840
841
        $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

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