diff --git a/public/main/exercise/exercise_submit.php b/public/main/exercise/exercise_submit.php
index e54d59350c5..788f7db69f1 100644
--- a/public/main/exercise/exercise_submit.php
+++ b/public/main/exercise/exercise_submit.php
@@ -467,6 +467,21 @@
}
}
Session::write('exe_id', $exe_id);
+
+// Always restore persisted question order from DB (data_tracking) on resume
+// Rationale: session may be lost between visits; data_tracking is the single source of truth
+// for the question order selected at the very first entry of the attempt.
+if (!empty($exercise_stat_info) && !empty($exercise_stat_info['data_tracking'])) {
+ $restoredQuestionList = array_map('intval', explode(',', $exercise_stat_info['data_tracking']));
+ // Keep original order but remove media questions that are not renderable
+ $restoredQuestionList = array_values(array_filter($restoredQuestionList, function (int $qid) {
+ $q = Question::read($qid);
+ return $q && $q->type !== MEDIA_QUESTION;
+ }));
+ // Store into session for navigation helpers; DB remains the canonical source.
+ Session::write('questionList', $restoredQuestionList);
+}
+
$checkAnswersUrl = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=check_answers&exe_id='.$exe_id.'&'.api_get_cidreq();
$saveDurationUrl = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=update_duration&exe_id='.$exe_id.'&'.api_get_cidreq();
$questionListInSession = Session::read('questionList');
@@ -482,42 +497,43 @@
}
if (!isset($questionListInSession)) {
- // Selects the list of question ID
- $questionList = $objExercise->getQuestionList();
+ // === FIX: Prefer persisted selection (data_tracking) to guarantee stable order across sessions ===
+ if (!empty($exercise_stat_info) && !empty($exercise_stat_info['data_tracking'])) {
+ $questionList = array_map('intval', explode(',', $exercise_stat_info['data_tracking']));
+ } else {
+ // Brand-new attempt fallback: build from current exercise definition
+ $questionList = $objExercise->getQuestionList();
+ }
- $questionList = array_filter($questionList, function(int $qid) {
+ // Keep order; filter out non-renderable media questions
+ $questionList = array_values(array_filter($questionList, function (int $qid) {
$q = Question::read($qid);
return $q && $q->type !== MEDIA_QUESTION;
- });
+ }));
- // Getting order from random
- if (
- (
- $objExercise->isRandom() ||
- !empty($objExercise->getRandomByCategory()) ||
- $selectionType > EX_Q_SELECTION_RANDOM
- ) &&
- isset($exercise_stat_info) &&
- !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;
- });
+ // Prepare category grouping when feature is enabled, using the persisted list order.
+ if ($allowBlockCategory) {
$categoryList = [];
- if ($allowBlockCategory) {
- foreach ($questionList as $question) {
- $categoryId = TestCategory::getCategoryForQuestion($question);
- $categoryList[$categoryId][] = $question;
- }
- Session::write('categoryList', $categoryList);
+ foreach ($questionList as $question) {
+ $categoryId = TestCategory::getCategoryForQuestion($question);
+ $categoryList[$categoryId][] = $question;
}
+ Session::write('categoryList', $categoryList);
}
+
Session::write('questionList', $questionList);
} else {
if (isset($objExercise) && isset($exerciseInSession)) {
$questionList = Session::read('questionList');
+ // Ensure categoryList exists if blocking by category is enabled
+ if ($allowBlockCategory && Session::read('categoryList') === null) {
+ $categoryList = [];
+ foreach ($questionList as $question) {
+ $categoryId = TestCategory::getCategoryForQuestion($question);
+ $categoryList[$categoryId][] = $question;
+ }
+ Session::write('categoryList', $categoryList);
+ }
}
}
// Array to check in order to block the chat
@@ -644,12 +660,19 @@
}
//in LP's is enabled the "remember question" feature?
+// Do not rely on random flags; always prefer persisted data_tracking when available
if (!isset($_SESSION['questionList'])) {
- // selects the list of question ID
- $questionList = $objExercise->get_validated_question_list();
- if ($objExercise->isRandom() && !empty($exercise_stat_info['data_tracking'])) {
- $questionList = explode(',', $exercise_stat_info['data_tracking']);
+ if (!empty($exercise_stat_info['data_tracking'])) {
+ $questionList = array_map('intval', explode(',', $exercise_stat_info['data_tracking']));
+ } else {
+ // Fallback for a brand-new attempt
+ $questionList = $objExercise->get_validated_question_list();
}
+ // Keep order; filter out non-renderable media questions
+ $questionList = array_values(array_filter($questionList, function (int $qid) {
+ $q = Question::read($qid);
+ return $q && $q->type !== MEDIA_QUESTION;
+ }));
Session::write('questionList', $questionList);
} else {
if (isset($objExercise) && isset($_SESSION['objExercise'])) {
@@ -1002,8 +1025,16 @@
$current_question = 1;
$latestQuestionId = Event::getLatestQuestionIdFromAttempt($exe_id);
if ($latestQuestionId) {
- $pos = (int) $objExercise->getPositionInCompressedQuestionList($latestQuestionId);
- $current_question = max(1, $pos + 1);
+ // Resume position using the persisted questionList order
+ // Using DB-backed order avoids inconsistencies when sessions expire.
+ $idx = array_search((int) $latestQuestionId, $questionList ?? [], true);
+ if ($idx === false) {
+ // Fallback to legacy computation if not found in current in-memory list
+ $pos = (int) $objExercise->getPositionInCompressedQuestionList($latestQuestionId);
+ $current_question = max(1, $pos + 1);
+ } else {
+ $current_question = $idx + 1;
+ }
}
}
@@ -1364,42 +1395,42 @@
$script_php = 'exercise_reminder.php';
}
- if (!empty($exercise_sound)) {
- echo '';
- echo '
';
- }
- // Get number of hotspot questions for javascript validation
- $number_of_hotspot_questions = 0;
- $i = 0;
- if (!empty($questionList)) {
- foreach ($questionList as $questionId) {
- $i++;
- $objQuestionTmp = Question::read($questionId);
- $selectType = $objQuestionTmp->selectType();
- // for sequential exercises
-
- if (ONE_PER_PAGE == $objExercise->type) {
- // if it is not the right question, goes to the next loop iteration
- if ($current_question != $i) {
- continue;
- } else {
- if (in_array($selectType, [HOT_SPOT, HOT_SPOT_COMBINATION, HOT_SPOT_DELINEATION])) {
- $number_of_hotspot_questions++;
- }
+}
+// Get number of hotspot questions for javascript validation
+$number_of_hotspot_questions = 0;
+$i = 0;
+if (!empty($questionList)) {
+ foreach ($questionList as $questionId) {
+ $i++;
+ $objQuestionTmp = Question::read($questionId);
+ $selectType = $objQuestionTmp->selectType();
+ // for sequential exercises
- break;
- }
+ if (ONE_PER_PAGE == $objExercise->type) {
+ // if it is not the right question, goes to the next loop iteration
+ if ($current_question != $i) {
+ continue;
} else {
if (in_array($selectType, [HOT_SPOT, HOT_SPOT_COMBINATION, HOT_SPOT_DELINEATION])) {
$number_of_hotspot_questions++;
}
+
+ break;
+ }
+ } else {
+ if (in_array($selectType, [HOT_SPOT, HOT_SPOT_COMBINATION, HOT_SPOT_DELINEATION])) {
+ $number_of_hotspot_questions++;
}
}
}
+}
if ($allowBlockCategory &&
ONE_PER_PAGE == $objExercise->type &&
@@ -1418,13 +1449,13 @@
}
}
}
- $saveIcon = Display::getMdiIcon(
- ActionIcon::SAVE_FORM,
- 'ch-tool-icon',
- null,
- ICON_SIZE_SMALL
- );
- $loading = Display::getMdiIcon('loading', 'animate-spin');
+$saveIcon = Display::getMdiIcon(
+ ActionIcon::SAVE_FORM,
+ 'ch-tool-icon',
+ null,
+ ICON_SIZE_SMALL
+);
+$loading = Display::getMdiIcon('loading', 'animate-spin');
echo '