Completed
Push — master ( 176486...dcf38a )
by Julito
52:32 queued 19:58
created

FillBlanks::isCorrect()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 1
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/* For licensing terms, see /license.txt */
3
4
/**
5
 *	Class FillBlanks
6
 *
7
 *	@author Eric Marguin
8
 * 	@author Julio Montoya multiple fill in blank option added
9
 *	@package chamilo.exercise
10
 **/
11
class FillBlanks extends Question
12
{
13
    public static $typePicture = 'fill_in_blanks.png';
14
    public static $explanationLangVar = 'FillBlanks';
15
16
    const FILL_THE_BLANK_STANDARD = 0;
17
    const FILL_THE_BLANK_MENU = 1;
18
    const FILL_THE_BLANK_SEVERAL_ANSWER = 2;
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
     * function which redefines Question::createAnswersForm
32
     * @param FormValidator $form
33
     */
34
    public function createAnswersForm($form)
35
    {
36
        $fillBlanksAllowedSeparator = self::getAllowedSeparator();
37
        $defaults = array();
38
39
        if (!empty($this->id)) {
40
            $objectAnswer = new Answer($this->id);
41
            $answer = $objectAnswer->selectAnswer(1);
42
            $listAnswersInfo = FillBlanks::getAnswerInfo($answer);
43
44
            if ($listAnswersInfo["switchable"]) {
45
                $defaults['multiple_answer'] = 1;
46
            } else {
47
                $defaults['multiple_answer'] = 0;
48
            }
49
            //take the complete string except after the last '::'
50
            $defaults['answer'] = $listAnswersInfo["text"];
51
            $defaults['select_separator'] = $listAnswersInfo["blankseparatornumber"];
52
            $blanksepartornumber = $listAnswersInfo["blankseparatornumber"];
53
        } else {
54
            $defaults['answer'] = get_lang('DefaultTextInBlanks');
55
            $defaults['select_separator'] = 0;
56
            $blanksepartornumber = 0;
57
        }
58
59
        $blankSeparatorStart = self::getStartSeparator($blanksepartornumber);
60
        $blankSeparatorEnd = self::getEndSeparator($blanksepartornumber);
61
62
        $setValues = null;
63
64
        if (isset($a_weightings) && count($a_weightings) > 0) {
0 ignored issues
show
Bug introduced by
The variable $a_weightings seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
65
            foreach ($a_weightings as $i => $weighting) {
66
                $setValues .= 'document.getElementById("weighting['.$i.']").value = "'.$weighting.'";';
67
            }
68
        }
69
        // javascript
70
        echo '<script>
71
72
            var blankSeparatortStart = "'.$blankSeparatorStart.'";
73
            var blankSeparatortEnd = "'.$blankSeparatorEnd.'";
74
            var blankSeparatortStartRegexp = getBlankSeparatorRegexp(blankSeparatortStart);
75
            var blankSeparatortEndRegexp = getBlankSeparatorRegexp(blankSeparatortEnd);
76
77
            CKEDITOR.on("instanceCreated", function(e) {
78
                if (e.editor.name === "answer") {
79
                    e.editor.on("change", updateBlanks);
80
                }
81
            });
82
83
            var firstTime = true;
84
85
            function updateBlanks()
86
            {
87
                if (firstTime) {
88
                    var field = document.getElementById("answer");
89
                    var answer = field.value;
90
                } else {
91
                    var answer = CKEDITOR.instances["answer"].getData();
92
                }
93
94
                // disable the save button, if not blanks have been created
95
                $("button").attr("disabled", "disabled");
96
                $("#defineoneblank").show();
97
98
                var blanksRegexp = "/"+blankSeparatortStartRegexp+"[^"+blankSeparatortStartRegexp+"]*"+blankSeparatortEndRegexp+"/g";
99
100
                var blanks = answer.match(eval(blanksRegexp));
101
                var fields = "<div class=\"form-group \">";
102
                fields += "<label class=\"col-sm-2 control-label\">'.get_lang('Weighting').'</label>";
103
                fields += "<div class=\"col-sm-8\">";
104
                fields += "<table>";
105
                fields += "<tr><th style=\"padding:0 20px\">'.get_lang("WordTofind").'</th><th style=\"padding:0 20px\">'.get_lang("QuestionWeighting").'</th><th style=\"padding:0 20px\">'.get_lang("BlankInputSize").'</th></tr>";
106
107
                if (blanks != null) {
108
                    for (var i=0 ; i < blanks.length ; i++){
109
                        // remove forbidden characters that causes bugs
110
                        blanks[i] = removeForbiddenChars(blanks[i]);
111
                        // trim blanks between brackets
112
                        blanks[i] = trimBlanksBetweenSeparator(blanks[i], blankSeparatortStart, blankSeparatortEnd);
113
114
                        // if the word is empty []
115
                        if (blanks[i] == blankSeparatortStartRegexp+blankSeparatortEndRegexp) {
116
                            break;
117
                        }
118
                        // get input size
119
                        var lainputsize = 200;
120
                        var lainputsizetrue = 200;
121
                        if ($("#samplesize\\\["+i+"\\\]").width()) {
122
                        // this is a weird patch to avoid to reduce the size of input blank when you are writing in the ckeditor.
123
                            lainputsize = $("#samplesize\\\["+i+"\\\]").width();
124
                            lainputsizetrue = $("#samplesize\\\["+i+"\\\]").width() + 9;
125
                        }
126
127
                        if (document.getElementById("weighting["+i+"]")) {
128
                            var value = document.getElementById("weighting["+i+"]").value;
129
                        } else {
130
                            var value = "10";
131
                        }
132
                        fields += "<tr>";
133
                        fields += "<td>"+blanks[i]+"</td>";
134
                        fields += "<td><input style=\"width:35px\" value=\""+value+"\" type=\"text\" id=\"weighting["+i+"]\" name=\"weighting["+i+"]\" /></td>";
135
                        fields += "<td>";
136
                        fields += "<input type=\"button\" value=\"-\" onclick=\"changeInputSize(-1, "+i+")\">";
137
                        fields += "<input type=\"button\" value=\"+\" onclick=\"changeInputSize(1, "+i+")\">";
138
                        fields += "<input value=\""+blanks[i].substr(1, blanks[i].length - 2)+"\" style=\"width:"+lainputsizetrue+"px\" disabled=disabled id=\"samplesize["+i+"]\"/>";
139
                        fields += "<input type=\"hidden\" id=\"sizeofinput["+i+"]\" name=\"sizeofinput["+i+"]\" value=\""+lainputsize+"\" \"/>";
140
                        fields += "</td>";
141
                        fields += "</tr>";
142
                        // enable the save button
143
                        $("button").removeAttr("disabled");
144
                        $("#defineoneblank").hide();
145
                    }
146
                }
147
                document.getElementById("blanks_weighting").innerHTML = fields + "</table></div></div>";
148
                if (firstTime) {
149
                    firstTime = false;
150
                ';
151
152
        if (isset($listAnswersInfo) && count($listAnswersInfo["tabweighting"]) > 0) {
153
            foreach ($listAnswersInfo["tabweighting"] as $i => $weighting) {
154
                echo 'document.getElementById("weighting['.$i.']").value = "'.$weighting.'";';
155
            }
156
            foreach ($listAnswersInfo["tabinputsize"] as $i => $sizeOfInput) {
157
                echo 'document.getElementById("sizeofinput['.$i.']").value = "'.$sizeOfInput.'";';
158
                echo '$("#samplesize\\\['.$i.'\\\]").width('.$sizeOfInput.');';
159
            }
160
        }
161
162
        echo '}
163
            }
164
            window.onload = updateBlanks;
165
166
            function getInputSize() {
167
                var outTabSize = new Array();
168
                $("input").each(function() {
169
                    if ($(this).attr("id") && $(this).attr("id").match(/samplesize/)) {
170
                        var tabidnum = $(this).attr("id").match(/\d+/);
171
                        var idnum = tabidnum[0];
172
                        var thewidth = $(this).next().attr("value");
173
                        tabInputSize[idnum] = thewidth;
174
                    }
175
                });
176
            }
177
178
            function changeInputSize(inCoef, inIdNum)
179
            {
180
                var currentWidth = $("#samplesize\\\["+inIdNum+"\\\]").width();
181
                var newWidth = currentWidth + inCoef * 20;
182
                newWidth = Math.max(20, newWidth);
183
                newWidth = Math.min(newWidth, 600);
184
                $("#samplesize\\\["+inIdNum+"\\\]").width(newWidth);
185
                $("#sizeofinput\\\["+inIdNum+"\\\]").attr("value", newWidth);
186
            }
187
188
            function removeForbiddenChars(inTxt) {
189
                outTxt = inTxt;
190
191
                outTxt = outTxt.replace(/&quot;/g, ""); // remove the   char
192
                outTxt = outTxt.replace(/\x22/g, ""); // remove the   char
193
                outTxt = outTxt.replace(/"/g, ""); // remove the   char
194
                outTxt = outTxt.replace(/\\\\/g, ""); // remove the \ char
195
                outTxt = outTxt.replace(/&nbsp;/g, " ");
196
                outTxt = outTxt.replace(/^ +/, "");
197
                outTxt = outTxt.replace(/ +$/, "");
198
                return outTxt;
199
            }
200
201
            function changeBlankSeparator()
202
            {
203
                var separatorNumber = $("#select_separator").val();
204
                var tabSeparator = getSeparatorFromNumber(separatorNumber);
205
                blankSeparatortStart = tabSeparator[0];
206
                blankSeparatortEnd = tabSeparator[1];
207
                blankSeparatortStartRegexp = getBlankSeparatorRegexp(blankSeparatortStart);
208
                blankSeparatortEndRegexp = getBlankSeparatorRegexp(blankSeparatortEnd);
209
                updateBlanks();
210
            }
211
212
            // this function is the same than the PHP one
213
            // if modify it modify the php one escapeForRegexp
214
            function getBlankSeparatorRegexp(inTxt)
215
            {
216
                var tabSpecialChar = new Array(".", "+", "*", "?", "[", "^", "]", "$", "(", ")",
217
                    "{", "}", "=", "!", "<", ">", "|", ":", "-", ")");
218
                for (var i=0; i < tabSpecialChar.length; i++) {
219
                    if (inTxt == tabSpecialChar[i]) {
220
                        return "\\\"+inTxt;
221
                    }
222
                }
223
                return inTxt;
224
            }
225
226
            // this function is the same than the PHP one
227
            // if modify it modify the php one getAllowedSeparator
228
            function getSeparatorFromNumber(innumber)
229
            {
230
                tabSeparator = new Array();
231
                tabSeparator[0] = new Array("[", "]");
232
                tabSeparator[1] = new Array("{", "}");
233
                tabSeparator[2] = new Array("(", ")");
234
                tabSeparator[3] = new Array("*", "*");
235
                tabSeparator[4] = new Array("#", "#");
236
                tabSeparator[5] = new Array("%", "%");
237
                tabSeparator[6] = new Array("$", "$");
238
                return tabSeparator[innumber];
239
            }
240
241
            function trimBlanksBetweenSeparator(inTxt, inSeparatorStart, inSeparatorEnd)
242
            {
243
                // blankSeparatortStartRegexp
244
                // blankSeparatortEndRegexp
245
                var result = inTxt
246
                result = result.replace(inSeparatorStart, "");
247
                result = result.replace(inSeparatorEnd, "");
248
                result = result.trim();
249
                return inSeparatorStart+result+inSeparatorEnd;
250
            }
251
        </script>';
252
253
        // answer
254
        $form->addElement('label', null, '<br /><br />'.get_lang('TypeTextBelow').', '.get_lang('And').' '.get_lang('UseTagForBlank'));
255
        $form->addElement(
256
            'html_editor',
257
            'answer',
258
            Display::return_icon('fill_field.png'),
259
            ['id' => 'answer', 'onkeyup' => "javascript: updateBlanks(this);"],
260
            array('ToolbarSet' => 'TestQuestionDescription')
261
        );
262
        $form->addRule('answer',get_lang('GiveText'),'required');
263
264
        //added multiple answers
265
        $form->addElement('checkbox','multiple_answer','', get_lang('FillInBlankSwitchable'));
266
        $form->addElement(
267
            'select',
268
            'select_separator',
269
            get_lang("SelectFillTheBlankSeparator"),
270
            self::getAllowedSeparatorForSelect(),
271
            ' id="select_separator"   style="width:150px" onchange="changeBlankSeparator()" '
272
        );
273
        $form->addElement(
274
            'label',
275
            null,
276
            '<input type="button" onclick="updateBlanks()" value="'.get_lang('RefreshBlanks').'" class="btn btn-default" />'
277
        );
278
        $form->addElement('html','<div id="blanks_weighting"></div>');
279
280
        global $text;
281
        // setting the save button here and not in the question class.php
282
        $form->addElement('html','<div id="defineoneblank" style="color:#D04A66; margin-left:160px">'.get_lang('DefineBlanks').'</div>');
283
        $form->addButtonSave($text, 'submitQuestion');
284
285
        if (!empty($this->id)) {
286
            $form->setDefaults($defaults);
287
        } else {
288
            if ($this->isContent == 1) {
289
                $form->setDefaults($defaults);
290
            }
291
        }
292
    }
293
294
    /**
295
     * Function which creates the form to create/edit the answers of the question
296
     * @param FormValidator $form
297
     */
298
    public function processAnswersCreation($form)
299
    {
300
        $answer = $form->getSubmitValue('answer');
301
        // Due the ckeditor transform the elements to their HTML value
302
303
        //$answer = api_html_entity_decode($answer, ENT_QUOTES, $charset);
304
        //$answer = htmlentities(api_utf8_encode($answer));
305
306
        // remove the :: eventually written by the user
307
        $answer = str_replace('::', '', $answer);
308
309
        // remove starting and ending space and &nbsp;
310
        $answer = api_preg_replace("/\xc2\xa0/", " ", $answer);
311
312
        // start and end separator
313
        $blankStartSeparator = self::getStartSeparator($form->getSubmitValue('select_separator'));
314
        $blankEndSeparator = self::getEndSeparator($form->getSubmitValue('select_separator'));
315
        $blankStartSeparatorRegexp = self::escapeForRegexp($blankStartSeparator);
316
        $blankEndSeparatorRegexp = self::escapeForRegexp($blankEndSeparator);
317
318
        // remove spaces at the beginning and the end of text in square brackets
319
        $answer = preg_replace_callback(
320
            "/".$blankStartSeparatorRegexp."[^]]+".$blankEndSeparatorRegexp."/",
321
            function ($matches) use ($blankStartSeparator, $blankEndSeparator) {
322
                $matchingResult = $matches[0];
323
                $matchingResult = trim($matchingResult, $blankStartSeparator);
324
                $matchingResult = trim($matchingResult, $blankEndSeparator);
325
                $matchingResult = trim($matchingResult);
326
                // remove forbidden chars
327
                $matchingResult = str_replace("/\\/", "", $matchingResult);
328
                $matchingResult = str_replace('/"/', "", $matchingResult);
329
330
                return $blankStartSeparator.$matchingResult.$blankEndSeparator;
331
            },
332
            $answer
333
        );
334
335
        // get the blanks weightings
336
        $nb = preg_match_all(
337
            '/'.$blankStartSeparatorRegexp.'[^'.$blankStartSeparatorRegexp.']*'.$blankEndSeparatorRegexp.'/',
338
            $answer,
339
            $blanks
340
        );
341
342
        if (isset($_GET['editQuestion'])) {
343
            $this->weighting = 0;
344
        }
345
346
        /* if we have some [tobefound] in the text
347
        build the string to save the following in the answers table
348
        <p>I use a [computer] and a [pen].</p>
349
        becomes
350
        <p>I use a [computer] and a [pen].</p>::100,50:100,50@1
351
            ++++++++-------**
352
            --- -- --- -- -
353
            A B  (C) (D)(E)
354
        +++++++ : required, weighting of each words
355
        ------- : optional, input width to display, 200 if not present
356
        ** : equal @1 if "Allow answers order switches" has been checked, @ otherwise
357
        A : weighting for the word [computer]
358
        B : weighting for the word [pen]
359
        C : input width for the word [computer]
360
        D : input width for the word [pen]
361
        E : equal @1 if "Allow answers order switches" has been checked, @ otherwise
362
        */
363
        if ($nb > 0) {
364
            $answer .= '::';
365
            // weighting
366
            for ($i=0; $i < $nb; ++$i) {
367
                // enter the weighting of word $i
368
                $answer .= $form->getSubmitValue('weighting['.$i.']');
369
                // not the last word, add ","
370
                if ($i != $nb - 1) {
371
                    $answer .= ",";
372
                }
373
                // calculate the global weighting for the question
374
                $this -> weighting += $form->getSubmitValue('weighting['.$i.']');
375
            }
376
377
            // input width
378
            $answer .= ":";
379
            for ($i=0; $i < $nb; ++$i) {
380
                // enter the width of input for word $i
381
                $answer .= $form->getSubmitValue('sizeofinput['.$i.']');
382
                // not the last word, add ","
383
                if ($i != $nb - 1) {
384
                    $answer .= ",";
385
                }
386
            }
387
        }
388
389
        // write the blank separator code number
390
        // see function getAllowedSeparator
391
        /*
392
            0 [...]
393
            1 {...}
394
            2 (...)
395
            3 *...*
396
            4 #...#
397
            5 %...%
398
            6 $...$
399
         */
400
        $answer .= ":".$form->getSubmitValue('select_separator');
401
402
        // Allow answers order switches
403
        $is_multiple = $form -> getSubmitValue('multiple_answer');
404
        $answer.= '@'.$is_multiple;
405
406
        $this->save();
407
        $objAnswer = new Answer($this->id);
408
        $objAnswer->createAnswer($answer, 0, '', 0, 1);
409
        $objAnswer->save();
410
    }
411
412
    /**
413
     * @param null $feedback_type
414
     * @param null $counter
415
     * @param null $score
416
     * @return string
417
     */
418 View Code Duplication
    public function return_header($feedback_type = null, $counter = null, $score = null)
419
    {
420
        $header = parent::return_header($feedback_type, $counter, $score);
421
        $header .= '<table class="'.$this->question_table_class .'">
422
            <tr>
423
                <th>'.get_lang("Answer").'</th>
424
            </tr>';
425
426
        return $header;
427
    }
428
429
    /**
430
     * @param string $separatorStartRegexp
431
     * @param string $separatorEndRegexp
432
     * @param string $correctItemRegexp
433
     * @param integer $questionId
434
     * @param $correctItem
435
     * @param $attributes
436
     * @param string $answer
437
     * @param $listAnswersInfo
438
     * @param boolean $displayForStudent
439
     * @param integer $inBlankNumber
440
     * @return string
441
     */
442
    public static function getFillTheBlankHtml(
443
        $separatorStartRegexp,
444
        $separatorEndRegexp,
445
        $correctItemRegexp,
446
        $questionId,
447
        $correctItem,
448
        $attributes,
449
        $answer,
450
        $listAnswersInfo,
451
        $displayForStudent,
452
        $inBlankNumber
453
    ) {
454
        $result = "";
455
        $inTabTeacherSolution = $listAnswersInfo['tabwords'];
456
        $inTeacherSolution = $inTabTeacherSolution[$inBlankNumber];
457
        switch (self::getFillTheBlankAnswerType($inTeacherSolution)) {
458
            case self::FILL_THE_BLANK_MENU:
459
                $selected = '';
460
                // the blank menu
461
                $optionMenu = '';
462
                // display a menu from answer separated with |
463
                // if display for student, shuffle the correct answer menu
464
                $listMenu = self::getFillTheBlankMenuAnswers($inTeacherSolution, $displayForStudent);
465
                $result .= '<select name="choice['.$questionId.'][]">';
466
                for ($k=0; $k < count($listMenu); $k++) {
467
                    $selected = "";
468
                    if ($correctItem == $listMenu[$k]) {
469
                        $selected = " selected=selected ";
470
                    }
471
                    // if in teacher view, display the first item by default, which is the right answer
472
                    if ($k==0 && !$displayForStudent) {
473
                        $selected = " selected=selected ";
474
                    }
475
                    $optionMenu .= '<option '.$selected.' value="'.$listMenu[$k].'">'.$listMenu[$k].'</option>';
476
                }
477
                if ($selected == "") {
478
                    // no good answer have been found...
479
                    $selected = " selected=selected ";
480
                }
481
                $result .= "<option $selected value=''>--</option>";
482
                $result .= $optionMenu;
483
                $result .= '</select>';
484
                break;
485
            case self::FILL_THE_BLANK_SEVERAL_ANSWER:
486
                //no break
487
            case self::FILL_THE_BLANK_STANDARD:
488
            default:
489
                $result = Display::input('text', "choice[$questionId][]", $correctItem, $attributes);
490
                break;
491
        }
492
493
        return $result;
494
    }
495
496
    /**
497
     * Return an array with the different choices available
498
     * when the answers between bracket show as a menu
499
     * @param string $correctAnswer
500
     * @param bool $displayForStudent true if we want to shuffle the choices of the menu for students
501
     *
502
     * @return array
503
     */
504
    public static function getFillTheBlankMenuAnswers($correctAnswer, $displayForStudent)
505
    {
506
        // if $inDisplayForStudent, then shuffle the result array
507
        $listChoises = api_preg_split("/\|/", $correctAnswer);
508
        if ($displayForStudent) {
509
            shuffle($listChoises);
510
        }
511
512
        return $listChoises;
513
    }
514
515
    /**
516
     * Return the array index of the student answer
517
     * @param string $correctAnswer the menu Choice1|Choice2|Choice3
518
     * @param string $studentAnswer the student answer must be Choice1 or Choice2 or Choice3
519
     *
520
     * @return int  in the example 0 1 or 2 depending of the choice of the student
521
     */
522
    public static function getFillTheBlankMenuAnswerNum($correctAnswer, $studentAnswer)
523
    {
524
        $listChoices = self::getFillTheBlankMenuAnswers($correctAnswer, false);
525
        foreach ($listChoices as $num => $value) {
526
            if ($value == $studentAnswer) {
527
                return $num;
528
            }
529
        }
530
531
        // should not happened, because student choose the answer in a menu of possible answers
532
        return -1;
533
    }
534
535
536
    /**
537
     * Return the possible answer if the answer between brackets is a multiple choice menu
538
     * @param string $correctAnswer
539
     *
540
     * @return array
541
     */
542
    public static function getFillTheBlankSeveralAnswers($correctAnswer)
543
    {
544
        // is answer||Answer||response||Response , mean answer or Answer ...
545
        $listSeveral = api_preg_split("/\|\|/", $correctAnswer);
546
547
        return $listSeveral;
548
    }
549
550
    /**
551
     * Return true if student answer is right according to the correctAnswer
552
     * it is not as simple as equality, because of the type of Fill The Blank question
553
     * eg : studentAnswer = 'Un' and correctAnswer = 'Un||1||un'
554
     * @param string $studentAnswer [studentanswer] of the info array of the answer field
555
     * @param string $correctAnswer [tabwords] of the info array of the answer field
556
     *
557
     * @return bool
558
     */
559
    public static function isGoodStudentAnswer($studentAnswer, $correctAnswer)
560
    {
561
        switch (self::getFillTheBlankAnswerType($correctAnswer)) {
562
            case self::FILL_THE_BLANK_MENU:
563
                $listMenu = self::getFillTheBlankMenuAnswers($correctAnswer, false);
564
                $result = $listMenu[0] == $studentAnswer;
565
                break;
566
            case self::FILL_THE_BLANK_SEVERAL_ANSWER:
567
                // the answer must be one of the choice made
568
                $listSeveral = self::getFillTheBlankSeveralAnswers($correctAnswer);
569
                $result = in_array($studentAnswer, $listSeveral);
570
                break;
571
            case self::FILL_THE_BLANK_STANDARD:
572
            default:
573
                $result = $studentAnswer == $correctAnswer;
574
                break;
575
        }
576
577
        return $result;
578
    }
579
580
    /**
581
     * @param string $correctAnswer
582
     *
583
     * @return int
584
     */
585
    public static function getFillTheBlankAnswerType($correctAnswer)
586
    {
587
        if (api_strpos($correctAnswer, "|") && !api_strpos($correctAnswer, "||")) {
588
            return self::FILL_THE_BLANK_MENU;
589
        } elseif (api_strpos($correctAnswer, "||")) {
590
            return self::FILL_THE_BLANK_SEVERAL_ANSWER;
591
        } else {
592
            return self::FILL_THE_BLANK_STANDARD;
593
        }
594
    }
595
596
    /**
597
     * Return information about the answer
598
     * @param string $userAnswer the text of the answer of the question
599
     * @param bool   $isStudentAnswer true if it's a student answer false the empty question model
600
     *
601
     * @return array of information about the answer
602
     */
603
    public static function getAnswerInfo($userAnswer = "", $isStudentAnswer = false)
604
    {
605
        $listAnswerResults = array();
606
        $listAnswerResults['text'] = "";
607
        $listAnswerResults['wordsCount'] = 0;
608
        $listAnswerResults['tabwordsbracket'] = array();
609
        $listAnswerResults['tabwords'] = array();
610
        $listAnswerResults['tabweighting'] = array();
611
        $listAnswerResults['tabinputsize'] = array();
612
        $listAnswerResults['switchable'] = "";
613
        $listAnswerResults['studentanswer'] = array();
614
        $listAnswerResults['studentscore'] = array();
615
        $listAnswerResults['blankseparatornumber'] = 0;
616
        $listDoubleColon = array();
617
618
        api_preg_match("/(.*)::(.*)$/s", $userAnswer, $listResult);
619
620
        if (count($listResult) < 2) {
621
            $listDoubleColon[] = '';
622
            $listDoubleColon[] = '';
623
        } else {
624
            $listDoubleColon[] = $listResult[1];
625
            $listDoubleColon[] = $listResult[2];
626
        }
627
628
        $listAnswerResults['systemstring'] = $listDoubleColon[1];
629
630
        // make sure we only take the last bit to find special marks
631
        $listArobaseSplit = explode('@', $listDoubleColon[1]);
632
633
        if (count($listArobaseSplit) < 2) {
634
            $listArobaseSplit[1] = "";
635
        }
636
637
        // take the complete string except after the last '::'
638
        $listDetails = explode(":", $listArobaseSplit[0]);
639
640
        // < number of item after the ::[score]:[size]:[separator_id]@ , here there are 3
641
        if (count($listDetails) < 3) {
642
            $listWeightings = explode(',', $listDetails[0]);
643
            $listSizeOfInput = array();
644
            for ($i=0; $i < count($listWeightings); $i++) {
645
                $listSizeOfInput[] = 200;
646
            }
647
            $blankSeparatorNumber = 0;    // 0 is [...]
648
        } else {
649
            $listWeightings = explode(',', $listDetails[0]);
650
            $listSizeOfInput = explode(',', $listDetails[1]);
651
            $blankSeparatorNumber = $listDetails[2];
652
        }
653
654
        $listAnswerResults['text'] = $listDoubleColon[0];
655
        $listAnswerResults['tabweighting'] = $listWeightings;
656
        $listAnswerResults['tabinputsize'] = $listSizeOfInput;
657
        $listAnswerResults['switchable'] = $listArobaseSplit[1];
658
        $listAnswerResults['blankseparatorstart'] = self::getStartSeparator($blankSeparatorNumber);
659
        $listAnswerResults['blankseparatorend'] = self::getEndSeparator($blankSeparatorNumber);
660
        $listAnswerResults['blankseparatornumber'] = $blankSeparatorNumber;
661
662
        $blankCharStart = self::getStartSeparator($blankSeparatorNumber);
663
        $blankCharEnd = self::getEndSeparator($blankSeparatorNumber);
664
        $blankCharStartForRegexp = self::escapeForRegexp($blankCharStart);
665
        $blankCharEndForRegexp = self::escapeForRegexp($blankCharEnd);
666
667
        // get all blanks words
668
        $listAnswerResults['wordsCount'] = api_preg_match_all(
669
            '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
670
            $listDoubleColon[0],
671
            $listWords
672
        );
673
674
        if ($listAnswerResults['wordsCount'] > 0) {
675
            $listAnswerResults['tabwordsbracket'] = $listWords[0];
676
            // remove [ and ] in string
677
            array_walk(
678
                $listWords[0],
679
                function (&$value, $key, $tabBlankChar) {
680
                    $trimChars = "";
681
                    for ($i=0; $i < count($tabBlankChar); $i++) {
682
                        $trimChars .= $tabBlankChar[$i];
683
                    }
684
                    $value = trim($value, $trimChars);
685
                },
686
                array($blankCharStart, $blankCharEnd)
687
            );
688
            $listAnswerResults['tabwords'] = $listWords[0];
689
        }
690
691
        // get all common words
692
        $commonWords = api_preg_replace(
693
            '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
694
            "::",
695
            $listDoubleColon[0]
696
        );
697
698
        // if student answer, the second [] is the student answer,
699
        // the third is if student scored or not
700
        $listBrackets = array();
701
        $listWords =  array();
702
703
        if ($isStudentAnswer) {
704
            for ($i=0; $i < count($listAnswerResults['tabwords']); $i++) {
705
                $listBrackets[] = $listAnswerResults['tabwordsbracket'][$i];
706
                $listWords[] = $listAnswerResults['tabwords'][$i];
707
                if ($i+1 < count($listAnswerResults['tabwords'])) {
708
                    // should always be
709
                    $i++;
710
                }
711
                $listAnswerResults['studentanswer'][] = $listAnswerResults['tabwords'][$i];
712
                if ($i+1 < count($listAnswerResults['tabwords'])) {
713
                    // should always be
714
                    $i++;
715
                }
716
                $listAnswerResults['studentscore'][] = $listAnswerResults['tabwords'][$i];
717
            }
718
            $listAnswerResults['tabwords'] = $listWords;
719
            $listAnswerResults['tabwordsbracket'] = $listBrackets;
720
721
            // if we are in student view, we've got 3 times :::::: for common words
722
            $commonWords = api_preg_replace("/::::::/", "::", $commonWords);
723
        }
724
725
        $listAnswerResults['commonwords'] = explode("::", $commonWords);
726
727
        return $listAnswerResults;
728
    }
729
730
    /**
731
    * Return an array of student state answers for fill the blank questions
732
    * for each students that answered the question
733
    * -2  : didn't answer
734
    * -1  : student answer is wrong
735
    *  0  : student answer is correct
736
    * >0  : for fill the blank question with choice menu, is the index of the student answer (right answer indice is 0)
737
    *
738
    * @param integer $testId
739
    * @param integer $questionId
740
    * @param $studentsIdList
741
    * @param string $startDate
742
    * @param string $endDate
743
    * @param bool $useLastAnswerredAttempt
744
    * @return array
745
    * (
746
    *     [student_id] => Array
747
    *         (
748
    *             [first fill the blank for question] => -1
749
    *             [second fill the blank for question] => 2
750
    *             [third fill the blank for question] => -1
751
    *         )
752
    * )
753
    */
754
    public static function getFillTheBlankTabResult(
755
        $testId,
756
        $questionId,
757
        $studentsIdList,
758
        $startDate,
759
        $endDate,
760
        $useLastAnswerredAttempt = true
761
    ) {
762
        $tblTrackEAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
763
        $tblTrackEExercise = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
764
        $courseId = api_get_course_int_id();
765
        // request to have all the answers of student for this question
766
        // student may have doing it several time
767
        // student may have not answered the bracket id, in this case, is result of the answer is empty
768
        // we got the less recent attempt first
769
        $sql = 'SELECT * FROM '.$tblTrackEAttempt.' tea
770
               LEFT JOIN '.$tblTrackEExercise.' tee
771
               ON tee.exe_id = tea.exe_id
772
               AND tea.c_id = '.$courseId.'
773
               AND exe_exo_id = '.$testId.'
774
    
775
               WHERE 
776
                tee.c_id = '.$courseId.' AND 
777
                question_id = '.$questionId.' AND 
778
                tea.user_id IN ('.implode(',', $studentsIdList).')  AND 
779
                tea.tms >= "'.$startDate.'" AND 
780
                tea.tms <= "'.$endDate.'"
781
               ORDER BY user_id, tea.exe_id;
782
        ';
783
784
        $res = Database::query($sql);
785
        $tabUserResult = array();
786
        $bracketNumber = 0;
787
        // foreach attempts for all students starting with his older attempt
788
        while ($data = Database::fetch_array($res)) {
789
            $tabAnswer = FillBlanks::getAnswerInfo($data['answer'], true);
790
791
            // for each bracket to find in this question
792
            foreach ($tabAnswer['studentanswer'] as $bracketNumber => $studentAnswer) {
793
                if ($tabAnswer['studentanswer'][$bracketNumber] != '') {
794
                    // student has answered this bracket, cool
795
                    switch (FillBlanks::getFillTheBlankAnswerType($tabAnswer['tabwords'][$bracketNumber])) {
796
                        case self::FILL_THE_BLANK_MENU:
797
                            // get the indice of the choosen answer in the menu
798
                            // we know that the right answer is the first entry of the menu, ie 0
799
                            // (remember, menu entries are shuffled when taking the test)
800
                            $tabUserResult[$data['user_id']][$bracketNumber] = FillBlanks::getFillTheBlankMenuAnswerNum(
801
                                $tabAnswer['tabwords'][$bracketNumber],
802
                                $tabAnswer['studentanswer'][$bracketNumber]
803
                            );
804
                            break;
805
                        default:
806
                            if (FillBlanks::isGoodStudentAnswer(
807
                                $tabAnswer['studentanswer'][$bracketNumber],
808
                                $tabAnswer['tabwords'][$bracketNumber]
809
                            )
810
                            ) {
811
                                $tabUserResult[$data['user_id']][$bracketNumber] = 0;   //  right answer
812
                            } else {
813
                                $tabUserResult[$data['user_id']][$bracketNumber] = -1;  // wrong answer
814
                            }
815
                    }
816
                } else {
817
                    // student didn't answer this bracket
818
                    if ($useLastAnswerredAttempt) {
819
                        // if we take into account the last answered attempt
820
                        if (!isset($tabUserResult[$data['user_id']][$bracketNumber])) {
821
                            $tabUserResult[$data['user_id']][$bracketNumber] = -2;      // not answered
822
                        }
823
                    } else {
824
                        // we take the last attempt, even if the student answer the question before
825
                        $tabUserResult[$data['user_id']][$bracketNumber] = -2;      // not answered
826
                    }
827
                }
828
            }
829
        }
830
831
        return $tabUserResult;
832
    }
833
834
    /**
835
     * Return the number of student that give at leat an answer in the fill the blank test
836
     * @param $resultList
837
     * @return int
838
     */
839
    public static function getNbResultFillBlankAll($resultList)
840
    {
841
        $outRes = 0;
842
        // for each student in group
843
        foreach ($resultList as $userId => $tabValue) {
844
            $found = false;
845
            // for each bracket, if student has at least one answer ( choice > -2) then he pass the question
846
            foreach ($tabValue as $i => $choice) {
847
                if ($choice > -2 && !$found) {
848
                    $outRes++;
849
                    $found = true;
850
                }
851
            }
852
        }
853
854
        return $outRes;
855
    }
856
857
    /**
858
     * Replace the occurrence of blank word with [correct answer][student answer][answer is correct]
859
     * @param array $listWithStudentAnswer
860
     *
861
     * @return string
862
     */
863
    public static function getAnswerInStudentAttempt($listWithStudentAnswer)
864
    {
865
        $separatorStart = $listWithStudentAnswer['blankseparatorstart'];
866
        $separatorEnd = $listWithStudentAnswer['blankseparatorend'];
867
        // lets rebuild the sentence with [correct answer][student answer][answer is correct]
868
        $result = "";
869
        for ($i=0; $i < count($listWithStudentAnswer['commonwords']) - 1; $i++) {
870
            $result .= $listWithStudentAnswer['commonwords'][$i];
871
            $result .= $listWithStudentAnswer['tabwordsbracket'][$i];
872
            $result .= $separatorStart.$listWithStudentAnswer['studentanswer'][$i].$separatorEnd;
873
            $result .= $separatorStart.$listWithStudentAnswer['studentscore'][$i].$separatorEnd;
874
        }
875
        $result .= $listWithStudentAnswer['commonwords'][$i];
876
        $result .= "::";
877
        // add the system string
878
        $result .= $listWithStudentAnswer['systemstring'];
879
880
        return $result;
881
    }
882
883
    /**
884
     * This function is the same than the js one above getBlankSeparatorRegexp
885
     * @param string $inChar
886
     *
887
     * @return string
888
     */
889
    public static function escapeForRegexp($inChar)
890
    {
891
        $listChars = [
892
            ".",
893
            "+",
894
            "*",
895
            "?",
896
            "[",
897
            "^",
898
            "]",
899
            "$",
900
            "(",
901
            ")",
902
            "{",
903
            "}",
904
            "=",
905
            "!",
906
            ">",
907
            "|",
908
            ":",
909
            "-",
910
            ")",
911
        ];
912
913
        if (in_array($inChar, $listChars)) {
914
            return "\\".$inChar;
915
        } else {
916
            return $inChar;
917
        }
918
    }
919
920
    /**
921
     * return $text protected for use in regexp
922
     * @param string $text
923
     *
924
     * @return string
925
     */
926
    public static function getRegexpProtected($text)
927
    {
928
        $listRegexpCharacters = [
929
            "/",
930
            ".",
931
            "+",
932
            "*",
933
            "?",
934
            "[",
935
            "^",
936
            "]",
937
            "$",
938
            "(",
939
            ")",
940
            "{",
941
            "}",
942
            "=",
943
            "!",
944
            ">",
945
            "|",
946
            ":",
947
            "-",
948
            ")",
949
        ];
950
        $result = $text;
951
        for ($i=0; $i < count($listRegexpCharacters); $i++) {
952
            $result = str_replace($listRegexpCharacters[$i], "\\".$listRegexpCharacters[$i], $result);
953
        }
954
955
        return $result;
956
    }
957
958
959
    /**
960
     * This function must be the same than the js one getSeparatorFromNumber above
961
     * @return array
962
     */
963
    public static function getAllowedSeparator()
964
    {
965
        $fillBlanksAllowedSeparator = array(
966
            array('[', ']'),
967
            array('{', '}'),
968
            array('(', ')'),
969
            array('*', '*'),
970
            array('#', '#'),
971
            array('%', '%'),
972
            array('$', '$'),
973
        );
974
975
        return $fillBlanksAllowedSeparator;
976
    }
977
978
    /**
979
     * return the start separator for answer
980
     * @param string $number
981
     *
982
     * @return string
983
     */
984
    public static function getStartSeparator($number)
985
    {
986
        $listSeparators = self::getAllowedSeparator();
987
988
        return $listSeparators[$number][0];
989
    }
990
991
    /**
992
     * return the end separator for answer
993
     * @param string $number
994
     *
995
     * @return string
996
     */
997
    public static function getEndSeparator($number)
998
    {
999
        $listSeparators = self::getAllowedSeparator();
1000
1001
        return $listSeparators[$number][1];
1002
    }
1003
1004
    /**
1005
     * Return as a description text, array of allowed separators for question
1006
     * eg: array("[...]", "(...)")
1007
     * @return array
1008
     */
1009
    public static function getAllowedSeparatorForSelect()
1010
    {
1011
        $listResults = array();
1012
        $fillBlanksAllowedSeparator = self::getAllowedSeparator();
1013
        for ($i=0; $i < count($fillBlanksAllowedSeparator); $i++) {
1014
            $listResults[] = $fillBlanksAllowedSeparator[$i][0]."...".$fillBlanksAllowedSeparator[$i][1];
1015
        }
1016
1017
        return $listResults;
1018
    }
1019
1020
    /**
1021
     * return the code number of the separator for the question
1022
     * @param string $startSeparator
1023
     * @param string $endSeparator
1024
     *
1025
     * @return int
1026
     */
1027
    public function getDefaultSeparatorNumber($startSeparator, $endSeparator)
1028
    {
1029
        $listSeparators = self::getAllowedSeparator();
1030
        $result = 0;
1031
        for ($i=0; $i < count($listSeparators); $i++) {
1032
            if ($listSeparators[$i][0] == $startSeparator &&
1033
                $listSeparators[$i][1] == $endSeparator
1034
            ) {
1035
                $result = $i;
1036
            }
1037
        }
1038
1039
        return $result;
1040
    }
1041
1042
    /**
1043
     * return the HTML display of the answer
1044
     * @param string $answer
1045
     * @param bool   $resultsDisabled
1046
     * @param bool $showTotalScoreAndUserChoices
1047
     *
1048
     * @return string
1049
     */
1050
    public static function getHtmlDisplayForAnswer(
1051
        $answer,
1052
        $resultsDisabled = false,
1053
        $showTotalScoreAndUserChoices = false
1054
    ) {
1055
        $result = '';
1056
        $listStudentAnswerInfo = self::getAnswerInfo($answer, true);
1057
1058 View Code Duplication
        if ($resultsDisabled == RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT) {
1059
            if ($showTotalScoreAndUserChoices) {
1060
                $resultsDisabled = false;
1061
            } else {
1062
                $resultsDisabled = true;
1063
            }
1064
        }
1065
1066
        // rebuild the answer with good HTML style
1067
        // this is the student answer, right or wrong
1068
        for ($i=0; $i < count($listStudentAnswerInfo['studentanswer']); $i++) {
1069
            if ($listStudentAnswerInfo['studentscore'][$i] == 1) {
1070
                $listStudentAnswerInfo['studentanswer'][$i] = self::getHtmlRightAnswer(
1071
                    $listStudentAnswerInfo['studentanswer'][$i],
1072
                    $listStudentAnswerInfo['tabwords'][$i],
1073
                    $resultsDisabled
1074
                );
1075
            } else {
1076
                $listStudentAnswerInfo['studentanswer'][$i] = self::getHtmlWrongAnswer(
1077
                    $listStudentAnswerInfo['studentanswer'][$i],
1078
                    $listStudentAnswerInfo['tabwords'][$i],
1079
                    $resultsDisabled
1080
                );
1081
            }
1082
        }
1083
1084
        // rebuild the sentence with student answer inserted
1085
        for ($i=0; $i < count($listStudentAnswerInfo['commonwords']); $i++) {
1086
            $result .= isset($listStudentAnswerInfo['commonwords'][$i]) ? $listStudentAnswerInfo['commonwords'][$i] : '';
1087
            $result .= isset($listStudentAnswerInfo['studentanswer'][$i]) ? $listStudentAnswerInfo['studentanswer'][$i] : '';
1088
        }
1089
1090
        // the last common word (should be </p>)
1091
        $result .= isset($listStudentAnswerInfo['commonwords'][$i]) ? $listStudentAnswerInfo['commonwords'][$i] : '';
1092
1093
        return $result;
1094
    }
1095
1096
    /**
1097
     * return the HTML code of answer for correct and wrong answer
1098
     * @param string $answer
1099
     * @param string $correct
1100
     * @param string $right
1101
     * @param bool   $resultsDisabled
1102
     *
1103
     * @return string
1104
     */
1105
    public static function getHtmlAnswer($answer, $correct, $right, $resultsDisabled = false)
1106
    {
1107
        $style = "color: green";
1108
        if (!$right) {
1109
            $style = "color: red; text-decoration: line-through;";
1110
        }
1111
        $type = FillBlanks::getFillTheBlankAnswerType($correct);
1112
        switch ($type) {
1113
            case self::FILL_THE_BLANK_MENU:
1114
                $correctAnswerHtml = '';
1115
                $listPossibleAnswers = FillBlanks::getFillTheBlankMenuAnswers($correct, false);
1116
                $correctAnswerHtml .= "<span style='color: green'>".$listPossibleAnswers[0]."</span>";
1117
                $correctAnswerHtml .= " <span style='font-weight:normal'>(";
1118
                for ($i=1; $i < count($listPossibleAnswers); $i++) {
1119
                    $correctAnswerHtml .= $listPossibleAnswers[$i];
1120
                    if ($i != count($listPossibleAnswers) - 1) {
1121
                        $correctAnswerHtml .= " | ";
1122
                    }
1123
                }
1124
                $correctAnswerHtml .= ")</span>";
1125
                break;
1126
            case self::FILL_THE_BLANK_SEVERAL_ANSWER:
1127
                $listCorrects = explode("||", $correct);
1128
                $firstCorrect = $correct;
1129
                if (count($listCorrects) > 0) {
1130
                    $firstCorrect = $listCorrects[0];
1131
                }
1132
                $correctAnswerHtml = "<span style='color: green'>".$firstCorrect."</span>";
1133
                break;
1134
            case self::FILL_THE_BLANK_STANDARD:
1135
            default:
1136
                $correctAnswerHtml = "<span style='color: green'>".$correct."</span>";
1137
        }
1138
1139
        if ($resultsDisabled) {
1140
            $correctAnswerHtml = "<span title='".get_lang("ExerciseWithFeedbackWithoutCorrectionComment")."'> - </span>";
1141
        }
1142
1143
        $result = "<span style='border:1px solid black; border-radius:5px; padding:2px; font-weight:bold;'>";
1144
        $result .= "<span style='$style'>".$answer."</span>";
1145
        $result .= "&nbsp;<span style='font-size:120%;'>/</span>&nbsp;";
1146
        $result .= $correctAnswerHtml;
1147
        $result .= "</span>";
1148
1149
        return $result;
1150
    }
1151
1152
    /**
1153
     * return HTML code for correct answer
1154
     * @param string $answer
1155
     * @param string $correct
1156
     * @param bool   $resultsDisabled
1157
     *
1158
     * @return string
1159
     */
1160
    public static function getHtmlRightAnswer($answer, $correct, $resultsDisabled = false)
1161
    {
1162
        return self::getHtmlAnswer($answer, $correct, true, $resultsDisabled);
1163
    }
1164
1165
    /**
1166
     * return HTML code for wrong answer
1167
     * @param string $answer
1168
     * @param string $correct
1169
     * @param bool   $resultsDisabled
1170
     *
1171
     * @return string
1172
     */
1173
    public static function getHtmlWrongAnswer($answer, $correct, $resultsDisabled = false)
1174
    {
1175
        return self::getHtmlAnswer($answer, $correct, false, $resultsDisabled);
1176
    }
1177
1178
    /**
1179
     * Check if a answer is correct by its text
1180
     * @param string $answerText
1181
     * @return bool
1182
     */
1183
    public static function isCorrect($answerText)
1184
    {
1185
        $answerInfo = FillBlanks::getAnswerInfo($answerText, true);
1186
        $correctAnswerList = $answerInfo['tabwords'];
1187
        $studentAnswer = $answerInfo['studentanswer'];
1188
1189
        $isCorrect = true;
1190
1191
        foreach ($correctAnswerList as $i => $correctAnswer) {
1192
            $isGoodStudentAnswer = FillBlanks::isGoodStudentAnswer($studentAnswer[$i], $correctAnswer);
1193
1194
            $isCorrect = $isCorrect && $isGoodStudentAnswer;
1195
        }
1196
1197
        return $isCorrect;
1198
    }
1199
}
1200