MDL-55609 mod_assign: Remove shared setUp for all tests
[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->assertRegExp('/.*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->assertRegExp('/.*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         $this->assertDebuggingCalledCount(3);
149         $this->assertEquals(1, count($overview));
150         // Now assignment 4 should not show up.
151         $this->assertNotRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']);
152         $this->assertRegExp('/.*Assignment with submissions.*/', $overview[$course->id]['assign']);
154         $this->setUser($teacher);
155         $overview = array();
156         assign_print_overview($courses, $overview);
157         $this->assertDebuggingCalledCount(3);
158         $this->assertEquals(1, count($overview));
159         // Now assignment 4 should not show up.
160         $this->assertNotRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']);
161         $this->assertRegExp('/.*Assignment with submissions.*/', $overview[$course->id]['assign']);
162     }
164     /**
165      * Test that assign_print_overview does not return any assignments which are Open Offline.
166      */
167     public function test_assign_print_overview_open_offline() {
168         $this->resetAfterTest();
169         $course = $this->getDataGenerator()->create_course();
170         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
172         $this->setAdminUser();
173         $openassign = $this->create_instance($course, [
174                 'duedate' => time() + DAYSECS,
175                 'cutoffdate' => time() + (DAYSECS * 2),
176             ]);
178         $this->setUser($student);
179         $overview = [];
180         assign_print_overview([$course], $overview);
182         $this->assertDebuggingCalledCount(1);
183         $this->assertEquals(0, count($overview));
184     }
186     /**
187      * Test that assign_print_recent_activity shows ungraded submitted assignments.
188      */
189     public function test_print_recent_activity() {
190         $this->resetAfterTest();
191         $course = $this->getDataGenerator()->create_course();
192         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
193         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
194         $assign = $this->create_instance($course);
195         $this->submit_for_grading($student, $assign);
197         $this->setUser($teacher);
198         $this->expectOutputRegex('/submitted:/');
199         assign_print_recent_activity($course, true, time() - 3600);
200     }
202     /**
203      * Test that assign_print_recent_activity does not display any warnings when a custom fullname has been configured.
204      */
205     public function test_print_recent_activity_fullname() {
206         $this->resetAfterTest();
207         $course = $this->getDataGenerator()->create_course();
208         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
209         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
210         $assign = $this->create_instance($course);
211         $this->submit_for_grading($student, $assign);
213         $this->setUser($teacher);
214         $this->expectOutputRegex('/submitted:/');
215         set_config('fullnamedisplay', 'firstname, lastnamephonetic');
216         assign_print_recent_activity($course, false, time() - 3600);
217     }
219     /**
220      * Test that assign_print_recent_activity shows the blind marking ID.
221      */
222     public function test_print_recent_activity_fullname_blind_marking() {
223         $this->resetAfterTest();
224         $course = $this->getDataGenerator()->create_course();
225         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
226         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
228         $assign = $this->create_instance($course, [
229                 'blindmarking' => 1,
230             ]);
231         $this->add_submission($student, $assign);
232         $this->submit_for_grading($student, $assign);
234         $this->setUser($teacher);
235         $uniqueid = $assign->get_uniqueid_for_user($student->id);
236         $expectedstr = preg_quote(get_string('participant', 'mod_assign'), '/') . '.*' . $uniqueid;
237         $this->expectOutputRegex("/{$expectedstr}/");
238         assign_print_recent_activity($course, false, time() - 3600);
239     }
241     /**
242      * Test that assign_get_recent_mod_activity fetches the assignment correctly.
243      */
244     public function test_assign_get_recent_mod_activity() {
245         $this->resetAfterTest();
246         $course = $this->getDataGenerator()->create_course();
247         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
248         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
249         $assign = $this->create_instance($course);
250         $this->add_submission($student, $assign);
251         $this->submit_for_grading($student, $assign);
253         $index = 1;
254         $activities = [
255             $index => (object) [
256                 'type' => 'assign',
257                 'cmid' => $assign->get_course_module()->id,
258             ],
259         ];
261         $this->setUser($teacher);
262         assign_get_recent_mod_activity($activities, $index, time() - HOURSECS, $course->id, $assign->get_course_module()->id);
264         $activity = $activities[1];
265         $this->assertEquals("assign", $activity->type);
266         $this->assertEquals($student->id, $activity->user->id);
267     }
269     /**
270      * Ensure that assign_user_complete displays information about drafts.
271      */
272     public function test_assign_user_complete() {
273         global $PAGE, $DB;
275         $this->resetAfterTest();
276         $course = $this->getDataGenerator()->create_course();
277         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
278         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
279         $assign = $this->create_instance($course, ['submissiondrafts' => 1]);
280         $this->add_submission($student, $assign);
282         $PAGE->set_url(new moodle_url('/mod/assign/view.php', array('id' => $assign->get_course_module()->id)));
284         $submission = $assign->get_user_submission($student->id, true);
285         $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
286         $DB->update_record('assign_submission', $submission);
288         $this->expectOutputRegex('/Draft/');
289         assign_user_complete($course, $student, $assign->get_course_module(), $assign->get_instance());
290     }
292     /**
293      * Ensure that assign_user_outline fetches updated grades.
294      */
295     public function test_assign_user_outline() {
296         $this->resetAfterTest();
297         $course = $this->getDataGenerator()->create_course();
298         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
299         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
300         $assign = $this->create_instance($course);
302         $this->add_submission($student, $assign);
303         $this->submit_for_grading($student, $assign);
304         $this->mark_submission($teacher, $assign, $student, 50.0);
306         $this->setUser($teacher);
307         $data = $assign->get_user_grade($student->id, true);
308         $data->grade = '50.5';
309         $assign->update_grade($data);
311         $result = assign_user_outline($course, $student, $assign->get_course_module(), $assign->get_instance());
313         $this->assertRegExp('/50.5/', $result->info);
314     }
316     /**
317      * Ensure that assign_get_completion_state reflects the correct status at each point.
318      */
319     public function test_assign_get_completion_state() {
320         global $DB;
322         $this->resetAfterTest();
323         $course = $this->getDataGenerator()->create_course();
324         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
325         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
326         $assign = $this->create_instance($course, [
327                 'submissiondrafts' => 0,
328                 'completionsubmit' => 1
329             ]);
331         $this->setUser($student);
332         $result = assign_get_completion_state($course, $assign->get_course_module(), $student->id, false);
333         $this->assertFalse($result);
335         $this->add_submission($student, $assign);
336         $result = assign_get_completion_state($course, $assign->get_course_module(), $student->id, false);
337         $this->assertFalse($result);
339         $this->submit_for_grading($student, $assign);
340         $result = assign_get_completion_state($course, $assign->get_course_module(), $student->id, false);
341         $this->assertTrue($result);
343         $this->mark_submission($teacher, $assign, $student, 50.0);
344         $result = assign_get_completion_state($course, $assign->get_course_module(), $student->id, false);
345         $this->assertTrue($result);
346     }
348     /**
349      * Tests for mod_assign_refresh_events.
350      */
351     public function test_assign_refresh_events() {
352         global $DB;
354         $this->resetAfterTest();
356         $duedate = time();
357         $newduedate = $duedate + DAYSECS;
359         $this->setAdminUser();
361         $course = $this->getDataGenerator()->create_course();
362         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
363         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
364         $assign = $this->create_instance($course, [
365                 'duedate' => $duedate,
366             ]);
368         $instance = $assign->get_instance();
369         $eventparams = ['modulename' => 'assign', 'instance' => $instance->id];
371         // Make sure the calendar event for assignment 1 matches the initial due date.
372         $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST);
373         $this->assertEquals($eventtime, $duedate);
375         // Manually update assignment 1's due date.
376         $DB->update_record('assign', (object) ['id' => $instance->id, 'duedate' => $newduedate]);
378         // Then refresh the assignment events of assignment 1's course.
379         $this->assertTrue(assign_refresh_events($course->id));
381         // Confirm that the assignment 1's due date event now has the new due date after refresh.
382         $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST);
383         $this->assertEquals($eventtime, $newduedate);
385         // Create a second course and assignment.
386         $othercourse = $this->getDataGenerator()->create_course();;
387         $otherassign = $this->create_instance($othercourse, ['duedate' => $duedate, 'course' => $othercourse->id]);
388         $otherinstance = $otherassign->get_instance();
390         // Manually update assignment 1 and 2's due dates.
391         $newduedate += DAYSECS;
392         $DB->update_record('assign', (object)['id' => $instance->id, 'duedate' => $newduedate]);
393         $DB->update_record('assign', (object)['id' => $otherinstance->id, 'duedate' => $newduedate]);
395         // Refresh events of all courses.
396         $this->assertTrue(assign_refresh_events());
398         // Check the due date calendar event for assignment 1.
399         $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST);
400         $this->assertEquals($eventtime, $newduedate);
402         // Check the due date calendar event for assignment 2.
403         $eventparams['instance'] = $otherinstance->id;
404         $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST);
405         $this->assertEquals($eventtime, $newduedate);
407         // In case the course ID is passed as a numeric string.
408         $this->assertTrue(assign_refresh_events('' . $course->id));
410         // Non-existing course ID.
411         $this->assertFalse(assign_refresh_events(-1));
413         // Invalid course ID.
414         $this->assertFalse(assign_refresh_events('aaa'));
415     }
417     public function test_assign_core_calendar_is_event_visible_duedate_event_as_teacher() {
418         $this->resetAfterTest();
419         $course = $this->getDataGenerator()->create_course();
420         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
421         $assign = $this->create_instance($course);
423         $this->setAdminUser();
425         // Create a calendar event.
426         $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE);
428         // The teacher should see the due date event.
429         $this->setUser($teacher);
430         $this->assertTrue(mod_assign_core_calendar_is_event_visible($event));
431     }
433     public function test_assign_core_calendar_is_event_visible_duedate_event_as_student() {
434         $this->resetAfterTest();
435         $course = $this->getDataGenerator()->create_course();
436         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
437         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
438         $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
440         $this->setAdminUser();
442         // Create a calendar event.
443         $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE);
445         // The student should care about the due date event.
446         $this->setUser($student);
447         $this->assertTrue(mod_assign_core_calendar_is_event_visible($event));
448     }
450     public function test_assign_core_calendar_is_event_visible_gradingduedate_event_as_teacher() {
451         $this->resetAfterTest();
452         $course = $this->getDataGenerator()->create_course();
453         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
454         $assign = $this->create_instance($course);
456         // Create a calendar event.
457         $this->setAdminUser();
458         $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE);
460         // The teacher should see the due date event.
461         $this->setUser($teacher);
462         $this->assertTrue(mod_assign_core_calendar_is_event_visible($event));
463     }
465     public function test_assign_core_calendar_is_event_visible_gradingduedate_event_as_student() {
466         $this->resetAfterTest();
467         $course = $this->getDataGenerator()->create_course();
468         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
469         $assign = $this->create_instance($course);
471         // Create a calendar event.
472         $this->setAdminUser();
473         $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE);
475         // The student should not see the due date event.
476         $this->setUser($student);
477         $this->assertFalse(mod_assign_core_calendar_is_event_visible($event));
478     }
480     public function test_assign_core_calendar_provide_event_action_duedate_as_teacher() {
481         $this->resetAfterTest();
482         $course = $this->getDataGenerator()->create_course();
483         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
484         $assign = $this->create_instance($course);
486         // Create a calendar event.
487         $this->setAdminUser();
488         $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE);
490         // The teacher should see the event.
491         $this->setUser($teacher);
492         $factory = new \core_calendar\action_factory();
493         $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
495         // The teacher should not have an action for a due date event.
496         $this->assertNull($actionevent);
497     }
499     public function test_assign_core_calendar_provide_event_action_duedate_as_student() {
500         $this->resetAfterTest();
501         $course = $this->getDataGenerator()->create_course();
502         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
503         $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
505         // Create a calendar event.
506         $this->setAdminUser();
507         $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE);
509         // The student should see the event.
510         $this->setUser($student);
511         $factory = new \core_calendar\action_factory();
512         $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
514         // Confirm the event was decorated.
515         $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
516         $this->assertEquals(get_string('addsubmission', 'assign'), $actionevent->get_name());
517         $this->assertInstanceOf('moodle_url', $actionevent->get_url());
518         $this->assertEquals(1, $actionevent->get_item_count());
519         $this->assertTrue($actionevent->is_actionable());
520     }
522     public function test_assign_core_calendar_provide_event_action_gradingduedate_as_teacher() {
523         $this->resetAfterTest();
524         $course = $this->getDataGenerator()->create_course();
525         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
526         $assign = $this->create_instance($course);
528         // Create a calendar event.
529         $this->setAdminUser();
530         $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE);
532         $this->setUser($teacher);
533         $factory = new \core_calendar\action_factory();
534         $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
536         // Confirm the event was decorated.
537         $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
538         $this->assertEquals(get_string('grade'), $actionevent->get_name());
539         $this->assertInstanceOf('moodle_url', $actionevent->get_url());
540         $this->assertEquals(0, $actionevent->get_item_count());
541         $this->assertTrue($actionevent->is_actionable());
542     }
544     public function test_assign_core_calendar_provide_event_action_gradingduedate_as_student() {
545         $this->resetAfterTest();
546         $course = $this->getDataGenerator()->create_course();
547         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
548         $assign = $this->create_instance($course);
550         // Create a calendar event.
551         $this->setAdminUser();
552         $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE);
554         $this->setUser($student);
555         $factory = new \core_calendar\action_factory();
556         $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
558         // Confirm the event was decorated.
559         $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
560         $this->assertEquals(get_string('grade'), $actionevent->get_name());
561         $this->assertInstanceOf('moodle_url', $actionevent->get_url());
562         $this->assertEquals(0, $actionevent->get_item_count());
563         $this->assertFalse($actionevent->is_actionable());
564     }
566     public function test_assign_core_calendar_provide_event_action_duedate_as_student_submitted() {
567         $this->resetAfterTest();
568         $course = $this->getDataGenerator()->create_course();
569         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
570         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
571         $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
573         $this->setAdminUser();
575         // Create a calendar event.
576         $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE);
578         // Create an action factory.
579         $factory = new \core_calendar\action_factory();
581         // Submit as the student.
582         $this->add_submission($student, $assign);
583         $this->submit_for_grading($student, $assign);
585         // Confirm there was no event to action.
586         $factory = new \core_calendar\action_factory();
587         $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory);
588         $this->assertNull($actionevent);
589     }
591     /**
592      * Creates an action event.
593      *
594      * @param \stdClass $course The course the assignment is in
595      * @param assign $assign The assignment to create an event for
596      * @param string $eventtype The event type. eg. ASSIGN_EVENT_TYPE_DUE.
597      * @return bool|calendar_event
598      */
599     private function create_action_event($course, $assign, $eventtype) {
600         $event = new stdClass();
601         $event->name = 'Calendar event';
602         $event->modulename  = 'assign';
603         $event->courseid = $course->id;
604         $event->instance = $assign->get_instance()->id;
605         $event->type = CALENDAR_EVENT_TYPE_ACTION;
606         $event->eventtype = $eventtype;
607         $event->timestart = time();
609         return calendar_event::create($event);
610     }
612     /**
613      * Test the callback responsible for returning the completion rule descriptions.
614      * This function should work given either an instance of the module (cm_info), such as when checking the active rules,
615      * or if passed a stdClass of similar structure, such as when checking the the default completion settings for a mod type.
616      */
617     public function test_mod_assign_completion_get_active_rule_descriptions() {
618         $this->resetAfterTest();
619         $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
621         $this->setAdminUser();
623         // Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't.
624         $cm1 = $this->create_instance($course, ['completion' => '2', 'completionsubmit' => '1'])->get_course_module();
625         $cm2 = $this->create_instance($course, ['completion' => '2', 'completionsubmit' => '0'])->get_course_module();
627         // Data for the stdClass input type.
628         // This type of input would occur when checking the default completion rules for an activity type, where we don't have
629         // any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info.
630         $moddefaults = (object) [
631             'customdata' => [
632                 'customcompletionrules' => [
633                     'completionsubmit' => '1',
634                 ],
635             ],
636             'completion' => 2,
637         ];
639         $activeruledescriptions = [get_string('completionsubmit', 'assign')];
640         $this->assertEquals(mod_assign_get_completion_active_rule_descriptions($cm1), $activeruledescriptions);
641         $this->assertEquals(mod_assign_get_completion_active_rule_descriptions($cm2), []);
642         $this->assertEquals(mod_assign_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
643         $this->assertEquals(mod_assign_get_completion_active_rule_descriptions(new stdClass()), []);
644     }
646     /**
647      * Test that if some grades are not set, they are left alone and not rescaled
648      */
649     public function test_assign_rescale_activity_grades_some_unset() {
650         $this->resetAfterTest();
651         $course = $this->getDataGenerator()->create_course();
652         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
653         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
654         $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
656         // As a teacher.
657         $this->setUser($teacher);
658         $assign = $this->create_instance($course);
660         // Grade the student.
661         $data = ['grade' => 50];
662         $assign->testable_apply_grade_to_user((object)$data, $student->id, 0);
664         // Try getting another students grade. This will give a grade of ASSIGN_GRADE_NOT_SET (-1).
665         $assign->get_user_grade($otherstudent->id, true);
667         // Rescale.
668         assign_rescale_activity_grades($course, $assign->get_course_module(), 0, 100, 0, 10);
670         // Get the grades for both students.
671         $studentgrade = $assign->get_user_grade($student->id, true);
672         $otherstudentgrade = $assign->get_user_grade($otherstudent->id, true);
674         // Make sure the real grade is scaled, but the ASSIGN_GRADE_NOT_SET stays the same.
675         $this->assertEquals($studentgrade->grade, 5);
676         $this->assertEquals($otherstudentgrade->grade, ASSIGN_GRADE_NOT_SET);
677     }
679     /**
680      * Return false when there are not overrides for this assign instance.
681      */
682     public function test_assign_is_override_calendar_event_no_override() {
683         global $CFG, $DB;
684         require_once($CFG->dirroot . '/calendar/lib.php');
686         $this->resetAfterTest();
687         $course = $this->getDataGenerator()->create_course();
688         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
690         $this->setAdminUser();
692         $duedate = time();
693         $assign = $this->create_instance($course, ['duedate' => $duedate]);
695         $instance = $assign->get_instance();
696         $event = new \calendar_event((object)[
697             'modulename' => 'assign',
698             'instance' => $instance->id,
699             'userid' => $student->id,
700         ]);
702         $this->assertFalse($assign->is_override_calendar_event($event));
703     }
705     /**
706      * Return false if the given event isn't an assign module event.
707      */
708     public function test_assign_is_override_calendar_event_no_nodule_event() {
709         global $CFG, $DB;
710         require_once($CFG->dirroot . '/calendar/lib.php');
712         $this->resetAfterTest();
713         $course = $this->getDataGenerator()->create_course();
714         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
716         $this->setAdminUser();
718         $userid = $student->id;
719         $duedate = time();
720         $assign = $this->create_instance($course, ['duedate' => $duedate]);
722         $instance = $assign->get_instance();
723         $event = new \calendar_event((object)[
724             'userid' => $userid
725         ]);
727         $this->assertFalse($assign->is_override_calendar_event($event));
728     }
730     /**
731      * Return false if there is overrides for this use but they belong to another assign
732      * instance.
733      */
734     public function test_assign_is_override_calendar_event_different_assign_instance() {
735         global $CFG, $DB;
736         require_once($CFG->dirroot . '/calendar/lib.php');
738         $this->resetAfterTest();
739         $course = $this->getDataGenerator()->create_course();
740         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
742         $this->setAdminUser();
744         $duedate = time();
745         $assign = $this->create_instance($course, ['duedate' => $duedate]);
746         $instance = $assign->get_instance();
748         $otherassign = $this->create_instance($course, ['duedate' => $duedate]);
749         $otherinstance = $otherassign->get_instance();
751         $event = new \calendar_event((object) [
752             'modulename' => 'assign',
753             'instance' => $instance->id,
754             'userid' => $student->id,
755         ]);
757         $DB->insert_record('assign_overrides', (object) [
758                 'assignid' => $otherinstance->id,
759                 'userid' => $student->id,
760             ]);
762         $this->assertFalse($assign->is_override_calendar_event($event));
763     }
765     /**
766      * Return true if there is a user override for this event and assign instance.
767      */
768     public function test_assign_is_override_calendar_event_user_override() {
769         global $CFG, $DB;
770         require_once($CFG->dirroot . '/calendar/lib.php');
772         $this->resetAfterTest();
773         $course = $this->getDataGenerator()->create_course();
774         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
776         $this->setAdminUser();
778         $duedate = time();
779         $assign = $this->create_instance($course, ['duedate' => $duedate]);
781         $instance = $assign->get_instance();
782         $event = new \calendar_event((object) [
783             'modulename' => 'assign',
784             'instance' => $instance->id,
785             'userid' => $student->id,
786         ]);
789         $DB->insert_record('assign_overrides', (object) [
790                 'assignid' => $instance->id,
791                 'userid' => $student->id,
792             ]);
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         $course = $this->getDataGenerator()->create_course();
807         $this->setAdminUser();
809         $duedate = time();
810         $assign = $this->create_instance($course, ['duedate' => $duedate]);
811         $instance = $assign->get_instance();
812         $group = $this->getDataGenerator()->create_group(array('courseid' => $instance->course));
814         $event = new \calendar_event((object) [
815             'modulename' => 'assign',
816             'instance' => $instance->id,
817             'groupid' => $group->id,
818         ]);
820         $DB->insert_record('assign_overrides', (object) [
821                 'assignid' => $instance->id,
822                 'groupid' => $group->id,
823             ]);
825         $this->assertTrue($assign->is_override_calendar_event($event));
826     }
828     /**
829      * Unknown event types should not have any limit restrictions returned.
830      */
831     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_unkown_event_type() {
832         global $CFG;
833         require_once($CFG->dirroot . '/calendar/lib.php');
835         $this->resetAfterTest();
836         $course = $this->getDataGenerator()->create_course();
838         $this->setAdminUser();
840         $duedate = time();
841         $assign = $this->create_instance($course, ['duedate' => $duedate]);
842         $instance = $assign->get_instance();
844         $event = new \calendar_event((object) [
845             'courseid' => $instance->course,
846             'modulename' => 'assign',
847             'instance' => $instance->id,
848             'eventtype' => 'SOME RANDOM EVENT'
849         ]);
851         list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
852         $this->assertNull($min);
853         $this->assertNull($max);
854     }
856     /**
857      * Override events should not have any limit restrictions returned.
858      */
859     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_override_event() {
860         global $CFG, $DB;
861         require_once($CFG->dirroot . '/calendar/lib.php');
863         $this->resetAfterTest();
864         $course = $this->getDataGenerator()->create_course();
865         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
867         $this->setAdminUser();
869         $duedate = time();
870         $assign = $this->create_instance($course, ['duedate' => $duedate]);
871         $instance = $assign->get_instance();
873         $event = new \calendar_event((object) [
874             'courseid' => $instance->course,
875             'modulename' => 'assign',
876             'instance' => $instance->id,
877             'userid' => $student->id,
878             'eventtype' => ASSIGN_EVENT_TYPE_DUE
879         ]);
881         $record = (object) [
882             'assignid' => $instance->id,
883             'userid' => $student->id,
884         ];
886         $DB->insert_record('assign_overrides', $record);
888         list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
889         $this->assertFalse($min);
890         $this->assertFalse($max);
891     }
893     /**
894      * Assignments configured without a submissions from and cutoff date should not have
895      * any limits applied.
896      */
897     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_due_no_limit() {
898         global $CFG, $DB;
899         require_once($CFG->dirroot . '/calendar/lib.php');
901         $this->resetAfterTest();
902         $course = $this->getDataGenerator()->create_course();
904         $this->setAdminUser();
906         $duedate = time();
907         $assign = $this->create_instance($course, [
908             'duedate' => $duedate,
909             'allowsubmissionsfromdate' => 0,
910             'cutoffdate' => 0,
911         ]);
912         $instance = $assign->get_instance();
914         $event = new \calendar_event((object) [
915             'courseid' => $instance->course,
916             'modulename' => 'assign',
917             'instance' => $instance->id,
918             'eventtype' => ASSIGN_EVENT_TYPE_DUE
919         ]);
921         list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
922         $this->assertNull($min);
923         $this->assertNull($max);
924     }
926     /**
927      * Assignments should be bottom and top bound by the submissions from date and cutoff date
928      * respectively.
929      */
930     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_due_with_limits() {
931         global $CFG, $DB;
932         require_once($CFG->dirroot . '/calendar/lib.php');
934         $this->resetAfterTest();
935         $course = $this->getDataGenerator()->create_course();
937         $this->setAdminUser();
939         $duedate = time();
940         $submissionsfromdate = $duedate - DAYSECS;
941         $cutoffdate = $duedate + DAYSECS;
942         $assign = $this->create_instance($course, [
943             'duedate' => $duedate,
944             'allowsubmissionsfromdate' => $submissionsfromdate,
945             'cutoffdate' => $cutoffdate,
946         ]);
947         $instance = $assign->get_instance();
949         $event = new \calendar_event((object) [
950             'courseid' => $instance->course,
951             'modulename' => 'assign',
952             'instance' => $instance->id,
953             'eventtype' => ASSIGN_EVENT_TYPE_DUE
954         ]);
956         list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
957         $this->assertEquals($submissionsfromdate, $min[0]);
958         $this->assertNotEmpty($min[1]);
959         $this->assertEquals($cutoffdate, $max[0]);
960         $this->assertNotEmpty($max[1]);
961     }
963     /**
964      * Assignment grading due date should not have any limits of no due date and cutoff date is set.
965      */
966     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_gradingdue_no_limit() {
967         global $CFG, $DB;
968         require_once($CFG->dirroot . '/calendar/lib.php');
970         $this->resetAfterTest();
971         $course = $this->getDataGenerator()->create_course();
973         $this->setAdminUser();
975         $assign = $this->create_instance($course, [
976             'duedate' => 0,
977             'allowsubmissionsfromdate' => 0,
978             'cutoffdate' => 0,
979         ]);
980         $instance = $assign->get_instance();
982         $event = new \calendar_event((object) [
983             'courseid' => $instance->course,
984             'modulename' => 'assign',
985             'instance' => $instance->id,
986             'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE
987         ]);
989         list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
990         $this->assertNull($min);
991         $this->assertNull($max);
992     }
994     /**
995      * Assignment grading due event is minimum bound by the due date, if it is set.
996      */
997     public function test_mod_assign_core_calendar_get_valid_event_timestart_range_gradingdue_with_due_date() {
998         global $CFG, $DB;
999         require_once($CFG->dirroot . '/calendar/lib.php');
1001         $this->resetAfterTest();
1002         $course = $this->getDataGenerator()->create_course();
1004         $this->setAdminUser();
1006         $duedate = time();
1007         $assign = $this->create_instance($course, ['duedate' => $duedate]);
1008         $instance = $assign->get_instance();
1010         $event = new \calendar_event((object) [
1011             'courseid' => $instance->course,
1012             'modulename' => 'assign',
1013             'instance' => $instance->id,
1014             'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE
1015         ]);
1017         list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
1018         $this->assertEquals($duedate, $min[0]);
1019         $this->assertNotEmpty($min[1]);
1020         $this->assertNull($max);
1021     }
1023     /**
1024      * Non due date events should not update the assignment due date.
1025      */
1026     public function test_mod_assign_core_calendar_event_timestart_updated_non_due_event() {
1027         global $CFG, $DB;
1028         require_once($CFG->dirroot . '/calendar/lib.php');
1030         $this->resetAfterTest();
1031         $course = $this->getDataGenerator()->create_course();
1032         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1034         $this->setAdminUser();
1036         $duedate = time();
1037         $submissionsfromdate = $duedate - DAYSECS;
1038         $cutoffdate = $duedate + DAYSECS;
1039         $assign = $this->create_instance($course, [
1040             'duedate' => $duedate,
1041             'allowsubmissionsfromdate' => $submissionsfromdate,
1042             'cutoffdate' => $cutoffdate,
1043         ]);
1044         $instance = $assign->get_instance();
1046         $event = new \calendar_event((object) [
1047             'courseid' => $instance->course,
1048             'modulename' => 'assign',
1049             'instance' => $instance->id,
1050             'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE,
1051             'timestart' => $duedate + 1
1052         ]);
1054         mod_assign_core_calendar_event_timestart_updated($event, $instance);
1056         $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1057         $this->assertEquals($duedate, $newinstance->duedate);
1058     }
1060     /**
1061      * Due date override events should not change the assignment due date.
1062      */
1063     public function test_mod_assign_core_calendar_event_timestart_updated_due_event_override() {
1064         global $CFG, $DB;
1065         require_once($CFG->dirroot . '/calendar/lib.php');
1067         $this->resetAfterTest();
1068         $course = $this->getDataGenerator()->create_course();
1069         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1071         $this->setAdminUser();
1073         $duedate = time();
1074         $submissionsfromdate = $duedate - DAYSECS;
1075         $cutoffdate = $duedate + DAYSECS;
1076         $assign = $this->create_instance($course, [
1077             'duedate' => $duedate,
1078             'allowsubmissionsfromdate' => $submissionsfromdate,
1079             'cutoffdate' => $cutoffdate,
1080         ]);
1081         $instance = $assign->get_instance();
1083         $event = new \calendar_event((object) [
1084             'courseid' => $instance->course,
1085             'modulename' => 'assign',
1086             'instance' => $instance->id,
1087             'userid' => $student->id,
1088             'eventtype' => ASSIGN_EVENT_TYPE_DUE,
1089             'timestart' => $duedate + 1
1090         ]);
1092         $record = (object) [
1093             'assignid' => $instance->id,
1094             'userid' => $student->id,
1095             'duedate' => $duedate + 1,
1096         ];
1098         $DB->insert_record('assign_overrides', $record);
1100         mod_assign_core_calendar_event_timestart_updated($event, $instance);
1102         $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1103         $this->assertEquals($duedate, $newinstance->duedate);
1104     }
1106     /**
1107      * Due date events should update the assignment due date.
1108      */
1109     public function test_mod_assign_core_calendar_event_timestart_updated_due_event() {
1110         global $CFG, $DB;
1111         require_once($CFG->dirroot . '/calendar/lib.php');
1113         $this->resetAfterTest();
1114         $course = $this->getDataGenerator()->create_course();
1115         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1117         $this->setAdminUser();
1119         $duedate = time();
1120         $newduedate = $duedate + 1;
1121         $submissionsfromdate = $duedate - DAYSECS;
1122         $cutoffdate = $duedate + DAYSECS;
1123         $assign = $this->create_instance($course, [
1124             'duedate' => $duedate,
1125             'allowsubmissionsfromdate' => $submissionsfromdate,
1126             'cutoffdate' => $cutoffdate,
1127         ]);
1128         $instance = $assign->get_instance();
1130         $event = new \calendar_event((object) [
1131             'courseid' => $instance->course,
1132             'modulename' => 'assign',
1133             'instance' => $instance->id,
1134             'eventtype' => ASSIGN_EVENT_TYPE_DUE,
1135             'timestart' => $newduedate
1136         ]);
1138         mod_assign_core_calendar_event_timestart_updated($event, $instance);
1140         $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1141         $this->assertEquals($newduedate, $newinstance->duedate);
1142     }
1144     /**
1145      * If a student somehow finds a way to update the due date calendar event
1146      * then the callback should not be executed to update the assignment due
1147      * date as well otherwise that would be a security issue.
1148      */
1149     public function test_student_role_cant_update_due_event() {
1150         global $CFG, $DB;
1151         require_once($CFG->dirroot . '/calendar/lib.php');
1153         $this->resetAfterTest();
1154         $course = $this->getDataGenerator()->create_course();
1155         $context = context_course::instance($course->id);
1157         $roleid = $this->getDataGenerator()->create_role();
1158         $role = $DB->get_record('role', ['id' => $roleid]);
1159         $user = $this->getDataGenerator()->create_and_enrol($course, $role->shortname);
1161         $this->setAdminUser();
1163         $mapper = calendar_event_container::get_event_mapper();
1164         $now = time();
1165         $duedate = (new DateTime())->setTimestamp($now);
1166         $newduedate = (new DateTime())->setTimestamp($now)->modify('+1 day');
1167         $assign = $this->create_instance($course, [
1168             'course' => $course->id,
1169             'duedate' => $duedate->getTimestamp(),
1170         ]);
1171         $instance = $assign->get_instance();
1173         $record = $DB->get_record('event', [
1174             'courseid' => $course->id,
1175             'modulename' => 'assign',
1176             'instance' => $instance->id,
1177             'eventtype' => ASSIGN_EVENT_TYPE_DUE
1178         ]);
1180         $event = new \calendar_event($record);
1182         assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
1183         assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleid, $context, true);
1185         $this->setUser($user);
1187         calendar_local_api::update_event_start_day(
1188             $mapper->from_legacy_event_to_event($event),
1189             $newduedate
1190         );
1192         $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1193         $newevent = \calendar_event::load($event->id);
1194         // The due date shouldn't have changed even though we updated the calendar
1195         // event.
1196         $this->assertEquals($duedate->getTimestamp(), $newinstance->duedate);
1197         $this->assertEquals($newduedate->getTimestamp(), $newevent->timestart);
1198     }
1200     /**
1201      * A teacher with the capability to modify an assignment module should be
1202      * able to update the assignment due date by changing the due date calendar
1203      * event.
1204      */
1205     public function test_teacher_role_can_update_due_event() {
1206         global $CFG, $DB;
1207         require_once($CFG->dirroot . '/calendar/lib.php');
1209         $this->resetAfterTest();
1210         $course = $this->getDataGenerator()->create_course();
1211         $context = context_course::instance($course->id);
1212         $user = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1213         $roleid = $DB->get_field('role', 'id', ['shortname' => 'teacher']);
1215         $this->setAdminUser();
1217         $mapper = calendar_event_container::get_event_mapper();
1218         $now = time();
1219         $duedate = (new DateTime())->setTimestamp($now);
1220         $newduedate = (new DateTime())->setTimestamp($now)->modify('+1 day');
1221         $assign = $this->create_instance($course, [
1222             'course' => $course->id,
1223             'duedate' => $duedate->getTimestamp(),
1224         ]);
1225         $instance = $assign->get_instance();
1227         $record = $DB->get_record('event', [
1228             'courseid' => $course->id,
1229             'modulename' => 'assign',
1230             'instance' => $instance->id,
1231             'eventtype' => ASSIGN_EVENT_TYPE_DUE
1232         ]);
1234         $event = new \calendar_event($record);
1236         assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
1237         assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true);
1239         $this->setUser($user);
1240         // Trigger and capture the event when adding a contact.
1241         $sink = $this->redirectEvents();
1243         calendar_local_api::update_event_start_day(
1244             $mapper->from_legacy_event_to_event($event),
1245             $newduedate
1246         );
1248         $triggeredevents = $sink->get_events();
1249         $moduleupdatedevents = array_filter($triggeredevents, function($e) {
1250             return is_a($e, 'core\event\course_module_updated');
1251         });
1253         $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
1254         $newevent = \calendar_event::load($event->id);
1255         // The due date shouldn't have changed even though we updated the calendar
1256         // event.
1257         $this->assertEquals($newduedate->getTimestamp(), $newinstance->duedate);
1258         $this->assertEquals($newduedate->getTimestamp(), $newevent->timestart);
1259         // Confirm that a module updated event is fired when the module
1260         // is changed.
1261         $this->assertNotEmpty($moduleupdatedevents);
1262     }