See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 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 namespace mod_quiz; 18 19 use question_bank; 20 use question_engine; 21 use quiz; 22 use quiz_attempt; 23 24 defined('MOODLE_INTERNAL') || die(); 25 26 global $CFG; 27 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 28 29 /** 30 * Quiz attempt walk through. 31 * 32 * @package mod_quiz 33 * @category test 34 * @copyright 2013 The Open University 35 * @author Jamie Pratt <me@jamiep.org> 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 * @covers \quiz_attempt 38 */ 39 class attempt_walkthrough_test extends \advanced_testcase { 40 41 /** 42 * Create a quiz with questions and walk through a quiz attempt. 43 */ 44 public function test_quiz_attempt_walkthrough() { 45 global $SITE; 46 47 $this->resetAfterTest(true); 48 49 // Make a quiz. 50 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 51 52 $quiz = $quizgenerator->create_instance(array('course'=>$SITE->id, 'questionsperpage' => 0, 'grade' => 100.0, 53 'sumgrades' => 3)); 54 55 // Create a couple of questions. 56 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 57 58 $cat = $questiongenerator->create_question_category(); 59 $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); 60 $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 61 $matchq = $questiongenerator->create_question('match', null, ['category' => $cat->id]); 62 $description = $questiongenerator->create_question('description', null, ['category' => $cat->id]); 63 64 // Add them to the quiz. 65 quiz_add_quiz_question($saq->id, $quiz); 66 quiz_add_quiz_question($numq->id, $quiz); 67 quiz_add_quiz_question($matchq->id, $quiz); 68 quiz_add_quiz_question($description->id, $quiz); 69 70 // Make a user to do the quiz. 71 $user1 = $this->getDataGenerator()->create_user(); 72 73 $quizobj = quiz::create($quiz->id, $user1->id); 74 75 // Start the attempt. 76 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 77 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 78 79 $timenow = time(); 80 $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user1->id); 81 82 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 83 $this->assertEquals('1,2,3,4,0', $attempt->layout); 84 85 quiz_attempt_save_started($quizobj, $quba, $attempt); 86 87 // Process some responses from the student. 88 $attemptobj = quiz_attempt::create($attempt->id); 89 $this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question()); 90 // The student has not answered any questions. 91 $this->assertEquals(3, $attemptobj->get_number_of_unanswered_questions()); 92 93 $tosubmit = array(1 => array('answer' => 'frog'), 94 2 => array('answer' => '3.14')); 95 96 $attemptobj->process_submitted_actions($timenow, false, $tosubmit); 97 // The student has answered two questions, and only one remaining. 98 $this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions()); 99 100 $tosubmit = [ 101 3 => [ 102 'frog' => 'amphibian', 103 'cat' => 'mammal', 104 'newt' => '' 105 ] 106 ]; 107 108 $attemptobj->process_submitted_actions($timenow, false, $tosubmit); 109 // The student has answered three questions but one is invalid, so there is still one remaining. 110 $this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions()); 111 112 $tosubmit = [ 113 3 => [ 114 'frog' => 'amphibian', 115 'cat' => 'mammal', 116 'newt' => 'amphibian' 117 ] 118 ]; 119 120 $attemptobj->process_submitted_actions($timenow, false, $tosubmit); 121 // The student has answered three questions, so there are no remaining. 122 $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions()); 123 124 // Finish the attempt. 125 $attemptobj = quiz_attempt::create($attempt->id); 126 $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); 127 $attemptobj->process_finish($timenow, false); 128 129 // Re-load quiz attempt data. 130 $attemptobj = quiz_attempt::create($attempt->id); 131 132 // Check that results are stored as expected. 133 $this->assertEquals(1, $attemptobj->get_attempt_number()); 134 $this->assertEquals(3, $attemptobj->get_sum_marks()); 135 $this->assertEquals(true, $attemptobj->is_finished()); 136 $this->assertEquals($timenow, $attemptobj->get_submitted_date()); 137 $this->assertEquals($user1->id, $attemptobj->get_userid()); 138 $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); 139 $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions()); 140 141 // Check quiz grades. 142 $grades = quiz_get_user_grades($quiz, $user1->id); 143 $grade = array_shift($grades); 144 $this->assertEquals(100.0, $grade->rawgrade); 145 146 // Check grade book. 147 $gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $quiz->id, $user1->id); 148 $gradebookitem = array_shift($gradebookgrades->items); 149 $gradebookgrade = array_shift($gradebookitem->grades); 150 $this->assertEquals(100, $gradebookgrade->grade); 151 } 152 153 /** 154 * Create a quiz containing one question and a close time. 155 * 156 * The question is the standard shortanswer test question. 157 * The quiz is set to close 1 hour from now. 158 * The quiz is set to use a grade period of 1 hour once time expires. 159 * 160 * @return \stdClass the quiz that was created. 161 */ 162 protected function create_quiz_with_one_question(): \stdClass { 163 global $SITE; 164 $this->resetAfterTest(); 165 166 // Make a quiz. 167 $timeclose = time() + HOURSECS; 168 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 169 170 $quiz = $quizgenerator->create_instance( 171 ['course' => $SITE->id, 'timeclose' => $timeclose, 172 'overduehandling' => 'graceperiod', 'graceperiod' => HOURSECS]); 173 174 // Create a question. 175 /** @var \core_question_generator $questiongenerator */ 176 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 177 $cat = $questiongenerator->create_question_category(); 178 $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); 179 180 // Add them to the quiz. 181 quiz_add_quiz_question($saq->id, $quiz, 0, 1); 182 quiz_update_sumgrades($quiz); 183 184 return $quiz; 185 } 186 187 public function test_quiz_attempt_walkthrough_submit_time_recorded_correctly_when_overdue() { 188 189 $quiz = $this->create_quiz_with_one_question(); 190 191 // Make a user to do the quiz. 192 $user = $this->getDataGenerator()->create_user(); 193 $this->setUser($user); 194 $quizobj = quiz::create($quiz->id, $user->id); 195 196 // Start the attempt. 197 $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null); 198 199 // Process some responses from the student. 200 $attemptobj = quiz_attempt::create($attempt->id); 201 $this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions()); 202 $attemptobj->process_submitted_actions($quiz->timeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]); 203 204 // Attempt goes overdue (e.g. if cron ran). 205 $attemptobj = quiz_attempt::create($attempt->id); 206 $attemptobj->process_going_overdue($quiz->timeclose + 2 * get_config('quiz', 'graceperiodmin'), false); 207 208 // Verify the attempt state. 209 $attemptobj = quiz_attempt::create($attempt->id); 210 $this->assertEquals(1, $attemptobj->get_attempt_number()); 211 $this->assertEquals(false, $attemptobj->is_finished()); 212 $this->assertEquals(0, $attemptobj->get_submitted_date()); 213 $this->assertEquals($user->id, $attemptobj->get_userid()); 214 $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); 215 $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions()); 216 217 // Student submits the attempt during the grace period. 218 $attemptobj = quiz_attempt::create($attempt->id); 219 $attemptobj->process_attempt($quiz->timeclose + 30 * MINSECS, true, false, 1); 220 221 // Verify the attempt state. 222 $attemptobj = quiz_attempt::create($attempt->id); 223 $this->assertEquals(1, $attemptobj->get_attempt_number()); 224 $this->assertEquals(true, $attemptobj->is_finished()); 225 $this->assertEquals($quiz->timeclose + 30 * MINSECS, $attemptobj->get_submitted_date()); 226 $this->assertEquals($user->id, $attemptobj->get_userid()); 227 $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); 228 $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions()); 229 } 230 231 public function test_quiz_attempt_walkthrough_close_time_extended_at_last_minute() { 232 global $DB; 233 234 $quiz = $this->create_quiz_with_one_question(); 235 $originaltimeclose = $quiz->timeclose; 236 237 // Make a user to do the quiz. 238 $user = $this->getDataGenerator()->create_user(); 239 $this->setUser($user); 240 $quizobj = quiz::create($quiz->id, $user->id); 241 242 // Start the attempt. 243 $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null); 244 245 // Process some responses from the student during the attempt. 246 $attemptobj = quiz_attempt::create($attempt->id); 247 $attemptobj->process_submitted_actions($originaltimeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]); 248 249 // Teacher edits the quiz to extend the time-limit by one minute. 250 $DB->set_field('quiz', 'timeclose', $originaltimeclose + MINSECS, ['id' => $quiz->id]); 251 \course_modinfo::clear_instance_cache($quiz->course); 252 253 // Timer expires in the student browser and thinks it is time to submit the quiz. 254 // This sets $finishattempt to false - since the student did not click the button, and $timeup to true. 255 $attemptobj = quiz_attempt::create($attempt->id); 256 $attemptobj->process_attempt($originaltimeclose, false, true, 1); 257 258 // Verify the attempt state - the $timeup was ignored becuase things have changed server-side. 259 $attemptobj = quiz_attempt::create($attempt->id); 260 $this->assertEquals(1, $attemptobj->get_attempt_number()); 261 $this->assertFalse($attemptobj->is_finished()); 262 $this->assertEquals(quiz_attempt::IN_PROGRESS, $attemptobj->get_state()); 263 $this->assertEquals(0, $attemptobj->get_submitted_date()); 264 $this->assertEquals($user->id, $attemptobj->get_userid()); 265 } 266 267 /** 268 * Create a quiz with a random as well as other questions and walk through quiz attempts. 269 */ 270 public function test_quiz_with_random_question_attempt_walkthrough() { 271 global $SITE; 272 273 $this->resetAfterTest(true); 274 question_bank::get_qtype('random')->clear_caches_before_testing(); 275 276 $this->setAdminUser(); 277 278 // Make a quiz. 279 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 280 281 $quiz = $quizgenerator->create_instance(array('course' => $SITE->id, 'questionsperpage' => 2, 'grade' => 100.0, 282 'sumgrades' => 4)); 283 284 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 285 286 // Add two questions to question category. 287 $cat = $questiongenerator->create_question_category(); 288 $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); 289 $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 290 291 // Add random question to the quiz. 292 quiz_add_random_questions($quiz, 0, $cat->id, 1, false); 293 294 // Make another category. 295 $cat2 = $questiongenerator->create_question_category(); 296 $match = $questiongenerator->create_question('match', null, array('category' => $cat->id)); 297 298 quiz_add_quiz_question($match->id, $quiz, 0); 299 300 $multichoicemulti = $questiongenerator->create_question('multichoice', 'two_of_four', array('category' => $cat->id)); 301 302 quiz_add_quiz_question($multichoicemulti->id, $quiz, 0); 303 304 $multichoicesingle = $questiongenerator->create_question('multichoice', 'one_of_four', array('category' => $cat->id)); 305 306 quiz_add_quiz_question($multichoicesingle->id, $quiz, 0); 307 308 foreach (array($saq->id => 'frog', $numq->id => '3.14') as $randomqidtoselect => $randqanswer) { 309 // Make a new user to do the quiz each loop. 310 $user1 = $this->getDataGenerator()->create_user(); 311 $this->setUser($user1); 312 313 $quizobj = quiz::create($quiz->id, $user1->id); 314 315 // Start the attempt. 316 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 317 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 318 319 $timenow = time(); 320 $attempt = quiz_create_attempt($quizobj, 1, false, $timenow); 321 322 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow, array(1 => $randomqidtoselect)); 323 $this->assertEquals('1,2,0,3,4,0', $attempt->layout); 324 325 quiz_attempt_save_started($quizobj, $quba, $attempt); 326 327 // Process some responses from the student. 328 $attemptobj = quiz_attempt::create($attempt->id); 329 $this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question()); 330 $this->assertEquals(4, $attemptobj->get_number_of_unanswered_questions()); 331 332 $tosubmit = array(); 333 $selectedquestionid = $quba->get_question_attempt(1)->get_question_id(); 334 $tosubmit[1] = array('answer' => $randqanswer); 335 $tosubmit[2] = array( 336 'frog' => 'amphibian', 337 'cat' => 'mammal', 338 'newt' => 'amphibian'); 339 $tosubmit[3] = array('One' => '1', 'Two' => '0', 'Three' => '1', 'Four' => '0'); // First and third choice. 340 $tosubmit[4] = array('answer' => 'One'); // The first choice. 341 342 $attemptobj->process_submitted_actions($timenow, false, $tosubmit); 343 344 // Finish the attempt. 345 $attemptobj = quiz_attempt::create($attempt->id); 346 $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); 347 $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions()); 348 $attemptobj->process_finish($timenow, false); 349 350 // Re-load quiz attempt data. 351 $attemptobj = quiz_attempt::create($attempt->id); 352 353 // Check that results are stored as expected. 354 $this->assertEquals(1, $attemptobj->get_attempt_number()); 355 $this->assertEquals(4, $attemptobj->get_sum_marks()); 356 $this->assertEquals(true, $attemptobj->is_finished()); 357 $this->assertEquals($timenow, $attemptobj->get_submitted_date()); 358 $this->assertEquals($user1->id, $attemptobj->get_userid()); 359 $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); 360 $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions()); 361 362 // Check quiz grades. 363 $grades = quiz_get_user_grades($quiz, $user1->id); 364 $grade = array_shift($grades); 365 $this->assertEquals(100.0, $grade->rawgrade); 366 367 // Check grade book. 368 $gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $quiz->id, $user1->id); 369 $gradebookitem = array_shift($gradebookgrades->items); 370 $gradebookgrade = array_shift($gradebookitem->grades); 371 $this->assertEquals(100, $gradebookgrade->grade); 372 } 373 } 374 375 376 public function get_correct_response_for_variants() { 377 return array(array(1, 9.9), array(2, 8.5), array(5, 14.2), array(10, 6.8, true)); 378 } 379 380 protected $quizwithvariants = null; 381 382 /** 383 * Create a quiz with a single question with variants and walk through quiz attempts. 384 * 385 * @dataProvider get_correct_response_for_variants 386 */ 387 public function test_quiz_with_question_with_variants_attempt_walkthrough($variantno, $correctresponse, $done = false) { 388 global $SITE; 389 390 $this->resetAfterTest($done); 391 392 $this->setAdminUser(); 393 394 if ($this->quizwithvariants === null) { 395 // Make a quiz. 396 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 397 398 $this->quizwithvariants = $quizgenerator->create_instance(array('course'=>$SITE->id, 399 'questionsperpage' => 0, 400 'grade' => 100.0, 401 'sumgrades' => 1)); 402 403 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 404 405 $cat = $questiongenerator->create_question_category(); 406 $calc = $questiongenerator->create_question('calculatedsimple', 'sumwithvariants', array('category' => $cat->id)); 407 quiz_add_quiz_question($calc->id, $this->quizwithvariants, 0); 408 } 409 410 411 // Make a new user to do the quiz. 412 $user1 = $this->getDataGenerator()->create_user(); 413 $this->setUser($user1); 414 $quizobj = quiz::create($this->quizwithvariants->id, $user1->id); 415 416 // Start the attempt. 417 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 418 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 419 420 $timenow = time(); 421 $attempt = quiz_create_attempt($quizobj, 1, false, $timenow); 422 423 // Select variant. 424 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow, array(), array(1 => $variantno)); 425 $this->assertEquals('1,0', $attempt->layout); 426 quiz_attempt_save_started($quizobj, $quba, $attempt); 427 428 // Process some responses from the student. 429 $attemptobj = quiz_attempt::create($attempt->id); 430 $this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question()); 431 $this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions()); 432 433 $tosubmit = array(1 => array('answer' => $correctresponse)); 434 $attemptobj->process_submitted_actions($timenow, false, $tosubmit); 435 436 // Finish the attempt. 437 $attemptobj = quiz_attempt::create($attempt->id); 438 $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); 439 $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions()); 440 441 $attemptobj->process_finish($timenow, false); 442 443 // Re-load quiz attempt data. 444 $attemptobj = quiz_attempt::create($attempt->id); 445 446 // Check that results are stored as expected. 447 $this->assertEquals(1, $attemptobj->get_attempt_number()); 448 $this->assertEquals(1, $attemptobj->get_sum_marks()); 449 $this->assertEquals(true, $attemptobj->is_finished()); 450 $this->assertEquals($timenow, $attemptobj->get_submitted_date()); 451 $this->assertEquals($user1->id, $attemptobj->get_userid()); 452 $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); 453 $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions()); 454 455 // Check quiz grades. 456 $grades = quiz_get_user_grades($this->quizwithvariants, $user1->id); 457 $grade = array_shift($grades); 458 $this->assertEquals(100.0, $grade->rawgrade); 459 460 // Check grade book. 461 $gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $this->quizwithvariants->id, $user1->id); 462 $gradebookitem = array_shift($gradebookgrades->items); 463 $gradebookgrade = array_shift($gradebookitem->grades); 464 $this->assertEquals(100, $gradebookgrade->grade); 465 } 466 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body