diff --git a/mod/quiz/attempt.php b/mod/quiz/attempt.php index 181c9bc80c4..f9208e7ea04 100644 --- a/mod/quiz/attempt.php +++ b/mod/quiz/attempt.php @@ -95,18 +95,7 @@ if ($autosaveperiod) { } // Log this page view. -$params = array( - 'objectid' => $attemptid, - 'relateduserid' => $attemptobj->get_userid(), - 'courseid' => $attemptobj->get_courseid(), - 'context' => context_module::instance($attemptobj->get_cmid()), - 'other' => array( - 'quizid' => $attemptobj->get_quizid() - ) -); -$event = \mod_quiz\event\attempt_viewed::create($params); -$event->add_record_snapshot('quiz_attempts', $attemptobj->get_attempt()); -$event->trigger(); +$attemptobj->fire_attempt_viewed_event(); // Get the list of questions needed by this page. $slots = $attemptobj->get_slots($page); @@ -116,13 +105,9 @@ if (empty($slots)) { throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noquestionsfound'); } -// Update attempt page. -if ($attemptobj->get_currentpage() != $page) { - if ($attemptobj->get_navigation_method() == QUIZ_NAVMETHOD_SEQ && $attemptobj->get_currentpage() > $page) { - // Prevent out of sequence access. - redirect($attemptobj->start_attempt_url(null, $attemptobj->get_currentpage())); - } - $DB->set_field('quiz_attempts', 'currentpage', $page, array('id' => $attemptid)); +// Update attempt page, redirecting the user if $page is not valid. +if (!$attemptobj->set_currentpage($page)) { + redirect($attemptobj->start_attempt_url(null, $attemptobj->get_currentpage())); } // Initialise the JavaScript. diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 39f093e4780..b367917adad 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -2170,6 +2170,61 @@ class quiz_attempt { return $becomingabandoned ? self::ABANDONED : self::FINISHED; } + /** + * Check a page access to see if is an out of sequence access. + * + * @param int $page page number + * @return boolean false is is an out of sequence access, true otherwise. + * @since Moodle 3.1 + */ + public function check_page_access($page) { + global $DB; + + if ($this->get_currentpage() != $page) { + if ($this->get_navigation_method() == QUIZ_NAVMETHOD_SEQ && $this->get_currentpage() > $page) { + return false; + } + } + return true; + } + + /** + * Update attempt page. + * + * @param int $page page number + * @return boolean true if everything was ok, false otherwise (out of sequence access). + * @since Moodle 3.1 + */ + public function set_currentpage($page) { + global $DB; + + if ($this->check_page_access($page)) { + $DB->set_field('quiz_attempts', 'currentpage', $page, array('id' => $this->get_attemptid())); + return true; + } + return false; + } + + /** + * Trigger the attempt_viewed event. + * + * @since Moodle 3.1 + */ + public function fire_attempt_viewed_event() { + $params = array( + 'objectid' => $this->get_attemptid(), + 'relateduserid' => $this->get_userid(), + 'courseid' => $this->get_courseid(), + 'context' => context_module::instance($this->get_cmid()), + 'other' => array( + 'quizid' => $this->get_quizid() + ) + ); + $event = \mod_quiz\event\attempt_viewed::create($params); + $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); + $event->trigger(); + } + } diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index 21a23e726ff..9363a29fb64 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -845,12 +845,8 @@ class mod_quiz_external extends external_api { } // Prevent out of sequence access. - if ($attemptobj->get_currentpage() != $params['page']) { - if ($attemptobj->get_navigation_method() == QUIZ_NAVMETHOD_SEQ && - $attemptobj->get_currentpage() > $params['page']) { - - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access'); - } + if (!$attemptobj->check_page_access($params['page'])) { + throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access'); } // Check slots. @@ -1386,4 +1382,67 @@ class mod_quiz_external extends external_api { ); } + /** + * Describes the parameters for view_attempt. + * + * @return external_external_function_parameters + * @since Moodle 3.1 + */ + public static function view_attempt_parameters() { + return new external_function_parameters ( + array( + 'attemptid' => new external_value(PARAM_INT, 'attempt id'), + 'page' => new external_value(PARAM_INT, 'page number'), + ) + ); + } + + /** + * Trigger the attempt viewed event. + * + * @param int $attemptid attempt id + * @param int $page page number + * @return array of warnings and status result + * @since Moodle 3.1 + */ + public static function view_attempt($attemptid, $page) { + + $warnings = array(); + + $params = array( + 'attemptid' => $attemptid, + 'page' => $page, + ); + $params = self::validate_parameters(self::view_attempt_parameters(), $params); + list($attemptobj, $messages) = self::validate_attempt($params); + + // Log action. + $attemptobj->fire_attempt_viewed_event(); + + // Update attempt page, throwing an exception if $page is not valid. + if (!$attemptobj->set_currentpage($params['page'])) { + throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access'); + } + + $result = array(); + $result['status'] = true; + $result['warnings'] = $warnings; + return $result; + } + + /** + * Describes the view_attempt return value. + * + * @return external_single_structure + * @since Moodle 3.1 + */ + public static function view_attempt_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'status: true if success'), + 'warnings' => new external_warnings(), + ) + ); + } + } diff --git a/mod/quiz/db/services.php b/mod/quiz/db/services.php index 0abd1dcee13..33c9095a642 100644 --- a/mod/quiz/db/services.php +++ b/mod/quiz/db/services.php @@ -128,4 +128,13 @@ $functions = array( 'capabilities' => 'mod/quiz:reviewmyattempts', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), + + 'mod_quiz_view_attempt' => array( + 'classname' => 'mod_quiz_external', + 'methodname' => 'view_attempt', + 'description' => 'Trigger the attempt viewed event.', + '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 df10871c895..942dd8f629c 100644 --- a/mod/quiz/tests/external_test.php +++ b/mod/quiz/tests/external_test.php @@ -1247,4 +1247,53 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { } + /** + * Test test_view_attempt + */ + public function test_view_attempt() { + global $DB; + + // Create a new quiz with two questions and one attempt started. + list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false); + + // Test user with full capabilities. + $this->setUser($this->student); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + + $result = mod_quiz_external::view_attempt($attempt->id, 0); + $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result); + $this->assertTrue($result['status']); + + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = array_shift($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\mod_quiz\event\attempt_viewed', $event); + $this->assertEquals($context, $event->get_context()); + $this->assertEventContextNotUsed($event); + $this->assertNotEmpty($event->get_name()); + + // Now, force the quiz with QUIZ_NAVMETHOD_SEQ (sequencial) navigation method. + $DB->set_field('quiz', 'navmethod', QUIZ_NAVMETHOD_SEQ, array('id' => $quiz->id)); + // See next page. + $result = mod_quiz_external::view_attempt($attempt->id, 1); + $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result); + $this->assertTrue($result['status']); + + $events = $sink->get_events(); + $this->assertCount(2, $events); + + // Try to go to previous page. + try { + mod_quiz_external::view_attempt($attempt->id, 0); + $this->fail('Exception expected due to try to see a previous page.'); + } catch (moodle_quiz_exception $e) { + $this->assertEquals('Out of sequence access', $e->errorcode); + } + + } + } diff --git a/mod/quiz/version.php b/mod/quiz/version.php index 8bcb06f2f1b..8fa4cad4995 100644 --- a/mod/quiz/version.php +++ b/mod/quiz/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2015111612; +$plugin->version = 2015111613; $plugin->requires = 2015111000; $plugin->component = 'mod_quiz'; $plugin->cron = 60;