From 65778a19be46094b3f3005128104e89d523f8d0d Mon Sep 17 00:00:00 2001 From: Julio Montoya Date: Fri, 7 Aug 2015 12:48:40 +0200 Subject: [PATCH] Add fill in blanks code with some minor corrections (function name fixed) --- main/exercice/exercise.class.php | 74 +- main/exercice/fill_blanks.class.php | 938 ++++++++++++++++--- main/exercice/stats.php | 3 +- main/inc/lib/exercise.lib.php | 226 ++--- main/inc/lib/exercise_show_functions.lib.php | 34 +- 5 files changed, 990 insertions(+), 285 deletions(-) diff --git a/main/exercice/exercise.class.php b/main/exercice/exercise.class.php index 32e8cff443..3e13a8e30e 100755 --- a/main/exercice/exercise.class.php +++ b/main/exercice/exercise.class.php @@ -2497,13 +2497,83 @@ class Exercise break; // for fill in the blanks case FILL_IN_BLANKS: + + // insert the student result in the track_e_attempt table, field answer + // $answer is the answer like in the c_quiz_answer table for the question + // student datas are choice[] + + $listCorrectAnswers = FillBlanks::getAnswerInfo($answer); + $switchableAnswerSet = $listCorrectAnswers["switchable"]; + $answerWeighting = $listCorrectAnswers["tabweighting"]; + // user choices is an array $choice + + // get existing user data in n the BDD + if ($from_database) { + $queryfill = "SELECT answer + FROM $TBL_TRACK_ATTEMPT + WHERE exe_id = $exeId + AND question_id= ".intval($questionId); + $resfill = Database::query($queryfill); + $str = Database::result($resfill, 0, 'answer'); + + $listStudentResults = FillBlanks::getAnswerInfo($str, true); + $choice = $listStudentResults['studentanswer']; + } + + // loop other all blanks words + if (!$switchableAnswerSet) { + // not switchable answer, must be in the same place than teacher order + for ($i=0; $i < count($listCorrectAnswers['tabwords']); $i++) { + $studentAnswer = trim($choice[$i]); + $correctAnswer = $listCorrectAnswers['tabwords'][$i]; + $isAnswerCorrect = 0; + if (FillBlanks::isGoodStudentAnswer($studentAnswer, $correctAnswer)) { + // gives the related weighting to the student + $questionScore += $answerWeighting[$i]; + // increments total score + $totalScore += $answerWeighting[$i]; + $isAnswerCorrect = 1; + } + $listCorrectAnswers['studentanswer'][$i] = $studentAnswer; + $listCorrectAnswers['studentscore'][$i] = $isAnswerCorrect; + } + } else { + // switchable answer + $listStudentAsnwerTemp = $choice; + $listTeacherAnswerTemp = $listCorrectAnswers['tabwords']; + $listBadAnswerIndice = array(); + // for every teacher answer, check if there is a student answer + for ($i=0; $i < count($listStudentAsnwerTemp); $i++) { + $studentAnswer = trim($listStudentAsnwerTemp[$i]); + $found = false; + for ($j=0; $j < count($listTeacherAnswerTemp); $j++) { + $correctAnswer = $listTeacherAnswerTemp[$j]; + if (!$found) { + if (FillBlanks::isGoodStudentAnswer($studentAnswer, $correctAnswer)) { + $questionScore += $answerWeighting[$i]; + $totalScore += $answerWeighting[$i]; + $listTeacherAnswerTemp[$j] = ""; + $found = true; + } + } + } + $listCorrectAnswers['studentanswer'][$i] = $studentAnswer; + if (!$found) { + $listCorrectAnswers['studentscore'][$i] = 0; + } else { + $listCorrectAnswers['studentscore'][$i] = 1; + } + } + } + $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers); + // the question is encoded like this // [A] B [C] D [E] F::10,10,10@1 // number 1 before the "@" means that is a switchable fill in blank question // [A] B [C] D [E] F::10,10,10@ or [A] B [C] D [E] F::10,10,10 // means that is a normal fill blank question // first we explode the "::" - + /* $pre_array = explode('::', $answer); // is switchable fill blank or not $last = count($pre_array) - 1; @@ -2644,7 +2714,7 @@ class Exercise if (isset($real_text[$i +1])) { $answer .= $real_text[$i + 1]; } - } + }*/ break; // for calculated answer case CALCULATED_ANSWER: diff --git a/main/exercice/fill_blanks.class.php b/main/exercice/fill_blanks.class.php index cabe048afa..882d9428b7 100755 --- a/main/exercice/fill_blanks.class.php +++ b/main/exercice/fill_blanks.class.php @@ -10,8 +10,12 @@ **/ class FillBlanks extends Question { - static $typePicture = 'fill_in_blanks.png'; - static $explanationLangVar = 'FillBlanks'; + static $typePicture = 'fill_in_blanks.png'; + static $explanationLangVar = 'FillBlanks'; + + const FILL_THE_BLANK_STANDARD = 0; + const FILL_THE_BLANK_MENU = 1; + const FILL_THE_BLANK_SEVERAL_ANSWER = 2; /** * Constructor @@ -29,40 +33,35 @@ class FillBlanks extends Question */ function createAnswersForm($form) { + $fillBlanksAllowedSeparator = self::getAllowedSeparator(); + $defaults = array(); if (!empty($this->id)) { - $objAnswer = new Answer($this->id); - - // the question is encoded like this - // [A] B [C] D [E] F::10,10,10@1 - // number 1 before the "@" means that is a switchable fill in blank question - // [A] B [C] D [E] F::10,10,10@ or [A] B [C] D [E] F::10,10,10 - // means that is a normal fill blank question - - $pre_array = explode('::', $objAnswer->selectAnswer(1)); + $objectAnswer = new answer($this->id); + $answer = $objectAnswer->selectAnswer(1); + $listAnswersInfo = FillBlanks::getAnswerInfo($answer); - //make sure we only take the last bit to find special marks - $sz = count($pre_array); - $is_set_switchable = explode('@', $pre_array[$sz-1]); - - if ($is_set_switchable[1]) { + if ($listAnswersInfo["switchable"]) { $defaults['multiple_answer'] = 1; } else { $defaults['multiple_answer'] = 0; } - - //Take the complete string except after the last '::' - - $defaults['answer'] = ''; - for ($i=0; $i<($sz-1); $i++) { - $defaults['answer'] .= $pre_array[$i]; - } - $a_weightings = explode(',', $is_set_switchable[0]); + //take the complete string except after the last '::' + $defaults['answer'] = $listAnswersInfo["text"]; + $defaults['select_separator'] = $listAnswersInfo["blankseparatornumber"]; + $blanksepartornumber = $listAnswersInfo["blankseparatornumber"]; } else { $defaults['answer'] = get_lang('DefaultTextInBlanks'); + $defaults['select_separator'] = 0; + $blanksepartornumber = 0; } + $blankSeparatortStart = self::getStartSeparator($blanksepartornumber); + $blankSeparatortEnd = self::getEndSeparator($blanksepartornumber); + $blankSeparatortStartRegexp = self::escapeForRegexp($blankSeparatortStart); + $blankSeparatortEndRegexp = self::escapeForRegexp($blankSeparatortEnd); + $setValues = null; if (isset($a_weightings) && count($a_weightings) > 0) { @@ -70,119 +69,331 @@ class FillBlanks extends Question $setValues .= 'document.getElementById("weighting['.$i.']").value = "'.$weighting.'";'; } } - // javascript - echo ''; - - // answer - $form->addElement('label', null, '

