* @author Hubert Borderiou 2011-10-21 * @author ivantcholakov2009-07-20 * @author Julio Montoya */ class ExerciseLib { /** * Shows a question. * * @param Exercise $exercise * @param int $questionId $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 $show_answers * * @throws \Exception * * @return bool|int */ public static function showQuestion( $exercise, $questionId, $only_questions = false, $origin = false, $current_item = '', $show_title = true, $freeze = false, $user_choice = [], $show_comment = false, $show_answers = false, $show_icon = false ) { $course_id = $exercise->course_id; $exerciseId = $exercise->iId; if (empty($course_id)) { return ''; } $course = $exercise->course; // 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, $course)) { // Question not found return false; } $questionRequireAuth = WhispeakAuthPlugin::questionRequireAuthentify($questionId); if (EXERCISE_FEEDBACK_TYPE_END != $exercise->getFeedbackType()) { $show_comment = false; } $answerType = $objQuestionTmp->selectType(); $s = ''; if (HOT_SPOT != $answerType && HOT_SPOT_DELINEATION != $answerType && ANNOTATION != $answerType ) { // Question is not a hotspot if (!$only_questions) { $questionDescription = $objQuestionTmp->selectDescription(); if ($show_title) { if ($exercise->display_category_name) { TestCategory::displayCategoryAndTitle($objQuestionTmp->id); } $titleToDisplay = $objQuestionTmp->getTitleToDisplay($exercise, $current_item); if (READING_COMPREHENSION == $answerType) { // In READING_COMPREHENSION, the title of the question // contains the question itself, which can only be // shown at the end of the given time, so hide for now $titleToDisplay = Display::div( $current_item.'. '.get_lang('Reading comprehension'), ['class' => 'question_title'] ); } echo $titleToDisplay; } if ($questionRequireAuth) { WhispeakAuthPlugin::quizQuestionAuthentify($questionId, $exercise); return false; } if (!empty($questionDescription) && READING_COMPREHENSION != $answerType) { echo Display::div( $questionDescription, ['class' => 'question_description'] ); } } if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION]) && $freeze) { return ''; } echo '
'; // construction of the Answer object (also gets all answers details) $objAnswerTmp = new Answer($questionId, $course_id, $exercise); $nbrAnswers = $objAnswerTmp->selectNbrAnswers(); $quizQuestionOptions = 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 = []; //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; switch ($answerType) { case MATCHING: case DRAGGABLE: case MATCHING_DRAGGABLE: if (DRAGGABLE == $answerType) { $isVertical = 'v' === $objQuestionTmp->extra; $s .= '

' .get_lang('Sort the following options from the list as you see fit by dragging them to the lower areas. You can put them back in this area to modify your answer.') .'

"; $counterAnswer = 1; $s .= '
'; for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) { $answerCorrect = $objAnswerTmp->isCorrect($answerId); $windowId = $questionId.'_'.$counterAnswer; if ($answerCorrect) { $s .= '
'.$counterAnswer.'
'; $counterAnswer++; } } $s .= '
'; // $s .= '
'; } if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) { $s .= ''; //drag_question } $s .= ''; //question_options row // destruction of the Answer object unset($objAnswerTmp); // destruction of the Question object unset($objQuestionTmp); if ('export' == $origin) { return $s; } echo $s; } elseif (HOT_SPOT == $answerType || HOT_SPOT_DELINEATION == $answerType) { global $exe_id; $questionDescription = $objQuestionTmp->selectDescription(); // Get the answers, make a list $objAnswerTmp = new Answer($questionId, $course_id); $nbrAnswers = $objAnswerTmp->selectNbrAnswers(); // get answers of hotpost $answers_hotspot = []; for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) { $answers = $objAnswerTmp->selectAnswerByAutoId( $objAnswerTmp->selectAutoId($answerId) ); $answers_hotspot[$answers['iid']] = $objAnswerTmp->selectAnswer( $answerId ); } $answerList = ''; $hotspotColor = 0; if (HOT_SPOT_DELINEATION != $answerType) { $answerList = '
    '; if (!empty($answers_hotspot)) { Session::write("hotspot_ordered$questionId", array_keys($answers_hotspot)); foreach ($answers_hotspot as $value) { $answerList .= '
  1. '; if ($freeze) { $answerList .= ''.PHP_EOL; } $answerList .= $value; $answerList .= '
  2. '; $hotspotColor++; } } $answerList .= '
'; } if ($freeze) { $relPath = api_get_path(WEB_CODE_PATH); echo "
$answerList
"; return; } if (!$only_questions) { if ($show_title) { if ($exercise->display_category_name) { TestCategory::displayCategoryAndTitle($objQuestionTmp->id); } echo $objQuestionTmp->getTitleToDisplay($exercise, $current_item); } if ($questionRequireAuth) { WhispeakAuthPlugin::quizQuestionAuthentify($questionId, $exercise); return false; } //@todo I need to the get the feedback type echo <<
$questionDescription
HOTSPOT; } $relPath = api_get_path(WEB_CODE_PATH); $s .= "
$answerList
"; echo <<
HOTSPOT; } elseif (ANNOTATION == $answerType) { global $exe_id; $relPath = api_get_path(WEB_CODE_PATH); if (api_is_platform_admin() || api_is_course_admin()) { $questionRepo = Container::getQuestionRepository(); $questionEntity = $questionRepo->find($questionId); if ($freeze) { echo Display::img( $questionRepo->getHotSpotImageUrl($questionEntity), $objQuestionTmp->selectTitle(), ['width' => '600px'] ); return 0; } } if (!$only_questions) { if ($show_title) { if ($exercise->display_category_name) { TestCategory::displayCategoryAndTitle($objQuestionTmp->id); } echo $objQuestionTmp->getTitleToDisplay($exercise, $current_item); } if ($questionRequireAuth) { WhispeakAuthPlugin::quizQuestionAuthentify($questionId, $exercise); return false; } echo '
'.$objQuestionTmp->selectDescription().'
    '; } $objAnswerTmp = new Answer($questionId); $nbrAnswers = $objAnswerTmp->selectNbrAnswers(); unset($objAnswerTmp, $objQuestionTmp); } return $nbrAnswers; } /** * Get an HTML string with the list of exercises where the given question * is being used. * * @param int $questionId The iid of the question being observed * @param int $excludeTestId If defined, exclude this (current) test from the list of results * * @return string An HTML string containing a div and a table */ public static function showTestsWhereQuestionIsUsed(int $questionId, int $excludeTestId = 0) { $questionId = (int) $questionId; $excludeTestId = (int) $excludeTestId; $sql = "SELECT qz.title quiz_title, c.title course_title, s.name session_name, qz.iid as quiz_id, qz.c_id, qz.session_id FROM c_quiz qz, c_quiz_rel_question qq, course c, session s WHERE qz.c_id = c.id AND (qz.session_id = s.id OR qz.session_id = 0) AND qq.quiz_id = qz.iid AND "; if (!empty($excludeTestId)) { $sql .= " qz.iid != $excludeTestId AND "; } $sql .= " qq.question_id = $questionId GROUP BY qq.iid"; $result = []; $html = ""; $sqlResult = Database::query($sql); if (0 != Database::num_rows($sqlResult)) { while ($row = Database::fetch_array($sqlResult, 'ASSOC')) { $tmp = []; $tmp[0] = $row['course_title']; $tmp[1] = $row['session_name']; $tmp[2] = $row['quiz_title']; // Send do other test with r=1 to reset current test session variables $urlToQuiz = api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq().'&exerciseId='.$row['quiz_id'].'&r=1'; $tmp[3] = ''.Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit')).''; if (0 == (int) $row['session_id']) { $tmp[1] = '-'; } $result[] = $tmp; } $headers = [ get_lang('Course'), get_lang('Session'), get_lang('Quiz'), get_lang('LinkToTestEdition'), ]; $title = Display::div( get_lang('QuestionAlsoUsedInTheFollowingTests'), [ 'class' => 'section-title', 'style' => 'margin-top: 25px; border-bottom: none', ] ); $html = $title.Display::table($headers, $result); } echo $html; } /** * @param int $exeId * * @return array */ public static function get_exercise_track_exercise_info($exeId) { $quizTable = Database::get_course_table(TABLE_QUIZ_TEST); $trackExerciseTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); $courseTable = Database::get_main_table(TABLE_MAIN_COURSE); $exeId = (int) $exeId; $result = []; if (!empty($exeId)) { $sql = " SELECT q.*, tee.* FROM $quizTable as q INNER JOIN $trackExerciseTable as tee ON q.iid = tee.exe_exo_id WHERE tee.exe_id = $exeId"; $sqlResult = Database::query($sql); if (Database::num_rows($sqlResult)) { $result = Database::fetch_array($sqlResult, 'ASSOC'); $result['duration_formatted'] = ''; if (!empty($result['exe_duration'])) { $time = api_format_time($result['exe_duration'], 'js'); $result['duration_formatted'] = $time; } } } return $result; } /** * Validates the time control key. * * @param int $lp_id * @param int $lp_item_id * * @return bool */ public static function exercise_time_control_is_valid(Exercise $exercise, $lp_id = 0, $lp_item_id = 0) { $exercise_id = $exercise->getId(); $expiredTime = $exercise->expired_time; if (!empty($expiredTime)) { $current_expired_time_key = self::get_time_control_key( $exercise_id, $lp_id, $lp_item_id ); if (isset($_SESSION['expired_time'][$current_expired_time_key])) { $current_time = time(); $expired_time = api_strtotime( $_SESSION['expired_time'][$current_expired_time_key], 'UTC' ); $total_time_allowed = $expired_time + 30; if ($total_time_allowed < $current_time) { return false; } return true; } return false; } return true; } /** * Deletes the time control token. * * @param int $exercise_id * @param int $lp_id * @param int $lp_item_id */ public static function exercise_time_control_delete( $exercise_id, $lp_id = 0, $lp_item_id = 0 ) { $current_expired_time_key = self::get_time_control_key( $exercise_id, $lp_id, $lp_item_id ); unset($_SESSION['expired_time'][$current_expired_time_key]); } /** * Generates the time control key. * * @param int $exercise_id * @param int $lp_id * @param int $lp_item_id * * @return string */ public static function get_time_control_key( $exercise_id, $lp_id = 0, $lp_item_id = 0 ) { $exercise_id = (int) $exercise_id; $lp_id = (int) $lp_id; $lp_item_id = (int) $lp_item_id; return api_get_course_int_id().'_'. api_get_session_id().'_'. $exercise_id.'_'. api_get_user_id().'_'. $lp_id.'_'. $lp_item_id; } /** * Get session time control. * * @param int $exercise_id * @param int $lp_id * @param int $lp_item_id * * @return int */ public static function get_session_time_control_key( $exercise_id, $lp_id = 0, $lp_item_id = 0 ) { $return_value = 0; $time_control_key = self::get_time_control_key( $exercise_id, $lp_id, $lp_item_id ); if (isset($_SESSION['expired_time']) && isset($_SESSION['expired_time'][$time_control_key])) { $return_value = $_SESSION['expired_time'][$time_control_key]; } return $return_value; } /** * Gets count of exam results. * * @param int $exerciseId * @param array $conditions * @param int $courseId * @param bool $showSession * * @return array */ public static function get_count_exam_results($exerciseId, $conditions, $courseId, $showSession = false) { $count = self::get_exam_results_data( null, null, null, null, $exerciseId, $conditions, true, $courseId, $showSession ); return $count; } /** * Gets the exam'data results. * * @todo this function should be moved in a library + no global calls * * @param int $from * @param int $number_of_items * @param int $column * @param string $direction * @param int $exercise_id * @param null $extra_where_conditions * @param bool $get_count * @param int $courseId * @param bool $showSessionField * @param bool $showExerciseCategories * @param array $userExtraFieldsToAdd * @param bool $useCommaAsDecimalPoint * @param bool $roundValues * @param bool $getOnyIds * * @return array */ public static function get_exam_results_data( $from, $number_of_items, $column, $direction, $exercise_id, $extra_where_conditions = null, $get_count = false, $courseId = null, $showSessionField = false, $showExerciseCategories = false, $userExtraFieldsToAdd = [], $useCommaAsDecimalPoint = false, $roundValues = false, $getOnyIds = false ) { //@todo replace all this globals global $filter; $courseId = (int) $courseId; $course = api_get_course_entity($courseId); if (null === $course) { return []; } $sessionId = api_get_session_id(); $exercise_id = (int) $exercise_id; $is_allowedToEdit = api_is_allowed_to_edit(null, true) || api_is_allowed_to_edit(true) || api_is_drh() || api_is_student_boss() || api_is_session_admin(); $TBL_USER = Database::get_main_table(TABLE_MAIN_USER); $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST); $TBL_GROUP_REL_USER = Database::get_course_table(TABLE_GROUP_USER); $TBL_GROUP = Database::get_course_table(TABLE_GROUP); $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); $tblTrackAttemptQualify = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY); $session_id_and = ''; $sessionCondition = ''; if (!$showSessionField) { $session_id_and = api_get_session_condition($sessionId, true, false, 'te.session_id'); $sessionCondition = api_get_session_condition($sessionId, true, false, 'ttte.session_id'); } $exercise_where = ''; if (!empty($exercise_id)) { $exercise_where .= ' AND te.exe_exo_id = '.$exercise_id.' '; } // sql for chamilo-type tests for teacher / tutor view $sql_inner_join_tbl_track_exercices = " ( SELECT DISTINCT ttte.*, if(tr.exe_id,1, 0) as revised FROM $TBL_TRACK_EXERCISES ttte LEFT JOIN $tblTrackAttemptQualify tr ON (ttte.exe_id = tr.exe_id) AND tr.author > 0 WHERE c_id = $courseId AND exe_exo_id = $exercise_id $sessionCondition )"; if ($is_allowedToEdit) { //@todo fix to work with COURSE_RELATION_TYPE_RRHH in both queries // Hack in order to filter groups $sql_inner_join_tbl_user = ''; if (strpos($extra_where_conditions, 'group_id')) { $sql_inner_join_tbl_user = " ( SELECT u.id as user_id, firstname, lastname, official_code, email, username, g.name as group_name, g.id as group_id FROM $TBL_USER u INNER JOIN $TBL_GROUP_REL_USER gru ON (gru.user_id = u.id AND gru.c_id= $courseId ) INNER JOIN $TBL_GROUP g ON (gru.group_id = g.id AND g.c_id= $courseId ) )"; } if (strpos($extra_where_conditions, 'group_all')) { $extra_where_conditions = str_replace( "AND ( group_id = 'group_all' )", '', $extra_where_conditions ); $extra_where_conditions = str_replace( "AND group_id = 'group_all'", '', $extra_where_conditions ); $extra_where_conditions = str_replace( "group_id = 'group_all' AND", '', $extra_where_conditions ); $sql_inner_join_tbl_user = " ( SELECT u.id as user_id, firstname, lastname, official_code, email, username, '' as group_name, '' as group_id FROM $TBL_USER u )"; $sql_inner_join_tbl_user = null; } if (strpos($extra_where_conditions, 'group_none')) { $extra_where_conditions = str_replace( "AND ( group_id = 'group_none' )", "AND ( group_id is null )", $extra_where_conditions ); $extra_where_conditions = str_replace( "AND group_id = 'group_none'", "AND ( group_id is null )", $extra_where_conditions ); $sql_inner_join_tbl_user = " ( SELECT u.id as user_id, firstname, lastname, official_code, email, username, g.name as group_name, g.iid as group_id FROM $TBL_USER u LEFT OUTER JOIN $TBL_GROUP_REL_USER gru ON (gru.user_id = u.id AND gru.c_id= $courseId ) LEFT OUTER JOIN $TBL_GROUP g ON (gru.group_id = g.id AND g.c_id = $courseId ) )"; } // All $is_empty_sql_inner_join_tbl_user = false; if (empty($sql_inner_join_tbl_user)) { $is_empty_sql_inner_join_tbl_user = true; $sql_inner_join_tbl_user = " ( SELECT u.id as user_id, firstname, lastname, email, username, ' ' as group_name, '' as group_id, official_code FROM $TBL_USER u WHERE u.status NOT IN(".api_get_users_status_ignored_in_reports('string').") )"; } $sqlFromOption = " , $TBL_GROUP_REL_USER AS gru "; $sqlWhereOption = " AND gru.c_id = $courseId AND gru.user_id = user.id "; $first_and_last_name = api_is_western_name_order() ? "firstname, lastname" : "lastname, firstname"; if ($get_count) { $sql_select = 'SELECT count(te.exe_id) '; } else { $sql_select = "SELECT DISTINCT user.user_id, $first_and_last_name, official_code, ce.title, username, te.score, te.max_score, te.exe_date, te.exe_id, te.session_id, email as exemail, te.start_date, ce.expired_time, steps_counter, exe_user_id, te.exe_duration, te.status as completion_status, propagate_neg, revised, group_name, group_id, orig_lp_id, te.user_ip"; } $sql = " $sql_select FROM $TBL_EXERCISES AS ce INNER JOIN $sql_inner_join_tbl_track_exercices AS te ON (te.exe_exo_id = ce.iid) INNER JOIN $sql_inner_join_tbl_user AS user ON (user.user_id = exe_user_id) WHERE te.c_id = $courseId $session_id_and AND ce.active <> -1 $exercise_where $extra_where_conditions "; } $sql .= !str_contains($sql, 'WHERE') ? ' WHERE user.active <> '.USER_SOFT_DELETED : ' AND user.active <> '.USER_SOFT_DELETED; if (empty($sql)) { return false; } if ($get_count) { $resx = Database::query($sql); $rowx = Database::fetch_row($resx, 'ASSOC'); return $rowx[0]; } $teacher_list = CourseManager::get_teacher_list_from_course_code($course->getCode()); $teacher_id_list = []; if (!empty($teacher_list)) { foreach ($teacher_list as $teacher) { $teacher_id_list[] = $teacher['user_id']; } } $scoreDisplay = new ScoreDisplay(); $decimalSeparator = '.'; $thousandSeparator = ','; if ($useCommaAsDecimalPoint) { $decimalSeparator = ','; $thousandSeparator = ''; } $listInfo = []; $column = !empty($column) ? Database::escape_string($column) : null; $from = (int) $from; $number_of_items = (int) $number_of_items; $direction = !in_array(strtolower(trim($direction)), ['asc', 'desc']) ? 'asc' : $direction; if (!empty($column)) { $sql .= " ORDER BY `$column` $direction "; } if (!$getOnyIds) { $sql .= " LIMIT $from, $number_of_items"; } $results = []; $resx = Database::query($sql); while ($rowx = Database::fetch_array($resx, 'ASSOC')) { $results[] = $rowx; } $group_list = GroupManager::get_group_list(null, $course); $clean_group_list = []; if (!empty($group_list)) { foreach ($group_list as $group) { $clean_group_list[$group['iid']] = $group['name']; } } $lp_list_obj = new LearnpathList(api_get_user_id()); $lp_list = $lp_list_obj->get_flat_list(); $oldIds = array_column($lp_list, 'lp_old_id', 'iid'); if (is_array($results)) { $users_array_id = []; $from_gradebook = false; if (isset($_GET['gradebook']) && 'view' === $_GET['gradebook']) { $from_gradebook = true; } $sizeof = count($results); $locked = api_resource_is_locked_by_gradebook( $exercise_id, LINK_EXERCISE ); $timeNow = strtotime(api_get_utc_datetime()); // Looping results for ($i = 0; $i < $sizeof; $i++) { $revised = $results[$i]['revised']; if ('incomplete' === $results[$i]['completion_status']) { // If the exercise was incomplete, we need to determine // if it is still into the time allowed, or if its // allowed time has expired and it can be closed // (it's "unclosed") $minutes = $results[$i]['expired_time']; if (0 == $minutes) { // There's no time limit, so obviously the attempt // can still be "ongoing", but the teacher should // be able to choose to close it, so mark it as // "unclosed" instead of "ongoing" $revised = 2; } else { $allowedSeconds = $minutes * 60; $timeAttemptStarted = strtotime($results[$i]['start_date']); $secondsSinceStart = $timeNow - $timeAttemptStarted; if ($secondsSinceStart > $allowedSeconds) { $revised = 2; // mark as "unclosed" } else { $revised = 3; // mark as "ongoing" } } } if ($from_gradebook && ($is_allowedToEdit)) { if (in_array( $results[$i]['username'].$results[$i]['firstname'].$results[$i]['lastname'], $users_array_id )) { continue; } $users_array_id[] = $results[$i]['username'].$results[$i]['firstname'].$results[$i]['lastname']; } $lp_obj = isset($results[$i]['orig_lp_id']) && isset($lp_list[$results[$i]['orig_lp_id']]) ? $lp_list[$results[$i]['orig_lp_id']] : null; if (empty($lp_obj)) { // Try to get the old id (id instead of iid) $lpNewId = isset($results[$i]['orig_lp_id']) && isset($oldIds[$results[$i]['orig_lp_id']]) ? $oldIds[$results[$i]['orig_lp_id']] : null; if ($lpNewId) { $lp_obj = isset($lp_list[$lpNewId]) ? $lp_list[$lpNewId] : null; } } $lp_name = null; if ($lp_obj) { $url = api_get_path(WEB_CODE_PATH). 'lp/lp_controller.php?'.api_get_cidreq().'&action=view&lp_id='.$results[$i]['orig_lp_id']; $lp_name = Display::url( $lp_obj['lp_name'], $url, ['target' => '_blank'] ); } // Add all groups by user $group_name_list = ''; if ($is_empty_sql_inner_join_tbl_user) { $group_list = GroupManager::get_group_ids( api_get_course_int_id(), $results[$i]['user_id'] ); foreach ($group_list as $id) { if (isset($clean_group_list[$id])) { $group_name_list .= $clean_group_list[$id].'
    '; } } $results[$i]['group_name'] = $group_name_list; } $results[$i]['exe_duration'] = !empty($results[$i]['exe_duration']) ? round($results[$i]['exe_duration'] / 60) : 0; $id = $results[$i]['exe_id']; $dt = api_convert_and_format_date($results[$i]['max_score']); // we filter the results if we have the permission to $result_disabled = 0; if (isset($results[$i]['results_disabled'])) { $result_disabled = (int) $results[$i]['results_disabled']; } if (0 == $result_disabled) { $my_res = $results[$i]['score']; $my_total = $results[$i]['max_score']; $results[$i]['start_date'] = api_get_local_time($results[$i]['start_date']); $results[$i]['exe_date'] = api_get_local_time($results[$i]['exe_date']); if (!$results[$i]['propagate_neg'] && $my_res < 0) { $my_res = 0; } $score = self::show_score( $my_res, $my_total, true, true, false, false, $decimalSeparator, $thousandSeparator, $roundValues ); $actions = '
    '; if ($is_allowedToEdit) { if (isset($teacher_id_list)) { if (in_array( $results[$i]['exe_user_id'], $teacher_id_list )) { $actions .= Display::getMdiIcon('human-male-board', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Trainer')); } } $revisedLabel = ''; switch ($revised) { case 0: $actions .= "". Display::getMdiIcon(ActionIcon::GRADE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Grade activity') ); $actions .= ''; $revisedLabel = Display::label( get_lang('Not validated'), 'info' ); break; case 1: $actions .= "". Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit')); $actions .= ''; $revisedLabel = Display::label( get_lang('Validated'), 'success' ); break; case 2: //finished but not marked as such $actions .= ''. Display::getMdiIcon(ActionIcon::LOCK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Mark attempt as closed')); $actions .= ''; $revisedLabel = Display::label( get_lang('Unclosed'), 'warning' ); break; case 3: //still ongoing $actions .= Display::getMdiIcon('clock', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Attempt still going on. Please wait.')); $actions .= ''; $revisedLabel = Display::label( get_lang('Ongoing'), 'danger' ); break; } if (2 == $filter) { $actions .= ' '. Display::getMdiIcon('clipboard-text-clock', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('View changes history') ).''; } // Admin can always delete the attempt if ((false == $locked || api_is_platform_admin()) && !api_is_student_boss()) { $ip = Tracking::get_ip_from_user_event( $results[$i]['exe_user_id'], api_get_utc_datetime(), false ); $actions .= '' .Display::getMdiIcon('information', 'ch-tool-icon', null, ICON_SIZE_SMALL, $ip) .''; $recalculateUrl = api_get_path(WEB_CODE_PATH).'exercise/recalculate.php?'. api_get_cidreq().'&'. http_build_query([ 'id' => $id, 'exercise' => $exercise_id, 'user' => $results[$i]['exe_user_id'], ]); $actions .= Display::url( Display::getMdiIcon(ActionIcon::REFRESH, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Recalculate results')), $recalculateUrl, [ 'data-exercise' => $exercise_id, 'data-user' => $results[$i]['exe_user_id'], 'data-id' => $id, 'class' => 'exercise-recalculate', ] ); $filterByUser = isset($_GET['filter_by_user']) ? (int) $_GET['filter_by_user'] : 0; $delete_link = ''; $delete_link .= Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, addslashes(get_lang('Delete'))).''; if (api_is_drh() && !api_is_platform_admin()) { $delete_link = null; } if (api_is_session_admin()) { $delete_link = ''; } if (3 == $revised) { $delete_link = null; } $actions .= $delete_link; } } else { $attempt_url = api_get_path(WEB_CODE_PATH).'exercise/result.php?'.api_get_cidreq().'&id='.$results[$i]['exe_id'].'&sid='.$sessionId; $attempt_link = Display::url( get_lang('Show'), $attempt_url, [ 'class' => 'ajax btn btn--plain', 'data-title' => get_lang('Show'), ] ); $actions .= $attempt_link; } $actions .= '
    '; if (!empty($userExtraFieldsToAdd)) { foreach ($userExtraFieldsToAdd as $variable) { $extraFieldValue = new ExtraFieldValue('user'); $values = $extraFieldValue->get_values_by_handler_and_field_variable( $results[$i]['user_id'], $variable ); if (isset($values['value'])) { $results[$i][$variable] = $values['value']; } } } $exeId = $results[$i]['exe_id']; $results[$i]['id'] = $exeId; $category_list = []; if ($is_allowedToEdit) { $sessionName = ''; $sessionStartAccessDate = ''; if (!empty($results[$i]['session_id'])) { $sessionInfo = api_get_session_info($results[$i]['session_id']); if (!empty($sessionInfo)) { $sessionName = $sessionInfo['name']; $sessionStartAccessDate = api_get_local_time($sessionInfo['access_start_date']); } } $objExercise = new Exercise($courseId); if ($showExerciseCategories) { // Getting attempt info $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId); if (!empty($exercise_stat_info['data_tracking'])) { $question_list = explode(',', $exercise_stat_info['data_tracking']); if (!empty($question_list)) { foreach ($question_list as $questionId) { $objQuestionTmp = Question::read($questionId, $objExercise->course); // We're inside *one* question. Go through each possible answer for this question $result = $objExercise->manage_answer( $exeId, $questionId, null, 'exercise_result', false, false, true, false, $objExercise->selectPropagateNeg(), null, true ); $my_total_score = $result['score']; $my_total_weight = $result['weight']; // Category report $category_was_added_for_this_test = false; if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) { if (!isset($category_list[$objQuestionTmp->category]['score'])) { $category_list[$objQuestionTmp->category]['score'] = 0; } if (!isset($category_list[$objQuestionTmp->category]['total'])) { $category_list[$objQuestionTmp->category]['total'] = 0; } $category_list[$objQuestionTmp->category]['score'] += $my_total_score; $category_list[$objQuestionTmp->category]['total'] += $my_total_weight; $category_was_added_for_this_test = true; } if (isset($objQuestionTmp->category_list) && !empty($objQuestionTmp->category_list) ) { foreach ($objQuestionTmp->category_list as $category_id) { $category_list[$category_id]['score'] += $my_total_score; $category_list[$category_id]['total'] += $my_total_weight; $category_was_added_for_this_test = true; } } // No category for this question! if (false == $category_was_added_for_this_test) { if (!isset($category_list['none']['score'])) { $category_list['none']['score'] = 0; } if (!isset($category_list['none']['total'])) { $category_list['none']['total'] = 0; } $category_list['none']['score'] += $my_total_score; $category_list['none']['total'] += $my_total_weight; } } } } } foreach ($category_list as $categoryId => $result) { $scoreToDisplay = self::show_score( $result['score'], $result['total'], true, true, false, false, $decimalSeparator, $thousandSeparator, $roundValues ); $results[$i]['category_'.$categoryId] = $scoreToDisplay; $results[$i]['category_'.$categoryId.'_score_percentage'] = self::show_score( $result['score'], $result['total'], true, true, true, // $show_only_percentage = false true, // hide % sign $decimalSeparator, $thousandSeparator, $roundValues ); $results[$i]['category_'.$categoryId.'_only_score'] = $result['score']; $results[$i]['category_'.$categoryId.'_total'] = $result['total']; } $results[$i]['session'] = $sessionName; $results[$i]['session_access_start_date'] = $sessionStartAccessDate; $results[$i]['status'] = $revisedLabel; $results[$i]['score'] = $score; $results[$i]['score_percentage'] = self::show_score( $my_res, $my_total, true, true, true, true, $decimalSeparator, $thousandSeparator, $roundValues ); if ($roundValues) { $whole = floor($my_res); // 1 $fraction = $my_res - $whole; // .25 if ($fraction >= 0.5) { $onlyScore = ceil($my_res); } else { $onlyScore = round($my_res); } } else { $onlyScore = $scoreDisplay->format_score( $my_res, false, $decimalSeparator, $thousandSeparator ); } $results[$i]['only_score'] = $onlyScore; if ($roundValues) { $whole = floor($my_total); // 1 $fraction = $my_total - $whole; // .25 if ($fraction >= 0.5) { $onlyTotal = ceil($my_total); } else { $onlyTotal = round($my_total); } } else { $onlyTotal = $scoreDisplay->format_score( $my_total, false, $decimalSeparator, $thousandSeparator ); } $results[$i]['total'] = $onlyTotal; $results[$i]['lp'] = $lp_name; $results[$i]['actions'] = $actions; $listInfo[] = $results[$i]; } else { $results[$i]['status'] = $revisedLabel; $results[$i]['score'] = $score; $results[$i]['actions'] = $actions; $listInfo[] = $results[$i]; } } } } return $listInfo; } /** * @param $score * @param $weight * * @return array */ public static function convertScoreToPlatformSetting($score, $weight) { $maxNote = api_get_setting('exercise_max_score'); $minNote = api_get_setting('exercise_min_score'); if ('' != $maxNote && '' != $minNote) { if (!empty($weight) && (float) $weight !== (float) 0) { $score = $minNote + ($maxNote - $minNote) * $score / $weight; } else { $score = $minNote; } $weight = $maxNote; } return ['score' => $score, 'weight' => $weight]; } /** * Converts the score with the exercise_max_note and exercise_min_score * the platform settings + formats the results using the float_format function. * * @param float $score * @param float $weight * @param bool $show_percentage show percentage or not * @param bool $use_platform_settings use or not the platform settings * @param bool $show_only_percentage * @param bool $hidePercentageSign hide "%" sign * @param string $decimalSeparator * @param string $thousandSeparator * @param bool $roundValues This option rounds the float values into a int using ceil() * @param bool $removeEmptyDecimals * * @return string an html with the score modified */ public static function show_score( $score, $weight, $show_percentage = true, $use_platform_settings = true, $show_only_percentage = false, $hidePercentageSign = false, $decimalSeparator = '.', $thousandSeparator = ',', $roundValues = false, $removeEmptyDecimals = false ) { if (is_null($score) && is_null($weight)) { return '-'; } $decimalSeparator = empty($decimalSeparator) ? '.' : $decimalSeparator; $thousandSeparator = empty($thousandSeparator) ? ',' : $thousandSeparator; if ($use_platform_settings) { $result = self::convertScoreToPlatformSetting($score, $weight); $score = $result['score']; $weight = $result['weight']; } $percentage = (100 * $score) / (0 != $weight ? $weight : 1); // Formats values $percentage = float_format($percentage, 1); $score = float_format($score, 1); $weight = float_format($weight, 1); if ($roundValues) { $whole = floor($percentage); // 1 $fraction = $percentage - $whole; // .25 // Formats values if ($fraction >= 0.5) { $percentage = ceil($percentage); } else { $percentage = round($percentage); } $whole = floor($score); // 1 $fraction = $score - $whole; // .25 if ($fraction >= 0.5) { $score = ceil($score); } else { $score = round($score); } $whole = floor($weight); // 1 $fraction = $weight - $whole; // .25 if ($fraction >= 0.5) { $weight = ceil($weight); } else { $weight = round($weight); } } else { // Formats values $percentage = float_format($percentage, 1, $decimalSeparator, $thousandSeparator); $score = float_format($score, 1, $decimalSeparator, $thousandSeparator); $weight = float_format($weight, 1, $decimalSeparator, $thousandSeparator); } if ($show_percentage) { $percentageSign = ' %'; if ($hidePercentageSign) { $percentageSign = ''; } $html = $percentage."$percentageSign ($score / $weight)"; if ($show_only_percentage) { $html = $percentage.$percentageSign; } } else { if ($removeEmptyDecimals) { if (ScoreDisplay::hasEmptyDecimals($weight)) { $weight = round($weight); } } $html = $score.' / '.$weight; } // Over write score $scoreBasedInModel = self::convertScoreToModel($percentage); if (!empty($scoreBasedInModel)) { $html = $scoreBasedInModel; } // Ignore other formats and use the configuration['exercise_score_format'] value // But also keep the round values settings. $format = (int) api_get_setting('exercise.exercise_score_format'); if (!empty($format)) { $html = ScoreDisplay::instance()->display_score([$score, $weight], $format); } return Display::span($html, ['class' => 'score_exercise']); } /** * @param array $model * @param float $percentage * * @return string */ public static function getModelStyle($model, $percentage) { return '      '; } /** * @param float $percentage value between 0 and 100 * * @return string */ public static function convertScoreToModel($percentage) { $model = self::getCourseScoreModel(); if (!empty($model)) { $scoreWithGrade = []; foreach ($model['score_list'] as $item) { if ($percentage >= $item['min'] && $percentage <= $item['max']) { $scoreWithGrade = $item; break; } } if (!empty($scoreWithGrade)) { return self::getModelStyle($scoreWithGrade, $percentage); } } return ''; } /** * @return array */ public static function getCourseScoreModel() { $modelList = self::getScoreModels(); if (empty($modelList)) { return []; } $courseInfo = api_get_course_info(); if (!empty($courseInfo)) { $scoreModelId = api_get_course_setting('score_model_id'); if (-1 != $scoreModelId) { $modelIdList = array_column($modelList['models'], 'id'); if (in_array($scoreModelId, $modelIdList)) { foreach ($modelList['models'] as $item) { if ($item['id'] == $scoreModelId) { return $item; } } } } } return []; } /** * @return array */ public static function getScoreModels() { return api_get_setting('exercise.score_grade_model', true); } /** * @param float $score * @param float $weight * @param string $passPercentage * * @return bool */ public static function isSuccessExerciseResult($score, $weight, $passPercentage) { $percentage = float_format( ($score / (0 != $weight ? $weight : 1)) * 100, 1 ); if (isset($passPercentage) && !empty($passPercentage)) { if ($percentage >= $passPercentage) { return true; } } return false; } /** * @param string $name * @param $weight * @param $selected * * @return bool */ public static function addScoreModelInput( FormValidator $form, $name, $weight, $selected ) { $model = self::getCourseScoreModel(); if (empty($model)) { return false; } /** @var HTML_QuickForm_select $element */ $element = $form->createElement( 'select', $name, get_lang('Score'), [], ['class' => 'exercise_mark_select'] ); foreach ($model['score_list'] as $item) { $i = api_number_format($item['score_to_qualify'] / 100 * $weight, 2); $label = self::getModelStyle($item, $i); $attributes = [ 'class' => $item['css_class'], ]; if ($selected == $i) { $attributes['selected'] = 'selected'; } $element->addOption($label, $i, $attributes); } $form->addElement($element); } /** * @return string */ public static function getJsCode() { // Filling the scores with the right colors. $models = self::getCourseScoreModel(); $cssListToString = ''; if (!empty($models)) { $cssList = array_column($models['score_list'], 'css_class'); $cssListToString = implode(' ', $cssList); } if (empty($cssListToString)) { return ''; } $js = << 'width:40px; padding:2px 10px 0px 0px'] ); $res = $html; } return $res; } /** * Return true if pass_pourcentage activated (we use the pass pourcentage feature * return false if pass_percentage = 0 (we don't use the pass pourcentage feature. * * @param $value * * @return bool * In this version, pass_percentage and show_success_message are disabled if * pass_percentage is set to 0 */ public static function isPassPercentageEnabled($value) { return $value > 0; } /** * Converts a numeric value in a percentage example 0.66666 to 66.67 %. * * @param $value * * @return float Converted number */ public static function convert_to_percentage($value) { $return = '-'; if ('' != $value) { $return = float_format($value * 100, 1).' %'; } return $return; } /** * Getting all active exercises from a course from a session * (if a session_id is provided we will show all the exercises in the course + * all exercises in the session). * * @param array $course_info * @param int $session_id * @param bool $check_publication_dates * @param string $search Search exercise name * @param bool $search_all_sessions Search exercises in all sessions * @param int 0 = only inactive exercises * 1 = only active exercises, * 2 = all exercises * 3 = active <> -1 * * @return CQuiz[] */ public static function get_all_exercises( $course_info = null, $session_id = 0, $check_publication_dates = false, $search = '', $search_all_sessions = false, $active = 2 ) { $course_id = api_get_course_int_id(); if (!empty($course_info) && !empty($course_info['real_id'])) { $course_id = $course_info['real_id']; } if (-1 == $session_id) { $session_id = 0; } $course = api_get_course_entity($course_id); $session = api_get_session_entity($session_id); if (null === $course) { return []; } $repo = Container::getQuizRepository(); return $repo->findAllByCourse($course, $session, (string) $search, $active); // Show courses by active status /*if (true == $search_all_sessions) { $conditions = [ 'where' => [ $active_sql.' c_id = ? '.$needle_where.$timeConditions => [ $course_id, $needle, ], ], 'order' => 'title', ]; } else { if (empty($session_id)) { $conditions = [ 'where' => [ $active_sql.' (session_id = 0 OR session_id IS NULL) AND c_id = ? '.$needle_where.$timeConditions => [ $course_id, $needle, ], ], 'order' => 'title', ]; } else { $conditions = [ 'where' => [ $active_sql.' (session_id = 0 OR session_id IS NULL OR session_id = ? ) AND c_id = ? '.$needle_where.$timeConditions => [ $session_id, $course_id, $needle, ], ], 'order' => 'title', ]; } } $table = Database::get_course_table(TABLE_QUIZ_TEST); return Database::select('*', $table, $conditions);*/ } /** * Getting all exercises (active only or all) * from a course from a session * (if a session_id is provided we will show all the exercises in the * course + all exercises in the session). * * @param array course data * @param int session id * @param int course c_id * @param bool $only_active_exercises * * @return array array with exercise data * modified by Hubert Borderiou */ public static function get_all_exercises_for_course_id( $course_info = null, $session_id = 0, $course_id = 0, $only_active_exercises = true ) { $table = Database::get_course_table(TABLE_QUIZ_TEST); if ($only_active_exercises) { // Only active exercises. $sql_active_exercises = "active = 1 AND "; } else { // Not only active means visible and invisible NOT deleted (-2) $sql_active_exercises = "active IN (1, 0) AND "; } if (-1 == $session_id) { $session_id = 0; } $params = [ $session_id, $course_id, ]; if (empty($session_id)) { $conditions = [ 'where' => ["$sql_active_exercises (session_id = 0 OR session_id IS NULL) AND c_id = ?" => [$course_id]], 'order' => 'title', ]; } else { // All exercises $conditions = [ 'where' => ["$sql_active_exercises (session_id = 0 OR session_id IS NULL OR session_id = ? ) AND c_id=?" => $params], 'order' => 'title', ]; } return Database::select('*', $table, $conditions); } /** * Gets the position of the score based in a given score (result/weight) * and the exe_id based in the user list * (NO Exercises in LPs ). * * @param float $my_score user score to be compared *attention* * $my_score = score/weight and not just the score * @param int $my_exe_id exe id of the exercise * (this is necessary because if 2 students have the same score the one * with the minor exe_id will have a best position, just to be fair and FIFO) * @param int $exercise_id * @param string $course_code * @param int $session_id * @param array $user_list * @param bool $return_string * * @return int the position of the user between his friends in a course * (or course within a session) */ public static function get_exercise_result_ranking( $my_score, $my_exe_id, $exercise_id, $course_code, $session_id = 0, $user_list = [], $return_string = true ) { //No score given we return if (is_null($my_score)) { return '-'; } if (empty($user_list)) { return '-'; } $best_attempts = []; foreach ($user_list as $user_data) { $user_id = $user_data['user_id']; $best_attempts[$user_id] = self::get_best_attempt_by_user( $user_id, $exercise_id, $course_code, $session_id ); } if (empty($best_attempts)) { return 1; } else { $position = 1; $my_ranking = []; foreach ($best_attempts as $user_id => $result) { if (!empty($result['max_score']) && 0 != intval($result['max_score'])) { $my_ranking[$user_id] = $result['score'] / $result['max_score']; } else { $my_ranking[$user_id] = 0; } } //if (!empty($my_ranking)) { asort($my_ranking); $position = count($my_ranking); if (!empty($my_ranking)) { foreach ($my_ranking as $user_id => $ranking) { if ($my_score >= $ranking) { if ($my_score == $ranking && isset($best_attempts[$user_id]['exe_id'])) { $exe_id = $best_attempts[$user_id]['exe_id']; if ($my_exe_id < $exe_id) { $position--; } } else { $position--; } } } } //} $return_value = [ 'position' => $position, 'count' => count($my_ranking), ]; if ($return_string) { if (!empty($position) && !empty($my_ranking)) { $return_value = $position.'/'.count($my_ranking); } else { $return_value = '-'; } } return $return_value; } } /** * Gets the position of the score based in a given score (result/weight) and the exe_id based in all attempts * (NO Exercises in LPs ) old functionality by attempt. * * @param float user score to be compared attention => score/weight * @param int exe id of the exercise * (this is necessary because if 2 students have the same score the one * with the minor exe_id will have a best position, just to be fair and FIFO) * @param int exercise id * @param string course code * @param int session id * @param bool $return_string * * @return int the position of the user between his friends in a course (or course within a session) */ public static function get_exercise_result_ranking_by_attempt( $my_score, $my_exe_id, $exercise_id, $courseId, $session_id = 0, $return_string = true ) { if (empty($session_id)) { $session_id = 0; } if (is_null($my_score)) { return '-'; } $user_results = Event::get_all_exercise_results( $exercise_id, $courseId, $session_id, false ); $position_data = []; if (empty($user_results)) { return 1; } else { $position = 1; $my_ranking = []; foreach ($user_results as $result) { if (!empty($result['max_score']) && 0 != intval($result['max_score'])) { $my_ranking[$result['exe_id']] = $result['score'] / $result['max_score']; } else { $my_ranking[$result['exe_id']] = 0; } } asort($my_ranking); $position = count($my_ranking); if (!empty($my_ranking)) { foreach ($my_ranking as $exe_id => $ranking) { if ($my_score >= $ranking) { if ($my_score == $ranking) { if ($my_exe_id < $exe_id) { $position--; } } else { $position--; } } } } $return_value = [ 'position' => $position, 'count' => count($my_ranking), ]; if ($return_string) { if (!empty($position) && !empty($my_ranking)) { return $position.'/'.count($my_ranking); } } return $return_value; } } /** * Get the best attempt in a exercise (NO Exercises in LPs ). * * @param int $exercise_id * @param int $courseId * @param int $session_id * * @return array */ public static function get_best_attempt_in_course($exercise_id, $courseId, $session_id) { $user_results = Event::get_all_exercise_results( $exercise_id, $courseId, $session_id, false ); $best_score_data = []; $best_score = 0; if (!empty($user_results)) { foreach ($user_results as $result) { if (!empty($result['max_score']) && 0 != intval($result['max_score']) ) { $score = $result['score'] / $result['max_score']; if ($score >= $best_score) { $best_score = $score; $best_score_data = $result; } } } } return $best_score_data; } /** * Get the best score in a exercise (NO Exercises in LPs ). * * @param int $user_id * @param int $exercise_id * @param int $courseId * @param int $session_id * * @return array */ public static function get_best_attempt_by_user( $user_id, $exercise_id, $courseId, $session_id ) { $user_results = Event::get_all_exercise_results( $exercise_id, $courseId, $session_id, false, $user_id ); $best_score_data = []; $best_score = 0; if (!empty($user_results)) { foreach ($user_results as $result) { if (!empty($result['max_score']) && 0 != (float) $result['max_score']) { $score = $result['score'] / $result['max_score']; if ($score >= $best_score) { $best_score = $score; $best_score_data = $result; } } } } return $best_score_data; } /** * Get average score (NO Exercises in LPs ). * * @param int exercise id * @param int $courseId * @param int session id * * @return float Average score */ public static function get_average_score($exercise_id, $courseId, $session_id) { $user_results = Event::get_all_exercise_results( $exercise_id, $courseId, $session_id ); $avg_score = 0; if (!empty($user_results)) { foreach ($user_results as $result) { if (!empty($result['max_score']) && 0 != intval($result['max_score'])) { $score = $result['score'] / $result['max_score']; $avg_score += $score; } } $avg_score = float_format($avg_score / count($user_results), 1); } return $avg_score; } /** * Get average score by score (NO Exercises in LPs ). * * @param int $courseId * @param int session id * * @return float Average score */ public static function get_average_score_by_course($courseId, $session_id) { $user_results = Event::get_all_exercise_results_by_course( $courseId, $session_id, false ); $avg_score = 0; if (!empty($user_results)) { foreach ($user_results as $result) { if (!empty($result['max_score']) && 0 != intval( $result['max_score'] ) ) { $score = $result['score'] / $result['max_score']; $avg_score += $score; } } // We assume that all max_score $avg_score = $avg_score / count($user_results); } return $avg_score; } /** * @param int $user_id * @param int $courseId * @param int $session_id * * @return float|int */ public static function get_average_score_by_course_by_user( $user_id, $courseId, $session_id ) { $user_results = Event::get_all_exercise_results_by_user( $user_id, $courseId, $session_id ); $avg_score = 0; if (!empty($user_results)) { foreach ($user_results as $result) { if (!empty($result['max_score']) && 0 != intval($result['max_score'])) { $score = $result['score'] / $result['max_score']; $avg_score += $score; } } // We assume that all max_score $avg_score = ($avg_score / count($user_results)); } return $avg_score; } /** * Get average score by score (NO Exercises in LPs ). * * @param int $exercise_id * @param int $courseId * @param int $session_id * @param int $user_count * * @return float Best average score */ public static function get_best_average_score_by_exercise( $exercise_id, $courseId, $session_id, $user_count ) { $user_results = Event::get_best_exercise_results_by_user( $exercise_id, $courseId, $session_id ); $avg_score = 0; if (!empty($user_results)) { foreach ($user_results as $result) { if (!empty($result['max_score']) && 0 != intval($result['max_score'])) { $score = $result['score'] / $result['max_score']; $avg_score += $score; } } // We asumme that all max_score if (!empty($user_count)) { $avg_score = float_format($avg_score / $user_count, 1) * 100; } else { $avg_score = 0; } } return $avg_score; } /** * Get average score by score (NO Exercises in LPs ). * * @param int $exercise_id * @param int $courseId * @param int $session_id * * @return float Best average score */ public static function getBestScoreByExercise( $exercise_id, $courseId, $session_id ) { $user_results = Event::get_best_exercise_results_by_user( $exercise_id, $courseId, $session_id ); $avg_score = 0; if (!empty($user_results)) { foreach ($user_results as $result) { if (!empty($result['max_score']) && 0 != intval($result['max_score'])) { $score = $result['score'] / $result['max_score']; $avg_score += $score; } } } return $avg_score; } /** * Get student results (only in completed exercises) stats by question. * * @throws \Doctrine\DBAL\Exception */ public static function getStudentStatsByQuestion( int $questionId, int $exerciseId, string $courseCode, int $sessionId, bool $onlyStudent = false ): array { $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); $trackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT); $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER); $questionId = (int) $questionId; $exerciseId = (int) $exerciseId; $courseCode = Database::escape_string($courseCode); $sessionId = (int) $sessionId; $courseId = api_get_course_int_id($courseCode); $sql = "SELECT MAX(marks) as max, MIN(marks) as min, AVG(marks) as average FROM $trackExercises e "; $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id'); if ($onlyStudent) { $courseCondition = ''; if (empty($sessionId)) { $courseCondition = " INNER JOIN $courseUser c ON ( e.exe_user_id = c.user_id AND e.c_id = c.c_id AND c.status = ".STUDENT." AND relation_type <> 2 )"; } else { $sessionRelCourse = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER); $courseCondition = " INNER JOIN $sessionRelCourse sc ON ( e.exe_user_id = sc.user_id AND e.c_id = sc.c_id AND e.session_id = sc.session_id AND sc.status = ".SessionEntity::STUDENT." )"; } $sql .= $courseCondition; } $sql .= " INNER JOIN $trackAttempt a ON ( a.exe_id = e.exe_id ) WHERE exe_exo_id = $exerciseId AND e.c_id = $courseId AND question_id = $questionId AND e.status = '' $sessionCondition LIMIT 1"; $result = Database::query($sql); $return = []; if ($result) { $return = Database::fetch_array($result, 'ASSOC'); } return $return; } /** * Get the correct answer count for a fill blanks question. * * @param int $question_id * @param int $exercise_id * * @return array */ public static function getNumberStudentsFillBlanksAnswerCount( $question_id, $exercise_id ) { $listStudentsId = []; $listAllStudentInfo = CourseManager::get_student_list_from_course_code( api_get_course_id(), true ); foreach ($listAllStudentInfo as $i => $listStudentInfo) { $listStudentsId[] = $listStudentInfo['user_id']; } $listFillTheBlankResult = FillBlanks::getFillTheBlankResult( $exercise_id, $question_id, $listStudentsId, '1970-01-01', '3000-01-01' ); $arrayCount = []; foreach ($listFillTheBlankResult as $resultCount) { foreach ($resultCount as $index => $count) { //this is only for declare the array index per answer $arrayCount[$index] = 0; } } foreach ($listFillTheBlankResult as $resultCount) { foreach ($resultCount as $index => $count) { $count = (0 === $count) ? 1 : 0; $arrayCount[$index] += $count; } } return $arrayCount; } /** * Get the number of questions with answers. * * @param int $question_id * @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, $questionType = '' ) { $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT); $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER); $courseTable = Database::get_main_table(TABLE_MAIN_COURSE); $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER); $question_id = intval($question_id); $exercise_id = intval($exercise_id); $courseId = api_get_course_int_id($course_code); $session_id = intval($session_id); if (FILL_IN_BLANKS == $questionType) { $listStudentsId = []; $listAllStudentInfo = CourseManager::get_student_list_from_course_code( api_get_course_id(), true ); foreach ($listAllStudentInfo as $i => $listStudentInfo) { $listStudentsId[] = $listStudentInfo['user_id']; } $listFillTheBlankResult = FillBlanks::getFillTheBlankResult( $exercise_id, $question_id, $listStudentsId, '1970-01-01', '3000-01-01' ); return FillBlanks::getNbResultFillBlankAll($listFillTheBlankResult); } if (empty($session_id)) { $courseCondition = " INNER JOIN $courseUser cu ON cu.c_id = c.id AND cu.user_id = exe_user_id"; $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT; } else { $courseCondition = " INNER JOIN $courseUserSession cu ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)"; $courseConditionWhere = " AND cu.status = ".SessionEntity::STUDENT; } $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id'); $sql = "SELECT DISTINCT exe_user_id FROM $track_exercises e INNER JOIN $track_attempt a ON ( a.exe_id = e.exe_id ) INNER JOIN $courseTable c ON (c.id = e.c_id) $courseCondition WHERE exe_exo_id = $exercise_id AND e.c_id = $courseId AND question_id = $question_id AND answer <> '0' AND e.status = '' $courseConditionWhere $sessionCondition "; $result = Database::query($sql); $return = 0; if ($result) { $return = Database::num_rows($result); } return $return; } /** * Get number of answers to hotspot questions. */ public static function getNumberStudentsAnswerHotspotCount( int $answerId, int $questionId, int $exerciseId, string $courseCode, int $sessionId ): int { $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); $trackHotspot = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT); $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER); $courseTable = Database::get_main_table(TABLE_MAIN_COURSE); $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER); $questionId = (int) $questionId; $answerId = (int) $answerId; $exerciseId = (int) $exerciseId; $courseId = api_get_course_int_id($courseCode); $sessionId = (int) $sessionId; if (empty($sessionId)) { $courseCondition = " INNER JOIN $courseUser cu ON cu.c_id = c.id AND cu.user_id = exe_user_id"; $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT; } else { $courseCondition = " INNER JOIN $courseUserSession cu ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)"; $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT; } $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id'); $sql = "SELECT DISTINCT exe_user_id FROM $trackExercises e INNER JOIN $trackHotspot a ON (a.hotspot_exe_id = e.exe_id) INNER JOIN $courseTable c ON (a.c_id = c.id) $courseCondition WHERE exe_exo_id = $exerciseId AND a.c_id = $courseId AND hotspot_answer_id = $answerId AND hotspot_question_id = $questionId AND hotspot_correct = 1 AND e.status = '' $courseConditionWhere $sessionCondition "; $result = Database::query($sql); $return = 0; if ($result) { $return = Database::num_rows($result); } return $return; } /** * @param int $answer_id * @param int $question_id * @param int $exercise_id * @param string $course_code * @param int $session_id * @param string $question_type * @param string $correct_answer * @param string $current_answer * * @return int */ public static function get_number_students_answer_count( $answer_id, $question_id, $exercise_id, $course_code, $session_id, $question_type = null, $correct_answer = null, $current_answer = null ) { $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT); $courseTable = Database::get_main_table(TABLE_MAIN_COURSE); $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER); $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER); $question_id = (int) $question_id; $answer_id = (int) $answer_id; $exercise_id = (int) $exercise_id; $courseId = api_get_course_int_id($course_code); $session_id = (int) $session_id; switch ($question_type) { case FILL_IN_BLANKS: $answer_condition = ''; $select_condition = ' e.exe_id, answer '; break; case MATCHING: case MATCHING_DRAGGABLE: default: $answer_condition = " answer = $answer_id AND "; $select_condition = ' DISTINCT exe_user_id '; } if (empty($session_id)) { $courseCondition = " INNER JOIN $courseUser cu ON cu.c_id = c.id AND cu.user_id = exe_user_id"; $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT; } else { $courseCondition = " INNER JOIN $courseUserSession cu ON (cu.c_id = a.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)"; $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT; } $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id'); $sql = "SELECT $select_condition FROM $track_exercises e INNER JOIN $track_attempt a ON ( a.exe_id = e.exe_id ) INNER JOIN $courseTable c ON c.id = e.c_id $courseCondition WHERE exe_exo_id = $exercise_id AND e.c_id = $courseId AND $answer_condition question_id = $question_id AND e.status = '' $courseConditionWhere $sessionCondition "; $result = Database::query($sql); $return = 0; if ($result) { $good_answers = 0; switch ($question_type) { case FILL_IN_BLANKS: while ($row = Database::fetch_array($result, 'ASSOC')) { $fill_blank = self::check_fill_in_blanks( $correct_answer, $row['answer'], $current_answer ); if (isset($fill_blank[$current_answer]) && 1 == $fill_blank[$current_answer]) { $good_answers++; } } return $good_answers; break; case MATCHING: case MATCHING_DRAGGABLE: default: $return = Database::num_rows($result); } } return $return; } /** * Get the number of times an answer was selected. */ public static function getCountOfAnswers( int $answerId, int $questionId, int $exerciseId, string $courseCode, int $sessionId, $questionType = null, ): int { $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); $trackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT); $courseTable = Database::get_main_table(TABLE_MAIN_COURSE); $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER); $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER); $answerId = (int) $answerId; $questionId = (int) $questionId; $exerciseId = (int) $exerciseId; $courseId = api_get_course_int_id($courseCode); $sessionId = (int) $sessionId; $return = 0; $answerCondition = match ($questionType) { FILL_IN_BLANKS => '', default => " answer = $answerId AND ", }; if (empty($sessionId)) { $courseCondition = " INNER JOIN $courseUser cu ON cu.c_id = c.id AND cu.user_id = exe_user_id"; $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT; } else { $courseCondition = " INNER JOIN $courseUserSession cu ON (cu.c_id = a.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)"; $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT; } $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id'); $sql = "SELECT count(a.answer) as total FROM $trackExercises e INNER JOIN $trackAttempt a ON ( a.exe_id = e.exe_id ) INNER JOIN $courseTable c ON c.id = e.c_id $courseCondition WHERE exe_exo_id = $exerciseId AND e.c_id = $courseId AND $answerCondition question_id = $questionId AND e.status = '' $courseConditionWhere $sessionCondition "; $result = Database::query($sql); if ($result) { $count = Database::fetch_array($result); $return = (int) $count['total']; } return $return; } /** * @param array $answer * @param string $user_answer * * @return array */ public static function check_fill_in_blanks($answer, $user_answer, $current_answer) { // 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; $is_set_switchable = explode('@', $pre_array[$last]); $switchable_answer_set = false; if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) { $switchable_answer_set = true; } $answer = ''; for ($k = 0; $k < $last; $k++) { $answer .= $pre_array[$k]; } // splits weightings that are joined with a comma $answerWeighting = explode(',', $is_set_switchable[0]); // we save the answer because it will be modified //$temp = $answer; $temp = $answer; $answer = ''; $j = 0; //initialise answer tags $user_tags = $correct_tags = $real_text = []; // the loop will stop at the end of the text while (1) { // quits the loop if there are no more blanks (detect '[') if (false === ($pos = api_strpos($temp, '['))) { // adds the end of the text $answer = $temp; $real_text[] = $answer; break; //no more "blanks", quit the loop } // adds the piece of text that is before the blank //and ends with '[' into a general storage array $real_text[] = api_substr($temp, 0, $pos + 1); $answer .= api_substr($temp, 0, $pos + 1); //take the string remaining (after the last "[" we found) $temp = api_substr($temp, $pos + 1); // quit the loop if there are no more blanks, and update $pos to the position of next ']' if (false === ($pos = api_strpos($temp, ']'))) { // adds the end of the text $answer .= $temp; break; } $str = $user_answer; preg_match_all('#\[([^[]*)\]#', $str, $arr); $str = str_replace('\r\n', '', $str); $choices = $arr[1]; $choice = []; $check = false; $i = 0; foreach ($choices as $item) { if ($current_answer === $item) { $check = true; } if ($check) { $choice[] = $item; $i++; } if (3 == $i) { break; } } $tmp = api_strrpos($choice[$j], ' / '); if (false !== $tmp) { $choice[$j] = api_substr($choice[$j], 0, $tmp); } $choice[$j] = trim($choice[$j]); //Needed to let characters ' and " to work as part of an answer $choice[$j] = stripslashes($choice[$j]); $user_tags[] = api_strtolower($choice[$j]); //put the contents of the [] answer tag into correct_tags[] $correct_tags[] = api_strtolower(api_substr($temp, 0, $pos)); $j++; $temp = api_substr($temp, $pos + 1); } $answer = ''; $real_correct_tags = $correct_tags; $chosen_list = []; $good_answer = []; for ($i = 0; $i < count($real_correct_tags); $i++) { if (!$switchable_answer_set) { //needed to parse ' and " characters $user_tags[$i] = stripslashes($user_tags[$i]); if ($correct_tags[$i] == $user_tags[$i]) { $good_answer[$correct_tags[$i]] = 1; } elseif (!empty($user_tags[$i])) { $good_answer[$correct_tags[$i]] = 0; } else { $good_answer[$correct_tags[$i]] = 0; } } else { // switchable fill in the blanks if (in_array($user_tags[$i], $correct_tags)) { $correct_tags = array_diff($correct_tags, $chosen_list); $good_answer[$correct_tags[$i]] = 1; } elseif (!empty($user_tags[$i])) { $good_answer[$correct_tags[$i]] = 0; } else { $good_answer[$correct_tags[$i]] = 0; } } // adds the correct word, followed by ] to close the blank $answer .= ' / '.$real_correct_tags[$i].']'; if (isset($real_text[$i + 1])) { $answer .= $real_text[$i + 1]; } } return $good_answer; } /** * Return an HTML select menu with the student groups. * * @param string $name is the name and the id of the */ public static function displayGroupMenu($name, $default, $onchange = "") { // check the default value of option $tabSelected = [$default => " selected='selected' "]; $res = ""; return $res; } /** * @param int $exe_id */ public static function create_chat_exercise_session($exe_id) { if (!isset($_SESSION['current_exercises'])) { $_SESSION['current_exercises'] = []; } $_SESSION['current_exercises'][$exe_id] = true; } /** * @param int $exe_id */ public static function delete_chat_exercise_session($exe_id) { if (isset($_SESSION['current_exercises'])) { $_SESSION['current_exercises'][$exe_id] = false; } } /** * Display the exercise results. * * @param Exercise $objExercise * @param int $exeId * @param bool $save_user_result save users results (true) or just show the results (false) * @param string $remainingMessage * @param bool $allowSignature * @param bool $allowExportPdf * @param bool $isExport */ public static function displayQuestionListByAttempt( $objExercise, $exeId, $save_user_result = false, $remainingMessage = '', $allowSignature = false, $allowExportPdf = false, $isExport = false ) { $origin = api_get_origin(); $courseId = api_get_course_int_id(); $courseCode = api_get_course_id(); $sessionId = api_get_session_id(); // Getting attempt info $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId); // Getting question list $question_list = []; $studentInfo = []; if (!empty($exercise_stat_info['data_tracking'])) { $studentInfo = api_get_user_info($exercise_stat_info['exe_user_id']); $question_list = explode(',', $exercise_stat_info['data_tracking']); } else { // Try getting the question list only if save result is off if (false == $save_user_result) { $question_list = $objExercise->get_validated_question_list(); } if (in_array( $objExercise->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP] )) { $question_list = $objExercise->get_validated_question_list(); } } if ($objExercise->getResultAccess()) { if (false === $objExercise->hasResultsAccess($exercise_stat_info)) { echo Display::return_message( sprintf(get_lang('YouPassedTheLimitOfXMinutesToSeeTheResults'), $objExercise->getResultsAccess()) ); return false; } if (!empty($objExercise->getResultAccess())) { $url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id; echo $objExercise->returnTimeLeftDiv(); echo $objExercise->showSimpleTimeControl( $objExercise->getResultAccessTimeDiff($exercise_stat_info), $url ); } } $counter = 1; $total_score = $total_weight = 0; $exerciseContent = null; // Hide results $show_results = false; $show_only_score = false; if (in_array($objExercise->results_disabled, [ RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING, ] )) { $show_results = true; } if (in_array( $objExercise->results_disabled, [ RESULT_DISABLE_SHOW_SCORE_ONLY, RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES, RESULT_DISABLE_RANKING, ] ) ) { $show_only_score = true; } // Not display expected answer, but score, and feedback $show_all_but_expected_answer = false; if (RESULT_DISABLE_SHOW_SCORE_ONLY == $objExercise->results_disabled && EXERCISE_FEEDBACK_TYPE_END == $objExercise->getFeedbackType() ) { $show_all_but_expected_answer = true; $show_results = true; $show_only_score = false; } $showTotalScoreAndUserChoicesInLastAttempt = true; $showTotalScore = true; $showQuestionScore = true; $attemptResult = []; if (in_array( $objExercise->results_disabled, [ RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT, RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK, RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK, ]) ) { $show_only_score = true; $show_results = true; $numberAttempts = 0; if ($objExercise->attempts > 0) { $attempts = Event::getExerciseResultsByUser( api_get_user_id(), $objExercise->id, $courseId, $sessionId, $exercise_stat_info['orig_lp_id'], $exercise_stat_info['orig_lp_item_id'], 'desc' ); if ($attempts) { $numberAttempts = count($attempts); } if ($save_user_result) { $numberAttempts++; } $showTotalScore = false; if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT == $objExercise->results_disabled) { $showTotalScore = true; } $showTotalScoreAndUserChoicesInLastAttempt = false; if ($numberAttempts >= $objExercise->attempts) { $showTotalScore = true; $show_results = true; $show_only_score = false; $showTotalScoreAndUserChoicesInLastAttempt = true; } if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $objExercise->results_disabled) { $showTotalScore = true; $show_results = true; $show_only_score = false; $showTotalScoreAndUserChoicesInLastAttempt = false; if ($numberAttempts >= $objExercise->attempts) { $showTotalScoreAndUserChoicesInLastAttempt = true; } // Check if the current attempt is the last. if (false === $save_user_result && !empty($attempts)) { $showTotalScoreAndUserChoicesInLastAttempt = false; $position = 1; foreach ($attempts as $attempt) { if ($exeId == $attempt['exe_id']) { break; } $position++; } if ($position == $objExercise->attempts) { $showTotalScoreAndUserChoicesInLastAttempt = true; } } } } if (RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK == $objExercise->results_disabled ) { $show_only_score = false; $show_results = true; $show_all_but_expected_answer = false; $showTotalScore = false; $showQuestionScore = false; if ($numberAttempts >= $objExercise->attempts) { $showTotalScore = true; $showQuestionScore = true; } } } // When exporting to PDF hide feedback/comment/score show warning in hotspot. if ($allowExportPdf && $isExport) { $showTotalScore = false; $showQuestionScore = false; $objExercise->feedback_type = 2; $objExercise->hideComment = true; $objExercise->hideNoAnswer = true; $objExercise->results_disabled = 0; $objExercise->hideExpectedAnswer = true; $show_results = true; } if ('embeddable' !== $origin && !empty($exercise_stat_info['exe_user_id']) && !empty($studentInfo) ) { // Shows exercise header. echo $objExercise->showExerciseResultHeader( $studentInfo, $exercise_stat_info, $save_user_result, $allowSignature, $allowExportPdf ); } $question_list_answers = []; $category_list = []; $loadChoiceFromSession = false; $fromDatabase = true; $exerciseResult = null; $exerciseResultCoordinates = null; $delineationResults = null; if (true === $save_user_result && in_array( $objExercise->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP] )) { $loadChoiceFromSession = true; $fromDatabase = false; $exerciseResult = Session::read('exerciseResult'); $exerciseResultCoordinates = Session::read('exerciseResultCoordinates'); $delineationResults = Session::read('hotspot_delineation_result'); $delineationResults = isset($delineationResults[$objExercise->id]) ? $delineationResults[$objExercise->id] : null; } $countPendingQuestions = 0; $result = []; // Loop over all question to show results for each of them, one by one if (!empty($question_list)) { foreach ($question_list as $questionId) { // Creates a temporary Question object $objQuestionTmp = Question::read($questionId, $objExercise->course); // This variable came from exercise_submit_modal.php ob_start(); $choice = null; $delineationChoice = null; if ($loadChoiceFromSession) { $choice = isset($exerciseResult[$questionId]) ? $exerciseResult[$questionId] : null; $delineationChoice = isset($delineationResults[$questionId]) ? $delineationResults[$questionId] : null; } // We're inside *one* question. Go through each possible answer for this question $result = $objExercise->manage_answer( $exeId, $questionId, $choice, 'exercise_result', $exerciseResultCoordinates, $save_user_result, $fromDatabase, $show_results, $objExercise->selectPropagateNeg(), $delineationChoice, $showTotalScoreAndUserChoicesInLastAttempt ); if (empty($result)) { continue; } $total_score += $result['score']; $total_weight += $result['weight']; $question_list_answers[] = [ 'question' => $result['open_question'], 'answer' => $result['open_answer'], 'answer_type' => $result['answer_type'], 'generated_oral_file' => $result['generated_oral_file'], ]; $my_total_score = $result['score']; $my_total_weight = $result['weight']; $scorePassed = self::scorePassed($my_total_score, $my_total_weight); // Category report $category_was_added_for_this_test = false; if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) { if (!isset($category_list[$objQuestionTmp->category]['score'])) { $category_list[$objQuestionTmp->category]['score'] = 0; } if (!isset($category_list[$objQuestionTmp->category]['total'])) { $category_list[$objQuestionTmp->category]['total'] = 0; } if (!isset($category_list[$objQuestionTmp->category]['total_questions'])) { $category_list[$objQuestionTmp->category]['total_questions'] = 0; } if (!isset($category_list[$objQuestionTmp->category]['passed'])) { $category_list[$objQuestionTmp->category]['passed'] = 0; } if (!isset($category_list[$objQuestionTmp->category]['wrong'])) { $category_list[$objQuestionTmp->category]['wrong'] = 0; } if (!isset($category_list[$objQuestionTmp->category]['no_answer'])) { $category_list[$objQuestionTmp->category]['no_answer'] = 0; } $category_list[$objQuestionTmp->category]['score'] += $my_total_score; $category_list[$objQuestionTmp->category]['total'] += $my_total_weight; if ($scorePassed) { // Only count passed if score is not empty if (!empty($my_total_score)) { $category_list[$objQuestionTmp->category]['passed']++; } } else { if ($result['user_answered']) { $category_list[$objQuestionTmp->category]['wrong']++; } else { $category_list[$objQuestionTmp->category]['no_answer']++; } } $category_list[$objQuestionTmp->category]['total_questions']++; $category_was_added_for_this_test = true; } if (isset($objQuestionTmp->category_list) && !empty($objQuestionTmp->category_list)) { foreach ($objQuestionTmp->category_list as $category_id) { $category_list[$category_id]['score'] += $my_total_score; $category_list[$category_id]['total'] += $my_total_weight; $category_was_added_for_this_test = true; } } // No category for this question! if (false == $category_was_added_for_this_test) { if (!isset($category_list['none']['score'])) { $category_list['none']['score'] = 0; } if (!isset($category_list['none']['total'])) { $category_list['none']['total'] = 0; } $category_list['none']['score'] += $my_total_score; $category_list['none']['total'] += $my_total_weight; } if (0 == $objExercise->selectPropagateNeg() && $my_total_score < 0) { $my_total_score = 0; } $comnt = null; if ($show_results) { $comnt = Event::get_comments($exeId, $questionId); $teacherAudio = self::getOralFeedbackAudio( $exeId, $questionId, api_get_user_id() ); if (!empty($comnt) || $teacherAudio) { echo ''.get_lang('Feedback').''; } if (!empty($comnt)) { echo self::getFeedbackText($comnt); } if ($teacherAudio) { echo $teacherAudio; } } $calculatedScore = [ 'result' => self::show_score( $my_total_score, $my_total_weight, false ), 'pass' => $scorePassed, 'score' => $my_total_score, 'weight' => $my_total_weight, 'comments' => $comnt, 'user_answered' => $result['user_answered'], ]; $score = []; if ($show_results) { $score = $calculatedScore; } if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION])) { $reviewScore = [ 'score' => $my_total_score, 'comments' => Event::get_comments($exeId, $questionId), ]; $check = $objQuestionTmp->isQuestionWaitingReview($reviewScore); if (false === $check) { $countPendingQuestions++; } } $contents = ob_get_clean(); $questionContent = ''; if ($show_results) { $questionContent = '
    '; if (false === $showQuestionScore) { $score = []; } // Shows question title an description $questionContent .= $objQuestionTmp->return_header( $objExercise, $counter, $score ); } $counter++; $questionContent .= $contents; if ($show_results) { $questionContent .= '
    '; } $calculatedScore['question_content'] = $questionContent; $attemptResult[] = $calculatedScore; if ($objExercise->showExpectedChoice()) { $exerciseContent .= Display::panel($questionContent); } else { // $show_all_but_expected_answer should not happen at // the same time as $show_results if ($show_results && !$show_only_score) { $exerciseContent .= Display::panel($questionContent); } } } } // Display text when test is finished #4074 and for LP #4227 $endOfMessage = $objExercise->getFinishText($total_score, $total_weight); if (!empty($endOfMessage)) { echo Display::div( $endOfMessage, ['id' => 'quiz_end_message'] ); } $totalScoreText = null; $certificateBlock = ''; if (($show_results || $show_only_score) && $showTotalScore) { if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) { echo '

    '.get_lang('Your results').'


    '; } $totalScoreText .= '
    '; if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) { $totalScoreText .= self::getQuestionDiagnosisRibbon( $objExercise, $total_score, $total_weight, true ); } else { $pluginEvaluation = QuestionOptionsEvaluationPlugin::create(); if ('true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)) { $formula = $pluginEvaluation->getFormulaForExercise($objExercise->getId()); if (!empty($formula)) { $total_score = $pluginEvaluation->getResultWithFormula($exeId, $formula); $total_weight = $pluginEvaluation->getMaxScore(); } } $totalScoreText .= self::getTotalScoreRibbon( $objExercise, $total_score, $total_weight, true, $countPendingQuestions ); } $totalScoreText .= '
    '; if (!empty($studentInfo)) { $certificateBlock = self::generateAndShowCertificateBlock( $total_score, $total_weight, $objExercise, $studentInfo['id'], $courseId, $sessionId ); } } if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) { $chartMultiAnswer = MultipleAnswerTrueFalseDegreeCertainty::displayStudentsChartResults( $exeId, $objExercise ); echo $chartMultiAnswer; } if (!empty($category_list) && ($show_results || $show_only_score || RESULT_DISABLE_RADAR == $objExercise->results_disabled) ) { // Adding total $category_list['total'] = [ 'score' => $total_score, 'total' => $total_weight, ]; echo TestCategory::get_stats_table_by_attempt($objExercise, $category_list); } if ($show_all_but_expected_answer) { $exerciseContent .= Display::return_message(get_lang('Note: This test has been setup to hide the expected answers.')); } // Remove audio auto play from questions on results page - refs BT#7939 $exerciseContent = preg_replace( ['/autoplay[\=\".+\"]+/', '/autostart[\=\".+\"]+/'], '', $exerciseContent ); echo $totalScoreText; echo $certificateBlock; // Ofaj change BT#11784 if (('true' === api_get_setting('exercise.quiz_show_description_on_results_page')) && !empty($objExercise->description) ) { echo Display::div($objExercise->description, ['class' => 'exercise_description']); } echo $exerciseContent; if (!$show_only_score) { echo $totalScoreText; } if ($save_user_result) { // Tracking of results if ($exercise_stat_info) { $learnpath_id = $exercise_stat_info['orig_lp_id']; $learnpath_item_id = $exercise_stat_info['orig_lp_item_id']; $learnpath_item_view_id = $exercise_stat_info['orig_lp_item_view_id']; if (api_is_allowed_to_session_edit()) { Event::updateEventExercise( $exercise_stat_info['exe_id'], $objExercise->getId(), $total_score, $total_weight, $sessionId, $learnpath_id, $learnpath_item_id, $learnpath_item_view_id, $exercise_stat_info['exe_duration'], $question_list ); $allowStats = ('true' === api_get_setting('gradebook.allow_gradebook_stats')); if ($allowStats) { $objExercise->generateStats( $objExercise->getId(), api_get_course_info(), $sessionId ); } } } // Send notification at the end if (!api_is_allowed_to_edit(null, true) && !api_is_excluded_user_type() ) { $objExercise->send_mail_notification_for_exam( 'end', $question_list_answers, $origin, $exeId, $total_score, $total_weight ); } } if (in_array( $objExercise->selectResultsDisabled(), [RESULT_DISABLE_RANKING, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING] )) { echo Display::page_header(get_lang('Ranking'), null, 'h4'); echo self::displayResultsInRanking( $objExercise, api_get_user_id(), $courseId, $sessionId ); } if (!empty($remainingMessage)) { echo Display::return_message($remainingMessage, 'normal', false); } $failedAnswersCount = 0; $wrongQuestionHtml = ''; $all = ''; foreach ($attemptResult as $item) { if (false === $item['pass']) { $failedAnswersCount++; $wrongQuestionHtml .= $item['question_content'].'
    '; } $all .= $item['question_content'].'
    '; } $passed = self::isPassPercentageAttemptPassed( $objExercise, $total_score, $total_weight ); $percentage = 0; if (!empty($total_weight)) { $percentage = ($total_score / $total_weight) * 100; } return [ 'category_list' => $category_list, 'attempts_result_list' => $attemptResult, // array of results 'exercise_passed' => $passed, // boolean 'total_answers_count' => count($attemptResult), // int 'failed_answers_count' => $failedAnswersCount, // int 'failed_answers_html' => $wrongQuestionHtml, 'all_answers_html' => $all, 'total_score' => $total_score, 'total_weight' => $total_weight, 'total_percentage' => $percentage, 'count_pending_questions' => $countPendingQuestions, ]; } /** * Display the ranking of results in a exercise. * * @param Exercise $exercise * @param int $currentUserId * @param int $courseId * @param int $sessionId * * @return string */ public static function displayResultsInRanking($exercise, $currentUserId, $courseId, $sessionId = 0) { $exerciseId = $exercise->iId; $data = self::exerciseResultsInRanking($exerciseId, $courseId, $sessionId); $table = new HTML_Table(['class' => 'table table-hover table-striped table-bordered']); $table->setHeaderContents(0, 0, get_lang('Position'), ['class' => 'text-right']); $table->setHeaderContents(0, 1, get_lang('Username')); $table->setHeaderContents(0, 2, get_lang('Score'), ['class' => 'text-right']); $table->setHeaderContents(0, 3, get_lang('Date'), ['class' => 'text-center']); foreach ($data as $r => $item) { if (!isset($item[1])) { continue; } $selected = $item[1]->getId() == $currentUserId; foreach ($item as $c => $value) { $table->setCellContents($r + 1, $c, $value); $attrClass = ''; if (in_array($c, [0, 2])) { $attrClass = 'text-right'; } elseif (3 == $c) { $attrClass = 'text-center'; } if ($selected) { $attrClass .= ' warning'; } $table->setCellAttributes($r + 1, $c, ['class' => $attrClass]); } } return $table->toHtml(); } /** * Get the ranking for results in a exercise. * Function used internally by ExerciseLib::displayResultsInRanking. * * @param int $exerciseId * @param int $courseId * @param int $sessionId * * @return array */ public static function exerciseResultsInRanking($exerciseId, $courseId, $sessionId = 0) { $em = Database::getManager(); $dql = 'SELECT DISTINCT te.exeUserId FROM ChamiloCoreBundle:TrackEExercise te WHERE te.exeExoId = :id AND te.course = :cId'; $dql .= api_get_session_condition($sessionId, true, false, 'te.sessionId'); $result = $em ->createQuery($dql) ->setParameters(['id' => $exerciseId, 'cId' => $courseId]) ->getScalarResult(); $data = []; foreach ($result as $item) { $data[] = self::get_best_attempt_by_user($item['exeUserId'], $exerciseId, $courseId, $sessionId); } usort( $data, function ($a, $b) { if ($a['score'] != $b['score']) { return $a['score'] > $b['score'] ? -1 : 1; } if ($a['exe_date'] != $b['exe_date']) { return $a['exe_date'] < $b['exe_date'] ? -1 : 1; } return 0; } ); // flags to display the same position in case of tie $lastScore = $data[0]['score']; $position = 1; $data = array_map( function ($item) use (&$lastScore, &$position) { if ($item['score'] < $lastScore) { $position++; } $lastScore = $item['score']; return [ $position, api_get_user_entity($item['exe_user_id']), self::show_score($item['score'], $item['max_score'], true, true, true), api_convert_and_format_date($item['exe_date'], DATE_TIME_FORMAT_SHORT), ]; }, $data ); return $data; } /** * Get a special ribbon on top of "degree of certainty" questions ( * variation from getTotalScoreRibbon() for other question types). * * @param Exercise $objExercise * @param float $score * @param float $weight * @param bool $checkPassPercentage * * @return string */ public static function getQuestionDiagnosisRibbon($objExercise, $score, $weight, $checkPassPercentage = false) { $displayChartDegree = true; $ribbon = $displayChartDegree ? '
    ' : ''; if ($checkPassPercentage) { $passPercentage = $objExercise->selectPassPercentage(); $isSuccess = self::isSuccessExerciseResult($score, $weight, $passPercentage); // Color the final test score if pass_percentage activated $ribbonTotalSuccessOrError = ''; if (self::isPassPercentageEnabled($passPercentage)) { if ($isSuccess) { $ribbonTotalSuccessOrError = ' ribbon-total-success'; } else { $ribbonTotalSuccessOrError = ' ribbon-total-error'; } } $ribbon .= $displayChartDegree ? '
    ' : ''; } else { $ribbon .= $displayChartDegree ? '
    ' : ''; } if ($displayChartDegree) { $ribbon .= '

    '.get_lang('Score for the test').': '; $ribbon .= self::show_score($score, $weight, false, true); $ribbon .= '

    '; $ribbon .= '
    '; } if ($checkPassPercentage) { $ribbon .= self::showSuccessMessage( $score, $weight, $objExercise->selectPassPercentage() ); } $ribbon .= $displayChartDegree ? '
    ' : ''; return $ribbon; } public static function isPassPercentageAttemptPassed($objExercise, $score, $weight) { $passPercentage = $objExercise->selectPassPercentage(); return self::isSuccessExerciseResult($score, $weight, $passPercentage); } /** * @param float $score * @param float $weight * @param bool $checkPassPercentage * @param int $countPendingQuestions * * @return string */ public static function getTotalScoreRibbon( Exercise $objExercise, $score, $weight, $checkPassPercentage = false, $countPendingQuestions = 0 ) { $hide = (int) $objExercise->getPageConfigurationAttribute('hide_total_score'); if (1 === $hide) { return ''; } $passPercentage = $objExercise->selectPassPercentage(); $ribbon = '
    '; if ($checkPassPercentage) { $isSuccess = self::isSuccessExerciseResult( $score, $weight, $passPercentage ); // Color the final test score if pass_percentage activated $class = ''; if (self::isPassPercentageEnabled($passPercentage)) { if ($isSuccess) { $class = ' ribbon-total-success'; } else { $class = ' ribbon-total-error'; } } $ribbon .= '
    '; } else { $ribbon .= '
    '; } $ribbon .= '

    '.get_lang('Score for the test').': '; $ribbon .= self::show_score($score, $weight, false, true); $ribbon .= '

    '; $ribbon .= '
    '; if ($checkPassPercentage) { $ribbon .= self::showSuccessMessage( $score, $weight, $passPercentage ); } $ribbon .= '
    '; if (!empty($countPendingQuestions)) { $ribbon .= '
    '; $ribbon .= Display::return_message( sprintf( get_lang('Temporary score: %s open question(s) not corrected yet.'), $countPendingQuestions ), 'warning' ); } return $ribbon; } /** * @param int $countLetter * * @return mixed */ public static function detectInputAppropriateClass($countLetter) { $limits = [ 0 => 'input-mini', 10 => 'input-mini', 15 => 'input-medium', 20 => 'input-xlarge', 40 => 'input-xlarge', 60 => 'input-xxlarge', 100 => 'input-xxlarge', 200 => 'input-xxlarge', ]; foreach ($limits as $size => $item) { if ($countLetter <= $size) { return $item; } } return $limits[0]; } /** * @param int $senderId * @param array $course_info * @param string $test * @param string $url * * @return string */ public static function getEmailNotification($senderId, $course_info, $test, $url) { $teacher_info = api_get_user_info($senderId); $fromName = api_get_person_name( $teacher_info['firstname'], $teacher_info['lastname'], null, PERSON_NAME_EMAIL_ADDRESS ); $params = [ 'course_title' => Security::remove_XSS($course_info['name']), 'test_title' => Security::remove_XSS($test), 'url' => $url, 'teacher_name' => $fromName, ]; return Container::getTwig()->render( '@ChamiloCore/Mailer/Exercise/result_alert_body.html.twig', $params ); } /** * @return string */ public static function getNotCorrectedYetText() { return Display::return_message(get_lang('This answer has not yet been corrected. Meanwhile, your score for this question is set to 0, affecting the total score.'), 'warning'); } /** * @param string $message * * @return string */ public static function getFeedbackText($message) { return Display::return_message($message, 'warning', false); } /** * Get the recorder audio component for save a teacher audio feedback. * * @param int $attemptId * @param int $questionId * * @return string */ public static function getOralFeedbackForm($attemptId, $questionId) { $view = new Template('', false, false, false, false, false, false); $view->assign('type', Asset::EXERCISE_FEEDBACK); $view->assign('question_id', $questionId); $view->assign('t_exercise_id', $attemptId); $template = $view->get_template('exercise/oral_expression.html.twig'); return $view->fetch($template); } /** * Get the audio componen for a teacher audio feedback. * * @param int $attemptId * @param int $questionId * @param int $userId * * @return string */ public static function getOralFeedbackAudio($attemptId, $questionId, $userId) { /** @var TrackEExercise $tExercise */ $tExercise = Container::getTrackEExerciseRepository()->find($attemptId); if (null === $tExercise) { return ''; } $qAttempt = $tExercise->getAttemptByQuestionId($questionId); if (null === $qAttempt) { return ''; } $html = ''; $assetRepo = Container::getAssetRepository(); foreach ($qAttempt->getAttemptFeedbacks() as $attemptFeedback) { $html .= Display::tag( 'audio', '', [ 'src' => $assetRepo->getAssetUrl($attemptFeedback->getAsset()), 'controls' => '', ] ); } return $html; } public static function getNotificationSettings(): array { return [ 2 => get_lang('Paranoid: E-mail teacher when a student starts an exercise'), 1 => get_lang('Aware: E-mail teacher when a student ends an exercise'), // default 3 => get_lang('Relaxed open: E-mail teacher when a student ends an exercise, only if an open question is answered'), 4 => get_lang('Relaxed audio: E-mail teacher when a student ends an exercise, only if an oral question is answered'), ]; } /** * Get the additional actions added in exercise_additional_teacher_modify_actions configuration. * * @param int $exerciseId * @param int $iconSize * * @return string */ public static function getAdditionalTeacherActions($exerciseId, $iconSize = ICON_SIZE_SMALL) { $additionalActions = api_get_setting('exercise.exercise_additional_teacher_modify_actions', true) ?: []; $actions = []; if (is_array($additionalActions)) { foreach ($additionalActions as $additionalAction) { $actions[] = call_user_func( $additionalAction, $exerciseId, $iconSize ); } } return implode(PHP_EOL, $actions); } /** * @param int $userId * @param int $courseId * @param int $sessionId * * @return int */ public static function countAnsweredQuestionsByUserAfterTime(DateTime $time, $userId, $courseId, $sessionId) { $em = Database::getManager(); if (empty($sessionId)) { $sessionId = null; } $time = api_get_utc_datetime($time->format('Y-m-d H:i:s'), false, true); $result = $em ->createQuery(' SELECT COUNT(ea) FROM ChamiloCoreBundle:TrackEAttempt ea WHERE ea.userId = :user AND ea.cId = :course AND ea.sessionId = :session AND ea.tms > :time ') ->setParameters(['user' => $userId, 'course' => $courseId, 'session' => $sessionId, 'time' => $time]) ->getSingleScalarResult(); return $result; } /** * @param int $userId * @param int $numberOfQuestions * @param int $courseId * @param int $sessionId * * @throws \Doctrine\ORM\Query\QueryException * * @return bool */ public static function isQuestionsLimitPerDayReached($userId, $numberOfQuestions, $courseId, $sessionId) { $questionsLimitPerDay = (int) api_get_course_setting('quiz_question_limit_per_day'); if ($questionsLimitPerDay <= 0) { return false; } $midnightTime = ChamiloApi::getServerMidnightTime(); $answeredQuestionsCount = self::countAnsweredQuestionsByUserAfterTime( $midnightTime, $userId, $courseId, $sessionId ); return ($answeredQuestionsCount + $numberOfQuestions) > $questionsLimitPerDay; } /** * Check if an exercise complies with the requirements to be embedded in the mobile app or a video. * By making sure it is set on one question per page and it only contains unique-answer or multiple-answer questions * or unique-answer image. And that the exam does not have immediate feedback. * * @return bool */ public static function isQuizEmbeddable(CQuiz $exercise) { $em = Database::getManager(); if (ONE_PER_PAGE != $exercise->getType() || in_array($exercise->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]) ) { return false; } $countAll = $em ->createQuery('SELECT COUNT(qq) FROM ChamiloCourseBundle:CQuizQuestion qq INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq WITH qq.iid = qrq.question WHERE qrq.quiz = :id' ) ->setParameter('id', $exercise->getIid()) ->getSingleScalarResult(); $countOfAllowed = $em ->createQuery('SELECT COUNT(qq) FROM ChamiloCourseBundle:CQuizQuestion qq INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq WITH qq.iid = qrq.question WHERE qrq.quiz = :id AND qq.type IN (:types)' ) ->setParameters( [ 'id' => $exercise->getIid(), 'types' => [UNIQUE_ANSWER, MULTIPLE_ANSWER, UNIQUE_ANSWER_IMAGE], ] ) ->getSingleScalarResult(); return $countAll === $countOfAllowed; } /** * Generate a certificate linked to current quiz and. * Return the HTML block with links to download and view the certificate. * * @param float $totalScore * @param float $totalWeight * @param int $studentId * @param int $courseId * @param int $sessionId * * @return string */ public static function generateAndShowCertificateBlock( $totalScore, $totalWeight, Exercise $objExercise, $studentId, $courseId, $sessionId = 0 ) { if (('true' !== api_get_setting('exercise.quiz_generate_certificate_ending')) || !self::isSuccessExerciseResult($totalScore, $totalWeight, $objExercise->selectPassPercentage()) ) { return ''; } $repo = Container::getGradeBookCategoryRepository(); /** @var GradebookCategory $category */ $category = $repo->findOneBy( ['course' => $courseId, 'session' => $sessionId] ); if (null === $category) { return ''; } /*$category = Category::load(null, null, $courseCode, null, null, $sessionId, 'ORDER By id'); if (empty($category)) { return ''; }*/ $categoryId = $category->getId(); /*$link = LinkFactory::load( null, null, $objExercise->getId(), null, $courseCode, $categoryId );*/ if (empty($category->getLinks()->count())) { return ''; } $resourceDeletedMessage = Category::show_message_resource_delete($courseId); if (!empty($resourceDeletedMessage) || api_is_allowed_to_edit() || api_is_excluded_user_type()) { return ''; } $certificate = Category::generateUserCertificate($category, $studentId); if (!is_array($certificate)) { return ''; } return Category::getDownloadCertificateBlock($certificate); } /** * @param int $exerciseId */ public static function getExerciseTitleById($exerciseId) { $em = Database::getManager(); return $em ->createQuery('SELECT cq.title FROM ChamiloCourseBundle:CQuiz cq WHERE cq.iid = :iid' ) ->setParameter('iid', $exerciseId) ->getSingleScalarResult(); } /** * @param int $exeId ID from track_e_exercises * @param int $userId User ID * @param int $exerciseId Exercise ID * @param int $courseId Optional. Coure ID. * * @return TrackEExercise|null */ public static function recalculateResult($exeId, $userId, $exerciseId, $courseId = 0) { if (empty($userId) || empty($exerciseId)) { return null; } $em = Database::getManager(); /** @var TrackEExercise $trackedExercise */ $trackedExercise = $em->getRepository(TrackEExercise::class)->find($exeId); if (empty($trackedExercise)) { return null; } if ($trackedExercise->getUser()->getId() != $userId || $trackedExercise->getQuiz()?->getIid() != $exerciseId ) { return null; } $questionList = $trackedExercise->getDataTracking(); if (empty($questionList)) { return null; } $questionList = explode(',', $questionList); $exercise = new Exercise($courseId); $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : []; if (false === $exercise->read($exerciseId)) { return null; } $totalScore = 0; $totalWeight = 0; $pluginEvaluation = QuestionOptionsEvaluationPlugin::create(); $formula = 'true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE) ? $pluginEvaluation->getFormulaForExercise($exerciseId) : 0; if (empty($formula)) { foreach ($questionList as $questionId) { $question = Question::read($questionId, $courseInfo); if (false === $question) { continue; } $totalWeight += $question->selectWeighting(); // We're inside *one* question. Go through each possible answer for this question $result = $exercise->manage_answer( $exeId, $questionId, [], 'exercise_result', [], false, true, false, $exercise->selectPropagateNeg(), [], [], true ); // Adding the new score. $totalScore += $result['score']; } } else { $totalScore = $pluginEvaluation->getResultWithFormula($exeId, $formula); $totalWeight = $pluginEvaluation->getMaxScore(); } $trackedExercise ->setScore($totalScore) ->setMaxScore($totalWeight); $em->persist($trackedExercise); $em->flush(); return $trackedExercise; } public static function getTotalQuestionAnswered($courseId, $exerciseId, $questionId, $onlyStudents = false): int { $courseId = (int) $courseId; $exerciseId = (int) $exerciseId; $questionId = (int) $questionId; $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT); $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER); $courseUserJoin = ""; $studentsWhere = ""; if ($onlyStudents) { $courseUserJoin = " INNER JOIN $courseUser cu ON cu.c_id = te.c_id AND cu.user_id = exe_user_id"; $studentsWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT; } $sql = "SELECT count(distinct (te.exe_id)) total FROM $attemptTable t INNER JOIN $trackTable te ON (t.exe_id = te.exe_id) $courseUserJoin WHERE te.c_id = $courseId AND exe_exo_id = $exerciseId AND t.question_id = $questionId AND te.status != 'incomplete' $studentsWhere "; $queryTotal = Database::query($sql); $totalRow = Database::fetch_array($queryTotal, 'ASSOC'); $total = 0; if ($totalRow) { $total = (int) $totalRow['total']; } return $total; } public static function getWrongQuestionResults($courseId, $exerciseId, $sessionId = 0, $limit = 10) { $courseId = (int) $courseId; $exerciseId = (int) $exerciseId; $limit = (int) $limit; $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION); $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT); $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); $sessionCondition = ''; if (!empty($sessionId)) { $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id'); } $sql = "SELECT q.question, question_id, count(q.iid) count FROM $attemptTable t INNER JOIN $questionTable q ON (q.iid = t.question_id) INNER JOIN $trackTable te ON (t.exe_id = te.exe_id) WHERE te.c_id = $courseId AND t.marks != q.ponderation AND exe_exo_id = $exerciseId AND status != 'incomplete' $sessionCondition GROUP BY q.iid ORDER BY count DESC LIMIT $limit "; $result = Database::query($sql); return Database::store_result($result, 'ASSOC'); } public static function getExerciseResultsCount($type, $courseId, $exerciseId, $sessionId = 0) { $courseId = (int) $courseId; $exerciseId = (int) $exerciseId; $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); $sessionCondition = ''; if (!empty($sessionId)) { $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id'); } $selectCount = 'count(DISTINCT te.exe_id)'; $scoreCondition = ''; switch ($type) { case 'correct_student': $selectCount = 'count(DISTINCT te.exe_user_id)'; $scoreCondition = ' AND score = max_score '; break; case 'wrong_student': $selectCount = 'count(DISTINCT te.exe_user_id)'; $scoreCondition = ' AND score != max_score '; break; case 'correct': $scoreCondition = ' AND score = max_score '; break; case 'wrong': $scoreCondition = ' AND score != max_score '; break; } $sql = "SELECT $selectCount count FROM $trackTable te WHERE c_id = $courseId AND exe_exo_id = $exerciseId AND status != 'incomplete' $scoreCondition $sessionCondition "; $result = Database::query($sql); $totalRow = Database::fetch_array($result, 'ASSOC'); $total = 0; if ($totalRow) { $total = (int) $totalRow['count']; } return $total; } public static function parseContent($content, $stats, Exercise $exercise, $trackInfo, $currentUserId = 0) { $wrongAnswersCount = $stats['failed_answers_count']; $attemptDate = substr($trackInfo['exe_date'], 0, 10); $exerciseId = $exercise->iId; $resultsStudentUrl = api_get_path(WEB_CODE_PATH). 'exercise/result.php?id='.$exerciseId.'&'.api_get_cidreq(); $resultsTeacherUrl = api_get_path(WEB_CODE_PATH). 'exercise/exercise_show.php?action=edit&id='.$exerciseId.'&'.api_get_cidreq(); $content = str_replace( [ '((exercise_error_count))', '((all_answers_html))', '((all_answers_teacher_html))', '((exercise_title))', '((exercise_attempt_date))', '((link_to_test_result_page_student))', '((link_to_test_result_page_teacher))', ], [ $wrongAnswersCount, $stats['all_answers_html'], $stats['all_answers_teacher_html'], $exercise->get_formated_title(), $attemptDate, $resultsStudentUrl, $resultsTeacherUrl, ], $content ); $currentUserId = empty($currentUserId) ? api_get_user_id() : (int) $currentUserId; $content = AnnouncementManager::parseContent( $currentUserId, $content, api_get_course_id(), api_get_session_id() ); return $content; } public static function sendNotification( $currentUserId, $objExercise, $exercise_stat_info, $courseInfo, $attemptCountToSend, $stats, $statsTeacher ) { $notifications = api_get_configuration_value('exercise_finished_notification_settings'); if (empty($notifications)) { return false; } $studentId = $exercise_stat_info['exe_user_id']; $exerciseExtraFieldValue = new ExtraFieldValue('exercise'); $wrongAnswersCount = $stats['failed_answers_count']; $exercisePassed = $stats['exercise_passed']; $countPendingQuestions = $stats['count_pending_questions']; $stats['all_answers_teacher_html'] = $statsTeacher['all_answers_html']; // If there are no pending questions (Open questions). if (0 === $countPendingQuestions) { /*$extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable( $objExercise->iId, 'signature_mandatory' ); if ($extraFieldData && isset($extraFieldData['value']) && 1 === (int) $extraFieldData['value']) { if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($objExercise)) { $signature = ExerciseSignaturePlugin::getSignature($studentId, $exercise_stat_info); if (false !== $signature) { //return false; } } }*/ // Notifications. $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable( $objExercise->iId, 'notifications' ); $exerciseNotification = ''; if ($extraFieldData && isset($extraFieldData['value'])) { $exerciseNotification = $extraFieldData['value']; } $subject = sprintf(get_lang('WrongAttemptXInCourseX'), $attemptCountToSend, $courseInfo['title']); if ($exercisePassed) { $subject = sprintf(get_lang('ExerciseValidationInCourseX'), $courseInfo['title']); } if ($exercisePassed) { $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable( $objExercise->iId, 'MailSuccess' ); } else { $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable( $objExercise->iId, 'MailAttempt'.$attemptCountToSend ); } // Blocking exercise. $blockPercentageExtra = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable( $objExercise->iId, 'blocking_percentage' ); $blockPercentage = false; if ($blockPercentageExtra && isset($blockPercentageExtra['value']) && $blockPercentageExtra['value']) { $blockPercentage = $blockPercentageExtra['value']; } if ($blockPercentage) { $passBlock = $stats['total_percentage'] > $blockPercentage; if (false === $passBlock) { $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable( $objExercise->iId, 'MailIsBlockByPercentage' ); } } $extraFieldValueUser = new ExtraFieldValue('user'); if ($extraFieldData && isset($extraFieldData['value'])) { $content = $extraFieldData['value']; $content = self::parseContent($content, $stats, $objExercise, $exercise_stat_info, $studentId); //if (false === $exercisePassed) { if (0 !== $wrongAnswersCount) { $content .= $stats['failed_answers_html']; } $sendMessage = true; if (!empty($exerciseNotification)) { foreach ($notifications as $name => $notificationList) { if ($exerciseNotification !== $name) { continue; } foreach ($notificationList as $notificationName => $attemptData) { if ('student_check' === $notificationName) { $sendMsgIfInList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : ''; if (!empty($sendMsgIfInList)) { foreach ($sendMsgIfInList as $skipVariable => $skipValues) { $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable( $studentId, $skipVariable ); if (empty($userExtraFieldValue)) { $sendMessage = false; break; } else { $sendMessage = false; if (isset($userExtraFieldValue['value']) && in_array($userExtraFieldValue['value'], $skipValues) ) { $sendMessage = true; break; } } } } break; } } } } // Send to student. if ($sendMessage) { MessageManager::send_message($currentUserId, $subject, $content); } } if (!empty($exerciseNotification)) { foreach ($notifications as $name => $notificationList) { if ($exerciseNotification !== $name) { continue; } foreach ($notificationList as $attemptData) { $skipNotification = false; $skipNotificationList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : []; if (!empty($skipNotificationList)) { foreach ($skipNotificationList as $skipVariable => $skipValues) { $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable( $studentId, $skipVariable ); if (empty($userExtraFieldValue)) { $skipNotification = true; break; } else { if (isset($userExtraFieldValue['value'])) { if (!in_array($userExtraFieldValue['value'], $skipValues)) { $skipNotification = true; break; } } else { $skipNotification = true; break; } } } } if ($skipNotification) { continue; } $email = isset($attemptData['email']) ? $attemptData['email'] : ''; $emailList = explode(',', $email); if (empty($emailList)) { continue; } $attempts = isset($attemptData['attempts']) ? $attemptData['attempts'] : []; foreach ($attempts as $attempt) { $sendMessage = false; if (isset($attempt['attempt']) && $attemptCountToSend !== (int) $attempt['attempt']) { continue; } if (!isset($attempt['status'])) { continue; } if ($blockPercentage && isset($attempt['is_block_by_percentage'])) { if ($attempt['is_block_by_percentage']) { if ($passBlock) { continue; } } else { if (false === $passBlock) { continue; } } } switch ($attempt['status']) { case 'passed': if ($exercisePassed) { $sendMessage = true; } break; case 'failed': if (false === $exercisePassed) { $sendMessage = true; } break; case 'all': $sendMessage = true; break; } if ($sendMessage) { $attachments = []; if (isset($attempt['add_pdf']) && $attempt['add_pdf']) { // Get pdf content $pdfExtraData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable( $objExercise->iId, $attempt['add_pdf'] ); if ($pdfExtraData && isset($pdfExtraData['value'])) { $pdfContent = self::parseContent( $pdfExtraData['value'], $stats, $objExercise, $exercise_stat_info, $studentId ); @$pdf = new PDF(); $filename = get_lang('Exercise'); $cssFile = api_get_path(SYS_CSS_PATH).'themes/chamilo/default.css'; $pdfPath = @$pdf->content_to_pdf( "$pdfContent", file_get_contents($cssFile), $filename, api_get_course_id(), 'F', false, null, false, true ); $attachments[] = ['filename' => $filename, 'path' => $pdfPath]; } } $content = isset($attempt['content_default']) ? $attempt['content_default'] : ''; if (isset($attempt['content'])) { $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable( $objExercise->iId, $attempt['content'] ); if ($extraFieldData && isset($extraFieldData['value']) && !empty($extraFieldData['value'])) { $content = $extraFieldData['value']; } } if (!empty($content)) { $content = self::parseContent( $content, $stats, $objExercise, $exercise_stat_info, $studentId ); foreach ($emailList as $email) { if (empty($email)) { continue; } api_mail_html( null, $email, $subject, $content, null, null, [], $attachments ); } } if (isset($attempt['post_actions'])) { foreach ($attempt['post_actions'] as $action => $params) { switch ($action) { case 'subscribe_student_to_courses': foreach ($params as $code) { $courseInfo = api_get_course_info($code); CourseManager::subscribeUser( $currentUserId, $courseInfo['real_id'] ); break; } break; } } } } } } } } } } /** * Delete an exercise attempt. * * Log the exe_id deleted with the exe_user_id related. * * @param int $exeId */ public static function deleteExerciseAttempt($exeId) { $exeId = (int) $exeId; $trackExerciseInfo = self::get_exercise_track_exercise_info($exeId); if (empty($trackExerciseInfo)) { return; } $tblTrackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); $tblTrackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT); Database::query("DELETE FROM $tblTrackAttempt WHERE exe_id = $exeId"); Database::query("DELETE FROM $tblTrackExercises WHERE exe_id = $exeId"); Event::addEvent( LOG_EXERCISE_ATTEMPT_DELETE, LOG_EXERCISE_ATTEMPT, $exeId, api_get_utc_datetime() ); Event::addEvent( LOG_EXERCISE_ATTEMPT_DELETE, LOG_EXERCISE_AND_USER_ID, $exeId.'-'.$trackExerciseInfo['exe_user_id'], api_get_utc_datetime() ); } public static function scorePassed($score, $total) { $compareResult = bccomp($score, $total, 3); $scorePassed = 1 === $compareResult || 0 === $compareResult; if (false === $scorePassed) { $epsilon = 0.00001; if (abs($score - $total) < $epsilon) { $scorePassed = true; } } return $scorePassed; } }