cadf766014185591b867f45bcee0f076a9fdc8ab
[moodle.git] / mod / assign / tests / lib_test.php
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/>.
17 /**
18  * Unit tests for (some of) mod/assign/lib.php.
19  *
20  * @package    mod_assign
21  * @category   phpunit
22  * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
27 defined('MOODLE_INTERNAL') || die();
29 global $CFG;
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/base_test.php');
34 use \core_calendar\local\api as calendar_local_api;
35 use \core_calendar\local\event\container as calendar_event_container;
37 /**
38  * Unit tests for (some of) mod/assign/lib.php.
39  *
40  * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
41  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42  */
43 class mod_assign_lib_testcase extends mod_assign_base_testcase {
45     protected function setUp() {
46         parent::setUp();
48         // Add additional default data (some real attempts and stuff).
49         $this->setUser($this->editingteachers[0]);
50         $this->create_instance();
51         $assign = $this->create_instance(array('duedate' => time(),
52                                                'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
53                                                'maxattempts' => 3,
54                                                'submissiondrafts' => 1,
55                                                'assignsubmission_onlinetext_enabled' => 1));
57         // Add a submission.
58         $this->setUser($this->students[0]);
59         $submission = $assign->get_user_submission($this->students[0]->id, true);
60         $data = new stdClass();
61         $data->onlinetext_editor = array('itemid' => file_get_unused_draft_itemid(),
62                                          'text' => 'Submission text',
63                                          'format' => FORMAT_HTML);
64         $plugin = $assign->get_submission_plugin_by_type('onlinetext');
65         $plugin->save($submission, $data);
67         // And now submit it for marking.
68         $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
69         $assign->testable_update_submission($submission, $this->students[0]->id, true, false);
71         // Mark the submission.
72         $this->setUser($this->teachers[0]);
73         $data = new stdClass();
74         $data->grade = '50.0';
75         $assign->testable_apply_grade_to_user($data, $this->students[0]->id, 0);
77         // This is required so that the submissions timemodified > the grade timemodified.
78         $this->waitForSecond();
80         // Edit the submission again.
81         $this->setUser($this->students[0]);
82         $submission = $assign->get_user_submission($this->students[0]->id, true);
83         $assign->testable_update_submission($submission, $this->students[0]->id, true, false);
85         // This is required so that the submissions timemodified > the grade timemodified.
86         $this->waitForSecond();
88         // Allow the student another attempt.
89         $this->teachers[0]->ignoresesskey = true;
90         $this->setUser($this->teachers[0]);
91         $result = $assign->testable_process_add_attempt($this->students[0]->id);
92         // Add another submission.
93         $this->setUser($this->students[0]);
94         $submission = $assign->get_user_submission($this->students[0]->id, true);
95         $data = new stdClass();
96         $data->onlinetext_editor = array('itemid' => file_get_unused_draft_itemid(),
97                                          'text' => 'Submission text 2',
98                                          'format' => FORMAT_HTML);
99         $plugin = $assign->get_submission_plugin_by_type('onlinetext');
100         $plugin->save($submission, $data);
102         // And now submit it for marking (again).
103         $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
104         $assign->testable_update_submission($submission, $this->students[0]->id, true, false);
105     }
107     public function test_assign_print_overview() {
108         global $DB;
110         // Create one more assignment instance.
111         $this->setAdminUser();
112         $courses = $DB->get_records('course', array('id' => $this->course->id));
113         // Past assignments should not show up.
114         $pastassign = $this->create_instance(array('duedate' => time() - 370001,
115                                                    'cutoffdate' => time() - 370000,
116                                                    'nosubmissions' => 0,
117                                                    'assignsubmission_onlinetext_enabled' => 1));
118         // Open assignments should show up only if relevant.
119         $openassign = $this->create_instance(array('duedate' => time(),
120                                                    'cutoffdate' => time() + 370000,
121                                                    'nosubmissions' => 0,
122                                                    'assignsubmission_onlinetext_enabled' => 1));
123         $pastsubmission = $pastassign->get_user_submission($this->students[0]->id, true);
124         $opensubmission = $openassign->get_user_submission($this->students[0]->id, true);
126         // Check the overview as the different users.
127         // For students , open assignments should show only when there are no valid submissions.
128         $this->setUser($this->students[0]);
129         $overview = array();
130         assign_print_overview($courses, $overview);
131         $this->assertDebuggingCalledCount(3);
132         $this->assertEquals(1, count($overview));
133         $this->assertRegExp('/.*Assignment 4.*/', $overview[$this->course->id]['assign']); // No valid submission.
134         $this->assertNotRegExp('/.*Assignment 1.*/', $overview[$this->course->id]['assign']); // Has valid submission.
136         // And now submit the submission.
137         $opensubmission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
138         $openassign->testable_update_submission($opensubmission, $this->students[0]->id, true, false);
140         $overview = array();
141         assign_print_overview($courses, $overview);
142         $this->assertDebuggingCalledCount(3);
143         $this->assertEquals(0, count($overview));
145         $this->setUser($this->teachers[0]);
146         $overview = array();
147         assign_print_overview($courses, $overview);
148         $this->assertDebuggingCalledCount(3);
149         $this->assertEquals(1, count($overview));
150         // Submissions without a grade.
151         $this->assertRegExp('/.*Assignment 4.*/', $overview[$this->course->id]['assign']);
152         $this->assertRegExp('/.*Assignment 2.*/', $overview[$this->course->id]['assign']);
154         $this->setUser($this->editingteachers[0]);
155         $overview = array();
156         assign_print_overview($courses, $overview);
157         $this->assertDebuggingCalledCount(3);
158         $this->assertEquals(1, count($overview));
159         // Submissions without a grade.
160         $this->assertRegExp('/.*Assignment 4.*/', $overview[$this->course->id]['assign']);
161         $this->assertRegExp('/.*Assignment 2.*/', $overview[$this->course->id]['assign']);
163         // Let us grade a submission.
164         $this->setUser($this->teachers[0]);
165         $data = new stdClass();
166         $data->grade = '50.0';
167         $openassign->testable_apply_grade_to_user($data, $this->students[0]->id, 0);
169         // The assign_print_overview expects the grade date to be after the submission date.
170         $graderecord = $DB->get_record('assign_grades', array('assignment' => $openassign->get_instance()->id,
171             'userid' => $this->students[0]->id, 'attemptnumber' => 0));
172         $graderecord->timemodified += 1;
173         $DB->update_record('assign_grades', $graderecord);
175         $overview = array();
176         assign_print_overview($courses, $overview);
177         $this->assertDebuggingCalledCount(3);
178         $this->assertEquals(1, count($overview));
179         // Now assignment 4 should not show up.
180         $this->assertNotRegExp('/.*Assignment 4.*/', $overview[$this->course->id]['assign']);
181         $this->assertRegExp('/.*Assignment 2.*/', $overview[$this->course->id]['assign']);
183         $this->setUser($this->editingteachers[0]);
184         $overview = array();
185         assign_print_overview($courses, $overview);
186         $this->assertDebuggingCalledCount(3);
187         $this->assertEquals(1, count($overview));
188         // Now assignment 4 should not show up.
189         $this->assertNotRegExp('/.*Assignment 4.*/', $overview[$this->course->id]['assign']);
190         $this->assertRegExp('/.*Assignment 2.*/', $overview[$this->course->id]['assign']);
192         // Open offline assignments should not show any notification to students.
193         $openassign = $this->create_instance(array('duedate' => time(),
194                                                    'cutoffdate' => time() + 370000));
195         $this->setUser($this->students[0]);
196         $overview = array();
197         assign_print_overview($courses, $overview);
198         $this->assertDebuggingCalledCount(4);
199         $this->assertEquals(0, count($overview));
200     }
202     public function test_print_recent_activity() {
203         // Submitting an assignment generates a notification.
204         $this->preventResetByRollback();
205         $sink = $this->redirectMessages();
207         $this->setUser($this->editingteachers[0]);
208         $assign = $this->create_instance();
209         $data = new stdClass();
210         $data->userid = $this->students[0]->id;
211         $notices = array();
212         $this->setUser($this->students[0]);
213         $assign->submit_for_grading($data, $notices);
215         $this->setUser($this->editingteachers[0]);
216         $this->expectOutputRegex('/submitted:/');
217         assign_print_recent_activity($this->course, true, time() - 3600);
219         $sink->close();
220     }
222     /** Make sure fullname dosn't trigger any warnings when assign_print_recent_activity is triggered. */
223     public function test_print_recent_activity_fullname() {
224         // Submitting an assignment generates a notification.
225         $this->preventResetByRollback();
226         $sink = $this->redirectMessages();
228         $this->setUser($this->editingteachers[0]);
229         $assign = $this->create_instance();
231         $data = new stdClass();
232         $data->userid = $this->students[0]->id;
233         $notices = array();
234         $this->setUser($this->students[0]);
235         $assign->submit_for_grading($data, $notices);
237         $this->setUser($this->editingteachers[0]);
238         $this->expectOutputRegex('/submitted:/');
239         set_config('fullnamedisplay', 'firstname, lastnamephonetic');
240         assign_print_recent_activity($this->course, false, time() - 3600);
242         $sink->close();
243     }
245     /** Make sure blind marking shows participant \d+ not fullname when assign_print_recent_activity is triggered. */
246     public function test_print_recent_activity_fullname_blind_marking() {
247         // Submitting an assignment generates a notification in blind marking.
248         $this->preventResetByRollback();
249         $sink = $this->redirectMessages();
251         $this->setUser($this->editingteachers[0]);
252         $assign = $this->create_instance(array('blindmarking' => 1));
254         $data = new stdClass();
255         $data->userid = $this->students[0]->id;
256         $notices = array();
257         $this->setUser($this->students[0]);
258         $assign->submit_for_grading($data, $notices);
260         $this->setUser($this->editingteachers[0]);
261         $uniqueid = $assign->get_uniqueid_for_user($data->userid);
262         $expectedstr = preg_quote(get_string('participant', 'mod_assign'), '/') . '.*' . $uniqueid;
263         $this->expectOutputRegex("/{$expectedstr}/");
264         assign_print_recent_activity($this->course, false, time() - 3600);
266         $sink->close();
267     }
269     public function test_assign_get_recent_mod_activity() {
270         // Submitting an assignment generates a notification.
271         $this->preventResetByRollback();
272         $sink = $this->redirectMessages();
274         $this->setUser($this->editingteachers[0]);
275         $assign = $this->create_instance();
277         $data = new stdClass();
278         $data->userid = $this->students[0]->id;
279         $notices = array();
280         $this->setUser($this->students[0]);
281         $assign->submit_for_grading($data, $notices);
283         $this->setUser($this->editingteachers[0]);
284         $activities = array();
285         $index = 0;
287         $activity = new stdClass();
288         $activity->type    = 'activity';
289         $activity->cmid    = $assign->get_course_module()->id;
290         $activities[$index++] = $activity;
292         assign_get_recent_mod_activity( $activities,
293                                         $index,
294                                         time() - 3600,
295                                         $this->course->id,
296                                         $assign->get_course_module()->id);
298         $this->assertEquals("assign", $activities[1]->type);
299         $sink->close();
300     }
302     public function test_assign_user_complete() {
303         global $PAGE, $DB;
305         $this->setUser($this->editingteachers[0]);
306         $assign = $this->create_instance(array('submissiondrafts' => 1));
307         $PAGE->set_url(new moodle_url('/mod/assign/view.php', array('id' => $assign->get_course_module()->id)));
309         $submission = $assign->get_user_submission($this->students[0]->id, true);
310         $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
311         $DB->update_record('assign_submission', $submission);
313         $this->expectOutputRegex('/Draft/');
314         assign_user_complete($this->course, $this->students[0], $assign->get_course_module(), $assign->get_instance());
315     }
317     public function test_assign_user_outline() {
318         $this->setUser($this->editingteachers[0]);
319         $assign = $this->create_instance();
321         $this->setUser($this->teachers[0]);
322         $data = $assign->get_user_grade($this->students[0]->id, true);
323         $data->grade = '50.5';
324         $assign->update_grade($data);
326         $result = assign_user_outline($this->course, $this->students[0], $assign->get_course_module(), $assign->get_instance());
328         $this->assertRegExp('/50.5/', $result->info);
329     }
331     public function test_assign_get_completion_state() {
332         global $DB;
333         $assign = $this->create_instance(array('submissiondrafts' => 0, 'completionsubmit' => 1));
335         $this->setUser($this->students[0]);
336         $result = assign_get_completion_state($this->course, $assign->get_course_module(), $this->students[0]->id, false);
337         $this->assertFalse($result);
338         $submission = $assign->get_user_submission($this->students[0]->id, true);
339         $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
340         $DB->update_record('assign_submission', $submission);
342         $result = assign_get_completion_state($this->course, $assign->get_course_module(), $this->students[0]->id, false);
344         $this->assertTrue($result);
345     }
347     /**
348      * Tests for mod_assign_refresh_events.
349      */
350     public function test_assign_refresh_events() {
351         global $DB;
352         $duedate = time();
353         $newduedate = $duedate + DAYSECS;
354         $this->setAdminUser();
356         $assign = $this->create_instance(['duedate' => $duedate]);
358         // Make sure the calendar event for assignment 1 matches the initial due date.
359         $instance = $assign->get_instance();
360         $eventparams = ['modulename' => 'assign', 'instance' => $instance->id];
361         $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST);
362         $this->assertEquals($eventtime, $duedate);
364         // Manually update assignment 1's due date.
365         $DB->update_record('assign', (object)['id' => $instance->id, 'duedate' => $newduedate]);
367         // Then refresh the assignment events of assignment 1's course.
368         $this->assertTrue(assign_refresh_events($this->course->id));
370         // Confirm that the assignment 1's due date event now has the new due date after refresh.
371         $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST);
372         $this->assertEquals($eventtime, $newduedate);
374         // Create a second course and assignment.
375         $generator = $this->getDataGenerator();
376         $course2 = $generator->create_course();
377         $assign2 = $this->create_instance(['duedate' => $duedate, 'course' => $course2->id]);
378         $instance2 = $assign2->get_instance();
380         // Manually update assignment 1 and 2's due dates.
381         $newduedate += DAYSECS;
382         $DB->update_record('assign', (object)['id' => $instance->id, 'duedate' => $newduedate]);
383         $DB->update_record('assign', (object)['id' => $instance2->id, 'duedate' => $newduedate]);
385         // Refresh events of all courses.
386         $this->assertTrue(assign_refresh_events());
388         // Check the due date calendar event for assignment 1.
389         $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST);
390         $this->assertEquals($eventtime, $newduedate);
392         // Check the due date calendar event for assignment 2.
393         $eventparams['instance'] = $instance2->id;
394         $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST);
395         $this->assertEquals($eventtime, $newduedate);
397         // In case the course ID is passed as a numeric string.
398         $this->assertTrue(assign_refresh_events('' . $this->course->id));
400         // Non-existing course ID.
401         $this->assertFalse(assign_refresh_events(-1));
403         // Invalid course ID.
404         $this->assertFalse(assign_refresh_events('aaa'));
405     }
407     public function test_assign_core_calendar_is_event_visible_duedate_event_as_teacher() {
408         $this->setAdminUser();
410         // Create an assignment.
411         $assign = $this->create_instance();
413         // Create a calendar event.
414         $event = $this->create_action_event($assign->get_instance()->id, ASSIGN_EVENT_TYPE_DUE);
416         // Set the user to a teacher.
417         $this->setUser($this->editingteachers[0]);
419         // The teacher should see the due date event.
420         $this->assertTrue(mod_assign_core_calendar_is_event_visible($event));
421     }
423     public function test_assign_core_calendar_is_event_visible_duedate_event_as_student() {
424         $this->setAdminUser();
426         // Create an assignment.
427         $assign = $this->create_instance(array('assignsubmission_onlinetext_enabled' => 1));
429         // Create a calendar event.
430         $event = $this->create_action_event($assign->get_instance()->id, ASSIGN_EVENT_TYPE_DUE);
432         // Set the user to a student.
433         $this->setUser($this->students[0]);
435         // The student should care about the due date event.
436         $this->assertTrue(mod_assign_core_calendar_is_event_visible($event));
437     }
439     public function test_assign_core_calendar_is_event_visible_gradingduedate_event_as_teacher() {
440         $this->setAdminUser();
442         // Create an assignment.
443         $assign = $this->create_instance();
445         // Create a calendar event.
446         $event = $this->create_action_event($assign->get_instance()->id, ASSIGN_EVENT_TYPE_GRADINGDUE);
448         // Set the user to a teacher.
449         $this->setUser($this->editingteachers[0]);
451         // The teacher should care about the grading due date event.
452         $this->assertTrue(mod_assign_core_calendar_is_event_visible($event));
453     }
455     public function test_assign_core_calendar_is_event_visible_gradingduedate_event_as_student() {
456         $this->setAdminUser();
458         // Create an assignment.
459         $assign = $this->create_instance();
461         // Create a calendar event.
462         $event = $this->create_action_event($assign->get_instance()->id, ASSIGN_EVENT_TYPE_GRADINGDUE);
464         // Set the user to a student.
465         $this->setUser($this->students[0]);
467         // The student should not care about the grading due date event.
468         $this->assertFalse(mod_assign_core_calendar_is_event_visible($event));
469     }
471     public function test_assign_core_calendar_provide_event_action_duedate_as_teacher() {
472         $this->setAdminUser();
474         // Create an assignment.
475         $assign = $this->create_instance(array('assignsubmission_onlinetext_enabled' => 1));
477         // Create a calendar event.
478         $event = $this->create_action_event($assign->get_instance()->id, ASSIGN_EVENT_TYPE_DUE);
480         // Create an action factory.
481         $factory = new \core_calendar\action_factory();
483         // Set the user to a teacher.
484         $this->setUser($this->teachers[0]);
486         // Decorate action event.
487         $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
489         // The teacher should not have an action for a due date event.
490         $this->assertNull($actionevent);
491     }
493     public function test_assign_core_calendar_provide_event_action_duedate_as_student() {
494         $this->setAdminUser();
496         // Create an assignment.
497         $assign = $this->create_instance(array('assignsubmission_onlinetext_enabled' => 1));
499         // Create a calendar event.
500         $event = $this->create_action_event($assign->get_instance()->id, ASSIGN_EVENT_TYPE_DUE);
502         // Create an action factory.
503         $factory = new \core_calendar\action_factory();
505         // Set the user to a student.
506         $this->setUser($this->students[0]);
508         // Decorate action event.
509         $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
511         // Confirm the event was decorated.
512         $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
513         $this->assertEquals(get_string('addsubmission', 'assign'), $actionevent->get_name());
514         $this->assertInstanceOf('moodle_url', $actionevent->get_url());
515         $this->assertEquals(1, $actionevent->get_item_count());
516         $this->assertTrue($actionevent->is_actionable());
517     }
519     public function test_assign_core_calendar_provide_event_action_gradingduedate_as_teacher() {
520         $this->setAdminUser();
522         // Create an assignment.
523         $assign = $this->create_instance();
525         // Create a calendar event.
526         $event = $this->create_action_event($assign->get_instance()->id, ASSIGN_EVENT_TYPE_GRADINGDUE);
528         // Create an action factory.
529         $factory = new \core_calendar\action_factory();
531         // Set the user to a teacher.
532         $this->setUser($this->editingteachers[0]);
534         // Decorate action event.
535         $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
537         // Confirm the event was decorated.
538         $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
539         $this->assertEquals(get_string('grade'), $actionevent->get_name());
540         $this->assertInstanceOf('moodle_url', $actionevent->get_url());
541         $this->assertEquals(0, $actionevent->get_item_count());
542         $this->assertTrue($actionevent->is_actionable());
543     }
545     public function test_assign_core_calendar_provide_event_action_gradingduedate_as_student() {
546         $this->setAdminUser();
548         // Create an assignment.
549         $assign = $this->create_instance();
551         // Create a calendar event.
552         $event = $this->create_action_event($assign->get_instance()->id, ASSIGN_EVENT_TYPE_GRADINGDUE);
554         // Create an action factory.
555         $factory = new \core_calendar\action_factory();
557         // Set the user to a student.
558         $this->setUser($this->students[0]);
560         // Decorate action event.
561         $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
563         // Confirm the event was decorated.
564         $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
565         $this->assertEquals(get_string('grade'), $actionevent->get_name());
566         $this->assertInstanceOf('moodle_url', $actionevent->get_url());
567         $this->assertEquals(0, $actionevent->get_item_count());
568         $this->assertFalse($actionevent->is_actionable());
569     }
571     public function test_assign_core_calendar_provide_event_action_duedate_as_student_submitted() {
572         $this->setAdminUser();
574         // Create an assignment.
575         $assign = $this->create_instance(array('assignsubmission_onlinetext_enabled' => 1));
577         // Create a calendar event.
578         $event = $this->create_action_event($assign->get_instance()->id, ASSIGN_EVENT_TYPE_DUE);
580         // Create an action factory.
581         $factory = new \core_calendar\action_factory();
583         // Set the user to a student.
584         $this->setUser($this->students[0]);
586         // Submit the assignment.
587         $submission = $assign->get_user_submission($this->students[0]->id, true);
588         $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
589         $assign->testable_update_submission($submission, $this->students[0]->id, true, false);
590         $data = (object) [
591             'userid' => $this->students[0]->id,
592             'onlinetext_editor' => [
593                 'itemid' => file_get_unused_draft_itemid(),
594                 'text' => 'Submission text',
595                 'format' => FORMAT_MOODLE,
596             ],
597         ];
598         $plugin = $assign->get_submission_plugin_by_type('onlinetext');
599         $plugin->save($submission, $data);
601         // Create an action factory.
602         $factory = new \core_calendar\action_factory();
604         // Decorate action event.
605         $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
607         // Confirm there was no event to action.
608         $this->assertNull($actionevent);
609     }
611     /**
612      * Creates an action event.
613      *
614      * @param int $instanceid The assign id.
615      * @param string $eventtype The event type. eg. ASSIGN_EVENT_TYPE_DUE.
616      * @return bool|calendar_event
617      */
618     private function create_action_event($instanceid, $eventtype) {
619         $event = new stdClass();
620         $event->name = 'Calendar event';
621         $event->modulename  = 'assign';
622         $event->courseid = $this->course->id;
623         $event->instance = $instanceid;
624         $event->type = CALENDAR_EVENT_TYPE_ACTION;
625         $event->eventtype = $eventtype;
626         $event->timestart = time();
628         return calendar_event::create($event);
629     }
631     /**
632      * Test the callback responsible for returning the completion rule descriptions.
633      * This function should work given either an instance of the module (cm_info), such as when checking the active rules,
634      * or if passed a stdClass of similar structure, such as when checking the the default completion settings for a mod type.
635      */
636     public function test_mod_assign_completion_get_active_rule_descriptions() {
637         $this->resetAfterTest();
638         $this->setAdminUser();
640         // Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't.
641         $cm1 = $this->create_instance(['completion' => '2', 'completionsubmit' => '1'])->get_course_module();
642         $cm2 = $this->create_instance(['completion' => '2', 'completionsubmit' => '0'])->get_course_module();
644         // Data for the stdClass input type.
645         // This type of input would occur when checking the default completion rules for an activity type, where we don't have
646         // any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info.
647         $moddefaults = new stdClass();
648         $moddefaults->customdata = ['customcompletionrules' => ['completionsubmit' => '1']];
649         $moddefaults->completion = 2;
651         $activeruledescriptions = [get_string('completionsubmit', 'assign')];
652         $this->assertEquals(mod_assign_get_completion_active_rule_descriptions($cm1), $activeruledescriptions);
653         $this->assertEquals(mod_assign_get_completion_active_rule_descriptions($cm2), []);
654         $this->assertEquals(mod_assign_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
655         $this->assertEquals(mod_assign_get_completion_active_rule_descriptions(new stdClass()), []);
656     }
658     /**
659      * Test that if some grades are not set, they are left alone and not rescaled
660      */
661     public function test_assign_rescale_activity_grades_some_unset() {
662         $this->resetAfterTest();
664         // As a teacher...
665         $this->setUser($this->editingteachers[0]);
666         $assign = $this->create_instance();
668         // Grade the student.
669         $data = ['grade' => 50];
670         $assign->testable_apply_grade_to_user((object)$data, $this->students[0]->id, 0);
672         // Try getting another students grade. This will give a grade of ASSIGN_GRADE_NOT_SET (-1).
673         $assign->get_user_grade($this->students[1]->id, true);
675         // Rescale.
676         assign_rescale_activity_grades($this->course, $assign->get_course_module(), 0, 100, 0, 10);
678         // Get the grades for both students.
679         $student0grade = $assign->get_user_grade($this->students[0]->id, true);
680         $student1grade = $assign->get_user_grade($this->students[1]->id, true);
682         // Make sure the real grade is scaled, but the ASSIGN_GRADE_NOT_SET stays the same.
683         $this->assertEquals($student0grade->grade, 5);
684         $this->assertEquals($student1grade->grade, ASSIGN_GRADE_NOT_SET);
685     }
687     /**
688      * Return false when there are not overrides for this assign instance.
689      */
690     public function test_assign_is_override_calendar_event_no_override() {
691         global $CFG, $DB;
692         require_once($CFG->dirroot . '/calendar/lib.php');
694         $this->resetAfterTest();
695         $this->setAdminUser();
697         $userid = 1234;
698         $duedate = time();
699         $assign = $this->create_instance(['duedate' => $duedate]);
701         $instance = $assign->get_instance();
702         $event = new \calendar_event((object)[
703             'modulename' => 'assign',
704             'instance' => $instance->id,
705             'userid' => $userid
706         ]);
708         $this->assertFalse($assign->is_override_calendar_event($event));
709     }
711     /**
712      * Return false if the given event isn't an assign module event.
713      */
714     public function test_assign_is_override_calendar_event_no_nodule_event() {
715         global $CFG, $DB;
716         require_once($CFG->dirroot . '/calendar/lib.php');
718         $this->resetAfterTest();
719         $this->setAdminUser();
721         $userid = $this->students[0]->id;
722         $duedate = time();
723         $assign = $this->create_instance(['duedate' => $duedate]);
725         $instance = $assign->get_instance();
726         $event = new \calendar_event((object)[
727             'userid' => $userid
728         ]);
730         $this->assertFalse($assign->is_override_calendar_event($event));
731     }
733     /**
734      * Return false if there is overrides for this use but they belong to another assign
735      * instance.
736      */
737     public function test_assign_is_override_calendar_event_different_assign_instance() {
738         global $CFG, $DB;
739         require_once($CFG->dirroot . '/calendar/lib.php');
741         $this->resetAfterTest();
742         $this->setAdminUser();
744         $userid = 1234;
745         $duedate = time();
746         $assign = $this->create_instance(['duedate' => $duedate]);
747         $assign2 = $this->create_instance(['duedate' => $duedate]);
749         $instance = $assign->get_instance();
750         $event = new \calendar_event((object) [
751             'modulename' => 'assign',
752             'instance' => $instance->id,
753             'userid' => $userid
754         ]);
756         $record = (object) [
757             'assignid' => $assign2->get_instance()->id,
758             'userid' => $userid
759         ];
761         $DB->insert_record('assign_overrides', $record);
763         $this->assertFalse($assign->is_override_calendar_event($event));
764     }
766     /**
767      * Return true if there is a user override for this event and assign instance.
768      */
769     public function test_assign_is_override_calendar_event_user_override() {
770         global $CFG, $DB;
771         require_once($CFG->dirroot . '/calendar/lib.php');
773         $this->resetAfterTest();
774         $this->setAdminUser();
776         $userid = 1234;
777         $duedate = time();
778         $assign = $this->create_instance(['duedate' => $duedate]);
780         $instance = $assign->get_instance();
781         $event = new \calendar_event((object) [
782             'modulename' => 'assign',
783             'instance' => $instance->id,
784             'userid' => $userid
785         ]);
787         $record = (object) [
788             'assignid' => $instance->id,
789             'userid' => $userid
790         ];
792         $DB->insert_record('assign_overrides', $record);
794         $this->assertTrue($assign->is_override_calendar_event($event));
795     }
797     /**
798      * Return true if there is a group override for the event and assign instance.
799      */
800     public function test_assign_is_override_calendar_event_group_override() {
801         global $CFG, $DB;
802         require_once($CFG->dirroot . '/calendar/lib.php');
804         $this->resetAfterTest();
805         $this->setAdminUser();
807         $duedate = time();
808         $assign = $this->create_instance(['duedate' => $duedate]);
809         $instance = $assign->get_instance();
810         $group = $this->getDataGenerator()->create_group(array('courseid' => $instance->course));
811         $groupid = $group->id;
813         $event = new \calendar_event((object) [
814             'modulename' => 'assign',
815             'instance' => $instance->id,
816             'groupid' => $groupid
817         ]);
819         $record = (object) [
820             'assignid' => $instance->id,
821             'groupid' => $groupid
822         ];
824         $DB->insert_record('assign_overrides', $record);
826         $this->assertTrue($assign->is_override_calendar_event($event));
827     }
829     /**
830      * Unknown event types should not have any limit restrictions returned.
831      */
832     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_unkown_event_type() {
833         global $CFG;
834         require_once($CFG->dirroot . '/calendar/lib.php');
836         $this->resetAfterTest();
837         $this->setAdminUser();
839         $duedate = time();
840         $assign = $this->create_instance(['duedate' => $duedate]);
841         $instance = $assign->get_instance();
843         $event = new \calendar_event((object) [
844             'courseid' => $instance->course,
845             'modulename' => 'assign',
846             'instance' => $instance->id,
847             'eventtype' => 'SOME RANDOM EVENT'
848         ]);
850         list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
851         $this->assertNull($min);
852         $this->assertNull($max);
853     }
855     /**
856      * Override events should not have any limit restrictions returned.
857      */
858     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_override_event() {
859         global $CFG, $DB;
860         require_once($CFG->dirroot . '/calendar/lib.php');
862         $this->resetAfterTest();
863         $this->setAdminUser();
865         $duedate = time();
866         $assign = $this->create_instance(['duedate' => $duedate]);
867         $instance = $assign->get_instance();
868         $userid = $this->students[0]->id;
870         $event = new \calendar_event((object) [
871             'courseid' => $instance->course,
872             'modulename' => 'assign',
873             'instance' => $instance->id,
874             'userid' => $userid,
875             'eventtype' => ASSIGN_EVENT_TYPE_DUE
876         ]);
878         $record = (object) [
879             'assignid' => $instance->id,
880             'userid' => $userid
881         ];
883         $DB->insert_record('assign_overrides', $record);
885         list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
886         $this->assertFalse($min);
887         $this->assertFalse($max);
888     }
890     /**
891      * Assignments configured without a submissions from and cutoff date should not have
892      * any limits applied.
893      */
894     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_due_no_limit() {
895         global $CFG, $DB;
896         require_once($CFG->dirroot . '/calendar/lib.php');
898         $this->resetAfterTest();
899         $this->setAdminUser();
901         $duedate = time();
902         $assign = $this->create_instance([
903             'duedate' => $duedate,
904             'allowsubmissionsfromdate' => 0,
905             'cutoffdate' => 0,
906         ]);
907         $instance = $assign->get_instance();
908         $userid = $this->students[0]->id;
910         $event = new \calendar_event((object) [
911             'courseid' => $instance->course,
912             'modulename' => 'assign',
913             'instance' => $instance->id,
914             'eventtype' => ASSIGN_EVENT_TYPE_DUE
915         ]);
917         list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
918         $this->assertNull($min);
919         $this->assertNull($max);
920     }
922     /**
923      * Assignments should be bottom and top bound by the submissions from date and cutoff date
924      * respectively.
925      */
926     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_due_with_limits() {
927         global $CFG, $DB;
928         require_once($CFG->dirroot . '/calendar/lib.php');
930         $this->resetAfterTest();
931         $this->setAdminUser();
933         $duedate = time();
934         $submissionsfromdate = $duedate - DAYSECS;
935         $cutoffdate = $duedate + DAYSECS;
936         $assign = $this->create_instance([
937             'duedate' => $duedate,
938             'allowsubmissionsfromdate' => $submissionsfromdate,
939             'cutoffdate' => $cutoffdate,
940         ]);
941         $instance = $assign->get_instance();
942         $userid = $this->students[0]->id;
944         $event = new \calendar_event((object) [
945             'courseid' => $instance->course,
946             'modulename' => 'assign',
947             'instance' => $instance->id,
948             'eventtype' => ASSIGN_EVENT_TYPE_DUE
949         ]);
951         list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
952         $this->assertEquals($submissionsfromdate, $min[0]);
953         $this->assertNotEmpty($min[1]);
954         $this->assertEquals($cutoffdate, $max[0]);
955         $this->assertNotEmpty($max[1]);
956     }
958     /**
959      * Assignment grading due date should not have any limits of no due date and cutoff date is set.
960      */
961     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_gradingdue_no_limit() {
962         global $CFG, $DB;
963         require_once($CFG->dirroot . '/calendar/lib.php');
965         $this->resetAfterTest();
966         $this->setAdminUser();
968         $assign = $this->create_instance([
969             'duedate' => 0,
970             'allowsubmissionsfromdate' => 0,
971             'cutoffdate' => 0,
972         ]);
973         $instance = $assign->get_instance();
975         $event = new \calendar_event((object) [
976             'courseid' => $instance->course,
977             'modulename' => 'assign',
978             'instance' => $instance->id,
979             'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE
980         ]);
982         list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
983         $this->assertNull($min);
984         $this->assertNull($max);
985     }
987     /**
988      * Assignment grading due event is minimum bound by the due date, if it is set.
989      */
990     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_gradingdue_with_due_date() {
991         global $CFG, $DB;
992         require_once($CFG->dirroot . '/calendar/lib.php');
994         $this->resetAfterTest();
995         $this->setAdminUser();
997         $duedate = time();
998         $assign = $this->create_instance([
999             'duedate' => $duedate
1000         ]);
1001         $instance = $assign->get_instance();
1002         $userid = $this->students[0]->id;
1004         $event = new \calendar_event((object) [
1005             'courseid' => $instance->course,
1006             'modulename' => 'assign',
1007             'instance' => $instance->id,
1008             'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE
1009         ]);
1011         list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
1012         $this->assertEquals($duedate, $min[0]);
1013         $this->assertNotEmpty($min[1]);
1014         $this->assertNull($max);
1015     }
1017     /**
1018      * Non due date events should not update the assignment due date.
1019      */
1020     public function test_mod_assign_core_calendar_event_timestart_updated_non_due_event() {
1021         global $CFG, $DB;
1022         require_once($CFG->dirroot . '/calendar/lib.php');
1024         $this->resetAfterTest();
1025         $this->setAdminUser();
1027         $duedate = time();
1028         $submissionsfromdate = $duedate - DAYSECS;
1029         $cutoffdate = $duedate + DAYSECS;
1030         $assign = $this->create_instance([
1031             'duedate' => $duedate,
1032             'allowsubmissionsfromdate' => $submissionsfromdate,
1033             'cutoffdate' => $cutoffdate,
1034         ]);
1035         $instance = $assign->get_instance();
1037         $event = new \calendar_event((object) [
1038             'courseid' => $instance->course,
1039             'modulename' => 'assign',
1040             'instance' => $instance->id,
1041             'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE,
1042             'timestart' => $duedate + 1
1043         ]);
1045         mod_assign_core_calendar_event_timestart_updated($event, $instance);
1047         $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1048         $this->assertEquals($duedate, $newinstance->duedate);
1049     }
1051     /**
1052      * Due date override events should not change the assignment due date.
1053      */
1054     public function test_mod_assign_core_calendar_event_timestart_updated_due_event_override() {
1055         global $CFG, $DB;
1056         require_once($CFG->dirroot . '/calendar/lib.php');
1058         $this->resetAfterTest();
1059         $this->setAdminUser();
1061         $duedate = time();
1062         $submissionsfromdate = $duedate - DAYSECS;
1063         $cutoffdate = $duedate + DAYSECS;
1064         $assign = $this->create_instance([
1065             'duedate' => $duedate,
1066             'allowsubmissionsfromdate' => $submissionsfromdate,
1067             'cutoffdate' => $cutoffdate,
1068         ]);
1069         $instance = $assign->get_instance();
1070         $userid = $this->students[0]->id;
1072         $event = new \calendar_event((object) [
1073             'courseid' => $instance->course,
1074             'modulename' => 'assign',
1075             'instance' => $instance->id,
1076             'userid' => $userid,
1077             'eventtype' => ASSIGN_EVENT_TYPE_DUE,
1078             'timestart' => $duedate + 1
1079         ]);
1081         $record = (object) [
1082             'assignid' => $instance->id,
1083             'userid' => $userid,
1084             'duedate' => $duedate + 1
1085         ];
1087         $DB->insert_record('assign_overrides', $record);
1089         mod_assign_core_calendar_event_timestart_updated($event, $instance);
1091         $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1092         $this->assertEquals($duedate, $newinstance->duedate);
1093     }
1095     /**
1096      * Due date events should update the assignment due date.
1097      */
1098     public function test_mod_assign_core_calendar_event_timestart_updated_due_event() {
1099         global $CFG, $DB;
1100         require_once($CFG->dirroot . '/calendar/lib.php');
1102         $this->resetAfterTest();
1103         $this->setAdminUser();
1105         $duedate = time();
1106         $newduedate = $duedate + 1;
1107         $submissionsfromdate = $duedate - DAYSECS;
1108         $cutoffdate = $duedate + DAYSECS;
1109         $assign = $this->create_instance([
1110             'duedate' => $duedate,
1111             'allowsubmissionsfromdate' => $submissionsfromdate,
1112             'cutoffdate' => $cutoffdate,
1113         ]);
1114         $instance = $assign->get_instance();
1116         $event = new \calendar_event((object) [
1117             'courseid' => $instance->course,
1118             'modulename' => 'assign',
1119             'instance' => $instance->id,
1120             'eventtype' => ASSIGN_EVENT_TYPE_DUE,
1121             'timestart' => $newduedate
1122         ]);
1124         mod_assign_core_calendar_event_timestart_updated($event, $instance);
1126         $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1127         $this->assertEquals($newduedate, $newinstance->duedate);
1128     }
1130     /**
1131      * If a student somehow finds a way to update the due date calendar event
1132      * then the callback should not be executed to update the assignment due
1133      * date as well otherwise that would be a security issue.
1134      */
1135     public function test_student_role_cant_update_due_event() {
1136         global $CFG, $DB;
1137         require_once($CFG->dirroot . '/calendar/lib.php');
1139         $this->resetAfterTest();
1140         $this->setAdminUser();
1142         $mapper = calendar_event_container::get_event_mapper();
1143         $generator = $this->getDataGenerator();
1144         $user = $generator->create_user();
1145         $course = $generator->create_course();
1146         $context = context_course::instance($course->id);
1147         $roleid = $generator->create_role();
1148         $now = time();
1149         $duedate = (new DateTime())->setTimestamp($now);
1150         $newduedate = (new DateTime())->setTimestamp($now)->modify('+1 day');
1151         $assign = $this->create_instance([
1152             'course' => $course->id,
1153             'duedate' => $duedate->getTimestamp(),
1154         ]);
1155         $instance = $assign->get_instance();
1157         $generator->enrol_user($user->id, $course->id, 'student');
1158         $generator->role_assign($roleid, $user->id, $context->id);
1160         $record = $DB->get_record('event', [
1161             'courseid' => $course->id,
1162             'modulename' => 'assign',
1163             'instance' => $instance->id,
1164             'eventtype' => ASSIGN_EVENT_TYPE_DUE
1165         ]);
1167         $event = new \calendar_event($record);
1169         assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
1170         assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleid, $context, true);
1172         $this->setUser($user);
1174         calendar_local_api::update_event_start_day(
1175             $mapper->from_legacy_event_to_event($event),
1176             $newduedate
1177         );
1179         $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1180         $newevent = \calendar_event::load($event->id);
1181         // The due date shouldn't have changed even though we updated the calendar
1182         // event.
1183         $this->assertEquals($duedate->getTimestamp(), $newinstance->duedate);
1184         $this->assertEquals($newduedate->getTimestamp(), $newevent->timestart);
1185     }
1187     /**
1188      * A teacher with the capability to modify an assignment module should be
1189      * able to update the assignment due date by changing the due date calendar
1190      * event.
1191      */
1192     public function test_teacher_role_can_update_due_event() {
1193         global $CFG, $DB;
1194         require_once($CFG->dirroot . '/calendar/lib.php');
1196         $this->resetAfterTest();
1197         $this->setAdminUser();
1199         $mapper = calendar_event_container::get_event_mapper();
1200         $generator = $this->getDataGenerator();
1201         $user = $generator->create_user();
1202         $course = $generator->create_course();
1203         $context = context_course::instance($course->id);
1204         $roleid = $generator->create_role();
1205         $now = time();
1206         $duedate = (new DateTime())->setTimestamp($now);
1207         $newduedate = (new DateTime())->setTimestamp($now)->modify('+1 day');
1208         $assign = $this->create_instance([
1209             'course' => $course->id,
1210             'duedate' => $duedate->getTimestamp(),
1211         ]);
1212         $instance = $assign->get_instance();
1214         $generator->enrol_user($user->id, $course->id, 'teacher');
1215         $generator->role_assign($roleid, $user->id, $context->id);
1217         $record = $DB->get_record('event', [
1218             'courseid' => $course->id,
1219             'modulename' => 'assign',
1220             'instance' => $instance->id,
1221             'eventtype' => ASSIGN_EVENT_TYPE_DUE
1222         ]);
1224         $event = new \calendar_event($record);
1226         assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
1227         assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true);
1229         $this->setUser($user);
1230         // Trigger and capture the event when adding a contact.
1231         $sink = $this->redirectEvents();
1233         calendar_local_api::update_event_start_day(
1234             $mapper->from_legacy_event_to_event($event),
1235             $newduedate
1236         );
1238         $triggeredevents = $sink->get_events();
1239         $moduleupdatedevents = array_filter($triggeredevents, function($e) {
1240             return is_a($e, 'core\event\course_module_updated');
1241         });
1243         $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1244         $newevent = \calendar_event::load($event->id);
1245         // The due date shouldn't have changed even though we updated the calendar
1246         // event.
1247         $this->assertEquals($newduedate->getTimestamp(), $newinstance->duedate);
1248         $this->assertEquals($newduedate->getTimestamp(), $newevent->timestart);
1249         // Confirm that a module updated event is fired when the module
1250         // is changed.
1251         $this->assertNotEmpty($moduleupdatedevents);
1252     }