Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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