'.get_lang('TypeTextBelow').', '.get_lang('And').' '.get_lang('UseTagForBlank')); - $form->addElement( - 'html_editor', - 'answer', - '', - array( - 'id' => 'answer', - 'onkeyup' => '"javascript: updateBlanks(this);"' - ), - array('ToolbarSet' => 'TestQuestionDescription', 'Height' => '350')); - - $form->addRule('answer', get_lang('GiveText'),'required'); - $form->addRule('answer', get_lang('DefineBlanks'),'regex','/\[.*\]/'); - - //added multiple answers - $form->addElement('checkbox', 'multiple_answer','', get_lang('FillInBlankSwitchable')); - - $form->addElement('html', '
'); - - global $text, $class; - // setting the save button here and not in the question class.php - $form->addButtonSave($text, 'submitQuestion'); - - if (!empty($this->id)) { - $form -> setDefaults($defaults); - } else { - if ($this->isContent == 1) { - $form->setDefaults($defaults); - } - } - } - - /** - * abstract function which creates the form to create / edit the answers of the question - * @param FormValidator $form - */ - function processAnswersCreation($form) - { - global $charset; - $answer = $form->getSubmitValue('answer'); - - //remove the :: eventually written by the user + + function getInputSize() { + var outTabSize = new Array(); + $("input").each(function() { + if ($(this).attr("id") && $(this).attr("id").match(/samplesize/)) { + var tabidnum = $(this).attr("id").match(/\d+/); + var idnum = tabidnum[0]; + var thewidth = $(this).next().attr("value"); + tabInputSize[idnum] = thewidth; + } + }); + } + + function changeInputSize(inCoef, inIdNum) + { + var currentWidth = $("#samplesize\\\["+inIdNum+"\\\]").width(); + var newWidth = currentWidth + inCoef * 20; + newWidth = Math.max(20, newWidth); + newWidth = Math.min(newWidth, 600); + $("#samplesize\\\["+inIdNum+"\\\]").width(newWidth); + $("#sizeofinput\\\["+inIdNum+"\\\]").attr("value", newWidth); + } + + function removeForbiddenChars(inTxt) { + outTxt = inTxt; + + outTxt = outTxt.replace(/"/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(/ /g, " "); + outTxt = outTxt.replace(/^ +/, ""); + outTxt = outTxt.replace(/ +$/, ""); + return outTxt; + } + + function changeBlankSeparator() + { + var separatorNumber = $("#select_separator").val(); + var tabSeparator = getSeparatorFromNumber(separatorNumber); + blankSeparatortStart = tabSeparator[0]; + blankSeparatortEnd = tabSeparator[1]; + blankSeparatortStartRegexp = getBlankSeparatorRegexp(blankSeparatortStart); + blankSeparatortEndRegexp = getBlankSeparatorRegexp(blankSeparatortEnd); + 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(innumber) + { + tabSeparator = new Array(); + tabSeparator[0] = new Array("[", "]"); + tabSeparator[1] = new Array("{", "}"); + tabSeparator[2] = new Array("(", ")"); + tabSeparator[3] = new Array("*", "*"); + tabSeparator[4] = new Array("#", "#"); + tabSeparator[5] = new Array("%", "%"); + tabSeparator[6] = new Array("$", "$"); + return tabSeparator[innumber]; + } + + function trimBlanksBetweenSeparator(inTxt, inSeparatorStart, inSeparatorEnd) + { + // blankSeparatortStartRegexp + // blankSeparatortEndRegexp + var result = inTxt + result = result.replace(inSeparatorStart, ""); + result = result.replace(inSeparatorEnd, ""); + result = result.trim(); + return inSeparatorStart+result+inSeparatorEnd; + } + '; + + // answer + $form->addElement ('label', null, '

