2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
18 * Unit tests for (some of) mod/assign/lib.php.
22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
30 require_once($CFG->dirroot . '/mod/assign/lib.php');
31 require_once($CFG->dirroot . '/mod/assign/locallib.php');
32 require_once($CFG->dirroot . '/mod/assign/tests/generator.php');
34 use \core_calendar\local\api as calendar_local_api;
35 use \core_calendar\local\event\container as calendar_event_container;
38 * Unit tests for (some of) mod/assign/lib.php.
40 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44 class mod_assign_lib_testcase extends advanced_testcase {
46 // Use the generator helper.
47 use mod_assign_test_generator;
49 public function test_assign_print_overview() {
52 $this->resetAfterTest();
54 $course = $this->getDataGenerator()->create_course();
55 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
56 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
58 $this->setAdminUser();
60 // Assignment with default values.
61 $firstassign = $this->create_instance($course, ['name' => 'First Assignment']);
63 // Assignment with submissions.
64 $secondassign = $this->create_instance($course, [
65 'name' => 'Assignment with submissions',
67 'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
69 'submissiondrafts' => 1,
70 'assignsubmission_onlinetext_enabled' => 1,
72 $this->add_submission($student, $secondassign);
73 $this->submit_for_grading($student, $secondassign);
74 $this->mark_submission($teacher, $secondassign, $student, 50.0);
76 // Past assignments should not show up.
77 $pastassign = $this->create_instance($course, [
78 'name' => 'Past Assignment',
79 'duedate' => time() - DAYSECS - 1,
80 'cutoffdate' => time() - DAYSECS,
82 'assignsubmission_onlinetext_enabled' => 1,
85 // Open assignments should show up only if relevant.
86 $openassign = $this->create_instance($course, [
87 'name' => 'Open Assignment',
89 'cutoffdate' => time() + DAYSECS,
91 'assignsubmission_onlinetext_enabled' => 1,
93 $pastsubmission = $pastassign->get_user_submission($student->id, true);
94 $opensubmission = $openassign->get_user_submission($student->id, true);
96 // Check the overview as the different users.
97 // For students , open assignments should show only when there are no valid submissions.
98 $this->setUser($student);
100 $courses = $DB->get_records('course', array('id' => $course->id));
101 assign_print_overview($courses, $overview);
102 $this->assertDebuggingCalledCount(3);
103 $this->assertEquals(1, count($overview));
104 $this->assertRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']); // No valid submission.
105 $this->assertNotRegExp('/.*First Assignment.*/', $overview[$course->id]['assign']); // Has valid submission.
107 // And now submit the submission.
108 $opensubmission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
109 $openassign->testable_update_submission($opensubmission, $student->id, true, false);
112 assign_print_overview($courses, $overview);
113 $this->assertDebuggingCalledCount(3);
114 $this->assertEquals(0, count($overview));
116 $this->setUser($teacher);
118 assign_print_overview($courses, $overview);
119 $this->assertDebuggingCalledCount(3);
120 $this->assertEquals(1, count($overview));
121 // Submissions without a grade.
122 $this->assertRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']);
123 $this->assertNotRegExp('/.*Assignment with submissions.*/', $overview[$course->id]['assign']);
125 $this->setUser($teacher);
127 assign_print_overview($courses, $overview);
128 $this->assertDebuggingCalledCount(3);
129 $this->assertEquals(1, count($overview));
130 // Submissions without a grade.
131 $this->assertRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']);
132 $this->assertNotRegExp('/.*Assignment with submissions.*/', $overview[$course->id]['assign']);
134 // Let us grade a submission.
135 $this->setUser($teacher);
136 $data = new stdClass();
137 $data->grade = '50.0';
138 $openassign->testable_apply_grade_to_user($data, $student->id, 0);
140 // The assign_print_overview expects the grade date to be after the submission date.
141 $graderecord = $DB->get_record('assign_grades', array('assignment' => $openassign->get_instance()->id,
142 'userid' => $student->id, 'attemptnumber' => 0));
143 $graderecord->timemodified += 1;
144 $DB->update_record('assign_grades', $graderecord);
147 assign_print_overview($courses, $overview);
148 // Now assignment 4 should not show up.
149 $this->assertDebuggingCalledCount(3);
150 $this->assertEmpty($overview);
152 $this->setUser($teacher);
154 assign_print_overview($courses, $overview);
155 $this->assertDebuggingCalledCount(3);
156 // Now assignment 4 should not show up.
157 $this->assertEmpty($overview);
161 * Test that assign_print_overview does not return any assignments which are Open Offline.
163 public function test_assign_print_overview_open_offline() {
164 $this->resetAfterTest();
165 $course = $this->getDataGenerator()->create_course();
166 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
168 $this->setAdminUser();
169 $openassign = $this->create_instance($course, [
170 'duedate' => time() + DAYSECS,
171 'cutoffdate' => time() + (DAYSECS * 2),
174 $this->setUser($student);
176 assign_print_overview([$course], $overview);
178 $this->assertDebuggingCalledCount(1);
179 $this->assertEquals(0, count($overview));
183 * Test that assign_print_recent_activity shows ungraded submitted assignments.
185 public function test_print_recent_activity() {
186 $this->resetAfterTest();
187 $course = $this->getDataGenerator()->create_course();
188 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
189 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
190 $assign = $this->create_instance($course);
191 $this->submit_for_grading($student, $assign);
193 $this->setUser($teacher);
194 $this->expectOutputRegex('/submitted:/');
195 assign_print_recent_activity($course, true, time() - 3600);
199 * Test that assign_print_recent_activity does not display any warnings when a custom fullname has been configured.
201 public function test_print_recent_activity_fullname() {
202 $this->resetAfterTest();
203 $course = $this->getDataGenerator()->create_course();
204 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
205 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
206 $assign = $this->create_instance($course);
207 $this->submit_for_grading($student, $assign);
209 $this->setUser($teacher);
210 $this->expectOutputRegex('/submitted:/');
211 set_config('fullnamedisplay', 'firstname, lastnamephonetic');
212 assign_print_recent_activity($course, false, time() - 3600);
216 * Test that assign_print_recent_activity shows the blind marking ID.
218 public function test_print_recent_activity_fullname_blind_marking() {
219 $this->resetAfterTest();
220 $course = $this->getDataGenerator()->create_course();
221 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
222 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
224 $assign = $this->create_instance($course, [
227 $this->add_submission($student, $assign);
228 $this->submit_for_grading($student, $assign);
230 $this->setUser($teacher);
231 $uniqueid = $assign->get_uniqueid_for_user($student->id);
232 $expectedstr = preg_quote(get_string('participant', 'mod_assign'), '/') . '.*' . $uniqueid;
233 $this->expectOutputRegex("/{$expectedstr}/");
234 assign_print_recent_activity($course, false, time() - 3600);
238 * Test that assign_get_recent_mod_activity fetches the assignment correctly.
240 public function test_assign_get_recent_mod_activity() {
241 $this->resetAfterTest();
242 $course = $this->getDataGenerator()->create_course();
243 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
244 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
245 $assign = $this->create_instance($course);
246 $this->add_submission($student, $assign);
247 $this->submit_for_grading($student, $assign);
253 'cmid' => $assign->get_course_module()->id,
257 $this->setUser($teacher);
258 assign_get_recent_mod_activity($activities, $index, time() - HOURSECS, $course->id, $assign->get_course_module()->id);
260 $activity = $activities[1];
261 $this->assertEquals("assign", $activity->type);
262 $this->assertEquals($student->id, $activity->user->id);
266 * Ensure that assign_user_complete displays information about drafts.
268 public function test_assign_user_complete() {
271 $this->resetAfterTest();
272 $course = $this->getDataGenerator()->create_course();
273 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
274 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
275 $assign = $this->create_instance($course, ['submissiondrafts' => 1]);
276 $this->add_submission($student, $assign);
278 $PAGE->set_url(new moodle_url('/mod/assign/view.php', array('id' => $assign->get_course_module()->id)));
280 $submission = $assign->get_user_submission($student->id, true);
281 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
282 $DB->update_record('assign_submission', $submission);
284 $this->expectOutputRegex('/Draft/');
285 assign_user_complete($course, $student, $assign->get_course_module(), $assign->get_instance());
289 * Ensure that assign_user_outline fetches updated grades.
291 public function test_assign_user_outline() {
292 $this->resetAfterTest();
293 $course = $this->getDataGenerator()->create_course();
294 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
295 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
296 $assign = $this->create_instance($course);
298 $this->add_submission($student, $assign);
299 $this->submit_for_grading($student, $assign);
300 $this->mark_submission($teacher, $assign, $student, 50.0);
302 $this->setUser($teacher);
303 $data = $assign->get_user_grade($student->id, true);
304 $data->grade = '50.5';
305 $assign->update_grade($data);
307 $result = assign_user_outline($course, $student, $assign->get_course_module(), $assign->get_instance());
309 $this->assertRegExp('/50.5/', $result->info);
313 * Ensure that assign_get_completion_state reflects the correct status at each point.
315 public function test_assign_get_completion_state() {
318 $this->resetAfterTest();
319 $course = $this->getDataGenerator()->create_course();
320 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
321 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
322 $assign = $this->create_instance($course, [
323 'submissiondrafts' => 0,
324 'completionsubmit' => 1
327 $this->setUser($student);
328 $result = assign_get_completion_state($course, $assign->get_course_module(), $student->id, false);
329 $this->assertFalse($result);
331 $this->add_submission($student, $assign);
332 $result = assign_get_completion_state($course, $assign->get_course_module(), $student->id, false);
333 $this->assertFalse($result);
335 $this->submit_for_grading($student, $assign);
336 $result = assign_get_completion_state($course, $assign->get_course_module(), $student->id, false);
337 $this->assertTrue($result);
339 $this->mark_submission($teacher, $assign, $student, 50.0);
340 $result = assign_get_completion_state($course, $assign->get_course_module(), $student->id, false);
341 $this->assertTrue($result);
345 * Tests for mod_assign_refresh_events.
347 public function test_assign_refresh_events() {
350 $this->resetAfterTest();
353 $newduedate = $duedate + DAYSECS;
355 $this->setAdminUser();
357 $course = $this->getDataGenerator()->create_course();
358 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
359 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
360 $assign = $this->create_instance($course, [
361 'duedate' => $duedate,
364 $instance = $assign->get_instance();
365 $eventparams = ['modulename' => 'assign', 'instance' => $instance->id];
367 // Make sure the calendar event for assignment 1 matches the initial due date.
368 $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST);
369 $this->assertEquals($eventtime, $duedate);
371 // Manually update assignment 1's due date.
372 $DB->update_record('assign', (object) ['id' => $instance->id, 'duedate' => $newduedate]);
374 // Then refresh the assignment events of assignment 1's course.
375 $this->assertTrue(assign_refresh_events($course->id));
377 // Confirm that the assignment 1's due date event now has the new due date after refresh.
378 $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST);
379 $this->assertEquals($eventtime, $newduedate);
381 // Create a second course and assignment.
382 $othercourse = $this->getDataGenerator()->create_course();;
383 $otherassign = $this->create_instance($othercourse, ['duedate' => $duedate, 'course' => $othercourse->id]);
384 $otherinstance = $otherassign->get_instance();
386 // Manually update assignment 1 and 2's due dates.
387 $newduedate += DAYSECS;
388 $DB->update_record('assign', (object)['id' => $instance->id, 'duedate' => $newduedate]);
389 $DB->update_record('assign', (object)['id' => $otherinstance->id, 'duedate' => $newduedate]);
391 // Refresh events of all courses.
392 $this->assertTrue(assign_refresh_events());
394 // Check the due date calendar event for assignment 1.
395 $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST);
396 $this->assertEquals($eventtime, $newduedate);
398 // Check the due date calendar event for assignment 2.
399 $eventparams['instance'] = $otherinstance->id;
400 $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST);
401 $this->assertEquals($eventtime, $newduedate);
403 // In case the course ID is passed as a numeric string.
404 $this->assertTrue(assign_refresh_events('' . $course->id));
406 // Non-existing course ID.
407 $this->assertFalse(assign_refresh_events(-1));
409 // Invalid course ID.
410 $this->assertFalse(assign_refresh_events('aaa'));
413 public function test_assign_core_calendar_is_event_visible_duedate_event_as_teacher() {
414 $this->resetAfterTest();
415 $course = $this->getDataGenerator()->create_course();
416 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
417 $assign = $this->create_instance($course);
419 $this->setAdminUser();
421 // Create a calendar event.
422 $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE);
424 // The teacher should see the due date event.
425 $this->setUser($teacher);
426 $this->assertTrue(mod_assign_core_calendar_is_event_visible($event));
429 public function test_assign_core_calendar_is_event_visible_duedate_event_as_student() {
430 $this->resetAfterTest();
431 $course = $this->getDataGenerator()->create_course();
432 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
433 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
434 $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
436 $this->setAdminUser();
438 // Create a calendar event.
439 $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE);
441 // The student should care about the due date event.
442 $this->setUser($student);
443 $this->assertTrue(mod_assign_core_calendar_is_event_visible($event));
446 public function test_assign_core_calendar_is_event_visible_gradingduedate_event_as_teacher() {
447 $this->resetAfterTest();
448 $course = $this->getDataGenerator()->create_course();
449 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
450 $assign = $this->create_instance($course);
452 // Create a calendar event.
453 $this->setAdminUser();
454 $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE);
456 // The teacher should see the due date event.
457 $this->setUser($teacher);
458 $this->assertTrue(mod_assign_core_calendar_is_event_visible($event));
461 public function test_assign_core_calendar_is_event_visible_gradingduedate_event_as_student() {
462 $this->resetAfterTest();
463 $course = $this->getDataGenerator()->create_course();
464 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
465 $assign = $this->create_instance($course);
467 // Create a calendar event.
468 $this->setAdminUser();
469 $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE);
471 // The student should not see the due date event.
472 $this->setUser($student);
473 $this->assertFalse(mod_assign_core_calendar_is_event_visible($event));
476 public function test_assign_core_calendar_provide_event_action_duedate_as_teacher() {
477 $this->resetAfterTest();
478 $course = $this->getDataGenerator()->create_course();
479 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
480 $assign = $this->create_instance($course);
482 // Create a calendar event.
483 $this->setAdminUser();
484 $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE);
486 // The teacher should see the event.
487 $this->setUser($teacher);
488 $factory = new \core_calendar\action_factory();
489 $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
491 // The teacher should not have an action for a due date event.
492 $this->assertNull($actionevent);
495 public function test_assign_core_calendar_provide_event_action_duedate_as_student() {
496 $this->resetAfterTest();
497 $course = $this->getDataGenerator()->create_course();
498 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
499 $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
501 // Create a calendar event.
502 $this->setAdminUser();
503 $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE);
505 // The student should see the event.
506 $this->setUser($student);
507 $factory = new \core_calendar\action_factory();
508 $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
510 // Confirm the event was decorated.
511 $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
512 $this->assertEquals(get_string('addsubmission', 'assign'), $actionevent->get_name());
513 $this->assertInstanceOf('moodle_url', $actionevent->get_url());
514 $this->assertEquals(1, $actionevent->get_item_count());
515 $this->assertTrue($actionevent->is_actionable());
518 public function test_assign_core_calendar_provide_event_action_gradingduedate_as_teacher() {
519 $this->resetAfterTest();
520 $course = $this->getDataGenerator()->create_course();
521 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
522 $assign = $this->create_instance($course);
524 // Create a calendar event.
525 $this->setAdminUser();
526 $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE);
528 $this->setUser($teacher);
529 $factory = new \core_calendar\action_factory();
530 $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
532 // Confirm the event was decorated.
533 $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
534 $this->assertEquals(get_string('grade'), $actionevent->get_name());
535 $this->assertInstanceOf('moodle_url', $actionevent->get_url());
536 $this->assertEquals(0, $actionevent->get_item_count());
537 $this->assertTrue($actionevent->is_actionable());
540 public function test_assign_core_calendar_provide_event_action_gradingduedate_as_student() {
541 $this->resetAfterTest();
542 $course = $this->getDataGenerator()->create_course();
543 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
544 $assign = $this->create_instance($course);
546 // Create a calendar event.
547 $this->setAdminUser();
548 $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE);
550 $this->setUser($student);
551 $factory = new \core_calendar\action_factory();
552 $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
554 // Confirm the event was decorated.
555 $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
556 $this->assertEquals(get_string('grade'), $actionevent->get_name());
557 $this->assertInstanceOf('moodle_url', $actionevent->get_url());
558 $this->assertEquals(0, $actionevent->get_item_count());
559 $this->assertFalse($actionevent->is_actionable());
562 public function test_assign_core_calendar_provide_event_action_duedate_as_student_submitted() {
563 $this->resetAfterTest();
564 $course = $this->getDataGenerator()->create_course();
565 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
566 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
567 $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
569 $this->setAdminUser();
571 // Create a calendar event.
572 $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE);
574 // Create an action factory.
575 $factory = new \core_calendar\action_factory();
577 // Submit as the student.
578 $this->add_submission($student, $assign);
579 $this->submit_for_grading($student, $assign);
581 // Confirm there was no event to action.
582 $factory = new \core_calendar\action_factory();
583 $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
584 $this->assertNull($actionevent);
588 * Creates an action event.
590 * @param \stdClass $course The course the assignment is in
591 * @param assign $assign The assignment to create an event for
592 * @param string $eventtype The event type. eg. ASSIGN_EVENT_TYPE_DUE.
593 * @return bool|calendar_event
595 private function create_action_event($course, $assign, $eventtype) {
596 $event = new stdClass();
597 $event->name = 'Calendar event';
598 $event->modulename = 'assign';
599 $event->courseid = $course->id;
600 $event->instance = $assign->get_instance()->id;
601 $event->type = CALENDAR_EVENT_TYPE_ACTION;
602 $event->eventtype = $eventtype;
603 $event->timestart = time();
605 return calendar_event::create($event);
609 * Test the callback responsible for returning the completion rule descriptions.
610 * This function should work given either an instance of the module (cm_info), such as when checking the active rules,
611 * or if passed a stdClass of similar structure, such as when checking the the default completion settings for a mod type.
613 public function test_mod_assign_completion_get_active_rule_descriptions() {
614 $this->resetAfterTest();
615 $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
617 $this->setAdminUser();
619 // Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't.
620 $cm1 = $this->create_instance($course, ['completion' => '2', 'completionsubmit' => '1'])->get_course_module();
621 $cm2 = $this->create_instance($course, ['completion' => '2', 'completionsubmit' => '0'])->get_course_module();
623 // Data for the stdClass input type.
624 // This type of input would occur when checking the default completion rules for an activity type, where we don't have
625 // any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info.
626 $moddefaults = (object) [
628 'customcompletionrules' => [
629 'completionsubmit' => '1',
635 $activeruledescriptions = [get_string('completionsubmit', 'assign')];
636 $this->assertEquals(mod_assign_get_completion_active_rule_descriptions($cm1), $activeruledescriptions);
637 $this->assertEquals(mod_assign_get_completion_active_rule_descriptions($cm2), []);
638 $this->assertEquals(mod_assign_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
639 $this->assertEquals(mod_assign_get_completion_active_rule_descriptions(new stdClass()), []);
643 * Test that if some grades are not set, they are left alone and not rescaled
645 public function test_assign_rescale_activity_grades_some_unset() {
646 $this->resetAfterTest();
647 $course = $this->getDataGenerator()->create_course();
648 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
649 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
650 $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
653 $this->setUser($teacher);
654 $assign = $this->create_instance($course);
656 // Grade the student.
657 $data = ['grade' => 50];
658 $assign->testable_apply_grade_to_user((object)$data, $student->id, 0);
660 // Try getting another students grade. This will give a grade of ASSIGN_GRADE_NOT_SET (-1).
661 $assign->get_user_grade($otherstudent->id, true);
664 assign_rescale_activity_grades($course, $assign->get_course_module(), 0, 100, 0, 10);
666 // Get the grades for both students.
667 $studentgrade = $assign->get_user_grade($student->id, true);
668 $otherstudentgrade = $assign->get_user_grade($otherstudent->id, true);
670 // Make sure the real grade is scaled, but the ASSIGN_GRADE_NOT_SET stays the same.
671 $this->assertEquals($studentgrade->grade, 5);
672 $this->assertEquals($otherstudentgrade->grade, ASSIGN_GRADE_NOT_SET);
676 * Return false when there are not overrides for this assign instance.
678 public function test_assign_is_override_calendar_event_no_override() {
680 require_once($CFG->dirroot . '/calendar/lib.php');
682 $this->resetAfterTest();
683 $course = $this->getDataGenerator()->create_course();
684 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
686 $this->setAdminUser();
689 $assign = $this->create_instance($course, ['duedate' => $duedate]);
691 $instance = $assign->get_instance();
692 $event = new \calendar_event((object)[
693 'modulename' => 'assign',
694 'instance' => $instance->id,
695 'userid' => $student->id,
698 $this->assertFalse($assign->is_override_calendar_event($event));
702 * Return false if the given event isn't an assign module event.
704 public function test_assign_is_override_calendar_event_no_nodule_event() {
706 require_once($CFG->dirroot . '/calendar/lib.php');
708 $this->resetAfterTest();
709 $course = $this->getDataGenerator()->create_course();
710 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
712 $this->setAdminUser();
714 $userid = $student->id;
716 $assign = $this->create_instance($course, ['duedate' => $duedate]);
718 $instance = $assign->get_instance();
719 $event = new \calendar_event((object)[
723 $this->assertFalse($assign->is_override_calendar_event($event));
727 * Return false if there is overrides for this use but they belong to another assign
730 public function test_assign_is_override_calendar_event_different_assign_instance() {
732 require_once($CFG->dirroot . '/calendar/lib.php');
734 $this->resetAfterTest();
735 $course = $this->getDataGenerator()->create_course();
736 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
738 $this->setAdminUser();
741 $assign = $this->create_instance($course, ['duedate' => $duedate]);
742 $instance = $assign->get_instance();
744 $otherassign = $this->create_instance($course, ['duedate' => $duedate]);
745 $otherinstance = $otherassign->get_instance();
747 $event = new \calendar_event((object) [
748 'modulename' => 'assign',
749 'instance' => $instance->id,
750 'userid' => $student->id,
753 $DB->insert_record('assign_overrides', (object) [
754 'assignid' => $otherinstance->id,
755 'userid' => $student->id,
758 $this->assertFalse($assign->is_override_calendar_event($event));
762 * Return true if there is a user override for this event and assign instance.
764 public function test_assign_is_override_calendar_event_user_override() {
766 require_once($CFG->dirroot . '/calendar/lib.php');
768 $this->resetAfterTest();
769 $course = $this->getDataGenerator()->create_course();
770 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
772 $this->setAdminUser();
775 $assign = $this->create_instance($course, ['duedate' => $duedate]);
777 $instance = $assign->get_instance();
778 $event = new \calendar_event((object) [
779 'modulename' => 'assign',
780 'instance' => $instance->id,
781 'userid' => $student->id,
785 $DB->insert_record('assign_overrides', (object) [
786 'assignid' => $instance->id,
787 'userid' => $student->id,
790 $this->assertTrue($assign->is_override_calendar_event($event));
794 * Return true if there is a group override for the event and assign instance.
796 public function test_assign_is_override_calendar_event_group_override() {
798 require_once($CFG->dirroot . '/calendar/lib.php');
800 $this->resetAfterTest();
801 $course = $this->getDataGenerator()->create_course();
803 $this->setAdminUser();
806 $assign = $this->create_instance($course, ['duedate' => $duedate]);
807 $instance = $assign->get_instance();
808 $group = $this->getDataGenerator()->create_group(array('courseid' => $instance->course));
810 $event = new \calendar_event((object) [
811 'modulename' => 'assign',
812 'instance' => $instance->id,
813 'groupid' => $group->id,
816 $DB->insert_record('assign_overrides', (object) [
817 'assignid' => $instance->id,
818 'groupid' => $group->id,
821 $this->assertTrue($assign->is_override_calendar_event($event));
825 * Unknown event types should not have any limit restrictions returned.
827 public function test_mod_assign_core_calendar_get_valid_event_timestart_range_unkown_event_type() {
829 require_once($CFG->dirroot . '/calendar/lib.php');
831 $this->resetAfterTest();
832 $course = $this->getDataGenerator()->create_course();
834 $this->setAdminUser();
837 $assign = $this->create_instance($course, ['duedate' => $duedate]);
838 $instance = $assign->get_instance();
840 $event = new \calendar_event((object) [
841 'courseid' => $instance->course,
842 'modulename' => 'assign',
843 'instance' => $instance->id,
844 'eventtype' => 'SOME RANDOM EVENT'
847 list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
848 $this->assertNull($min);
849 $this->assertNull($max);
853 * Override events should not have any limit restrictions returned.
855 public function test_mod_assign_core_calendar_get_valid_event_timestart_range_override_event() {
857 require_once($CFG->dirroot . '/calendar/lib.php');
859 $this->resetAfterTest();
860 $course = $this->getDataGenerator()->create_course();
861 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
863 $this->setAdminUser();
866 $assign = $this->create_instance($course, ['duedate' => $duedate]);
867 $instance = $assign->get_instance();
869 $event = new \calendar_event((object) [
870 'courseid' => $instance->course,
871 'modulename' => 'assign',
872 'instance' => $instance->id,
873 'userid' => $student->id,
874 'eventtype' => ASSIGN_EVENT_TYPE_DUE
878 'assignid' => $instance->id,
879 'userid' => $student->id,
882 $DB->insert_record('assign_overrides', $record);
884 list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
885 $this->assertFalse($min);
886 $this->assertFalse($max);
890 * Assignments configured without a submissions from and cutoff date should not have
891 * any limits applied.
893 public function test_mod_assign_core_calendar_get_valid_event_timestart_range_due_no_limit() {
895 require_once($CFG->dirroot . '/calendar/lib.php');
897 $this->resetAfterTest();
898 $course = $this->getDataGenerator()->create_course();
900 $this->setAdminUser();
903 $assign = $this->create_instance($course, [
904 'duedate' => $duedate,
905 'allowsubmissionsfromdate' => 0,
908 $instance = $assign->get_instance();
910 $event = new \calendar_event((object) [
911 'courseid' => $instance->course,
912 'modulename' => 'assign',
913 'instance' => $instance->id,
914 'eventtype' => ASSIGN_EVENT_TYPE_DUE
917 list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
918 $this->assertNull($min);
919 $this->assertNull($max);
923 * Assignments should be bottom and top bound by the submissions from date and cutoff date
926 public function test_mod_assign_core_calendar_get_valid_event_timestart_range_due_with_limits() {
928 require_once($CFG->dirroot . '/calendar/lib.php');
930 $this->resetAfterTest();
931 $course = $this->getDataGenerator()->create_course();
933 $this->setAdminUser();
936 $submissionsfromdate = $duedate - DAYSECS;
937 $cutoffdate = $duedate + DAYSECS;
938 $assign = $this->create_instance($course, [
939 'duedate' => $duedate,
940 'allowsubmissionsfromdate' => $submissionsfromdate,
941 'cutoffdate' => $cutoffdate,
943 $instance = $assign->get_instance();
945 $event = new \calendar_event((object) [
946 'courseid' => $instance->course,
947 'modulename' => 'assign',
948 'instance' => $instance->id,
949 'eventtype' => ASSIGN_EVENT_TYPE_DUE
952 list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
953 $this->assertEquals($submissionsfromdate, $min[0]);
954 $this->assertNotEmpty($min[1]);
955 $this->assertEquals($cutoffdate, $max[0]);
956 $this->assertNotEmpty($max[1]);
960 * Assignment grading due date should not have any limits of no due date and cutoff date is set.
962 public function test_mod_assign_core_calendar_get_valid_event_timestart_range_gradingdue_no_limit() {
964 require_once($CFG->dirroot . '/calendar/lib.php');
966 $this->resetAfterTest();
967 $course = $this->getDataGenerator()->create_course();
969 $this->setAdminUser();
971 $assign = $this->create_instance($course, [
973 'allowsubmissionsfromdate' => 0,
976 $instance = $assign->get_instance();
978 $event = new \calendar_event((object) [
979 'courseid' => $instance->course,
980 'modulename' => 'assign',
981 'instance' => $instance->id,
982 'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE
985 list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
986 $this->assertNull($min);
987 $this->assertNull($max);
991 * Assignment grading due event is minimum bound by the due date, if it is set.
993 public function test_mod_assign_core_calendar_get_valid_event_timestart_range_gradingdue_with_due_date() {
995 require_once($CFG->dirroot . '/calendar/lib.php');
997 $this->resetAfterTest();
998 $course = $this->getDataGenerator()->create_course();
1000 $this->setAdminUser();
1003 $assign = $this->create_instance($course, ['duedate' => $duedate]);
1004 $instance = $assign->get_instance();
1006 $event = new \calendar_event((object) [
1007 'courseid' => $instance->course,
1008 'modulename' => 'assign',
1009 'instance' => $instance->id,
1010 'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE
1013 list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
1014 $this->assertEquals($duedate, $min[0]);
1015 $this->assertNotEmpty($min[1]);
1016 $this->assertNull($max);
1020 * Non due date events should not update the assignment due date.
1022 public function test_mod_assign_core_calendar_event_timestart_updated_non_due_event() {
1024 require_once($CFG->dirroot . '/calendar/lib.php');
1026 $this->resetAfterTest();
1027 $course = $this->getDataGenerator()->create_course();
1028 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1030 $this->setAdminUser();
1033 $submissionsfromdate = $duedate - DAYSECS;
1034 $cutoffdate = $duedate + DAYSECS;
1035 $assign = $this->create_instance($course, [
1036 'duedate' => $duedate,
1037 'allowsubmissionsfromdate' => $submissionsfromdate,
1038 'cutoffdate' => $cutoffdate,
1040 $instance = $assign->get_instance();
1042 $event = new \calendar_event((object) [
1043 'courseid' => $instance->course,
1044 'modulename' => 'assign',
1045 'instance' => $instance->id,
1046 'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE,
1047 'timestart' => $duedate + 1
1050 mod_assign_core_calendar_event_timestart_updated($event, $instance);
1052 $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1053 $this->assertEquals($duedate, $newinstance->duedate);
1057 * Due date override events should not change the assignment due date.
1059 public function test_mod_assign_core_calendar_event_timestart_updated_due_event_override() {
1061 require_once($CFG->dirroot . '/calendar/lib.php');
1063 $this->resetAfterTest();
1064 $course = $this->getDataGenerator()->create_course();
1065 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1067 $this->setAdminUser();
1070 $submissionsfromdate = $duedate - DAYSECS;
1071 $cutoffdate = $duedate + DAYSECS;
1072 $assign = $this->create_instance($course, [
1073 'duedate' => $duedate,
1074 'allowsubmissionsfromdate' => $submissionsfromdate,
1075 'cutoffdate' => $cutoffdate,
1077 $instance = $assign->get_instance();
1079 $event = new \calendar_event((object) [
1080 'courseid' => $instance->course,
1081 'modulename' => 'assign',
1082 'instance' => $instance->id,
1083 'userid' => $student->id,
1084 'eventtype' => ASSIGN_EVENT_TYPE_DUE,
1085 'timestart' => $duedate + 1
1088 $record = (object) [
1089 'assignid' => $instance->id,
1090 'userid' => $student->id,
1091 'duedate' => $duedate + 1,
1094 $DB->insert_record('assign_overrides', $record);
1096 mod_assign_core_calendar_event_timestart_updated($event, $instance);
1098 $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1099 $this->assertEquals($duedate, $newinstance->duedate);
1103 * Due date events should update the assignment due date.
1105 public function test_mod_assign_core_calendar_event_timestart_updated_due_event() {
1107 require_once($CFG->dirroot . '/calendar/lib.php');
1109 $this->resetAfterTest();
1110 $course = $this->getDataGenerator()->create_course();
1111 $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1113 $this->setAdminUser();
1116 $newduedate = $duedate + 1;
1117 $submissionsfromdate = $duedate - DAYSECS;
1118 $cutoffdate = $duedate + DAYSECS;
1119 $assign = $this->create_instance($course, [
1120 'duedate' => $duedate,
1121 'allowsubmissionsfromdate' => $submissionsfromdate,
1122 'cutoffdate' => $cutoffdate,
1124 $instance = $assign->get_instance();
1126 $event = new \calendar_event((object) [
1127 'courseid' => $instance->course,
1128 'modulename' => 'assign',
1129 'instance' => $instance->id,
1130 'eventtype' => ASSIGN_EVENT_TYPE_DUE,
1131 'timestart' => $newduedate
1134 mod_assign_core_calendar_event_timestart_updated($event, $instance);
1136 $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1137 $this->assertEquals($newduedate, $newinstance->duedate);
1141 * If a student somehow finds a way to update the due date calendar event
1142 * then the callback should not be executed to update the assignment due
1143 * date as well otherwise that would be a security issue.
1145 public function test_student_role_cant_update_due_event() {
1147 require_once($CFG->dirroot . '/calendar/lib.php');
1149 $this->resetAfterTest();
1150 $course = $this->getDataGenerator()->create_course();
1151 $context = context_course::instance($course->id);
1153 $roleid = $this->getDataGenerator()->create_role();
1154 $role = $DB->get_record('role', ['id' => $roleid]);
1155 $user = $this->getDataGenerator()->create_and_enrol($course, $role->shortname);
1157 $this->setAdminUser();
1159 $mapper = calendar_event_container::get_event_mapper();
1161 $duedate = (new DateTime())->setTimestamp($now);
1162 $newduedate = (new DateTime())->setTimestamp($now)->modify('+1 day');
1163 $assign = $this->create_instance($course, [
1164 'course' => $course->id,
1165 'duedate' => $duedate->getTimestamp(),
1167 $instance = $assign->get_instance();
1169 $record = $DB->get_record('event', [
1170 'courseid' => $course->id,
1171 'modulename' => 'assign',
1172 'instance' => $instance->id,
1173 'eventtype' => ASSIGN_EVENT_TYPE_DUE
1176 $event = new \calendar_event($record);
1178 assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
1179 assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleid, $context, true);
1181 $this->setUser($user);
1183 calendar_local_api::update_event_start_day(
1184 $mapper->from_legacy_event_to_event($event),
1188 $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1189 $newevent = \calendar_event::load($event->id);
1190 // The due date shouldn't have changed even though we updated the calendar
1192 $this->assertEquals($duedate->getTimestamp(), $newinstance->duedate);
1193 $this->assertEquals($newduedate->getTimestamp(), $newevent->timestart);
1197 * A teacher with the capability to modify an assignment module should be
1198 * able to update the assignment due date by changing the due date calendar
1201 public function test_teacher_role_can_update_due_event() {
1203 require_once($CFG->dirroot . '/calendar/lib.php');
1205 $this->resetAfterTest();
1206 $course = $this->getDataGenerator()->create_course();
1207 $context = context_course::instance($course->id);
1208 $user = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1209 $roleid = $DB->get_field('role', 'id', ['shortname' => 'teacher']);
1211 $this->setAdminUser();
1213 $mapper = calendar_event_container::get_event_mapper();
1215 $duedate = (new DateTime())->setTimestamp($now);
1216 $newduedate = (new DateTime())->setTimestamp($now)->modify('+1 day');
1217 $assign = $this->create_instance($course, [
1218 'course' => $course->id,
1219 'duedate' => $duedate->getTimestamp(),
1221 $instance = $assign->get_instance();
1223 $record = $DB->get_record('event', [
1224 'courseid' => $course->id,
1225 'modulename' => 'assign',
1226 'instance' => $instance->id,
1227 'eventtype' => ASSIGN_EVENT_TYPE_DUE
1230 $event = new \calendar_event($record);
1232 assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
1233 assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true);
1235 $this->setUser($user);
1236 // Trigger and capture the event when adding a contact.
1237 $sink = $this->redirectEvents();
1239 calendar_local_api::update_event_start_day(
1240 $mapper->from_legacy_event_to_event($event),
1244 $triggeredevents = $sink->get_events();
1245 $moduleupdatedevents = array_filter($triggeredevents, function($e) {
1246 return is_a($e, 'core\event\course_module_updated');
1249 $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1250 $newevent = \calendar_event::load($event->id);
1251 // The due date shouldn't have changed even though we updated the calendar
1253 $this->assertEquals($newduedate->getTimestamp(), $newinstance->duedate);
1254 $this->assertEquals($newduedate->getTimestamp(), $newevent->timestart);
1255 // Confirm that a module updated event is fired when the module
1257 $this->assertNotEmpty($moduleupdatedevents);