chamilo/chamilo-lms

View on GitHub
public/main/exercise/fill_blanks.class.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

/* For licensing terms, see /license.txt */

use Chamilo\CoreBundle\Component\Utils\StateIcon;

/**
 *  Class FillBlanks.
 *
 * @author Eric Marguin
 * @author Julio Montoya multiple fill in blank option added.
 */
class FillBlanks extends Question
{
    public const FILL_THE_BLANK_STANDARD = 0;
    public const FILL_THE_BLANK_MENU = 1;
    public const FILL_THE_BLANK_SEVERAL_ANSWER = 2;

    public $typePicture = 'fill_in_blanks.png';
    public $explanationLangVar = 'Fill blanks or form';

    /**
     * Constructor.
     */
    public function __construct()
    {
        parent::__construct();
        $this->type = FILL_IN_BLANKS;
        $this->isContent = $this->getIsContent();
    }

    public function createAnswersForm($form)
    {
        $defaults = [];
        $defaults['answer'] = get_lang('DefaultTextInBlanks');
        $defaults['select_separator'] = 0;
        $blankSeparatorNumber = 0;
        if (!empty($this->id)) {
            $objectAnswer = new Answer($this->id);
            $answer = $objectAnswer->selectAnswer(1);
            $listAnswersInfo = self::getAnswerInfo($answer);
            $defaults['multiple_answer'] = 0;
            if ($listAnswersInfo['switchable']) {
                $defaults['multiple_answer'] = 1;
            }
            // Take the complete string except after the last '::'
            $defaults['answer'] = $listAnswersInfo['text'];
            $defaults['select_separator'] = $listAnswersInfo['blank_separator_number'];
            $blankSeparatorNumber = $listAnswersInfo['blank_separator_number'];
        }

        $blankSeparatorStart = self::getStartSeparator($blankSeparatorNumber);
        $blankSeparatorEnd = self::getEndSeparator($blankSeparatorNumber);
        $setWeightAndSize = '';
        if (isset($listAnswersInfo) && count($listAnswersInfo['weighting']) > 0) {
            foreach ($listAnswersInfo['weighting'] as $i => $weighting) {
                $setWeightAndSize .= 'document.getElementById("weighting['.$i.']").value = "'.$weighting.'";';
            }
            foreach ($listAnswersInfo['input_size'] as $i => $sizeOfInput) {
                $setWeightAndSize .= 'document.getElementById("sizeofinput['.$i.']").value = "'.$sizeOfInput.'";';
                $setWeightAndSize .= 'document.getElementById("samplesize['.$i.']").style.width = "'.$sizeOfInput.'px";';
            }
        }

        echo '<script>
            var firstTime = true;
            var originalOrder = new Array();
            var blankSeparatorStart = "'.$blankSeparatorStart.'";
            var blankSeparatorEnd = "'.$blankSeparatorEnd.'";
            var blankSeparatorStartRegexp = getBlankSeparatorRegexp(blankSeparatorStart);
            var blankSeparatorEndRegexp = getBlankSeparatorRegexp(blankSeparatorEnd);
            var blanksRegexp = "/"+blankSeparatorStartRegexp+"[^"+blankSeparatorStartRegexp+"]*"+blankSeparatorEndRegexp+"/g";

            function attachEventsToEditor() {
                var editor = tinymce.get("answer");
                if (editor) {
                    console.log("Editor found, attaching events.");

                    editor.on("keyup", function() {
                        updateBlanks();
                    });

                    editor.on("SetContent", function() {
                        updateBlanks();
                    });

                    editor.on("ExecCommand", function() {
                        updateBlanks();
                    });

                    editor.on("Paste", function() {
                        updateBlanks();
                    });
                } else {
                    console.log("Editor not yet available, retrying...");
                    setTimeout(attachEventsToEditor, 100); // Retry after 100 ms
                }
            }

            document.addEventListener("DOMContentLoaded", function() {
                attachEventsToEditor();
            });

            function updateBlanks()
            {
                var answer;
                if (firstTime) {
                    var field = document.getElementById("answer");
                    answer = field.value;
                } else {
                    answer = tinymce.get("answer").getContent();
                }

                // disable the save button, if not blanks have been created
                $("button").attr("disabled", "disabled");
                $("#defineoneblank").show();

                var blanks = answer.match(eval(blanksRegexp));
                var fields = "<div class=\"form-group \">";
                fields += "<label class=\"col-sm-2 control-label\"></label>";
                fields += "<div class=\"col-sm-8\">";
                fields += "<table class=\"data_table\">";
                fields += "<tr><th style=\"width:220px\">'.get_lang('Word to find').'</th>";
                fields += "<th style=\"width:50px\">'.get_lang('Score').'</th>";
                fields += "<th>'.get_lang('Input size of box to fill').'</th></tr>";

                if (blanks != null) {
                    for (var i=0; i < blanks.length; i++) {
                        // remove forbidden characters that causes bugs
                        blanks[i] = removeForbiddenChars(blanks[i]);
                        // trim blanks between brackets
                        blanks[i] = trimBlanksBetweenSeparator(blanks[i], blankSeparatorStart, blankSeparatorEnd);

                        // if the word is empty []
                        if (blanks[i] == blankSeparatorStartRegexp+blankSeparatorEndRegexp) {
                            break;
                        }

                        // get input size
                        var inputSize = 100;
                        var textValue = blanks[i].substr(1, blanks[i].length - 2);
                        var btoaValue = textValue.hashCode();

                        if (firstTime == false) {
                            var element = document.getElementById("samplesize["+i+"]");
                            if (element) {
                                inputSize = document.getElementById("sizeofinput["+i+"]").value;
                            }
                        }

                        if (document.getElementById("weighting["+i+"]")) {
                            var value = document.getElementById("weighting["+i+"]").value;
                        } else {
                            var value = "1";
                        }
                        var blanksWithColor = trimBlanksBetweenSeparator(blanks[i], blankSeparatorStart, blankSeparatorEnd, 1);

                        fields += "<tr>";
                        fields += "<td>"+blanksWithColor+"</td>";
                        fields += "<td><input class=\"form-control\" style=\"width:60px\" value=\""+value+"\" type=\"text\" id=\"weighting["+i+"]\" name=\"weighting["+i+"]\" /></td>";
                        fields += "<td>";
                        fields += "<input class=\"btn btn--plain\" type=\"button\" value=\"-\" onclick=\"changeInputSize(-1, "+i+")\">&nbsp;";
                        fields += "<input class=\"btn btn--plain\" type=\"button\" value=\"+\" onclick=\"changeInputSize(1, "+i+")\">&nbsp;";
                        fields += "&nbsp;&nbsp;<input class=\"sample\" id=\"samplesize["+i+"]\" data-btoa=\""+btoaValue+"\"   type=\"text\" value=\""+textValue+"\" style=\"width:"+inputSize+"px\" disabled=disabled />";
                        fields += "<input id=\"sizeofinput["+i+"]\" type=\"hidden\" value=\""+inputSize+"\" name=\"sizeofinput["+i+"]\"  />";
                        fields += "</td>";
                        fields += "</tr>";

                        // enable the save button
                        $("button").removeAttr("disabled");
                        $("#defineoneblank").hide();
                    }
                }

                document.getElementById("blanks_weighting").innerHTML = fields + "</table></div></div>";

                $(originalOrder).each(function(i, data) {
                     if (firstTime == false) {
                        value = data.value;
                        var d = $("input.sample[data-btoa=\'"+value+"\']");
                        var id = d.attr("id");
                        if (id) {
                            var sizeInputId = id.replace("samplesize", "sizeofinput");
                            var sizeInputId = sizeInputId.replace("[", "\\\[");
                            var sizeInputId = sizeInputId.replace("]", "\\\]");
                            $("#"+sizeInputId).val(data.width);
                            d.outerWidth(data.width+"px");
                        }
                    }
                });

                updateOrder(blanks);

                if (firstTime) {
                    firstTime = false;
                    '.$setWeightAndSize.'
                }
            }

            window.onload = updateBlanks;
            String.prototype.hashCode = function() {
                var hash = 0, i, chr, len;
                if (this.length === 0) return hash;
                for (i = 0, len = this.length; i < len; i++) {
                    chr   = this.charCodeAt(i);
                    hash  = ((hash << 5) - hash) + chr;
                    hash |= 0; // Convert to 32bit integer
                }
                return hash;
            };

            function updateOrder(blanks)
            {
                originalOrder = new Array();
                 if (blanks != null) {
                    for (var i=0; i < blanks.length; i++) {
                        // remove forbidden characters that causes bugs
                        blanks[i] = removeForbiddenChars(blanks[i]);
                        // trim blanks between brackets
                        blanks[i] = trimBlanksBetweenSeparator(blanks[i], blankSeparatorStart, blankSeparatorEnd);

                        // if the word is empty []
                        if (blanks[i] == blankSeparatorStartRegexp+blankSeparatorEndRegexp) {
                            break;
                        }
                        var textValue = blanks[i].substr(1, blanks[i].length - 2);
                        var btoaValue = textValue.hashCode();

                        if (firstTime == false) {
                            var element = document.getElementById("samplesize["+i+"]");
                            if (element) {
                                inputSize = document.getElementById("sizeofinput["+i+"]").value;
                                originalOrder.push({ "width" : inputSize, "value": btoaValue });
                            }
                        }
                    }
                }
            }

            function changeInputSize(coef, inIdNum)
            {
                var sampleSizeSelector = "#samplesize\\\[" + inIdNum + "\\\]";
                var sizeOfInputSelector = "#sizeofinput\\\[" + inIdNum + "\\\]";

                var currentWidth = $(sampleSizeSelector).outerWidth();

                if (currentWidth === undefined) {
                    return;
                }

                var newWidth = currentWidth + coef * 20;
                newWidth = Math.max(20, newWidth);
                newWidth = Math.min(newWidth, 600);

                $(sampleSizeSelector).outerWidth(newWidth);
                $(sizeOfInputSelector).attr("value", newWidth);

                var answer;
                if (firstTime) {
                    var field = document.getElementById("answer");
                    answer = field.value;
                } else {
                    answer = tinymce.get("answer").getContent();
                }
                var blanks = answer.match(eval(blanksRegexp));

                updateOrder(blanks);
            }

            function removeForbiddenChars(inTxt)
            {
                outTxt = inTxt;
                outTxt = outTxt.replace(/&quot;/g, ""); // remove the   char
                outTxt = outTxt.replace(/\x22/g, ""); // remove the   char
                outTxt = outTxt.replace(/"/g, ""); // remove the   char
                outTxt = outTxt.replace(/\\\\/g, ""); // remove the \ char
                outTxt = outTxt.replace(/&nbsp;/g, " ");
                outTxt = outTxt.replace(/^ +/, "");
                outTxt = outTxt.replace(/ +$/, "");

                return outTxt;
            }

            function changeBlankSeparator()
            {
                var definedSeparator = $("[name=select_separator] option:selected").text();
                $("[name=select_separator] option").each(function (index, value) {
                    $("#defineoneblank").html($("#defineoneblank").html().replace($(value).html(), definedSeparator))
                });
                var separatorNumber = $("#select_separator").val();
                var tabSeparator = getSeparatorFromNumber(separatorNumber);
                blankSeparatorStart = tabSeparator[0];
                blankSeparatorEnd = tabSeparator[1];
                blankSeparatorStartRegexp = getBlankSeparatorRegexp(blankSeparatorStart);
                blankSeparatorEndRegexp = getBlankSeparatorRegexp(blankSeparatorEnd);
                blanksRegexp = "/"+blankSeparatorStartRegexp+"[^"+blankSeparatorStartRegexp+"]*"+blankSeparatorEndRegexp+"/g";
                updateBlanks();
            }

            // this function is the same than the PHP one
            // if modify it modify the php one escapeForRegexp
            function getBlankSeparatorRegexp(inTxt)
            {
                var tabSpecialChar = new Array(".", "+", "*", "?", "[", "^", "]", "$", "(", ")",
                    "{", "}", "=", "!", "<", ">", "|", ":", "-", ")");
                for (var i=0; i < tabSpecialChar.length; i++) {
                    if (inTxt == tabSpecialChar[i]) {
                        return "\\\"+inTxt;
                    }
                }
                return inTxt;
            }

            // this function is the same than the PHP one
            // if modify it modify the php one getAllowedSeparator
            function getSeparatorFromNumber(number)
            {
                var separator = new Array();
                separator[0] = new Array("[", "]");
                separator[1] = new Array("{", "}");
                separator[2] = new Array("(", ")");
                separator[3] = new Array("*", "*");
                separator[4] = new Array("#", "#");
                separator[5] = new Array("%", "%");
                separator[6] = new Array("$", "$");
                return separator[number];
            }

            function trimBlanksBetweenSeparator(inTxt, inSeparatorStart, inSeparatorEnd, addColor)
            {
                var result = inTxt
                result = result.replace(inSeparatorStart, "");
                result = result.replace(inSeparatorEnd, "");
                result = result.trim();

                if (addColor == 1) {
                    var resultParts = result.split("|");
                    var partsToString = "";
                    resultParts.forEach(function(item, index) {
                        if (index == 0) {
                            item = "<b><font style=\"color:green\"> " + item +"</font></b>";
                        }
                        if (index < resultParts.length - 1) {
                            item  = item + " | ";
                        }
                        partsToString += item;
                    });
                    result = partsToString;
                }

                return inSeparatorStart+result+inSeparatorEnd;
            }

        </script>';

        // answer
        $form->addLabel(
            null,
            get_lang('Please type your text below').', '.
            get_lang('and').' '.
            get_lang('use square brackets [...] to define one or more blanks')
        );
        $form->addHtmlEditor(
            'answer',
            Display::return_icon('fill_field.png'),
            true,
            false,
            ['ToolbarSet' => 'TestQuestionDescription'],
            ['id' => 'answer'],
        );
        $form->addRule('answer', get_lang('Please type the text'), 'required');

        //added multiple answers
        $form->addCheckBox('multiple_answer', '', get_lang('Allow answers order switches'));
        $form->addSelect(
            'select_separator',
            get_lang('Select a blanks marker'),
            self::getAllowedSeparatorForSelect(),
            [
                'id' => 'select_separator',
                'style' => 'width:150px',
                'class' => 'form-control',
                'onchange' => 'changeBlankSeparator()',
            ]
        );
        $form->addLabel(
            null,
            '<input type="button" onclick="updateBlanks()" value="'.get_lang('Refresh terms').'" class="btn btn--plain" />'
        );

        $form->addHtml('<div id="blanks_weighting"></div>');

        global $text;
        // setting the save button here and not in the question class.php
        $form->addHtml('<div id="defineoneblank" style="color:#D04A66; margin-left:160px">'.get_lang('Please define at least one blank with square brackets [...]').'</div>');
        $form->addButtonSave($text, 'submitQuestion');

        if (!empty($this->id)) {
            $form->setDefaults($defaults);
        } else {
            if (1 == $this->isContent) {
                $form->setDefaults($defaults);
            }
        }
    }

    public function processAnswersCreation($form, $exercise)
    {
        $answer = $form->getSubmitValue('answer');
        // Due the ckeditor transform the elements to their HTML value

        //$answer = api_html_entity_decode($answer, ENT_QUOTES, $charset);
        //$answer = htmlentities(api_utf8_encode($answer));

        // remove the "::" eventually written by the user
        $answer = str_replace('::', '', $answer);

        // remove starting and ending space and &nbsp;
        $answer = api_preg_replace("/\xc2\xa0/", ' ', $answer);

        // start and end separator
        $blankStartSeparator = self::getStartSeparator($form->getSubmitValue('select_separator'));
        $blankEndSeparator = self::getEndSeparator($form->getSubmitValue('select_separator'));
        $blankStartSeparatorRegexp = self::escapeForRegexp($blankStartSeparator);
        $blankEndSeparatorRegexp = self::escapeForRegexp($blankEndSeparator);

        // remove spaces at the beginning and the end of text in square brackets
        $answer = preg_replace_callback(
            '/'.$blankStartSeparatorRegexp.'[^]]+'.$blankEndSeparatorRegexp.'/',
            function ($matches) use ($blankStartSeparator, $blankEndSeparator) {
                $matchingResult = $matches[0];
                $matchingResult = trim($matchingResult, $blankStartSeparator);
                $matchingResult = trim($matchingResult, $blankEndSeparator);
                $matchingResult = trim($matchingResult);
                // remove forbidden chars
                $matchingResult = str_replace('/\\/', '', $matchingResult);
                $matchingResult = str_replace('/"/', '', $matchingResult);

                return $blankStartSeparator.$matchingResult.$blankEndSeparator;
            },
            $answer
        );

        // get the blanks weightings
        $nb = preg_match_all(
            '/'.$blankStartSeparatorRegexp.'[^'.$blankStartSeparatorRegexp.']*'.$blankEndSeparatorRegexp.'/',
            $answer,
            $blanks
        );

        if (isset($_GET['editQuestion'])) {
            $this->weighting = 0;
        }

        /* if we have some [tobefound] in the text
        build the string to save the following in the answers table
        <p>I use a [computer] and a [pen].</p>
        becomes
        <p>I use a [computer] and a [pen].</p>::100,50:100,50@1
            ++++++++-------**
            --- -- --- -- -
            A B  (C) (D)(E)
        +++++++ : required, weighting of each words
        ------- : optional, input width to display, 200 if not present
        ** : equal @1 if "Allow answers order switches" has been checked, @ otherwise
        A : weighting for the word [computer]
        B : weighting for the word [pen]
        C : input width for the word [computer]
        D : input width for the word [pen]
        E : equal @1 if "Allow answers order switches" has been checked, @ otherwise
        */
        if ($nb > 0) {
            $answer .= '::';
            // weighting
            for ($i = 0; $i < $nb; $i++) {
                // enter the weighting of word $i
                $answer .= $form->getSubmitValue('weighting['.$i.']');
                // not the last word, add ","
                if ($i != $nb - 1) {
                    $answer .= ',';
                }
                // calculate the global weighting for the question
                $this->weighting += (float) $form->getSubmitValue('weighting['.$i.']');
            }

            // input width
            $answer .= ':';
            for ($i = 0; $i < $nb; $i++) {
                // enter the width of input for word $i
                $answer .= $form->getSubmitValue('sizeofinput['.$i.']');
                // not the last word, add ","
                if ($i != $nb - 1) {
                    $answer .= ',';
                }
            }
        }

        // write the blank separator code number
        // see function getAllowedSeparator
        /*
            0 [...]
            1 {...}
            2 (...)
            3 *...*
            4 #...#
            5 %...%
            6 $...$
         */
        $answer .= ':'.$form->getSubmitValue('select_separator');

        // Allow answers order switches
        $is_multiple = $form->getSubmitValue('multiple_answer');
        $answer .= '@'.$is_multiple;

        $this->save($exercise);
        $objAnswer = new Answer($this->id);
        $objAnswer->createAnswer($answer, 0, '', 0, 1);
        $objAnswer->save();
    }

    public function return_header(Exercise $exercise, $counter = null, $score = [])
    {
        $header = parent::return_header($exercise, $counter, $score);
        $header .= '<table class="'.$this->questionTableClass.'">
            <tr>
                <th>'.get_lang('Answer').'</th>
            </tr>';

        return $header;
    }

    /**
     * @param int    $currentQuestion
     * @param int    $questionId
     * @param string $correctItem
     * @param array  $attributes
     * @param string $answer
     * @param array  $listAnswersInfo
     * @param bool   $displayForStudent
     * @param int    $inBlankNumber
     * @param string $labelId
     *
     * @return string
     */
    public static function getFillTheBlankHtml(
        $currentQuestion,
        $questionId,
        $correctItem,
        $attributes,
        $answer,
        $listAnswersInfo,
        $displayForStudent,
        $inBlankNumber,
        $labelId = ''
    ) {
        $inTabTeacherSolution = $listAnswersInfo['words'];
        $inTeacherSolution = $inTabTeacherSolution[$inBlankNumber];

        if (empty($labelId)) {
            $labelId = 'choice_id_'.$currentQuestion.'_'.$inBlankNumber;
        }

        switch (self::getFillTheBlankAnswerType($inTeacherSolution)) {
            case self::FILL_THE_BLANK_MENU:
                $selected = '';
                // the blank menu
                // display a menu from answer separated with |
                // if display for student, shuffle the correct answer menu
                $listMenu = self::getFillTheBlankMenuAnswers(
                    $inTeacherSolution,
                    $displayForStudent
                );

                $resultOptions = ['' => '--'];
                foreach ($listMenu as $item) {
                    $resultOptions[sha1($item)] = $item;
                }

                foreach ($resultOptions as $key => $value) {
                    if ($correctItem == $value) {
                        $selected = $key;

                        break;
                    }
                }
                $width = '';
                if (!empty($attributes['style'])) {
                    $width = str_replace('width:', '', $attributes['style']);
                }

                $result = Display::select(
                    "choice[$questionId][]",
                    $resultOptions,
                    $selected,
                    [
                        'class' => 'form-control',
                        'data-width' => $width,
                        'id' => $labelId,
                    ],
                    false
                );

                break;
            case self::FILL_THE_BLANK_SEVERAL_ANSWER:
            case self::FILL_THE_BLANK_STANDARD:
            default:
                $attributes['id'] = $labelId;
                $result = Display::input(
                    'text',
                    "choice[$questionId][]",
                    $correctItem,
                    $attributes
                );

                break;
        }

        return $result;
    }

    /**
     * Return an array with the different choices available
     * when the answers between bracket show as a menu.
     *
     * @param string $correctAnswer
     * @param bool   $displayForStudent true if we want to shuffle the choices of the menu for students
     *
     * @return array
     */
    public static function getFillTheBlankMenuAnswers($correctAnswer, $displayForStudent)
    {
        $list = api_preg_split("/\|/", $correctAnswer);
        foreach ($list as &$item) {
            $item = self::trimOption($item);
            $item = api_html_entity_decode($item);
        }
        // The list is always in the same order, there's no option to allow or disable shuffle options.
        if ($displayForStudent) {
            shuffle_assoc($list);
        }

        return $list;
    }

    /**
     * Return the array index of the student answer.
     *
     * @param string $correctAnswer the menu Choice1|Choice2|Choice3
     * @param string $studentAnswer the student answer must be Choice1 or Choice2 or Choice3
     *
     * @return int in the example 0 1 or 2 depending of the choice of the student
     */
    public static function getFillTheBlankMenuAnswerNum($correctAnswer, $studentAnswer)
    {
        $listChoices = self::getFillTheBlankMenuAnswers($correctAnswer, false);
        foreach ($listChoices as $num => $value) {
            if ($value == $studentAnswer) {
                return $num;
            }
        }

        // should not happened, because student choose the answer in a menu of possible answers
        return -1;
    }

    /**
     * Return the possible answer if the answer between brackets is a multiple choice menu.
     *
     * @param string $correctAnswer
     *
     * @return array
     */
    public static function getFillTheBlankSeveralAnswers($correctAnswer)
    {
        // is answer||Answer||response||Response , mean answer or Answer ...
        return api_preg_split("/\|\|/", $correctAnswer);
    }

    /**
     * Return true if student answer is right according to the correctAnswer
     * it is not as simple as equality, because of the type of Fill The Blank question
     * eg : studentAnswer = 'Un' and correctAnswer = 'Un||1||un'.
     *
     * @param string $studentAnswer [student_answer] of the info array of the answer field
     * @param string $correctAnswer [words] of the info array of the answer field
     * @param bool   $fromDatabase
     *
     * @return bool
     */
    public static function isStudentAnswerGood($studentAnswer, $correctAnswer, $fromDatabase = false)
    {
        $result = false;
        switch (self::getFillTheBlankAnswerType($correctAnswer)) {
            case self::FILL_THE_BLANK_MENU:
                $listMenu = self::getFillTheBlankMenuAnswers($correctAnswer, false);
                if ('' != $studentAnswer && isset($listMenu[0])) {
                    // First item is always the correct one.
                    $item = $listMenu[0];
                    if (!$fromDatabase) {
                        $item = sha1($item);
                        //$studentAnswer = sha1($studentAnswer);
                    }
                    if ($item === $studentAnswer) {
                        $result = true;
                    }
                }

                break;
            case self::FILL_THE_BLANK_SEVERAL_ANSWER:
                // the answer must be one of the choice made
                $listSeveral = self::getFillTheBlankSeveralAnswers($correctAnswer);
                $listSeveral = array_map(
                    function ($item) {
                        return self::trimOption(api_html_entity_decode($item));
                    },
                    $listSeveral
                );
                //$studentAnswer = htmlspecialchars($studentAnswer);
                $result = in_array($studentAnswer, $listSeveral);

                break;
            case self::FILL_THE_BLANK_STANDARD:
            default:
                $correctAnswer = api_html_entity_decode($correctAnswer);
                //$studentAnswer = htmlspecialchars($studentAnswer);
                $result = $studentAnswer == self::trimOption($correctAnswer);

                break;
        }

        return $result;
    }

    /**
     * @param string $correctAnswer
     *
     * @return int
     */
    public static function getFillTheBlankAnswerType($correctAnswer)
    {
        $type = self::FILL_THE_BLANK_STANDARD;
        if (api_strpos($correctAnswer, '|') && !api_strpos($correctAnswer, '||')) {
            $type = self::FILL_THE_BLANK_MENU;
        } elseif (api_strpos($correctAnswer, '||')) {
            $type = self::FILL_THE_BLANK_SEVERAL_ANSWER;
        }

        return $type;
    }

    /**
     * Return information about the answer.
     *
     * @param string $userAnswer      the text of the answer of the question
     * @param bool   $isStudentAnswer true if it's a student answer false the empty question model
     *
     * @return array of information about the answer
     */
    public static function getAnswerInfo($userAnswer = '', $isStudentAnswer = false)
    {
        $listAnswerResults = [];
        $listAnswerResults['text'] = '';
        $listAnswerResults['words_count'] = 0;
        $listAnswerResults['words_with_bracket'] = [];
        $listAnswerResults['words'] = [];
        $listAnswerResults['weighting'] = [];
        $listAnswerResults['input_size'] = [];
        $listAnswerResults['switchable'] = '';
        $listAnswerResults['student_answer'] = [];
        $listAnswerResults['student_score'] = [];
        $listAnswerResults['blank_separator_number'] = 0;
        $listDoubleColon = [];

        api_preg_match('/(.*)::(.*)$/s', $userAnswer, $listResult);

        if (count($listResult) < 2) {
            $listDoubleColon[] = '';
            $listDoubleColon[] = '';
        } else {
            $listDoubleColon[] = $listResult[1];
            $listDoubleColon[] = $listResult[2];
        }

        $listAnswerResults['system_string'] = $listDoubleColon[1];

        // Make sure we only take the last bit to find special marks
        $listArobaseSplit = explode('@', $listDoubleColon[1]);

        if (count($listArobaseSplit) < 2) {
            $listArobaseSplit[1] = '';
        }

        // Take the complete string except after the last '::'
        $listDetails = explode(':', $listArobaseSplit[0]);

        // < number of item after the ::[score]:[size]:[separator_id]@ , here there are 3
        if (count($listDetails) < 3) {
            $listWeightings = explode(',', $listDetails[0]);
            $listSizeOfInput = [];
            for ($i = 0; $i < count($listWeightings); $i++) {
                $listSizeOfInput[] = 200;
            }
            $blankSeparatorNumber = 0; // 0 is [...]
        } else {
            $listWeightings = explode(',', $listDetails[0]);
            $listSizeOfInput = explode(',', $listDetails[1]);
            $blankSeparatorNumber = $listDetails[2];
        }

        $listAnswerResults['text'] = $listDoubleColon[0];
        $listAnswerResults['weighting'] = $listWeightings;
        $listAnswerResults['input_size'] = $listSizeOfInput;
        $listAnswerResults['switchable'] = $listArobaseSplit[1];
        $listAnswerResults['blank_separator_start'] = self::getStartSeparator($blankSeparatorNumber);
        $listAnswerResults['blank_separator_end'] = self::getEndSeparator($blankSeparatorNumber);
        $listAnswerResults['blank_separator_number'] = $blankSeparatorNumber;

        $blankCharStart = self::getStartSeparator($blankSeparatorNumber);
        $blankCharEnd = self::getEndSeparator($blankSeparatorNumber);
        $blankCharStartForRegexp = self::escapeForRegexp($blankCharStart);
        $blankCharEndForRegexp = self::escapeForRegexp($blankCharEnd);
        $listWords = [];
        // Get all blanks words
        $listAnswerResults['words_count'] = api_preg_match_all(
            '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
            $listDoubleColon[0],
            $listWords
        );

        if ($listAnswerResults['words_count'] > 0) {
            $listAnswerResults['words_with_bracket'] = $listWords[0];
            // remove [ and ] in string
            array_walk(
                $listWords[0],
                function (&$value, $key, $tabBlankChar) {
                    $trimChars = '';
                    for ($i = 0; $i < count($tabBlankChar); $i++) {
                        $trimChars .= $tabBlankChar[$i];
                    }
                    $value = trim($value, $trimChars);
                },
                [$blankCharStart, $blankCharEnd]
            );
            $listAnswerResults['words'] = $listWords[0];
        }

        // Get all common words
        $commonWords = api_preg_replace(
            '/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/',
            '::',
            $listDoubleColon[0]
        );

        // if student answer, the second [] is the student answer,
        // the third is if student scored or not
        $listBrackets = [];
        $listWords = [];
        if ($isStudentAnswer) {
            for ($i = 0; $i < count($listAnswerResults['words']); $i++) {
                $listBrackets[] = $listAnswerResults['words_with_bracket'][$i];
                $listWords[] = $listAnswerResults['words'][$i];
                if ($i + 1 < count($listAnswerResults['words'])) {
                    // should always be
                    $i++;
                }
                $listAnswerResults['student_answer'][] = $listAnswerResults['words'][$i];
                if ($i + 1 < count($listAnswerResults['words'])) {
                    // should always be
                    $i++;
                }
                $listAnswerResults['student_score'][] = $listAnswerResults['words'][$i];
            }
            $listAnswerResults['words'] = $listWords;
            $listAnswerResults['words_with_bracket'] = $listBrackets;

            // if we are in student view, we've got 3 times :::::: for common words
            $commonWords = api_preg_replace('/::::::/', '::', $commonWords);
        }
        $listAnswerResults['common_words'] = explode('::', $commonWords);

        return $listAnswerResults;
    }

    /**
     * Return an array of student state answers for fill the blank questions
     * for each students that answered the question
     * -2  : didn't answer
     * -1  : student answer is wrong
     *  0  : student answer is correct
     * >0  : fill the blank question with choice menu, is the index of the student answer (right answer index is 0).
     *
     * @param int $testId
     * @param int $questionId
     * @param $studentsIdList
     * @param string $startDate
     * @param string $endDate
     * @param bool   $useLastAnsweredAttempt
     *
     * @return array
     *               (
     *               [student_id] => Array
     *               (
     *               [first fill the blank for question] => -1
     *               [second fill the blank for question] => 2
     *               [third fill the blank for question] => -1
     *               )
     *               )
     */
    public static function getFillTheBlankResult(
        $testId,
        $questionId,
        $studentsIdList,
        $startDate,
        $endDate,
        $useLastAnsweredAttempt = true
    ) {
        $tblTrackEAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
        $tblTrackEExercise = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
        $courseId = api_get_course_int_id();
        // If no user has answered questions, no need to go further. Return empty array.
        if (empty($studentsIdList)) {
            return [];
        }
        // request to have all the answers of student for this question
        // student may have doing it several time
        // student may have not answered the bracket id, in this case, is result of the answer is empty
        // we got the less recent attempt first
        $sql = 'SELECT * FROM '.$tblTrackEAttempt.' tea
                LEFT JOIN '.$tblTrackEExercise.' tee
                ON
                    tee.exe_id = tea.exe_id AND
                    exe_exo_id = '.$testId.'
               WHERE
                    tee.c_id = '.$courseId.' AND
                    question_id = '.$questionId.' AND
                    tea.user_id IN ('.implode(',', $studentsIdList).')  AND
                    tea.tms >= "'.$startDate.'" AND
                    tea.tms <= "'.$endDate.'"
               ORDER BY user_id, tea.exe_id;
        ';

        $res = Database::query($sql);
        $userResult = [];
        // foreach attempts for all students starting with his older attempt
        while ($data = Database::fetch_array($res)) {
            $answer = self::getAnswerInfo($data['answer'], true);

            // for each bracket to find in this question
            foreach ($answer['student_answer'] as $bracketNumber => $studentAnswer) {
                if ('' != $answer['student_answer'][$bracketNumber]) {
                    // student has answered this bracket, cool
                    switch (self::getFillTheBlankAnswerType($answer['words'][$bracketNumber])) {
                        case self::FILL_THE_BLANK_MENU:
                            // get the indice of the choosen answer in the menu
                            // we know that the right answer is the first entry of the menu, ie 0
                            // (remember, menu entries are shuffled when taking the test)
                            $userResult[$data['user_id']][$bracketNumber] = self::getFillTheBlankMenuAnswerNum(
                                $answer['words'][$bracketNumber],
                                $answer['student_answer'][$bracketNumber]
                            );

                            break;
                        default:
                            if (self::isStudentAnswerGood(
                                $answer['student_answer'][$bracketNumber],
                                $answer['words'][$bracketNumber]
                            )
                            ) {
                                $userResult[$data['user_id']][$bracketNumber] = 0; //  right answer
                            } else {
                                $userResult[$data['user_id']][$bracketNumber] = -1; // wrong answer
                            }
                    }
                } else {
                    // student didn't answer this bracket
                    if ($useLastAnsweredAttempt) {
                        // if we take into account the last answered attempt
                        if (!isset($userResult[$data['user_id']][$bracketNumber])) {
                            $userResult[$data['user_id']][$bracketNumber] = -2; // not answered
                        }
                    } else {
                        // we take the last attempt, even if the student answer the question before
                        $userResult[$data['user_id']][$bracketNumber] = -2; // not answered
                    }
                }
            }
        }

        return $userResult;
    }

    /**
     * Return the number of student that give at leat an answer in the fill the blank test.
     *
     * @param array $resultList
     *
     * @return int
     */
    public static function getNbResultFillBlankAll($resultList)
    {
        $outRes = 0;
        // for each student in group
        foreach ($resultList as $list) {
            $found = false;
            // for each bracket, if student has at least one answer ( choice > -2) then he pass the question
            foreach ($list as $choice) {
                if ($choice > -2 && !$found) {
                    $outRes++;
                    $found = true;
                }
            }
        }

        return $outRes;
    }

    /**
     * Replace the occurrence of blank word with [correct answer][student answer][answer is correct].
     *
     * @param array $listWithStudentAnswer
     *
     * @return string
     */
    public static function getAnswerInStudentAttempt($listWithStudentAnswer)
    {
        $separatorStart = $listWithStudentAnswer['blank_separator_start'];
        $separatorEnd = $listWithStudentAnswer['blank_separator_end'];
        // lets rebuild the sentence with [correct answer][student answer][answer is correct]
        $result = '';
        for ($i = 0; $i < count($listWithStudentAnswer['common_words']) - 1; $i++) {
            $answerValue = null;
            if (isset($listWithStudentAnswer['student_answer'][$i])) {
                $answerValue = $listWithStudentAnswer['student_answer'][$i];
            }
            $scoreValue = null;
            if (isset($listWithStudentAnswer['student_score'][$i])) {
                $scoreValue = $listWithStudentAnswer['student_score'][$i];
            }

            $result .= $listWithStudentAnswer['common_words'][$i];
            $result .= $listWithStudentAnswer['words_with_bracket'][$i];
            $result .= $separatorStart.$answerValue.$separatorEnd;
            $result .= $separatorStart.$scoreValue.$separatorEnd;
        }
        $result .= $listWithStudentAnswer['common_words'][$i];
        $result .= '::';
        // add the system string
        $result .= $listWithStudentAnswer['system_string'];

        return $result;
    }

    /**
     * This function is the same than the js one above getBlankSeparatorRegexp.
     *
     * @param string $inChar
     *
     * @return string
     */
    public static function escapeForRegexp($inChar)
    {
        $listChars = [
            '.',
            '+',
            '*',
            '?',
            '[',
            '^',
            ']',
            '$',
            '(',
            ')',
            '{',
            '}',
            '=',
            '!',
            '>',
            '|',
            ':',
            '-',
            ')',
        ];

        if (in_array($inChar, $listChars)) {
            return '\\'.$inChar;
        } else {
            return $inChar;
        }
    }

    /**
     * This function must be the same than the js one getSeparatorFromNumber above.
     *
     * @return array
     */
    public static function getAllowedSeparator()
    {
        return [
            ['[', ']'],
            ['{', '}'],
            ['(', ')'],
            ['*', '*'],
            ['#', '#'],
            ['%', '%'],
            ['$', '$'],
        ];
    }

    /**
     * return the start separator for answer.
     *
     * @param string $number
     *
     * @return string
     */
    public static function getStartSeparator($number)
    {
        $listSeparators = self::getAllowedSeparator();

        return $listSeparators[$number][0];
    }

    /**
     * return the end separator for answer.
     *
     * @param string $number
     *
     * @return string
     */
    public static function getEndSeparator($number)
    {
        $listSeparators = self::getAllowedSeparator();

        return $listSeparators[$number][1];
    }

    /**
     * Return as a description text, array of allowed separators for question
     * eg: array("[...]", "(...)").
     *
     * @return array
     */
    public static function getAllowedSeparatorForSelect()
    {
        $listResults = [];
        $allowedSeparator = self::getAllowedSeparator();
        foreach ($allowedSeparator as $part) {
            $listResults[] = $part[0].'...'.$part[1];
        }

        return $listResults;
    }

    /**
     * return the HTML display of the answer.
     *
     * @param string $answer
     * @param int    $feedbackType
     * @param bool   $resultsDisabled
     * @param bool   $showTotalScoreAndUserChoices
     *
     * @return string
     */
    public static function getHtmlDisplayForAnswer(
        $answer,
        $feedbackType,
        $resultsDisabled = false,
        $showTotalScoreAndUserChoices = false
    ) {
        $result = '';
        $listStudentAnswerInfo = self::getAnswerInfo($answer, true);

        // rebuild the answer with good HTML style
        // this is the student answer, right or wrong
        for ($i = 0; $i < count($listStudentAnswerInfo['student_answer']); $i++) {
            if (1 == $listStudentAnswerInfo['student_score'][$i]) {
                $listStudentAnswerInfo['student_answer'][$i] = self::getHtmlRightAnswer(
                    $listStudentAnswerInfo['student_answer'][$i],
                    $listStudentAnswerInfo['words'][$i],
                    $feedbackType,
                    $resultsDisabled,
                    $showTotalScoreAndUserChoices
                );
            } else {
                $listStudentAnswerInfo['student_answer'][$i] = self::getHtmlWrongAnswer(
                    $listStudentAnswerInfo['student_answer'][$i],
                    $listStudentAnswerInfo['words'][$i],
                    $feedbackType,
                    $resultsDisabled,
                    $showTotalScoreAndUserChoices
                );
            }
        }

        // rebuild the sentence with student answer inserted
        for ($i = 0; $i < count($listStudentAnswerInfo['common_words']); $i++) {
            if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $resultsDisabled) {
                if (empty($listStudentAnswerInfo['student_answer'][$i])) {
                    continue;
                }
            }
            $result .= isset($listStudentAnswerInfo['common_words'][$i]) ? $listStudentAnswerInfo['common_words'][$i] : '';
            $studentLabel = isset($listStudentAnswerInfo['student_answer'][$i]) ? $listStudentAnswerInfo['student_answer'][$i] : '';
            $result .= $studentLabel;
        }

        // the last common word (should be </p>)
        $result .= isset($listStudentAnswerInfo['common_words'][$i]) ? $listStudentAnswerInfo['common_words'][$i] : '';

        return $result;
    }

