From 8dfc932016af59bbcf4026fd4386c23a5d2eddf0 Mon Sep 17 00:00:00 2001 From: Julio Montoya Date: Fri, 6 Nov 2020 08:55:36 +0100 Subject: [PATCH] Exercises: allow_time_per_question requires DB change to save spent time BT#17791 --- main/exercise/exercise.class.php | 90 ++- main/exercise/exercise_submit.php | 1099 ++++++++++++++------------- main/inc/ajax/exercise.ajax.php | 48 +- main/inc/lib/events.lib.php | 315 ++++---- main/install/configuration.dist.php | 4 +- 5 files changed, 856 insertions(+), 700 deletions(-) diff --git a/main/exercise/exercise.class.php b/main/exercise/exercise.class.php index 12adbf32b0..d59363d139 100755 --- a/main/exercise/exercise.class.php +++ b/main/exercise/exercise.class.php @@ -3577,6 +3577,7 @@ class Exercise * @param bool $showTotalScoreAndUserChoicesInLastAttempt * @param bool $updateResults * @param bool $showHotSpotDelineationTable + * @param int $questionDuration seconds * * @todo reduce parameters of this function * @@ -3595,7 +3596,8 @@ class Exercise $hotspot_delineation_result = [], $showTotalScoreAndUserChoicesInLastAttempt = true, $updateResults = false, - $showHotSpotDelineationTable = false + $showHotSpotDelineationTable = false, + $questionDuration = 0 ) { $debug = false; //needed in order to use in the exercise_attempt() for the time @@ -3604,6 +3606,7 @@ class Exercise $em = Database::getManager(); $feedback_type = $this->getFeedbackType(); $results_disabled = $this->selectResultsDisabled(); + $questionDuration = (int) $questionDuration; if ($debug) { error_log("<------ manage_answer ------> "); @@ -5694,10 +5697,6 @@ class Exercise '; if ($next == 0) { - /*$try = $try_hotspot; - $lp = $lp_hotspot; - $destinationid = $select_question_hotspot; - $url = $url_hotspot;*/ } else { $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers); $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers); @@ -5862,7 +5861,8 @@ class Exercise $exeId, $i, $this->id, - $updateResults + $updateResults, + $questionDuration ); } } else { @@ -5873,7 +5873,8 @@ class Exercise $exeId, $i, $this->id, - $updateResults + $updateResults, + $questionDuration ); } if ($debug) { @@ -5887,7 +5888,9 @@ class Exercise $quesId, $exeId, 0, - $this->id + $this->id, + false, + $questionDuration ); } } elseif ($answerType == MULTIPLE_ANSWER || $answerType == GLOBAL_MULTIPLE_ANSWER) { @@ -5895,17 +5898,44 @@ class Exercise $reply = array_keys($choice); for ($i = 0; $i < count($reply); $i++) { $ans = $reply[$i]; - Event::saveQuestionAttempt($questionScore, $ans, $quesId, $exeId, $i, $this->id); + Event::saveQuestionAttempt( + $questionScore, + $ans, + $quesId, + $exeId, + $i, + $this->id, + false, + $questionDuration + ); } } else { - Event::saveQuestionAttempt($questionScore, 0, $quesId, $exeId, 0, $this->id); + Event::saveQuestionAttempt( + $questionScore, + 0, + $quesId, + $exeId, + 0, + $this->id, + false, + $questionDuration + ); } } elseif ($answerType == MULTIPLE_ANSWER_COMBINATION) { if ($choice != 0) { $reply = array_keys($choice); for ($i = 0; $i < count($reply); $i++) { $ans = $reply[$i]; - Event::saveQuestionAttempt($questionScore, $ans, $quesId, $exeId, $i, $this->id); + Event::saveQuestionAttempt( + $questionScore, + $ans, + $quesId, + $exeId, + $i, + $this->id, + false, + $questionDuration + ); } } else { Event::saveQuestionAttempt( @@ -5914,7 +5944,9 @@ class Exercise $quesId, $exeId, 0, - $this->id + $this->id, + false, + $questionDuration ); } } elseif (in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE])) { @@ -5926,7 +5958,9 @@ class Exercise $quesId, $exeId, $j, - $this->id + $this->id, + false, + $questionDuration ); } } @@ -5938,7 +5972,9 @@ class Exercise $quesId, $exeId, 0, - $this->id + $this->id, + false, + $questionDuration ); } elseif ($answerType == ORAL_EXPRESSION) { $answer = $choice; @@ -5950,6 +5986,7 @@ class Exercise 0, $this->id, false, + $questionDuration, $objQuestionTmp->getAbsoluteFilePath() ); } elseif ( @@ -5959,7 +5996,7 @@ class Exercise ) ) { $answer = $choice; - Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0, $this->id); + Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0, $this->id, false, $questionDuration); } elseif ($answerType == HOT_SPOT || $answerType == ANNOTATION) { $answer = []; if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) { @@ -5998,9 +6035,27 @@ class Exercise error_log('Empty: exerciseResultCoordinates'); } } - Event::saveQuestionAttempt($questionScore, implode('|', $answer), $quesId, $exeId, 0, $this->id); + Event::saveQuestionAttempt( + $questionScore, + implode('|', $answer), + $quesId, + $exeId, + 0, + $this->id, + false, + $questionDuration + ); } else { - Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0, $this->id); + Event::saveQuestionAttempt( + $questionScore, + $answer, + $quesId, + $exeId, + 0, + $this->id, + false, + $questionDuration + ); } } @@ -9952,6 +10007,7 @@ class Exercise Session::erase('exerciseResult'); Session::erase('firstTime'); Session::erase('time_per_question'); + Session::erase('question_start'); Session::erase('exerciseResultCoordinates'); Session::erase('hotspot_coord'); Session::erase('hotspot_dest'); diff --git a/main/exercise/exercise_submit.php b/main/exercise/exercise_submit.php index c098311377..514072b900 100755 --- a/main/exercise/exercise_submit.php +++ b/main/exercise/exercise_submit.php @@ -62,8 +62,6 @@ $htmlHeadXtra[] = $js; $htmlHeadXtra[] = api_get_js('jqueryui-touch-punch/jquery.ui.touch-punch.min.js'); $htmlHeadXtra[] = api_get_js('jquery.jsPlumb.all.js'); $htmlHeadXtra[] = api_get_js('d3/jquery.xcolor.js'); - -// This library is necessary for the time control feature. $htmlHeadXtra[] = api_get_css(api_get_path(WEB_LIBRARY_PATH).'javascript/epiclock/renderers/minute/epiclock.minute.css'); $htmlHeadXtra[] = api_get_js('epiclock/javascript/jquery.dateformat.min.js'); $htmlHeadXtra[] = api_get_js('epiclock/javascript/jquery.epiclock.min.js'); @@ -109,8 +107,7 @@ if (isset($zoomOptions['options']) && !in_array($origin, ['embeddable', 'mobilea $(document).contextmenu(function() { return false; - }) - + }); }); '; } @@ -130,7 +127,7 @@ $exerciseResultCoordinates = isset($_REQUEST['exerciseResultCoordinates']) ? $_R $choice = isset($_REQUEST['choice']) ? $_REQUEST['choice'] : null; $choice = empty($choice) ? isset($_REQUEST['choice2']) ? $_REQUEST['choice2'] : null : null; $questionCategoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : 0; -$current_question = isset($_REQUEST['num']) ? (int) $_REQUEST['num'] : null; +$current_question = $currentQuestionFromUrl = isset($_REQUEST['num']) ? (int) $_REQUEST['num'] : null; $currentAnswer = isset($_REQUEST['num_answer']) ? (int) $_REQUEST['num_answer'] : null; $logInfo = [ @@ -189,13 +186,13 @@ if (!isset($objExercise) && isset($exerciseInSession)) { $exerciseInSession = Session::read('objExercise'); -//3. $objExercise is not set, then return to the exercise list. +// 3. $objExercise is not set, then return to the exercise list. if (!is_object($objExercise)) { header('Location: exercise.php'); exit; } -// if the user has submitted the form +// if the user has submitted the form. $exercise_title = $objExercise->selectTitle(); $exercise_sound = $objExercise->selectSound(); @@ -307,7 +304,6 @@ if ($objExercise->selectAttempts() > 0) { ); $attempt_html .= $messageReachedMax; - if (!empty($last_attempt_info['question_list'])) { foreach ($last_attempt_info['question_list'] as $questions) { foreach ($questions as $question_data) { @@ -495,10 +491,6 @@ if (!empty($exercise_stat_info['questions_to_check'])) { } $params = "exe_id=$exe_id&exerciseId=$exerciseId&learnpath_id=$learnpath_id&learnpath_item_id=$learnpath_item_id&learnpath_item_view_id=$learnpath_item_view_id&".api_get_cidreq().'&reminder='.$reminder; -if ($debug) { - error_log("6.1 params: -> $params"); -} - if (2 == $reminder && empty($myRemindList)) { if ($debug) { error_log('6.2 calling the exercise_reminder.php'); @@ -522,7 +514,7 @@ if ($time_control) { } if (!isset($_SESSION['expired_time'][$current_expired_time_key])) { - //Timer - Get expired_time for a student + // Timer - Get expired_time for a student. if (!empty($exercise_stat_info)) { $expired_time_of_this_attempt = $exercise_stat_info['expired_time_control']; if ($debug) { @@ -585,7 +577,8 @@ $time_left = api_strtotime($clock_expired_time, 'UTC') - time(); * The time control feature is enable here - this feature is enable for a jquery plugin called epiclock * for more details of how it works see this link : http://eric.garside.name/docs.html?p=epiclock */ -if ($time_control) { //Sends the exercise form when the expired time is finished +if ($time_control) { + //Sends the exercise form when the expired time is finished. $htmlHeadXtra[] = $objExercise->showTimeControlJS($time_left); } @@ -604,8 +597,7 @@ if (!isset($_SESSION['questionList'])) { } $isLastQuestionInCategory = 0; -if ( - api_get_configuration_value('block_category_questions') && +if (api_get_configuration_value('block_category_questions') && ONE_PER_PAGE == $objExercise->type && EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM == $selectionType ) { @@ -659,7 +651,6 @@ if ( $count = 1; $total = count($categoryList[$categoryId]); - //var_dump($categoryList[$categoryId]); foreach ($categoryList[$categoryId] as $checkQuestionId) { if ((int) $checkQuestionId === $questionCheck->iid) { break; @@ -670,14 +661,11 @@ if ( if ($count === $total) { $isLastQuestionInCategory = $categoryId; } - - //var_dump($isLastQuestionInCategory, $blockedCategories); if (0 === $isLastQuestionInCategory) { $showPreviousButton = false; } } - //var_dump($categoryInfo, $count, $total); // Blocked if category was already answered if ($categoryId && in_array($categoryId, $blockedCategories)) { // Redirect to category intro @@ -928,13 +916,98 @@ if (api_is_in_gradebook()) { ]; } -$interbreadcrumb[] = [ - 'url' => 'exercise.php?'.api_get_cidreq(), - 'name' => get_lang('Exercises'), -]; +$interbreadcrumb[] = ['url' => 'exercise.php?'.api_get_cidreq(), 'name' => get_lang('Exercises')]; $interbreadcrumb[] = ['url' => '#', 'name' => $objExercise->selectTitle(true)]; -if (!in_array($origin, ['learnpath', 'embeddable', 'mobileapp'])) { //so we are not in learnpath tool +// Time per question. +$questionTimeCondition = ''; +if ($allowTimePerQuestion && $objExercise->type == ONE_PER_PAGE) { + $objQuestionTmp = null; + $previousQuestion = null; + if (!empty($questionList)) { + $i = 0; + foreach ($questionList as $questionId) { + $i++; + $objQuestionTmp = Question::read($questionId); + // if it is not the right question, goes to the next loop iteration + if ($current_question == $i) { + break; + } + $previousQuestion = $objQuestionTmp; + } + } + + $extraFieldValue = new ExtraFieldValue('question'); + $value = $extraFieldValue->get_values_by_handler_and_field_variable($objQuestionTmp->iid, 'time'); + if (!empty($value) && isset($value['value']) && !empty($value['value'])) { + $seconds = (int) $value['value']; + $now = time(); + $loadedQuestions = array_keys(Session::read('question_start', [])); + if (!empty($loadedQuestions)) { + if (!in_array($objQuestionTmp->iid, $loadedQuestions)) { + api_not_allowed(true); + } + } + + $timeSpent = Event::getAttemptQuestionDuration($exe_id, $objQuestionTmp->iid); + $loadedQuestions = array_keys(Session::read('question_start')); + // Block if another question is loaded at the same time. + if (!in_array($objQuestionTmp->iid, $loadedQuestions)) { + api_not_allowed(true); + } + /*$timePerQuestion = Session::read('time_per_question', []); + if (!empty($timePerQuestion) && isset($timePerQuestion[$objQuestionTmp->iid])) { + $time = $timePerQuestion[$objQuestionTmp->iid]; + } else { + $time = $timePerQuestion[$objQuestionTmp->iid] = $now; + Session::write('time_per_question', $timePerQuestion); + } + $timeSpent = $now - $time;*/ + + /*if (!empty($questionAttempt) && isset($questionAttempt['tms'])) { + var_dump('from DB'); + var_dump($questionAttempt['tms']); + $time = api_strtotime($questionAttempt['tms'], 'UTC'); + $timeSpent = $now - $time; + } else { + var_dump('from session'); + $timePerQuestion = Session::read('time_per_question', []); + if (!empty($timePerQuestion) && isset($timePerQuestion[$objQuestionTmp->iid])) { + $time = $timePerQuestion[$objQuestionTmp->iid]; + } else { + $time = $timePerQuestion[$objQuestionTmp->iid] = $now; + Session::write('time_per_question', $timePerQuestion); + } + var_dump($timePerQuestion); + $timeSpent = $now - $time; + }*/ + //var_dump($timeSpent); + //var_dump(api_get_utc_datetime($now).' - '.api_get_utc_datetime($time)); + // Redirect to next question. + if ($timeSpent > $seconds) { + $nextQuestion = (int) $currentQuestionFromUrl + 1; + $nextQuestionUrl = api_get_path(WEB_CODE_PATH). + "exercise/exercise_submit.php?$params&num=$nextQuestion&remind_question_id=$remind_question_id"; + api_location($nextQuestionUrl); + } + + $seconds = $seconds - $timeSpent; + //var_dump($seconds); + $questionTimeCondition = " + var timer = new easytimer.Timer(); + timer.start({countdown: true, startValues: {seconds: $seconds}}); + timer.addEventListener('secondsUpdated', function (e) { + $('#question_timer').html(timer.getTimeValues().toString()); + }); + timer.addEventListener('targetAchieved', function (e) { + $('.question-validate-btn').click(); + }); + "; + } +} + +if (!in_array($origin, ['learnpath', 'embeddable', 'mobileapp'])) { + //so we are not in learnpath tool SessionManager::addFlashSessionReadOnly(); Display::display_header(null, 'Exercises'); } else { @@ -950,7 +1023,6 @@ if ($origin === 'mobileapp') { } $show_quiz_edition = $objExercise->added_in_lp(); - // I'm in a preview mode if (api_is_course_admin() && !in_array($origin, ['learnpath', 'embeddable'])) { echo '
'; @@ -987,7 +1059,6 @@ if (!api_is_allowed_to_session_edit()) { $exercise_timeover = false; $limit_time_exists = !empty($objExercise->start_time) || !empty($objExercise->end_time) ? true : false; - if ($limit_time_exists) { $exercise_start_time = api_strtotime($objExercise->start_time, 'UTC'); $exercise_end_time = api_strtotime($objExercise->end_time, 'UTC'); @@ -1116,595 +1187,577 @@ if ($reminder == 2) { } } +if (!empty($error)) { + Display::addFlash(Display::return_message($error, 'error', false)); + api_not_allowed(); + exit; +} + +$script_php = 'exercise_result.php'; if ($objExercise->review_answers) { $script_php = 'exercise_reminder.php'; -} else { - $script_php = 'exercise_result.php'; } -if (!empty($error)) { - echo Display::return_message($error, 'error', false); -} else { - if (!empty($exercise_sound)) { - echo ""; - echo ", get_lang('Sound')."; - } - // Get number of hotspot questions for javascript validation - $number_of_hotspot_questions = 0; - $onsubmit = ''; - $i = 0; - if (!empty($questionList)) { - foreach ($questionList as $questionId) { - $i++; - $objQuestionTmp = Question::read($questionId); - $selectType = $objQuestionTmp->selectType(); - // for sequential exercises. - if ($objExercise->type == ONE_PER_PAGE) { - // if it is not the right question, goes to the next loop iteration - if ($current_question != $i) { - continue; - } else { - if ($selectType == HOT_SPOT || $selectType == HOT_SPOT_DELINEATION) { - $number_of_hotspot_questions++; - } - break; - } +if (!empty($exercise_sound)) { + echo ""; + echo ", get_lang('Sound')."; +} +// Get number of hotspot questions for javascript validation +$number_of_hotspot_questions = 0; +$i = 0; +if (!empty($questionList)) { + foreach ($questionList as $questionId) { + $i++; + $objQuestionTmp = Question::read($questionId); + $selectType = $objQuestionTmp->selectType(); + // for sequential exercises. + if ($objExercise->type == ONE_PER_PAGE) { + // if it is not the right question, goes to the next loop iteration + if ($current_question != $i) { + continue; } else { if ($selectType == HOT_SPOT || $selectType == HOT_SPOT_DELINEATION) { $number_of_hotspot_questions++; } + break; + } + } else { + if ($selectType == HOT_SPOT || $selectType == HOT_SPOT_DELINEATION) { + $number_of_hotspot_questions++; } } } +} - $saveIcon = Display::return_icon( - 'save.png', - get_lang('Saved'), - [], - ICON_SIZE_SMALL, - false, - true - ); +$saveIcon = Display::return_icon( + 'save.png', + get_lang('Saved'), + [], + ICON_SIZE_SMALL, + false, + true +); - $questionTimeCondition = ''; - if ($allowTimePerQuestion && $objExercise->type == ONE_PER_PAGE) { - $extraFieldValue = new ExtraFieldValue('question'); - $value = $extraFieldValue->get_values_by_handler_and_field_variable($objQuestionTmp->iid, 'time'); - if (!empty($value) && isset($value['value']) && !empty($value['value'])) { - $seconds = (int) $value['value']; - $questionTimeCondition = " - var timer = new easytimer.Timer(); - //timer.start(); - timer.start({countdown: true, startValues: {seconds: $seconds}}); - timer.addEventListener('secondsUpdated', function (e) { - $('#question_timer').html(timer.getTimeValues().toString()); - }); - timer.addEventListener('targetAchieved', function (e) { - $('.question-validate-btn').click(); - }); - "; - } - } - echo ''; - - echo '
- - - - - - - - - - '; - - // Show list of questions - $i = 1; - $attempt_list = []; - if (isset($exe_id)) { - $attempt_list = Event::getAllExerciseEventByExeId($exe_id); - } - - $remind_list = []; - if (isset($exercise_stat_info['questions_to_check']) && - !empty($exercise_stat_info['questions_to_check']) - ) { - $remind_list = explode(',', $exercise_stat_info['questions_to_check']); + }, + error: function() { + $("#save_for_now_"+question_id).html(\''. + Display::return_icon('error.png', get_lang('Error'), [], ICON_SIZE_SMALL).'\'); + } + }); } - foreach ($questionList as $questionId) { - // for sequential exercises - if (ONE_PER_PAGE == $objExercise->type) { - // if it is not the right question, goes to the next loop iteration - if ($current_question != $i) { - $i++; - continue; - } else { - if (!in_array($objExercise->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) { - // if the user has already answered this question - if (isset($exerciseResult[$questionId])) { - // construction of the Question object - $objQuestionTmp = Question::read($questionId); - $questionName = $objQuestionTmp->selectTitle(); - // destruction of the Question object - unset($objQuestionTmp); - echo Display::return_message(get_lang('AlreadyAnswered')); - $i++; - break; - } + function save_now_all(validate) { + // 1. Input choice. + var my_choice = $(\'*[name*="choice"]\').serialize(); + + // 2. Reminder. + var remind_list = $(\'*[name*="remind_list"]\').serialize(); + + // 3. Hotspots. + var hotspot = $(\'*[name*="hotspot"]\').serialize(); + + // Question list. + var question_list = ['.implode(',', $questionList).']; + var free_answers = {}; + $.each(question_list, function(index, my_question_id) { + // Checking Ckeditor + if (my_question_id) { + if (CKEDITOR.instances["choice["+my_question_id+"]"]) { + var ckContent = CKEDITOR.instances["choice["+my_question_id+"]"].getData(); + free_answers["free_choice["+my_question_id+"]"] = ckContent; } + } + }); - if (1 === $exerciseInSession->getPreventBackwards()) { - if (isset($attempt_list[$questionId])) { - echo Display::return_message(get_lang('AlreadyAnswered')); - $i++; - break; + free_answers = $.param(free_answers); + $("#save_all_response").html(\''.Display::returnFontAwesomeIcon('spinner', null, true, 'fa-spin').'\'); + + var requestData = "'.$params.'&type=all"; + requestData += "&" + my_choice; + requestData += hotspot ? ("&" + hotspot) : ""; + requestData += free_answers ? ("&" + free_answers) : ""; + requestData += remind_list ? ("&" + remind_list) : ""; + + $.ajax({ + type:"post", + url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=save_exercise_by_now", + data: requestData, + success: function(return_value) { + if (return_value == "ok") { + if (validate == "validate") { + window.location = "'.$script_php.'?'.$params.'"; + } else { + $("#save_all_response").html(\''.Display::return_icon('accept.png').'\'); } + } else { + $("#save_all_response").html(\''.Display::return_icon('wrong.gif').'\'); } } - } + }); + return false; + } - $user_choice = null; - if (isset($attempt_list[$questionId])) { - $user_choice = $attempt_list[$questionId]; - } elseif ($objExercise->getSaveCorrectAnswers()) { - $correctAnswers = []; - switch ($objExercise->getSaveCorrectAnswers()) { - case 1: - $correctAnswers = $objExercise->getCorrectAnswersInAllAttempts( - $learnpath_id, - $learnpath_item_id - ); - break; - case 2: - $correctAnswers = $objExercise->getAnswersInAllAttempts( - $learnpath_id, - $learnpath_item_id, - false - ); + function validate_all() { + save_now_all("validate"); + } + + window.quizTimeEnding = false; +'; + +echo ' + + + + + + + + + + '; + +// Show list of questions +$i = 1; +$attempt_list = []; +if (isset($exe_id)) { + $attempt_list = Event::getAllExerciseEventByExeId($exe_id); +} + +$remind_list = []; +if (isset($exercise_stat_info['questions_to_check']) && + !empty($exercise_stat_info['questions_to_check']) +) { + $remind_list = explode(',', $exercise_stat_info['questions_to_check']); +} + +foreach ($questionList as $questionId) { + // for sequential exercises + if (ONE_PER_PAGE == $objExercise->type) { + // if it is not the right question, goes to the next loop iteration + if ($current_question != $i) { + $i++; + continue; + } else { + if (!in_array($objExercise->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) { + // if the user has already answered this question + if (isset($exerciseResult[$questionId])) { + // construction of the Question object + $objQuestionTmp = Question::read($questionId); + $questionName = $objQuestionTmp->selectTitle(); + // destruction of the Question object + unset($objQuestionTmp); + echo Display::return_message(get_lang('AlreadyAnswered')); + $i++; break; + } } - if (isset($correctAnswers[$questionId])) { - $user_choice = $correctAnswers[$questionId]; + if (1 === $exerciseInSession->getPreventBackwards()) { + if (isset($attempt_list[$questionId])) { + echo Display::return_message(get_lang('AlreadyAnswered')); + $i++; + break; + } } } + } - $remind_highlight = ''; - // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise see #4542 no_remind_highlight class hide with jquery - if ($objExercise->type == ALL_ON_ONE_PAGE && - isset($_GET['reminder']) && $_GET['reminder'] == 2 - ) { - $remind_highlight = 'no_remind_highlight'; + $user_choice = null; + if (isset($attempt_list[$questionId])) { + $user_choice = $attempt_list[$questionId]; + } elseif ($objExercise->getSaveCorrectAnswers()) { + $correctAnswers = []; + switch ($objExercise->getSaveCorrectAnswers()) { + case 1: + $correctAnswers = $objExercise->getCorrectAnswersInAllAttempts( + $learnpath_id, + $learnpath_item_id + ); + break; + case 2: + $correctAnswers = $objExercise->getAnswersInAllAttempts( + $learnpath_id, + $learnpath_item_id, + false + ); + break; } - $exerciseActions = ''; - $is_remind_on = false; - $attributes = ['id' => 'remind_list['.$questionId.']']; - if (in_array($questionId, $remind_list)) { - $is_remind_on = true; - $attributes['checked'] = 1; - $remind_question = true; - $remind_highlight = ' remind_highlight '; + if (isset($correctAnswers[$questionId])) { + $user_choice = $correctAnswers[$questionId]; } + } - // Showing the exercise description - if (!empty($objExercise->description)) { - if ($objExercise->type == ONE_PER_PAGE || ($objExercise->type != ONE_PER_PAGE && $i == 1)) { - echo Display::panelCollapse( - ''.get_lang('ExerciseDescriptionLabel').'', - $objExercise->description, - 'exercise-description', - [], - 'description', - 'exercise-collapse', - false, - true - ); - } - } + $remind_highlight = ''; + // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise see #4542 no_remind_highlight class hide with jquery + if ($objExercise->type == ALL_ON_ONE_PAGE && + isset($_GET['reminder']) && $_GET['reminder'] == 2 + ) { + $remind_highlight = 'no_remind_highlight'; + } - echo '
'; - $showQuestion = true; - $exerciseResultFromSession = Session::read('exerciseResult'); - if ($objExercise->getFeedbackType() === EXERCISE_FEEDBACK_TYPE_POPUP && - isset($exerciseResultFromSession[$questionId]) - ) { - $showQuestion = false; - } + $exerciseActions = ''; + $is_remind_on = false; + $attributes = ['id' => 'remind_list['.$questionId.']']; + if (in_array($questionId, $remind_list)) { + $is_remind_on = true; + $attributes['checked'] = 1; + $remind_question = true; + $remind_highlight = ' remind_highlight '; + } - // Shows the question and its answers - if ($showQuestion) { - ExerciseLib::showQuestion( - $objExercise, - $questionId, - false, - $origin, - $i, - $objExercise->getHideQuestionTitle() ? false : true, - false, - $user_choice, - false, - null, + // Showing the exercise description + if (!empty($objExercise->description)) { + if ($objExercise->type == ONE_PER_PAGE || ($objExercise->type != ONE_PER_PAGE && $i == 1)) { + echo Display::panelCollapse( + ''.get_lang('ExerciseDescriptionLabel').'', + $objExercise->description, + 'exercise-description', + [], + 'description', + 'exercise-collapse', false, true ); - } else { - echo Display::return_message(get_lang('AlreadyAnswered')); } + } - // Button save and continue - switch ($objExercise->type) { - case ONE_PER_PAGE: - $exerciseActions .= $objExercise->show_button( - $questionId, - $current_question, - [], - [], - $myRemindList, - $showPreviousButton - ); - break; - case ALL_ON_ONE_PAGE: - if (api_is_allowed_to_session_edit()) { - $button = [ - Display::button( - 'save_now', - get_lang('SaveForNow'), - [ - 'type' => 'button', - 'class' => 'btn btn-info', - 'data-question' => $questionId, - ] - ), - ' ', - ]; - $exerciseActions .= Display::div( - implode(PHP_EOL, $button), - ['class' => 'exercise_save_now_button'] - ); - } - break; - } + echo '
'; + $showQuestion = true; + $exerciseResultFromSession = Session::read('exerciseResult'); + if ($objExercise->getFeedbackType() === EXERCISE_FEEDBACK_TYPE_POPUP && + isset($exerciseResultFromSession[$questionId]) + ) { + $showQuestion = false; + } - // Checkbox review answers - if ($objExercise->review_answers) { - $remind_question_div = Display::tag( - 'label', - Display::input( - 'checkbox', - 'remind_list['.$questionId.']', - '', - $attributes - ).get_lang('ReviewQuestionLater'), - [ - 'class' => 'checkbox', - 'for' => 'remind_list['.$questionId.']', - ] - ); - $exerciseActions .= Display::div( - $remind_question_div, - ['class' => 'exercise_save_now_button'] - ); - } - echo Display::div($exerciseActions, ['class' => 'form-actions']); - echo '
'; + // Shows the question and its answers + if ($showQuestion) { + ExerciseLib::showQuestion( + $objExercise, + $questionId, + false, + $origin, + $i, + $objExercise->getHideQuestionTitle() ? false : true, + false, + $user_choice, + false, + null, + false, + true + ); + } else { + echo Display::return_message(get_lang('AlreadyAnswered')); + } - $i++; - // for sequential exercises - if ($objExercise->type == ONE_PER_PAGE) { - // quits the loop + // Button save and continue + switch ($objExercise->type) { + case ONE_PER_PAGE: + $exerciseActions .= $objExercise->show_button( + $questionId, + $current_question, + [], + [], + $myRemindList, + $showPreviousButton + ); + break; + case ALL_ON_ONE_PAGE: + if (api_is_allowed_to_session_edit()) { + $button = [ + Display::button( + 'save_now', + get_lang('SaveForNow'), + [ + 'type' => 'button', + 'class' => 'btn btn-info', + 'data-question' => $questionId, + ] + ), + ' ', + ]; + $exerciseActions .= Display::div( + implode(PHP_EOL, $button), + ['class' => 'exercise_save_now_button'] + ); + } break; - } } - if ($objExercise->type == ALL_ON_ONE_PAGE) { - $exerciseActions = $objExercise->show_button( - $questionId, - $current_question + // Checkbox review answers + if ($objExercise->review_answers) { + $remind_question_div = Display::tag( + 'label', + Display::input( + 'checkbox', + 'remind_list['.$questionId.']', + '', + $attributes + ).get_lang('ReviewQuestionLater'), + [ + 'class' => 'checkbox', + 'for' => 'remind_list['.$questionId.']', + ] ); - echo Display::div($exerciseActions, ['class' => 'exercise_actions']); - echo '
'; + $exerciseActions .= Display::div( + $remind_question_div, + ['class' => 'exercise_save_now_button'] + ); + } + echo Display::div($exerciseActions, ['class' => 'form-actions']); + echo '
'; + + $i++; + // for sequential exercises + if ($objExercise->type == ONE_PER_PAGE) { + // quits the loop + break; } - echo '
'; } + +if ($objExercise->type == ALL_ON_ONE_PAGE) { + $exerciseActions = $objExercise->show_button( + $questionId, + $current_question + ); + echo Display::div($exerciseActions, ['class' => 'exercise_actions']); + echo '
'; +} +echo ''; + if (!in_array($origin, ['learnpath', 'embeddable'])) { // So we are not in learnpath tool echo '
'; //End glossary div diff --git a/main/inc/ajax/exercise.ajax.php b/main/inc/ajax/exercise.ajax.php index 21574837e1..d124a3b02d 100755 --- a/main/inc/ajax/exercise.ajax.php +++ b/main/inc/ajax/exercise.ajax.php @@ -389,8 +389,7 @@ switch ($action) { $choice = isset($_REQUEST['choice']) ? $_REQUEST['choice'] : []; // certainty degree choice - $choiceDegreeCertainty = isset($_REQUEST['choiceDegreeCertainty']) - ? $_REQUEST['choiceDegreeCertainty'] : []; + $choiceDegreeCertainty = isset($_REQUEST['choiceDegreeCertainty'])? $_REQUEST['choiceDegreeCertainty'] : []; // Hot spot coordinates from all questions. $hot_spot_coordinates = isset($_REQUEST['hotspot']) ? $_REQUEST['hotspot'] : []; @@ -450,13 +449,11 @@ switch ($action) { $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId); $exercise_id = $exercise_stat_info['exe_exo_id']; $attemptList = []; - // First time here we create an attempt (getting the exe_id). if (!empty($exercise_stat_info)) { // We know the user we get the exe_id. $exeId = $exercise_stat_info['exe_id']; $total_score = $exercise_stat_info['exe_result']; - // Getting the list of attempts $attemptList = Event::getAllExerciseEventByExeId($exeId); } @@ -495,7 +492,7 @@ switch ($action) { } } - // Getting the total weight if the request is simple + // Getting the total weight if the request is simple. $total_weight = 0; if ($type === 'simple') { foreach ($question_list as $my_question_id) { @@ -510,7 +507,7 @@ switch ($action) { } // Check we have at least one non-empty answer in the array - // provided by the user's click on the "Finish test" button + // provided by the user's click on the "Finish test" button. if ('all' === $type) { $atLeastOneAnswer = false; foreach ($question_list as $my_question_id) { @@ -534,24 +531,14 @@ switch ($action) { // Looping the question list from database (not from the user answer) foreach ($question_list as $my_question_id) { if ($type === 'simple' && $question_id != $my_question_id) { - if ($debug) { - error_log('Skipping question '.$my_question_id.' in single-question save action'); - } continue; } - if ($debug) { - error_log("Saving question_id = $my_question_id "); - } - $my_choice = isset($choice[$my_question_id]) ? $choice[$my_question_id] : null; - if ($debug) { + error_log("Saving question_id = $my_question_id "); error_log("my_choice = ".print_r($my_choice, 1).""); } - - // Creates a temporary Question object $objQuestionTmp = Question::read($my_question_id, $objExercise->course); - $myChoiceDegreeCertainty = null; if ($objQuestionTmp->type === MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) { if (isset($choiceDegreeCertainty[$my_question_id])) { @@ -560,7 +547,7 @@ switch ($action) { } // Getting free choice data. - if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION]) && $type == 'all') { + if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION]) && $type === 'all') { $my_choice = isset($_REQUEST['free_choice'][$my_question_id]) && !empty($_REQUEST['free_choice'][$my_question_id]) ? $_REQUEST['free_choice'][$my_question_id] : null; @@ -581,7 +568,7 @@ switch ($action) { $hotspot_delineation_result = $_SESSION['hotspot_delineation_result'][$objExercise->selectId()][$my_question_id]; } - if ($type === 'simple') { + if ('simple' === $type) { // Getting old attempt in order to decrease the total score. $old_result = $objExercise->manage_answer( $exeId, @@ -627,6 +614,8 @@ switch ($action) { } } + $questionDuration = Event::getAttemptQuestionDuration($exeId, $objQuestionTmp->iid); + // We're inside *one* question. Go through each possible answer for this question if ($objQuestionTmp->type === MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) { $myChoiceTmp = []; @@ -642,7 +631,11 @@ switch ($action) { false, false, $objExercise->selectPropagateNeg(), - $hotspot_delineation_result + $hotspot_delineation_result, + true, + false, + false, + $questionDuration ); } else { $result = $objExercise->manage_answer( @@ -655,11 +648,15 @@ switch ($action) { false, false, $objExercise->selectPropagateNeg(), - $hotspot_delineation_result + $hotspot_delineation_result, + true, + false, + false, + $questionDuration ); } - // Adding the new score. + // Adding the new score. $total_score += $result['score']; if ($debug) { @@ -715,6 +712,11 @@ switch ($action) { $remind_list ); + $questionStart = Session::read('question_start'); + unset($questionStart[$my_question_id]); + array_filter($questionStart); + Session::write('question_start', $questionStart); + // Destruction of the Question object unset($objQuestionTmp); if ($debug) { @@ -737,7 +739,7 @@ switch ($action) { exit; } - if ($type == 'all') { + if ($type === 'all') { echo 'ok'; exit; } diff --git a/main/inc/lib/events.lib.php b/main/inc/lib/events.lib.php index ce08307fcd..4264960b42 100644 --- a/main/inc/lib/events.lib.php +++ b/main/inc/lib/events.lib.php @@ -403,21 +403,26 @@ class Event /** * Update the TRACK_E_EXERCICES exercises. + * Record result of user when an exercise was done. * - * @param int exeid id of the attempt - * @param int exo_id exercise id - * @param mixed result score - * @param int weighting ( higher score ) - * @param int duration ( duration of the attempt in seconds ) - * @param int session_id - * @param int learnpath_id (id of the learnpath) - * @param int learnpath_item_id (id of the learnpath_item) + * @param int $exeId + * @param int $exoId + * @param mixed $score + * @param int $weighting + * @param int $sessionId + * @param int $learnpathId + * @param int $learnpathItemId + * @param int $learnpathItemViewId + * @param int $duration + * @param array $questionsList + * @param string $status + * @param array $remindList + * @param null $endDate * * @return bool * * @author Sebastien Piraux * @author Julio Montoya Armas Reworked 2010 - * @desc Record result of user when an exercise was done */ public static function updateEventExercise( $exeId, @@ -437,15 +442,6 @@ class Event if (empty($exeId)) { return false; } - - /* - * Code commented due BT#8423 do not change the score to 0. - * - * Validation in case of fraud with actived control time - if (!ExerciseLib::exercise_time_control_is_valid($exo_id, $learnpath_id, $learnpath_item_id)) { - $score = 0; - } - */ if (!isset($status) || empty($status)) { $status = ''; } else { @@ -506,19 +502,20 @@ class Event /** * Record an event for this attempt at answering an exercise. * - * @param float Score achieved - * @param string Answer given - * @param int Question ID - * @param int Exercise attempt ID a.k.a exe_id (from track_e_exercise) - * @param int Position - * @param int Exercise ID (from c_quiz) - * @param bool update results? - * @param $fileName string Filename (for audio answers - using nanogong) - * @param int User ID The user who's going to get this score. Default value of null means "get from context". - * @param int Course ID (from the "id" column of course table). Default value of null means "get from context". - * @param int Session ID (from the session table). Default value of null means "get from context". - * @param int Learnpath ID (from c_lp table). Default value of null means "get from context". - * @param int Learnpath item ID (from the c_lp_item table). Default value of null means "get from context". + * @param float $score Score achieved + * @param string $answer Answer given + * @param int $question_id + * @param int $exe_id Exercise attempt ID a.k.a exe_id (from track_e_exercise) + * @param int $position + * @param int $exercise_id From c_quiz + * @param bool $updateResults + * @param int $duration Time spent in seconds + * @param string $fileName Filename (for audio answers - using nanogong) + * @param int $user_id The user who's going to get this score. + * @param int $course_id Default value of null means "get from context". + * @param int $session_id Default value of null means "get from context". + * @param int $learnpath_id (from c_lp table). Default value of null means "get from context". + * @param int $learnpath_item_id (from the c_lp_item table). Default value of null means "get from context". * * @return bool Result of the insert query */ @@ -530,6 +527,7 @@ class Event $position, $exercise_id = 0, $updateResults = false, + $questionDuration = 0, $fileName = null, $user_id = null, $course_id = null, @@ -538,12 +536,13 @@ class Event $learnpath_item_id = null ) { global $debug; - $question_id = Database::escape_string($question_id); - $exe_id = Database::escape_string($exe_id); - $position = Database::escape_string($position); - $now = api_get_utc_datetime(); + $questionDuration = (int) $questionDuration; + $question_id = (int) $question_id; + $exe_id = (int) $exe_id; + $position = (int) $position; $course_id = (int) $course_id; - $recording = api_get_configuration_value('quiz_answer_extra_recording') == true; + $now = api_get_utc_datetime(); + $recording = api_get_configuration_value('quiz_answer_extra_recording'); // check user_id or get from context if (empty($user_id)) { @@ -590,121 +589,119 @@ class Event $answer = 0; } - if (!empty($question_id) && !empty($exe_id) && !empty($user_id)) { - if (is_null($answer)) { - $answer = ''; - } + if (empty($question_id) || empty($exe_id) || empty($user_id)) { + return false; + } - if (is_null($score)) { - $score = 0; - } + if (is_null($answer)) { + $answer = ''; + } - $attempt = [ - 'user_id' => $user_id, - 'question_id' => $question_id, - 'answer' => $answer, - 'marks' => $score, - 'c_id' => $course_id, - 'session_id' => $session_id, - 'position' => $position, - 'tms' => $now, - 'filename' => !empty($fileName) ? basename($fileName) : $fileName, - 'teacher_comment' => '', - ]; + if (is_null($score)) { + $score = 0; + } - // Check if attempt exists. - $sql = "SELECT exe_id FROM $TBL_TRACK_ATTEMPT - WHERE - c_id = $course_id AND - session_id = $session_id AND - exe_id = $exe_id AND - user_id = $user_id AND - question_id = $question_id AND - position = $position"; - $result = Database::query($sql); - if (Database::num_rows($result)) { - if ($debug) { - error_log("Attempt already exist: exe_id: $exe_id - user_id:$user_id - question_id:$question_id"); - } - if ($updateResults == false) { - //The attempt already exist do not update use update_event_exercise() instead - return false; - } - } else { - $attempt['exe_id'] = $exe_id; - } + $attempt = [ + 'user_id' => $user_id, + 'question_id' => $question_id, + 'answer' => $answer, + 'marks' => $score, + 'c_id' => $course_id, + 'session_id' => $session_id, + 'position' => $position, + 'tms' => $now, + 'filename' => !empty($fileName) ? basename($fileName) : $fileName, + 'teacher_comment' => '', + ]; - if ($debug) { - error_log("updateResults : $updateResults"); - error_log("Saving question attempt: "); - error_log($sql); + if (api_get_configuration_value('allow_time_per_question')) { + $attempt['seconds_spent'] = $questionDuration; + } + + // Check if attempt exists. + $sql = "SELECT exe_id FROM $TBL_TRACK_ATTEMPT + WHERE + c_id = $course_id AND + session_id = $session_id AND + exe_id = $exe_id AND + user_id = $user_id AND + question_id = $question_id AND + position = $position"; + $result = Database::query($sql); + $attemptData = []; + if (Database::num_rows($result)) { + $attemptData = Database::fetch_array($result, 'ASSOC'); + if ($updateResults == false) { + // The attempt already exist do not update use update_event_exercise() instead + return false; } + } else { + $attempt['exe_id'] = $exe_id; + } - $recording_table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING); + if ($debug) { + error_log("updateResults : $updateResults"); + error_log("Saving question attempt: "); + error_log($sql); + } - if ($updateResults == false) { - $attempt_id = Database::insert($TBL_TRACK_ATTEMPT, $attempt); + $recording_table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING); + if ($updateResults == false) { + $attempt_id = Database::insert($TBL_TRACK_ATTEMPT, $attempt); + if ($recording) { + $attempt_recording = [ + 'exe_id' => $exe_id, + 'question_id' => $question_id, + 'answer' => $answer, + 'marks' => $score, + 'insert_date' => $now, + 'session_id' => $session_id, + ]; + Database::insert($recording_table, $attempt_recording); + } + } else { + if (api_get_configuration_value('allow_time_per_question')) { + $attempt['seconds_spent'] = $questionDuration + (int) $attemptData['seconds_spent']; + } + Database::update( + $TBL_TRACK_ATTEMPT, + $attempt, + [ + 'exe_id = ? AND question_id = ? AND user_id = ? ' => [ + $exe_id, + $question_id, + $user_id, + ], + ] + ); - if ($debug) { - error_log("Insert attempt with id #$attempt_id"); - } + if ($recording) { + $attempt_recording = [ + 'exe_id' => $exe_id, + 'question_id' => $question_id, + 'answer' => $answer, + 'marks' => $score, + 'insert_date' => $now, + 'session_id' => $session_id, + ]; - if ($recording) { - if ($debug) { - error_log("Saving e attempt recording "); - } - $attempt_recording = [ - 'exe_id' => $exe_id, - 'question_id' => $question_id, - 'answer' => $answer, - 'marks' => $score, - 'insert_date' => $now, - 'session_id' => $session_id, - ]; - Database::insert($recording_table, $attempt_recording); - } - } else { Database::update( - $TBL_TRACK_ATTEMPT, - $attempt, + $recording_table, + $attempt_recording, [ - 'exe_id = ? AND question_id = ? AND user_id = ? ' => [ + 'exe_id = ? AND question_id = ? AND session_id = ? ' => [ $exe_id, $question_id, - $user_id, + $session_id, ], ] ); - - if ($recording) { - $attempt_recording = [ - 'exe_id' => $exe_id, - 'question_id' => $question_id, - 'answer' => $answer, - 'marks' => $score, - 'insert_date' => $now, - 'session_id' => $session_id, - ]; - - Database::update( - $recording_table, - $attempt_recording, - [ - 'exe_id = ? AND question_id = ? AND session_id = ? ' => [ - $exe_id, - $question_id, - $session_id, - ], - ] - ); - } - $attempt_id = $exe_id; } - - return $attempt_id; - } else { - return false; + $attempt_id = $exe_id; } + + return $attempt_id; + } /** @@ -1197,7 +1194,7 @@ class Event $lpInteraction = Database::get_course_table(TABLE_LP_IV_INTERACTION); $lpObjective = Database::get_course_table(TABLE_LP_IV_OBJECTIVE); - if (empty($course)) { + if (empty($course) || empty($user_id)) { return false; } @@ -1732,13 +1729,13 @@ class Event $courseId, $session_id = 0 ) { - $table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); + $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); $courseId = (int) $courseId; $exercise_id = (int) $exercise_id; $session_id = (int) $session_id; $user_id = (int) $user_id; - $sql = "SELECT * FROM $table_track_exercises + $sql = "SELECT * FROM $table WHERE status = '' AND c_id = $courseId AND @@ -1935,6 +1932,26 @@ class Event return $list; } + public static function getQuestionAttemptByExeIdAndQuestion($exeId, $questionId) + { + $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT); + $exeId = (int) $exeId; + $questionId = (int) $questionId; + + $sql = "SELECT * FROM $table + WHERE + exe_id = $exeId AND + question_id = $questionId + ORDER BY position"; + $result = Database::query($sql); + $attempt = []; + if (Database::num_rows($result)) { + $attempt = Database::fetch_array($result, 'ASSOC'); + } + + return $attempt; + } + /** * Delete one record from the track_e_attempt table (recorded quiz answer) * and register the deletion event (LOG_QUESTION_RESULT_DELETE) in @@ -2566,4 +2583,30 @@ class Event return true; } + + public static function getAttemptQuestionDuration($exeId, $questionId) + { + // Check current attempt. + $questionAttempt = self::getQuestionAttemptByExeIdAndQuestion($exeId, $questionId); + $alreadySpent = 0; + if (!empty($questionAttempt) && $questionAttempt['seconds_spent']) { + $alreadySpent = $questionAttempt['seconds_spent']; + } + $now = time(); + $questionStart = Session::read('question_start', []); + if (!empty($questionStart) && + isset($questionStart[$questionId]) && !empty($questionStart[$questionId]) + ) { + $time = $questionStart[$questionId]; + } else { + $diff = 0; + if (!empty($alreadySpent)) { + $diff = $alreadySpent; + } + $time = $questionStart[$questionId] = $now - $diff; + Session::write('question_start', $questionStart); + } + + return $now - $time; + } } diff --git a/main/install/configuration.dist.php b/main/install/configuration.dist.php index b507aa233e..d5b2484936 100755 --- a/main/install/configuration.dist.php +++ b/main/install/configuration.dist.php @@ -1727,7 +1727,9 @@ $_configuration['auth_password_links'] = [ // Resource sequence: Validate course in the same session. //$_configuration['course_sequence_valid_only_in_same_session'] = false; -// Allow time per question. Requires a question text extra field called "time", value in seconds. +// Allow time per question. BT#17791 +// Requires a question text extra field called "time", value in seconds. +// ALTER TABLE track_e_attempt ADD COLUMN seconds_spent INT; //$_configuration['allow_time_per_question'] = true; // Disable change user visibility tool icon.