See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Quiz module external functions tests. 19 * 20 * @package mod_quiz 21 * @category external 22 * @copyright 2016 Juan Leyva <juan@moodle.com> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 * @since Moodle 3.1 25 */ 26 27 namespace mod_quiz\external; 28 29 use externallib_advanced_testcase; 30 use mod_quiz_external; 31 use mod_quiz_display_options; 32 use question_usage_by_activity; 33 use quiz; 34 use quiz_attempt; 35 36 defined('MOODLE_INTERNAL') || die(); 37 38 global $CFG; 39 40 require_once($CFG->dirroot . '/webservice/tests/helpers.php'); 41 42 /** 43 * Silly class to access mod_quiz_external internal methods. 44 * 45 * @package mod_quiz 46 * @copyright 2016 Juan Leyva <juan@moodle.com> 47 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 48 * @since Moodle 3.1 49 */ 50 class testable_mod_quiz_external extends mod_quiz_external { 51 52 /** 53 * Public accessor. 54 * 55 * @param array $params Array of parameters including the attemptid and preflight data 56 * @param bool $checkaccessrules whether to check the quiz access rules or not 57 * @param bool $failifoverdue whether to return error if the attempt is overdue 58 * @return array containing the attempt object and access messages 59 */ 60 public static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) { 61 return parent::validate_attempt($params, $checkaccessrules, $failifoverdue); 62 } 63 64 /** 65 * Public accessor. 66 * 67 * @param array $params Array of parameters including the attemptid 68 * @return array containing the attempt object and display options 69 */ 70 public static function validate_attempt_review($params) { 71 return parent::validate_attempt_review($params); 72 } 73 } 74 75 /** 76 * Quiz module external functions tests 77 * 78 * @package mod_quiz 79 * @category external 80 * @copyright 2016 Juan Leyva <juan@moodle.com> 81 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 82 * @since Moodle 3.1 83 */ 84 class external_test extends externallib_advanced_testcase { 85 86 /** 87 * Set up for every test 88 */ 89 public function setUp(): void { 90 global $DB; 91 $this->resetAfterTest(); 92 $this->setAdminUser(); 93 94 // Setup test data. 95 $this->course = $this->getDataGenerator()->create_course(); 96 $this->quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $this->course->id)); 97 $this->context = \context_module::instance($this->quiz->cmid); 98 $this->cm = get_coursemodule_from_instance('quiz', $this->quiz->id); 99 100 // Create users. 101 $this->student = self::getDataGenerator()->create_user(); 102 $this->teacher = self::getDataGenerator()->create_user(); 103 104 // Users enrolments. 105 $this->studentrole = $DB->get_record('role', array('shortname' => 'student')); 106 $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 107 // Allow student to receive messages. 108 $coursecontext = \context_course::instance($this->course->id); 109 assign_capability('mod/quiz:emailnotifysubmission', CAP_ALLOW, $this->teacherrole->id, $coursecontext, true); 110 111 $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual'); 112 $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual'); 113 } 114 115 /** 116 * Test that a sequential navigation quiz is not allowing to see questions in advance except if reviewing 117 */ 118 public function test_sequential_navigation_view_attempt() { 119 // Test user with full capabilities. 120 $quiz = $this->prepare_sequential_quiz(); 121 $attemptobj = $this->create_quiz_attempt_object($quiz); 122 $this->setUser($this->student); 123 // Check out of sequence access for view. 124 $this->assertNotEmpty(mod_quiz_external::view_attempt($attemptobj->get_attemptid(), 0, [])); 125 try { 126 mod_quiz_external::view_attempt($attemptobj->get_attemptid(), 3, []); 127 $this->fail('Exception expected due to out of sequence access.'); 128 } catch (\moodle_exception $e) { 129 $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage()); 130 } 131 } 132 133 /** 134 * Test that a sequential navigation quiz is not allowing to see questions in advance for a student 135 */ 136 public function test_sequential_navigation_attempt_summary() { 137 // Test user with full capabilities. 138 $quiz = $this->prepare_sequential_quiz(); 139 $attemptobj = $this->create_quiz_attempt_object($quiz); 140 $this->setUser($this->student); 141 // Check that we do not return other questions than the one currently viewed. 142 $result = mod_quiz_external::get_attempt_summary($attemptobj->get_attemptid()); 143 $this->assertCount(1, $result['questions']); 144 $this->assertStringContainsString('Question (1)', $result['questions'][0]['html']); 145 } 146 147 /** 148 * Test that a sequential navigation quiz is not allowing to see questions in advance for student 149 */ 150 public function test_sequential_navigation_get_attempt_data() { 151 // Test user with full capabilities. 152 $quiz = $this->prepare_sequential_quiz(); 153 $attemptobj = $this->create_quiz_attempt_object($quiz); 154 $this->setUser($this->student); 155 // Test invalid instance id. 156 try { 157 mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 2); 158 $this->fail('Exception expected due to out of sequence access.'); 159 } catch (\moodle_exception $e) { 160 $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage()); 161 } 162 // Now we moved to page 1, we should see page 2 and 1 but not 0 or 3. 163 $attemptobj->set_currentpage(1); 164 // Test invalid instance id. 165 try { 166 mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 0); 167 $this->fail('Exception expected due to out of sequence access.'); 168 } catch (\moodle_exception $e) { 169 $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage()); 170 } 171 172 try { 173 mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 3); 174 $this->fail('Exception expected due to out of sequence access.'); 175 } catch (\moodle_exception $e) { 176 $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage()); 177 } 178 179 // Now we can see page 1. 180 $result = mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 1); 181 $this->assertCount(1, $result['questions']); 182 $this->assertStringContainsString('Question (2)', $result['questions'][0]['html']); 183 } 184 185 /** 186 * Prepare quiz for sequential navigation tests 187 * 188 * @return quiz 189 */ 190 private function prepare_sequential_quiz() { 191 // Create a new quiz with 5 questions and one attempt started. 192 // Create a new quiz with attempts. 193 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 194 $data = [ 195 'course' => $this->course->id, 196 'sumgrades' => 2, 197 'preferredbehaviour' => 'deferredfeedback', 198 'navmethod' => QUIZ_NAVMETHOD_SEQ 199 ]; 200 $quiz = $quizgenerator->create_instance($data); 201 202 // Now generate the questions. 203 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 204 $cat = $questiongenerator->create_question_category(); 205 for ($pageindex = 1; $pageindex <= 5; $pageindex++) { 206 $question = $questiongenerator->create_question('truefalse', null, [ 207 'category' => $cat->id, 208 'questiontext' => ['text' => "Question ($pageindex)"] 209 ]); 210 quiz_add_quiz_question($question->id, $quiz, $pageindex); 211 } 212 213 $quizobj = quiz::create($quiz->id, $this->student->id); 214 // Set grade to pass. 215 $item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', 216 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); 217 $item->gradepass = 80; 218 $item->update(); 219 return $quizobj; 220 } 221 222 /** 223 * Create question attempt 224 * 225 * @param quiz $quizobj 226 * @param int|null $userid 227 * @param bool|null $ispreview 228 * @return quiz_attempt 229 * @throws \moodle_exception 230 */ 231 private function create_quiz_attempt_object($quizobj, $userid = null, $ispreview = false) { 232 global $USER; 233 $timenow = time(); 234 // Now, do one attempt. 235 $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 236 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 237 $attemptnumber = 1; 238 if (!empty($USER->id)) { 239 $attemptnumber = count(quiz_get_user_attempts($quizobj->get_quizid(), $USER->id)) + 1; 240 } 241 $attempt = quiz_create_attempt($quizobj, $attemptnumber, false, $timenow, $ispreview, $userid ?? $this->student->id); 242 quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow); 243 quiz_attempt_save_started($quizobj, $quba, $attempt); 244 $attemptobj = quiz_attempt::create($attempt->id); 245 return $attemptobj; 246 } 247 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body