MDL-74610 quiz: add extra grades to web services the mobile app uses

This commit is contained in:
Tim Hunt
2024-04-09 15:11:44 +01:00
parent e6e427073c
commit 0b7805199f
2 changed files with 133 additions and 14 deletions
+36 -2
View File
@@ -412,6 +412,9 @@ class mod_quiz_external extends external_api {
// Update quiz with override information.
$quiz = quiz_update_effective_access($quiz, $params['userid']);
$attempts = quiz_get_user_attempts($quiz->id, $user->id, $params['status'], $params['includepreviews']);
$quizobj = new quiz_settings($quiz, $cm, $course);
$gradeitemmarks = $quizobj->get_grade_calculator()->compute_grade_item_totals_for_attempts(
array_column($attempts, 'uniqueid'));
$attemptresponse = [];
foreach ($attempts as $attempt) {
$reviewoptions = quiz_get_review_options($quiz, $attempt, $context);
@@ -419,6 +422,15 @@ class mod_quiz_external extends external_api {
($reviewoptions->marks < question_display_options::MARK_AND_MAX || $attempt->state != quiz_attempt::FINISHED)) {
// Blank the mark if the teacher does not allow it.
$attempt->sumgrades = null;
} else if (isset($gradeitemmarks[$attempt->uniqueid])) {
$attempt->gradeitemmarks = [];
foreach ($gradeitemmarks[$attempt->uniqueid] as $gradeitem) {
$attempt->gradeitemmarks[] = [
'name' => \core_external\util::format_string($gradeitem->name, $context),
'grade' => $gradeitem->grade,
'maxgrade' => $gradeitem->maxgrade,
];
}
}
$attemptresponse[] = $attempt;
}
@@ -459,6 +471,13 @@ class mod_quiz_external extends external_api {
'timecheckstate' => new external_value(PARAM_INT, 'Next time quiz cron should check attempt for
state changes. NULL means never check.', VALUE_OPTIONAL),
'sumgrades' => new external_value(PARAM_FLOAT, 'Total marks for this attempt.', VALUE_OPTIONAL),
'gradeitemmarks' => new external_multiple_structure(
new external_single_structure([
'name' => new external_value(PARAM_RAW, 'The name of this grade item.'),
'grade' => new external_value(PARAM_FLOAT, 'The grade this attempt earned for this item.'),
'maxgrade' => new external_value(PARAM_FLOAT, 'The total this grade is out of.'),
], 'The grade for each grade item.'),
'If the quiz has additional grades set up, the mark for each grade for this attempt.', VALUE_OPTIONAL),
'gradednotificationsenttime' => new external_value(PARAM_INT,
'Time when the student was notified that manual grading of their attempt was complete.', VALUE_OPTIONAL),
]
@@ -1455,7 +1474,6 @@ class mod_quiz_external extends external_api {
* @since Moodle 3.1
*/
public static function get_attempt_review($attemptid, $page = -1) {
global $PAGE;
$warnings = [];
@@ -1465,7 +1483,7 @@ class mod_quiz_external extends external_api {
];
$params = self::validate_parameters(self::get_attempt_review_parameters(), $params);
list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
[$attemptobj, $displayoptions] = self::validate_attempt_review($params);
if ($params['page'] !== -1) {
$page = $attemptobj->force_page_number_into_range($params['page']);
@@ -1502,6 +1520,22 @@ class mod_quiz_external extends external_api {
];
}
if (!has_capability('mod/quiz:viewreports', $attemptobj->get_context()) &&
($displayoptions->marks < question_display_options::MARK_AND_MAX ||
$attemptobj->get_attempt()->state != quiz_attempt::FINISHED)) {
// Blank the mark if the teacher does not allow it.
$result['attempt']->sumgrades = null;
} else {
$result['attempt']->gradeitemmarks = [];
foreach ($attemptobj->get_grade_item_totals() as $gradeitem) {
$result['attempt']->gradeitemmarks[] = [
'name' => \core_external\util::format_string($gradeitem->name, $attemptobj->get_context()),
'grade' => $gradeitem->grade,
'maxgrade' => $gradeitem->maxgrade,
];
}
}
$result['grade'] = $grade;
$result['warnings'] = $warnings;
return $result;
+97 -12
View File
@@ -84,6 +84,7 @@ class testable_mod_quiz_external extends mod_quiz_external {
* @copyright 2016 Juan Leyva <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.1
* @covers \mod_quiz_external
*/
class external_test extends externallib_advanced_testcase {
@@ -423,13 +424,10 @@ class external_test extends externallib_advanced_testcase {
}
/**
* Test get_user_attempts
*/
public function test_get_user_attempts() {
public function test_get_user_attempts(): void {
// Create a quiz with one attempt finished.
list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true);
[$quiz, $context, $quizobj, $attempt, $attemptobj] = $this->create_quiz_with_questions(true, true);
$this->setUser($this->student);
$result = mod_quiz_external::get_user_attempts($quiz->id);
@@ -507,6 +505,41 @@ class external_test extends externallib_advanced_testcase {
}
}
public function test_get_user_attempts_with_extra_grades(): void {
global $DB;
// Create a quiz with one attempt finished.
[$quiz, , , $attempt, $attemptobj] = $this->create_quiz_with_questions(true, true);
// Add some extra grade items.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$listeninggrade = $quizgenerator->create_grade_item(['quizid' => $attemptobj->get_quizid(), 'name' => 'Listening']);
$readinggrade = $quizgenerator->create_grade_item(['quizid' => $attemptobj->get_quizid(), 'name' => 'Reading']);
$structure = $attemptobj->get_quizobj()->get_structure();
$structure->update_slot_grade_item($structure->get_slot_by_number(1), $listeninggrade->id);
$structure->update_slot_grade_item($structure->get_slot_by_number(2), $readinggrade->id);
$this->setUser($this->student);
$result = mod_quiz_external::get_user_attempts($quiz->id);
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
$this->assertCount(1, $result['attempts']);
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
// Verify additional grades.
$this->assertEquals(['name' => 'Listening', 'grade' => 1, 'maxgrade' => 1], $result['attempts'][0]['gradeitemmarks'][0]);
$this->assertEquals(['name' => 'Reading', 'grade' => 0, 'maxgrade' => 1], $result['attempts'][0]['gradeitemmarks'][1]);
// Now change the review options, so marks are not displayed, and check the result.
$DB->set_field('quiz', 'reviewmarks', 0, ['id' => $quiz->id]);
$result = mod_quiz_external::get_user_attempts($quiz->id);
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
$this->assertCount(1, $result['attempts']);
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
$this->assertArrayNotHasKey('gradeitemmarks', $result['attempts'][0]);
}
/**
* Test get_user_attempts with marks hidden
*/
@@ -1664,15 +1697,14 @@ class external_test extends externallib_advanced_testcase {
}
}
/**
* Test get_attempt_review
*/
public function test_get_attempt_review() {
public function test_get_attempt_review(): void {
global $DB;
// Create a new quiz with two questions and one attempt finished.
list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, true);
[$quiz, , , $attempt] = $this->create_quiz_with_questions(true, true);
// Add feedback to the quiz.
$feedback = new \stdClass();
@@ -1703,10 +1735,55 @@ class external_test extends externallib_advanced_testcase {
$this->assertEquals('gaveup', $result['questions'][1]['state']);
$this->assertEquals(2, $result['questions'][1]['slot']);
// Only first page.
$result = mod_quiz_external::get_attempt_review($attempt->id, 0);
$result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
$this->assertEquals(50, $result['grade']);
$this->assertEquals(1, $result['attempt']['attempt']);
$this->assertEquals('finished', $result['attempt']['state']);
$this->assertEquals(1, $result['attempt']['sumgrades']);
$this->assertCount(1, $result['questions']);
$this->assertEquals('gradedright', $result['questions'][0]['state']);
$this->assertEquals(1, $result['questions'][0]['slot']);
$this->assertCount(1, $result['additionaldata']);
$this->assertEquals('feedback', $result['additionaldata'][0]['id']);
$this->assertEquals('Feedback', $result['additionaldata'][0]['title']);
$this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']);
}
/**
* Test get_attempt_review
*/
public function test_get_attempt_review_with_extra_grades(): void {
global $DB;
// Create a new quiz with two questions and one attempt finished.
$this->setUser($this->student);
[, , , $attempt, $attemptobj] = $this->create_quiz_with_questions(true, true);
// Add some extra grade items.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$listeninggrade = $quizgenerator->create_grade_item(['quizid' => $attemptobj->get_quizid(), 'name' => 'Listening']);
$readinggrade = $quizgenerator->create_grade_item(['quizid' => $attemptobj->get_quizid(), 'name' => 'Reading']);
$structure = $attemptobj->get_quizobj()->get_structure();
$structure->update_slot_grade_item($structure->get_slot_by_number(1), $listeninggrade->id);
$structure->update_slot_grade_item($structure->get_slot_by_number(2), $readinggrade->id);
$result = mod_quiz_external::get_attempt_review($attempt->id);
$result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
// Two questions, one completed and correct, the other gave up.
$this->assertEquals(50, $result['grade']);
$this->assertEquals(1, $result['attempt']['attempt']);
$this->assertEquals('finished', $result['attempt']['state']);
$this->assertEquals(1, $result['attempt']['sumgrades']);
$this->assertCount(2, $result['questions']);
$this->assertEquals('gradedright', $result['questions'][0]['state']);
$this->assertEquals(1, $result['questions'][0]['slot']);
$this->assertEquals('gaveup', $result['questions'][1]['state']);
$this->assertEquals(2, $result['questions'][1]['slot']);
// Only first page.
$result = mod_quiz_external::get_attempt_review($attempt->id, 0);
@@ -1720,11 +1797,19 @@ class external_test extends externallib_advanced_testcase {
$this->assertEquals('gradedright', $result['questions'][0]['state']);
$this->assertEquals(1, $result['questions'][0]['slot']);
$this->assertCount(1, $result['additionaldata']);
$this->assertEquals('feedback', $result['additionaldata'][0]['id']);
$this->assertEquals('Feedback', $result['additionaldata'][0]['title']);
$this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']);
// Verify additional grades.
$this->assertEquals(['name' => 'Listening', 'grade' => 1, 'maxgrade' => 1], $result['attempt']['gradeitemmarks'][0]);
$this->assertEquals(['name' => 'Reading', 'grade' => 0, 'maxgrade' => 1], $result['attempt']['gradeitemmarks'][1]);
// Now change the review options, so marks are not displayed, and check the result.
$DB->set_field('quiz', 'reviewmarks', 0, ['id' => $attemptobj->get_quizid()]);
$result = mod_quiz_external::get_attempt_review($attempt->id, 0);
$result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
$this->assertEquals(1, $result['attempt']['attempt']);
$this->assertEquals('finished', $result['attempt']['state']);
$this->assertNull($result['attempt']['sumgrades']);
$this->assertArrayNotHasKey('gradeitemmarks', $result['attempt']);
}
/**