'.get_lang('TypeTextBelow').', '.get_lang('And').' '.get_lang('UseTagForBlank')); + $form->addElement ('html_editor', 'answer', '','id="answer" cols="122" rows="6" onkeyup="javascript: updateBlanks(this);"', array('ToolbarSet' => 'TestQuestionDescription', 'Width' => '100%', 'Height' => '350')); + $form -> addRule ('answer',get_lang('GiveText'),'required'); + + //added multiple answers + $form->addElement ('checkbox','multiple_answer','', get_lang('FillInBlankSwitchable')); + $form->addElement('select', 'select_separator', get_lang("SelectFillTheBlankSeparator"), self::getAllowedSeparatorForSelect(), ' id="select_separator" style="width:150px" onchange="changeBlankSeparator()" '); + $form->addElement ('label', null, ''); + $form->addElement('html','
'); + + global $text, $class; + // setting the save button here and not in the question class.php + $form->addElement('html','
'.get_lang('DefineBlanks').'
'); + $form->addElement('style_submit_button','submitQuestion',$text, 'class="'.$class.'"'); + + if (!empty($this -> id)) { + $form -> setDefaults($defaults); + } else { + if ($this -> isContent == 1) { + $form -> setDefaults($defaults); + } + } + } + + /** + * abstract function which creates the form to create / edit the answers of the question + * @param FormValidator $form + */ + public function processAnswersCreation($form) + { + global $charset; + + $answer = $form->getSubmitValue('answer'); + + // Due the fckeditor transform the elements to their HTML value + $answer = api_html_entity_decode($answer, ENT_QUOTES, $charset); + + // remove the :: eventually written by the user $answer = str_replace('::', '', $answer); + // remove starting and ending space and   + $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('/\[[^\]]*\]/', $answer, $blanks); + $nb = preg_match_all('/'.$blankStartSeparatorRegexp.'[^'.$blankStartSeparatorRegexp.']*'.$blankEndSeparatorRegexp.'/', $answer, $blanks); if (isset($_GET['editQuestion'])) { - $this ->weighting = 0; - } - - if ($nb > 0) { - $answer .= '::'; - for ($i=0 ; $i < $nb; ++$i) { - $blankItem = $blanks[0][$i]; - $replace = array("[", "]"); - $newBlankItem = str_replace($replace, "", $blankItem); - $newBlankItem = "[".trim($newBlankItem)."]"; - $answer = str_replace($blankItem, $newBlankItem, $answer); - $answer .= $form->getSubmitValue('weighting['.$i.']').','; + $this -> weighting = 0; + } + + /* if we have some [tobefound] in the text + build the string to save the following in the answers table +

I use a [computer] and a [pen].

+ becomes +

I use a [computer] and a [pen].

