9c758c1086a650e489ba21e22682dbb0cff23e6e
[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/generator.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  */
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() {
50         global $DB;
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',
66                 'duedate' => time(),
67                 'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
68                 'maxattempts' => 3,
69                 'submissiondrafts' => 1,
70                 'assignsubmission_onlinetext_enabled' => 1,
71             ]);
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,
81                 'nosubmissions' => 0,
82                 'assignsubmission_onlinetext_enabled' => 1,
83             ]);
85         // Open assignments should show up only if relevant.
86         $openassign = $this->create_instance($course, [
87                 'name' => 'Open Assignment',
88                 'duedate' => time(),
89                 'cutoffdate' => time() + DAYSECS,
90                 'nosubmissions' => 0,
91                 'assignsubmission_onlinetext_enabled' => 1,
92             ]);
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);
99         $overview = array();
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);
111         $overview = array();
112         assign_print_overview($courses, $overview);
113         $this->assertDebuggingCalledCount(3);
114         $this->assertEquals(0, count($overview));
116         $this->setUser($teacher);
117         $overview = array();
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);
126         $overview = array();
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);
146         $overview = array();
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);
153         $overview = array();
154         assign_print_overview($courses, $overview);
155         $this->assertDebuggingCalledCount(3);
156         // Now assignment 4 should not show up.
157         $this->assertEmpty($overview);
158     }
160     /**
161      * Test that assign_print_overview does not return any assignments which are Open Offline.
162      */
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),
172             ]);
174         $this->setUser($student);
175         $overview = [];
176         assign_print_overview([$course], $overview);
178         $this->assertDebuggingCalledCount(1);
179         $this->assertEquals(0, count($overview));
180     }
182     /**
183      * Test that assign_print_recent_activity shows ungraded submitted assignments.
184      */
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);
196     }
198     /**
199      * Test that assign_print_recent_activity does not display any warnings when a custom fullname has been configured.
200      */
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);
213     }
215     /**
216      * Test that assign_print_recent_activity shows the blind marking ID.
217      */
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, [
225                 'blindmarking' => 1,
226             ]);
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);
235     }
237     /**
238      * Test that assign_get_recent_mod_activity fetches the assignment correctly.
239      */
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);
249         $index = 1;
250         $activities = [
251             $index => (object) [
252                 'type' => 'assign',
253                 'cmid' => $assign->get_course_module()->id,
254             ],
255         ];
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);
263     }
265     /**
266      * Ensure that assign_user_complete displays information about drafts.
267      */
268     public function test_assign_user_complete() {
269         global $PAGE, $DB;
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());
286     }
288     /**
289      * Ensure that assign_user_outline fetches updated grades.
290      */
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);
310     }
312     /**
313      * Ensure that assign_get_completion_state reflects the correct status at each point.
314      */
315     public function test_assign_get_completion_state() {
316         global $DB;
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
325             ]);
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);
342     }
344     /**
345      * Tests for mod_assign_refresh_events.
346      */
347     public function test_assign_refresh_events() {
348         global $DB;
350         $this->resetAfterTest();
352         $duedate = time();
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,
362             ]);
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'));
411     }
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));
427     }
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));
444     }
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));
459     }
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));
474     }
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);
493     }
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());
516     }
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());
538     }
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());
560     }
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);
585     }
587     /**
588      * Creates an action event.
589      *
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
594      */
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);
606     }
608     /**
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.
612      */
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) [
627             'customdata' => [
628                 'customcompletionrules' => [
629                     'completionsubmit' => '1',
630                 ],
631             ],
632             'completion' => 2,
633         ];
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()), []);
640     }
642     /**
643      * Test that if some grades are not set, they are left alone and not rescaled
644      */
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');
652         // As a teacher.
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);
663         // Rescale.
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);
673     }
675     /**
676      * Return false when there are not overrides for this assign instance.
677      */
678     public function test_assign_is_override_calendar_event_no_override() {
679         global $CFG, $DB;
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();
688         $duedate = time();
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,
696         ]);
698         $this->assertFalse($assign->is_override_calendar_event($event));
699     }
701     /**
702      * Return false if the given event isn't an assign module event.
703      */
704     public function test_assign_is_override_calendar_event_no_nodule_event() {
705         global $CFG, $DB;
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;
715         $duedate = time();
716         $assign = $this->create_instance($course, ['duedate' => $duedate]);
718         $instance = $assign->get_instance();
719         $event = new \calendar_event((object)[
720             'userid' => $userid
721         ]);
723         $this->assertFalse($assign->is_override_calendar_event($event));
724     }
726     /**
727      * Return false if there is overrides for this use but they belong to another assign
728      * instance.
729      */
730     public function test_assign_is_override_calendar_event_different_assign_instance() {
731         global $CFG, $DB;
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();
740         $duedate = time();
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,
751         ]);
753         $DB->insert_record('assign_overrides', (object) [
754                 'assignid' => $otherinstance->id,
755                 'userid' => $student->id,
756             ]);
758         $this->assertFalse($assign->is_override_calendar_event($event));
759     }
761     /**
762      * Return true if there is a user override for this event and assign instance.
763      */
764     public function test_assign_is_override_calendar_event_user_override() {
765         global $CFG, $DB;
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();
774         $duedate = time();
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,
782         ]);
785         $DB->insert_record('assign_overrides', (object) [
786                 'assignid' => $instance->id,
787                 'userid' => $student->id,
788             ]);
790         $this->assertTrue($assign->is_override_calendar_event($event));
791     }
793     /**
794      * Return true if there is a group override for the event and assign instance.
795      */
796     public function test_assign_is_override_calendar_event_group_override() {
797         global $CFG, $DB;
798         require_once($CFG->dirroot . '/calendar/lib.php');
800         $this->resetAfterTest();
801         $course = $this->getDataGenerator()->create_course();
803         $this->setAdminUser();
805         $duedate = time();
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,
814         ]);
816         $DB->insert_record('assign_overrides', (object) [
817                 'assignid' => $instance->id,
818                 'groupid' => $group->id,
819             ]);
821         $this->assertTrue($assign->is_override_calendar_event($event));
822     }
824     /**
825      * Unknown event types should not have any limit restrictions returned.
826      */
827     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_unkown_event_type() {
828         global $CFG;
829         require_once($CFG->dirroot . '/calendar/lib.php');
831         $this->resetAfterTest();
832         $course = $this->getDataGenerator()->create_course();
834         $this->setAdminUser();
836         $duedate = time();
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'
845         ]);
847         list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
848         $this->assertNull($min);
849         $this->assertNull($max);
850     }
852     /**
853      * Override events should not have any limit restrictions returned.
854      */
855     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_override_event() {
856         global $CFG, $DB;
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();
865         $duedate = time();
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
875         ]);
877         $record = (object) [
878             'assignid' => $instance->id,
879             'userid' => $student->id,
880         ];
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);
887     }
889     /**
890      * Assignments configured without a submissions from and cutoff date should not have
891      * any limits applied.
892      */
893     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_due_no_limit() {
894         global $CFG, $DB;
895         require_once($CFG->dirroot . '/calendar/lib.php');
897         $this->resetAfterTest();
898         $course = $this->getDataGenerator()->create_course();
900         $this->setAdminUser();
902         $duedate = time();
903         $assign = $this->create_instance($course, [
904             'duedate' => $duedate,
905             'allowsubmissionsfromdate' => 0,
906             'cutoffdate' => 0,
907         ]);
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
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         $course = $this->getDataGenerator()->create_course();
933         $this->setAdminUser();
935         $duedate = time();
936         $submissionsfromdate = $duedate - DAYSECS;
937         $cutoffdate = $duedate + DAYSECS;
938         $assign = $this->create_instance($course, [
939             'duedate' => $duedate,
940             'allowsubmissionsfromdate' => $submissionsfromdate,
941             'cutoffdate' => $cutoffdate,
942         ]);
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
950         ]);
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]);
957     }
959     /**
960      * Assignment grading due date should not have any limits of no due date and cutoff date is set.
961      */
962     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_gradingdue_no_limit() {
963         global $CFG, $DB;
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, [
972             'duedate' => 0,
973             'allowsubmissionsfromdate' => 0,
974             'cutoffdate' => 0,
975         ]);
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
983         ]);
985         list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
986         $this->assertNull($min);
987         $this->assertNull($max);
988     }
990     /**
991      * Assignment grading due event is minimum bound by the due date, if it is set.
992      */
993     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_gradingdue_with_due_date() {
994         global $CFG, $DB;
995         require_once($CFG->dirroot . '/calendar/lib.php');
997         $this->resetAfterTest();
998         $course = $this->getDataGenerator()->create_course();
1000         $this->setAdminUser();
1002         $duedate = time();
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
1011         ]);
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);
1017     }
1019     /**
1020      * Non due date events should not update the assignment due date.
1021      */
1022     public function test_mod_assign_core_calendar_event_timestart_updated_non_due_event() {
1023         global $CFG, $DB;
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();
1032         $duedate = time();
1033         $submissionsfromdate = $duedate - DAYSECS;
1034         $cutoffdate = $duedate + DAYSECS;
1035         $assign = $this->create_instance($course, [
1036             'duedate' => $duedate,
1037             'allowsubmissionsfromdate' => $submissionsfromdate,
1038             'cutoffdate' => $cutoffdate,
1039         ]);
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
1048         ]);
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);
1054     }
1056     /**
1057      * Due date override events should not change the assignment due date.
1058      */
1059     public function test_mod_assign_core_calendar_event_timestart_updated_due_event_override() {
1060         global $CFG, $DB;
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();
1069         $duedate = time();
1070         $submissionsfromdate = $duedate - DAYSECS;
1071         $cutoffdate = $duedate + DAYSECS;
1072         $assign = $this->create_instance($course, [
1073             'duedate' => $duedate,
1074             'allowsubmissionsfromdate' => $submissionsfromdate,
1075             'cutoffdate' => $cutoffdate,
1076         ]);
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
1086         ]);
1088         $record = (object) [
1089             'assignid' => $instance->id,
1090             'userid' => $student->id,
1091             'duedate' => $duedate + 1,
1092         ];
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);
1100     }
1102     /**
1103      * Due date events should update the assignment due date.
1104      */
1105     public function test_mod_assign_core_calendar_event_timestart_updated_due_event() {
1106         global $CFG, $DB;
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();
1115         $duedate = time();
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,
1123         ]);
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
1132         ]);
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);
1138     }
1140     /**
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.
1144      */
1145     public function test_student_role_cant_update_due_event() {
1146         global $CFG, $DB;
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();
1160         $now = time();
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(),
1166         ]);
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
1174         ]);
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),
1185             $newduedate
1186         );
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
1191         // event.
1192         $this->assertEquals($duedate->getTimestamp(), $newinstance->duedate);
1193         $this->assertEquals($newduedate->getTimestamp(), $newevent->timestart);
1194     }
1196     /**
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
1199      * event.
1200      */
1201     public function test_teacher_role_can_update_due_event() {
1202         global $CFG, $DB;
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();
1214         $now = time();
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(),
1220         ]);
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
1228         ]);
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),
1241             $newduedate
1242         );
1244         $triggeredevents = $sink->get_events();
1245         $moduleupdatedevents = array_filter($triggeredevents, function($e) {
1246             return is_a($e, 'core\event\course_module_updated');
1247         });
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
1252         // event.
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
1256         // is changed.
1257         $this->assertNotEmpty($moduleupdatedevents);
1258     }