Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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 quiz;
  28  use quiz_attempt;
  29  
  30  defined('MOODLE_INTERNAL') || die();
  31  
  32  global $CFG;
  33  require_once($CFG->dirroot . '/mod/quiz/lib.php');
  34  
  35  /**
  36   * @copyright  2008 The Open University
  37   * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
  38   */
  39  class lib_test extends \advanced_testcase {
  40      public function test_quiz_has_grades() {
  41          $quiz = new \stdClass();
  42          $quiz->grade = '100.0000';
  43          $quiz->sumgrades = '100.0000';
  44          $this->assertTrue(quiz_has_grades($quiz));
  45          $quiz->sumgrades = '0.0000';
  46          $this->assertFalse(quiz_has_grades($quiz));
  47          $quiz->grade = '0.0000';
  48          $this->assertFalse(quiz_has_grades($quiz));
  49          $quiz->sumgrades = '100.0000';
  50          $this->assertFalse(quiz_has_grades($quiz));
  51      }
  52  
  53      public function test_quiz_format_grade() {
  54          $quiz = new \stdClass();
  55          $quiz->decimalpoints = 2;
  56          $this->assertEquals(quiz_format_grade($quiz, 0.12345678), format_float(0.12, 2));
  57          $this->assertEquals(quiz_format_grade($quiz, 0), format_float(0, 2));
  58          $this->assertEquals(quiz_format_grade($quiz, 1.000000000000), format_float(1, 2));
  59          $quiz->decimalpoints = 0;
  60          $this->assertEquals(quiz_format_grade($quiz, 0.12345678), '0');
  61      }
  62  
  63      public function test_quiz_get_grade_format() {
  64          $quiz = new \stdClass();
  65          $quiz->decimalpoints = 2;
  66          $this->assertEquals(quiz_get_grade_format($quiz), 2);
  67          $this->assertEquals($quiz->questiondecimalpoints, -1);
  68          $quiz->questiondecimalpoints = 2;
  69          $this->assertEquals(quiz_get_grade_format($quiz), 2);
  70          $quiz->decimalpoints = 3;
  71          $quiz->questiondecimalpoints = -1;
  72          $this->assertEquals(quiz_get_grade_format($quiz), 3);
  73          $quiz->questiondecimalpoints = 4;
  74          $this->assertEquals(quiz_get_grade_format($quiz), 4);
  75      }
  76  
  77      public function test_quiz_format_question_grade() {
  78          $quiz = new \stdClass();
  79          $quiz->decimalpoints = 2;
  80          $quiz->questiondecimalpoints = 2;
  81          $this->assertEquals(quiz_format_question_grade($quiz, 0.12345678), format_float(0.12, 2));
  82          $this->assertEquals(quiz_format_question_grade($quiz, 0), format_float(0, 2));
  83          $this->assertEquals(quiz_format_question_grade($quiz, 1.000000000000), format_float(1, 2));
  84          $quiz->decimalpoints = 3;
  85          $quiz->questiondecimalpoints = -1;
  86          $this->assertEquals(quiz_format_question_grade($quiz, 0.12345678), format_float(0.123, 3));
  87          $this->assertEquals(quiz_format_question_grade($quiz, 0), format_float(0, 3));
  88          $this->assertEquals(quiz_format_question_grade($quiz, 1.000000000000), format_float(1, 3));
  89          $quiz->questiondecimalpoints = 4;
  90          $this->assertEquals(quiz_format_question_grade($quiz, 0.12345678), format_float(0.1235, 4));
  91          $this->assertEquals(quiz_format_question_grade($quiz, 0), format_float(0, 4));
  92          $this->assertEquals(quiz_format_question_grade($quiz, 1.000000000000), format_float(1, 4));
  93      }
  94  
  95      /**
  96       * Test deleting a quiz instance.
  97       */
  98      public function test_quiz_delete_instance() {
  99          global $SITE, $DB;
 100          $this->resetAfterTest(true);
 101          $this->setAdminUser();
 102  
 103          // Setup a quiz with 1 standard and 1 random question.
 104          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 105          $quiz = $quizgenerator->create_instance(array('course' => $SITE->id, 'questionsperpage' => 3, 'grade' => 100.0));
 106  
 107          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 108          $cat = $questiongenerator->create_question_category();
 109          $standardq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 110  
 111          quiz_add_quiz_question($standardq->id, $quiz);
 112          quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
 113  
 114          // Get the random question.
 115          $randomq = $DB->get_record('question', array('qtype' => 'random'));
 116  
 117          quiz_delete_instance($quiz->id);
 118  
 119          // Check that the random question was deleted.
 120          $count = $DB->count_records('question', array('id' => $randomq->id));
 121          $this->assertEquals(0, $count);
 122          // Check that the standard question was not deleted.
 123          $count = $DB->count_records('question', array('id' => $standardq->id));
 124          $this->assertEquals(1, $count);
 125  
 126          // Check that all the slots were removed.
 127          $count = $DB->count_records('quiz_slots', array('quizid' => $quiz->id));
 128          $this->assertEquals(0, $count);
 129  
 130          // Check that the quiz was removed.
 131          $count = $DB->count_records('quiz', array('id' => $quiz->id));
 132          $this->assertEquals(0, $count);
 133      }
 134  
 135      /**
 136       * Setup function for all test_quiz_get_completion_state_* tests.
 137       *
 138       * @param array $completionoptions ['nbstudents'] => int, ['qtype'] => string, ['quizoptions'] => array
 139       * @throws dml_exception
 140       * @return array [$course, $students, $quiz, $cm]
 141       */
 142      private function setup_quiz_for_testing_completion(array $completionoptions) {
 143          global $CFG, $DB;
 144  
 145          $this->resetAfterTest(true);
 146  
 147          // Enable completion before creating modules, otherwise the completion data is not written in DB.
 148          $CFG->enablecompletion = true;
 149  
 150          // Create a course and students.
 151          $studentrole = $DB->get_record('role', ['shortname' => 'student']);
 152          $course = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
 153          $students = [];
 154          for ($i = 0; $i < $completionoptions['nbstudents']; $i++) {
 155              $students[$i] = $this->getDataGenerator()->create_user();
 156              $this->assertTrue($this->getDataGenerator()->enrol_user($students[$i]->id, $course->id, $studentrole->id));
 157          }
 158  
 159          // Make a quiz.
 160          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 161          $data = array_merge([
 162              'course' => $course->id,
 163              'grade' => 100.0,
 164              'questionsperpage' => 0,
 165              'sumgrades' => 1,
 166              'completion' => COMPLETION_TRACKING_AUTOMATIC
 167          ], $completionoptions['quizoptions']);
 168          $quiz = $quizgenerator->create_instance($data);
 169          $cm = get_coursemodule_from_id('quiz', $quiz->cmid);
 170  
 171          // Create a question.
 172          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 173  
 174          $cat = $questiongenerator->create_question_category();
 175          $question = $questiongenerator->create_question($completionoptions['qtype'], null, ['category' => $cat->id]);
 176          quiz_add_quiz_question($question->id, $quiz);
 177  
 178          // Set grade to pass.
 179          $item = \grade_item::fetch(['courseid' => $course->id, 'itemtype' => 'mod', 'itemmodule' => 'quiz',
 180              'iteminstance' => $quiz->id, 'outcomeid' => null]);
 181          $item->gradepass = 80;
 182          $item->update();
 183  
 184          return [
 185              $course,
 186              $students,
 187              $quiz,
 188              $cm
 189          ];
 190      }
 191  
 192      /**
 193       * Helper function for all test_quiz_get_completion_state_* tests.
 194       * Starts an attempt, processes responses and finishes the attempt.
 195       *
 196       * @param $attemptoptions ['quiz'] => object, ['student'] => object, ['tosubmit'] => array, ['attemptnumber'] => int
 197       */
 198      private function do_attempt_quiz($attemptoptions) {
 199          $quizobj = quiz::create($attemptoptions['quiz']->id);
 200  
 201          // Start the passing attempt.
 202          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 203          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 204  
 205          $timenow = time();
 206          $attempt = quiz_create_attempt($quizobj, $attemptoptions['attemptnumber'], false, $timenow, false,
 207              $attemptoptions['student']->id);
 208          quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptoptions['attemptnumber'], $timenow);
 209          quiz_attempt_save_started($quizobj, $quba, $attempt);
 210  
 211          // Process responses from the student.
 212          $attemptobj = quiz_attempt::create($attempt->id);
 213          $attemptobj->process_submitted_actions($timenow, false, $attemptoptions['tosubmit']);
 214  
 215          // Finish the attempt.
 216          $attemptobj = quiz_attempt::create($attempt->id);
 217          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 218          $attemptobj->process_finish($timenow, false);
 219      }
 220  
 221      /**
 222       * Test checking the completion state of a quiz.
 223       * The quiz requires a passing grade to be completed.
 224       */
 225      public function test_quiz_get_completion_state_completionpass() {
 226  
 227          list($course, $students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
 228              'nbstudents' => 2,
 229              'qtype' => 'numerical',
 230              'quizoptions' => [
 231                  'completionusegrade' => 1,
 232                  'completionpass' => 1
 233              ]
 234          ]);
 235  
 236          list($passstudent, $failstudent) = $students;
 237  
 238          // Do a passing attempt.
 239          $this->do_attempt_quiz([
 240             'quiz' => $quiz,
 241             'student' => $passstudent,
 242             'attemptnumber' => 1,
 243             'tosubmit' => [1 => ['answer' => '3.14']]
 244          ]);
 245  
 246          // Check the results.
 247          $this->assertTrue(quiz_get_completion_state($course, $cm, $passstudent->id, 'return'));
 248  
 249          // Do a failing attempt.
 250          $this->do_attempt_quiz([
 251              'quiz' => $quiz,
 252              'student' => $failstudent,
 253              'attemptnumber' => 1,
 254              'tosubmit' => [1 => ['answer' => '0']]
 255          ]);
 256  
 257          // Check the results.
 258          $this->assertFalse(quiz_get_completion_state($course, $cm, $failstudent->id, 'return'));
 259  
 260          $this->assertDebuggingCalledCount(3, [
 261              'quiz_completion_check_passing_grade_or_all_attempts has been deprecated.',
 262              'quiz_completion_check_min_attempts has been deprecated.',
 263              'quiz_completion_check_passing_grade_or_all_attempts has been deprecated.',
 264          ]);
 265      }
 266  
 267      /**
 268       * Test checking the completion state of a quiz.
 269       * To be completed, this quiz requires either a passing grade or for all attempts to be used up.
 270       */
 271      public function test_quiz_get_completion_state_completionexhausted() {
 272  
 273          list($course, $students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
 274              'nbstudents' => 2,
 275              'qtype' => 'numerical',
 276              'quizoptions' => [
 277                  'attempts' => 2,
 278                  'completionusegrade' => 1,
 279                  'completionpass' => 1,
 280                  'completionattemptsexhausted' => 1
 281              ]
 282          ]);
 283  
 284          list($passstudent, $exhauststudent) = $students;
 285  
 286          // Start a passing attempt.
 287          $this->do_attempt_quiz([
 288              'quiz' => $quiz,
 289              'student' => $passstudent,
 290              'attemptnumber' => 1,
 291              'tosubmit' => [1 => ['answer' => '3.14']]
 292          ]);
 293  
 294          // Check the results. Quiz is completed by $passstudent because of passing grade.
 295          $this->assertTrue(quiz_get_completion_state($course, $cm, $passstudent->id, 'return'));
 296  
 297          // Do a failing attempt.
 298          $this->do_attempt_quiz([
 299              'quiz' => $quiz,
 300              'student' => $exhauststudent,
 301              'attemptnumber' => 1,
 302              'tosubmit' => [1 => ['answer' => '0']]
 303          ]);
 304  
 305          // Check the results. Quiz is not completed by $exhauststudent yet because of failing grade and of remaining attempts.
 306          $this->assertFalse(quiz_get_completion_state($course, $cm, $exhauststudent->id, 'return'));
 307  
 308          // Do a second failing attempt.
 309          $this->do_attempt_quiz([
 310              'quiz' => $quiz,
 311              'student' => $exhauststudent,
 312              'attemptnumber' => 2,
 313              'tosubmit' => [1 => ['answer' => '0']]
 314          ]);
 315  
 316          // Check the results. Quiz is completed by $exhauststudent because there are no remaining attempts.
 317          $this->assertTrue(quiz_get_completion_state($course, $cm, $exhauststudent->id, 'return'));
 318  
 319          $this->assertDebuggingCalledCount(5, [
 320              'quiz_completion_check_passing_grade_or_all_attempts has been deprecated.',
 321              'quiz_completion_check_min_attempts has been deprecated.',
 322              'quiz_completion_check_passing_grade_or_all_attempts has been deprecated.',
 323              'quiz_completion_check_passing_grade_or_all_attempts has been deprecated.',
 324              'quiz_completion_check_min_attempts has been deprecated.',
 325          ]);
 326      }
 327  
 328      /**
 329       * Test checking the completion state of a quiz.
 330       * To be completed, this quiz requires a minimum number of attempts.
 331       */
 332      public function test_quiz_get_completion_state_completionminattempts() {
 333  
 334          list($course, $students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
 335              'nbstudents' => 1,
 336              'qtype' => 'essay',
 337              'quizoptions' => [
 338                  'completionminattemptsenabled' => 1,
 339                  'completionminattempts' => 2
 340              ]
 341          ]);
 342  
 343          list($student) = $students;
 344  
 345          // Do a first attempt.
 346          $this->do_attempt_quiz([
 347              'quiz' => $quiz,
 348              'student' => $student,
 349              'attemptnumber' => 1,
 350              'tosubmit' => [1 => ['answer' => 'Lorem ipsum.', 'answerformat' => '1']]
 351          ]);
 352  
 353          // Check the results. Quiz is not completed yet because only one attempt was done.
 354          $this->assertFalse(quiz_get_completion_state($course, $cm, $student->id, 'return'));
 355  
 356          // Do a second attempt.
 357          $this->do_attempt_quiz([
 358              'quiz' => $quiz,
 359              'student' => $student,
 360              'attemptnumber' => 2,
 361              'tosubmit' => [1 => ['answer' => 'Lorem ipsum.', 'answerformat' => '1']]
 362          ]);
 363  
 364          // Check the results. Quiz is completed by $student because two attempts were done.
 365          $this->assertTrue(quiz_get_completion_state($course, $cm, $student->id, 'return'));
 366  
 367          $this->assertDebuggingCalledCount(4, [
 368              'quiz_completion_check_passing_grade_or_all_attempts has been deprecated.',
 369              'quiz_completion_check_min_attempts has been deprecated.',
 370              'quiz_completion_check_passing_grade_or_all_attempts has been deprecated.',
 371              'quiz_completion_check_min_attempts has been deprecated.',
 372          ]);
 373      }
 374  
 375      /**
 376       * Test checking the completion state of a quiz.
 377       * To be completed, this quiz requires a minimum number of attempts AND a passing grade.
 378       * This is somewhat of an edge case as it is hard to imagine a scenario in which these precise settings are useful.
 379       * Nevertheless, this test makes sure these settings interact as intended.
 380       */
 381      public function  test_quiz_get_completion_state_completionminattempts_pass() {
 382  
 383          list($course, $students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
 384              'nbstudents' => 1,
 385              'qtype' => 'numerical',
 386              'quizoptions' => [
 387                  'attempts' => 2,
 388                  'completionusegrade' => 1,
 389                  'completionpass' => 1,
 390                  'completionminattemptsenabled' => 1,
 391                  'completionminattempts' => 2
 392              ]
 393          ]);
 394  
 395          list($student) = $students;
 396  
 397          // Start a first attempt.
 398          $this->do_attempt_quiz([
 399              'quiz' => $quiz,
 400              'student' => $student,
 401              'attemptnumber' => 1,
 402              'tosubmit' => [1 => ['answer' => '3.14']]
 403          ]);
 404  
 405          // Check the results. Even though one requirement is met (passing grade) quiz is not completed yet because only
 406          // one attempt was done.
 407          $this->assertFalse(quiz_get_completion_state($course, $cm, $student->id, 'return'));
 408  
 409          // Start a second attempt.
 410          $this->do_attempt_quiz([
 411              'quiz' => $quiz,
 412              'student' => $student,
 413              'attemptnumber' => 2,
 414              'tosubmit' => [1 => ['answer' => '42']]
 415          ]);
 416  
 417          // Check the results. Quiz is completed by $student because two attempts were done AND a passing grade was obtained.
 418          $this->assertTrue(quiz_get_completion_state($course, $cm, $student->id, 'return'));
 419  
 420          $this->assertDebuggingCalledCount(4, [
 421              'quiz_completion_check_passing_grade_or_all_attempts has been deprecated.',
 422              'quiz_completion_check_min_attempts has been deprecated.',
 423              'quiz_completion_check_passing_grade_or_all_attempts has been deprecated.',
 424              'quiz_completion_check_min_attempts has been deprecated.',
 425          ]);
 426      }
 427  
 428      public function test_quiz_get_user_attempts() {
 429          global $DB;
 430          $this->resetAfterTest();
 431  
 432          $dg = $this->getDataGenerator();
 433          $quizgen = $dg->get_plugin_generator('mod_quiz');
 434          $course = $dg->create_course();
 435          $u1 = $dg->create_user();
 436          $u2 = $dg->create_user();
 437          $u3 = $dg->create_user();
 438          $u4 = $dg->create_user();
 439          $role = $DB->get_record('role', ['shortname' => 'student']);
 440  
 441          $dg->enrol_user($u1->id, $course->id, $role->id);
 442          $dg->enrol_user($u2->id, $course->id, $role->id);
 443          $dg->enrol_user($u3->id, $course->id, $role->id);
 444          $dg->enrol_user($u4->id, $course->id, $role->id);
 445  
 446          $quiz1 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
 447          $quiz2 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
 448  
 449          // Questions.
 450          $questgen = $dg->get_plugin_generator('core_question');
 451          $quizcat = $questgen->create_question_category();
 452          $question = $questgen->create_question('numerical', null, ['category' => $quizcat->id]);
 453          quiz_add_quiz_question($question->id, $quiz1);
 454          quiz_add_quiz_question($question->id, $quiz2);
 455  
 456          $quizobj1a = quiz::create($quiz1->id, $u1->id);
 457          $quizobj1b = quiz::create($quiz1->id, $u2->id);
 458          $quizobj1c = quiz::create($quiz1->id, $u3->id);
 459          $quizobj1d = quiz::create($quiz1->id, $u4->id);
 460          $quizobj2a = quiz::create($quiz2->id, $u1->id);
 461  
 462          // Set attempts.
 463          $quba1a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1a->get_context());
 464          $quba1a->set_preferred_behaviour($quizobj1a->get_quiz()->preferredbehaviour);
 465          $quba1b = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1b->get_context());
 466          $quba1b->set_preferred_behaviour($quizobj1b->get_quiz()->preferredbehaviour);
 467          $quba1c = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1c->get_context());
 468          $quba1c->set_preferred_behaviour($quizobj1c->get_quiz()->preferredbehaviour);
 469          $quba1d = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1d->get_context());
 470          $quba1d->set_preferred_behaviour($quizobj1d->get_quiz()->preferredbehaviour);
 471          $quba2a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
 472          $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
 473  
 474          $timenow = time();
 475  
 476          // User 1 passes quiz 1.
 477          $attempt = quiz_create_attempt($quizobj1a, 1, false, $timenow, false, $u1->id);
 478          quiz_start_new_attempt($quizobj1a, $quba1a, $attempt, 1, $timenow);
 479          quiz_attempt_save_started($quizobj1a, $quba1a, $attempt);
 480          $attemptobj = quiz_attempt::create($attempt->id);
 481          $attemptobj->process_submitted_actions($timenow, false, [1 => ['answer' => '3.14']]);
 482          $attemptobj->process_finish($timenow, false);
 483  
 484          // User 2 goes overdue in quiz 1.
 485          $attempt = quiz_create_attempt($quizobj1b, 1, false, $timenow, false, $u2->id);
 486          quiz_start_new_attempt($quizobj1b, $quba1b, $attempt, 1, $timenow);
 487          quiz_attempt_save_started($quizobj1b, $quba1b, $attempt);
 488          $attemptobj = quiz_attempt::create($attempt->id);
 489          $attemptobj->process_going_overdue($timenow, true);
 490  
 491          // User 3 does not finish quiz 1.
 492          $attempt = quiz_create_attempt($quizobj1c, 1, false, $timenow, false, $u3->id);
 493          quiz_start_new_attempt($quizobj1c, $quba1c, $attempt, 1, $timenow);
 494          quiz_attempt_save_started($quizobj1c, $quba1c, $attempt);
 495  
 496          // User 4 abandons the quiz 1.
 497          $attempt = quiz_create_attempt($quizobj1d, 1, false, $timenow, false, $u4->id);
 498          quiz_start_new_attempt($quizobj1d, $quba1d, $attempt, 1, $timenow);
 499          quiz_attempt_save_started($quizobj1d, $quba1d, $attempt);
 500          $attemptobj = quiz_attempt::create($attempt->id);
 501          $attemptobj->process_abandon($timenow, true);
 502  
 503          // User 1 attempts the quiz three times (abandon, finish, in progress).
 504          $quba2a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
 505          $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
 506  
 507          $attempt = quiz_create_attempt($quizobj2a, 1, false, $timenow, false, $u1->id);
 508          quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 1, $timenow);
 509          quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
 510          $attemptobj = quiz_attempt::create($attempt->id);
 511          $attemptobj->process_abandon($timenow, true);
 512  
 513          $quba2a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
 514          $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
 515  
 516          $attempt = quiz_create_attempt($quizobj2a, 2, false, $timenow, false, $u1->id);
 517          quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 2, $timenow);
 518          quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
 519          $attemptobj = quiz_attempt::create($attempt->id);
 520          $attemptobj->process_finish($timenow, false);
 521  
 522          $quba2a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
 523          $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
 524  
 525          $attempt = quiz_create_attempt($quizobj2a, 3, false, $timenow, false, $u1->id);
 526          quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 3, $timenow);
 527          quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
 528  
 529          // Check for user 1.
 530          $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'all');
 531          $this->assertCount(1, $attempts);
 532          $attempt = array_shift($attempts);
 533          $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
 534          $this->assertEquals($u1->id, $attempt->userid);
 535          $this->assertEquals($quiz1->id, $attempt->quiz);
 536  
 537          $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'finished');
 538          $this->assertCount(1, $attempts);
 539          $attempt = array_shift($attempts);
 540          $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
 541          $this->assertEquals($u1->id, $attempt->userid);
 542          $this->assertEquals($quiz1->id, $attempt->quiz);
 543  
 544          $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'unfinished');
 545          $this->assertCount(0, $attempts);
 546  
 547          // Check for user 2.
 548          $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'all');
 549          $this->assertCount(1, $attempts);
 550          $attempt = array_shift($attempts);
 551          $this->assertEquals(quiz_attempt::OVERDUE, $attempt->state);
 552          $this->assertEquals($u2->id, $attempt->userid);
 553          $this->assertEquals($quiz1->id, $attempt->quiz);
 554  
 555          $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'finished');
 556          $this->assertCount(0, $attempts);
 557  
 558          $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'unfinished');
 559          $this->assertCount(1, $attempts);
 560          $attempt = array_shift($attempts);
 561          $this->assertEquals(quiz_attempt::OVERDUE, $attempt->state);
 562          $this->assertEquals($u2->id, $attempt->userid);
 563          $this->assertEquals($quiz1->id, $attempt->quiz);
 564  
 565          // Check for user 3.
 566          $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'all');
 567          $this->assertCount(1, $attempts);
 568          $attempt = array_shift($attempts);
 569          $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
 570          $this->assertEquals($u3->id, $attempt->userid);
 571          $this->assertEquals($quiz1->id, $attempt->quiz);
 572  
 573          $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'finished');
 574          $this->assertCount(0, $attempts);
 575  
 576          $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'unfinished');
 577          $this->assertCount(1, $attempts);
 578          $attempt = array_shift($attempts);
 579          $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
 580          $this->assertEquals($u3->id, $attempt->userid);
 581          $this->assertEquals($quiz1->id, $attempt->quiz);
 582  
 583          // Check for user 4.
 584          $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'all');
 585          $this->assertCount(1, $attempts);
 586          $attempt = array_shift($attempts);
 587          $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
 588          $this->assertEquals($u4->id, $attempt->userid);
 589          $this->assertEquals($quiz1->id, $attempt->quiz);
 590  
 591          $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'finished');
 592          $this->assertCount(1, $attempts);
 593          $attempt = array_shift($attempts);
 594          $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
 595          $this->assertEquals($u4->id, $attempt->userid);
 596          $this->assertEquals($quiz1->id, $attempt->quiz);
 597  
 598          $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'unfinished');
 599          $this->assertCount(0, $attempts);
 600  
 601          // Multiple attempts for user 1 in quiz 2.
 602          $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'all');
 603          $this->assertCount(3, $attempts);
 604          $attempt = array_shift($attempts);
 605          $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
 606          $this->assertEquals($u1->id, $attempt->userid);
 607          $this->assertEquals($quiz2->id, $attempt->quiz);
 608          $attempt = array_shift($attempts);
 609          $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
 610          $this->assertEquals($u1->id, $attempt->userid);
 611          $this->assertEquals($quiz2->id, $attempt->quiz);
 612          $attempt = array_shift($attempts);
 613          $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
 614          $this->assertEquals($u1->id, $attempt->userid);
 615          $this->assertEquals($quiz2->id, $attempt->quiz);
 616  
 617          $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'finished');
 618          $this->assertCount(2, $attempts);
 619          $attempt = array_shift($attempts);
 620          $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
 621          $attempt = array_shift($attempts);
 622          $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
 623  
 624          $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'unfinished');
 625          $this->assertCount(1, $attempts);
 626          $attempt = array_shift($attempts);
 627  
 628          // Multiple quiz attempts fetched at once.
 629          $attempts = quiz_get_user_attempts([$quiz1->id, $quiz2->id], $u1->id, 'all');
 630          $this->assertCount(4, $attempts);
 631          $attempt = array_shift($attempts);
 632          $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
 633          $this->assertEquals($u1->id, $attempt->userid);
 634          $this->assertEquals($quiz1->id, $attempt->quiz);
 635          $attempt = array_shift($attempts);
 636          $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
 637          $this->assertEquals($u1->id, $attempt->userid);
 638          $this->assertEquals($quiz2->id, $attempt->quiz);
 639          $attempt = array_shift($attempts);
 640          $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
 641          $this->assertEquals($u1->id, $attempt->userid);
 642          $this->assertEquals($quiz2->id, $attempt->quiz);
 643          $attempt = array_shift($attempts);
 644          $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
 645          $this->assertEquals($u1->id, $attempt->userid);
 646          $this->assertEquals($quiz2->id, $attempt->quiz);
 647      }
 648  
 649      /**
 650       * Test for quiz_get_group_override_priorities().
 651       */
 652      public function test_quiz_get_group_override_priorities() {
 653          global $DB;
 654          $this->resetAfterTest();
 655  
 656          $dg = $this->getDataGenerator();
 657          $quizgen = $dg->get_plugin_generator('mod_quiz');
 658          $course = $dg->create_course();
 659  
 660          $quiz = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
 661  
 662          $this->assertNull(quiz_get_group_override_priorities($quiz->id));
 663  
 664          $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
 665          $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
 666  
 667          $now = 100;
 668          $override1 = (object)[
 669              'quiz' => $quiz->id,
 670              'groupid' => $group1->id,
 671              'timeopen' => $now,
 672              'timeclose' => $now + 20
 673          ];
 674          $DB->insert_record('quiz_overrides', $override1);
 675  
 676          $override2 = (object)[
 677              'quiz' => $quiz->id,
 678              'groupid' => $group2->id,
 679              'timeopen' => $now - 10,
 680              'timeclose' => $now + 10
 681          ];
 682          $DB->insert_record('quiz_overrides', $override2);
 683  
 684          $priorities = quiz_get_group_override_priorities($quiz->id);
 685          $this->assertNotEmpty($priorities);
 686  
 687          $openpriorities = $priorities['open'];
 688          // Override 2's time open has higher priority since it is sooner than override 1's.
 689          $this->assertEquals(2, $openpriorities[$override1->timeopen]);
 690          $this->assertEquals(1, $openpriorities[$override2->timeopen]);
 691  
 692          $closepriorities = $priorities['close'];
 693          // Override 1's time close has higher priority since it is later than override 2's.
 694          $this->assertEquals(1, $closepriorities[$override1->timeclose]);
 695          $this->assertEquals(2, $closepriorities[$override2->timeclose]);
 696      }
 697  
 698      public function test_quiz_core_calendar_provide_event_action_open() {
 699          $this->resetAfterTest();
 700  
 701          $this->setAdminUser();
 702  
 703          // Create a course.
 704          $course = $this->getDataGenerator()->create_course();
 705          // Create a student and enrol into the course.
 706          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 707          // Create a quiz.
 708          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id,
 709              'timeopen' => time() - DAYSECS, 'timeclose' => time() + DAYSECS));
 710  
 711          // Create a calendar event.
 712          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
 713          // Now, log in as student.
 714          $this->setUser($student);
 715          // Create an action factory.
 716          $factory = new \core_calendar\action_factory();
 717  
 718          // Decorate action event.
 719          $actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory);
 720  
 721          // Confirm the event was decorated.
 722          $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
 723          $this->assertEquals(get_string('attemptquiznow', 'quiz'), $actionevent->get_name());
 724          $this->assertInstanceOf('moodle_url', $actionevent->get_url());
 725          $this->assertEquals(1, $actionevent->get_item_count());
 726          $this->assertTrue($actionevent->is_actionable());
 727      }
 728  
 729      public function test_quiz_core_calendar_provide_event_action_open_for_user() {
 730          $this->resetAfterTest();
 731  
 732          $this->setAdminUser();
 733  
 734          // Create a course.
 735          $course = $this->getDataGenerator()->create_course();
 736          // Create a student and enrol into the course.
 737          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 738          // Create a quiz.
 739          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id,
 740              'timeopen' => time() - DAYSECS, 'timeclose' => time() + DAYSECS));
 741  
 742          // Create a calendar event.
 743          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
 744  
 745          // Create an action factory.
 746          $factory = new \core_calendar\action_factory();
 747  
 748          // Decorate action event for the student.
 749          $actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id);
 750  
 751          // Confirm the event was decorated.
 752          $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
 753          $this->assertEquals(get_string('attemptquiznow', 'quiz'), $actionevent->get_name());
 754          $this->assertInstanceOf('moodle_url', $actionevent->get_url());
 755          $this->assertEquals(1, $actionevent->get_item_count());
 756          $this->assertTrue($actionevent->is_actionable());
 757      }
 758  
 759      public function test_quiz_core_calendar_provide_event_action_closed() {
 760          $this->resetAfterTest();
 761  
 762          $this->setAdminUser();
 763  
 764          // Create a course.
 765          $course = $this->getDataGenerator()->create_course();
 766  
 767          // Create a quiz.
 768          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id,
 769              'timeclose' => time() - DAYSECS));
 770  
 771          // Create a calendar event.
 772          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_CLOSE);
 773  
 774          // Create an action factory.
 775          $factory = new \core_calendar\action_factory();
 776  
 777          // Confirm the result was null.
 778          $this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory));
 779      }
 780  
 781      public function test_quiz_core_calendar_provide_event_action_closed_for_user() {
 782          $this->resetAfterTest();
 783  
 784          $this->setAdminUser();
 785  
 786          // Create a course.
 787          $course = $this->getDataGenerator()->create_course();
 788  
 789          // Create a student.
 790          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 791  
 792          // Create a quiz.
 793          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id,
 794              'timeclose' => time() - DAYSECS));
 795  
 796          // Create a calendar event.
 797          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_CLOSE);
 798  
 799          // Create an action factory.
 800          $factory = new \core_calendar\action_factory();
 801  
 802          // Confirm the result was null.
 803          $this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id));
 804      }
 805  
 806      public function test_quiz_core_calendar_provide_event_action_open_in_future() {
 807          $this->resetAfterTest();
 808  
 809          $this->setAdminUser();
 810  
 811          // Create a course.
 812          $course = $this->getDataGenerator()->create_course();
 813          // Create a student and enrol into the course.
 814          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 815          // Create a quiz.
 816          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id,
 817              'timeopen' => time() + DAYSECS));
 818  
 819          // Create a calendar event.
 820          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_CLOSE);
 821          // Now, log in as student.
 822          $this->setUser($student);
 823          // Create an action factory.
 824          $factory = new \core_calendar\action_factory();
 825  
 826          // Decorate action event.
 827          $actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory);
 828  
 829          // Confirm the event was decorated.
 830          $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
 831          $this->assertEquals(get_string('attemptquiznow', 'quiz'), $actionevent->get_name());
 832          $this->assertInstanceOf('moodle_url', $actionevent->get_url());
 833          $this->assertEquals(1, $actionevent->get_item_count());
 834          $this->assertFalse($actionevent->is_actionable());
 835      }
 836  
 837      public function test_quiz_core_calendar_provide_event_action_open_in_future_for_user() {
 838          $this->resetAfterTest();
 839  
 840          $this->setAdminUser();
 841  
 842          // Create a course.
 843          $course = $this->getDataGenerator()->create_course();
 844          // Create a student and enrol into the course.
 845          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 846          // Create a quiz.
 847          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id,
 848              'timeopen' => time() + DAYSECS));
 849  
 850          // Create a calendar event.
 851          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_CLOSE);
 852  
 853          // Create an action factory.
 854          $factory = new \core_calendar\action_factory();
 855  
 856          // Decorate action event for the student.
 857          $actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id);
 858  
 859          // Confirm the event was decorated.
 860          $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
 861          $this->assertEquals(get_string('attemptquiznow', 'quiz'), $actionevent->get_name());
 862          $this->assertInstanceOf('moodle_url', $actionevent->get_url());
 863          $this->assertEquals(1, $actionevent->get_item_count());
 864          $this->assertFalse($actionevent->is_actionable());
 865      }
 866  
 867      public function test_quiz_core_calendar_provide_event_action_no_capability() {
 868          global $DB;
 869  
 870          $this->resetAfterTest();
 871          $this->setAdminUser();
 872  
 873          // Create a course.
 874          $course = $this->getDataGenerator()->create_course();
 875  
 876          // Create a student.
 877          $student = $this->getDataGenerator()->create_user();
 878          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 879  
 880          // Enrol student.
 881          $this->assertTrue($this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id));
 882  
 883          // Create a quiz.
 884          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
 885  
 886          // Remove the permission to attempt or review the quiz for the student role.
 887          $coursecontext = \context_course::instance($course->id);
 888          assign_capability('mod/quiz:reviewmyattempts', CAP_PROHIBIT, $studentrole->id, $coursecontext);
 889          assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $studentrole->id, $coursecontext);
 890  
 891          // Create a calendar event.
 892          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
 893  
 894          // Create an action factory.
 895          $factory = new \core_calendar\action_factory();
 896  
 897          // Set current user to the student.
 898          $this->setUser($student);
 899  
 900          // Confirm null is returned.
 901          $this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory));
 902      }
 903  
 904      public function test_quiz_core_calendar_provide_event_action_no_capability_for_user() {
 905          global $DB;
 906  
 907          $this->resetAfterTest();
 908          $this->setAdminUser();
 909  
 910          // Create a course.
 911          $course = $this->getDataGenerator()->create_course();
 912  
 913          // Create a student.
 914          $student = $this->getDataGenerator()->create_user();
 915          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 916  
 917          // Enrol student.
 918          $this->assertTrue($this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id));
 919  
 920          // Create a quiz.
 921          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
 922  
 923          // Remove the permission to attempt or review the quiz for the student role.
 924          $coursecontext = \context_course::instance($course->id);
 925          assign_capability('mod/quiz:reviewmyattempts', CAP_PROHIBIT, $studentrole->id, $coursecontext);
 926          assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $studentrole->id, $coursecontext);
 927  
 928          // Create a calendar event.
 929          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
 930  
 931          // Create an action factory.
 932          $factory = new \core_calendar\action_factory();
 933  
 934          // Confirm null is returned.
 935          $this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id));
 936      }
 937  
 938      public function test_quiz_core_calendar_provide_event_action_already_finished() {
 939          global $DB;
 940  
 941          $this->resetAfterTest();
 942  
 943          $this->setAdminUser();
 944  
 945          // Create a course.
 946          $course = $this->getDataGenerator()->create_course();
 947  
 948          // Create a student.
 949          $student = $this->getDataGenerator()->create_user();
 950          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 951  
 952          // Enrol student.
 953          $this->assertTrue($this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id));
 954  
 955          // Create a quiz.
 956          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id,
 957              'sumgrades' => 1));
 958  
 959          // Add a question to the quiz.
 960          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 961          $cat = $questiongenerator->create_question_category();
 962          $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
 963          quiz_add_quiz_question($question->id, $quiz);
 964  
 965          // Get the quiz object.
 966          $quizobj = quiz::create($quiz->id, $student->id);
 967  
 968          // Create an attempt for the student in the quiz.
 969          $timenow = time();
 970          $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $student->id);
 971          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 972          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 973          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 974          quiz_attempt_save_started($quizobj, $quba, $attempt);
 975  
 976          // Finish the attempt.
 977          $attemptobj = quiz_attempt::create($attempt->id);
 978          $attemptobj->process_finish($timenow, false);
 979  
 980          // Create a calendar event.
 981          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
 982  
 983          // Create an action factory.
 984          $factory = new \core_calendar\action_factory();
 985  
 986          // Set current user to the student.
 987          $this->setUser($student);
 988  
 989          // Confirm null is returned.
 990          $this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory));
 991      }
 992  
 993      public function test_quiz_core_calendar_provide_event_action_already_finished_for_user() {
 994          global $DB;
 995  
 996          $this->resetAfterTest();
 997  
 998          $this->setAdminUser();
 999  
1000          // Create a course.
1001          $course = $this->getDataGenerator()->create_course();
1002  
1003          // Create a student.
1004          $student = $this->getDataGenerator()->create_user();
1005          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1006  
1007          // Enrol student.
1008          $this->assertTrue($this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id));
1009  
1010          // Create a quiz.
1011          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id,
1012              'sumgrades' => 1));
1013  
1014          // Add a question to the quiz.
1015          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
1016          $cat = $questiongenerator->create_question_category();
1017          $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
1018          quiz_add_quiz_question($question->id, $quiz);
1019  
1020          // Get the quiz object.
1021          $quizobj = quiz::create($quiz->id, $student->id);
1022  
1023          // Create an attempt for the student in the quiz.
1024          $timenow = time();
1025          $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $student->id);
1026          $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1027          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1028          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
1029          quiz_attempt_save_started($quizobj, $quba, $attempt);
1030  
1031          // Finish the attempt.
1032          $attemptobj = quiz_attempt::create($attempt->id);
1033          $attemptobj->process_finish($timenow, false);
1034  
1035          // Create a calendar event.
1036          $event = $this->create_action_event($course->id, $quiz->id, QUIZ_EVENT_TYPE_OPEN);
1037  
1038          // Create an action factory.
1039          $factory = new \core_calendar\action_factory();
1040  
1041          // Confirm null is returned.
1042          $this->assertNull(mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id));
1043      }
1044  
1045      public function test_quiz_core_calendar_provide_event_action_already_completed() {
1046          $this->resetAfterTest();
1047          set_config('enablecompletion', 1);
1048          $this->setAdminUser();
1049  
1050          // Create the activity.
1051          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1052          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id),
1053              array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS));
1054  
1055          // Get some additional data.
1056          $cm = get_coursemodule_from_instance('quiz', $quiz->id);
1057  
1058          // Create a calendar event.
1059          $event = $this->create_action_event($course->id, $quiz->id,
1060              \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
1061  
1062          // Mark the activity as completed.
1063          $completion = new \completion_info($course);
1064          $completion->set_module_viewed($cm);
1065  
1066          // Create an action factory.
1067          $factory = new \core_calendar\action_factory();
1068  
1069          // Decorate action event.
1070          $actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory);
1071  
1072          // Ensure result was null.
1073          $this->assertNull($actionevent);
1074      }
1075  
1076      public function test_quiz_core_calendar_provide_event_action_already_completed_for_user() {
1077          $this->resetAfterTest();
1078          set_config('enablecompletion', 1);
1079          $this->setAdminUser();
1080  
1081          // Create the activity.
1082          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1083          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id),
1084              array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS));
1085  
1086          // Enrol a student in the course.
1087          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1088  
1089          // Get some additional data.
1090          $cm = get_coursemodule_from_instance('quiz', $quiz->id);
1091  
1092          // Create a calendar event.
1093          $event = $this->create_action_event($course->id, $quiz->id,
1094              \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
1095  
1096          // Mark the activity as completed for the student.
1097          $completion = new \completion_info($course);
1098          $completion->set_module_viewed($cm, $student->id);
1099  
1100          // Create an action factory.
1101          $factory = new \core_calendar\action_factory();
1102  
1103          // Decorate action event for the student.
1104          $actionevent = mod_quiz_core_calendar_provide_event_action($event, $factory, $student->id);
1105  
1106          // Ensure result was null.
1107          $this->assertNull($actionevent);
1108      }
1109  
1110      /**
1111       * Creates an action event.
1112       *
1113       * @param int $courseid
1114       * @param int $instanceid The quiz id.
1115       * @param string $eventtype The event type. eg. QUIZ_EVENT_TYPE_OPEN.
1116       * @return bool|calendar_event
1117       */
1118      private function create_action_event($courseid, $instanceid, $eventtype) {
1119          $event = new \stdClass();
1120          $event->name = 'Calendar event';
1121          $event->modulename  = 'quiz';
1122          $event->courseid = $courseid;
1123          $event->instance = $instanceid;
1124          $event->type = CALENDAR_EVENT_TYPE_ACTION;
1125          $event->eventtype = $eventtype;
1126          $event->timestart = time();
1127  
1128          return \calendar_event::create($event);
1129      }
1130  
1131      /**
1132       * Test the callback responsible for returning the completion rule descriptions.
1133       * This function should work given either an instance of the module (cm_info), such as when checking the active rules,
1134       * or if passed a stdClass of similar structure, such as when checking the the default completion settings for a mod type.
1135       */
1136      public function test_mod_quiz_completion_get_active_rule_descriptions() {
1137          $this->resetAfterTest();
1138          $this->setAdminUser();
1139  
1140          // Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't.
1141          $course = $this->getDataGenerator()->create_course(['enablecompletion' => 2]);
1142          $quiz1 = $this->getDataGenerator()->create_module('quiz', [
1143              'course' => $course->id,
1144              'completion' => 2,
1145              'completionusegrade' => 1,
1146              'completionattemptsexhausted' => 1,
1147              'completionpass' => 1
1148          ]);
1149          $quiz2 = $this->getDataGenerator()->create_module('quiz', [
1150              'course' => $course->id,
1151              'completion' => 2,
1152              'completionusegrade' => 0
1153          ]);
1154          $cm1 = \cm_info::create(get_coursemodule_from_instance('quiz', $quiz1->id));
1155          $cm2 = \cm_info::create(get_coursemodule_from_instance('quiz', $quiz2->id));
1156  
1157          // Data for the stdClass input type.
1158          // This type of input would occur when checking the default completion rules for an activity type, where we don't have
1159          // any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info.
1160          $moddefaults = new \stdClass();
1161          $moddefaults->customdata = ['customcompletionrules' => [
1162              'completionattemptsexhausted' => 1,
1163              'completionpass' => 1
1164          ]];
1165          $moddefaults->completion = 2;
1166  
1167          $activeruledescriptions = [
1168              get_string('completionpassorattemptsexhausteddesc', 'quiz'),
1169          ];
1170          $this->assertEquals(mod_quiz_get_completion_active_rule_descriptions($cm1), $activeruledescriptions);
1171          $this->assertEquals(mod_quiz_get_completion_active_rule_descriptions($cm2), []);
1172          $this->assertEquals(mod_quiz_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
1173          $this->assertEquals(mod_quiz_get_completion_active_rule_descriptions(new \stdClass()), []);
1174      }
1175  
1176      /**
1177       * A user who does not have capabilities to add events to the calendar should be able to create a quiz.
1178       */
1179      public function test_creation_with_no_calendar_capabilities() {
1180          $this->resetAfterTest();
1181          $course = self::getDataGenerator()->create_course();
1182          $context = \context_course::instance($course->id);
1183          $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
1184          $roleid = self::getDataGenerator()->create_role();
1185          self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
1186          assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
1187          $generator = self::getDataGenerator()->get_plugin_generator('mod_quiz');
1188          // Create an instance as a user without the calendar capabilities.
1189          $this->setUser($user);
1190          $time = time();
1191          $params = array(
1192              'course' => $course->id,
1193              'timeopen' => $time + 200,
1194              'timeclose' => $time + 2000,
1195          );
1196          $generator->create_instance($params);
1197      }
1198  }