Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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   * Unit tests for (some of) mod/quiz/locallib.php.
  19   *
  20   * @package    mod_quiz
  21   * @category   test
  22   * @copyright  2008 The Open University
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
  24   */
  25  namespace mod_quiz;
  26  
  27  use core_external\external_api;
  28  use mod_quiz\quiz_settings;
  29  
  30  defined('MOODLE_INTERNAL') || die();
  31  
  32  global $CFG;
  33  require_once($CFG->dirroot . '/mod/quiz/lib.php');
  34  require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
  35  
  36  /**
  37   * @copyright  2008 The Open University
  38   * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
  39   */
  40  class lib_test extends \advanced_testcase {
  41      use \quiz_question_helper_test_trait;
  42  
  43      public function test_quiz_has_grades() {
  44          $quiz = new \stdClass();
  45          $quiz->grade = '100.0000';
  46          $quiz->sumgrades = '100.0000';
  47          $this->assertTrue(quiz_has_grades($quiz));
  48          $quiz->sumgrades = '0.0000';
  49          $this->assertFalse(quiz_has_grades($quiz));
  50          $quiz->grade = '0.0000';
  51          $this->assertFalse(quiz_has_grades($quiz));
  52          $quiz->sumgrades = '100.0000';
  53          $this->assertFalse(quiz_has_grades($quiz));
  54      }
  55  
  56      public function test_quiz_format_grade() {
  57          $quiz = new \stdClass();
  58          $quiz->decimalpoints = 2;
  59          $this->assertEquals(quiz_format_grade($quiz, 0.12345678), format_float(0.12, 2));
  60          $this->assertEquals(quiz_format_grade($quiz, 0), format_float(0, 2));
  61          $this->assertEquals(quiz_format_grade($quiz, 1.000000000000), format_float(1, 2));
  62          $quiz->decimalpoints = 0;
  63          $this->assertEquals(quiz_format_grade($quiz, 0.12345678), '0');
  64      }
  65  
  66      public function test_quiz_get_grade_format() {
  67          $quiz = new \stdClass();
  68          $quiz->decimalpoints = 2;
  69          $this->assertEquals(quiz_get_grade_format($quiz), 2);
  70          $this->assertEquals($quiz->questiondecimalpoints, -1);
  71          $quiz->questiondecimalpoints = 2;
  72          $this->assertEquals(quiz_get_grade_format($quiz), 2);
  73          $quiz->decimalpoints = 3;
  74          $quiz->questiondecimalpoints = -1;
  75          $this->assertEquals(quiz_get_grade_format($quiz), 3);
  76          $quiz->questiondecimalpoints = 4;
  77          $this->assertEquals(quiz_get_grade_format($quiz), 4);
  78      }
  79  
  80      public function test_quiz_format_question_grade() {
  81          $quiz = new \stdClass();
  82          $quiz->decimalpoints = 2;
  83          $quiz->questiondecimalpoints = 2;
  84          $this->assertEquals(quiz_format_question_grade($quiz, 0.12345678), format_float(0.12, 2));
  85          $this->assertEquals(quiz_format_question_grade($quiz, 0), format_float(0, 2));
  86          $this->assertEquals(quiz_format_question_grade($quiz, 1.000000000000), format_float(1, 2));
  87          $quiz->decimalpoints = 3;
  88          $quiz->questiondecimalpoints = -1;
  89          $this->assertEquals(quiz_format_question_grade($quiz, 0.12345678), format_float(0.123, 3));
  90          $this->assertEquals(quiz_format_question_grade($quiz, 0), format_float(0, 3));
  91          $this->assertEquals(quiz_format_question_grade($quiz, 1.000000000000), format_float(1, 3));
  92          $quiz->questiondecimalpoints = 4;
  93          $this->assertEquals(quiz_format_question_grade($quiz, 0.12345678), format_float(0.1235, 4));
  94          $this->assertEquals(quiz_format_question_grade($quiz, 0), format_float(0, 4));
  95          $this->assertEquals(quiz_format_question_grade($quiz, 1.000000000000), format_float(1, 4));
  96      }
  97  
  98      /**
  99       * Test deleting a quiz instance.
 100       */
 101      public function test_quiz_delete_instance() {
 102          global $SITE, $DB;
 103          $this->resetAfterTest(true);
 104          $this->setAdminUser();
 105  
 106          // Setup a quiz with 1 standard and 1 random question.
 107          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 108          $quiz = $quizgenerator->create_instance(['course' => $SITE->id, 'questionsperpage' => 3, 'grade' => 100.0]);
 109  
 110          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 111          $cat = $questiongenerator->create_question_category();
 112          $standardq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 113  
 114          quiz_add_quiz_question($standardq->id, $quiz);
 115          $this->add_random_questions($quiz->id, 0, $cat->id, 1);
 116  
 117          // Get the random question.
 118          $randomq = $DB->get_record('question', ['qtype' => 'random']);
 119  
 120          quiz_delete_instance($quiz->id);
 121  
 122          // Check that the random question was deleted.
 123          if ($randomq) {
 124              $count = $DB->count_records('question', ['id' => $randomq->id]);
 125              $this->assertEquals(0, $count);
 126          }
 127          // Check that the standard question was not deleted.
 128          $count = $DB->count_records('question', ['id' => $standardq->id]);
 129          $this->assertEquals(1, $count);
 130  
 131          // Check that all the slots were removed.
 132          $count = $DB->count_records('quiz_slots', ['quizid' => $quiz->id]);
 133          $this->assertEquals(0, $count);
 134  
 135          // Check that the quiz was removed.
 136          $count = $DB->count_records('quiz', ['id' => $quiz->id]);
 137          $this->assertEquals(0, $count);
 138      }
 139  
 140      public function test_quiz_get_user_attempts() {
 141          global $DB;
 142          $this->resetAfterTest();
 143  
 144          $dg = $this->getDataGenerator();
 145          $quizgen = $dg->get_plugin_generator('mod_quiz');
 146          $course = $dg->create_course();
 147          $u1 = $dg->create_user();
 148          $u2 = $dg->create_user();
 149          $u3 = $dg->create_user();
 150          $u4 = $dg->create_user();
 151          $role = $DB->get_record('role', ['shortname' => 'student']);
 152  
 153          $dg->enrol_user($u1->id, $course->id, $role->id);
 154          $dg->enrol_user($u2->id, $course->id, $role->id);
 155          $dg->enrol_user($u3->id, $course->id, $role->id);
 156          $dg->enrol_user($u4->id, $course->id, $role->id);
 157  
 158          $quiz1 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
 159          $quiz2 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
 160  
 161          // Questions.
 162          $questgen = $dg->get_plugin_generator('core_question');
 163          $quizcat = $questgen->create_question_category();
 164          $question = $questgen->create_question('numerical', null, ['category' => $quizcat->id]);
 165          quiz_add_quiz_question($question->id, $quiz1);
 166          quiz_add_quiz_question($question->id, $quiz2);
 167  
 168          $quizobj1a = quiz_settings::create($quiz1->id, $u1->id);
 169          $quizobj1b = quiz_settings::create($quiz1->id, $u2->id);
 170          $quizobj1c = quiz_settings::create($quiz1->id, $u3->id);
 171          $quizobj1d = quiz_settings::create($quiz1->id, $u4->id);
 172          $quizobj2a = quiz_settings::create($quiz2->id, $u1->id);
 173  
 174          // Set attempts.
 175          $quba1a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1a->get_context());
 176          $quba1a->set_preferred_behaviour($quizobj1a->get_quiz()->preferredbehaviour);
 177          $quba1b = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1b->get_context());
 178          $quba1b->set_preferred_behaviour($quizobj1b->get_quiz()->preferredbehaviour);
 179          $quba1c = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1c->get_context());
 180          $quba1c->set_preferred_behaviour($quizobj1c->get_quiz()->preferredbehaviour);
 181          $quba1d = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1d->get_context());
 182          $quba1d->set_preferred_behaviour($quizobj1d->get_quiz()->preferredbehaviour);
 183          $quba2a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
 184          $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
 185  
 186          $timenow = time();
 187  
 188          // User 1 passes quiz 1.
 189          $attempt = quiz_create_attempt($quizobj1a, 1, false, $timenow, false, $u1->id);
 190          quiz_start_new_attempt($quizobj1a, $quba1a, $attempt, 1, $timenow);
 191          quiz_attempt_save_started($quizobj1a, $quba1a, $attempt);
 192          $attemptobj = quiz_attempt::create($attempt->id);
 193          $attemptobj->process_submitted_actions($timenow, false, [1 => ['answer' => '3.14']]);
 194          $attemptobj->process_finish($timenow, false);
 195  
 196          // User 2 goes overdue in quiz 1.
 197          $attempt = quiz_create_attempt($quizobj1b, 1, false, $timenow, false, $u2->id);
 198          quiz_start_new_attempt($quizobj1b, $quba1b, $attempt, 1, $timenow);
 199          quiz_attempt_save_started($quizobj1b, $quba1b, $attempt);
 200          $attemptobj = quiz_attempt::create($attempt->id);
 201          $attemptobj->process_going_overdue($timenow, true);
 202  
 203          // User 3 does not finish quiz 1.
 204          $attempt = quiz_create_attempt($quizobj1c, 1, false, $timenow, false, $u3->id);
 205          quiz_start_new_attempt($quizobj1c, $quba1c, $attempt, 1, $timenow);
 206          quiz_attempt_save_started($quizobj1c, $quba1c, $attempt);
 207  
 208          // User 4 abandons the quiz 1.
 209          $attempt = quiz_create_attempt($quizobj1d, 1, false, $timenow, false, $u4->id);
 210          quiz_start_new_attempt($quizobj1d, $quba1d, $attempt, 1, $timenow);
 211          quiz_attempt_save_started($quizobj1d, $quba1d, $attempt);
 212          $attemptobj = quiz_attempt::create($attempt->id);
 213          $attemptobj->process_abandon($timenow, true);
 214  
 215          // User 1 attempts the quiz three times (abandon, finish, in progress).
 216          $quba2a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
 217          $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
 218  
 219          $attempt = quiz_create_attempt($quizobj2a, 1, false, $timenow, false, $u1->id);
 220          quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 1, $timenow);
 221          quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
 222          $attemptobj = quiz_attempt::create($attempt->id);
 223          $attemptobj->process_abandon($timenow, true);
 224  
 225          $quba2a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
 226          $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
 227  
 228          $attempt = quiz_create_attempt($quizobj2a, 2, false, $timenow, false, $u1->id);
 229          quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 2, $timenow);
 230          quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
 231          $attemptobj = quiz_attempt::create($attempt->id);
 232          $attemptobj->process_finish($timenow, false);
 233  
 234          $quba2a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
 235          $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
 236  
 237          $attempt = quiz_create_attempt($quizobj2a, 3, false, $timenow, false, $u1->id);
 238          quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 3, $timenow);
 239          quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
 240  
 241          // Check for user 1.
 242          $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'all');
 243          $this->assertCount(1, $attempts);
 244          $attempt = array_shift($attempts);
 245          $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
 246          $this->assertEquals($u1->id, $attempt->userid);
 247          $this->assertEquals($quiz1->id, $attempt->quiz);
 248  
 249          $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'finished');
 250          $this->assertCount(1, $attempts);
 251          $attempt = array_shift($attempts);
 252          $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
 253          $this->assertEquals($u1->id, $attempt->userid);
 254          $this->assertEquals($quiz1->id, $attempt->quiz);
 255  
 256          $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'unfinished');
 257          $this->assertCount(0, $attempts);
 258  
 259          // Check for user 2.
 260          $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'all');
 261          $this->assertCount(1, $attempts);
 262          $attempt = array_shift($attempts);
 263          $this->assertEquals(quiz_attempt::OVERDUE, $attempt->state);
 264          $this->assertEquals($u2->id, $attempt->userid);
 265          $this->assertEquals($quiz1->id, $attempt->quiz);
 266  
 267          $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'finished');
 268          $this->assertCount(0, $attempts);
 269  
 270          $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'unfinished');
 271          $this->assertCount(1, $attempts);
 272          $attempt = array_shift($attempts);
 273          $this->assertEquals(quiz_attempt::OVERDUE, $attempt->state);
 274          $this->assertEquals($u2->id, $attempt->userid);
 275          $this->assertEquals($quiz1->id, $attempt->quiz);
 276  
 277          // Check for user 3.
 278          $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'all');
 279          $this->assertCount(1, $attempts);
 280          $attempt = array_shift($attempts);
 281          $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
 282          $this->assertEquals($u3->id, $attempt->userid);
 283          $this->assertEquals($quiz1->id, $attempt->quiz);
 284  
 285          $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'finished');
 286          $this->assertCount(0, $attempts);
 287  
 288          $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'unfinished');
 289          $this->assertCount(1, $attempts);
 290          $attempt = array_shift($attempts);
 291          $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
 292          $this->assertEquals($u3->id, $attempt->userid);
 293          $this->assertEquals($quiz1->id, $attempt->quiz);
 294  
 295          // Check for user 4.
 296          $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'all');
 297          $this->assertCount(1, $attempts);
 298          $attempt = array_shift($attempts);
 299          $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
 300          $this->assertEquals($u4->id, $attempt->userid);
 301          $this->assertEquals($quiz1->id, $attempt->quiz);
 302  
 303          $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'finished');
 304          $this->assertCount(1, $attempts);
 305          $attempt = array_shift($attempts);
 306          $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
 307          $this->assertEquals($u4->id, $attempt->userid);
 308          $this->assertEquals($quiz1->id, $attempt->quiz);
 309  
 310          $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'unfinished');
 311          $this->assertCount(0, $attempts);
 312  
 313          // Multiple attempts for user 1 in quiz 2.
 314          $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'all');
 315          $this->assertCount(3, $attempts);
 316          $attempt = array_shift($attempts);
 317          $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
 318          $this->assertEquals($u1->id, $attempt->userid);
 319          $this->assertEquals($quiz2->id, $attempt->quiz);
 320          $attempt = array_shift($attempts);
 321          $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
 322          $this->assertEquals($u1->id, $attempt->userid);
 323          $this->assertEquals($quiz2->id, $attempt->quiz);
 324          $attempt = array_shift($attempts);
 325          $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
 326          $this->assertEquals($u1->id, $attempt->userid);
 327          $this->assertEquals($quiz2->id, $attempt->quiz);
 328  
 329          $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'finished');
 330          $this->assertCount(2, $attempts);
 331          $attempt = array_shift($attempts);
 332          $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
 333          $attempt = array_shift($attempts);
 334          $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
 335  
 336          $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'unfinished');
 337          $this->assertCount(1, $attempts);
 338          $attempt = array_shift($attempts);
 339  
 340          // Multiple quiz attempts fetched at once.
 341          $attempts = quiz_get_user_attempts([$quiz1->id, $quiz2->id], $u1->id, 'all');
 342          $this->assertCount(4, $attempts);
 343          $attempt = array_shift($attempts);
 344          $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
 345          $this->assertEquals($u1->id, $attempt->userid);
 346          $this->assertEquals($quiz1->id, $attempt->quiz);
 347          $attempt = array_shift($attempts);
 348          $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
 349          $this->assertEquals($u1->id, $attempt->userid);
 350          $this->assertEquals($quiz2->id, $attempt->quiz);
 351          $attempt = array_shift($attempts);
 352          $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
 353          $this->assertEquals($u1->id, $attempt->userid);
 354          $this->assertEquals($quiz2->id, $attempt->quiz);
 355          $attempt = array_shift($attempts);
 356          $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
 357          $this->assertEquals($u1->id, $attempt->userid);
 358          $this->assertEquals($quiz2->id, $attempt->quiz);
 359      }
 360  
 361      /**
 362       * Test for quiz_get_group_override_priorities().
 363       */
 364      public function test_quiz_get_group_override_priorities() {
 365          global $DB;
 366          $this->resetAfterTest();
 367  
 368          $dg = $this->getDataGenerator();
 369          $quizgen = $dg->get_plugin_generator('mod_quiz');
 370          $course = $dg->create_course();
 371  
 372          $quiz = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
 373  
 374          $this->assertNull(quiz_get_group_override_priorities($quiz->id));
 375  
 376          $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
 377          $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
 378  
 379          $now = 100;
 380          $override1 = (object)[
 381              'quiz' => $quiz->id,
 382              'groupid' => $group1->id,
 383              'timeopen' => $now,
 384              'timeclose' => $now + 20
 385          ];
 386          $DB->insert_record('quiz_overrides', $override1);
 387  
 388          $override2 = (object)[
 389              'quiz' => $quiz->id,
 390              'groupid' => $group2->id,
 391              'timeopen' => $now - 10,
 392              'timeclose' => $now + 10
 393          ];
 394          $DB->insert_record('quiz_overrides', $override2);
 395  
 396          $priorities = quiz_get_group_override_priorities($quiz->id);
 397          $this->assertNotEmpty($priorities);
 398  
 399          $openpriorities = $priorities['open'];
 400          // Override 2's time open has higher priority since it is sooner than override 1's.
 401          $this->assertEquals(2, $openpriorities[$override1->timeopen]);
 402          $this->assertEquals(1, $openpriorities[$override2->timeopen]);
 403  
 404          $closepriorities = $priorities['close'];
 405          // Override 1's time close has higher priority since it is later than override 2's.
 406          $this->assertEquals(1, $closepriorities[$override1->timeclose]);
 407          $this->assertEquals(2, $closepriorities[$override2->timeclose]);
 408      }
 409  
 410      public function test_quiz_core_calendar_provide_event_action_open() {
 411          $this->resetAfterTest();
 412  
 413          $this->setAdminUser();
 414  
 415          // Create a course.
 416          $course = $this->getDataGenerator()->create_course();
 417          // Create a student and enrol into the course.
 418          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 419          // Create a quiz.
 420          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
 421              'timeopen' => time() - DAYSECS, 'timeclose' => time() + DAYSECS]);
 422  
 423          // Create a calendar event.
 424          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
 425          // Now, log in as student.
 426          $this->setUser($student);
 427          // Create an action factory.
 428          $factory = new \core_calendar\action_factory();
 429  
 430          // Decorate action event.
 431          $actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory);
 432  
 433          // Confirm the event was decorated.
 434          $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
 435          $this->assertEquals(get_string('attemptquiznow', 'quiz'), $actionevent->get_name());
 436          $this->assertInstanceOf('moodle_url', $actionevent->get_url());
 437          $this->assertEquals(1, $actionevent->get_item_count());
 438          $this->assertTrue($actionevent->is_actionable());
 439      }
 440  
 441      public function test_quiz_core_calendar_provide_event_action_open_for_user() {
 442          $this->resetAfterTest();
 443  
 444          $this->setAdminUser();
 445  
 446          // Create a course.
 447          $course = $this->getDataGenerator()->create_course();
 448          // Create a student and enrol into the course.
 449          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 450          // Create a quiz.
 451          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
 452              'timeopen' => time() - DAYSECS, 'timeclose' => time() + DAYSECS]);
 453  
 454          // Create a calendar event.
 455          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
 456  
 457          // Create an action factory.
 458          $factory = new \core_calendar\action_factory();
 459  
 460          // Decorate action event for the student.
 461          $actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id);
 462  
 463          // Confirm the event was decorated.
 464          $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
 465          $this->assertEquals(get_string('attemptquiznow', 'quiz'), $actionevent->get_name());
 466          $this->assertInstanceOf('moodle_url', $actionevent->get_url());
 467          $this->assertEquals(1, $actionevent->get_item_count());
 468          $this->assertTrue($actionevent->is_actionable());
 469      }
 470  
 471      public function test_quiz_core_calendar_provide_event_action_closed() {
 472          $this->resetAfterTest();
 473  
 474          $this->setAdminUser();
 475  
 476          // Create a course.
 477          $course = $this->getDataGenerator()->create_course();
 478  
 479          // Create a quiz.
 480          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
 481              'timeclose' => time() - DAYSECS]);
 482  
 483          // Create a calendar event.
 484          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_CLOSE);
 485  
 486          // Create an action factory.
 487          $factory = new \core_calendar\action_factory();
 488  
 489          // Confirm the result was null.
 490          $this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory));
 491      }
 492  
 493      public function test_quiz_core_calendar_provide_event_action_closed_for_user() {
 494          $this->resetAfterTest();
 495  
 496          $this->setAdminUser();
 497  
 498          // Create a course.
 499          $course = $this->getDataGenerator()->create_course();
 500  
 501          // Create a student.
 502          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 503  
 504          // Create a quiz.
 505          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
 506              'timeclose' => time() - DAYSECS]);
 507  
 508          // Create a calendar event.
 509          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_CLOSE);
 510  
 511          // Create an action factory.
 512          $factory = new \core_calendar\action_factory();
 513  
 514          // Confirm the result was null.
 515          $this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id));
 516      }
 517  
 518      public function test_quiz_core_calendar_provide_event_action_open_in_future() {
 519          $this->resetAfterTest();
 520  
 521          $this->setAdminUser();
 522  
 523          // Create a course.
 524          $course = $this->getDataGenerator()->create_course();
 525          // Create a student and enrol into the course.
 526          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 527          // Create a quiz.
 528          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
 529              'timeopen' => time() + DAYSECS]);
 530  
 531          // Create a calendar event.
 532          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_CLOSE);
 533          // Now, log in as student.
 534          $this->setUser($student);
 535          // Create an action factory.
 536          $factory = new \core_calendar\action_factory();
 537  
 538          // Decorate action event.
 539          $actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory);
 540  
 541          // Confirm the event was decorated.
 542          $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
 543          $this->assertEquals(get_string('attemptquiznow', 'quiz'), $actionevent->get_name());
 544          $this->assertInstanceOf('moodle_url', $actionevent->get_url());
 545          $this->assertEquals(1, $actionevent->get_item_count());
 546          $this->assertFalse($actionevent->is_actionable());
 547      }
 548  
 549      public function test_quiz_core_calendar_provide_event_action_open_in_future_for_user() {
 550          $this->resetAfterTest();
 551  
 552          $this->setAdminUser();
 553  
 554          // Create a course.
 555          $course = $this->getDataGenerator()->create_course();
 556          // Create a student and enrol into the course.
 557          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 558          // Create a quiz.
 559          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
 560              'timeopen' => time() + DAYSECS]);
 561  
 562          // Create a calendar event.
 563          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_CLOSE);
 564  
 565          // Create an action factory.
 566          $factory = new \core_calendar\action_factory();
 567  
 568          // Decorate action event for the student.
 569          $actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id);
 570  
 571          // Confirm the event was decorated.
 572          $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
 573          $this->assertEquals(get_string('attemptquiznow', 'quiz'), $actionevent->get_name());
 574          $this->assertInstanceOf('moodle_url', $actionevent->get_url());
 575          $this->assertEquals(1, $actionevent->get_item_count());
 576          $this->assertFalse($actionevent->is_actionable());
 577      }
 578  
 579      public function test_quiz_core_calendar_provide_event_action_no_capability() {
 580          global $DB;
 581  
 582          $this->resetAfterTest();
 583          $this->setAdminUser();
 584  
 585          // Create a course.
 586          $course = $this->getDataGenerator()->create_course();
 587  
 588          // Create a student.
 589          $student = $this->getDataGenerator()->create_user();
 590          $studentrole = $DB->get_record('role', ['shortname' => 'student']);
 591  
 592          // Enrol student.
 593          $this->assertTrue($this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id));
 594  
 595          // Create a quiz.
 596          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
 597  
 598          // Remove the permission to attempt or review the quiz for the student role.
 599          $coursecontext = \context_course::instance($course->id);
 600          assign_capability('mod/quiz:reviewmyattempts', CAP_PROHIBIT, $studentrole->id, $coursecontext);
 601          assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $studentrole->id, $coursecontext);
 602  
 603          // Create a calendar event.
 604          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
 605  
 606          // Create an action factory.
 607          $factory = new \core_calendar\action_factory();
 608  
 609          // Set current user to the student.
 610          $this->setUser($student);
 611  
 612          // Confirm null is returned.
 613          $this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory));
 614      }
 615  
 616      public function test_quiz_core_calendar_provide_event_action_no_capability_for_user() {
 617          global $DB;
 618  
 619          $this->resetAfterTest();
 620          $this->setAdminUser();
 621  
 622          // Create a course.
 623          $course = $this->getDataGenerator()->create_course();
 624  
 625          // Create a student.
 626          $student = $this->getDataGenerator()->create_user();
 627          $studentrole = $DB->get_record('role', ['shortname' => 'student']);
 628  
 629          // Enrol student.
 630          $this->assertTrue($this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id));
 631  
 632          // Create a quiz.
 633          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
 634  
 635          // Remove the permission to attempt or review the quiz for the student role.
 636          $coursecontext = \context_course::instance($course->id);
 637          assign_capability('mod/quiz:reviewmyattempts', CAP_PROHIBIT, $studentrole->id, $coursecontext);
 638          assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $studentrole->id, $coursecontext);
 639  
 640          // Create a calendar event.
 641          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
 642  
 643          // Create an action factory.
 644          $factory = new \core_calendar\action_factory();
 645  
 646          // Confirm null is returned.
 647          $this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id));
 648      }
 649  
 650      public function test_quiz_core_calendar_provide_event_action_already_finished() {
 651          global $DB;
 652  
 653          $this->resetAfterTest();
 654  
 655          $this->setAdminUser();
 656  
 657          // Create a course.
 658          $course = $this->getDataGenerator()->create_course();
 659  
 660          // Create a student.
 661          $student = $this->getDataGenerator()->create_user();
 662          $studentrole = $DB->get_record('role', ['shortname' => 'student']);
 663  
 664          // Enrol student.
 665          $this->assertTrue($this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id));
 666  
 667          // Create a quiz.
 668          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
 669              'sumgrades' => 1]);
 670  
 671          // Add a question to the quiz.
 672          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 673          $cat = $questiongenerator->create_question_category();
 674          $question = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
 675          quiz_add_quiz_question($question->id, $quiz);
 676  
 677          // Get the quiz object.
 678          $quizobj = quiz_settings::create($quiz->id, $student->id);
 679  
 680          // Create an attempt for the student in the quiz.
 681          $timenow = time();
 682          $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $student->id);
 683          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 684          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 685          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 686          quiz_attempt_save_started($quizobj, $quba, $attempt);
 687  
 688          // Finish the attempt.
 689          $attemptobj = quiz_attempt::create($attempt->id);
 690          $attemptobj->process_finish($timenow, false);
 691  
 692          // Create a calendar event.
 693          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
 694  
 695          // Create an action factory.
 696          $factory = new \core_calendar\action_factory();
 697  
 698          // Set current user to the student.
 699          $this->setUser($student);
 700  
 701          // Confirm null is returned.
 702          $this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory));
 703      }
 704  
 705      public function test_quiz_core_calendar_provide_event_action_already_finished_for_user() {
 706          global $DB;
 707  
 708          $this->resetAfterTest();
 709  
 710          $this->setAdminUser();
 711  
 712          // Create a course.
 713          $course = $this->getDataGenerator()->create_course();
 714  
 715          // Create a student.
 716          $student = $this->getDataGenerator()->create_user();
 717          $studentrole = $DB->get_record('role', ['shortname' => 'student']);
 718  
 719          // Enrol student.
 720          $this->assertTrue($this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id));
 721  
 722          // Create a quiz.
 723          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id,
 724              'sumgrades' => 1]);
 725  
 726          // Add a question to the quiz.
 727          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 728          $cat = $questiongenerator->create_question_category();
 729          $question = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
 730          quiz_add_quiz_question($question->id, $quiz);
 731  
 732          // Get the quiz object.
 733          $quizobj = quiz_settings::create($quiz->id, $student->id);
 734  
 735          // Create an attempt for the student in the quiz.
 736          $timenow = time();
 737          $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $student->id);
 738          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 739          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 740          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 741          quiz_attempt_save_started($quizobj, $quba, $attempt);
 742  
 743          // Finish the attempt.
 744          $attemptobj = quiz_attempt::create($attempt->id);
 745          $attemptobj->process_finish($timenow, false);
 746  
 747          // Create a calendar event.
 748          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
 749  
 750          // Create an action factory.
 751          $factory = new \core_calendar\action_factory();
 752  
 753          // Confirm null is returned.
 754          $this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id));
 755      }
 756  
 757      public function test_quiz_core_calendar_provide_event_action_already_completed() {
 758          $this->resetAfterTest();
 759          set_config('enablecompletion', 1);
 760          $this->setAdminUser();
 761  
 762          // Create the activity.
 763          $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
 764          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id],
 765              ['completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS]);
 766  
 767          // Get some additional data.
 768          $cm = get_coursemodule_from_instance('quiz', $quiz->id);
 769  
 770          // Create a calendar event.
 771          $event = $this->create_action_event($course->id, $quiz->id,
 772              \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
 773  
 774          // Mark the activity as completed.
 775          $completion = new \completion_info($course);
 776          $completion->set_module_viewed($cm);
 777  
 778          // Create an action factory.
 779          $factory = new \core_calendar\action_factory();
 780  
 781          // Decorate action event.
 782          $actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory);
 783  
 784          // Ensure result was null.
 785          $this->assertNull($actionevent);
 786      }
 787  
 788      public function test_quiz_core_calendar_provide_event_action_already_completed_for_user() {
 789          $this->resetAfterTest();
 790          set_config('enablecompletion', 1);
 791          $this->setAdminUser();
 792  
 793          // Create the activity.
 794          $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
 795          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id],
 796              ['completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS]);
 797  
 798          // Enrol a student in the course.
 799          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 800  
 801          // Get some additional data.
 802          $cm = get_coursemodule_from_instance('quiz', $quiz->id);
 803  
 804          // Create a calendar event.
 805          $event = $this->create_action_event($course->id, $quiz->id,
 806              \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
 807  
 808          // Mark the activity as completed for the student.
 809          $completion = new \completion_info($course);
 810          $completion->set_module_viewed($cm, $student->id);
 811  
 812          // Create an action factory.
 813          $factory = new \core_calendar\action_factory();
 814  
 815          // Decorate action event for the student.
 816          $actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id);
 817  
 818          // Ensure result was null.
 819          $this->assertNull($actionevent);
 820      }
 821  
 822      /**
 823       * Creates an action event.
 824       *
 825       * @param int $courseid
 826       * @param int $instanceid The quiz id.
 827       * @param string $eventtype The event type. eg. QUIZ_EVENT_TYPE_OPEN.
 828       * @return bool|calendar_event
 829       */
 830      private function create_action_event($courseid, $instanceid, $eventtype) {
 831          $event = new \stdClass();
 832          $event->name = 'Calendar event';
 833          $event->modulename  = 'quiz';
 834          $event->courseid = $courseid;
 835          $event->instance = $instanceid;
 836          $event->type = CALENDAR_EVENT_TYPE_ACTION;
 837          $event->eventtype = $eventtype;
 838          $event->timestart = time();
 839  
 840          return \calendar_event::create($event);
 841      }
 842  
 843      /**
 844       * Test the callback responsible for returning the completion rule descriptions.
 845       * This function should work given either an instance of the module (cm_info), such as when checking the active rules,
 846       * or if passed a stdClass of similar structure, such as when checking the the default completion settings for a mod type.
 847       */
 848      public function test_mod_quiz_completion_get_active_rule_descriptions() {
 849          $this->resetAfterTest();
 850          $this->setAdminUser();
 851  
 852          // Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't.
 853          $course = $this->getDataGenerator()->create_course(['enablecompletion' => 2]);
 854          $quiz1 = $this->getDataGenerator()->create_module('quiz', [
 855              'course' => $course->id,
 856              'completion' => 2,
 857              'completionusegrade' => 1,
 858              'completionpassgrade' => 1,
 859              'completionattemptsexhausted' => 1,
 860          ]);
 861          $quiz2 = $this->getDataGenerator()->create_module('quiz', [
 862              'course' => $course->id,
 863              'completion' => 2,
 864              'completionusegrade' => 0
 865          ]);
 866          $cm1 = \cm_info::create(get_coursemodule_from_instance('quiz', $quiz1->id));
 867          $cm2 = \cm_info::create(get_coursemodule_from_instance('quiz', $quiz2->id));
 868  
 869          // Data for the stdClass input type.
 870          // This type of input would occur when checking the default completion rules for an activity type, where we don't have
 871          // any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info.
 872          $moddefaults = new \stdClass();
 873          $moddefaults->customdata = ['customcompletionrules' => [
 874              'completionattemptsexhausted' => 1,
 875          ]];
 876          $moddefaults->completion = 2;
 877  
 878          $activeruledescriptions = [
 879              get_string('completionpassorattemptsexhausteddesc', 'quiz'),
 880          ];
 881          $this->assertEquals(mod_quiz_get_completion_active_rule_descriptions($cm1), $activeruledescriptions);
 882          $this->assertEquals(mod_quiz_get_completion_active_rule_descriptions($cm2), []);
 883          $this->assertEquals(mod_quiz_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
 884          $this->assertEquals(mod_quiz_get_completion_active_rule_descriptions(new \stdClass()), []);
 885      }
 886  
 887      /**
 888       * A user who does not have capabilities to add events to the calendar should be able to create a quiz.
 889       */
 890      public function test_creation_with_no_calendar_capabilities() {
 891          $this->resetAfterTest();
 892          $course = self::getDataGenerator()->create_course();
 893          $context = \context_course::instance($course->id);
 894          $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
 895          $roleid = self::getDataGenerator()->create_role();
 896          self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
 897          assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
 898          $generator = self::getDataGenerator()->get_plugin_generator('mod_quiz');
 899          // Create an instance as a user without the calendar capabilities.
 900          $this->setUser($user);
 901          $time = time();
 902          $params = [
 903              'course' => $course->id,
 904              'timeopen' => $time + 200,
 905              'timeclose' => $time + 2000,
 906          ];
 907          $generator->create_instance($params);
 908      }
 909  
 910      /**
 911       * Data provider for summarise_response() test cases.
 912       *
 913       * @return array List of data sets (test cases)
 914       */
 915      public function mod_quiz_inplace_editable_provider(): array {
 916          return [
 917              'set to A1' => [1, 'A1'],
 918              'set with HTML characters' => [2, 'A & &amp; <-:'],
 919              'set to integer' => [3, '3'],
 920              'set to blank' => [4, ''],
 921              'set with Unicode characters' => [1, 'L\'Aina Lluís^'],
 922              'set with Unicode at the truncation point' => [1, '123456789012345碁'],
 923              'set with HTML Char at the truncation point' => [1, '123456789012345>'],
 924          ];
 925      }
 926  
 927      /**
 928       * Test customised and automated question numbering for a given slot number and customised value.
 929       *
 930       * @dataProvider mod_quiz_inplace_editable_provider
 931       * @param int $slotnumber
 932       * @param string $newvalue
 933       * @covers ::mod_quiz_inplace_editable
 934       */
 935      public function test_mod_quiz_inplace_editable(int $slotnumber, string $newvalue): void {
 936          global $CFG;
 937          require_once($CFG->dirroot . '/lib/external/externallib.php');
 938          $this->resetAfterTest();
 939  
 940          $this->setAdminUser();
 941          $course = self::getDataGenerator()->create_course();
 942          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id, 'sumgrades' => 1]);
 943          $cm = get_coursemodule_from_id('quiz', $quiz->cmid);
 944  
 945          // Add few questions to the quiz.
 946          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 947          $cat = $questiongenerator->create_question_category();
 948  
 949          $question = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
 950          quiz_add_quiz_question($question->id, $quiz);
 951  
 952          $question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 953          quiz_add_quiz_question($question->id, $quiz);
 954  
 955          $question = $questiongenerator->create_question('multichoice', null, ['category' => $cat->id]);
 956          quiz_add_quiz_question($question->id, $quiz);
 957  
 958          $question = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
 959          quiz_add_quiz_question($question->id, $quiz);
 960  
 961          // Create the quiz object.
 962          $quizobj = new quiz_settings($quiz, $cm, $course);
 963          $structure = $quizobj->get_structure();
 964  
 965          $slots = $structure->get_slots();
 966          $this->assertEquals(4, count($slots));
 967  
 968          $slotid = $structure->get_slot_id_for_slot($slotnumber);
 969          $inplaceeditable = mod_quiz_inplace_editable('slotdisplaynumber', $slotid, $newvalue);
 970          $result = \core_external::update_inplace_editable('mod_quiz', 'slotdisplaynumber', $slotid, $newvalue);
 971          $result = external_api::clean_returnvalue(\core_external::update_inplace_editable_returns(), $result);
 972  
 973          $this->assertEquals(count((array) $inplaceeditable), count($result));
 974          $this->assertEquals($slotid, $result['itemid']);
 975          if ($newvalue === '' || is_null($newvalue)) {
 976              // Check against default.
 977              $this->assertEquals($slotnumber, $result['displayvalue']);
 978              $this->assertEquals($slotnumber, $result['value']);
 979          } else {
 980              // Check against the custom number.
 981              $this->assertEquals(s($newvalue), $result['displayvalue']);
 982              $this->assertEquals($newvalue, $result['value']);
 983          }
 984      }
 985  }