From 3589b6591a5af05cff9af88350bc94a960a6752e Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Thu, 21 Jan 2016 22:39:39 +0100 Subject: [PATCH] MDL-52868 mod_quiz: New Web Service mod_quiz_get_attempt_review --- mod/quiz/classes/external.php | 132 +++++++++++++++++++++++++++++++ mod/quiz/db/services.php | 9 +++ mod/quiz/tests/external_test.php | 128 +++++++++++++++++++++++++++++- mod/quiz/version.php | 2 +- 4 files changed, 269 insertions(+), 2 deletions(-) diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index 9cb983ef154..21a23e726ff 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -1254,4 +1254,136 @@ class mod_quiz_external extends external_api { ); } + /** + * Validate an attempt finished for review. The attempt would be reviewed by a user or a teacher. + * + * @param array $params Array of parameters including the attemptid + * @return array containing the attempt object and display options + * @since Moodle 3.1 + * @throws moodle_exception + * @throws moodle_quiz_exception + */ + protected static function validate_attempt_review($params) { + + $attemptobj = quiz_attempt::create($params['attemptid']); + $attemptobj->check_review_capability(); + + $displayoptions = $attemptobj->get_display_options(true); + if ($attemptobj->is_own_attempt()) { + if (!$attemptobj->is_finished()) { + throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptclosed'); + } else if (!$displayoptions->attempt) { + throw new moodle_exception($attemptobj->cannot_review_message()); + } + } else if (!$attemptobj->is_review_allowed()) { + throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreviewattempt'); + } + return array($attemptobj, $displayoptions); + } + + /** + * Describes the parameters for get_attempt_review. + * + * @return external_external_function_parameters + * @since Moodle 3.1 + */ + public static function get_attempt_review_parameters() { + return new external_function_parameters ( + array( + 'attemptid' => new external_value(PARAM_INT, 'attempt id'), + 'page' => new external_value(PARAM_INT, 'page number, empty for all the questions in all the pages', + VALUE_DEFAULT, -1), + ) + ); + } + + /** + * Returns review information for the given finished attempt, can be used by users or teachers. + * + * @param int $attemptid attempt id + * @param int $page page number, empty for all the questions in all the pages + * @return array of warnings and the attempt data, feedback and questions + * @since Moodle 3.1 + * @throws moodle_exception + * @throws moodle_quiz_exception + */ + public static function get_attempt_review($attemptid, $page = -1) { + global $PAGE; + + $warnings = array(); + + $params = array( + 'attemptid' => $attemptid, + 'page' => $page, + ); + $params = self::validate_parameters(self::get_attempt_review_parameters(), $params); + + list($attemptobj, $displayoptions) = self::validate_attempt_review($params); + + if ($params['page'] !== -1) { + $page = $attemptobj->force_page_number_into_range($params['page']); + } else { + $page = 'all'; + } + + // Prepare the output. + $result = array(); + $result['attempt'] = $attemptobj->get_attempt(); + $result['questions'] = self::get_attempt_questions_data($attemptobj, true, $page, true); + + $result['additionaldata'] = array(); + // Summary data (from behaviours). + $summarydata = $attemptobj->get_additional_summary_data($displayoptions); + foreach ($summarydata as $key => $data) { + // This text does not need formatting (no need for external_format_[string|text]). + $result['additionaldata'][] = array( + 'id' => $key, + 'title' => $data['title'], $attemptobj->get_quizobj()->get_context()->id, + 'content' => $data['content'], + ); + } + + // Feedback if there is any, and the user is allowed to see it now. + $grade = quiz_rescale_grade($attemptobj->get_attempt()->sumgrades, $attemptobj->get_quiz(), false); + + $feedback = $attemptobj->get_overall_feedback($grade); + if ($displayoptions->overallfeedback && $feedback) { + $result['additionaldata'][] = array( + 'id' => 'feedback', + 'title' => get_string('feedback', 'quiz'), + 'content' => $feedback, + ); + } + + $result['grade'] = $grade; + $result['warnings'] = $warnings; + return $result; + } + + /** + * Describes the get_attempt_review return value. + * + * @return external_single_structure + * @since Moodle 3.1 + */ + public static function get_attempt_review_returns() { + return new external_single_structure( + array( + 'grade' => new external_value(PARAM_RAW, 'grade for the quiz (or empty or "notyetgraded")'), + 'attempt' => self::attempt_structure(), + 'additionaldata' => new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_ALPHANUMEXT, 'id of the data'), + 'title' => new external_value(PARAM_TEXT, 'data title'), + 'content' => new external_value(PARAM_RAW, 'data content'), + ) + ) + ), + 'questions' => new external_multiple_structure(self::question_structure()), + 'warnings' => new external_warnings(), + ) + ); + } + } diff --git a/mod/quiz/db/services.php b/mod/quiz/db/services.php index 0b68d9b9122..0abd1dcee13 100644 --- a/mod/quiz/db/services.php +++ b/mod/quiz/db/services.php @@ -119,4 +119,13 @@ $functions = array( 'capabilities' => 'mod/quiz:attempt', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), + + 'mod_quiz_get_attempt_review' => array( + 'classname' => 'mod_quiz_external', + 'methodname' => 'get_attempt_review', + 'description' => 'Returns review information for the given finished attempt, can be used by users or teachers.', + 'type' => 'read', + 'capabilities' => 'mod/quiz:reviewmyattempts', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) + ), ); diff --git a/mod/quiz/tests/external_test.php b/mod/quiz/tests/external_test.php index 5b02191f3f7..df10871c895 100644 --- a/mod/quiz/tests/external_test.php +++ b/mod/quiz/tests/external_test.php @@ -51,6 +51,16 @@ class testable_mod_quiz_external extends mod_quiz_external { public static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) { return parent::validate_attempt($params, $checkaccessrules, $failifoverdue); } + + /** + * Public accessor. + * + * @param array $params Array of parameters including the attemptid + * @return array containing the attempt object and display options + */ + public static function validate_attempt_review($params) { + return parent::validate_attempt_review($params); + } } /** @@ -101,7 +111,7 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { // Create a new quiz with attempts. $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); $data = array('course' => $this->course->id, - 'sumgrades' => 1); + 'sumgrades' => 2); $quiz = $quizgenerator->create_instance($data); $context = context_module::instance($quiz->cmid); @@ -1121,4 +1131,120 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { } + /** + * Test validate_attempt_review + */ + public function test_validate_attempt_review() { + global $DB; + + // Create a new quiz with one attempt started. + list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true); + + $this->setUser($this->student); + + // Invalid attempt, invalid id. + try { + $params = array('attemptid' => -1); + testable_mod_quiz_external::validate_attempt_review($params); + $this->fail('Exception expected due invalid id.'); + } catch (dml_missing_record_exception $e) { + $this->assertEquals('invalidrecord', $e->errorcode); + } + + // Invalid attempt, not closed. + try { + $params = array('attemptid' => $attempt->id); + testable_mod_quiz_external::validate_attempt_review($params); + $this->fail('Exception expected due not closed attempt.'); + } catch (moodle_quiz_exception $e) { + $this->assertEquals('attemptclosed', $e->errorcode); + } + + // Test ok case (finished attempt). + list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true); + + $params = array('attemptid' => $attempt->id); + testable_mod_quiz_external::validate_attempt_review($params); + + // Teacher should be able to view the review of one student's attempt. + $this->setUser($this->teacher); + testable_mod_quiz_external::validate_attempt_review($params); + + // We should not see other students attempts. + $anotherstudent = self::getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual'); + + $this->setUser($anotherstudent); + try { + $params = array('attemptid' => $attempt->id); + testable_mod_quiz_external::validate_attempt_review($params); + $this->fail('Exception expected due missing permissions.'); + } catch (moodle_quiz_exception $e) { + $this->assertEquals('noreviewattempt', $e->errorcode); + } + } + + + /** + * Test get_attempt_review + */ + public function test_get_attempt_review() { + 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); + + // Add feedback to the quiz. + $feedback = new stdClass(); + $feedback->quizid = $quiz->id; + $feedback->feedbacktext = 'Feedback text 1'; + $feedback->feedbacktextformat = 1; + $feedback->mingrade = 49; + $feedback->maxgrade = 100; + $feedback->id = $DB->insert_record('quiz_feedback', $feedback); + + $feedback->feedbacktext = 'Feedback text 2'; + $feedback->feedbacktextformat = 1; + $feedback->mingrade = 30; + $feedback->maxgrade = 48; + $feedback->id = $DB->insert_record('quiz_feedback', $feedback); + + $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']); + + $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']); + + // 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']); + + } + } diff --git a/mod/quiz/version.php b/mod/quiz/version.php index fc8f6adaac1..8bcb06f2f1b 100644 --- a/mod/quiz/version.php +++ b/mod/quiz/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2015111611; +$plugin->version = 2015111612; $plugin->requires = 2015111000; $plugin->component = 'mod_quiz'; $plugin->cron = 60;