diff --git a/assets/css/app.scss b/assets/css/app.scss index 5d6286f08b5..f5334d39268 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -908,6 +908,12 @@ img.course-tool__icon { } } +.media-group { border:2px solid #337ab7; background:#f5fafd; padding:1rem; margin:2rem 0; border-radius:4px; } +.media-content { margin-bottom:1rem; } +.media-description { font-style:italic; margin-bottom:1rem; } +.media-children { margin-left:1rem; } +.media-group h4 { margin-top:0; color:#23527c; } + @import "~@fancyapps/fancybox/dist/jquery.fancybox.css"; @import "~timepicker/jquery.timepicker.min.css"; @import "~qtip2/dist/jquery.qtip.min.css"; diff --git a/public/img/icons/22/media.png b/public/img/icons/22/media.png new file mode 100644 index 00000000000..5aebce0f961 Binary files /dev/null and b/public/img/icons/22/media.png differ diff --git a/public/img/icons/22/page_break.png b/public/img/icons/22/page_break.png new file mode 100644 index 00000000000..426ad53ee6d Binary files /dev/null and b/public/img/icons/22/page_break.png differ diff --git a/public/img/icons/64/media.png b/public/img/icons/64/media.png new file mode 100644 index 00000000000..113735ef4f2 Binary files /dev/null and b/public/img/icons/64/media.png differ diff --git a/public/img/icons/64/page_break.png b/public/img/icons/64/page_break.png new file mode 100644 index 00000000000..d41ae68dad6 Binary files /dev/null and b/public/img/icons/64/page_break.png differ diff --git a/public/main/exercise/MediaQuestion.php b/public/main/exercise/MediaQuestion.php new file mode 100644 index 00000000000..d42a36d5eec --- /dev/null +++ b/public/main/exercise/MediaQuestion.php @@ -0,0 +1,74 @@ +type = MEDIA_QUESTION; + // Mark as content so it’s not counted towards score + $this->isContent = 1; + } + + /** + * Form to create / edit a Media item. + */ + public function createForm(&$form, $exercise) + { + // Title for the media block + $form->addText( + 'questionName', + get_lang('Media Title'), + false, + ['maxlength' => 255] + ); + + // WYSIWYG for the media content (could be text, embed code, etc.) + $editorConfig = [ + 'ToolbarSet' => 'TestQuestionDescription', + 'Height' => '150' + ]; + $form->addHtmlEditor( + 'questionDescription', + get_lang('Media Content'), + false, + false, + $editorConfig + ); + + global $text; + $form->addButtonSave($text, 'submitQuestion'); + + // Populate defaults if editing + $defaults = [ + 'questionName' => $this->question, + 'questionDescription' => $this->description + ]; + $form->setDefaults($defaults); + } + + /** + * No answers to configure for media. + */ + public function createAnswersForm($form) {} + + public function processAnswersCreation($form, $exercise) {} + + /** + * On save, treat like any other question: persist and attach to the exercise. + */ + public function processCreation(FormValidator $form, Exercise $exercise) + { + $this->updateTitle($form->getSubmitValue('questionName')); + $this->updateDescription($form->getSubmitValue('questionDescription')); + $this->save($exercise); + $exercise->addToList($this->id); + } +} diff --git a/public/main/exercise/PageBreakQuestion.php b/public/main/exercise/PageBreakQuestion.php new file mode 100644 index 00000000000..44a430bbc60 --- /dev/null +++ b/public/main/exercise/PageBreakQuestion.php @@ -0,0 +1,57 @@ +type = PAGE_BREAK; + $this->isContent = 1; + } + + public function createForm(&$form, $exercise) + { + $form->addText( + 'questionName', + get_lang('Page Title'), + false, + ['maxlength' => 255] + ); + $editorConfig = [ + 'ToolbarSet' => 'TestQuestionDescription', + 'Height' => '100' + ]; + $form->addHtmlEditor( + 'questionDescription', + get_lang('Page Introduction'), + false, + false, + $editorConfig + ); + + global $text; + $form->addButtonSave($text, 'submitQuestion'); + + $defaults = [ + 'questionName' => $this->question, + 'questionDescription' => $this->description + ]; + $form->setDefaults($defaults); + } + + public function createAnswersForm($form) {} + + public function processAnswersCreation($form, $exercise) {} + + public function processCreation(FormValidator $form, Exercise $exercise) + { + $this->updateTitle($form->getSubmitValue('questionName')); + $this->updateDescription($form->getSubmitValue('questionDescription')); + $this->save($exercise); + $exercise->addToList($this->id); + } +} diff --git a/public/main/exercise/exercise.class.php b/public/main/exercise/exercise.class.php index 32dfdfc09be..dcb0f27449c 100644 --- a/public/main/exercise/exercise.class.php +++ b/public/main/exercise/exercise.class.php @@ -3143,6 +3143,19 @@ public function save_stat_track_exercise_info( $clock_expired_time = null; } + $questionList = array_filter( + $questionList, + function (int $qid) { + $q = Question::read($qid); + return $q + && !in_array( + $q->type, + [PAGE_BREAK, MEDIA_QUESTION], + true + ); + } + ); + $questionList = array_map('intval', $questionList); $em = Database::getManager(); @@ -5885,6 +5898,13 @@ public function manage_answer( // Store results directly in the database // For all in one page exercises, the results will be // stored by exercise_results.php (using the session) + if (in_array( + $objQuestionTmp->type, + [PAGE_BREAK, MEDIA_QUESTION], + true + )) { + $save_results = false; + } if ($save_results) { if ($debug) { error_log("Save question results $save_results"); diff --git a/public/main/exercise/exercise_show.php b/public/main/exercise/exercise_show.php index 5597c455f87..4ab956c7227 100644 --- a/public/main/exercise/exercise_show.php +++ b/public/main/exercise/exercise_show.php @@ -404,6 +404,7 @@ function getFCK(vals, marksid) { $marksid = ''; $countPendingQuestions = 0; +$panelsByParent = []; foreach ($questionList as $questionId) { $choice = isset($exerciseResult[$questionId]) ? $exerciseResult[$questionId] : ''; // destruction of the Question object @@ -419,6 +420,11 @@ function getFCK(vals, marksid) { $questionWeighting = $objQuestionTmp->selectWeighting(); $answerType = $objQuestionTmp->selectType(); + $objQ = Question::read($questionId, $objExercise->course); + if (!$objQ || $objQ->type === MEDIA_QUESTION) { + continue; + } + // Start buffer ob_start(); if (MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType) { @@ -882,7 +888,10 @@ class="exercise_mark_select" $counter++; $questionContent .= $contents; $questionContent .= ''; - $exercise_content .= Display::panel($questionContent); + //$exercise_content .= Display::panel($questionContent); + $panelHtml = Display::panel($questionContent); + $parentId = (int) $objQ->parent_id; + $panelsByParent[$parentId][] = $panelHtml; } // end of large foreach on questions // Display the text when finished message if we are on a LP #4227 @@ -966,7 +975,44 @@ class="exercise_mark_select" } echo $totalScoreText; -echo $exercise_content; +//echo $exercise_content; +foreach ($panelsByParent as $pid => $panels) { + if ($pid !== 0) { + $mediaQ = Question::read($pid, $objExercise->course); + echo '
'; + echo '
'; + ob_start(); + $objExercise->manage_answer( + $id, + $pid, + null, + 'exercise_show', + [], + false, + true, + $show_results, + $objExercise->selectPropagateNeg() + ); + echo ob_get_clean(); + echo '
'; + + if (!empty($mediaQ->question)) { + echo '
'; + echo $mediaQ->description; + echo '
'; + } + + echo '
'; + } + + foreach ($panels as $panelHtml) { + echo $panelHtml; + } + + if ($pid !== 0) { + echo '
'; + } +} // only show "score" in bottom of page if there's exercise content if ($show_results) { diff --git a/public/main/exercise/exercise_submit.php b/public/main/exercise/exercise_submit.php index 84026cdae43..dca92d29a69 100644 --- a/public/main/exercise/exercise_submit.php +++ b/public/main/exercise/exercise_submit.php @@ -56,23 +56,8 @@ $htmlHeadXtra[] = ''; $htmlHeadXtra[] = api_get_js('jquery.highlight.js'); } - -//$js = ''; -//$htmlHeadXtra[] = $js; - -//$htmlHeadXtra[] = api_get_js('jqueryui-touch-punch/jquery.ui.touch-punch.min.js'); -//$htmlHeadXtra[] = api_get_js('jquery.jsPlumb.all.js'); -//$htmlHeadXtra[] = api_get_js('d3/jquery.xcolor.js'); - -//This library is necessary for the time control feature -//tmlHeadXtra[] = api_get_css(api_get_path(WEB_LIBRARY_PATH).'javascript/epiclock/stylesheet/jquery.epiclock.css'); -//$htmlHeadXtra[] = api_get_css(api_get_path(WEB_LIBRARY_PATH).'javascript/epiclock/renderers/minute/epiclock.minute.css'); -//$htmlHeadXtra[] = api_get_js('epiclock/javascript/jquery.dateformat.min.js'); -//$htmlHeadXtra[] = api_get_js('epiclock/javascript/jquery.epiclock.min.js'); -//$htmlHeadXtra[] = api_get_js('epiclock/renderers/minute/epiclock.minute.js'); $htmlHeadXtra[] = api_get_build_js('legacy_exercise.js'); $htmlHeadXtra[] = ''; -//$htmlHeadXtra[] = ''; if ('true' === api_get_setting('exercise.quiz_prevent_copy_paste')) { $htmlHeadXtra[] = ''; } @@ -130,6 +115,8 @@ $questionCategoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : 0; $current_question = $currentQuestionFromUrl = isset($_REQUEST['num']) ? (int) $_REQUEST['num'] : null; $currentAnswer = isset($_REQUEST['num_answer']) ? (int) $_REQUEST['num_answer'] : null; +$page = isset($_REQUEST['page']) ? (int) $_REQUEST['page'] : 1; +$currentBreakId = isset($_REQUEST['currentBreakId']) ? (int) $_REQUEST['currentBreakId'] : null; $logInfo = [ 'tool' => TOOL_QUIZ, @@ -378,6 +365,27 @@ // Fix in order to get the correct question list. $questionListUncompressed = $objExercise->getQuestionListWithMediasUncompressed(); Session::write('question_list_uncompressed', $questionListUncompressed); + +if (ONE_PER_PAGE == $objExercise->type) { + $filtered = []; + foreach ($questionListUncompressed as $qid) { + $q = Question::read($qid); + if ( + $q + && $q->type !== PAGE_BREAK + && $q->type !== MEDIA_QUESTION + ) { + $filtered[] = $qid; + } + } + $questionListUncompressed = $filtered; + Session::write('question_list_uncompressed', $questionListUncompressed); + + if (Session::read('questionList') !== null) { + Session::write('questionList', $filtered); + } +} + $clock_expired_time = null; if (empty($exercise_stat_info)) { $disable = ('true' === api_get_setting('exercise.exercises_disable_new_attempts')); @@ -447,22 +455,10 @@ if (!empty($resolvedQuestions) && !empty($exercise_stat_info['data_tracking']) ) { - /*$last = current(end($resolvedQuestions)); - $attemptQuestionList = explode(',', $exercise_stat_info['data_tracking']); - $count = 1; - foreach ($attemptQuestionList as $question) { - if ($last['question_id'] == $question) { - break; - } - $count++; - } - $current_question = $count; - */ // Get current question based in data_tracking question list, instead of track_e_attempt order BT#17789. $resolvedQuestionsQuestionIds = array_keys($resolvedQuestions); $count = 0; $attemptQuestionList = explode(',', $exercise_stat_info['data_tracking']); - //var_dump($attemptQuestionList, $resolvedQuestionsQuestionIds); foreach ($attemptQuestionList as $index => $question) { if (in_array($question, $resolvedQuestionsQuestionIds)) { $count = $index; @@ -470,7 +466,6 @@ } } $current_question = $count; - //var_dump($current_question, $index);exit; } } } @@ -493,11 +488,13 @@ // Selects the list of question ID $questionList = $objExercise->getQuestionList(); - // Media questions. - $media_is_activated = $objExercise->mediaIsActivated(); + $questionList = array_filter($questionList, function(int $qid) { + $q = Question::read($qid); + return $q && $q->type !== MEDIA_QUESTION; + }); // Getting order from random - if (false == $media_is_activated && + if ( ( $objExercise->isRandom() || !empty($objExercise->getRandomByCategory()) || @@ -507,6 +504,10 @@ !empty($exercise_stat_info['data_tracking']) ) { $questionList = explode(',', $exercise_stat_info['data_tracking']); + $questionList = array_filter($questionList, function(int $qid) { + $q = Question::read($qid); + return $q && $q->type !== MEDIA_QUESTION; + }); $categoryList = []; if ($allowBlockCategory) { foreach ($questionList as $question) { @@ -531,7 +532,11 @@ $myRemindList = array_filter($myRemindList); } -$params = "exe_id=$exe_id&exerciseId=$exerciseId&learnpath_id=$learnpath_id&learnpath_item_id=$learnpath_item_id&learnpath_item_view_id=$learnpath_item_view_id&".api_get_cidreq().'&reminder='.$reminder; +$params = "exe_id=$exe_id&exerciseId=$exerciseId&learnpath_id=$learnpath_id" + . "&learnpath_item_id=$learnpath_item_id&learnpath_item_view_id=$learnpath_item_view_id" + . "&page=" . ($page ?? 1) + . "&" . api_get_cidreq(); + if (2 === $reminder && empty($myRemindList)) { if ($debug) { @@ -638,6 +643,102 @@ } } +// Remove any leading page breaks +while (count($questionList) > 0) { + // reset() moves the internal pointer to the first element… + $firstId = reset($questionList); + // …key() returns its key, so we can unset by that key + $firstKey = key($questionList); + $q = Question::read((int) $firstId); + if ($q && $q->type === PAGE_BREAK) { + unset($questionList[$firstKey]); + } else { + // stop once the first element is not a page break + break; + } +} + +// Remove any trailing page breaks +while (count($questionList) > 0) { + // end() moves the internal pointer to the last element + $lastId = end($questionList); + $lastKey = key($questionList); + $q = Question::read((int) $lastId); + if ($q && $q->type === PAGE_BREAK) { + unset($questionList[$lastKey]); + } else { + // stop once the last element is not a page break + break; + } +} + +$hasMediaWithChildren = false; +$mediaQids = array_filter($objExercise->getQuestionOrderedList(), function(int $qid) { + $q = Question::read($qid); + return $q && $q->type === MEDIA_QUESTION; +}); + +if (!empty($mediaQids)) { + foreach ($questionListUncompressed as $qid) { + $q = Question::read($qid); + if ($q && in_array($q->parent_id, $mediaQids, true)) { + $hasMediaWithChildren = true; + break; + } + } +} + +$forceGrouped = (ONE_PER_PAGE === $objExercise->type && $hasMediaWithChildren); +if ($forceGrouped) { + $objExercise->type = ALL_ON_ONE_PAGE; +} + +if (ALL_ON_ONE_PAGE === $objExercise->type || $forceGrouped) { + $flat = array_filter($questionList, function(int $qid) { + $q = Question::read($qid); + return $q && $q->type !== MEDIA_QUESTION; + }); + + if ($hasMediaWithChildren) { + $groups = []; + $seen = []; + foreach ($flat as $qid) { + $q = Question::read($qid); + if ($q->parent_id > 0) { + $pid = $q->parent_id; + $groups[$pid]['questions'][] = $qid; + $seen[$qid] = true; + } + } + foreach ($flat as $qid) { + if (!isset($seen[$qid])) { + $groups[$qid]['questions'] = [$qid]; + } + } + $pages = array_values($groups); + $totalPages = count($pages); + $page = min(max(1, $page), $totalPages); + $questionList = $pages[$page - 1]['questions']; + $currentBreakId = null; + } else { + $pages = [[]]; + $breakIds = [null]; + foreach ($flat as $qid) { + $q = Question::read($qid); + if ($q->type === PAGE_BREAK) { + $pages[] = []; + $breakIds[] = $qid; + } else { + $pages[count($pages) - 1][] = $qid; + } + } + $totalPages = count($pages); + $page = min(max(1, $page), $totalPages); + $questionList = $pages[$page - 1]; + $currentBreakId = ($page > 1 ? $breakIds[$page - 1] : null); + } +} + $isLastQuestionInCategory = 0; if ($allowBlockCategory && ONE_PER_PAGE == $objExercise->type && @@ -683,13 +784,6 @@ $count++; } - //var_dump($questionCheck);exit; - // Use reminder list to get the current question. - /*if (2 === $reminder && !empty($myRemindList)) { - $remindQuestionId = current($myRemindList); - $questionCheck = Question::read($remindQuestionId); - }*/ - $categoryId = 0; if (null !== $questionCheck) { $categoryId = $questionCheck->category; @@ -698,12 +792,10 @@ if ($objExercise->review_answers && isset($_GET['category_id'])) { $categoryId = $_GET['category_id'] ?? 0; } - //var_dump($categoryId, $categoryList); if (!empty($categoryId)) { $categoryInfo = $categoryList[$categoryId]; $count = 1; $total = count($categoryList[$categoryId]); - //var_dump($questionCheck); foreach ($categoryList[$categoryId] as $checkQuestionId) { if ((int) $checkQuestionId === (int) $questionCheck->iid) { break; @@ -711,7 +803,6 @@ $count++; } - //var_dump($count , $total); if ($count === $total) { $isLastQuestionInCategory = $categoryId; if ($isLastQuestionInCategory) { @@ -731,7 +822,6 @@ // $isLastQuestionInCategory = $categoryId; } } - //var_dump($categoryId, $blockedCategories, $isLastQuestionInCategory); // Blocked if category was already answered. if ($categoryId && in_array($categoryId, $blockedCategories)) { @@ -1334,6 +1424,43 @@ function updateDuration() { } } + var page = '.(int) $page.'; + var totalPages = '.(int) ($totalPages ?? 1).'; + function navigateNext() { + var url; + if (page === totalPages) { + url = "exercise_result.php?'.api_get_cidreq().'&exe_id='.$exe_id.'&learnpath_id='.$learnpath_id.'&learnpath_item_id='.$learnpath_item_id.'&learnpath_item_view_id='.$learnpath_item_view_id.'"; + } else { + url = "'.api_get_self().'?'.api_get_cidreq().'&exerciseId='.$exerciseId.'&page=" + (page + 1) + "&reminder='.$reminder.'"; + } + window.location = url; + } + + function save_question_list(question_list) { + if (!question_list.length) { + return navigateNext(); + } + var saves = $.map(question_list, function(qid) { + var my_choice = $(\'*[name*="choice[\'+qid+\']"]\').serialize(); + var remind_list = $(\'*[name*="remind_list"]\').serialize(); + var hotspot = $(\'*[name*="hotspot[\'+qid+\']"]\').serialize(); + var dc = $(\'*[name*="choiceDegreeCertainty[\'+qid+\']"]\').serialize(); + var dataStr = "'.$params.'&type=simple&question_id="+qid + +"&"+my_choice + + (hotspot ? "&"+hotspot : "") + + (remind_list ? "&"+remind_list : "") + + (dc ? "&"+dc : ""); + return $.ajax({ + type: "POST", + url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=save_exercise_by_now", + data: dataStr + }); + }); + $.when.apply($, saves).always(function(){ + navigateNext(); + }); + } + $(function() { '.$questionTimeCondition.' //This pre-load the save.png icon @@ -1376,20 +1503,27 @@ function updateDuration() { $(\'button[name="save_question_list"]\').on(\'touchstart click\', function (e) { e.preventDefault(); e.stopPropagation(); - var $this = $(this); - var questionList = $this.data(\'list\').split(","); + var $btn = $(this); + + $(\'button[name="save_question_list"]\').prop(\'disabled\', true); + $btn.append(\' ' . addslashes($loading) . '\'); - save_question_list(questionList); + var listStr = $btn.data(\'list\') || \'\'; + if (!listStr) { + return navigateNext(); + } + var arr = listStr.toString().split(\',\'); + save_question_list(arr); }); - $(\'button[name="check_answers"]\').on(\'touchstart click\', function (e) { - e.preventDefault(); - e.stopPropagation(); - var $this = $(this); - var questionId = parseInt($this.data(\'question\')) || 0; + $(\'button[name="check_answers"]\').on(\'touchstart click\', function (e) { + e.preventDefault(); + e.stopPropagation(); + var $this = $(this); + var questionId = parseInt($this.data(\'question\')) || 0; - save_now(questionId, "check_answers"); - }); + save_now(questionId, "check_answers"); + }); $(\'button[name="save_now"]\').on(\'touchstart click\', function (e) { e.preventDefault(); @@ -1421,26 +1555,12 @@ function previous_question(question_num) { } function previous_question_and_save(previous_question_id, question_id_to_save) { - var url = "exercise_submit.php?'.$params.'&num="+previous_question_id; - //Save the current question - save_now(question_id_to_save, url); - } - - function save_question_list(question_list) { - $.each(question_list, function(key, question_id) { - save_now(question_id, null); - }); - - var url = ""; - if ('.$reminder.' == 1 ) { - url = "exercise_reminder.php?'.$params.'&num='.$current_question.'"; - } else if ('.$reminder.' == 2 ) { - url = "exercise_submit.php?'.$params.'&num='.$current_question.'&remind_question_id='.$remind_question_id.'&reminder=2"; - } else { - url = "exercise_submit.php?'.$params.'&num='.$current_question.'&remind_question_id='.$remind_question_id.'"; - } + save_now(question_id_to_save, null); + var url = \'exercise_submit.php?'. api_get_cidreq() .'&exerciseId='. $exerciseId .'&page=\' + + previous_question_id + + \'&reminder='. $reminder .'\'; window.location = url; - } + } function redirectExerciseToResult() { @@ -1632,9 +1752,12 @@ function validate_all() { window.quizTimeEnding = false; '; - echo '
+echo ' @@ -1660,6 +1783,18 @@ function validate_all() { $remind_list = explode(',', $exercise_stat_info['questions_to_check']); } + if ($currentBreakId) { + ExerciseLib::showQuestion( + $objExercise, + $currentBreakId, + false, + $origin, + '', + false + ); + } + + $prevParent = null; foreach ($questionList as $questionId) { // for sequential exercises if (ONE_PER_PAGE == $objExercise->type) { @@ -1757,6 +1892,25 @@ function validate_all() { } } + $q = Question::read($questionId); + $currentParent = $q->parent_id > 0 ? $q->parent_id : null; + if ($currentParent !== $prevParent) { + if ($prevParent !== null) { + echo "\n"; + } + if ($currentParent !== null) { + echo '
'; + ExerciseLib::showQuestion( + $objExercise, + $currentParent, + false, + $origin, + '', + false + ); + } + } + echo '
'; $showQuestion = true; @@ -1769,6 +1923,7 @@ function validate_all() { // Shows the question and its answers if ($showQuestion) { + $user_choice = $attempt_list[$questionId] ?? null; ExerciseLib::showQuestion( $objExercise, $questionId, @@ -1788,39 +1943,41 @@ function validate_all() { } // Button save and continue - switch ($objExercise->type) { - case ONE_PER_PAGE: - $exerciseActions .= $objExercise->show_button( - $questionId, - $current_question, - [], - [], - $myRemindList, - $showPreviousButton - ); - - break; - case ALL_ON_ONE_PAGE: - if (api_is_allowed_to_session_edit()) { - $button = [ - Display::button( - 'save_now', - get_lang('Save and continue'), - [ - 'type' => 'button', - 'class' => 'btn btn--info', - 'data-question' => $questionId, - ] - ), - ' ', - ]; - $exerciseActions .= Display::div( - implode(PHP_EOL, $button), - ['class' => 'exercise_save_now_button mb-4'] + if (!$hasMediaWithChildren) { + switch ($objExercise->type) { + case ONE_PER_PAGE: + $exerciseActions .= $objExercise->show_button( + $questionId, + $current_question, + [], + [], + $myRemindList, + $showPreviousButton ); - } - break; + break; + case ALL_ON_ONE_PAGE: + if (api_is_allowed_to_session_edit()) { + $button = [ + Display::button( + 'save_now', + get_lang('Save and continue'), + [ + 'type' => 'button', + 'class' => 'btn btn--info', + 'data-question' => $questionId, + ] + ), + ' ', + ]; + $exerciseActions .= Display::div( + implode(PHP_EOL, $button), + ['class' => 'exercise_save_now_button mb-4'] + ); + } + + break; + } } // Checkbox review answers @@ -1847,6 +2004,8 @@ function validate_all() { echo '
'; $i++; + $prevParent = $currentParent; + // for sequential exercises if (ONE_PER_PAGE == $objExercise->type) { // quits the loop @@ -1854,13 +2013,35 @@ function validate_all() { } } - if (ALL_ON_ONE_PAGE == $objExercise->type) { - $exerciseActions = $objExercise->show_button( - $questionId, - $current_question - ); - echo Display::div($exerciseActions, ['class' => 'exercise_actions']); - echo '
'; +if ($prevParent !== null) { + echo "
\n"; +} + + +if (ALL_ON_ONE_PAGE == $objExercise->type || $forceGrouped) { + //$currentPageIds = implode(',', $pages[$page - 1]); + $currentPageIds = implode(',', $questionList); + echo '
'; + if ($page > 1) { + $prevUrl = api_get_self() . '?' . api_get_cidreq() + . "&exerciseId=$exerciseId&page=" . ($page - 1) + . "&reminder=$reminder"; + echo ' '; + } + + $label = $page < $totalPages + ? get_lang('Next') . ' ›' + : get_lang('End test'); + echo ''; + + echo '
'; } echo '
'; if (!in_array($origin, ['learnpath', 'embeddable'])) { diff --git a/public/main/exercise/question.class.php b/public/main/exercise/question.class.php index cc54ef952d8..33a8fbfd581 100644 --- a/public/main/exercise/question.class.php +++ b/public/main/exercise/question.class.php @@ -68,9 +68,10 @@ abstract class Question UNIQUE_ANSWER_IMAGE => ['UniqueAnswerImage.php', 'UniqueAnswerImage'], DRAGGABLE => ['Draggable.php', 'Draggable'], MATCHING_DRAGGABLE => ['MatchingDraggable.php', 'MatchingDraggable'], - //MEDIA_QUESTION => array('media_question.class.php' , 'MediaQuestion') + MEDIA_QUESTION => ['MediaQuestion.php', 'MediaQuestion'], ANNOTATION => ['Annotation.php', 'Annotation'], READING_COMPREHENSION => ['ReadingComprehension.php', 'ReadingComprehension'], + PAGE_BREAK => ['PageBreakQuestion.php', 'PageBreakQuestion'], ]; /** @@ -211,6 +212,10 @@ public static function read($id, $course_info = [], $getExerciseList = true) } } + $objQuestion->parent_id = isset($object->parent_media_id) + ? (int) $object->parent_media_id + : 0; + return $objQuestion; } } @@ -544,7 +549,8 @@ public function save($exercise) ->setType($this->type) ->setExtra($this->extra) ->setLevel((int) $this->level) - ->setFeedback($this->feedback); + ->setFeedback($this->feedback) + ->setParentMediaId($this->parent_id); if (!empty($categoryId)) { $category = $questionCategoryRepo->find($categoryId); @@ -586,13 +592,9 @@ public function save($exercise) ->setExtra($this->extra) ->setLevel((int) $this->level) ->setFeedback($this->feedback) + ->setParentMediaId($this->parent_id) ->setParent($courseEntity) - ->addCourseLink( - $courseEntity, - api_get_session_entity(), - api_get_group_entity() - ) - ; + ->addCourseLink($courseEntity, api_get_session_entity(), api_get_group_entity()); $em->persist($question); $em->flush(); @@ -843,6 +845,7 @@ public function addToList($exerciseId, $fromSave = false) public function removeFromList($exerciseId, $courseId = 0) { $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION); + $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION); $id = (int) $this->id; $exerciseId = (int) $exerciseId; @@ -881,6 +884,11 @@ public function removeFromList($exerciseId, $courseId = 0) quiz_id = $exerciseId"; Database::query($sql); + $reset = "UPDATE $tableQuestion + SET parent_media_id = NULL + WHERE parent_media_id = $id"; + Database::query($reset); + return true; } } @@ -936,6 +944,11 @@ public function delete($deleteFromEx = 0) } } + $reset = "UPDATE $TBL_QUESTIONS + SET parent_media_id = NULL + WHERE parent_media_id = $id"; + Database::query($reset); + $sql = "DELETE FROM $TBL_EXERCISE_QUESTION WHERE question_id = ".$id; Database::query($sql); @@ -1260,6 +1273,14 @@ public function createForm(&$form, $exercise) get_lang('Category'), TestCategory::getCategoriesIdAndName() ); + + $courseMedias = self::prepare_course_media_select($exercise->iId); + $form->addSelect( + 'parent_id', + get_lang('Attach to media'), + $courseMedias + ); + if (EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM == $exercise->getQuestionSelectionType() && ('true' === api_get_setting('exercise.allow_mandatory_question_in_category')) ) { @@ -1310,9 +1331,6 @@ public function createForm(&$form, $exercise) break; } - //Medias - //$course_medias = self::prepare_course_media_select(api_get_course_int_id()); - //$form->addSelect('parent_id', get_lang('Attach to media'), $course_medias); } $form->addElement('html', ''); @@ -1356,13 +1374,15 @@ public function createForm(&$form, $exercise) $extraField->addElements($form, $this->iid); // default values - $defaults = []; - $defaults['questionName'] = $this->question; - $defaults['questionDescription'] = $this->description; - $defaults['questionLevel'] = $this->level; - $defaults['questionCategory'] = $this->category; - $defaults['feedback'] = $this->feedback; - $defaults['mandatory'] = $this->mandatory; + $defaults = [ + 'questionName' => $this->question, + 'questionDescription' => $this->description, + 'questionLevel' => $this->level, + 'questionCategory' => $this->category, + 'feedback' => $this->feedback, + 'mandatory' => $this->mandatory, + 'parent_id' => $this->parent_id, + ]; // Came from he question pool if (isset($_GET['fromExercise'])) { @@ -1387,6 +1407,7 @@ public function createForm(&$form, $exercise) */ public function processCreation(FormValidator $form, Exercise $exercise) { + $this->parent_id = (int) $form->getSubmitValue('parent_id'); $this->updateTitle($form->getSubmitValue('questionName')); $this->updateDescription($form->getSubmitValue('questionDescription')); $this->updateLevel($form->getSubmitValue('questionLevel')); @@ -1896,19 +1917,35 @@ public static function get_count_course_medias($course_id) * * @return array */ - public static function prepare_course_media_select($course_id) + public static function prepare_course_media_select(int $quizId): array { - $medias = self::get_course_medias($course_id); - $media_list = []; - $media_list[0] = get_lang('Not linked to media'); + $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION); + $tableRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION); - if (!empty($medias)) { - foreach ($medias as $media) { - $media_list[$media['id']] = empty($media['question']) ? get_lang('Untitled') : $media['question']; - } + $medias = Database::select( + '*', + "$tableQuestion q + JOIN $tableRelQuestion rq ON rq.question_id = q.iid", + [ + 'where' => [ + 'rq.quiz_id = ? AND (q.parent_media_id IS NULL OR q.parent_media_id = 0) AND q.type = ?' + => [$quizId, MEDIA_QUESTION], + ], + 'order' => 'question ASC', + ] + ); + + $mediaList = [ + 0 => get_lang('Not linked to media'), + ]; + + foreach ($medias as $media) { + $mediaList[$media['question_id']] = empty($media['question']) + ? get_lang('Untitled') + : $media['question']; } - return $media_list; + return $mediaList; } /** diff --git a/public/main/exercise/question_list_admin.inc.php b/public/main/exercise/question_list_admin.inc.php index ec9fc38a97f..ca5f9db4179 100644 --- a/public/main/exercise/question_list_admin.inc.php +++ b/public/main/exercise/question_list_admin.inc.php @@ -127,6 +127,15 @@ selectType() === ONE_PER_PAGE + && $objExercise->hasQuestionWithType(PAGE_BREAK) +) { + echo Display::return_message( + get_lang('This exercise contains page-break questions, which will only take effect if the exercise is set to “All questions on one page".'), + 'warning' + ); +} + // Filter the type of questions we can add Question::displayTypeMenu($objExercise); diff --git a/public/main/inc/lib/api.lib.php b/public/main/inc/lib/api.lib.php index a855f1a7071..9cca9b5cc63 100644 --- a/public/main/inc/lib/api.lib.php +++ b/public/main/inc/lib/api.lib.php @@ -494,6 +494,7 @@ define('ANNOTATION', 20); define('READING_COMPREHENSION', 21); define('MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY', 22); +define('PAGE_BREAK', 23); define('EXERCISE_CATEGORY_RANDOM_SHUFFLED', 1); define('EXERCISE_CATEGORY_RANDOM_ORDERED', 2); diff --git a/public/main/inc/lib/events.lib.php b/public/main/inc/lib/events.lib.php index 95c53fdddb3..7d0af55ccb4 100644 --- a/public/main/inc/lib/events.lib.php +++ b/public/main/inc/lib/events.lib.php @@ -368,6 +368,21 @@ public static function updateEventExercise( $questionsList = array_map('intval', $questionsList); } + if (!empty($questionsList)) { + $questionsList = array_map('intval', $questionsList); + $questionsList = array_filter( + $questionsList, + function (int $qid) { + $q = Question::read($qid); + return $q && !in_array( + $q->type, + [PAGE_BREAK, MEDIA_QUESTION], + true + ); + } + ); + } + if (!empty($remindList)) { $remindList = array_map('intval', $remindList); $remindList = array_filter($remindList); diff --git a/public/main/inc/lib/exercise.lib.php b/public/main/inc/lib/exercise.lib.php index c1ef62169c5..819d18f7d99 100644 --- a/public/main/inc/lib/exercise.lib.php +++ b/public/main/inc/lib/exercise.lib.php @@ -76,6 +76,25 @@ public static function showQuestion( } $answerType = $objQuestionTmp->selectType(); + + if (MEDIA_QUESTION === $answerType) { + $mediaHtml = $objQuestionTmp->selectDescription(); + if (!empty($mediaHtml)) { + echo '
'. $mediaHtml .'
'; + } + return 0; + } + + if (PAGE_BREAK === $answerType) { + $description = $objQuestionTmp->selectDescription(); + if (!$only_questions && !empty($description)) { + echo '
' + . $description . + '
'; + } + return 0; + } + $s = ''; if (HOT_SPOT != $answerType && HOT_SPOT_DELINEATION != $answerType && @@ -4426,6 +4445,7 @@ public static function displayQuestionListByAttempt( $countPendingQuestions = 0; $result = []; + $panelsByParent = []; // Loop over all question to show results for each of them, one by one if (!empty($question_list)) { foreach ($question_list as $questionId) { @@ -4610,15 +4630,43 @@ public static function displayQuestionListByAttempt( $calculatedScore['question_content'] = $questionContent; $attemptResult[] = $calculatedScore; + $parentId = intval($objQuestionTmp->parent_id ?: 0); + $panelsByParent[$parentId][] = Display::panel($questionContent); + } - 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); + foreach ($panelsByParent as $pid => $panels) { + if ($pid !== 0) { + $mediaQ = Question::read($pid, $objExercise->course); + echo '
'; + echo '
'; + ob_start(); + $objExercise->manage_answer( + $exeId, + $pid, + null, + 'exercise_show', + [], + false, + true, + $show_results, + $objExercise->selectPropagateNeg() + ); + echo ob_get_clean(); + echo '
'; + if (!empty($mediaQ->description)) { + echo '
' + . $mediaQ->description + . '
'; } + echo '
'; + } + + foreach ($panels as $panelHtml) { + echo $panelHtml; + } + + if ($pid !== 0) { + echo '
'; } } } @@ -4709,7 +4757,6 @@ public static function displayQuestionListByAttempt( $exerciseContent ); - echo $totalScoreText; echo $certificateBlock; // Ofaj change BT#11784 diff --git a/public/main/template/default/exercise/partials/result_exercise.html.twig b/public/main/template/default/exercise/partials/result_exercise.html.twig index e331f809d4a..5f25cef0bc7 100644 --- a/public/main/template/default/exercise/partials/result_exercise.html.twig +++ b/public/main/template/default/exercise/partials/result_exercise.html.twig @@ -2,125 +2,116 @@ {% autoescape false %}
-
-
+
+
{% if 'editor.save_titles_as_html'|api_get_setting == 'true' %} - {{ data.title }} +
{{ data.title|raw }}
{% else %} -

{{ data.title }}

+

{{ data.title }}

{% endif %} -
-
-
- +
+
+
+ {{ data.username }}
- -
-
-
- {{ 'Username'|trans }} - {{ 'ToolIcon::MEMBER' | mdi_icon }} {{ data.username }} +
+
+
+ {{ 'ToolIcon::MEMBER'|mdi_icon }} + {{ 'Username'|trans }}: + {{ data.username }}
{% if data.start_date %} -
- {{ 'Start date'|trans }} - {{ 'ToolIcon::AGENDA' | mdi_icon }} - {{ data.start_date }} +
+ {{ 'ToolIcon::AGENDA'|mdi_icon }} + {{ 'Start date'|trans }}: + {{ data.start_date }}
{% endif %} {% if data.duration %} -
- {{ 'Duration'|trans }} - {{ 'alarm' | mdi_icon }} - {{ data.duration }} +
+ {{ 'alarm'|mdi_icon }} + {{ 'Duration'|trans }}: + {{ data.duration }}
{% endif %} {% if data.ip %} -
- {{ 'IP'|trans }} - {{ 'laptop' | mdi_icon }} - {{ data.ip }} +
+ {{ 'laptop'|mdi_icon }} + {{ 'IP'|trans }}: + {{ data.ip }}
{% endif %} - +
+ -
-
+ +
+
{% if data.number_of_answers_saved != data.number_of_answers %} - - {{ '%d / %d answers saved.'|trans|format(data.number_of_answers_saved, data.number_of_answers) }} - + + {{ '%d / %d answers saved.'|trans|format(data.number_of_answers_saved, data.number_of_answers) }} + {% else %} - - {{ '%d / %d answers saved.'|trans|format(data.number_of_answers_saved, data.number_of_answers) }} - - {% endif %} - - {% if 'exercise.quiz_confirm_saved_answers'|api_get_setting == 'true' %} - {% set enable_form = data.track_confirmation.updatedAt is empty and data.track_confirmation.userId == _u.id %} -
-
-
-
- -
- {% if enable_form %} - - {{ 'If you are not satisfied, do not check the acceptance box and consult the course manager or the platform administrator.'|trans }} - - {% endif %} -
-
- {% if enable_form %} -
-
- - -
-
- {% endif %} -
+ + {{ '%d / %d answers saved.'|trans|format(data.number_of_answers_saved, data.number_of_answers) }} + {% endif %}
+ {% if 'exercise.quiz_confirm_saved_answers'|api_get_setting == 'true' %} + {% set enable_form = data.track_confirmation.updatedAt is empty and data.track_confirmation.userId == _u.id %} +
+ + + {% if enable_form %} +

+ {{ 'If you are not satisfied, do not check the acceptance box and consult the course manager or the platform administrator.'|trans }} +

+
+ + +
+ {% endif %} +
+ {% endif %}