    /**
     * return the HTML code of answer for correct and wrong answer.
     *
     * @param string $answer
     * @param string $correct
     * @param string $right
     * @param int    $feedbackType
     * @param bool   $resultsDisabled
     * @param bool   $showTotalScoreAndUserChoices
     *
     * @return string
     */
    public static function getHtmlAnswer(
        $answer,
        $correct,
        $right,
        $feedbackType,
        $resultsDisabled = false,
        $showTotalScoreAndUserChoices = false
    ) {
        $hideExpectedAnswer = false;
        $hideUserSelection = false;
        switch ($resultsDisabled) {
            case RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING:
            case RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER:
                $hideUserSelection = true;

                break;
            case RESULT_DISABLE_SHOW_SCORE_ONLY:
                if (0 == $feedbackType) {
                    $hideExpectedAnswer = true;
                }

                break;
            case RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK:
            case RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK:
            case RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT:
                $hideExpectedAnswer = true;
                if ($showTotalScoreAndUserChoices) {
                    $hideExpectedAnswer = false;
                }

                break;
        }

        $style = 'feedback-green';
        $iconAnswer = Display::getMdiIcon(StateIcon::COMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Correct'));
        if (!$right) {
            $style = 'feedback-red';
            $iconAnswer = Display::getMdiIcon(StateIcon::INCOMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Incorrect'));
        }

        $correctAnswerHtml = '';
        $type = self::getFillTheBlankAnswerType($correct);
        switch ($type) {
            case self::FILL_THE_BLANK_MENU:
                $listPossibleAnswers = self::getFillTheBlankMenuAnswers($correct, false);
                $correctAnswerHtml .= "<span class='correct-answer'><strong>".$listPossibleAnswers[0].'</strong>';
                $correctAnswerHtml .= ' (';
                for ($i = 1; $i < count($listPossibleAnswers); $i++) {
                    $correctAnswerHtml .= $listPossibleAnswers[$i];
                    if ($i != count($listPossibleAnswers) - 1) {
                        $correctAnswerHtml .= ' | ';
                    }
                }
                $correctAnswerHtml .= ')</span>';

                break;
            case self::FILL_THE_BLANK_SEVERAL_ANSWER:
                $listCorrects = explode('||', $correct);
                $firstCorrect = $correct;
                if (count($listCorrects) > 0) {
                    $firstCorrect = $listCorrects[0];
                }
                $correctAnswerHtml = "<span class='correct-answer'>".$firstCorrect.'</span>';

                break;
            case self::FILL_THE_BLANK_STANDARD:
            default:
                $correctAnswerHtml = "<span class='correct-answer'>".$correct.'</span>';
        }

        if ($hideExpectedAnswer) {
            $correctAnswerHtml = "<span
                class='feedback-green'
                title='".get_lang('Note: This test has been setup to hide the expected answers.')."'> &#8212; </span>";
        }

        $result = "<span class='feedback-question'>";
        if (false === $hideUserSelection) {
            $result .= $iconAnswer."<span class='$style'>".$answer.'</span>';
        }
        $result .= "<span class='feedback-separator'>|</span>";
        $result .= $correctAnswerHtml;
        $result .= '</span>';

        return $result;
    }

    /**
     * return HTML code for correct answer.
     *
     * @param string $answer
     * @param string $correct
     * @param string $feedbackType
     * @param bool   $resultsDisabled
     * @param bool   $showTotalScoreAndUserChoices
     *
     * @return string
     */
    public static function getHtmlRightAnswer(
        $answer,
        $correct,
        $feedbackType,
        $resultsDisabled = false,
        $showTotalScoreAndUserChoices = false
    ) {
        return self::getHtmlAnswer(
            $answer,
            $correct,
            true,
            $feedbackType,
            $resultsDisabled,
            $showTotalScoreAndUserChoices
        );
    }

    /**
     * return HTML code for wrong answer.
     *
     * @param string $answer
     * @param string $correct
     * @param string $feedbackType
     * @param bool   $resultsDisabled
     * @param bool   $showTotalScoreAndUserChoices
     *
     * @return string
     */
    public static function getHtmlWrongAnswer(
        $answer,
        $correct,
        $feedbackType,
        $resultsDisabled = false,
        $showTotalScoreAndUserChoices = false
    ) {
        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $resultsDisabled) {
            return '';
        }

        return self::getHtmlAnswer(
            $answer,
            $correct,
            false,
            $feedbackType,
            $resultsDisabled,
            $showTotalScoreAndUserChoices
        );
    }

    /**
     * Check if a answer is correct by its text.
     *
     * @param string $answerText
     *
     * @return bool
     */
    public static function isCorrect($answerText)
    {
        $answerInfo = self::getAnswerInfo($answerText, true);
        $correctAnswerList = $answerInfo['words'];
        $studentAnswer = $answerInfo['student_answer'];
        $isCorrect = true;

        foreach ($correctAnswerList as $i => $correctAnswer) {
            $value = self::isStudentAnswerGood($studentAnswer[$i], $correctAnswer);
            $isCorrect = $isCorrect && $value;
        }

        return $isCorrect;
    }

    /**
     * Clear the answer entered by student.
     *
     * @param string $answer
     *
     * @return string
     */
    public static function clearStudentAnswer($answer)
    {
        $answer = htmlentities(api_utf8_encode($answer), ENT_QUOTES);
        $answer = str_replace('&#039;', '&#39;', $answer); // fix apostrophe
        $answer = api_preg_replace('/\s\s+/', ' ', $answer); // replace excess white spaces
        $answer = strtr($answer, array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES)));

        return trim($answer);
    }

    /**
     * Removes double spaces between words.
     *
     * @param string $text
     *
     * @return string
     */
    private static function trimOption($text)
    {
        $text = trim($text);

        return preg_replace("/\s+/", ' ', $text);
    }
}