Search moodle.org's
Developer Documentation

  • 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 37 and 311] [Versions 38 and 311] [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  }