Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace mod_quiz;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  global $CFG;
  22  require_once($CFG->dirroot . '/mod/quiz/lib.php');
  23  
  24  /**
  25   * Unit tests for the calendar event modification callbacks used
  26   * for dragging and dropping quiz calendar events in the calendar
  27   * UI.
  28   *
  29   * @package    mod_quiz
  30   * @category   test
  31   * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
  32   * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
  33   */
  34  class calendar_event_modified_test extends \advanced_testcase {
  35  
  36      /**
  37       * Create an instance of the quiz activity.
  38       *
  39       * @param array $properties Properties to set on the activity
  40       * @return stdClass Quiz activity instance
  41       */
  42      protected function create_quiz_instance(array $properties) {
  43          global $DB;
  44  
  45          $generator = $this->getDataGenerator();
  46  
  47          if (empty($properties['course'])) {
  48              $course = $generator->create_course();
  49              $courseid = $course->id;
  50          } else {
  51              $courseid = $properties['course'];
  52          }
  53  
  54          $quizgenerator = $generator->get_plugin_generator('mod_quiz');
  55          $quiz = $quizgenerator->create_instance(array_merge(['course' => $courseid], $properties));
  56  
  57          if (isset($properties['timemodified'])) {
  58              // The generator overrides the timemodified value to set it as
  59              // the current time even if a value is provided so we need to
  60              // make sure it's set back to the requested value.
  61              $quiz->timemodified = $properties['timemodified'];
  62              $DB->update_record('quiz', $quiz);
  63          }
  64  
  65          return $quiz;
  66      }
  67  
  68      /**
  69       * Create a calendar event for a quiz activity instance.
  70       *
  71       * @param \stdClass $quiz The activity instance
  72       * @param array $eventproperties Properties to set on the calendar event
  73       * @return calendar_event
  74       */
  75      protected function create_quiz_calendar_event(\stdClass $quiz, array $eventproperties) {
  76          $defaultproperties = [
  77              'name' => 'Test event',
  78              'description' => '',
  79              'format' => 1,
  80              'courseid' => $quiz->course,
  81              'groupid' => 0,
  82              'userid' => 2,
  83              'modulename' => 'quiz',
  84              'instance' => $quiz->id,
  85              'eventtype' => QUIZ_EVENT_TYPE_OPEN,
  86              'timestart' => time(),
  87              'timeduration' => 86400,
  88              'visible' => 1
  89          ];
  90  
  91          return new \calendar_event(array_merge($defaultproperties, $eventproperties));
  92      }
  93  
  94      /**
  95       * An unkown event type should not change the quiz instance.
  96       */
  97      public function test_mod_quiz_core_calendar_event_timestart_updated_unknown_event() {
  98          global $DB;
  99  
 100          $this->resetAfterTest(true);
 101          $this->setAdminUser();
 102          $timeopen = time();
 103          $timeclose = $timeopen + DAYSECS;
 104          $quiz = $this->create_quiz_instance(['timeopen' => $timeopen, 'timeclose' => $timeclose]);
 105          $event = $this->create_quiz_calendar_event($quiz, [
 106              'eventtype' => QUIZ_EVENT_TYPE_OPEN . "SOMETHING ELSE",
 107              'timestart' => 1
 108          ]);
 109  
 110          mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
 111  
 112          $quiz = $DB->get_record('quiz', ['id' => $quiz->id]);
 113          $this->assertEquals($timeopen, $quiz->timeopen);
 114          $this->assertEquals($timeclose, $quiz->timeclose);
 115      }
 116  
 117      /**
 118       * A QUIZ_EVENT_TYPE_OPEN event should update the timeopen property of
 119       * the quiz activity.
 120       */
 121      public function test_mod_quiz_core_calendar_event_timestart_updated_open_event() {
 122          global $DB;
 123  
 124          $this->resetAfterTest(true);
 125          $this->setAdminUser();
 126          $timeopen = time();
 127          $timeclose = $timeopen + DAYSECS;
 128          $timemodified = 1;
 129          $newtimeopen = $timeopen - DAYSECS;
 130          $quiz = $this->create_quiz_instance([
 131              'timeopen' => $timeopen,
 132              'timeclose' => $timeclose,
 133              'timemodified' => $timemodified
 134          ]);
 135          $event = $this->create_quiz_calendar_event($quiz, [
 136              'eventtype' => QUIZ_EVENT_TYPE_OPEN,
 137              'timestart' => $newtimeopen
 138          ]);
 139  
 140          mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
 141  
 142          $quiz = $DB->get_record('quiz', ['id' => $quiz->id]);
 143          // Ensure the timeopen property matches the event timestart.
 144          $this->assertEquals($newtimeopen, $quiz->timeopen);
 145          // Ensure the timeclose isn't changed.
 146          $this->assertEquals($timeclose, $quiz->timeclose);
 147          // Ensure the timemodified property has been changed.
 148          $this->assertNotEquals($timemodified, $quiz->timemodified);
 149      }
 150  
 151      /**
 152       * A QUIZ_EVENT_TYPE_CLOSE event should update the timeclose property of
 153       * the quiz activity.
 154       */
 155      public function test_mod_quiz_core_calendar_event_timestart_updated_close_event() {
 156          global $DB;
 157  
 158          $this->resetAfterTest(true);
 159          $this->setAdminUser();
 160          $timeopen = time();
 161          $timeclose = $timeopen + DAYSECS;
 162          $timemodified = 1;
 163          $newtimeclose = $timeclose + DAYSECS;
 164          $quiz = $this->create_quiz_instance([
 165              'timeopen' => $timeopen,
 166              'timeclose' => $timeclose,
 167              'timemodified' => $timemodified
 168          ]);
 169          $event = $this->create_quiz_calendar_event($quiz, [
 170              'eventtype' => QUIZ_EVENT_TYPE_CLOSE,
 171              'timestart' => $newtimeclose
 172          ]);
 173  
 174          mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
 175  
 176          $quiz = $DB->get_record('quiz', ['id' => $quiz->id]);
 177          // Ensure the timeclose property matches the event timestart.
 178          $this->assertEquals($newtimeclose, $quiz->timeclose);
 179          // Ensure the timeopen isn't changed.
 180          $this->assertEquals($timeopen, $quiz->timeopen);
 181          // Ensure the timemodified property has been changed.
 182          $this->assertNotEquals($timemodified, $quiz->timemodified);
 183      }
 184  
 185      /**
 186       * A QUIZ_EVENT_TYPE_OPEN event should not update the timeopen property of
 187       * the quiz activity if it's an override.
 188       */
 189      public function test_mod_quiz_core_calendar_event_timestart_updated_open_event_override() {
 190          global $DB;
 191  
 192          $this->resetAfterTest(true);
 193          $this->setAdminUser();
 194          $user = $this->getDataGenerator()->create_user();
 195          $timeopen = time();
 196          $timeclose = $timeopen + DAYSECS;
 197          $timemodified = 1;
 198          $newtimeopen = $timeopen - DAYSECS;
 199          $quiz = $this->create_quiz_instance([
 200              'timeopen' => $timeopen,
 201              'timeclose' => $timeclose,
 202              'timemodified' => $timemodified
 203          ]);
 204          $event = $this->create_quiz_calendar_event($quiz, [
 205              'userid' => $user->id,
 206              'eventtype' => QUIZ_EVENT_TYPE_OPEN,
 207              'timestart' => $newtimeopen
 208          ]);
 209          $record = (object) [
 210              'quiz' => $quiz->id,
 211              'userid' => $user->id
 212          ];
 213  
 214          $DB->insert_record('quiz_overrides', $record);
 215  
 216          mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
 217  
 218          $quiz = $DB->get_record('quiz', ['id' => $quiz->id]);
 219          // Ensure the timeopen property doesn't change.
 220          $this->assertEquals($timeopen, $quiz->timeopen);
 221          // Ensure the timeclose isn't changed.
 222          $this->assertEquals($timeclose, $quiz->timeclose);
 223          // Ensure the timemodified property has not been changed.
 224          $this->assertEquals($timemodified, $quiz->timemodified);
 225      }
 226  
 227      /**
 228       * If a student somehow finds a way to update the quiz calendar event
 229       * then the callback should not update the quiz activity otherwise that
 230       * would be a security issue.
 231       */
 232      public function test_student_role_cant_update_quiz_activity() {
 233          global $DB;
 234  
 235          $this->resetAfterTest();
 236          $this->setAdminUser();
 237  
 238          $generator = $this->getDataGenerator();
 239          $user = $generator->create_user();
 240          $course = $generator->create_course();
 241          $context = \context_course::instance($course->id);
 242          $roleid = $generator->create_role();
 243          $now = time();
 244          $timeopen = (new \DateTime())->setTimestamp($now);
 245          $newtimeopen = (new \DateTime())->setTimestamp($now)->modify('+1 day');
 246          $quiz = $this->create_quiz_instance([
 247              'course' => $course->id,
 248              'timeopen' => $timeopen->getTimestamp()
 249          ]);
 250  
 251          $generator->enrol_user($user->id, $course->id, 'student');
 252          $generator->role_assign($roleid, $user->id, $context->id);
 253  
 254          $event = $this->create_quiz_calendar_event($quiz, [
 255              'eventtype' => QUIZ_EVENT_TYPE_OPEN,
 256              'timestart' => $timeopen->getTimestamp()
 257          ]);
 258  
 259          assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleid, $context, true);
 260  
 261          $this->setUser($user);
 262  
 263          mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
 264  
 265          $newquiz = $DB->get_record('quiz', ['id' => $quiz->id]);
 266          // The time open shouldn't have changed even though we updated the calendar
 267          // event.
 268          $this->assertEquals($timeopen->getTimestamp(), $newquiz->timeopen);
 269      }
 270  
 271      /**
 272       * A teacher with the capability to modify a quiz module should be
 273       * able to update the quiz activity dates by changing the calendar
 274       * event.
 275       */
 276      public function test_teacher_role_can_update_quiz_activity() {
 277          global $DB;
 278  
 279          $this->resetAfterTest();
 280          $this->setAdminUser();
 281  
 282          $generator = $this->getDataGenerator();
 283          $user = $generator->create_user();
 284          $course = $generator->create_course();
 285          $context = \context_course::instance($course->id);
 286          $roleid = $generator->create_role();
 287          $now = time();
 288          $timeopen = (new \DateTime())->setTimestamp($now);
 289          $newtimeopen = (new \DateTime())->setTimestamp($now)->modify('+1 day');
 290          $quiz = $this->create_quiz_instance([
 291              'course' => $course->id,
 292              'timeopen' => $timeopen->getTimestamp()
 293          ]);
 294  
 295          $generator->enrol_user($user->id, $course->id, 'teacher');
 296          $generator->role_assign($roleid, $user->id, $context->id);
 297  
 298          $event = $this->create_quiz_calendar_event($quiz, [
 299              'eventtype' => QUIZ_EVENT_TYPE_OPEN,
 300              'timestart' => $newtimeopen->getTimestamp()
 301          ]);
 302  
 303          assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true);
 304  
 305          $this->setUser($user);
 306  
 307          // Trigger and capture the event.
 308          $sink = $this->redirectEvents();
 309  
 310          mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
 311  
 312          $triggeredevents = $sink->get_events();
 313          $moduleupdatedevents = array_filter($triggeredevents, function($e) {
 314              return is_a($e, 'core\event\course_module_updated');
 315          });
 316  
 317          $newquiz = $DB->get_record('quiz', ['id' => $quiz->id]);
 318          // The should be updated along with the event because the user has sufficient
 319          // capabilities.
 320          $this->assertEquals($newtimeopen->getTimestamp(), $newquiz->timeopen);
 321          // Confirm that a module updated event is fired when the module
 322          // is changed.
 323          $this->assertNotEmpty($moduleupdatedevents);
 324      }
 325  
 326  
 327      /**
 328       * An unkown event type should not have any limits
 329       */
 330      public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_unknown_event() {
 331          global $DB;
 332  
 333          $this->resetAfterTest(true);
 334          $this->setAdminUser();
 335          $timeopen = time();
 336          $timeclose = $timeopen + DAYSECS;
 337          $quiz = $this->create_quiz_instance([
 338              'timeopen' => $timeopen,
 339              'timeclose' => $timeclose
 340          ]);
 341          $event = $this->create_quiz_calendar_event($quiz, [
 342              'eventtype' => QUIZ_EVENT_TYPE_OPEN . "SOMETHING ELSE",
 343              'timestart' => 1
 344          ]);
 345  
 346          list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz);
 347          $this->assertNull($min);
 348          $this->assertNull($max);
 349      }
 350  
 351      /**
 352       * The open event should be limited by the quiz's timeclose property, if it's set.
 353       */
 354      public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_open_event() {
 355          global $DB;
 356  
 357          $this->resetAfterTest(true);
 358          $this->setAdminUser();
 359          $timeopen = time();
 360          $timeclose = $timeopen + DAYSECS;
 361          $quiz = $this->create_quiz_instance([
 362              'timeopen' => $timeopen,
 363              'timeclose' => $timeclose
 364          ]);
 365          $event = $this->create_quiz_calendar_event($quiz, [
 366              'eventtype' => QUIZ_EVENT_TYPE_OPEN,
 367              'timestart' => 1
 368          ]);
 369  
 370          // The max limit should be bounded by the timeclose value.
 371          list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz);
 372  
 373          $this->assertNull($min);
 374          $this->assertEquals($timeclose, $max[0]);
 375  
 376          // No timeclose value should result in no upper limit.
 377          $quiz->timeclose = 0;
 378          list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz);
 379  
 380          $this->assertNull($min);
 381          $this->assertNull($max);
 382      }
 383  
 384      /**
 385       * An override event should not have any limits.
 386       */
 387      public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_override_event() {
 388          global $DB;
 389  
 390          $this->resetAfterTest(true);
 391          $this->setAdminUser();
 392          $generator = $this->getDataGenerator();
 393          $user = $generator->create_user();
 394          $course = $generator->create_course();
 395          $timeopen = time();
 396          $timeclose = $timeopen + DAYSECS;
 397          $quiz = $this->create_quiz_instance([
 398              'course' => $course->id,
 399              'timeopen' => $timeopen,
 400              'timeclose' => $timeclose
 401          ]);
 402          $event = $this->create_quiz_calendar_event($quiz, [
 403              'userid' => $user->id,
 404              'eventtype' => QUIZ_EVENT_TYPE_OPEN,
 405              'timestart' => 1
 406          ]);
 407          $record = (object) [
 408              'quiz' => $quiz->id,
 409              'userid' => $user->id
 410          ];
 411  
 412          $DB->insert_record('quiz_overrides', $record);
 413  
 414          list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz);
 415  
 416          $this->assertFalse($min);
 417          $this->assertFalse($max);
 418      }
 419  
 420      /**
 421       * The close event should be limited by the quiz's timeopen property, if it's set.
 422       */
 423      public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_close_event() {
 424          global $DB;
 425  
 426          $this->resetAfterTest(true);
 427          $this->setAdminUser();
 428          $timeopen = time();
 429          $timeclose = $timeopen + DAYSECS;
 430          $quiz = $this->create_quiz_instance([
 431              'timeopen' => $timeopen,
 432              'timeclose' => $timeclose
 433          ]);
 434          $event = $this->create_quiz_calendar_event($quiz, [
 435              'eventtype' => QUIZ_EVENT_TYPE_CLOSE,
 436              'timestart' => 1,
 437          ]);
 438  
 439          // The max limit should be bounded by the timeclose value.
 440          list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz);
 441  
 442          $this->assertEquals($timeopen, $min[0]);
 443          $this->assertNull($max);
 444  
 445          // No timeclose value should result in no upper limit.
 446          $quiz->timeopen = 0;
 447          list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz);
 448  
 449          $this->assertNull($min);
 450          $this->assertNull($max);
 451      }
 452  
 453      /**
 454       * When the close date event is changed and it results in the time close value of
 455       * the quiz being updated then the open quiz attempts should also be updated.
 456       */
 457      public function test_core_calendar_event_timestart_updated_update_quiz_attempt() {
 458          global $DB;
 459  
 460          $this->resetAfterTest();
 461          $this->setAdminUser();
 462  
 463          $generator = $this->getDataGenerator();
 464          $teacher = $generator->create_user();
 465          $student = $generator->create_user();
 466          $course = $generator->create_course();
 467          $context = \context_course::instance($course->id);
 468          $roleid = $generator->create_role();
 469          $now = time();
 470          $timelimit = 600;
 471          $timeopen = (new \DateTime())->setTimestamp($now);
 472          $timeclose = (new \DateTime())->setTimestamp($now)->modify('+1 day');
 473          // The new close time being earlier than the time open + time limit should
 474          // result in an update to the quiz attempts.
 475          $newtimeclose = $timeopen->getTimestamp() + $timelimit - 10;
 476          $quiz = $this->create_quiz_instance([
 477              'course' => $course->id,
 478              'timeopen' => $timeopen->getTimestamp(),
 479              'timeclose' => $timeclose->getTimestamp(),
 480              'timelimit' => $timelimit
 481          ]);
 482  
 483          $generator->enrol_user($student->id, $course->id, 'student');
 484          $generator->enrol_user($teacher->id, $course->id, 'teacher');
 485          $generator->role_assign($roleid, $teacher->id, $context->id);
 486  
 487          $event = $this->create_quiz_calendar_event($quiz, [
 488              'eventtype' => QUIZ_EVENT_TYPE_CLOSE,
 489              'timestart' => $newtimeclose
 490          ]);
 491  
 492          assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true);
 493  
 494          $attemptid = $DB->insert_record(
 495              'quiz_attempts',
 496              [
 497                  'quiz' => $quiz->id,
 498                  'userid' => $student->id,
 499                  'state' => 'inprogress',
 500                  'timestart' => $timeopen->getTimestamp(),
 501                  'timecheckstate' => 0,
 502                  'layout' => '',
 503                  'uniqueid' => 1
 504              ]
 505          );
 506  
 507          $this->setUser($teacher);
 508  
 509          mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
 510  
 511          $quiz = $DB->get_record('quiz', ['id' => $quiz->id]);
 512          $attempt = $DB->get_record('quiz_attempts', ['id' => $attemptid]);
 513          // When the close date is changed so that it's earlier than the time open
 514          // plus the time limit of the quiz then the attempt's timecheckstate should
 515          // be updated to the new time close date of the quiz.
 516          $this->assertEquals($newtimeclose, $attempt->timecheckstate);
 517          $this->assertEquals($newtimeclose, $quiz->timeclose);
 518      }
 519  }