diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index 4cc7a7aa7a9..5c3b4098756 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -968,41 +968,18 @@ class mod_quiz_external extends external_api { $qattempt = $attemptobj->get_question_attempt($slot); $questiondef = $qattempt->get_question(true); - // Get response files (for questions like essay that allows attachments). - $responsefileareas = []; - foreach (question_bank::get_qtype($qtype)->response_file_areas() as $area) { - if ($files = $attemptobj->get_question_attempt($slot)->get_last_qt_files($area, $contextid)) { - $responsefileareas[$area]['area'] = $area; - $responsefileareas[$area]['files'] = []; - - foreach ($files as $file) { - $responsefileareas[$area]['files'][] = [ - 'filename' => $file->get_filename(), - 'fileurl' => $qattempt->get_response_file_url($file), - 'filesize' => $file->get_filesize(), - 'filepath' => $file->get_filepath(), - 'mimetype' => $file->get_mimetype(), - 'timemodified' => $file->get_timemodified(), - ]; - } - } - } - // Check display settings for question. $settings = $questiondef->get_question_definition_for_external_rendering($qattempt, $displayoptions); + // Navigation information. $question = [ 'slot' => $slot, - 'type' => $qtype, 'page' => $attemptobj->get_question_page($slot), 'questionnumber' => $attemptobj->get_question_number($slot), 'flagged' => $attemptobj->is_question_flagged($slot), - 'html' => $attemptobj->render_question($slot, $review, $renderer) . $PAGE->requires->get_end_code(), - 'responsefileareas' => $responsefileareas, 'sequencecheck' => $qattempt->get_sequence_check_count(), 'lastactiontime' => $qattempt->get_last_step()->get_timecreated(), 'hasautosavedstep' => $qattempt->has_autosaved_step(), - 'settings' => !empty($settings) ? json_encode($settings) : null, ]; if ($question['questionnumber'] === (string) (int) $question['questionnumber']) { @@ -1023,9 +1000,44 @@ class mod_quiz_external extends external_api { if ($displayoptions->marks >= question_display_options::MARK_AND_MAX) { $question['mark'] = $attemptobj->get_question_mark($slot); } - if ($attemptobj->check_page_access($attemptobj->get_question_page($slot), false)) { - $questions[] = $question; + + // Check access. This is needed especially when sequential navigation is enforced. To prevent the student see "future" questions. + $haveaccess = $attemptobj->check_page_access($attemptobj->get_question_page($slot), false); + if (!$haveaccess) { + $question['type'] = ''; + $question['html'] = ''; } + + // For visited pages/questions it is ok to keep data the user already saw. + $questionalreadyseen = $attemptobj->get_currentpage() >= $attemptobj->get_question_page($slot); + + // Information when only the user has access to the question at any moment (free navigation) or already seen. + if ($haveaccess || $questionalreadyseen) { + // Get response files (for questions like essay that allows attachments). + $responsefileareas = []; + foreach (question_bank::get_qtype($qtype)->response_file_areas() as $area) { + if ($files = $attemptobj->get_question_attempt($slot)->get_last_qt_files($area, $contextid)) { + $responsefileareas[$area]['area'] = $area; + $responsefileareas[$area]['files'] = []; + + foreach ($files as $file) { + $responsefileareas[$area]['files'][] = [ + 'filename' => $file->get_filename(), + 'fileurl' => $qattempt->get_response_file_url($file), + 'filesize' => $file->get_filesize(), + 'filepath' => $file->get_filepath(), + 'mimetype' => $file->get_mimetype(), + 'timemodified' => $file->get_timemodified(), + ]; + } + } + } + $question['type'] = $qtype; + $question['html'] = $attemptobj->render_question($slot, $review, $renderer) . $PAGE->requires->get_end_code(); + $question['responsefileareas'] = $responsefileareas; + $question['settings'] = !empty($settings) ? json_encode($settings) : null; + } + $questions[] = $question; } return $questions; } diff --git a/mod/quiz/tests/external/external_test.php b/mod/quiz/tests/external/external_test.php index d3796d5fbf7..5718aec9c85 100644 --- a/mod/quiz/tests/external/external_test.php +++ b/mod/quiz/tests/external/external_test.php @@ -2074,17 +2074,20 @@ class external_test extends externallib_advanced_testcase { } /** - * Test that a sequential navigation quiz is not allowing to see questions in advance for a student + * Test that a sequential navigation quiz is not allowing to see questions content in advance for a student. */ public function test_sequential_navigation_attempt_summary() { // Test user with full capabilities. $quiz = $this->prepare_sequential_quiz(); $attemptobj = $this->create_quiz_attempt_object($quiz); $this->setUser($this->student); - // Check that we do not return other questions than the one currently viewed. + // Check that we do not return content from other questions except than the ones currently viewed. $result = mod_quiz_external::get_attempt_summary($attemptobj->get_attemptid()); - $this->assertCount(1, $result['questions']); - $this->assertStringContainsString('Question (1)', $result['questions'][0]['html']); + $this->assertStringContainsString('Question (1)', $result['questions'][0]['html']); // Current question. + $this->assertEmpty($result['questions'][1]['html']); // Next question. + $this->assertEmpty($result['questions'][2]['html']); // And more. + $this->assertEmpty($result['questions'][3]['html']); // And more. + $this->assertEmpty($result['questions'][4]['html']); // And more. } /** @@ -2137,6 +2140,7 @@ class external_test extends externallib_advanced_testcase { $data = [ 'course' => $this->course->id, 'sumgrades' => 2, + 'questionsperpage' => 1, 'preferredbehaviour' => 'deferredfeedback', 'navmethod' => QUIZ_NAVMETHOD_SEQ ];