From 0057c2ced6dfe087c5dd841623b2c678ad88befa Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Mon, 8 Feb 2016 15:39:41 +0100 Subject: [PATCH 1/2] MDL-52788 mod_quiz: Move start attempt code to new API functions --- mod/quiz/accessmanager.php | 20 ++++++ mod/quiz/accessmanager_form.php | 9 +-- mod/quiz/locallib.php | 121 ++++++++++++++++++++++++++++++++ mod/quiz/startattempt.php | 85 ++-------------------- 4 files changed, 148 insertions(+), 87 deletions(-) diff --git a/mod/quiz/accessmanager.php b/mod/quiz/accessmanager.php index 81208fb6e2e..c225a6b75e7 100644 --- a/mod/quiz/accessmanager.php +++ b/mod/quiz/accessmanager.php @@ -539,4 +539,24 @@ class quiz_access_manager { $this->attempt_must_be_in_popup(), $this->get_popup_options()); } } + + /** + * Run the preflight checks using the given data in all the rules supporting them. + * + * @param array $data passed data for validation + * @param array $files un-used, Moodle seems to not support it anymore + * @param int|null $attemptid the id of the current attempt, if there is one, + * otherwise null. + * @return array of errors, empty array means no erros + * @since Moodle 3.1 + */ + public function validate_preflight_check($data, $files, $attemptid) { + $errors = array(); + foreach ($this->rules as $rule) { + if ($rule->is_preflight_check_required($attemptid)) { + $errors = $rule->validate_preflight_check($data, $files, $errors, $attemptid); + } + } + return $errors; + } } diff --git a/mod/quiz/accessmanager_form.php b/mod/quiz/accessmanager_form.php index 41f29afefac..dd3b48ffb1e 100644 --- a/mod/quiz/accessmanager_form.php +++ b/mod/quiz/accessmanager_form.php @@ -62,12 +62,9 @@ class mod_quiz_preflight_check_form extends moodleform { public function validation($data, $files) { $errors = parent::validation($data, $files); - foreach ($this->_customdata['rules'] as $rule) { - if ($rule->is_preflight_check_required($this->_customdata['attemptid'])) { - $errors = $rule->validate_preflight_check($data, $files, $errors, - $this->_customdata['attemptid']); - } - } + $timenow = time(); + $accessmanager = $this->_customdata['quizobj']->get_access_manager($timenow); + $errors = array_merge($errors, $accessmanager->validate_preflight_check($data, $files, $this->_customdata['attemptid'])); return $errors; } diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index 59add9fe9d1..24db2b26241 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -2151,5 +2151,126 @@ function quiz_view($quiz, $course, $cm, $context) { // Completion. $completion = new completion_info($course); $completion->set_module_viewed($cm); +} +/** + * Validate permissions for creating a new attempt and start a new preview attempt if required. + * + * @param quiz $quizobj quiz object + * @param quiz_access_manager $accessmanager quiz access manager + * @param bool $forcenew whether was required to start a new preview attempt + * @param int $page page to jump to in the attempt + * @param bool $redirect whether to redirect or throw exceptions (for web or ws usage) + * @return array an array containing the attempt information, access error messages and the page to jump to in the attempt + * @throws moodle_quiz_exception + * @since Moodle 3.1 + */ +function quiz_validate_new_attempt(quiz $quizobj, quiz_access_manager $accessmanager, $forcenew, $page, $redirect) { + global $DB, $USER; + $timenow = time(); + + if ($quizobj->is_preview_user() && $forcenew) { + $accessmanager->current_attempt_finished(); + } + + // Check capabilities. + if (!$quizobj->is_preview_user()) { + $quizobj->require_capability('mod/quiz:attempt'); + } + + // Check to see if a new preview was requested. + if ($quizobj->is_preview_user() && $forcenew) { + // To force the creation of a new preview, we mark the current attempt (if any) + // as finished. It will then automatically be deleted below. + $DB->set_field('quiz_attempts', 'state', quiz_attempt::FINISHED, + array('quiz' => $quizobj->get_quizid(), 'userid' => $USER->id)); + } + + // Look for an existing attempt. + $attempts = quiz_get_user_attempts($quizobj->get_quizid(), $USER->id, 'all', true); + $lastattempt = end($attempts); + + $attemptnumber = null; + // If an in-progress attempt exists, check password then redirect to it. + if ($lastattempt && ($lastattempt->state == quiz_attempt::IN_PROGRESS || + $lastattempt->state == quiz_attempt::OVERDUE)) { + $currentattemptid = $lastattempt->id; + $messages = $accessmanager->prevent_access(); + + // If the attempt is now overdue, deal with that. + $quizobj->create_attempt_object($lastattempt)->handle_if_time_expired($timenow, true); + + // And, if the attempt is now no longer in progress, redirect to the appropriate place. + if ($lastattempt->state == quiz_attempt::ABANDONED || $lastattempt->state == quiz_attempt::FINISHED) { + if ($redirect) { + redirect($quizobj->review_url($lastattempt->id)); + } else { + throw new moodle_quiz_exception($quizobj, 'attemptalreadyclosed'); + } + } + + // If the page number was not explicitly in the URL, go to the current page. + if ($page == -1) { + $page = $lastattempt->currentpage; + } + + } else { + while ($lastattempt && $lastattempt->preview) { + $lastattempt = array_pop($attempts); + } + + // Get number for the next or unfinished attempt. + if ($lastattempt) { + $attemptnumber = $lastattempt->attempt + 1; + } else { + $lastattempt = false; + $attemptnumber = 1; + } + $currentattemptid = null; + + $messages = $accessmanager->prevent_access() + + $accessmanager->prevent_new_attempt(count($attempts), $lastattempt); + + if ($page == -1) { + $page = 0; + } + } + return array($currentattemptid, $attemptnumber, $lastattempt, $messages, $page); +} + +/** + * Prepare and start a new attempt deleting the previous preview attempts. + * + * @param quiz $quizobj quiz object + * @param int $attemptnumber the attempt number + * @param object $lastattempt last attempt object + * @return object the new attempt + * @since Moodle 3.1 + */ +function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $lastattempt) { + global $DB, $USER; + + // Delete any previous preview attempts belonging to this user. + quiz_delete_previews($quizobj->get_quiz(), $USER->id); + + $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); + $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); + + // Create the new attempt and initialize the question sessions + $timenow = time(); // Update time now, in case the server is running really slowly. + $attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, $timenow, $quizobj->is_preview_user()); + + if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) { + $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow); + } else { + $attempt = quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt); + } + + $transaction = $DB->start_delegated_transaction(); + + $attempt = quiz_attempt_save_started($quizobj, $quba, $attempt); + + $transaction->allow_commit(); + + return $attempt; } diff --git a/mod/quiz/startattempt.php b/mod/quiz/startattempt.php index 9b625ec7c0f..e94a71f2497 100644 --- a/mod/quiz/startattempt.php +++ b/mod/quiz/startattempt.php @@ -62,67 +62,10 @@ if (!$quizobj->has_questions()) { // Create an object to manage all the other (non-roles) access rules. $timenow = time(); $accessmanager = $quizobj->get_access_manager($timenow); -if ($quizobj->is_preview_user() && $forcenew) { - $accessmanager->current_attempt_finished(); -} -// Check capabilities. -if (!$quizobj->is_preview_user()) { - $quizobj->require_capability('mod/quiz:attempt'); -} - -// Check to see if a new preview was requested. -if ($quizobj->is_preview_user() && $forcenew) { - // To force the creation of a new preview, we mark the current attempt (if any) - // as finished. It will then automatically be deleted below. - $DB->set_field('quiz_attempts', 'state', quiz_attempt::FINISHED, - array('quiz' => $quizobj->get_quizid(), 'userid' => $USER->id)); -} - -// Look for an existing attempt. -$attempts = quiz_get_user_attempts($quizobj->get_quizid(), $USER->id, 'all', true); -$lastattempt = end($attempts); - -// If an in-progress attempt exists, check password then redirect to it. -if ($lastattempt && ($lastattempt->state == quiz_attempt::IN_PROGRESS || - $lastattempt->state == quiz_attempt::OVERDUE)) { - $currentattemptid = $lastattempt->id; - $messages = $accessmanager->prevent_access(); - - // If the attempt is now overdue, deal with that. - $quizobj->create_attempt_object($lastattempt)->handle_if_time_expired($timenow, true); - - // And, if the attempt is now no longer in progress, redirect to the appropriate place. - if ($lastattempt->state == quiz_attempt::ABANDONED || $lastattempt->state == quiz_attempt::FINISHED) { - redirect($quizobj->review_url($lastattempt->id)); - } - - // If the page number was not explicitly in the URL, go to the current page. - if ($page == -1) { - $page = $lastattempt->currentpage; - } - -} else { - while ($lastattempt && $lastattempt->preview) { - $lastattempt = array_pop($attempts); - } - - // Get number for the next or unfinished attempt. - if ($lastattempt) { - $attemptnumber = $lastattempt->attempt + 1; - } else { - $lastattempt = false; - $attemptnumber = 1; - } - $currentattemptid = null; - - $messages = $accessmanager->prevent_access() + - $accessmanager->prevent_new_attempt(count($attempts), $lastattempt); - - if ($page == -1) { - $page = 0; - } -} +// Validate permissions for creating a new attempt and start a new preview attempt if required. +list($currentattemptid, $attemptnumber, $lastattempt, $messages, $page) = + quiz_validate_new_attempt($quizobj, $accessmanager, $forcenew, $page, true); // Check access. if (!$quizobj->is_preview_user() && $messages) { @@ -165,27 +108,7 @@ if ($currentattemptid) { } } -// Delete any previous preview attempts belonging to this user. -quiz_delete_previews($quizobj->get_quiz(), $USER->id); - -$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); -$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); - -// Create the new attempt and initialize the question sessions -$timenow = time(); // Update time now, in case the server is running really slowly. -$attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, $timenow, $quizobj->is_preview_user()); - -if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) { - $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow); -} else { - $attempt = quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt); -} - -$transaction = $DB->start_delegated_transaction(); - -$attempt = quiz_attempt_save_started($quizobj, $quba, $attempt); - -$transaction->allow_commit(); +$attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt); // Redirect to the attempt page. redirect($quizobj->attempt_url($attempt->id, $page)); From b89544404e7d847b2c60250ef8a5deb0af2b5e68 Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Fri, 15 Jan 2016 14:43:08 +0100 Subject: [PATCH 2/2] MDL-52788 mod_quiz: New Web Service mod_quiz_start_attempt --- mod/quiz/classes/external.php | 194 ++++++++++++++++++++++++++----- mod/quiz/db/services.php | 9 ++ mod/quiz/tests/external_test.php | 117 +++++++++++++++++++ mod/quiz/version.php | 2 +- 4 files changed, 293 insertions(+), 29 deletions(-) diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index cddad330a1f..214b9b49da0 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -405,6 +405,40 @@ class mod_quiz_external extends external_api { return $result; } + /** + * Describes a single attempt structure. + * + * @return external_single_structure the attempt structure + */ + private static function attempt_structure() { + return 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), + ) + ); + } + /** * Describes the get_user_attempts return value. * @@ -414,34 +448,7 @@ class mod_quiz_external extends external_api { 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), - ) - ) - ), + 'attempts' => new external_multiple_structure(self::attempt_structure()), 'warnings' => new external_warnings(), ) ); @@ -636,4 +643,135 @@ class mod_quiz_external extends external_api { ); } + /** + * Describes the parameters for start_attempt. + * + * @return external_external_function_parameters + * @since Moodle 3.1 + */ + public static function start_attempt_parameters() { + return new external_function_parameters ( + array( + 'quizid' => new external_value(PARAM_INT, 'quiz instance id'), + 'preflightdata' => new external_multiple_structure( + new external_single_structure( + array( + 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'), + 'value' => new external_value(PARAM_RAW, 'data value'), + ) + ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array() + ), + 'forcenew' => new external_value(PARAM_BOOL, 'Whether to force a new attempt or not.', VALUE_DEFAULT, false), + + ) + ); + } + + /** + * Starts a new attempt at a quiz. + * + * @param int $quizid quiz instance id + * @param array $preflightdata preflight required data (like passwords) + * @param bool $forcenew Whether to force a new attempt or not. + * @return array of warnings and the attempt basic data + * @since Moodle 3.1 + * @throws moodle_quiz_exception + */ + public static function start_attempt($quizid, $preflightdata = array(), $forcenew = false) { + global $DB, $USER; + + $warnings = array(); + $attempt = array(); + + $params = array( + 'quizid' => $quizid, + 'preflightdata' => $preflightdata, + 'forcenew' => $forcenew, + ); + $params = self::validate_parameters(self::start_attempt_parameters(), $params); + $forcenew = $params['forcenew']; + + // 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); + + $quizobj = quiz::create($cm->instance, $USER->id); + + // Check questions. + if (!$quizobj->has_questions()) { + throw new moodle_quiz_exception($quizobj, 'noquestionsfound'); + } + + // Create an object to manage all the other (non-roles) access rules. + $timenow = time(); + $accessmanager = $quizobj->get_access_manager($timenow); + + // Validate permissions for creating a new attempt and start a new preview attempt if required. + list($currentattemptid, $attemptnumber, $lastattempt, $messages, $page) = + quiz_validate_new_attempt($quizobj, $accessmanager, $forcenew, -1, false); + + // Check access. + if (!$quizobj->is_preview_user() && $messages) { + // Create warnings with the exact messages. + foreach ($messages as $message) { + $warnings[] = array( + 'item' => 'quiz', + 'itemid' => $quiz->id, + 'warningcode' => '1', + 'message' => clean_text($message, PARAM_TEXT) + ); + } + } else { + if ($accessmanager->is_preflight_check_required($currentattemptid)) { + // Need to do some checks before allowing the user to continue. + + $provideddata = array(); + foreach ($params['preflightdata'] as $data) { + $provideddata[$data['name']] = $data['value']; + } + + $errors = $accessmanager->validate_preflight_check($provideddata, [], $currentattemptid); + + if (!empty($errors)) { + throw new moodle_quiz_exception($quizobj, array_shift($errors)); + } + + // Pre-flight check passed. + $accessmanager->notify_preflight_check_passed($currentattemptid); + } + + if ($currentattemptid) { + if ($lastattempt->state == quiz_attempt::OVERDUE) { + throw new moodle_quiz_exception($quizobj, 'stateoverdue'); + } else { + throw new moodle_quiz_exception($quizobj, 'attemptstillinprogress'); + } + } + $attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt); + } + + $result = array(); + $result['attempt'] = $attempt; + $result['warnings'] = $warnings; + return $result; + } + + /** + * Describes the start_attempt return value. + * + * @return external_single_structure + * @since Moodle 3.1 + */ + public static function start_attempt_returns() { + return new external_single_structure( + array( + 'attempt' => self::attempt_structure(), + 'warnings' => new external_warnings(), + ) + ); + } + } diff --git a/mod/quiz/db/services.php b/mod/quiz/db/services.php index ff73ee6cbaf..1367b271a62 100644 --- a/mod/quiz/db/services.php +++ b/mod/quiz/db/services.php @@ -73,4 +73,13 @@ $functions = array( 'capabilities' => 'mod/quiz:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), + + 'mod_quiz_start_attempt' => array( + 'classname' => 'mod_quiz_external', + 'methodname' => 'start_attempt', + 'description' => 'Starts a new attempt at a quiz.', + 'type' => 'write', + 'capabilities' => 'mod/quiz:attempt', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) + ), ); diff --git a/mod/quiz/tests/external_test.php b/mod/quiz/tests/external_test.php index 5b6653172ed..b3711459dc6 100644 --- a/mod/quiz/tests/external_test.php +++ b/mod/quiz/tests/external_test.php @@ -585,4 +585,121 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { } } + /** + * Test start_attempt + */ + public function test_start_attempt() { + global $DB; + + // 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); + $context = context_module::instance($quiz->cmid); + + try { + mod_quiz_external::start_attempt($quiz->id); + $this->fail('Exception expected due to missing questions.'); + } catch (moodle_quiz_exception $e) { + $this->assertEquals('noquestionsfound', $e->errorcode); + } + + // Create a question. + $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(); + + $this->setUser($this->student); + + // Try to open attempt in closed quiz. + $quiz->timeopen = time() - WEEKSECS; + $quiz->timeclose = time() - DAYSECS; + $DB->update_record('quiz', $quiz); + $result = mod_quiz_external::start_attempt($quiz->id); + $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result); + + $this->assertEquals([], $result['attempt']); + $this->assertCount(1, $result['warnings']); + + // Now with a password. + $quiz->timeopen = 0; + $quiz->timeclose = 0; + $quiz->password = 'abc'; + $DB->update_record('quiz', $quiz); + + try { + mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'bad'))); + $this->fail('Exception expected due to invalid passwod.'); + } catch (moodle_exception $e) { + $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode); + } + + // Now, try everything correct. + $result = mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc'))); + $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result); + + $this->assertEquals(1, $result['attempt']['attempt']); + $this->assertEquals($this->student->id, $result['attempt']['userid']); + $this->assertEquals($quiz->id, $result['attempt']['quiz']); + $this->assertCount(0, $result['warnings']); + $attemptid = $result['attempt']['id']; + + // We are good, try to start a new attempt now. + + try { + mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc'))); + $this->fail('Exception expected due to attempt not finished.'); + } catch (moodle_quiz_exception $e) { + $this->assertEquals('attemptstillinprogress', $e->errorcode); + } + + // Finish the started attempt. + + // Process some responses from the student. + $timenow = time(); + $attemptobj = quiz_attempt::create($attemptid); + $tosubmit = array(1 => array('answer' => '3.14')); + $attemptobj->process_submitted_actions($timenow, false, $tosubmit); + + // Finish the attempt. + $attemptobj = quiz_attempt::create($attemptid); + $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); + $attemptobj->process_finish($timenow, false); + + // We should be able to start a new attempt. + $result = mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc'))); + $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result); + + $this->assertEquals(2, $result['attempt']['attempt']); + $this->assertEquals($this->student->id, $result['attempt']['userid']); + $this->assertEquals($quiz->id, $result['attempt']['quiz']); + $this->assertCount(0, $result['warnings']); + + // Test user with no capabilities. + // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles. + assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id); + // Empty all the caches that may be affected by this change. + accesslib_clear_all_caches_for_unit_testing(); + course_modinfo::clear_instance_cache(); + + try { + mod_quiz_external::start_attempt($quiz->id); + $this->fail('Exception expected due to missing capability.'); + } catch (required_capability_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + } + } diff --git a/mod/quiz/version.php b/mod/quiz/version.php index a9fedb3ebd9..57b48ce8227 100644 --- a/mod/quiz/version.php +++ b/mod/quiz/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2015111606; +$plugin->version = 2015111607; $plugin->requires = 2015111000; $plugin->component = 'mod_quiz'; $plugin->cron = 60;