::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 weightning for the question $this -> weighting += $form->getSubmitValue('weighting['.$i.']'); } - $answer = api_substr($answer, 0, -1); - } - $is_multiple = $form->getSubmitValue('multiple_answer'); - $answer.= '@'.$is_multiple; + // 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 .= ","; + } + } + } - $this->save(); - $objAnswer = new Answer($this->id); - $objAnswer->createAnswer($answer, 0, '', 0, '1'); + // 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(); + $objAnswer = new answer($this->id); + $objAnswer->createAnswer($answer, 0, '', 0, 1); $objAnswer->save(); - } + } /** * @param null $feedback_type @@ -190,13 +401,508 @@ class FillBlanks extends Question * @param null $score * @return null|string */ - function return_header($feedback_type = null, $counter = null, $score = null) + public function return_header($feedback_type = null, $counter = null, $score = null) { - $header = parent::return_header($feedback_type, $counter, $score); - $header .= ' - + $header = parent::return_header($feedback_type, $counter, $score); + $header .= '
+ - '; + '; return $header; - } + } + + /** + * @param $separatorStartRegexp + * @param $separatorEndRegexp + * @param $correctItemRegexp + * @param $questionId + * @param $correctItem + * @param $attributes + * @param $answer + * @param $listAnswersInfo + * @param $displayForStudent + * @return string + */ + public static function getFillTheBlankHtml($separatorStartRegexp, $separatorEndRegexp, $correctItemRegexp, $questionId, $correctItem, $attributes, $answer, $listAnswersInfo, $displayForStudent, $inBlankNumber) + { + $result = ""; + $inTabTeacherSolution = $listAnswersInfo['tabwords']; + $inTeacherSolution = $inTabTeacherSolution[$inBlankNumber]; + switch (self::getFillTheBlankAnswerType($inTeacherSolution)) { + case self::FILL_THE_BLANK_MENU: + $selected = ""; + // the blank menu + $optionMenu = ""; + // display a menu from answer separated with | + // if display for student, shuffle the correct answer menu + $listMenu = self::getFillTheBlankMenuAnswers($inTeacherSolution, $displayForStudent); + $result .= ''; + break; + case self::FILL_THE_BLANK_SEVERAL_ANSWER: + //no break + case self::FILL_THE_BLANK_STANDARD: + default: + $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 $correctAnswer + * @param $displayForStudent true if we want to shuffle the choices of the menu for students + * @return array + */ + public static function getFillTheBlankMenuAnswers($correctAnswer, $displayForStudent) + { + // if $inDisplayForStudent, then shuffle the result array + $listChoises = api_preg_split("/\|/", $correctAnswer); + if ($displayForStudent) { + shuffle($listChoises); + } + return $listChoises; + } + + + /** + * Return the array index of the student answer + * @param $correctAnswer the menu Choice1|Choice2|Choice3 + * @param $studentAnswer the student answer must be Choice1 or Choice2 or Choice3 + * @return int in the exemple 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; + } + } + return -1; // should not happened, because student choose the answer in a menu of possible answers + } + + + /** + * Return the possible answer if the answer between brackets is a multiple choice menu + * @param $correctAnswer + * @return array + */ + public static function getFillTheBlankSeveralAnswers($correctAnswer) + { + // is answer||Answer||response||Response , mean answer or Answer ... + $listSeveral = api_preg_split("/\|\|/", $correctAnswer); + return $listSeveral; + } + + /** + * 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 $studentAnswer the [studentanswer] of the info array of the answer field + * @param $correctAnswer the [tabwords] of the info array of the answer field + * @return bool + */ + public static function isGoodStudentAnswer($studentAnswer, $correctAnswer) + { + switch (self::getFillTheBlankAnswerType($correctAnswer)) { + case self::FILL_THE_BLANK_MENU: + $listMenu = self::getFillTheBlankMenuAnswers($correctAnswer, false); + return ($listMenu[0] == $studentAnswer); + break; + case self::FILL_THE_BLANK_SEVERAL_ANSWER: + // the answer must be one of the choice made + $listSeveral = self::getFillTheBlankSeveralAnswers($correctAnswer); + return (in_array($studentAnswer, $listSeveral)); + break; + case self::FILL_THE_BLANK_STANDARD: + default: + return ($studentAnswer == $correctAnswer); + } + } + + + public static function getFillTheBlankAnswerType($correctAnswer) + { + if (api_strpos($correctAnswer, "|") && !api_strpos($correctAnswer, "||")) { + return self::FILL_THE_BLANK_MENU; + } elseif (api_strpos($correctAnswer, "||")) { + return self::FILL_THE_BLANK_SEVERAL_ANSWER; + } else { + return self::FILL_THE_BLANK_STANDARD; + } + } + + /** + * Return information about the answer + * @param string $answer : the text of the answer of the question + * @param bool $inIsStudentAnswer : true if it is a student answer and not the empty question model + * @return array of information about the answer + */ + public static function getAnswerInfo($userAnswer = "", $isStudentAnswer = false) + { + $listAnswerResults = array(); + $listAnswerResults['text'] = ""; + $listAnswerResults['wordsCount'] = 0; + $listAnswerResults['tabwordsbracket'] = array(); + $listAnswerResults['tabwords'] = array(); + $listAnswerResults['tabweighting'] = array(); + $listAnswerResults['tabinputsize'] = array(); + $listAnswerResults['switchable'] = ""; + $listAnswerResults['studentanswer'] = array(); + $listAnswerResults['studentscore'] = array(); + $listAnswerResults['blankseparatornumber'] = 0; + + api_preg_match("/(.*)::(.*)$/s", $userAnswer, $listResult); + + if (count($listResult) < 2) { + $listDoubleColon[] = $listResult; + $listDoubleColon[] = ""; + } else { + $listDoubleColon[] = $listResult[1]; + $listDoubleColon[] = $listResult[2]; + } + + $listAnswerResults['systemstring'] = $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 = array(); + 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['tabweighting'] = $listWeightings; + $listAnswerResults['tabinputsize'] = $listSizeOfInput; + $listAnswerResults['switchable'] = $listArobaseSplit[1]; + $listAnswerResults['blankseparatorstart'] = self::getStartSeparator($blankSeparatorNumber); + $listAnswerResults['blankseparatorend'] = self::getEndSeparator($blankSeparatorNumber); + $listAnswerResults['blankseparatornumber'] = $blankSeparatorNumber; + + $blankCharStart = self::getStartSeparator($blankSeparatorNumber); + $blankCharEnd = self::getEndSeparator($blankSeparatorNumber); + $blankCharStartForRegexp = self::escapeForRegexp($blankCharStart); + $blankCharEndForRegexp = self::escapeForRegexp($blankCharEnd); + + // get all blanks words + $listAnswerResults['wordsCount'] = preg_match_all('/'.$blankCharStartForRegexp.'[^'.$blankCharEndForRegexp.']*'.$blankCharEndForRegexp.'/', $listDoubleColon[0], $listWords); + if ($listAnswerResults['wordsCount'] > 0) { + $listAnswerResults['tabwordsbracket'] = $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); + }, + array($blankCharStart, $blankCharEnd) + ); + $listAnswerResults['tabwords'] = $listWords[0]; + } + + // get all common words + $commonWords = 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 = array(); + $listWords = array(); + if ($isStudentAnswer) { + for ($i=0; $i < count($listAnswerResults['tabwords']); $i++) { + $listBrackets[] = $listAnswerResults['tabwordsbracket'][$i]; + $listWords[] = $listAnswerResults['tabwords'][$i]; + if ($i+1 < count($listAnswerResults['tabwords'])) { // should always be + $i++; + } + $listAnswerResults['studentanswer'][] = $listAnswerResults['tabwords'][$i]; + if ($i+1 < count($listAnswerResults['tabwords'])) { // should always be + $i++; + } + $listAnswerResults['studentscore'][] = $listAnswerResults['tabwords'][$i]; + } + $listAnswerResults['tabwords'] = $listWords; + $listAnswerResults['tabwordsbracket'] = $listBrackets; + + // if we are in student view, we've got 3 times :::::: for common words + $commonWords = preg_replace("/::::::/", "::", $commonWords); + } + $listAnswerResults['commonwords'] = explode("::", $commonWords); + + return $listAnswerResults; + } + + + /** + * Replace the occurence of blank word with [correct answer][student answer][answer is correct] + * @param array $listWithStudentAnswer + * @return string + */ + public static function getAnswerInStudentAttempt($listWithStudentAnswer) + { + + $separatorStart = $listWithStudentAnswer['blankseparatorstart']; + $separatorEnd = $listWithStudentAnswer['blankseparatorend']; + // lets rebuild the sentence with [correct answer][student answer][answer is correct] + $result = ""; + for ($i=0; $i < count($listWithStudentAnswer['commonwords']) - 1; $i++) { + $result .= $listWithStudentAnswer['commonwords'][$i]; + $result .= $listWithStudentAnswer['tabwordsbracket'][$i]; + $result .= $separatorStart.$listWithStudentAnswer['studentanswer'][$i].$separatorEnd; + $result .= $separatorStart.$listWithStudentAnswer['studentscore'][$i].$separatorEnd; + } + $result .= $listWithStudentAnswer['commonwords'][$i]; + $result .= "::"; + // add the system string + $result .= $listWithStudentAnswer['systemstring']; + return $result; + } + + /** + * This function is the same than the js one above getBlankSeparatorRegexp + * @param $inChar + * @return string + */ + public static function escapeForRegexp($inChar) + { + if (in_array($inChar, array(".", "+", "*", "?", "[", "^", "]", "$", "(", ")", "{", "}", "=", "!", ">", "|", ":", "-", ")"))) { + return "\\".$inChar; + } else { + return $inChar; + } + } + + /** + * return $text protected for use in regexp + * @param $text + * @return mixed + */ + public static function getRegexpProtected($text) + { + $listRegexpCharacters = array("/", ".", "+", "*", "?", "[", "^", "]", "$", "(", ")", "{", "}", "=", "!", ">", "|", ":", "-", ")"); + $result = $text; + for ($i=0; $i < count($listRegexpCharacters); $i++) { + $result = str_replace($listRegexpCharacters[$i], "\\".$listRegexpCharacters[$i], $result); + } + return $result; + } + + + /** + * This function must be the same than the js one getSeparatorFromNumber above + * @return array + */ + public static function getAllowedSeparator() + { + $fillBlanksAllowedSeparator = array( + array('[', ']'), + array('{', '}'), + array('(', ')'), + array('*', '*'), + array('#', '#'), + array('%', '%'), + array('$', '$'), + ); + return $fillBlanksAllowedSeparator; + } + + /** + * return the start separator for answer + * @param $number + * @return mixed + */ + public static function getStartSeparator($number) + { + $listSeparators = self::getAllowedSeparator(); + return $listSeparators[$number][0]; + } + + /** + * return the end separator for answer + * @param $number + * @return mixed + */ + public static function getEndSeparator($number) + { + $listSeparators = self::getAllowedSeparator(); + return $listSeparators[$number][1]; + } + + /** + * return as a desciption text, array of allowed separtors for question eg: array("[...]", "(...)") + * @return array + */ + public static function getAllowedSeparatorForSelect() + { + $listResults = array(); + $fillBlanksAllowedSeparator = self::getAllowedSeparator(); + for ($i=0; $i < count($fillBlanksAllowedSeparator); $i++) { + $listResults[] = $fillBlanksAllowedSeparator[$i][0]."...".$fillBlanksAllowedSeparator[$i][1]; + } + return $listResults; + } + + /** + * return the code number of the separator for the question + * @param $startSeparator + * @param $endSeparator + * @return int + */ + public function getDefaultSeparatorNumber($startSeparator, $endSeparator) + { + $listSeparators = self::getAllowedSeparator(); + $result = 0; + for ($i=0; $i < count($listSeparators); $i++) { + if ($listSeparators[$i][0] == $startSeparator && $listSeparators[$i][1] == $endSeparator) { + $result = $i; + } + } + return $result; + } + + /** + * return the HTML display of the answer + * @param $answer + * @return string + */ + public static function getHtmlDisplayForAnswer($answer, $resultsDisabled = false) + { + $result = ""; + $listStudentAnswerInfo = self::getAnswerInfo($answer, true); + + // rebluid the answer with good HTML style + // this is the student answer, right or wrong + for ($i=0; $i < count($listStudentAnswerInfo['studentanswer']); $i++) { + if ($listStudentAnswerInfo['studentscore'][$i] == 1) { + $listStudentAnswerInfo['studentanswer'][$i] = self::getHtmlRightAnswer($listStudentAnswerInfo['studentanswer'][$i], $listStudentAnswerInfo['tabwords'][$i], $resultsDisabled); + } else { + $listStudentAnswerInfo['studentanswer'][$i] = self::getHtmlWrongAnswer($listStudentAnswerInfo['studentanswer'][$i], $listStudentAnswerInfo['tabwords'][$i], $resultsDisabled); + } + } + + + // rebuild the sentence with student answer inserted + for ($i=0; $i < count($listStudentAnswerInfo['commonwords']); $i++) { + $result .= $listStudentAnswerInfo['commonwords'][$i]; + $result .= $listStudentAnswerInfo['studentanswer'][$i]; + } + + // the last common word (should be

) + $result .= $listStudentAnswerInfo['commonwords'][$i]; + return $result; + } + + + /** + * return the HTML code of answer for correct and wrong answer + * @param $answer + * @param $correct + * @param $right + * @param $resultsDisabled + * @return string + */ + public static function getHtmlAnswer($answer, $correct, $right, $resultsDisabled = false) + { + $style = "color: green"; + if (!$right) { + $style = "color: red; text-decoration: line-through;"; + } + $type = FillBlanks::getFillTheBlankAnswerType($correct); + switch ($type) { + case self::FILL_THE_BLANK_MENU: + $correctAnswerHtml = ""; + $listPossibleAnswers = FillBlanks::getFillTheBlankMenuAnswers($correct, false); + $correctAnswerHtml .= "".$listPossibleAnswers[0].""; + $correctAnswerHtml .= " ("; + for ($i=1; $i < count($listPossibleAnswers); $i++) { + $correctAnswerHtml .= $listPossibleAnswers[$i]; + if ($i != count($listPossibleAnswers) - 1) { + $correctAnswerHtml .= " | "; + } + } + $correctAnswerHtml .= ")"; + break; + case self::FILL_THE_BLANK_SEVERAL_ANSWER: + $listCorrects = explode("||", $correct); + $firstCorrect = $correct; + if (count($listCorrects) > 0) { + $firstCorrect = $listCorrects[0]; + } + $correctAnswerHtml = "".$firstCorrect.""; + break; + case self::FILL_THE_BLANK_STANDARD: + default: + $correctAnswerHtml = "".$correct.""; + } + + if ($resultsDisabled) { + $correctAnswerHtml = " - "; + } + + $result = ""; + $result .= "".$answer.""; + $result .= " / "; + $result .= $correctAnswerHtml; + $result .= ""; + return $result; + } + + /** + * return HTML code for correct answer + * @param $answer + * @param $correct + * @return string + */ + public static function getHtmlRightAnswer($answer, $correct, $resultsDisabled = false) + { + return self::getHtmlAnswer($answer, $correct, true, $resultsDisabled); + } + + /** + * return HTML code for wrong answer + * @param $answer + * @param $correct + * @return string + */ + public static function getHtmlWrongAnswer($answer, $correct, $resultsDisabled = false) + { + return self::getHtmlAnswer($answer, $correct, false, $resultsDisabled); + } } diff --git a/main/exercice/stats.php b/main/exercice/stats.php index 713347e0bf..c715646ceb 100755 --- a/main/exercice/stats.php +++ b/main/exercice/stats.php @@ -52,7 +52,8 @@ if (!empty($question_list)) { $question_id, $exercise_id, $courseCode, - $sessionId + $sessionId, + $question_obj->type ); $data[$question_id]['name'] = cut($question_obj->question, 100); diff --git a/main/inc/lib/exercise.lib.php b/main/inc/lib/exercise.lib.php index f2899490d3..07d5828035 100644 --- a/main/inc/lib/exercise.lib.php +++ b/main/inc/lib/exercise.lib.php @@ -548,170 +548,73 @@ HTML; $s .= ''; } elseif ($answerType == FILL_IN_BLANKS) { - /* - * In the FILL_IN_BLANKS test - * you mustn't have [ and ] in the textarea - * you mustn't have :: in the textarea - * the text to find mustn't be empty or contains only spaces - * the text to find mustn't contains HTML tags - * the text to find mustn't contains char " - */ + // display the question, with field empty, for student to fill it, + // or filled to display the answer in the Question preview of the exercice/admin.php page + $displayForStudent = true; + $listAnswerInformations = FillBlanks::getAnswerInfo($answer); + $separatorStartRegexp = FillBlanks::escapeForRegexp($listAnswerInformations['blankseparatorstart']); + $separatorEndRegexp = FillBlanks::escapeForRegexp($listAnswerInformations['blankseparatorend']); list($answer) = explode('::', $answer); - // $correct_answer_list array of array with correct anwsers 0=> [0=>[\p] 1=>[plop]] - api_preg_match_all( - '/\[[^]]+\]/', - $answer, - $correct_answer_list - ); + //Correct answers + $correctAnswerList = $listAnswerInformations['tabwords']; - // get student answer to display it if student go back to previous fillBlank answer question in a test + //Student's answer + $studentAnswerList = array(); if (isset($user_choice[0]['answer'])) { - api_preg_match_all( - '/\[[^]]+\]/', - $user_choice[0]['answer'], - $student_answer_list - ); - $student_answer_list_tobecleaned = $student_answer_list[0]; - $student_answer_list = array(); - // here we got the student answer in a test - // let's clean up the results - /* - Array - ( - [0] => Array - ( - [0] => [yer / ici] - [1] => [plop / /p] - ) - ) - */ - for ($i = 0; $i < count( - $student_answer_list_tobecleaned - ); $i++) { - $answer_corrected = $student_answer_list_tobecleaned[$i]; - /* - * we got if student answer is wrong - * [rrr / /p] - * or if student answer is good - * [plop / plop] - * or if student didn't answer [] - */ - $answer_corrected = api_preg_replace( - '| / .*$|', - '', - $answer_corrected - ); - /* - * we got [rrr or [plop or [ - */ - $answer_corrected = api_preg_replace( - '/^\[/', - '', - $answer_corrected - ); - /* - * we got rrr or plop - * non breakable spaces     from /main/exercice/exercise.class.php have been removed l 2391 and l 2370 - */ - $answer_corrected = api_preg_replace( - '|^|', - '', - $answer_corrected - ); - $answer_corrected = api_preg_replace( - '|$|', - '', - $answer_corrected - ); - $answer_corrected = '[' . $answer_corrected . ']'; - /* - * we got [rrr] or [plop] or [] - */ - $student_answer_list[] = $answer_corrected; - } + $arrayStudentAnswer = FillBlanks::getAnswerInfo($user_choice[0]['answer'], true); + $studentAnswerList = $arrayStudentAnswer['studentanswer']; } - // If display preview of answer in test view for exemple, set the student answer to the correct answers + // If the question must be shown with the answer (in page exercice/admin.php) for teacher preview + // set the student-answer to the correct answer if ($debug_mark_answer) { - // contain the rights answers surronded with brackets - $student_answer_list = $correct_answer_list[0]; + $studentAnswerList = $correctAnswerList; + $displayForStudent = false; } - /* - Split the response by bracket - tab_comments is an array with text surrounding the text to find - we add a space before and after the answer_question to be sure to - have a block of text before and after [xxx] patterns - so we have n text to find ([xxx]) and n+1 block of texts before, - between and after the text to find - */ - $tab_comments = api_preg_split( - '/\[[^]]+\]/', - ' ' . $answer . ' ' - ); - - if (!empty($correct_answer_list) && !empty($student_answer_list)) { + if (!empty($correctAnswerList) && !empty($studentAnswerList)) { $answer = ""; - - $i = 0; - foreach ($student_answer_list as $student_item) { - // remove surronding brackets - $student_response = api_substr( - $student_item, - 1, - api_strlen($student_item) - 2 - ); - - $size = strlen($student_item); - $attributes['class'] = self::detectInputAppropriateClass( - $size - ); - - $answer .= $tab_comments[$i] . - Display::input( - 'text', - "choice[$questionId][]", - $student_response, - $attributes - ); - $i++; + for ($i = 0; $i < count($listAnswerInformations["commonwords"]) - 1; $i++) { + // display the common word + $answer .= $listAnswerInformations["commonwords"][$i]; + // display the blank word + $correctItem = $listAnswerInformations["tabwords"][$i]; + $correctItemRegexp = $correctItem; + // replace / with \/ to allow the preg_replace bellow and all the regexp char + $correctItemRegexp = FillBlanks::getRegexpProtected($correctItemRegexp); + if (isset($studentAnswerList[$i])) { + // If student already started this test and answered this question, + // fill the blank with his previous answers + // may be "" if student viewed the question, but did not fill the blanks + $correctItem = $studentAnswerList[$i]; + } + $attributes["style"] = "width:".$listAnswerInformations["tabinputsize"][$i]."px"; + $answer .= FillBlanks::getFillTheBlankHtml($separatorStartRegexp, $separatorEndRegexp, $correctItemRegexp, $questionId, $correctItem, $attributes, $answer, $listAnswerInformations, $displayForStudent, $i); } - $answer .= $tab_comments[$i]; + // display the last common word + $answer .= $listAnswerInformations["commonwords"][$i]; } else { - // display exercise with empty input fields - // every [xxx] are replaced with an empty input field - foreach ($correct_answer_list[0] as $item) { - $size = strlen($item); - $attributes['class'] = self::detectInputAppropriateClass( - $size - ); - - $attributes['class'] .= ' block_on_enter'; - - $answer = str_replace( - $item, - Display::input( - 'text', - "choice[$questionId][]", - '', - $attributes - ), - $answer - ); + // display empty [input] with the right width for student to fill it + $separatorStartRegexp = FillBlanks::escapeForRegexp($listAnswerInformations['blankseparatorstart']); + $separatorEndRegexp = FillBlanks::escapeForRegexp($listAnswerInformations['blankseparatorend']); + $answer = ""; + for ($i = 0; $i < count($listAnswerInformations["commonwords"]) - 1; $i++) { + // display the common words + $answer .= $listAnswerInformations["commonwords"][$i]; + // display the blank word + $attributes["style"] = "width:".$listAnswerInformations["tabinputsize"][$i]."px"; + $correctItem = $listAnswerInformations["tabwords"][$i]; + $correctItemRegexp = $correctItem; + // replace / with \/ to allow the preg_replace bellow and all the regexp char + $correctItemRegexp = FillBlanks::getRegexpProtected($correctItemRegexp); + $answer .= FillBlanks::getFillTheBlankHtml($separatorStartRegexp, $separatorEndRegexp, $correctItemRegexp, $questionId, '', $attributes, $answer, $listAnswerInformations, $displayForStudent, $i); } - /*$answer = api_preg_replace( - '/\[[^]]+\]/', - Display::input( - 'text', - "choice[$questionId][]", - '', - $attributes - ), $answer);*/ + // display the last common word + $answer .= $listAnswerInformations["commonwords"][$i]; } - $html = '
'.$answer.'
'; - $s .= $html ; + $s .= $answer; } elseif ($answerType == CALCULATED_ANSWER) { /* @@ -3047,13 +2950,15 @@ HTML; * @param int $exercise_id * @param string $course_code * @param int $session_id + * @param string $questionType * @return int */ public static function get_number_students_question_with_answer_count( $question_id, $exercise_id, $course_code, - $session_id + $session_id, + $questionType = '' ) { $track_exercises = Database::get_main_table( TABLE_STATISTIC_TRACK_E_EXERCISES @@ -3072,6 +2977,27 @@ HTML; $courseId = api_get_course_int_id($course_code); $session_id = intval($session_id); + if ($questionType == FILL_IN_BLANKS) { + $listStudentsId = array(); + $listAllStudentInfo = CourseManager::get_student_list_from_course_code( + api_get_course_id(), + true + ); + foreach ($listAllStudentInfo as $i => $listStudentInfo) { + $listStudentsId[] = $listStudentInfo['user_id']; + } + + $listFillTheBlankResult = getFillTheBlankTabResult( + $exercise_id, + $question_id, + $listStudentsId, + '1970-01-01', + '3000-01-01' + ); + + return getNbResultFillBlankAll($listFillTheBlankResult); + } + if (empty($session_id)) { $courseCondition = " INNER JOIN $courseUser cu diff --git a/main/inc/lib/exercise_show_functions.lib.php b/main/inc/lib/exercise_show_functions.lib.php index f7c50af6de..91a8ce52b9 100755 --- a/main/inc/lib/exercise_show_functions.lib.php +++ b/main/inc/lib/exercise_show_functions.lib.php @@ -22,31 +22,33 @@ class ExerciseShowFunctions * @param string Answer text * @param int Exercise ID * @param int Question ID + * @param int $resultsDisabled * @return void */ - static function display_fill_in_blanks_answer($feedback_type, $answer, $id, $questionId) + static function display_fill_in_blanks_answer($feedbackType, $answer, $id, $questionId, $resultsDisabled) { + $answerHTML = FillBlanks::getHtmlDisplayForAnswer($answer, $resultsDisabled); if (empty($id)) { - echo '
'; + echo ''; } else { - ?> - + ?> + - - - + + + -
'.get_lang("Answer").'
'. Security::remove_XSS($answer).'
'; + echo Security::remove_XSS($answerHTML, COURSEMANAGERLOWSECURITY); + echo '
- + - - + +