MDL-77564 Quiz display options: Hide or show the grade information
[moodle.git] / mod / quiz / tests / calendar_event_modified_test.php
blob542040123c705b25140a12caeb7363c92246a7b4
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.
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/>.
17 namespace mod_quiz;
19 defined('MOODLE_INTERNAL') || die();
21 global $CFG;
22 require_once($CFG->dirroot . '/mod/quiz/lib.php');
24 /**
25 * Unit tests for the calendar event modification callbacks used
26 * for dragging and dropping quiz calendar events in the calendar
27 * UI.
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
34 class calendar_event_modified_test extends \advanced_testcase {
36 /**
37 * Create an instance of the quiz activity.
39 * @param array $properties Properties to set on the activity
40 * @return stdClass Quiz activity instance
42 protected function create_quiz_instance(array $properties) {
43 global $DB;
45 $generator = $this->getDataGenerator();
47 if (empty($properties['course'])) {
48 $course = $generator->create_course();
49 $courseid = $course->id;
50 } else {
51 $courseid = $properties['course'];
54 $quizgenerator = $generator->get_plugin_generator('mod_quiz');
55 $quiz = $quizgenerator->create_instance(array_merge(['course' => $courseid], $properties));
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);
65 return $quiz;
68 /**
69 * Create a calendar event for a quiz activity instance.
71 * @param \stdClass $quiz The activity instance
72 * @param array $eventproperties Properties to set on the calendar event
73 * @return calendar_event
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
91 return new \calendar_event(array_merge($defaultproperties, $eventproperties));
94 /**
95 * An unkown event type should not change the quiz instance.
97 public function test_mod_quiz_core_calendar_event_timestart_updated_unknown_event() {
98 global $DB;
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
110 mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
112 $quiz = $DB->get_record('quiz', ['id' => $quiz->id]);
113 $this->assertEquals($timeopen, $quiz->timeopen);
114 $this->assertEquals($timeclose, $quiz->timeclose);
118 * A QUIZ_EVENT_TYPE_OPEN event should update the timeopen property of
119 * the quiz activity.
121 public function test_mod_quiz_core_calendar_event_timestart_updated_open_event() {
122 global $DB;
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
135 $event = $this->create_quiz_calendar_event($quiz, [
136 'eventtype' => QUIZ_EVENT_TYPE_OPEN,
137 'timestart' => $newtimeopen
140 mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
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);
152 * A QUIZ_EVENT_TYPE_CLOSE event should update the timeclose property of
153 * the quiz activity.
155 public function test_mod_quiz_core_calendar_event_timestart_updated_close_event() {
156 global $DB;
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
169 $event = $this->create_quiz_calendar_event($quiz, [
170 'eventtype' => QUIZ_EVENT_TYPE_CLOSE,
171 'timestart' => $newtimeclose
174 mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
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);
186 * A QUIZ_EVENT_TYPE_OPEN event should not update the timeopen property of
187 * the quiz activity if it's an override.
189 public function test_mod_quiz_core_calendar_event_timestart_updated_open_event_override() {
190 global $DB;
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
204 $event = $this->create_quiz_calendar_event($quiz, [
205 'userid' => $user->id,
206 'eventtype' => QUIZ_EVENT_TYPE_OPEN,
207 'timestart' => $newtimeopen
209 $record = (object) [
210 'quiz' => $quiz->id,
211 'userid' => $user->id
214 $DB->insert_record('quiz_overrides', $record);
216 mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
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);
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.
232 public function test_student_role_cant_update_quiz_activity() {
233 global $DB;
235 $this->resetAfterTest();
236 $this->setAdminUser();
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()
251 $generator->enrol_user($user->id, $course->id, 'student');
252 $generator->role_assign($roleid, $user->id, $context->id);
254 $event = $this->create_quiz_calendar_event($quiz, [
255 'eventtype' => QUIZ_EVENT_TYPE_OPEN,
256 'timestart' => $timeopen->getTimestamp()
259 assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleid, $context, true);
261 $this->setUser($user);
263 mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
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);
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.
276 public function test_teacher_role_can_update_quiz_activity() {
277 global $DB;
279 $this->resetAfterTest();
280 $this->setAdminUser();
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()
295 $generator->enrol_user($user->id, $course->id, 'teacher');
296 $generator->role_assign($roleid, $user->id, $context->id);
298 $event = $this->create_quiz_calendar_event($quiz, [
299 'eventtype' => QUIZ_EVENT_TYPE_OPEN,
300 'timestart' => $newtimeopen->getTimestamp()
303 assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true);
305 $this->setUser($user);
307 // Trigger and capture the event.
308 $sink = $this->redirectEvents();
310 mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
312 $triggeredevents = $sink->get_events();
313 $moduleupdatedevents = array_filter($triggeredevents, function($e) {
314 return is_a($e, 'core\event\course_module_updated');
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);
328 * An unkown event type should not have any limits
330 public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_unknown_event() {
331 global $DB;
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
341 $event = $this->create_quiz_calendar_event($quiz, [
342 'eventtype' => QUIZ_EVENT_TYPE_OPEN . "SOMETHING ELSE",
343 'timestart' => 1
346 list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz);
347 $this->assertNull($min);
348 $this->assertNull($max);
352 * The open event should be limited by the quiz's timeclose property, if it's set.
354 public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_open_event() {
355 global $DB;
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
365 $event = $this->create_quiz_calendar_event($quiz, [
366 'eventtype' => QUIZ_EVENT_TYPE_OPEN,
367 'timestart' => 1
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);
373 $this->assertNull($min);
374 $this->assertEquals($timeclose, $max[0]);
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);
380 $this->assertNull($min);
381 $this->assertNull($max);
385 * An override event should not have any limits.
387 public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_override_event() {
388 global $DB;
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
402 $event = $this->create_quiz_calendar_event($quiz, [
403 'userid' => $user->id,
404 'eventtype' => QUIZ_EVENT_TYPE_OPEN,
405 'timestart' => 1
407 $record = (object) [
408 'quiz' => $quiz->id,
409 'userid' => $user->id
412 $DB->insert_record('quiz_overrides', $record);
414 list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz);
416 $this->assertFalse($min);
417 $this->assertFalse($max);
421 * The close event should be limited by the quiz's timeopen property, if it's set.
423 public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_close_event() {
424 global $DB;
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
434 $event = $this->create_quiz_calendar_event($quiz, [
435 'eventtype' => QUIZ_EVENT_TYPE_CLOSE,
436 'timestart' => 1,
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);
442 $this->assertEquals($timeopen, $min[0]);
443 $this->assertNull($max);
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);
449 $this->assertNull($min);
450 $this->assertNull($max);
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.
457 public function test_core_calendar_event_timestart_updated_update_quiz_attempt() {
458 global $DB;
460 $this->resetAfterTest();
461 $this->setAdminUser();
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
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);
487 $event = $this->create_quiz_calendar_event($quiz, [
488 'eventtype' => QUIZ_EVENT_TYPE_CLOSE,
489 'timestart' => $newtimeclose
492 assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true);
494 $attemptid = $DB->insert_record(
495 'quiz_attempts',
497 'quiz' => $quiz->id,
498 'userid' => $student->id,
499 'state' => 'inprogress',
500 'timestart' => $timeopen->getTimestamp(),
501 'timecheckstate' => 0,
502 'layout' => '',
503 'uniqueid' => 1
507 $this->setUser($teacher);
509 mod_quiz_core_calendar_event_timestart_updated($event, $quiz);
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);