iId = 0; $this->id = 0; $this->exercise = ''; $this->description = ''; $this->sound = ''; $this->type = ALL_ON_ONE_PAGE; $this->random = 0; $this->random_answers = 0; $this->active = 1; $this->questionList = []; $this->timeLimit = 0; $this->end_time = ''; $this->start_time = ''; $this->results_disabled = 1; $this->expired_time = 0; $this->propagate_neg = 0; $this->saveCorrectAnswers = 0; $this->review_answers = false; $this->randomByCat = 0; $this->text_when_finished = ''; $this->display_category_name = 0; $this->pass_percentage = 0; $this->modelType = 1; $this->questionSelectionType = EX_Q_SELECTION_ORDERED; $this->endButton = 0; $this->scoreTypeModel = 0; $this->globalCategoryId = null; $this->notifications = []; $this->exerciseCategoryId = 0; $this->pageResultConfiguration; if (!empty($courseId)) { $courseInfo = api_get_course_info_by_id($courseId); } else { $courseInfo = api_get_course_info(); } $this->course_id = $courseInfo['real_id']; $this->course = $courseInfo; $this->sessionId = api_get_session_id(); // ALTER TABLE c_quiz_question ADD COLUMN feedback text; $this->questionFeedbackEnabled = api_get_configuration_value('allow_quiz_question_feedback'); $this->showPreviousButton = true; } /** * Reads exercise information from the data base. * * @author Olivier Brouckaert * * @param int $id - exercise Id * @param bool $parseQuestionList * * @return bool - true if exercise exists, otherwise false */ public function read($id, $parseQuestionList = true) { $table = Database::get_course_table(TABLE_QUIZ_TEST); $tableLpItem = Database::get_course_table(TABLE_LP_ITEM); $id = (int) $id; if (empty($this->course_id)) { return false; } $sql = "SELECT * FROM $table WHERE c_id = ".$this->course_id." AND id = ".$id; $result = Database::query($sql); // if the exercise has been found if ($object = Database::fetch_object($result)) { $this->iId = $object->iid; $this->id = $id; $this->exercise = $object->title; $this->name = $object->title; $this->title = $object->title; $this->description = $object->description; $this->sound = $object->sound; $this->type = $object->type; if (empty($this->type)) { $this->type = ONE_PER_PAGE; } $this->random = $object->random; $this->random_answers = $object->random_answers; $this->active = $object->active; $this->results_disabled = $object->results_disabled; $this->attempts = $object->max_attempt; $this->feedback_type = $object->feedback_type; $this->sessionId = $object->session_id; $this->propagate_neg = $object->propagate_neg; $this->saveCorrectAnswers = $object->save_correct_answers; $this->randomByCat = $object->random_by_category; $this->text_when_finished = $object->text_when_finished; $this->display_category_name = $object->display_category_name; $this->pass_percentage = $object->pass_percentage; $this->is_gradebook_locked = api_resource_is_locked_by_gradebook($id, LINK_EXERCISE); $this->review_answers = (isset($object->review_answers) && $object->review_answers == 1) ? true : false; $this->globalCategoryId = isset($object->global_category_id) ? $object->global_category_id : null; $this->questionSelectionType = isset($object->question_selection_type) ? $object->question_selection_type : null; $this->hideQuestionTitle = isset($object->hide_question_title) ? (int) $object->hide_question_title : 0; $this->autolaunch = isset($object->autolaunch) ? (int) $object->autolaunch : 0; $this->exerciseCategoryId = isset($object->exercise_category_id) ? (int) $object->exercise_category_id : 0; $this->notifications = []; if (!empty($object->notifications)) { $this->notifications = explode(',', $object->notifications); } if (!empty($object->page_result_configuration)) { $this->pageResultConfiguration = $object->page_result_configuration; } if (isset($object->show_previous_button)) { $this->showPreviousButton = $object->show_previous_button == 1 ? true : false; } $sql = "SELECT lp_id, max_score FROM $tableLpItem WHERE c_id = {$this->course_id} AND item_type = '".TOOL_QUIZ."' AND path = '".$id."'"; $result = Database::query($sql); if (Database::num_rows($result) > 0) { $this->exercise_was_added_in_lp = true; $this->lpList = Database::store_result($result, 'ASSOC'); } $this->force_edit_exercise_in_lp = api_get_setting('lp.show_invisible_exercise_in_lp_toc') === 'true'; $this->edit_exercise_in_lp = true; if ($this->exercise_was_added_in_lp) { $this->edit_exercise_in_lp = $this->force_edit_exercise_in_lp == true; } if (!empty($object->end_time)) { $this->end_time = $object->end_time; } if (!empty($object->start_time)) { $this->start_time = $object->start_time; } // Control time $this->expired_time = $object->expired_time; // Checking if question_order is correctly set if ($parseQuestionList) { $this->setQuestionList(true); } //overload questions list with recorded questions list //load questions only for exercises of type 'one question per page' //this is needed only is there is no questions // @todo not sure were in the code this is used somebody mess with the exercise tool // @todo don't know who add that config and why $_configuration['live_exercise_tracking'] /*global $_configuration, $questionList; if ($this->type == ONE_PER_PAGE && $_SERVER['REQUEST_METHOD'] != 'POST' && defined('QUESTION_LIST_ALREADY_LOGGED') && isset($_configuration['live_exercise_tracking']) && $_configuration['live_exercise_tracking'] ) { $this->questionList = $questionList; }*/ return true; } return false; } /** * @return string */ public function getCutTitle() { $title = $this->getUnformattedTitle(); return cut($title, EXERCISE_MAX_NAME_SIZE); } /** * returns the exercise ID. * * @author Olivier Brouckaert * * @return int - exercise ID */ public function selectId() { return $this->id; } /** * returns the exercise title. * * @author Olivier Brouckaert * * @param bool $unformattedText Optional. Get the title without HTML tags * * @return string - exercise title */ public function selectTitle($unformattedText = false) { if ($unformattedText) { return $this->getUnformattedTitle(); } return $this->exercise; } /** * returns the number of attempts setted. * * @return int - exercise attempts */ public function selectAttempts() { return $this->attempts; } /** * Returns the number of FeedbackType * 0: Feedback , 1: DirectFeedback, 2: NoFeedback. * * @return int - exercise attempts */ public function getFeedbackType() { return (int) $this->feedback_type; } /** * returns the time limit. * * @return int */ public function selectTimeLimit() { return $this->timeLimit; } /** * returns the exercise description. * * @author Olivier Brouckaert * * @return string - exercise description */ public function selectDescription() { return $this->description; } /** * returns the exercise sound file. * * @author Olivier Brouckaert * * @return string - exercise description */ public function selectSound() { return $this->sound; } /** * returns the exercise type. * * @author Olivier Brouckaert * * @return int - exercise type */ public function selectType() { return $this->type; } /** * @return int */ public function getModelType() { return $this->modelType; } /** * @return int */ public function selectEndButton() { return $this->endButton; } /** * @author hubert borderiou 30-11-11 * * @return int : do we display the question category name for students */ public function selectDisplayCategoryName() { return $this->display_category_name; } /** * @return int */ public function selectPassPercentage() { return $this->pass_percentage; } /** * Modify object to update the switch display_category_name. * * @author hubert borderiou 30-11-11 * * @param int $value is an integer 0 or 1 */ public function updateDisplayCategoryName($value) { $this->display_category_name = $value; } /** * @author hubert borderiou 28-11-11 * * @return string html text : the text to display ay the end of the test */ public function getTextWhenFinished() { return $this->text_when_finished; } /** * @param string $text * * @author hubert borderiou 28-11-11 */ public function updateTextWhenFinished($text) { $this->text_when_finished = $text; } /** * return 1 or 2 if randomByCat. * * @author hubert borderiou * * @return int - quiz random by category */ public function getRandomByCategory() { return $this->randomByCat; } /** * return 0 if no random by cat * return 1 if random by cat, categories shuffled * return 2 if random by cat, categories sorted by alphabetic order. * * @author hubert borderiou * * @return int - quiz random by category */ public function isRandomByCat() { $res = EXERCISE_CATEGORY_RANDOM_DISABLED; if ($this->randomByCat == EXERCISE_CATEGORY_RANDOM_SHUFFLED) { $res = EXERCISE_CATEGORY_RANDOM_SHUFFLED; } elseif ($this->randomByCat == EXERCISE_CATEGORY_RANDOM_ORDERED) { $res = EXERCISE_CATEGORY_RANDOM_ORDERED; } return $res; } /** * return nothing * update randomByCat value for object. * * @param int $random * * @author hubert borderiou */ public function updateRandomByCat($random) { $this->randomByCat = EXERCISE_CATEGORY_RANDOM_DISABLED; if (in_array( $random, [ EXERCISE_CATEGORY_RANDOM_SHUFFLED, EXERCISE_CATEGORY_RANDOM_ORDERED, EXERCISE_CATEGORY_RANDOM_DISABLED, ] )) { $this->randomByCat = $random; } } /** * Tells if questions are selected randomly, and if so returns the draws. * * @author Carlos Vargas * * @return int - results disabled exercise */ public function selectResultsDisabled() { return $this->results_disabled; } /** * tells if questions are selected randomly, and if so returns the draws. * * @author Olivier Brouckaert * * @return bool */ public function isRandom() { $isRandom = false; // "-1" means all questions will be random if ($this->random > 0 || $this->random == -1) { $isRandom = true; } return $isRandom; } /** * returns random answers status. * * @author Juan Carlos Rana */ public function getRandomAnswers() { return $this->random_answers; } /** * Same as isRandom() but has a name applied to values different than 0 or 1. * * @return int */ public function getShuffle() { return $this->random; } /** * returns the exercise status (1 = enabled ; 0 = disabled). * * @author Olivier Brouckaert * * @return int - 1 if enabled, otherwise 0 */ public function selectStatus() { return $this->active; } /** * If false the question list will be managed as always if true * the question will be filtered * depending of the exercise settings (table c_quiz_rel_category). * * @param bool $status active or inactive grouping */ public function setCategoriesGrouping($status) { $this->categories_grouping = (bool) $status; } /** * @return int */ public function getHideQuestionTitle() { return $this->hideQuestionTitle; } /** * @param $value */ public function setHideQuestionTitle($value) { $this->hideQuestionTitle = (int) $value; } /** * @return int */ public function getScoreTypeModel() { return $this->scoreTypeModel; } /** * @param int $value */ public function setScoreTypeModel($value) { $this->scoreTypeModel = (int) $value; } /** * @return int */ public function getGlobalCategoryId() { return $this->globalCategoryId; } /** * @param int $value */ public function setGlobalCategoryId($value) { if (is_array($value) && isset($value[0])) { $value = $value[0]; } $this->globalCategoryId = (int) $value; } /** * @param int $start * @param int $limit * @param int $sidx * @param string $sord * @param array $whereCondition * @param array $extraFields * * @return array */ public function getQuestionListPagination( $start, $limit, $sidx, $sord, $whereCondition = [], $extraFields = [] ) { if (!empty($this->id)) { $category_list = TestCategory::getListOfCategoriesNameForTest( $this->id, false ); $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION); $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION); $sql = "SELECT q.iid FROM $TBL_EXERCICE_QUESTION e INNER JOIN $TBL_QUESTIONS q ON (e.question_id = q.id AND e.c_id = ".$this->course_id." ) WHERE e.exercice_id = '".$this->id."' "; $orderCondition = ' ORDER BY question_order '; if (!empty($sidx) && !empty($sord)) { if ($sidx === 'question') { if (in_array(strtolower($sord), ['desc', 'asc'])) { $orderCondition = " ORDER BY q.$sidx $sord"; } } } $sql .= $orderCondition; $limitCondition = null; if (isset($start) && isset($limit)) { $start = (int) $start; $limit = (int) $limit; $limitCondition = " LIMIT $start, $limit"; } $sql .= $limitCondition; $result = Database::query($sql); $questions = []; if (Database::num_rows($result)) { if (!empty($extraFields)) { $extraFieldValue = new ExtraFieldValue('question'); } while ($question = Database::fetch_array($result, 'ASSOC')) { /** @var Question $objQuestionTmp */ $objQuestionTmp = Question::read($question['iid']); $category_labels = TestCategory::return_category_labels( $objQuestionTmp->category_list, $category_list ); if (empty($category_labels)) { $category_labels = '-'; } // Question type $typeImg = $objQuestionTmp->getTypePicture(); $typeExpl = $objQuestionTmp->getExplanation(); $question_media = null; if (!empty($objQuestionTmp->parent_id)) { $objQuestionMedia = Question::read($objQuestionTmp->parent_id); $question_media = Question::getMediaLabel($objQuestionMedia->question); } $questionType = Display::tag( 'div', Display::return_icon($typeImg, $typeExpl, [], ICON_SIZE_MEDIUM).$question_media ); $question = [ 'id' => $question['iid'], 'question' => $objQuestionTmp->selectTitle(), 'type' => $questionType, 'category' => Display::tag( 'div', ''.$category_labels.'' ), 'score' => $objQuestionTmp->selectWeighting(), 'level' => $objQuestionTmp->level, ]; if (!empty($extraFields)) { foreach ($extraFields as $extraField) { $value = $extraFieldValue->get_values_by_handler_and_field_id( $question['id'], $extraField['id'] ); $stringValue = null; if ($value) { $stringValue = $value['field_value']; } $question[$extraField['field_variable']] = $stringValue; } } $questions[] = $question; } } return $questions; } } /** * Get question count per exercise from DB (any special treatment). * * @return int */ public function getQuestionCount() { $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION); $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION); $sql = "SELECT count(q.id) as count FROM $TBL_EXERCICE_QUESTION e INNER JOIN $TBL_QUESTIONS q ON (e.question_id = q.id AND e.c_id = q.c_id) WHERE e.c_id = {$this->course_id} AND e.exercice_id = ".$this->id; $result = Database::query($sql); $count = 0; if (Database::num_rows($result)) { $row = Database::fetch_array($result); $count = (int) $row['count']; } return $count; } /** * @return array */ public function getQuestionOrderedListByName() { if (empty($this->course_id) || empty($this->id)) { return []; } $exerciseQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION); $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION); // Getting question list from the order (question list drag n drop interface ). $sql = "SELECT e.question_id FROM $exerciseQuestionTable e INNER JOIN $questionTable q ON (e.question_id= q.id AND e.c_id = q.c_id) WHERE e.c_id = {$this->course_id} AND e.exercice_id = '".$this->id."' ORDER BY q.question"; $result = Database::query($sql); $list = []; if (Database::num_rows($result)) { $list = Database::store_result($result, 'ASSOC'); } return $list; } /** * Selecting question list depending in the exercise-category * relationship (category table in exercise settings). * * @param array $question_list * @param int $questionSelectionType * * @return array */ public function getQuestionListWithCategoryListFilteredByCategorySettings( $question_list, $questionSelectionType ) { $result = [ 'question_list' => [], 'category_with_questions_list' => [], ]; // Order/random categories $cat = new TestCategory(); // Setting category order. switch ($questionSelectionType) { case EX_Q_SELECTION_ORDERED: // 1 case EX_Q_SELECTION_RANDOM: // 2 // This options are not allowed here. break; case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED: // 3 $categoriesAddedInExercise = $cat->getCategoryExerciseTree( $this, $this->course['real_id'], 'title ASC', false, true ); $questions_by_category = TestCategory::getQuestionsByCat( $this->id, $question_list, $categoriesAddedInExercise ); $question_list = $this->pickQuestionsPerCategory( $categoriesAddedInExercise, $question_list, $questions_by_category, true, false ); break; case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED: // 4 case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7 $categoriesAddedInExercise = $cat->getCategoryExerciseTree( $this, $this->course['real_id'], null, true, true ); $questions_by_category = TestCategory::getQuestionsByCat( $this->id, $question_list, $categoriesAddedInExercise ); $question_list = $this->pickQuestionsPerCategory( $categoriesAddedInExercise, $question_list, $questions_by_category, true, false ); break; case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM: // 5 $categoriesAddedInExercise = $cat->getCategoryExerciseTree( $this, $this->course['real_id'], 'title ASC', false, true ); $questions_by_category = TestCategory::getQuestionsByCat( $this->id, $question_list, $categoriesAddedInExercise ); $question_list = $this->pickQuestionsPerCategory( $categoriesAddedInExercise, $question_list, $questions_by_category, true, true ); break; case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM: // 6 case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED: $categoriesAddedInExercise = $cat->getCategoryExerciseTree( $this, $this->course['real_id'], null, true, true ); $questions_by_category = TestCategory::getQuestionsByCat( $this->id, $question_list, $categoriesAddedInExercise ); $question_list = $this->pickQuestionsPerCategory( $categoriesAddedInExercise, $question_list, $questions_by_category, true, true ); break; case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7 break; case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED: // 8 break; case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED: // 9 $categoriesAddedInExercise = $cat->getCategoryExerciseTree( $this, $this->course['real_id'], 'root ASC, lft ASC', false, true ); $questions_by_category = TestCategory::getQuestionsByCat( $this->id, $question_list, $categoriesAddedInExercise ); $question_list = $this->pickQuestionsPerCategory( $categoriesAddedInExercise, $question_list, $questions_by_category, true, false ); break; case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM: // 10 $categoriesAddedInExercise = $cat->getCategoryExerciseTree( $this, $this->course['real_id'], 'root, lft ASC', false, true ); $questions_by_category = TestCategory::getQuestionsByCat( $this->id, $question_list, $categoriesAddedInExercise ); $question_list = $this->pickQuestionsPerCategory( $categoriesAddedInExercise, $question_list, $questions_by_category, true, true ); break; } $result['question_list'] = isset($question_list) ? $question_list : []; $result['category_with_questions_list'] = isset($questions_by_category) ? $questions_by_category : []; $parentsLoaded = []; // Adding category info in the category list with question list: if (!empty($questions_by_category)) { $newCategoryList = []; $em = Database::getManager(); foreach ($questions_by_category as $categoryId => $questionList) { $cat = new TestCategory(); $cat = $cat->getCategory($categoryId); if ($cat) { $cat = (array) $cat; $cat['iid'] = $cat['id']; } $categoryParentInfo = null; // Parent is not set no loop here if (isset($cat['parent_id']) && !empty($cat['parent_id'])) { /** @var CQuizCategory $categoryEntity */ if (!isset($parentsLoaded[$cat['parent_id']])) { $categoryEntity = $em->find('ChamiloCoreBundle:CQuizCategory', $cat['parent_id']); $parentsLoaded[$cat['parent_id']] = $categoryEntity; } else { $categoryEntity = $parentsLoaded[$cat['parent_id']]; } $repo = $em->getRepository('ChamiloCoreBundle:CQuizCategory'); $path = $repo->getPath($categoryEntity); $index = 0; if ($this->categoryMinusOne) { //$index = 1; } /** @var CQuizCategory $categoryParent */ foreach ($path as $categoryParent) { $visibility = $categoryParent->getVisibility(); if ($visibility == 0) { $categoryParentId = $categoryId; $categoryTitle = $cat['title']; if (count($path) > 1) { continue; } } else { $categoryParentId = $categoryParent->getIid(); $categoryTitle = $categoryParent->getTitle(); } $categoryParentInfo['id'] = $categoryParentId; $categoryParentInfo['iid'] = $categoryParentId; $categoryParentInfo['parent_path'] = null; $categoryParentInfo['title'] = $categoryTitle; $categoryParentInfo['name'] = $categoryTitle; $categoryParentInfo['parent_id'] = null; break; } } $cat['parent_info'] = $categoryParentInfo; $newCategoryList[$categoryId] = [ 'category' => $cat, 'question_list' => $questionList, ]; } $result['category_with_questions_list'] = $newCategoryList; } return $result; } /** * returns the array with the question ID list. * * @param bool $fromDatabase Whether the results should be fetched in the database or just from memory * @param bool $adminView Whether we should return all questions (admin view) or * just a list limited by the max number of random questions * * @author Olivier Brouckaert * * @return array - question ID list */ public function selectQuestionList($fromDatabase = false, $adminView = false) { if ($fromDatabase && !empty($this->id)) { $nbQuestions = $this->getQuestionCount(); $questionSelectionType = $this->getQuestionSelectionType(); switch ($questionSelectionType) { case EX_Q_SELECTION_ORDERED: $questionList = $this->getQuestionOrderedList($adminView); break; case EX_Q_SELECTION_RANDOM: // Not a random exercise, or if there are not at least 2 questions if ($this->random == 0 || $nbQuestions < 2) { $questionList = $this->getQuestionOrderedList($adminView); } else { $questionList = $this->getRandomList($adminView); } break; default: $questionList = $this->getQuestionOrderedList($adminView); $result = $this->getQuestionListWithCategoryListFilteredByCategorySettings( $questionList, $questionSelectionType ); $this->categoryWithQuestionList = $result['category_with_questions_list']; $questionList = $result['question_list']; break; } return $questionList; } return $this->questionList; } /** * returns the number of questions in this exercise. * * @author Olivier Brouckaert * * @return int - number of questions */ public function selectNbrQuestions() { return count($this->questionList); } /** * @return int */ public function selectPropagateNeg() { return $this->propagate_neg; } /** * @return int */ public function getSaveCorrectAnswers() { return $this->saveCorrectAnswers; } /** * Selects questions randomly in the question list. * * @author Olivier Brouckaert * @author Hubert Borderiou 15 nov 2011 * * @param bool $adminView Whether we should return all * questions (admin view) or just a list limited by the max number of random questions * * @return array - if the exercise is not set to take questions randomly, returns the question list * without randomizing, otherwise, returns the list with questions selected randomly */ public function getRandomList($adminView = false) { $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION); $question = Database::get_course_table(TABLE_QUIZ_QUESTION); $random = isset($this->random) && !empty($this->random) ? $this->random : 0; // Random with limit $randomLimit = " ORDER BY RAND() LIMIT $random"; // Random with no limit if ($random == -1) { $randomLimit = ' ORDER BY RAND() '; } // Admin see the list in default order if ($adminView === true) { // If viewing it as admin for edition, don't show it randomly, use title + id $randomLimit = 'ORDER BY e.question_order'; } $sql = "SELECT e.question_id FROM $quizRelQuestion e INNER JOIN $question q ON (e.question_id= q.id AND e.c_id = q.c_id) WHERE e.c_id = {$this->course_id} AND e.exercice_id = '".Database::escape_string($this->id)."' $randomLimit "; $result = Database::query($sql); $questionList = []; while ($row = Database::fetch_object($result)) { $questionList[] = $row->question_id; } return $questionList; } /** * returns 'true' if the question ID is in the question list. * * @author Olivier Brouckaert * * @param int $questionId - question ID * * @return bool - true if in the list, otherwise false */ public function isInList($questionId) { $inList = false; if (is_array($this->questionList)) { $inList = in_array($questionId, $this->questionList); } return $inList; } /** * If current exercise has a question. * * @param int $questionId * * @return int */ public function hasQuestion($questionId) { $questionId = (int) $questionId; $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION); $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION); $sql = "SELECT q.id FROM $TBL_EXERCICE_QUESTION e INNER JOIN $TBL_QUESTIONS q ON (e.question_id = q.id AND e.c_id = q.c_id) WHERE q.id = $questionId AND e.c_id = {$this->course_id} AND e.exercice_id = ".$this->id; $result = Database::query($sql); return Database::num_rows($result) > 0; } /** * changes the exercise title. * * @author Olivier Brouckaert * * @param string $title - exercise title */ public function updateTitle($title) { $this->title = $this->exercise = $title; } /** * changes the exercise max attempts. * * @param int $attempts - exercise max attempts */ public function updateAttempts($attempts) { $this->attempts = $attempts; } /** * changes the exercise feedback type. * * @param int $feedback_type */ public function updateFeedbackType($feedback_type) { $this->feedback_type = $feedback_type; } /** * changes the exercise description. * * @author Olivier Brouckaert * * @param string $description - exercise description */ public function updateDescription($description) { $this->description = $description; } /** * changes the exercise expired_time. * * @author Isaac flores * * @param int $expired_time The expired time of the quiz */ public function updateExpiredTime($expired_time) { $this->expired_time = $expired_time; } /** * @param $value */ public function updatePropagateNegative($value) { $this->propagate_neg = $value; } /** * @param $value int */ public function updateSaveCorrectAnswers($value) { $this->saveCorrectAnswers = (int) $value; } /** * @param $value */ public function updateReviewAnswers($value) { $this->review_answers = isset($value) && $value ? true : false; } /** * @param $value */ public function updatePassPercentage($value) { $this->pass_percentage = $value; } /** * @param string $text */ public function updateEmailNotificationTemplate($text) { $this->emailNotificationTemplate = $text; } /** * @param string $text */ public function setEmailNotificationTemplateToUser($text) { $this->emailNotificationTemplateToUser = $text; } /** * @param string $value */ public function setNotifyUserByEmail($value) { $this->notifyUserByEmail = $value; } /** * @param int $value */ public function updateEndButton($value) { $this->endButton = (int) $value; } /** * @param string $value */ public function setOnSuccessMessage($value) { $this->onSuccessMessage = $value; } /** * @param string $value */ public function setOnFailedMessage($value) { $this->onFailedMessage = $value; } /** * @param $value */ public function setModelType($value) { $this->modelType = (int) $value; } /** * @param int $value */ public function setQuestionSelectionType($value) { $this->questionSelectionType = (int) $value; } /** * @return int */ public function getQuestionSelectionType() { return (int) $this->questionSelectionType; } /** * @param array $categories */ public function updateCategories($categories) { if (!empty($categories)) { $categories = array_map('intval', $categories); $this->categories = $categories; } } /** * changes the exercise sound file. * * @author Olivier Brouckaert * * @param string $sound - exercise sound file * @param string $delete - ask to delete the file */ public function updateSound($sound, $delete) { global $audioPath, $documentPath; $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT); if ($sound['size'] && (strstr($sound['type'], 'audio') || strstr($sound['type'], 'video'))) { $this->sound = $sound['name']; if (@move_uploaded_file($sound['tmp_name'], $audioPath.'/'.$this->sound)) { $sql = "SELECT 1 FROM $TBL_DOCUMENT WHERE c_id = ".$this->course_id." AND path = '".str_replace($documentPath, '', $audioPath).'/'.$this->sound."'"; $result = Database::query($sql); if (!Database::num_rows($result)) { DocumentManager::addDocument( $this->course, str_replace($documentPath, '', $audioPath).'/'.$this->sound, 'file', $sound['size'], $sound['name'] ); } } } elseif ($delete && is_file($audioPath.'/'.$this->sound)) { $this->sound = ''; } } /** * changes the exercise type. * * @author Olivier Brouckaert * * @param int $type - exercise type */ public function updateType($type) { $this->type = $type; } /** * sets to 0 if questions are not selected randomly * if questions are selected randomly, sets the draws. * * @author Olivier Brouckaert * * @param int $random - 0 if not random, otherwise the draws */ public function setRandom($random) { $this->random = $random; } /** * sets to 0 if answers are not selected randomly * if answers are selected randomly. * * @author Juan Carlos Rana * * @param int $random_answers - random answers */ public function updateRandomAnswers($random_answers) { $this->random_answers = $random_answers; } /** * enables the exercise. * * @author Olivier Brouckaert */ public function enable() { $this->active = 1; } /** * disables the exercise. * * @author Olivier Brouckaert */ public function disable() { $this->active = 0; } /** * Set disable results. */ public function disable_results() { $this->results_disabled = true; } /** * Enable results. */ public function enable_results() { $this->results_disabled = false; } /** * @param int $results_disabled */ public function updateResultsDisabled($results_disabled) { $this->results_disabled = (int) $results_disabled; } /** * updates the exercise in the data base. * * @author Olivier Brouckaert */ public function save() { $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST); $id = $this->id; $exercise = $this->exercise; $description = $this->description; $sound = $this->sound; $type = $this->type; $attempts = isset($this->attempts) ? $this->attempts : 0; $feedback_type = isset($this->feedback_type) ? $this->feedback_type : 0; $random = $this->random; $random_answers = $this->random_answers; $active = $this->active; $propagate_neg = (int) $this->propagate_neg; $saveCorrectAnswers = isset($this->saveCorrectAnswers) ? (int) $this->saveCorrectAnswers : 0; $review_answers = isset($this->review_answers) && $this->review_answers ? 1 : 0; $randomByCat = (int) $this->randomByCat; $text_when_finished = $this->text_when_finished; $display_category_name = (int) $this->display_category_name; $pass_percentage = (int) $this->pass_percentage; $session_id = $this->sessionId; // If direct we do not show results $results_disabled = (int) $this->results_disabled; if (in_array($feedback_type, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) { $results_disabled = 0; } $expired_time = (int) $this->expired_time; $repo = Container::getExerciseRepository(); // Exercise already exists if ($id) { // we prepare date in the database using the api_get_utc_datetime() function $start_time = null; if (!empty($this->start_time)) { $start_time = $this->start_time; } $end_time = null; if (!empty($this->end_time)) { $end_time = $this->end_time; } $params = [ 'title' => $exercise, 'description' => $description, ]; $paramsExtra = [ 'sound' => $sound, 'type' => $type, 'random' => $random, 'random_answers' => $random_answers, 'active' => $active, 'feedback_type' => $feedback_type, 'start_time' => $start_time, 'end_time' => $end_time, 'max_attempt' => $attempts, 'expired_time' => $expired_time, 'propagate_neg' => $propagate_neg, 'save_correct_answers' => $saveCorrectAnswers, 'review_answers' => $review_answers, 'random_by_category' => $randomByCat, 'text_when_finished' => $text_when_finished, 'display_category_name' => $display_category_name, 'pass_percentage' => $pass_percentage, 'results_disabled' => $results_disabled, 'question_selection_type' => $this->getQuestionSelectionType(), 'hide_question_title' => $this->getHideQuestionTitle(), ]; $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting'); if ($allow === true) { $paramsExtra['show_previous_button'] = $this->showPreviousButton(); } $allow = api_get_configuration_value('allow_exercise_categories'); if ($allow === true) { if (!empty($this->getExerciseCategoryId())) { $paramsExtra['exercise_category_id'] = $this->getExerciseCategoryId(); } } $allow = api_get_configuration_value('allow_notification_setting_per_exercise'); if ($allow === true) { $notifications = $this->getNotifications(); $notifications = implode(',', $notifications); $paramsExtra['notifications'] = $notifications; } if (!empty($this->pageResultConfiguration)) { $paramsExtra['page_result_configuration'] = $this->pageResultConfiguration; } $params = array_merge($params, $paramsExtra); Database::update( $TBL_EXERCISES, $params, ['c_id = ? AND id = ?' => [$this->course_id, $id]] ); $exerciseEntity = $repo->find($id); $node = $exerciseEntity->getResourceNode(); $node->setName($exercise); $repo->getEntityManager()->merge($node); $repo->getEntityManager()->flush(); if (api_get_setting('search_enabled') == 'true') { $this->search_engine_edit(); } } else { // Creates a new exercise // In this case of new exercise, we don't do the api_get_utc_datetime() // for date because, bellow, we call function api_set_default_visibility() // In this function, api_set_default_visibility, // the Quiz is saved too, with an $id and api_get_utc_datetime() is done. // If we do it now, it will be done twice (cf. https://support.chamilo.org/issues/6586) $start_time = null; if (!empty($this->start_time)) { $start_time = $this->start_time; } $end_time = null; if (!empty($this->end_time)) { $end_time = $this->end_time; } $params = [ 'c_id' => $this->course_id, 'start_time' => $start_time, 'end_time' => $end_time, 'title' => $exercise, 'description' => $description, 'sound' => $sound, 'type' => $type, 'random' => $random, 'random_answers' => $random_answers, 'active' => $active, 'results_disabled' => $results_disabled, 'max_attempt' => $attempts, 'feedback_type' => $feedback_type, 'expired_time' => $expired_time, 'session_id' => $session_id, 'review_answers' => $review_answers, 'random_by_category' => $randomByCat, 'text_when_finished' => $text_when_finished, 'display_category_name' => $display_category_name, 'pass_percentage' => $pass_percentage, 'save_correct_answers' => $saveCorrectAnswers, 'propagate_neg' => $propagate_neg, 'hide_question_title' => $this->getHideQuestionTitle(), ]; $allow = api_get_configuration_value('allow_exercise_categories'); if ($allow === true) { if (!empty($this->getExerciseCategoryId())) { $params['exercise_category_id'] = $this->getExerciseCategoryId(); } } $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting'); if ($allow === true) { $params['show_previous_button'] = $this->showPreviousButton(); } $allow = api_get_configuration_value('allow_notification_setting_per_exercise'); if ($allow === true) { $notifications = $this->getNotifications(); $params['notifications'] = ''; if (!empty($notifications)) { $notifications = implode(',', $notifications); $params['notifications'] = $notifications; } } if (!empty($this->pageResultConfiguration)) { $params['page_result_configuration'] = $this->pageResultConfiguration; } $this->id = $this->iId = Database::insert($TBL_EXERCISES, $params); if ($this->id) { $sql = "UPDATE $TBL_EXERCISES SET id = iid WHERE iid = {$this->id} "; Database::query($sql); $sql = "UPDATE $TBL_EXERCISES SET question_selection_type= ".$this->getQuestionSelectionType()." WHERE id = ".$this->id." AND c_id = ".$this->course_id; Database::query($sql); $exercise = $repo->find($this->id); $node = $repo->addResourceNode($exercise, api_get_user_entity(api_get_user_id())); $repo->addResourceToCourse( $node, ResourceLink::VISIBILITY_PUBLISHED, api_get_course_entity(), api_get_session_entity(), api_get_group_entity() ); // insert into the item_property table api_item_property_update( $this->course, TOOL_QUIZ, $this->id, 'QuizAdded', api_get_user_id() ); // This function save the quiz again, carefull about start_time // and end_time if you remove this line (see above) api_set_default_visibility( $this->id, TOOL_QUIZ, null, $this->course ); if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) { $this->search_engine_save(); } } } $this->save_categories_in_exercise($this->categories); return $this->iId; } /** * Updates question position. * * @return bool */ public function update_question_positions() { $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION); // Fixes #3483 when updating order $questionList = $this->selectQuestionList(true); $this->id = (int) $this->id; if (empty($this->id)) { return false; } if (!empty($questionList)) { foreach ($questionList as $position => $questionId) { $position = (int) $position; $questionId = (int) $questionId; $sql = "UPDATE $table SET question_order ='".$position."' WHERE c_id = ".$this->course_id." AND question_id = ".$questionId." AND exercice_id=".$this->id; Database::query($sql); } } return true; } /** * Adds a question into the question list. * * @author Olivier Brouckaert * * @param int $questionId - question ID * * @return bool - true if the question has been added, otherwise false */ public function addToList($questionId) { // checks if the question ID is not in the list if (!$this->isInList($questionId)) { // selects the max position if (!$this->selectNbrQuestions()) { $pos = 1; } else { if (is_array($this->questionList)) { $pos = max(array_keys($this->questionList)) + 1; } } $this->questionList[$pos] = $questionId; return true; } return false; } /** * removes a question from the question list. * * @author Olivier Brouckaert * * @param int $questionId - question ID * * @return bool - true if the question has been removed, otherwise false */ public function removeFromList($questionId) { // searches the position of the question ID in the list $pos = array_search($questionId, $this->questionList); // question not found if ($pos === false) { return false; } else { // dont reduce the number of random question if we use random by category option, or if // random all questions if ($this->isRandom() && $this->isRandomByCat() == 0) { if (count($this->questionList) >= $this->random && $this->random > 0) { $this->random--; $this->save(); } } // deletes the position from the array containing the wanted question ID unset($this->questionList[$pos]); return true; } } /** * deletes the exercise from the database * Notice : leaves the question in the data base. * * @author Olivier Brouckaert */ public function delete() { $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access'); if ($limitTeacherAccess && !api_is_platform_admin()) { return false; } $locked = api_resource_is_locked_by_gradebook( $this->id, LINK_EXERCISE ); if ($locked) { return false; } $table = Database::get_course_table(TABLE_QUIZ_TEST); $sql = "UPDATE $table SET active='-1' WHERE c_id = ".$this->course_id." AND id = ".intval($this->id); Database::query($sql); api_item_property_update( $this->course, TOOL_QUIZ, $this->id, 'QuizDeleted', api_get_user_id() ); api_item_property_update( $this->course, TOOL_QUIZ, $this->id, 'delete', api_get_user_id() ); Skill::deleteSkillsFromItem($this->iId, ITEM_TYPE_EXERCISE); if (api_get_setting('search_enabled') === 'true' && extension_loaded('xapian') ) { $this->search_engine_delete(); } $linkInfo = GradebookUtils::isResourceInCourseGradebook( $this->course['code'], LINK_EXERCISE, $this->id, $this->sessionId ); if ($linkInfo !== false) { GradebookUtils::remove_resource_from_course_gradebook($linkInfo['id']); } return true; } /** * Creates the form to create / edit an exercise. * * @param FormValidator $form * @param string $type */ public function createForm($form, $type = 'full') { if (empty($type)) { $type = 'full'; } // Form title $form_title = get_lang('Create a new test'); if (!empty($_GET['exerciseId'])) { $form_title = get_lang('Edit test name and settings'); } $form->addHeader($form_title); // Title. if (api_get_configuration_value('save_titles_as_html')) { $form->addHtmlEditor( 'exerciseTitle', get_lang('Test name'), false, false, ['ToolbarSet' => 'TitleAsHtml'] ); } else { $form->addElement( 'text', 'exerciseTitle', get_lang('Test name'), ['id' => 'exercise_title'] ); } $form->addElement('advanced_settings', 'advanced_params', get_lang('Advanced settings')); $form->addElement('html', '