From c161ecff889bc3e431bf79ad7d45115c9c3e558c Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Thu, 14 Jan 2016 13:11:02 +0100 Subject: [PATCH] MDL-52670 mod_quiz: New Web Service mod_quiz_get_user_attempts --- lib/db/services.php | 1 + mod/quiz/classes/external.php | 116 ++++++++++++++++++++++++++++++ mod/quiz/db/services.php | 8 +++ mod/quiz/tests/external_test.php | 118 +++++++++++++++++++++++++++++++ mod/quiz/version.php | 2 +- 5 files changed, 244 insertions(+), 1 deletion(-) diff --git a/lib/db/services.php b/lib/db/services.php index 7d65f862bd6..a7de93cc746 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1311,6 +1311,7 @@ $services = array( 'mod_imscp_view_imscp', 'mod_imscp_get_imscps_by_courses', 'mod_quiz_get_quizzes_by_courses', + 'mod_quiz_get_user_attempts', 'mod_quiz_view_quiz', 'mod_glossary_get_glossaries_by_courses', 'mod_wiki_get_wikis_by_courses', diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index 130fc3d5931..785621946cc 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -331,4 +331,120 @@ class mod_quiz_external extends external_api { ); } + /** + * Describes the parameters for get_user_attempts. + * + * @return external_external_function_parameters + * @since Moodle 3.1 + */ + public static function get_user_attempts_parameters() { + return new external_function_parameters ( + array( + 'quizid' => new external_value(PARAM_INT, 'quiz instance id'), + 'userid' => new external_value(PARAM_INT, 'user id, empty for current user', VALUE_DEFAULT, 0), + 'status' => new external_value(PARAM_ALPHA, 'quiz status: all, finished or unfinished', VALUE_DEFAULT, 'finished'), + 'includepreviews' => new external_value(PARAM_BOOL, 'whether to include previews or not', VALUE_DEFAULT, false), + + ) + ); + } + + /** + * Return a list of attempts for the given quiz and user. + * + * @param int $quizid quiz instance id + * @param int $userid user id + * @param string $status quiz status: all, finished or unfinished + * @param bool $includepreviews whether to include previews or not + * @return array of warnings and the list of attempts + * @since Moodle 3.1 + * @throws invalid_parameter_exception + */ + public static function get_user_attempts($quizid, $userid = 0, $status = 'finished', $includepreviews = false) { + global $DB, $USER; + + $warnings = array(); + + $params = array( + 'quizid' => $quizid, + 'userid' => $userid, + 'status' => $status, + 'includepreviews' => $includepreviews, + ); + $params = self::validate_parameters(self::get_user_attempts_parameters(), $params); + + // Request and permission validation. + $quiz = $DB->get_record('quiz', array('id' => $params['quizid']), '*', MUST_EXIST); + list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz'); + + $context = context_module::instance($cm->id); + self::validate_context($context); + + if (!in_array($params['status'], array('all', 'finished', 'unfinished'))) { + throw new invalid_parameter_exception('Invalid status value'); + } + + // Default value for userid. + if (empty($params['userid'])) { + $params['userid'] = $USER->id; + } + + $user = core_user::get_user($params['userid'], '*', MUST_EXIST); + core_user::require_active_user($user); + + // Extra checks so only users with permissions can view other users attempts. + if ($USER->id != $user->id) { + require_capability('mod/quiz:viewreports', $context); + } + + $attempts = quiz_get_user_attempts($quiz->id, $user->id, $params['status'], $params['includepreviews']); + + $result = array(); + $result['attempts'] = $attempts; + $result['warnings'] = $warnings; + return $result; + } + + /** + * Describes the get_user_attempts return value. + * + * @return external_single_structure + * @since Moodle 3.1 + */ + public static function get_user_attempts_returns() { + return new external_single_structure( + array( + 'attempts' => new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'Attempt id.', VALUE_OPTIONAL), + 'quiz' => new external_value(PARAM_INT, 'Foreign key reference to the quiz that was attempted.', + VALUE_OPTIONAL), + 'userid' => new external_value(PARAM_INT, 'Foreign key reference to the user whose attempt this is.', + VALUE_OPTIONAL), + 'attempt' => new external_value(PARAM_INT, 'Sequentially numbers this students attempts at this quiz.', + VALUE_OPTIONAL), + 'uniqueid' => new external_value(PARAM_INT, 'Foreign key reference to the question_usage that holds the + details of the the question_attempts that make up this quiz + attempt.', VALUE_OPTIONAL), + 'layout' => new external_value(PARAM_RAW, 'Attempt layout.', VALUE_OPTIONAL), + 'currentpage' => new external_value(PARAM_INT, 'Attempt current page.', VALUE_OPTIONAL), + 'preview' => new external_value(PARAM_INT, 'Whether is a preview attempt or not.', VALUE_OPTIONAL), + 'state' => new external_value(PARAM_ALPHA, 'The current state of the attempts. \'inprogress\', + \'overdue\', \'finished\' or \'abandoned\'.', VALUE_OPTIONAL), + 'timestart' => new external_value(PARAM_INT, 'Time when the attempt was started.', VALUE_OPTIONAL), + 'timefinish' => new external_value(PARAM_INT, 'Time when the attempt was submitted. + 0 if the attempt has not been submitted yet.', VALUE_OPTIONAL), + 'timemodified' => new external_value(PARAM_INT, 'Last modified time.', VALUE_OPTIONAL), + '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), + ) + ) + ), + 'warnings' => new external_warnings(), + ) + ); + } + } diff --git a/mod/quiz/db/services.php b/mod/quiz/db/services.php index cf5adbd3aa5..792445dd5c4 100644 --- a/mod/quiz/db/services.php +++ b/mod/quiz/db/services.php @@ -44,4 +44,12 @@ $functions = array( 'type' => 'write', 'capabilities' => 'mod/quiz:view' ), + + 'mod_quiz_get_user_attempts' => array( + 'classname' => 'mod_quiz_external', + 'methodname' => 'get_user_attempts', + 'description' => 'Return a list of attempts for the given quiz and user.', + 'type' => 'read', + 'capabilities' => 'mod/quiz:view' + ), ); diff --git a/mod/quiz/tests/external_test.php b/mod/quiz/tests/external_test.php index 5cd2645b4d0..f2867469d9a 100644 --- a/mod/quiz/tests/external_test.php +++ b/mod/quiz/tests/external_test.php @@ -268,4 +268,122 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { } + /** + * Test get_user_attempts + */ + public function test_get_user_attempts() { + + // Create a new quiz with attempts. + $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); + $data = array('course' => $this->course->id, + 'sumgrades' => 1); + $quiz = $quizgenerator->create_instance($data); + + // Create a couple of questions. + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + + $cat = $questiongenerator->create_question_category(); + $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); + quiz_add_quiz_question($question->id, $quiz); + + $quizobj = quiz::create($quiz->id, $this->student->id); + + // Set grade to pass. + $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', + 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); + $item->gradepass = 80; + $item->update(); + + // Start the passing attempt. + $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); + $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); + + $timenow = time(); + $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id); + quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); + quiz_attempt_save_started($quizobj, $quba, $attempt); + + // Process some responses from the student. + $attemptobj = quiz_attempt::create($attempt->id); + $tosubmit = array(1 => array('answer' => '3.14')); + $attemptobj->process_submitted_actions($timenow, false, $tosubmit); + + // Finish the attempt. + $attemptobj = quiz_attempt::create($attempt->id); + $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); + $attemptobj->process_finish($timenow, false); + + $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']); + $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']); + $this->assertEquals($this->student->id, $result['attempts'][0]['userid']); + $this->assertEquals(1, $result['attempts'][0]['attempt']); + + // Test filters. Only finished. + $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'finished', false); + $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']); + + // Test filters. All attempts. + $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false); + $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']); + + // Test filters. Unfinished. + $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false); + $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); + + $this->assertCount(0, $result['attempts']); + + // Start a new attempt, but not finish it. + $timenow = time(); + $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id); + $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); + $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); + + quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); + quiz_attempt_save_started($quizobj, $quba, $attempt); + + // Test filters. All attempts. + $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false); + $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); + + $this->assertCount(2, $result['attempts']); + + // Test filters. Unfinished. + $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false); + $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); + + $this->assertCount(1, $result['attempts']); + + // Test manager can see user attempts. + $this->setUser($this->teacher); + $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id); + $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); + + $this->assertCount(1, $result['attempts']); + $this->assertEquals($this->student->id, $result['attempts'][0]['userid']); + + $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'all'); + $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); + + $this->assertCount(2, $result['attempts']); + $this->assertEquals($this->student->id, $result['attempts'][0]['userid']); + + // Invalid parameters. + try { + mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'INVALID_PARAMETER'); + $this->fail('Exception expected due to missing capability.'); + } catch (invalid_parameter_exception $e) { + $this->assertEquals('invalidparameter', $e->errorcode); + } + } } diff --git a/mod/quiz/version.php b/mod/quiz/version.php index 70a0e4b90c2..29f52724f8a 100644 --- a/mod/quiz/version.php +++ b/mod/quiz/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2015111602; +$plugin->version = 2015111603; $plugin->requires = 2015111000; $plugin->component = 'mod_quiz'; $plugin->cron = 60;