* @author Hubert Borderiou 2011-10-21
* @author ivantcholakov2009-07-20
*
*/
class ExerciseLib
{
/**
* Shows a question
*
* @param int $questionId question id
* @param bool $only_questions if true only show the questions, no exercise title
* @param bool $origin i.e = learnpath
* @param string $current_item current item from the list of questions
* @param bool $show_title
* @param bool $freeze
* @param array $user_choice
* @param bool $show_comment
* @param bool $exercise_feedback
* @param bool $show_answers
* */
public static function showQuestion(
$questionId,
$only_questions = false,
$origin = false,
$current_item = '',
$show_title = true,
$freeze = false,
$user_choice = array(),
$show_comment = false,
$exercise_feedback = null,
$show_answers = false
) {
// Change false to true in the following line to enable answer hinting
$debug_mark_answer = $show_answers;
// Reads question information
if (!$objQuestionTmp = Question::read($questionId)) {
// Question not found
return false;
}
if ($exercise_feedback != EXERCISE_FEEDBACK_TYPE_END) {
$show_comment = false;
}
$answerType = $objQuestionTmp->selectType();
$pictureName = $objQuestionTmp->selectPicture();
$s = '';
if ($answerType != HOT_SPOT && $answerType != HOT_SPOT_DELINEATION) {
// Question is not a hotspot
if (!$only_questions) {
$questionDescription = $objQuestionTmp->selectDescription();
if ($show_title) {
TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
echo Display::div(
$current_item . '. ' . $objQuestionTmp->selectTitle(),
array('class' => 'question_title')
);
}
if (!empty($questionDescription)) {
echo Display::div(
$questionDescription,
array('class' => 'question_description')
);
}
}
if (in_array($answerType, array(FREE_ANSWER, ORAL_EXPRESSION)) &&
$freeze
) {
return '';
}
echo '
';
// construction of the Answer object (also gets all answers details)
$objAnswerTmp = new Answer($questionId);
$nbrAnswers = $objAnswerTmp->selectNbrAnswers();
$course_id = api_get_course_int_id();
$quiz_question_options = Question::readQuestionOption(
$questionId,
$course_id
);
// For "matching" type here, we need something a little bit special
// because the match between the suggestions and the answers cannot be
// done easily (suggestions and answers are in the same table), so we
// have to go through answers first (elems with "correct" value to 0).
$select_items = array();
//This will contain the number of answers on the left side. We call them
// suggestions here, for the sake of comprehensions, while the ones
// on the right side are called answers
$num_suggestions = 0;
if (in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE])) {
if ($answerType == DRAGGABLE) {
$s .= '
';
} else {
$s .= <<
HTML;
}
// Iterate through answers
$x = 1;
//mark letters for each answer
$letter = 'A';
$answer_matching = array();
$cpt1 = array();
for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
$answerCorrect = $objAnswerTmp->isCorrect($answerId);
$numAnswer = $objAnswerTmp->selectAutoId($answerId);
$answer = $objAnswerTmp->selectAnswer($answerId);
if ($answerCorrect == 0) {
// options (A, B, C, ...) that will be put into the list-box
// have the "correct" field set to 0 because they are answer
$cpt1[$x] = $letter;
$answer_matching[$x] = $objAnswerTmp->selectAnswerByAutoId(
$numAnswer
);
$x++;
$letter++;
}
}
$i = 1;
$select_items[0]['id'] = 0;
$select_items[0]['letter'] = '--';
$select_items[0]['answer'] = '';
foreach ($answer_matching as $id => $value) {
$select_items[$i]['id'] = $value['id'];
$select_items[$i]['letter'] = $cpt1[$id];
$select_items[$i]['answer'] = $value['answer'];
$i++;
}
$user_choice_array_position = array();
if (!empty($user_choice)) {
foreach ($user_choice as $item) {
$user_choice_array_position[$item['position']] = $item['answer'];
}
}
$num_suggestions = ($nbrAnswers - $x) + 1;
} elseif ($answerType == FREE_ANSWER) {
$fck_content = isset($user_choice[0]) && !empty($user_choice[0]['answer']) ? $user_choice[0]['answer'] : null;
$form = new FormValidator('free_choice_'.$questionId);
$config = array(
'ToolbarSet' => 'TestFreeAnswer'
);
$form->addHtmlEditor("choice[" . $questionId . "]", null, false, false, $config);
$form->setDefaults(array("choice[" . $questionId . "]" => $fck_content));
$s .= $form->returnForm();
} elseif ($answerType == ORAL_EXPRESSION) {
// Add nanog
if (api_get_setting('enable_nanogong') == 'true') {
//@todo pass this as a parameter
global $exercise_stat_info, $exerciseId, $exe_id;
if (!empty($exercise_stat_info)) {
$params = array(
'exercise_id' => $exercise_stat_info['exe_exo_id'],
'exe_id' => $exercise_stat_info['exe_id'],
'question_id' => $questionId
);
} else {
$params = array(
'exercise_id' => $exerciseId,
'exe_id' => 'temp_exe',
'question_id' => $questionId
);
}
$nano = new Nanogong($params);
echo $nano->show_button();
}
$form = new FormValidator('free_choice_'.$questionId);
$config = array(
'ToolbarSet' => 'TestFreeAnswer'
);
$form->addHtmlEditor("choice[" . $questionId . "]", null, false, false, $config);
//$form->setDefaults(array("choice[" . $questionId . "]" => $fck_content));
$s .= $form->return_form();
}
// Now navigate through the possible answers, using the max number of
// answers for the question as a limiter
$lines_count = 1; // a counter for matching-type answers
if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE ||
$answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE
) {
$header = Display::tag('th', get_lang('Options'));
foreach ($objQuestionTmp->options as $item) {
if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE) {
if (in_array($item, $objQuestionTmp->options)) {
$header .= Display::tag('th', get_lang($item));
} else {
$header .= Display::tag('th', $item);
}
} else {
$header .= Display::tag('th', $item);
}
}
if ($show_comment) {
$header .= Display::tag('th', get_lang('Feedback'));
}
$s .= '
';
} elseif ($answerType == FILL_IN_BLANKS) {
// 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 answers
$correctAnswerList = $listAnswerInformations['tabwords'];
//Student's answer
$studentAnswerList = array();
if (isset($user_choice[0]['answer'])) {
$arrayStudentAnswer = FillBlanks::getAnswerInfo($user_choice[0]['answer'], true);
$studentAnswerList = $arrayStudentAnswer['studentanswer'];
}
// 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) {
$studentAnswerList = $correctAnswerList;
$displayForStudent = false;
}
if (!empty($correctAnswerList) && !empty($studentAnswerList)) {
$answer = "";
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);
}
// display the last common word
$answer .= $listAnswerInformations["commonwords"][$i];
} else {
// 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);
}
// display the last common word
$answer .= $listAnswerInformations["commonwords"][$i];
}
$s .= $answer;
} elseif ($answerType == CALCULATED_ANSWER) {
/*
* In the CALCULATED_ANSWER 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 "
*/
if ($origin !== null) {
global $exe_id;
$trackAttempts = Database::get_main_table(
TABLE_STATISTIC_TRACK_E_ATTEMPT
);
$sqlTrackAttempt = 'SELECT answer FROM ' . $trackAttempts . ' WHERE exe_id=' . $exe_id . ' AND question_id=' . $questionId;
$rsLastAttempt = Database::query($sqlTrackAttempt);
$rowLastAttempt = Database::fetch_array($rsLastAttempt);
$answer = $rowLastAttempt['answer'];
if (empty($answer)) {
$_SESSION['calculatedAnswerId'][$questionId] = mt_rand(
1,
$nbrAnswers
);
$answer = $objAnswerTmp->selectAnswer(
$_SESSION['calculatedAnswerId'][$questionId]
);
}
}
list($answer) = explode('@@', $answer);
// $correctAnswerList array of array with correct anwsers 0=> [0=>[\p] 1=>[plop]]
api_preg_match_all(
'/\[[^]]+\]/',
$answer,
$correctAnswerList
);
// get student answer to display it if student go back to previous calculated answer question in a test
if (isset($user_choice[0]['answer'])) {
api_preg_match_all(
'/\[[^]]+\]/',
$answer,
$studentAnswerList
);
$studentAnswerListTobecleaned = $studentAnswerList[0];
$studentAnswerList = array();
for ($i = 0; $i < count(
$studentAnswerListTobecleaned
); $i++) {
$answerCorrected = $studentAnswerListTobecleaned[$i];
$answerCorrected = api_preg_replace(
'| / .*$|',
'',
$answerCorrected
);
$answerCorrected = api_preg_replace(
'/^\[/',
'',
$answerCorrected
);
$answerCorrected = api_preg_replace(
'|^|',
'',
$answerCorrected
);
$answerCorrected = api_preg_replace(
'|$|',
'',
$answerCorrected
);
$answerCorrected = '[' . $answerCorrected . ']';
$studentAnswerList[] = $answerCorrected;
}
}
// If display preview of answer in test view for exemple, set the student answer to the correct answers
if ($debug_mark_answer) {
// contain the rights answers surronded with brackets
$studentAnswerList = $correctAnswerList[0];
}
/*
Split the response by bracket
tabComments is an array with text surrounding the text to find
we add a space before and after the answerQuestion 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
*/
$tabComments = api_preg_split(
'/\[[^]]+\]/',
' ' . $answer . ' '
);
if (!empty($correctAnswerList) && !empty($studentAnswerList)) {
$answer = "";
$i = 0;
foreach ($studentAnswerList as $studentItem) {
// remove surronding brackets
$studentResponse = api_substr(
$studentItem,
1,
api_strlen($studentItem) - 2
);
$size = strlen($studentItem);
$attributes['class'] = self::detectInputAppropriateClass(
$size
);
$answer .= $tabComments[$i] .
Display::input(
'text',
"choice[$questionId][]",
$studentResponse,
$attributes
);
$i++;
}
$answer .= $tabComments[$i];
} else {
// display exercise with empty input fields
// every [xxx] are replaced with an empty input field
foreach ($correctAnswerList[0] as $item) {
$size = strlen($item);
$attributes['class'] = self::detectInputAppropriateClass(
$size
);
$answer = str_replace(
$item,
Display::input(
'text',
"choice[$questionId][]",
'',
$attributes
),
$answer
);
}
}
if ($origin !== null) {
$s = $answer;
break;
} else {
$s .= $answer;
}
} elseif ($answerType == MATCHING) {
// matching type, showing suggestions and answers
// TODO: replace $answerId by $numAnswer
if ($answerCorrect != 0) {
// only show elements to be answered (not the contents of
// the select boxes, who are corrrect = 0)
$s .= '
';
$parsed_answer = $answer;
//left part questions
$s .= '
' . $lines_count . '. ' . $parsed_answer . '
';
//middle part (matches selects)
$s .= '
';
$s .= '
';
if (isset($select_items[$lines_count])) {
$s .= '
';
$lines_count++;
//if the left side of the "matching" has been completely
// shown but the right side still has values to show...
if (($lines_count - 1) == $num_suggestions) {
// if it remains answers to shown at the right side
while (isset($select_items[$lines_count])) {
$s .= '