Merge branch 'MDL-79295-master' of https://github.com/jleyva/moodle

This commit is contained in:
Ilya Tregubov
2024-02-06 11:15:48 +08:00
3 changed files with 80 additions and 33 deletions
+52 -28
View File
@@ -933,10 +933,13 @@ class mod_quiz_external extends external_api {
'hasautosavedstep' => new external_value(PARAM_BOOL, 'whether this question attempt has autosaved data',
VALUE_OPTIONAL),
'flagged' => new external_value(PARAM_BOOL, 'whether the question is flagged or not'),
'state' => new external_value(PARAM_ALPHA, 'the state where the question is in.
'state' => new external_value(PARAM_ALPHA, 'the state where the question is in terms of correctness.
It will not be returned if the user cannot see it due to the quiz display correctness settings.',
VALUE_OPTIONAL),
'status' => new external_value(PARAM_RAW, 'current formatted state of the question', VALUE_OPTIONAL),
'stateclass' => new external_value(PARAM_NOTAGS,
'A machine-readable class name for the state that this question attempt is in, as returned by question_usage_by_activity::get_question_state_class().
Always returned.', VALUE_OPTIONAL),
'status' => new external_value(PARAM_RAW, 'Human readable state of the question.', VALUE_OPTIONAL),
'blockedbyprevious' => new external_value(PARAM_BOOL, 'whether the question is blocked by the previous question',
VALUE_OPTIONAL),
'mark' => new external_value(PARAM_RAW, 'the mark awarded.
@@ -970,41 +973,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']) {
@@ -1016,6 +996,8 @@ class mod_quiz_external extends external_api {
if ($showcorrectness) {
$question['state'] = (string) $attemptobj->get_question_state($slot);
}
// The stateclass is used for CSS classes but also for the lang strings.
$question['stateclass'] = $attemptobj->get_question_state_class($slot, $displayoptions->correctness);
$question['status'] = $attemptobj->get_question_status($slot, $displayoptions->correctness);
$question['blockedbyprevious'] = $attemptobj->is_blocked_by_previous_question($slot);
}
@@ -1025,9 +1007,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;
}
@@ -1165,6 +1182,12 @@ class mod_quiz_external extends external_api {
$result['warnings'] = $warnings;
$result['questions'] = self::get_attempt_questions_data($attemptobj, false, 'all');
if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS && $attemptobj->get_quiz()->navmethod == 'free') {
// Only count the unanswered question if the navigation method is set to free.
$result['totalunanswered'] = $attemptobj->get_number_of_unanswered_questions();
}
return $result;
}
@@ -1178,6 +1201,7 @@ class mod_quiz_external extends external_api {
return new external_single_structure(
[
'questions' => new external_multiple_structure(self::question_structure()),
'totalunanswered' => new external_value(PARAM_INT, 'Total unanswered questions.', VALUE_OPTIONAL),
'warnings' => new external_warnings(),
]
);
+23 -5
View File
@@ -1165,7 +1165,9 @@ class external_test extends externallib_advanced_testcase {
$this->assertArrayNotHasKey('number', $result['questions'][0]);
$this->assertEquals('1.a', $result['questions'][0]['questionnumber']);
$this->assertEquals('numerical', $result['questions'][0]['type']);
$this->assertEquals('notyetanswered', $result['questions'][0]['stateclass']);
$this->assertArrayNotHasKey('state', $result['questions'][0]); // We don't receive the state yet.
$this->assertEquals('notyetanswered', $result['questions'][0]['stateclass']);
$this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']);
$this->assertFalse($result['questions'][0]['flagged']);
$this->assertEquals(0, $result['questions'][0]['page']);
@@ -1187,6 +1189,7 @@ class external_test extends externallib_advanced_testcase {
$this->assertEquals(2, $result['questions'][0]['questionnumber']);
$this->assertEquals(2, $result['questions'][0]['number']);
$this->assertEquals('numerical', $result['questions'][0]['type']);
$this->assertEquals('notyetanswered', $result['questions'][0]['stateclass']);
$this->assertArrayNotHasKey('state', $result['questions'][0]); // We don't receive the state yet.
$this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']);
$this->assertFalse($result['questions'][0]['flagged']);
@@ -1201,6 +1204,7 @@ class external_test extends externallib_advanced_testcase {
// Now we should receive the question state.
$result = mod_quiz_external::get_attempt_review($attempt->id, 1);
$result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
$this->assertEquals('notanswered', $result['questions'][0]['stateclass']);
$this->assertEquals('gaveup', $result['questions'][0]['state']);
// Change setting and expect two pages.
@@ -1297,7 +1301,9 @@ class external_test extends externallib_advanced_testcase {
// Check the state, flagged and mark data is correct.
$this->assertEquals('todo', $result['questions'][0]['state']);
$this->assertEquals('notyetanswered', $result['questions'][0]['stateclass']);
$this->assertEquals('todo', $result['questions'][1]['state']);
$this->assertEquals('notyetanswered', $result['questions'][1]['stateclass']);
$this->assertEquals(1, $result['questions'][0]['number']);
$this->assertEquals(2, $result['questions'][1]['number']);
$this->assertFalse($result['questions'][0]['flagged']);
@@ -1315,6 +1321,7 @@ class external_test extends externallib_advanced_testcase {
$this->assertNotEmpty(5, $result['questions'][0]['settings']);
// Check at least some settings returned.
$this->assertCount(4, (array) json_decode($result['questions'][0]['settings']));
$this->assertEquals(2, $result['totalunanswered']); // All questions are unanswered.
// Submit a response for the first question.
$tosubmit = [1 => ['answer' => '3.14']];
@@ -1324,7 +1331,9 @@ class external_test extends externallib_advanced_testcase {
// Check it's marked as completed only the first one.
$this->assertEquals('complete', $result['questions'][0]['state']);
$this->assertEquals('answersaved', $result['questions'][0]['stateclass']);
$this->assertEquals('todo', $result['questions'][1]['state']);
$this->assertEquals('notyetanswered', $result['questions'][1]['stateclass']);
$this->assertEquals(1, $result['questions'][0]['number']);
$this->assertEquals(2, $result['questions'][1]['number']);
$this->assertFalse($result['questions'][0]['flagged']);
@@ -1337,7 +1346,7 @@ class external_test extends externallib_advanced_testcase {
$this->assertGreaterThanOrEqual($timenow, $result['questions'][1]['lastactiontime']);
$this->assertEquals(false, $result['questions'][0]['hasautosavedstep']);
$this->assertEquals(false, $result['questions'][1]['hasautosavedstep']);
$this->assertEquals(1, $result['totalunanswered']); // Only one question is unanswered.
}
/**
@@ -1370,7 +1379,9 @@ class external_test extends externallib_advanced_testcase {
// Check it's marked as completed only the first one.
$this->assertEquals('complete', $result['questions'][0]['state']);
$this->assertEquals('answersaved', $result['questions'][0]['stateclass']);
$this->assertEquals('todo', $result['questions'][1]['state']);
$this->assertEquals('notyetanswered', $result['questions'][1]['stateclass']);
$this->assertEquals(1, $result['questions'][0]['number']);
$this->assertEquals(2, $result['questions'][1]['number']);
$this->assertFalse($result['questions'][0]['flagged']);
@@ -1403,8 +1414,10 @@ class external_test extends externallib_advanced_testcase {
// Check it's marked as completed only the first one.
$this->assertEquals('complete', $result['questions'][0]['state']);
$this->assertEquals('answersaved', $result['questions'][0]['stateclass']);
$this->assertEquals(1, $result['questions'][0]['sequencecheck']);
$this->assertEquals('complete', $result['questions'][1]['state']);
$this->assertEquals('answersaved', $result['questions'][1]['stateclass']);
$this->assertEquals(1, $result['questions'][1]['sequencecheck']);
}
@@ -2155,17 +2168,21 @@ 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.
$this->assertNotContains('totalunanswered', $result); // For sequential quizzes, unanswered questions are not considered.
}
/**
@@ -2218,6 +2235,7 @@ class external_test extends externallib_advanced_testcase {
$data = [
'course' => $this->course->id,
'sumgrades' => 2,
'questionsperpage' => 1,
'preferredbehaviour' => 'deferredfeedback',
'navmethod' => QUIZ_NAVMETHOD_SEQ
];
+5
View File
@@ -4,6 +4,11 @@ This files describes API changes in the quiz code.
* A quiz_structure_modified callback has been added for quiz_ plugins, called from
grade_calculator::recompute_quiz_sumgrades(). Plugins can implement this by creating a `quiz_structure_modified`
class in their namespace with a static `callback` method, see quiz_statistics as an example.
* External functions returning question information, currently get_attempt_summary(), get_attempt_data() and get_attemp_review()
now return a new field called "stateclass". A machine-readable class name for the state that this question attempt is in,
as returned by question_usage_by_activity::get_question_state_class().
* External function mod_quiz_external::get_attempt_summary() now returns a new field "totalunanswered", with the total number of
unanswered questions.
=== 4.3 ===