diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index 240acdf5116..79be9ea2f4f 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -1701,4 +1701,111 @@ class mod_quiz_external extends external_api { ); } + /** + * Describes the parameters for get_attempt_access_information. + * + * @return external_external_function_parameters + * @since Moodle 3.1 + */ + public static function get_attempt_access_information_parameters() { + return new external_function_parameters ( + array( + 'quizid' => new external_value(PARAM_INT, 'quiz instance id'), + 'attemptid' => new external_value(PARAM_INT, 'attempt id, 0 for the user last attempt if exists', VALUE_DEFAULT, 0), + ) + ); + } + + /** + * Return access information for a given attempt in a quiz. + * + * @param int $quizid quiz instance id + * @param int $attemptid attempt id, 0 for the user last attempt if exists + * @return array of warnings and the access information + * @since Moodle 3.1 + * @throws moodle_quiz_exception + */ + public static function get_attempt_access_information($quizid, $attemptid = 0) { + global $DB, $USER; + + $warnings = array(); + + $params = array( + 'quizid' => $quizid, + 'attemptid' => $attemptid, + ); + $params = self::validate_parameters(self::get_attempt_access_information_parameters(), $params); + + list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']); + + $attempttocheck = 0; + if (!empty($params['attemptid'])) { + $attemptobj = quiz_attempt::create($params['attemptid']); + if ($attemptobj->get_userid() != $USER->id) { + throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt'); + } + $attempttocheck = $attemptobj->get_attempt(); + } + + // Access manager now. + $quizobj = quiz::create($cm->instance, $USER->id); + $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false); + $timenow = time(); + $accessmanager = new quiz_access_manager($quizobj, $timenow, $ignoretimelimits); + + $attempts = quiz_get_user_attempts($quiz->id, $USER->id, 'finished', true); + $lastfinishedattempt = end($attempts); + if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) { + $attempts[] = $unfinishedattempt; + + // Check if the attempt is now overdue. In that case the state will change. + $quizobj->create_attempt_object($unfinishedattempt)->handle_if_time_expired(time(), false); + + if ($unfinishedattempt->state != quiz_attempt::IN_PROGRESS and $unfinishedattempt->state != quiz_attempt::OVERDUE) { + $lastfinishedattempt = $unfinishedattempt; + } + } + $numattempts = count($attempts); + + if (!$attempttocheck) { + $attempttocheck = $unfinishedattempt ? $unfinishedattempt : $lastfinishedattempt; + } + + $result = array(); + $result['isfinished'] = $accessmanager->is_finished($numattempts, $lastfinishedattempt); + $result['preventnewattemptreasons'] = $accessmanager->prevent_new_attempt($numattempts, $lastfinishedattempt); + + if ($attempttocheck) { + $endtime = $accessmanager->get_end_time($attempttocheck); + $result['endtime'] = ($endtime === false) ? 0 : $endtime; + $attemptid = $unfinishedattempt ? $unfinishedattempt->id : null; + $result['ispreflightcheckrequired'] = $accessmanager->is_preflight_check_required($attemptid); + } + + $result['warnings'] = $warnings; + return $result; + } + + /** + * Describes the get_attempt_access_information return value. + * + * @return external_single_structure + * @since Moodle 3.1 + */ + public static function get_attempt_access_information_returns() { + return new external_single_structure( + array( + 'endtime' => new external_value(PARAM_INT, 'When the attempt must be submitted (determined by rules).', + VALUE_OPTIONAL), + 'isfinished' => new external_value(PARAM_BOOL, 'Whether there is no way the user will ever be allowed to attempt.'), + 'ispreflightcheckrequired' => new external_value(PARAM_BOOL, 'whether a check is required before the user + starts/continues his attempt.', VALUE_OPTIONAL), + 'preventnewattemptreasons' => new external_multiple_structure( + new external_value(PARAM_TEXT, 'access restriction description'), + 'list of reasons'), + 'warnings' => new external_warnings(), + ) + ); + } + } diff --git a/mod/quiz/db/services.php b/mod/quiz/db/services.php index f9aea418478..c2c062d2182 100644 --- a/mod/quiz/db/services.php +++ b/mod/quiz/db/services.php @@ -173,4 +173,13 @@ $functions = array( 'capabilities' => 'mod/quiz:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), + + 'mod_quiz_get_attempt_access_information' => array( + 'classname' => 'mod_quiz_external', + 'methodname' => 'get_attempt_access_information', + 'description' => 'Return access information for a given attempt in a quiz.', + 'type' => 'read', + 'capabilities' => 'mod/quiz:view', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) + ), ); diff --git a/mod/quiz/tests/external_test.php b/mod/quiz/tests/external_test.php index 73a1270e611..6a1245afd3f 100644 --- a/mod/quiz/tests/external_test.php +++ b/mod/quiz/tests/external_test.php @@ -1461,4 +1461,89 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { $this->assertCount(1, $result['preventaccessreasons']); } + + /** + * Test get_attempt_access_information + */ + public function test_get_attempt_access_information() { + global $DB; + + // Create a new quiz with attempts. + $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); + $data = array('course' => $this->course->id, + 'sumgrades' => 2); + $quiz = $quizgenerator->create_instance($data); + + // Create some 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); + + $question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); + quiz_add_quiz_question($question->id, $quiz); + + // Add new question types in the category (for the random one). + $question = $questiongenerator->create_question('truefalse', null, array('category' => $cat->id)); + $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id)); + + $question = $questiongenerator->create_question('random', 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(); + + $this->setUser($this->student); + + // Default restrictions (none). + $result = mod_quiz_external::get_attempt_access_information($quiz->id); + $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result); + + $expected = array( + 'isfinished' => false, + 'preventnewattemptreasons' => [], + 'warnings' => [] + ); + + $this->assertEquals($expected, $result); + + // Limited attempts. + $quiz->attempts = 1; + $DB->update_record('quiz', $quiz); + + // Now, do one 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); + + // Can we start a new attempt? We shall not! + $result = mod_quiz_external::get_attempt_access_information($quiz->id, $attempt->id); + $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result); + + // Now new attemps allowed. + $this->assertCount(1, $result['preventnewattemptreasons']); + $this->assertFalse($result['ispreflightcheckrequired']); + $this->assertEquals(get_string('nomoreattempts', 'quiz'), $result['preventnewattemptreasons'][0]); + + } }