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/>.
16
17/**
18 * Unit tests for (some of) mod/assign/locallib.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 */
25
26
27defined('MOODLE_INTERNAL') || die();
28
29global $CFG;
30require_once($CFG->dirroot . '/mod/assign/locallib.php');
31require_once($CFG->dirroot . '/mod/assign/upgradelib.php');
32require_once($CFG->dirroot . '/mod/assign/tests/generator.php');
33
34/**
35 * Unit tests for (some of) mod/assign/locallib.php.
36 *
37 * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
38 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 */
40class mod_assign_locallib_testcase extends advanced_testcase {
41
42    // Use the generator helper.
43    use mod_assign_test_generator;
44
45    public function test_return_links() {
46        global $PAGE;
47
48        $this->resetAfterTest();
49        $course = $this->getDataGenerator()->create_course();
50
51        $assign = $this->create_instance($course);
52        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
53
54        $assign->register_return_link('RETURNACTION', ['param' => 1]);
55        $this->assertEquals('RETURNACTION', $assign->get_return_action());
56        $this->assertEquals(['param' => 1], $assign->get_return_params());
57    }
58
59    public function test_get_feedback_plugins() {
60        $this->resetAfterTest();
61        $course = $this->getDataGenerator()->create_course();
62        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
63
64        $this->setUser($teacher);
65        $assign = $this->create_instance($course);
66        $installedplugins = array_keys(core_component::get_plugin_list('assignfeedback'));
67
68        foreach ($assign->get_feedback_plugins() as $plugin) {
69            $this->assertContains($plugin->get_type(), $installedplugins, 'Feedback plugin not in list of installed plugins');
70        }
71    }
72
73    public function test_get_submission_plugins() {
74        $this->resetAfterTest();
75        $course = $this->getDataGenerator()->create_course();
76        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
77
78        $this->setUser($teacher);
79        $assign = $this->create_instance($course);
80        $installedplugins = array_keys(core_component::get_plugin_list('assignsubmission'));
81
82        foreach ($assign->get_submission_plugins() as $plugin) {
83            $this->assertContains($plugin->get_type(), $installedplugins, 'Submission plugin not in list of installed plugins');
84        }
85    }
86
87    public function test_is_blind_marking() {
88        $this->resetAfterTest();
89        $course = $this->getDataGenerator()->create_course();
90        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
91        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
92
93        $this->setUser($teacher);
94        $assign = $this->create_instance($course, ['blindmarking' => 1]);
95        $this->assertEquals(true, $assign->is_blind_marking());
96
97        // Test cannot see student names.
98        $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
99        $output = $assign->get_renderer()->render($gradingtable);
100        $this->assertEquals(true, strpos($output, get_string('hiddenuser', 'assign')));
101
102        // Test students cannot reveal identities.
103        $nopermission = false;
104        $student->ignoresesskey = true;
105        $this->setUser($student);
106        $this->expectException('required_capability_exception');
107        $assign->reveal_identities();
108        $student->ignoresesskey = false;
109
110        // Test teachers cannot reveal identities.
111        $nopermission = false;
112        $teacher->ignoresesskey = true;
113        $this->setUser($teacher);
114        $this->expectException('required_capability_exception');
115        $assign->reveal_identities();
116        $teacher->ignoresesskey = false;
117
118        // Test sesskey is required.
119        $this->setUser($teacher);
120        $this->expectException('moodle_exception');
121        $assign->reveal_identities();
122
123        // Test editingteacher can reveal identities if sesskey is ignored.
124        $teacher->ignoresesskey = true;
125        $this->setUser($teacher);
126        $assign->reveal_identities();
127        $this->assertEquals(false, $assign->is_blind_marking());
128        $teacher->ignoresesskey = false;
129
130        // Test student names are visible.
131        $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
132        $output = $assign->get_renderer()->render($gradingtable);
133        $this->assertEquals(false, strpos($output, get_string('hiddenuser', 'assign')));
134
135        // Set this back to default.
136        $teacher->ignoresesskey = false;
137    }
138
139    /**
140     * Data provider for test_get_assign_perpage
141     *
142     * @return array Provider data
143     */
144    public function get_assign_perpage_provider() {
145        return array(
146            array(
147                'maxperpage' => -1,
148                'userprefs' => array(
149                    -1 => -1,
150                    10 => 10,
151                    20 => 20,
152                    50 => 50,
153                ),
154            ),
155            array(
156                'maxperpage' => 15,
157                'userprefs' => array(
158                    -1 => 15,
159                    10 => 10,
160                    20 => 15,
161                    50 => 15,
162                ),
163            ),
164        );
165    }
166
167    /**
168     * Test maxperpage
169     *
170     * @dataProvider get_assign_perpage_provider
171     * @param integer $maxperpage site config value
172     * @param array $userprefs Array of user preferences and expected page sizes
173     */
174    public function test_get_assign_perpage($maxperpage, $userprefs) {
175        $this->resetAfterTest();
176        $course = $this->getDataGenerator()->create_course();
177        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
178        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
179
180        $this->setUser($teacher);
181        $assign = $this->create_instance($course);
182
183        set_config('maxperpage', $maxperpage, 'assign');
184        set_user_preference('assign_perpage', null);
185        $this->assertEquals(10, $assign->get_assign_perpage());
186        foreach ($userprefs as $pref => $perpage) {
187            set_user_preference('assign_perpage', $pref);
188            $this->assertEquals($perpage, $assign->get_assign_perpage());
189        }
190    }
191
192    /**
193     * Test filter by requires grading.
194     *
195     * This is specifically checking an assignment with no grade to make sure we do not
196     * get an exception thrown when rendering the grading table for this type of assignment.
197     */
198    public function test_gradingtable_filter_by_requiresgrading_no_grade() {
199        global $PAGE;
200
201        $this->resetAfterTest();
202
203        $course = $this->getDataGenerator()->create_course();
204        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
205        $this->setUser($teacher);
206        $assign = $this->create_instance($course, [
207                'assignsubmission_onlinetext_enabled' => 1,
208                'assignfeedback_comments_enabled' => 0,
209                'grade' => GRADE_TYPE_NONE
210            ]);
211
212        $PAGE->set_url(new moodle_url('/mod/assign/view.php', array(
213            'id' => $assign->get_course_module()->id,
214            'action' => 'grading',
215        )));
216
217        // Render the table with the requires grading filter.
218        $gradingtable = new assign_grading_table($assign, 1, ASSIGN_FILTER_REQUIRE_GRADING, 0, true);
219        $output = $assign->get_renderer()->render($gradingtable);
220
221        // Test that the filter function does not throw errors for assignments with no grade.
222        $this->assertStringContainsString(get_string('nothingtodisplay'), $output);
223    }
224
225
226    /**
227     * Test submissions with extension date.
228     */
229    public function test_gradingtable_extension_due_date() {
230        global $PAGE;
231
232        $this->resetAfterTest();
233        $course = $this->getDataGenerator()->create_course();
234        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
235        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
236
237        // Setup the assignment.
238        $this->setUser($teacher);
239        $time = time();
240        $assign = $this->create_instance($course, [
241                'assignsubmission_onlinetext_enabled' => 1,
242                'duedate' => time() - (4 * DAYSECS),
243            ]);
244        $PAGE->set_url(new moodle_url('/mod/assign/view.php', array(
245            'id' => $assign->get_course_module()->id,
246            'action' => 'grading',
247        )));
248
249        // Check that the assignment is late.
250        $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
251        $output = $assign->get_renderer()->render($gradingtable);
252        $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
253        $this->assertStringContainsString(get_string('overdue', 'assign', format_time((4 * DAYSECS))), $output);
254
255        // Grant an extension.
256        $extendedtime = $time + (2 * DAYSECS);
257        $assign->testable_save_user_extension($student->id, $extendedtime);
258        $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
259        $output = $assign->get_renderer()->render($gradingtable);
260        $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
261        $this->assertStringContainsString(get_string('userextensiondate', 'assign', userdate($extendedtime)), $output);
262
263        // Simulate a submission.
264        $this->setUser($student);
265        $submission = $assign->get_user_submission($student->id, true);
266        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
267        $assign->testable_update_submission($submission, $student->id, true, false);
268        $data = new stdClass();
269        $data->onlinetext_editor = [
270            'itemid' => file_get_unused_draft_itemid(),
271            'text' => 'Submission text',
272            'format' => FORMAT_MOODLE,
273        ];
274        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
275        $plugin->save($submission, $data);
276
277        // Verify output.
278        $this->setUser($teacher);
279        $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
280        $output = $assign->get_renderer()->render($gradingtable);
281        $this->assertStringContainsString(get_string('submissionstatus_submitted', 'assign'), $output);
282        $this->assertStringContainsString(get_string('userextensiondate', 'assign', userdate($extendedtime)), $output);
283    }
284
285    /**
286     * Test that late submissions with extension date calculate correctly.
287     */
288    public function test_gradingtable_extension_date_calculation_for_lateness() {
289        global $PAGE;
290
291        $this->resetAfterTest();
292        $course = $this->getDataGenerator()->create_course();
293        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
294        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
295
296        // Setup the assignment.
297        $this->setUser($teacher);
298        $time = time();
299        $assign = $this->create_instance($course, [
300                'assignsubmission_onlinetext_enabled' => 1,
301                'duedate' => time() - (4 * DAYSECS),
302            ]);
303        $PAGE->set_url(new moodle_url('/mod/assign/view.php', array(
304            'id' => $assign->get_course_module()->id,
305            'action' => 'grading',
306        )));
307
308        // Check that the assignment is late.
309        $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
310        $output = $assign->get_renderer()->render($gradingtable);
311        $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
312        $difftime = time() - $time;
313        $this->assertStringContainsString(get_string('overdue', 'assign', format_time((4 * DAYSECS) + $difftime)), $output);
314
315        // Grant an extension that is in the past.
316        $assign->testable_save_user_extension($student->id, $time - (2 * DAYSECS));
317        $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
318        $output = $assign->get_renderer()->render($gradingtable);
319        $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
320        $this->assertStringContainsString(get_string('userextensiondate', 'assign', userdate($time - (2 * DAYSECS))), $output);
321        $difftime = time() - $time;
322        $this->assertStringContainsString(get_string('overdue', 'assign', format_time((2 * DAYSECS) + $difftime)), $output);
323
324        // Simulate a submission.
325        $this->setUser($student);
326        $submission = $assign->get_user_submission($student->id, true);
327        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
328        $assign->testable_update_submission($submission, $student->id, true, false);
329        $data = new stdClass();
330        $data->onlinetext_editor = [
331            'itemid' => file_get_unused_draft_itemid(),
332            'text' => 'Submission text',
333            'format' => FORMAT_MOODLE,
334        ];
335        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
336        $plugin->save($submission, $data);
337        $submittedtime = time();
338
339        // Verify output.
340        $this->setUser($teacher);
341        $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
342        $output = $assign->get_renderer()->render($gradingtable);
343        $this->assertStringContainsString(get_string('submissionstatus_submitted', 'assign'), $output);
344        $this->assertStringContainsString(get_string('userextensiondate', 'assign', userdate($time - (2 * DAYSECS))), $output);
345
346        $difftime = $submittedtime - $time;
347        $this->assertStringContainsString(get_string('submittedlateshort', 'assign', format_time((2 * DAYSECS) + $difftime)),
348            $output);
349    }
350
351    public function test_gradingtable_status_rendering() {
352        global $PAGE;
353
354        $this->resetAfterTest();
355        $course = $this->getDataGenerator()->create_course();
356        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
357        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
358
359        // Setup the assignment.
360        $this->setUser($teacher);
361        $time = time();
362        $assign = $this->create_instance($course, [
363            'assignsubmission_onlinetext_enabled' => 1,
364            'duedate' => $time - (4 * DAYSECS),
365         ]);
366        $PAGE->set_url(new moodle_url('/mod/assign/view.php', array(
367            'id' => $assign->get_course_module()->id,
368            'action' => 'grading',
369        )));
370
371        // Check that the assignment is late.
372        $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
373        $output = $assign->get_renderer()->render($gradingtable);
374        $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
375        $difftime = time() - $time;
376        $this->assertStringContainsString(get_string('overdue', 'assign', format_time((4 * DAYSECS) + $difftime)), $output);
377
378        // Simulate a student viewing the assignment without submitting.
379        $this->setUser($student);
380        $submission = $assign->get_user_submission($student->id, true);
381        $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
382        $assign->testable_update_submission($submission, $student->id, true, false);
383        $submittedtime = time();
384
385        // Verify output.
386        $this->setUser($teacher);
387        $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
388        $output = $assign->get_renderer()->render($gradingtable);
389        $difftime = $submittedtime - $time;
390        $this->assertStringContainsString(get_string('overdue', 'assign', format_time((4 * DAYSECS) + $difftime)), $output);
391
392        $document = new DOMDocument();
393        @$document->loadHTML($output);
394        $xpath = new DOMXPath($document);
395        $this->assertEmpty($xpath->evaluate('string(//td[@id="mod_assign_grading-' . $assign->get_context()->id. '_r0_c8"])'));
396    }
397
398    /**
399     * Check that group submission information is rendered correctly in the
400     * grading table.
401     */
402    public function test_gradingtable_group_submissions_rendering() {
403        global $PAGE;
404
405        $this->resetAfterTest();
406        $course = $this->getDataGenerator()->create_course();
407        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
408
409        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
410        groups_add_member($group, $teacher);
411
412        $students = [];
413
414        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
415        $students[] = $student;
416        groups_add_member($group, $student);
417
418        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
419        $students[] = $student;
420        groups_add_member($group, $student);
421
422        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
423        $students[] = $student;
424        groups_add_member($group, $student);
425
426        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
427        $students[] = $student;
428        groups_add_member($group, $student);
429
430        // Verify group assignments.
431        $this->setUser($teacher);
432        $assign = $this->create_instance($course, [
433            'teamsubmission' => 1,
434            'assignsubmission_onlinetext_enabled' => 1,
435            'submissiondrafts' => 1,
436            'requireallteammemberssubmit' => 0,
437        ]);
438        $PAGE->set_url(new moodle_url('/mod/assign/view.php', array(
439            'id' => $assign->get_course_module()->id,
440            'action' => 'grading',
441        )));
442
443        // Add a submission.
444        $this->setUser($student);
445        $data = new stdClass();
446        $data->onlinetext_editor = [
447            'itemid' => file_get_unused_draft_itemid(),
448            'text' => 'Submission text',
449            'format' => FORMAT_MOODLE,
450        ];
451        $notices = array();
452        $assign->save_submission($data, $notices);
453
454        $submission = $assign->get_group_submission($student->id, 0, true);
455        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
456        $assign->testable_update_submission($submission, $student->id, true, true);
457
458        // Check output.
459        $this->setUser($teacher);
460        $gradingtable = new assign_grading_table($assign, 4, '', 0, true);
461        $output = $assign->get_renderer()->render($gradingtable);
462        $document = new DOMDocument();
463        @$document->loadHTML($output);
464        $xpath = new DOMXPath($document);
465
466        // The XPath expression is based on the unique ID of the table.
467        $xpathuniqueidroot = 'mod_assign_grading-' . $assign->get_context()->id;
468
469        // Check status.
470        $this->assertSame(get_string('submissionstatus_submitted', 'assign'),
471            $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c4"]/div[@class="submissionstatussubmitted"])'));
472        $this->assertSame(get_string('submissionstatus_submitted', 'assign'),
473            $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c4"]/div[@class="submissionstatussubmitted"])'));
474
475        // Check submission last modified date.
476        $this->assertGreaterThan(0, strtotime($xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c8"])')));
477        $this->assertGreaterThan(0, strtotime($xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c8"])')));
478
479        // Check group.
480        $this->assertSame($group->name, $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c5"])'));
481        $this->assertSame($group->name, $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c5"])'));
482
483        // Check submission text.
484        $this->assertSame('Submission text', $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c9"]/div/div)'));
485        $this->assertSame('Submission text', $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c9"]/div/div)'));
486
487        // Check comments can be made.
488        $this->assertEquals(1, $xpath->evaluate('count(//td[@id="' . $xpathuniqueidroot . '_r0_c10"]//textarea)'));
489        $this->assertEquals(1, $xpath->evaluate('count(//td[@id="' . $xpathuniqueidroot . '_r3_c10"]//textarea)'));
490    }
491
492    public function test_show_intro() {
493        $this->resetAfterTest();
494        $course = $this->getDataGenerator()->create_course();
495        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
496
497        // Test whether we are showing the intro at the correct times.
498        $this->setUser($teacher);
499        $assign = $this->create_instance($course, ['alwaysshowdescription' => 1]);
500
501        $this->assertEquals(true, $assign->testable_show_intro());
502
503        $tomorrow = time() + DAYSECS;
504
505        $assign = $this->create_instance($course, [
506                'alwaysshowdescription' => 0,
507                'allowsubmissionsfromdate' => $tomorrow,
508            ]);
509        $this->assertEquals(false, $assign->testable_show_intro());
510        $yesterday = time() - DAYSECS;
511        $assign = $this->create_instance($course, [
512                'alwaysshowdescription' => 0,
513                'allowsubmissionsfromdate' => $yesterday,
514            ]);
515        $this->assertEquals(true, $assign->testable_show_intro());
516    }
517
518    public function test_has_submissions_or_grades() {
519        $this->resetAfterTest();
520        $course = $this->getDataGenerator()->create_course();
521        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
522        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
523
524        $this->setUser($teacher);
525        $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
526        $instance = $assign->get_instance();
527
528        // Should start empty.
529        $this->assertEquals(false, $assign->has_submissions_or_grades());
530
531        // Simulate a submission.
532        $this->setUser($student);
533        $submission = $assign->get_user_submission($student->id, true);
534
535        // The submission is still new.
536        $this->assertEquals(false, $assign->has_submissions_or_grades());
537
538        // Submit the submission.
539        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
540        $assign->testable_update_submission($submission, $student->id, true, false);
541        $data = new stdClass();
542        $data->onlinetext_editor = array(
543            'itemid' => file_get_unused_draft_itemid(),
544            'text' => 'Submission text',
545            'format' => FORMAT_MOODLE);
546        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
547        $plugin->save($submission, $data);
548
549        // Now test again.
550        $this->assertEquals(true, $assign->has_submissions_or_grades());
551    }
552
553    public function test_delete_grades() {
554        $this->resetAfterTest();
555        $course = $this->getDataGenerator()->create_course();
556        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
557        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
558
559        $this->setUser($teacher);
560        $assign = $this->create_instance($course);
561
562        // Simulate adding a grade.
563        $this->setUser($teacher);
564        $data = new stdClass();
565        $data->grade = '50.0';
566        $assign->testable_apply_grade_to_user($data, $student->id, 0);
567
568        // Now see if the data is in the gradebook.
569        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id);
570
571        $this->assertNotEquals(0, count($gradinginfo->items));
572
573        $assign->testable_delete_grades();
574        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id);
575
576        $this->assertEquals(0, count($gradinginfo->items));
577    }
578
579    public function test_delete_instance() {
580        $this->resetAfterTest();
581        $course = $this->getDataGenerator()->create_course();
582        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
583        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
584
585        $this->setUser($teacher);
586        $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
587
588        // Simulate adding a grade.
589        $this->setUser($teacher);
590        $data = new stdClass();
591        $data->grade = '50.0';
592        $assign->testable_apply_grade_to_user($data, $student->id, 0);
593
594        // Simulate a submission.
595        $this->add_submission($student, $assign);
596
597        // Now try and delete.
598        $this->setUser($teacher);
599        $this->assertEquals(true, $assign->delete_instance());
600    }
601
602    public function test_reset_userdata() {
603        global $DB;
604
605        $this->resetAfterTest();
606        $course = $this->getDataGenerator()->create_course();
607        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
608        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
609
610        $now = time();
611        $this->setUser($teacher);
612        $assign = $this->create_instance($course, [
613                'assignsubmission_onlinetext_enabled' => 1,
614                'duedate' => $now,
615            ]);
616
617        // Simulate adding a grade.
618        $this->add_submission($student, $assign);
619        $this->submit_for_grading($student, $assign);
620        $this->mark_submission($teacher, $assign, $student, 50.0);
621
622        // Simulate a submission.
623        $this->setUser($student);
624        $submission = $assign->get_user_submission($student->id, true);
625        $data = new stdClass();
626        $data->onlinetext_editor = array(
627            'itemid' => file_get_unused_draft_itemid(),
628            'text' => 'Submission text',
629            'format' => FORMAT_MOODLE);
630        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
631        $plugin->save($submission, $data);
632
633        $this->assertEquals(true, $assign->has_submissions_or_grades());
634        // Now try and reset.
635        $data = new stdClass();
636        $data->reset_assign_submissions = 1;
637        $data->reset_gradebook_grades = 1;
638        $data->reset_assign_user_overrides = 1;
639        $data->reset_assign_group_overrides = 1;
640        $data->courseid = $course->id;
641        $data->timeshift = DAYSECS;
642        $this->setUser($teacher);
643        $assign->reset_userdata($data);
644        $this->assertEquals(false, $assign->has_submissions_or_grades());
645
646        // Reload the instance data.
647        $instance = $DB->get_record('assign', array('id' => $assign->get_instance()->id));
648        $this->assertEquals($now + DAYSECS, $instance->duedate);
649
650        // Test reset using assign_reset_userdata().
651        $assignduedate = $instance->duedate; // Keep old updated value for comparison.
652        $data->timeshift = (2 * DAYSECS);
653        assign_reset_userdata($data);
654        $instance = $DB->get_record('assign', array('id' => $assign->get_instance()->id));
655        $this->assertEquals($assignduedate + (2 * DAYSECS), $instance->duedate);
656
657        // Create one more assignment and reset, make sure time shifted for previous assignment is not changed.
658        $assign2 = $this->create_instance($course, [
659                'assignsubmission_onlinetext_enabled' => 1,
660                'duedate' => $now,
661            ]);
662        $assignduedate = $instance->duedate;
663        $data->timeshift = 3 * DAYSECS;
664        $assign2->reset_userdata($data);
665        $instance = $DB->get_record('assign', array('id' => $assign->get_instance()->id));
666        $this->assertEquals($assignduedate, $instance->duedate);
667        $instance2 = $DB->get_record('assign', array('id' => $assign2->get_instance()->id));
668        $this->assertEquals($now + 3 * DAYSECS, $instance2->duedate);
669
670        // Reset both assignments using assign_reset_userdata() and make sure both assignments have same date.
671        $assignduedate = $instance->duedate;
672        $assign2duedate = $instance2->duedate;
673        $data->timeshift = (4 * DAYSECS);
674        assign_reset_userdata($data);
675        $instance = $DB->get_record('assign', array('id' => $assign->get_instance()->id));
676        $this->assertEquals($assignduedate + (4 * DAYSECS), $instance->duedate);
677        $instance2 = $DB->get_record('assign', array('id' => $assign2->get_instance()->id));
678        $this->assertEquals($assign2duedate + (4 * DAYSECS), $instance2->duedate);
679    }
680
681    public function test_plugin_settings() {
682        global $DB;
683
684        $this->resetAfterTest();
685
686        $course = $this->getDataGenerator()->create_course();
687        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
688
689        $now = time();
690        $this->setUser($teacher);
691        $assign = $this->create_instance($course, [
692                'assignsubmission_file_enabled' => 1,
693                'assignsubmission_file_maxfiles' => 12,
694                'assignsubmission_file_maxsizebytes' => 10,
695            ]);
696
697        $plugin = $assign->get_submission_plugin_by_type('file');
698        $this->assertEquals('12', $plugin->get_config('maxfilesubmissions'));
699    }
700
701    public function test_update_calendar() {
702        global $DB;
703
704        $this->resetAfterTest();
705
706        $course = $this->getDataGenerator()->create_course();
707        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
708
709        $this->setUser($teacher);
710        $userctx = context_user::instance($teacher->id)->id;
711
712        // Hack to pretend that there was an editor involved. We need both $_POST and $_REQUEST, and a sesskey.
713        $draftid = file_get_unused_draft_itemid();
714        $_REQUEST['introeditor'] = $draftid;
715        $_POST['introeditor'] = $draftid;
716        $_POST['sesskey'] = sesskey();
717
718        // Write links to a draft area.
719        $fakearealink1 = file_rewrite_pluginfile_urls('<a href="@@PLUGINFILE@@/pic.gif">link</a>', 'draftfile.php', $userctx,
720            'user', 'draft', $draftid);
721        $fakearealink2 = file_rewrite_pluginfile_urls('<a href="@@PLUGINFILE@@/pic.gif">new</a>', 'draftfile.php', $userctx,
722            'user', 'draft', $draftid);
723
724        // Create a new assignment with links to a draft area.
725        $now = time();
726        $assign = $this->create_instance($course, [
727                'duedate' => $now,
728                'intro' => $fakearealink1,
729                'introformat' => FORMAT_HTML
730            ]);
731
732        // See if there is an event in the calendar.
733        $params = array('modulename' => 'assign', 'instance' => $assign->get_instance()->id);
734        $event = $DB->get_record('event', $params);
735        $this->assertNotEmpty($event);
736        $this->assertSame('link', $event->description);     // The pluginfile links are removed.
737
738        // Make sure the same works when updating the assignment.
739        $instance = $assign->get_instance();
740        $instance->instance = $instance->id;
741        $instance->intro = $fakearealink2;
742        $instance->introformat = FORMAT_HTML;
743        $assign->update_instance($instance);
744        $params = array('modulename' => 'assign', 'instance' => $assign->get_instance()->id);
745        $event = $DB->get_record('event', $params);
746        $this->assertNotEmpty($event);
747        $this->assertSame('new', $event->description);     // The pluginfile links are removed.
748
749        // Create an assignment with a description that should be hidden.
750        $assign = $this->create_instance($course, [
751                'duedate' => $now + 160,
752                'alwaysshowdescription' => false,
753                'allowsubmissionsfromdate' => $now + 60,
754                'intro' => 'Some text',
755            ]);
756
757        // Get the event from the calendar.
758        $params = array('modulename' => 'assign', 'instance' => $assign->get_instance()->id);
759        $event = $DB->get_record('event', [
760            'modulename' => 'assign',
761            'instance' => $assign->get_instance()->id,
762        ]);
763
764        $this->assertEmpty($event->description);
765
766        // Change the allowsubmissionfromdate to the past - do this directly in the DB
767        // because if we call the assignment update method - it will update the calendar
768        // and we want to test that this works from cron.
769        $DB->set_field('assign', 'allowsubmissionsfromdate', $now - 60, array('id' => $assign->get_instance()->id));
770        // Run cron to update the event in the calendar.
771        assign::cron();
772        $event = $DB->get_record('event', $params);
773
774        $this->assertStringContainsString('Some text', $event->description);
775
776    }
777
778    public function test_update_instance() {
779        global $DB;
780
781        $this->resetAfterTest();
782
783        $course = $this->getDataGenerator()->create_course();
784        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
785
786        $this->setUser($teacher);
787        $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
788
789        $now = time();
790        $instance = $assign->get_instance();
791        $instance->duedate = $now;
792        $instance->instance = $instance->id;
793        $instance->assignsubmission_onlinetext_enabled = 1;
794
795        $assign->update_instance($instance);
796
797        $instance = $DB->get_record('assign', ['id' => $assign->get_instance()->id]);
798        $this->assertEquals($now, $instance->duedate);
799    }
800
801    public function test_cannot_submit_empty() {
802        global $PAGE;
803
804        $this->resetAfterTest();
805
806        $course = $this->getDataGenerator()->create_course();
807        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
808
809        $assign = $this->create_instance($course, ['submissiondrafts' => 1]);
810
811        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
812
813        // Test you cannot see the submit button for an offline assignment regardless.
814        $this->setUser($student);
815        $output = $assign->view_student_summary($student, true);
816        $this->assertStringNotContainsString(get_string('submitassignment', 'assign'),
817            $output, 'Can submit empty offline assignment');
818    }
819
820    public function test_cannot_submit_empty_no_submission() {
821        global $PAGE;
822
823        $this->resetAfterTest();
824
825        $course = $this->getDataGenerator()->create_course();
826        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
827
828        $assign = $this->create_instance($course, [
829            'submissiondrafts' => 1,
830            'assignsubmission_onlinetext_enabled' => 1,
831        ]);
832
833        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
834
835        // Test you cannot see the submit button for an online text assignment with no submission.
836        $this->setUser($student);
837        $output = $assign->view_student_summary($student, true);
838        $this->assertStringNotContainsString(get_string('submitassignment', 'assign'),
839            $output, 'Cannot submit empty onlinetext assignment');
840    }
841
842    public function test_can_submit_with_submission() {
843        global $PAGE;
844
845        $this->resetAfterTest();
846
847        $course = $this->getDataGenerator()->create_course();
848        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
849
850        $assign = $this->create_instance($course, [
851            'submissiondrafts' => 1,
852            'assignsubmission_onlinetext_enabled' => 1,
853        ]);
854
855        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
856
857        // Add a draft.
858        $this->add_submission($student, $assign);
859
860        // Test you can see the submit button for an online text assignment with a submission.
861        $this->setUser($student);
862        $output = $assign->view_student_summary($student, true);
863        $this->assertStringContainsString(get_string('submitassignment', 'assign'),
864            $output, 'Can submit non empty onlinetext assignment');
865    }
866
867    /**
868     * Test new_submission_empty
869     *
870     * We only test combinations of plugins here. Individual plugins are tested
871     * in their respective test files.
872     *
873     * @dataProvider test_new_submission_empty_testcases
874     * @param string $data The file submission data
875     * @param bool $expected The expected return value
876     */
877    public function test_new_submission_empty($data, $expected) {
878        $this->resetAfterTest();
879
880        $course = $this->getDataGenerator()->create_course();
881        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
882
883        $assign = $this->create_instance($course, [
884                'assignsubmission_file_enabled' => 1,
885                'assignsubmission_file_maxfiles' => 12,
886                'assignsubmission_file_maxsizebytes' => 10,
887                'assignsubmission_onlinetext_enabled' => 1,
888            ]);
889        $this->setUser($student);
890        $submission = new stdClass();
891
892        if ($data['file'] && isset($data['file']['filename'])) {
893            $itemid = file_get_unused_draft_itemid();
894            $submission->files_filemanager = $itemid;
895            $data['file'] += ['contextid' => context_user::instance($student->id)->id, 'itemid' => $itemid];
896            $fs = get_file_storage();
897            $fs->create_file_from_string((object)$data['file'], 'Content of ' . $data['file']['filename']);
898        }
899
900        if ($data['onlinetext']) {
901            $submission->onlinetext_editor = ['text' => $data['onlinetext']];
902        }
903
904        $result = $assign->new_submission_empty($submission);
905        $this->assertTrue($result === $expected);
906    }
907
908    /**
909     * Dataprovider for the test_new_submission_empty testcase
910     *
911     * @return array of testcases
912     */
913    public function test_new_submission_empty_testcases() {
914        return [
915            'With file and onlinetext' => [
916                [
917                    'file' => [
918                        'component' => 'user',
919                        'filearea' => 'draft',
920                        'filepath' => '/',
921                        'filename' => 'not_a_virus.exe'
922                    ],
923                    'onlinetext' => 'Balin Fundinul Uzbadkhazaddumu'
924                ],
925                false
926            ]
927        ];
928    }
929
930    public function test_list_participants() {
931        global $CFG;
932
933        $this->resetAfterTest();
934
935        $course = $this->getDataGenerator()->create_course();
936        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
937
938        // Create 10 students.
939        for ($i = 0; $i < 10; $i++) {
940            $this->getDataGenerator()->create_and_enrol($course, 'student');
941        }
942
943        $this->setUser($teacher);
944        $assign = $this->create_instance($course, ['grade' => 100]);
945
946        $this->assertCount(10, $assign->list_participants(null, true));
947    }
948
949    public function test_list_participants_activeenrol() {
950        global $CFG, $DB;
951
952        $this->resetAfterTest();
953
954        $course = $this->getDataGenerator()->create_course();
955        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
956
957        // Create 10 students.
958        for ($i = 0; $i < 10; $i++) {
959            $this->getDataGenerator()->create_and_enrol($course, 'student');
960        }
961
962        // Create 10 suspended students.
963        for ($i = 0; $i < 10; $i++) {
964            $this->getDataGenerator()->create_and_enrol($course, 'student', null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
965        }
966
967        $this->setUser($teacher);
968        set_user_preference('grade_report_showonlyactiveenrol', false);
969        $assign = $this->create_instance($course, ['grade' => 100]);
970
971        $this->assertCount(10, $assign->list_participants(null, true));
972    }
973
974    public function test_list_participants_with_group_restriction() {
975        global $CFG;
976
977        $this->resetAfterTest();
978
979        $course = $this->getDataGenerator()->create_course();
980        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
981        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
982        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
983        $unrelatedstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
984
985        // Turn on availability and a group restriction, and check that it doesn't show users who aren't in the group.
986        $CFG->enableavailability = true;
987
988        $specialgroup = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
989        $assign = $this->create_instance($course, [
990            'grade' => 100,
991            'availability' => json_encode(
992                \core_availability\tree::get_root_json([\availability_group\condition::get_json($specialgroup->id)])
993            ),
994        ]);
995
996        groups_add_member($specialgroup, $student);
997        groups_add_member($specialgroup, $otherstudent);
998        $this->assertEquals(2, count($assign->list_participants(null, true)));
999    }
1000
1001    public function test_get_participant_user_not_exist() {
1002        $this->resetAfterTest();
1003        $course = $this->getDataGenerator()->create_course();
1004
1005        $assign = $this->create_instance($course);
1006        $this->assertNull($assign->get_participant('-1'));
1007    }
1008
1009    public function test_get_participant_not_enrolled() {
1010        $this->resetAfterTest();
1011        $course = $this->getDataGenerator()->create_course();
1012        $assign = $this->create_instance($course);
1013
1014        $user = $this->getDataGenerator()->create_user();
1015        $this->assertNull($assign->get_participant($user->id));
1016    }
1017
1018    public function test_get_participant_no_submission() {
1019        $this->resetAfterTest();
1020        $course = $this->getDataGenerator()->create_course();
1021        $assign = $this->create_instance($course);
1022        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1023
1024        $participant = $assign->get_participant($student->id);
1025
1026        $this->assertEquals($student->id, $participant->id);
1027        $this->assertFalse($participant->submitted);
1028        $this->assertFalse($participant->requiregrading);
1029        $this->assertFalse($participant->grantedextension);
1030    }
1031
1032    public function test_get_participant_granted_extension() {
1033        $this->resetAfterTest();
1034        $course = $this->getDataGenerator()->create_course();
1035        $assign = $this->create_instance($course);
1036        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1037        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1038
1039        $this->setUser($teacher);
1040        $assign->save_user_extension($student->id, time());
1041        $participant = $assign->get_participant($student->id);
1042
1043        $this->assertEquals($student->id, $participant->id);
1044        $this->assertFalse($participant->submitted);
1045        $this->assertFalse($participant->requiregrading);
1046        $this->assertTrue($participant->grantedextension);
1047    }
1048
1049    public function test_get_participant_with_ungraded_submission() {
1050        $this->resetAfterTest();
1051        $course = $this->getDataGenerator()->create_course();
1052        $assign = $this->create_instance($course);
1053        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1054        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1055
1056        // Simulate a submission.
1057        $this->add_submission($student, $assign);
1058        $this->submit_for_grading($student, $assign);
1059
1060        $participant = $assign->get_participant($student->id);
1061
1062        $this->assertEquals($student->id, $participant->id);
1063        $this->assertTrue($participant->submitted);
1064        $this->assertTrue($participant->requiregrading);
1065        $this->assertFalse($participant->grantedextension);
1066    }
1067
1068    /**
1069     * Tests that if a student with no submission who can no longer submit is not a participant.
1070     */
1071    public function test_get_participant_with_no_submission_no_capability() {
1072        global $DB;
1073        $this->resetAfterTest();
1074        $course = self::getDataGenerator()->create_course();
1075        $coursecontext = context_course::instance($course->id);
1076        $assign = $this->create_instance($course);
1077        $teacher = self::getDataGenerator()->create_and_enrol($course, 'teacher');
1078        $student = self::getDataGenerator()->create_and_enrol($course, 'student');
1079
1080        // Remove the students capability to submit.
1081        $role = $DB->get_field('role', 'id', ['shortname' => 'student']);
1082        assign_capability('mod/assign:submit', CAP_PROHIBIT, $role, $coursecontext);
1083
1084        $participant = $assign->get_participant($student->id);
1085
1086        self::assertNull($participant);
1087    }
1088
1089    /**
1090     * Tests that if a student that has submitted but can no longer submit is a participant.
1091     */
1092    public function test_get_participant_with_submission_no_capability() {
1093        global $DB;
1094        $this->resetAfterTest();
1095        $course = self::getDataGenerator()->create_course();
1096        $coursecontext = context_course::instance($course->id);
1097        $assign = $this->create_instance($course);
1098        $teacher = self::getDataGenerator()->create_and_enrol($course, 'teacher');
1099        $student = self::getDataGenerator()->create_and_enrol($course, 'student');
1100
1101        // Simulate a submission.
1102        $this->add_submission($student, $assign);
1103        $this->submit_for_grading($student, $assign);
1104
1105        // Remove the students capability to submit.
1106        $role = $DB->get_field('role', 'id', ['shortname' => 'student']);
1107        assign_capability('mod/assign:submit', CAP_PROHIBIT, $role, $coursecontext);
1108
1109        $participant = $assign->get_participant($student->id);
1110
1111        self::assertNotNull($participant);
1112        self::assertEquals($student->id, $participant->id);
1113        self::assertTrue($participant->submitted);
1114        self::assertTrue($participant->requiregrading);
1115        self::assertFalse($participant->grantedextension);
1116    }
1117
1118    public function test_get_participant_with_graded_submission() {
1119        $this->resetAfterTest();
1120        $course = $this->getDataGenerator()->create_course();
1121        $assign = $this->create_instance($course);
1122        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1123        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1124
1125        // Simulate a submission.
1126        $this->add_submission($student, $assign);
1127        $this->submit_for_grading($student, $assign);
1128
1129        $this->mark_submission($teacher, $assign, $student, 50.0);
1130
1131        $data = new stdClass();
1132        $data->grade = '50.0';
1133        $assign->testable_apply_grade_to_user($data, $student->id, 0);
1134
1135        $participant = $assign->get_participant($student->id);
1136
1137        $this->assertEquals($student->id, $participant->id);
1138        $this->assertTrue($participant->submitted);
1139        $this->assertFalse($participant->requiregrading);
1140        $this->assertFalse($participant->grantedextension);
1141    }
1142
1143    /**
1144     * No active group and non-group submissions disallowed => 2 groups.
1145     */
1146    public function test_count_teams_no_active_non_group_allowed() {
1147        $this->resetAfterTest();
1148
1149        $course = $this->getDataGenerator()->create_course();
1150        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1151        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1152        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1153
1154        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
1155        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1156        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1157        groups_add_member($group1, $student1);
1158        groups_add_member($group2, $student2);
1159
1160        $this->setUser($teacher);
1161        $assign = $this->create_instance($course, ['teamsubmission' => 1]);
1162
1163        $this->assertEquals(2, $assign->count_teams());
1164    }
1165
1166    /**
1167     * No active group and non group submissions allowed => 2 groups + the default one.
1168     */
1169    public function test_count_teams_non_group_allowed() {
1170        $this->resetAfterTest();
1171
1172        $course = $this->getDataGenerator()->create_course();
1173        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1174        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1175        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1176        $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1177
1178        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
1179        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1180
1181        $this->getDataGenerator()->create_grouping_group(array('groupid' => $group1->id, 'groupingid' => $grouping->id));
1182        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1183        $this->getDataGenerator()->create_grouping_group(array('groupid' => $group2->id, 'groupingid' => $grouping->id));
1184
1185        groups_add_member($group1, $student1);
1186        groups_add_member($group2, $student2);
1187
1188        $assign = $this->create_instance($course, [
1189            'teamsubmission' => 1,
1190            'teamsubmissiongroupingid' => $grouping->id,
1191            'preventsubmissionnotingroup' => false,
1192        ]);
1193
1194        $this->setUser($teacher);
1195        $this->assertEquals(3, $assign->count_teams());
1196
1197        // Active group only.
1198        $this->assertEquals(1, $assign->count_teams($group1->id));
1199        $this->assertEquals(1, $assign->count_teams($group2->id));
1200    }
1201
1202    /**
1203     * Active group => just selected one.
1204     */
1205    public function test_count_teams_no_active_group() {
1206        $this->resetAfterTest();
1207
1208        $course = $this->getDataGenerator()->create_course();
1209        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1210        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1211        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1212        $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1213
1214        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
1215        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1216
1217        $this->getDataGenerator()->create_grouping_group(array('groupid' => $group1->id, 'groupingid' => $grouping->id));
1218        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1219        $this->getDataGenerator()->create_grouping_group(array('groupid' => $group2->id, 'groupingid' => $grouping->id));
1220
1221        groups_add_member($group1, $student1);
1222        groups_add_member($group2, $student2);
1223
1224        $assign = $this->create_instance($course, [
1225            'teamsubmission' => 1,
1226            'preventsubmissionnotingroup' => true,
1227        ]);
1228
1229        $this->setUser($teacher);
1230        $this->assertEquals(2, $assign->count_teams());
1231
1232        // Active group only.
1233        $this->assertEquals(1, $assign->count_teams($group1->id));
1234        $this->assertEquals(1, $assign->count_teams($group2->id));
1235    }
1236
1237    /**
1238     * Active group => just selected one.
1239     */
1240    public function test_count_teams_groups_only() {
1241        $this->resetAfterTest();
1242
1243        $course = $this->getDataGenerator()->create_course();
1244        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
1245
1246        $assign = $this->create_instance($course, [
1247            'teamsubmission' => 1,
1248            'teamsubmissiongroupingid' => $grouping->id,
1249            'preventsubmissionnotingroup' => false,
1250        ]);
1251        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1252
1253        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1254        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1255        groups_add_member($group1, $student1);
1256
1257        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
1258        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1259        groups_add_member($group2, $student2);
1260
1261        $this->getDataGenerator()->create_grouping_group(array('groupid' => $group1->id, 'groupingid' => $grouping->id));
1262        $this->getDataGenerator()->create_grouping_group(array('groupid' => $group2->id, 'groupingid' => $grouping->id));
1263
1264        $this->setUser($teacher);
1265
1266        $assign = $this->create_instance($course, [
1267            'teamsubmission' => 1,
1268            'preventsubmissionnotingroup' => true,
1269        ]);
1270        $this->assertEquals(2, $assign->count_teams());
1271    }
1272
1273    public function test_submit_to_default_group() {
1274        global $DB, $SESSION;
1275
1276        $this->resetAfterTest();
1277
1278        $course = $this->getDataGenerator()->create_course();
1279        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1280        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1281
1282        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
1283        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1284
1285        $assign = $this->create_instance($course, [
1286            'teamsubmission' => 1,
1287            'assignsubmission_onlinetext_enabled' => 1,
1288            'submissiondrafts' => 0,
1289            'groupmode' => VISIBLEGROUPS,
1290        ]);
1291
1292        $usergroup = $assign->get_submission_group($student->id);
1293        $this->assertFalse($usergroup, 'New student is in default group');
1294
1295        // Add a submission.
1296        $this->add_submission($student, $assign);
1297        $this->submit_for_grading($student, $assign);
1298
1299        // Set active groups to all groups.
1300        $this->setUser($teacher);
1301        $SESSION->activegroup[$course->id]['aag'][0] = 0;
1302        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1303
1304        // Set an active group.
1305        $SESSION->activegroup[$course->id]['aag'][0] = (int) $group->id;
1306        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1307    }
1308
1309    public function test_count_submissions_no_draft() {
1310        $this->resetAfterTest();
1311
1312        $course = $this->getDataGenerator()->create_course();
1313        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1314        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1315
1316        $assign = $this->create_instance($course, [
1317            'assignsubmission_onlinetext_enabled' => 1,
1318        ]);
1319
1320        $assign->get_user_submission($student->id, true);
1321
1322        // Note: Drafts count as a submission.
1323        $this->assertEquals(0, $assign->count_grades());
1324        $this->assertEquals(0, $assign->count_submissions());
1325        $this->assertEquals(1, $assign->count_submissions(true));
1326        $this->assertEquals(0, $assign->count_submissions_need_grading());
1327        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_NEW));
1328        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
1329        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1330        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_REOPENED));
1331    }
1332
1333    public function test_count_submissions_draft() {
1334        $this->resetAfterTest();
1335
1336        $course = $this->getDataGenerator()->create_course();
1337        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1338        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1339
1340        $assign = $this->create_instance($course, [
1341            'assignsubmission_onlinetext_enabled' => 1,
1342        ]);
1343
1344        $this->add_submission($student, $assign);
1345
1346        // Note: Drafts count as a submission.
1347        $this->assertEquals(0, $assign->count_grades());
1348        $this->assertEquals(1, $assign->count_submissions());
1349        $this->assertEquals(1, $assign->count_submissions(true));
1350        $this->assertEquals(0, $assign->count_submissions_need_grading());
1351        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_NEW));
1352        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
1353        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1354        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_REOPENED));
1355    }
1356
1357    public function test_count_submissions_submitted() {
1358        global $SESSION;
1359
1360        $this->resetAfterTest();
1361
1362        $course = $this->getDataGenerator()->create_course();
1363        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1364        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1365
1366        $assign = $this->create_instance($course, [
1367            'assignsubmission_onlinetext_enabled' => 1,
1368        ]);
1369
1370        $this->add_submission($student, $assign);
1371        $this->submit_for_grading($student, $assign);
1372
1373        $this->assertEquals(0, $assign->count_grades());
1374        $this->assertEquals(1, $assign->count_submissions());
1375        $this->assertEquals(1, $assign->count_submissions(true));
1376        $this->assertEquals(1, $assign->count_submissions_need_grading());
1377        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_NEW));
1378        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
1379        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1380        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_REOPENED));
1381    }
1382
1383    public function test_count_submissions_graded() {
1384        $this->resetAfterTest();
1385
1386        $course = $this->getDataGenerator()->create_course();
1387        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1388        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1389
1390        $assign = $this->create_instance($course, [
1391            'assignsubmission_onlinetext_enabled' => 1,
1392        ]);
1393
1394        $this->add_submission($student, $assign);
1395        $this->submit_for_grading($student, $assign);
1396        $this->mark_submission($teacher, $assign, $student, 50.0);
1397
1398        // Although it has been graded, it is still marked as submitted.
1399        $this->assertEquals(1, $assign->count_grades());
1400        $this->assertEquals(1, $assign->count_submissions());
1401        $this->assertEquals(1, $assign->count_submissions(true));
1402        $this->assertEquals(0, $assign->count_submissions_need_grading());
1403        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_NEW));
1404        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
1405        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1406        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_REOPENED));
1407    }
1408
1409    public function test_count_submissions_graded_group() {
1410        global $SESSION;
1411
1412        $this->resetAfterTest();
1413
1414        $course = $this->getDataGenerator()->create_course();
1415        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1416        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1417        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1418        $othergroup = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1419        groups_add_member($group, $student);
1420
1421        $assign = $this->create_instance($course, [
1422            'assignsubmission_onlinetext_enabled' => 1,
1423            'groupmode' => VISIBLEGROUPS,
1424        ]);
1425
1426        $this->add_submission($student, $assign);
1427        $this->submit_for_grading($student, $assign);
1428
1429        // The user should still be listed when fetching all groups.
1430        $this->setUser($teacher);
1431        $SESSION->activegroup[$course->id]['aag'][0] = 0;
1432        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1433
1434        // The user should still be listed when fetching just their group.
1435        $SESSION->activegroup[$course->id]['aag'][0] = $group->id;
1436        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1437
1438        // The user should still be listed when fetching just their group.
1439        $SESSION->activegroup[$course->id]['aag'][0] = $othergroup->id;
1440        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1441    }
1442
1443    // TODO
1444    public function x_test_count_submissions_for_team() {
1445        $this->resetAfterTest();
1446
1447        $course = $this->getDataGenerator()->create_course();
1448        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1449        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1450        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1451        $othergroup = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1452        groups_add_member($group, $student);
1453
1454        $assign = $this->create_instance($course, [
1455            'assignsubmission_onlinetext_enabled' => 1,
1456            'teamsubmission' => 1,
1457        ]);
1458
1459        // Add a graded submission.
1460        $this->add_submission($student, $assign);
1461
1462        // Simulate adding a grade.
1463        $this->setUser($teacher);
1464        $data = new stdClass();
1465        $data->grade = '50.0';
1466        $assign->testable_apply_grade_to_user($data, $this->extrastudents[0]->id, 0);
1467
1468        // Simulate a submission.
1469        $this->setUser($this->extrastudents[1]);
1470        $submission = $assign->get_group_submission($this->extrastudents[1]->id, $groupid, true);
1471        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
1472        $assign->testable_update_submission($submission, $this->extrastudents[1]->id, true, false);
1473        $data = new stdClass();
1474        $data->onlinetext_editor = array(
1475            'itemid' => file_get_unused_draft_itemid(),
1476            'text' => 'Submission text',
1477            'format' => FORMAT_MOODLE);
1478        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
1479        $plugin->save($submission, $data);
1480
1481        // Simulate a submission.
1482        $this->setUser($this->extrastudents[2]);
1483        $submission = $assign->get_group_submission($this->extrastudents[2]->id, $groupid, true);
1484        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
1485        $assign->testable_update_submission($submission, $this->extrastudents[2]->id, true, false);
1486        $data = new stdClass();
1487        $data->onlinetext_editor = array(
1488            'itemid' => file_get_unused_draft_itemid(),
1489            'text' => 'Submission text',
1490            'format' => FORMAT_MOODLE);
1491        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
1492        $plugin->save($submission, $data);
1493
1494        // Simulate a submission.
1495        $this->setUser($this->extrastudents[3]);
1496        $submission = $assign->get_group_submission($this->extrastudents[3]->id, $groupid, true);
1497        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
1498        $assign->testable_update_submission($submission, $this->extrastudents[3]->id, true, false);
1499        $data = new stdClass();
1500        $data->onlinetext_editor = array(
1501            'itemid' => file_get_unused_draft_itemid(),
1502            'text' => 'Submission text',
1503            'format' => FORMAT_MOODLE);
1504        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
1505        $plugin->save($submission, $data);
1506
1507        // Simulate adding a grade.
1508        $this->setUser($teacher);
1509        $data = new stdClass();
1510        $data->grade = '50.0';
1511        $assign->testable_apply_grade_to_user($data, $this->extrastudents[3]->id, 0);
1512        $assign->testable_apply_grade_to_user($data, $this->extrasuspendedstudents[0]->id, 0);
1513
1514        // Create a new submission with status NEW.
1515        $this->setUser($this->extrastudents[4]);
1516        $submission = $assign->get_group_submission($this->extrastudents[4]->id, $groupid, true);
1517
1518        $this->assertEquals(2, $assign->count_grades());
1519        $this->assertEquals(4, $assign->count_submissions());
1520        $this->assertEquals(5, $assign->count_submissions(true));
1521        $this->assertEquals(3, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
1522        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
1523    }
1524
1525    public function test_get_grading_userid_list_only_active() {
1526        $this->resetAfterTest();
1527
1528        $course = $this->getDataGenerator()->create_course();
1529        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1530        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1531        $suspendedstudent = $this->getDataGenerator()->create_and_enrol(
1532            $course, 'student', null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
1533
1534        $this->setUser($teacher);
1535
1536        $assign = $this->create_instance($course);
1537        $this->assertCount(1, $assign->testable_get_grading_userid_list());
1538    }
1539
1540    public function test_get_grading_userid_list_all() {
1541        $this->resetAfterTest();
1542
1543        $course = $this->getDataGenerator()->create_course();
1544        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1545        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1546        $suspendedstudent = $this->getDataGenerator()->create_and_enrol(
1547            $course, 'student', null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
1548
1549        $this->setUser($teacher);
1550        set_user_preference('grade_report_showonlyactiveenrol', false);
1551
1552        $assign = $this->create_instance($course);
1553        $this->assertCount(2, $assign->testable_get_grading_userid_list());
1554    }
1555
1556    public function test_cron() {
1557        global $PAGE;
1558        $this->resetAfterTest();
1559
1560        // First run cron so there are no messages waiting to be sent (from other tests).
1561        cron_setup_user();
1562        assign::cron();
1563
1564        $course = $this->getDataGenerator()->create_course();
1565        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1566        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1567
1568        // Now create an assignment and add some feedback.
1569        $this->setUser($teacher);
1570        $assign = $this->create_instance($course, [
1571            'sendstudentnotifications' => 1,
1572        ]);
1573
1574        $this->add_submission($student, $assign);
1575        $this->submit_for_grading($student, $assign);
1576        $this->mark_submission($teacher, $assign, $student, 50.0);
1577
1578        $this->expectOutputRegex('/Done processing 1 assignment submissions/');
1579        cron_setup_user();
1580        $sink = $this->redirectMessages();
1581        assign::cron();
1582        $messages = $sink->get_messages();
1583
1584        $this->assertEquals(1, count($messages));
1585        $this->assertEquals(1, $messages[0]->notification);
1586        $this->assertEquals($assign->get_instance()->name, $messages[0]->contexturlname);
1587        // Test customdata.
1588        $customdata = json_decode($messages[0]->customdata);
1589        $this->assertEquals($assign->get_course_module()->id, $customdata->cmid);
1590        $this->assertEquals($assign->get_instance()->id, $customdata->instance);
1591        $this->assertEquals('feedbackavailable', $customdata->messagetype);
1592        $userpicture = new user_picture($teacher);
1593        $userpicture->size = 1; // Use f1 size.
1594        $this->assertEquals($userpicture->get_url($PAGE)->out(false), $customdata->notificationiconurl);
1595        $this->assertEquals(0, $customdata->uniqueidforuser);   // Not used in this case.
1596        $this->assertFalse($customdata->blindmarking);
1597    }
1598
1599    public function test_cron_without_notifications() {
1600        $this->resetAfterTest();
1601
1602        // First run cron so there are no messages waiting to be sent (from other tests).
1603        cron_setup_user();
1604        assign::cron();
1605
1606        $course = $this->getDataGenerator()->create_course();
1607        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1608        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1609
1610        // Now create an assignment and add some feedback.
1611        $this->setUser($teacher);
1612        $assign = $this->create_instance($course, [
1613            'sendstudentnotifications' => 1,
1614        ]);
1615
1616        $this->add_submission($student, $assign);
1617        $this->submit_for_grading($student, $assign);
1618        $this->mark_submission($teacher, $assign, $student, 50.0, [
1619            'sendstudentnotifications' => 0,
1620        ]);
1621
1622        cron_setup_user();
1623        $sink = $this->redirectMessages();
1624        assign::cron();
1625        $messages = $sink->get_messages();
1626
1627        $this->assertEquals(0, count($messages));
1628    }
1629
1630    public function test_cron_regraded() {
1631        $this->resetAfterTest();
1632
1633        // First run cron so there are no messages waiting to be sent (from other tests).
1634        cron_setup_user();
1635        assign::cron();
1636
1637        $course = $this->getDataGenerator()->create_course();
1638        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1639        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1640
1641        // Now create an assignment and add some feedback.
1642        $this->setUser($teacher);
1643        $assign = $this->create_instance($course, [
1644            'sendstudentnotifications' => 1,
1645        ]);
1646
1647        $this->add_submission($student, $assign);
1648        $this->submit_for_grading($student, $assign);
1649        $this->mark_submission($teacher, $assign, $student, 50.0);
1650
1651        $this->expectOutputRegex('/Done processing 1 assignment submissions/');
1652        cron_setup_user();
1653        assign::cron();
1654
1655        // Regrade.
1656        $this->mark_submission($teacher, $assign, $student, 50.0);
1657
1658        $this->expectOutputRegex('/Done processing 1 assignment submissions/');
1659        cron_setup_user();
1660        $sink = $this->redirectMessages();
1661        assign::cron();
1662        $messages = $sink->get_messages();
1663
1664        $this->assertEquals(1, count($messages));
1665        $this->assertEquals(1, $messages[0]->notification);
1666        $this->assertEquals($assign->get_instance()->name, $messages[0]->contexturlname);
1667    }
1668
1669    /**
1670     * Test delivery of grade notifications as controlled by marking workflow.
1671     */
1672    public function test_markingworkflow_cron() {
1673        $this->resetAfterTest();
1674
1675        // First run cron so there are no messages waiting to be sent (from other tests).
1676        cron_setup_user();
1677        assign::cron();
1678
1679        $course = $this->getDataGenerator()->create_course();
1680        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1681        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1682
1683        // Now create an assignment and add some feedback.
1684        $this->setUser($teacher);
1685        $assign = $this->create_instance($course, [
1686            'sendstudentnotifications' => 1,
1687            'markingworkflow' => 1,
1688        ]);
1689
1690        // Mark a submission but set the workflowstate to an unreleased state.
1691        // This should not trigger a notification.
1692        $this->add_submission($student, $assign);
1693        $this->submit_for_grading($student, $assign);
1694        $this->mark_submission($teacher, $assign, $student, 50.0, [
1695            'sendstudentnotifications' => 1,
1696            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE,
1697        ]);
1698
1699        cron_setup_user();
1700        $sink = $this->redirectMessages();
1701        assign::cron();
1702        $messages = $sink->get_messages();
1703
1704        $this->assertEquals(0, count($messages));
1705
1706        // Transition to the released state.
1707        $this->setUser($teacher);
1708        $submission = $assign->get_user_submission($student->id, true);
1709        $submission->workflowstate = ASSIGN_MARKING_WORKFLOW_STATE_RELEASED;
1710        $assign->testable_apply_grade_to_user($submission, $student->id, 0);
1711
1712        // Now run cron and see that one message was sent.
1713        cron_setup_user();
1714        $sink = $this->redirectMessages();
1715        $this->expectOutputRegex('/Done processing 1 assignment submissions/');
1716        assign::cron();
1717        $messages = $sink->get_messages();
1718
1719        $this->assertEquals(1, count($messages));
1720        $this->assertEquals(1, $messages[0]->notification);
1721        $this->assertEquals($assign->get_instance()->name, $messages[0]->contexturlname);
1722    }
1723
1724    public function test_cron_message_includes_courseid() {
1725        $this->resetAfterTest();
1726
1727        // First run cron so there are no messages waiting to be sent (from other tests).
1728        cron_setup_user();
1729        assign::cron();
1730
1731        $course = $this->getDataGenerator()->create_course();
1732        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1733        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1734
1735        // Now create an assignment and add some feedback.
1736        $this->setUser($teacher);
1737        $assign = $this->create_instance($course, [
1738            'sendstudentnotifications' => 1,
1739        ]);
1740
1741        // Mark a submission but set the workflowstate to an unreleased state.
1742        // This should not trigger a notification.
1743        $this->add_submission($student, $assign);
1744        $this->submit_for_grading($student, $assign);
1745        $this->mark_submission($teacher, $assign, $student);
1746        phpunit_util::stop_message_redirection();
1747
1748        // Now run cron and see that one message was sent.
1749        cron_setup_user();
1750        $this->preventResetByRollback();
1751        $sink = $this->redirectEvents();
1752        $this->expectOutputRegex('/Done processing 1 assignment submissions/');
1753        assign::cron();
1754
1755        $events = $sink->get_events();
1756        $event = reset($events);
1757        $this->assertInstanceOf('\core\event\notification_sent', $event);
1758        $this->assertEquals($assign->get_course()->id, $event->other['courseid']);
1759        $sink->close();
1760    }
1761
1762    public function test_is_graded() {
1763        $this->resetAfterTest();
1764
1765        $course = $this->getDataGenerator()->create_course();
1766        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1767        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1768        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
1769
1770        $assign = $this->create_instance($course);
1771
1772        $this->add_submission($student, $assign);
1773        $this->submit_for_grading($student, $assign);
1774        $this->mark_submission($teacher, $assign, $student, 50.0);
1775
1776        $this->setUser($teacher);
1777        $this->assertEquals(true, $assign->testable_is_graded($student->id));
1778        $this->assertEquals(false, $assign->testable_is_graded($otherstudent->id));
1779    }
1780
1781    public function test_can_grade() {
1782        global $DB;
1783
1784        $this->resetAfterTest();
1785
1786        $course = $this->getDataGenerator()->create_course();
1787        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1788        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1789
1790        $assign = $this->create_instance($course);
1791
1792        $this->setUser($student);
1793        $this->assertEquals(false, $assign->can_grade());
1794
1795        $this->setUser($teacher);
1796        $this->assertEquals(true, $assign->can_grade());
1797
1798        // Test the viewgrades capability for other users.
1799        $this->setUser();
1800        $this->assertTrue($assign->can_grade($teacher->id));
1801        $this->assertFalse($assign->can_grade($student->id));
1802
1803        // Test the viewgrades capability - without mod/assign:grade.
1804        $this->setUser($student);
1805
1806        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1807        assign_capability('mod/assign:viewgrades', CAP_ALLOW, $studentrole->id, $assign->get_context()->id);
1808        $this->assertEquals(false, $assign->can_grade());
1809    }
1810
1811    public function test_can_view_submission() {
1812        global $DB;
1813
1814        $this->resetAfterTest();
1815
1816        $course = $this->getDataGenerator()->create_course();
1817        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1818        $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
1819        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1820        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
1821        $suspendedstudent = $this->getDataGenerator()->create_and_enrol(
1822            $course, 'student', null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
1823
1824        $assign = $this->create_instance($course);
1825
1826        $this->setUser($student);
1827        $this->assertEquals(true, $assign->can_view_submission($student->id));
1828        $this->assertEquals(false, $assign->can_view_submission($otherstudent->id));
1829        $this->assertEquals(false, $assign->can_view_submission($teacher->id));
1830
1831        $this->setUser($teacher);
1832        $this->assertEquals(true, $assign->can_view_submission($student->id));
1833        $this->assertEquals(true, $assign->can_view_submission($otherstudent->id));
1834        $this->assertEquals(true, $assign->can_view_submission($teacher->id));
1835        $this->assertEquals(false, $assign->can_view_submission($suspendedstudent->id));
1836
1837        $this->setUser($editingteacher);
1838        $this->assertEquals(true, $assign->can_view_submission($student->id));
1839        $this->assertEquals(true, $assign->can_view_submission($otherstudent->id));
1840        $this->assertEquals(true, $assign->can_view_submission($teacher->id));
1841        $this->assertEquals(true, $assign->can_view_submission($suspendedstudent->id));
1842
1843        // Test the viewgrades capability - without mod/assign:grade.
1844        $this->setUser($student);
1845        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1846        assign_capability('mod/assign:viewgrades', CAP_ALLOW, $studentrole->id, $assign->get_context()->id);
1847        $this->assertEquals(true, $assign->can_view_submission($student->id));
1848        $this->assertEquals(true, $assign->can_view_submission($otherstudent->id));
1849        $this->assertEquals(true, $assign->can_view_submission($teacher->id));
1850        $this->assertEquals(false, $assign->can_view_submission($suspendedstudent->id));
1851    }
1852
1853    public function test_update_submission() {
1854        $this->resetAfterTest();
1855
1856        $course = $this->getDataGenerator()->create_course();
1857        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1858        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1859
1860        $assign = $this->create_instance($course);
1861
1862        $this->add_submission($student, $assign);
1863        $submission = $assign->get_user_submission($student->id, 0);
1864        $assign->testable_update_submission($submission, $student->id, true, true);
1865
1866        $this->setUser($teacher);
1867
1868        // Verify the gradebook update.
1869        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);
1870
1871        $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
1872        $this->assertEquals($student->id, $gradinginfo->items[0]->grades[$student->id]->usermodified);
1873    }
1874
1875    public function test_update_submission_team() {
1876        $this->resetAfterTest();
1877
1878        $course = $this->getDataGenerator()->create_course();
1879        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1880        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1881
1882        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1883        groups_add_member($group, $student);
1884
1885        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
1886        groups_add_member($group, $otherstudent);
1887
1888        $assign = $this->create_instance($course, [
1889            'teamsubmission' => 1,
1890        ]);
1891
1892        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);
1893        $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
1894        $this->assertNull($gradinginfo->items[0]->grades[$student->id]->usermodified);
1895
1896        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $otherstudent->id);
1897        $this->asserttrue(isset($gradinginfo->items[0]->grades[$otherstudent->id]));
1898        $this->assertNull($gradinginfo->items[0]->grades[$otherstudent->id]->usermodified);
1899
1900        $this->add_submission($student, $assign);
1901        $submission = $assign->get_group_submission($student->id, 0, true);
1902        $assign->testable_update_submission($submission, $student->id, true, true);
1903
1904        // Verify the gradebook update for the student.
1905        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);
1906
1907        $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
1908        $this->assertEquals($student->id, $gradinginfo->items[0]->grades[$student->id]->usermodified);
1909
1910        // Verify the gradebook update for the other student.
1911        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $otherstudent->id);
1912
1913        $this->assertTrue(isset($gradinginfo->items[0]->grades[$otherstudent->id]));
1914        $this->assertEquals($otherstudent->id, $gradinginfo->items[0]->grades[$otherstudent->id]->usermodified);
1915    }
1916
1917    public function test_update_submission_suspended() {
1918        $this->resetAfterTest();
1919
1920        $course = $this->getDataGenerator()->create_course();
1921        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1922        $student = $this->getDataGenerator()->create_and_enrol($course, 'student', null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
1923
1924        $assign = $this->create_instance($course);
1925
1926        $this->add_submission($student, $assign);
1927        $submission = $assign->get_user_submission($student->id, 0);
1928        $assign->testable_update_submission($submission, $student->id, true, false);
1929
1930        $this->setUser($teacher);
1931
1932        // Verify the gradebook update.
1933        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);
1934
1935        $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
1936        $this->assertEquals($student->id, $gradinginfo->items[0]->grades[$student->id]->usermodified);
1937    }
1938
1939    public function test_update_submission_blind() {
1940        $this->resetAfterTest();
1941
1942        $course = $this->getDataGenerator()->create_course();
1943        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1944        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1945
1946        $assign = $this->create_instance($course, [
1947            'blindmarking' => 1,
1948        ]);
1949
1950        $this->add_submission($student, $assign);
1951        $submission = $assign->get_user_submission($student->id, 0);
1952        $assign->testable_update_submission($submission, $student->id, true, false);
1953
1954        // Verify the gradebook update.
1955        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);
1956
1957        // The usermodified is not set because this is blind marked.
1958        $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
1959        $this->assertNull($gradinginfo->items[0]->grades[$student->id]->usermodified);
1960    }
1961
1962    public function test_group_submissions_submit_for_marking_requireallteammemberssubmit() {
1963        global $PAGE;
1964
1965        $this->resetAfterTest();
1966
1967        $course = $this->getDataGenerator()->create_course();
1968        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
1969        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
1970
1971        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
1972        groups_add_member($group, $student);
1973
1974        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
1975        groups_add_member($group, $otherstudent);
1976
1977        $assign = $this->create_instance($course, [
1978            'teamsubmission' => 1,
1979            'assignsubmission_onlinetext_enabled' => 1,
1980            'submissiondrafts' => 1,
1981            'requireallteammemberssubmit' => 1,
1982        ]);
1983
1984        // Now verify group assignments.
1985        $this->setUser($teacher);
1986        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
1987
1988        // Add a submission.
1989        $this->add_submission($student, $assign);
1990
1991        // Check we can see the submit button.
1992        $this->setUser($student);
1993        $output = $assign->view_student_summary($student, true);
1994        $this->assertStringContainsString(get_string('submitassignment', 'assign'), $output);
1995
1996        $submission = $assign->get_group_submission($student->id, 0, true);
1997        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
1998        $assign->testable_update_submission($submission, $student->id, true, true);
1999
2000        // Check that the student does not see "Submit" button.
2001        $output = $assign->view_student_summary($student, true);
2002        $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);
2003
2004        // Change to another user in the same group.
2005        $this->setUser($otherstudent);
2006        $output = $assign->view_student_summary($otherstudent, true);
2007        $this->assertStringContainsString(get_string('submitassignment', 'assign'), $output);
2008
2009        $submission = $assign->get_group_submission($otherstudent->id, 0, true);
2010        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2011        $assign->testable_update_submission($submission, $otherstudent->id, true, true);
2012        $output = $assign->view_student_summary($otherstudent, true);
2013        $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);
2014    }
2015
2016    public function test_group_submissions_submit_for_marking() {
2017        global $PAGE;
2018
2019        $this->resetAfterTest();
2020
2021        $course = $this->getDataGenerator()->create_course();
2022        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2023        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2024
2025        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2026        groups_add_member($group, $student);
2027
2028        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
2029        groups_add_member($group, $otherstudent);
2030
2031        // Now verify group assignments.
2032        $this->setUser($teacher);
2033        $time = time();
2034        $assign = $this->create_instance($course, [
2035            'teamsubmission' => 1,
2036            'assignsubmission_onlinetext_enabled' => 1,
2037            'submissiondrafts' => 1,
2038            'requireallteammemberssubmit' => 0,
2039            'duedate' => $time - (2 * DAYSECS),
2040        ]);
2041        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2042
2043        // Add a submission.
2044        $this->add_submission($student, $assign);
2045
2046        // Check we can see the submit button.
2047        $output = $assign->view_student_summary($student, true);
2048        $this->assertStringContainsString(get_string('submitassignment', 'assign'), $output);
2049        $this->assertStringContainsString(get_string('timeremaining', 'assign'), $output);
2050        $difftime = time() - $time;
2051        $this->assertStringContainsString(get_string('overdue', 'assign', format_time((2 * DAYSECS) + $difftime)), $output);
2052
2053        $submission = $assign->get_group_submission($student->id, 0, true);
2054        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2055        $assign->testable_update_submission($submission, $student->id, true, true);
2056
2057        // Check that the student does not see "Submit" button.
2058        $output = $assign->view_student_summary($student, true);
2059        $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);
2060
2061        // Change to another user in the same group.
2062        $this->setUser($otherstudent);
2063        $output = $assign->view_student_summary($otherstudent, true);
2064        $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);
2065
2066        // Check that time remaining is not overdue.
2067        $this->assertStringContainsString(get_string('timeremaining', 'assign'), $output);
2068        $difftime = time() - $time;
2069        $this->assertStringContainsString(get_string('submittedlate', 'assign', format_time((2 * DAYSECS) + $difftime)), $output);
2070
2071        $submission = $assign->get_group_submission($otherstudent->id, 0, true);
2072        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2073        $assign->testable_update_submission($submission, $otherstudent->id, true, true);
2074        $output = $assign->view_student_summary($otherstudent, true);
2075        $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);
2076    }
2077
2078    public function test_submissions_open() {
2079        global $DB;
2080
2081        $this->resetAfterTest();
2082
2083        $course = $this->getDataGenerator()->create_course();
2084        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2085        $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2086        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2087        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
2088        $suspendedstudent = $this->getDataGenerator()->create_and_enrol(
2089            $course, 'student', null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
2090
2091        $this->setAdminUser();
2092
2093        $now = time();
2094        $tomorrow = $now + DAYSECS;
2095        $oneweek = $now + WEEKSECS;
2096        $yesterday = $now - DAYSECS;
2097
2098        $assign = $this->create_instance($course);
2099        $this->assertEquals(true, $assign->testable_submissions_open($student->id));
2100
2101        $assign = $this->create_instance($course, ['duedate' => $tomorrow]);
2102        $this->assertEquals(true, $assign->testable_submissions_open($student->id));
2103
2104        $assign = $this->create_instance($course, ['duedate' => $yesterday]);
2105        $this->assertEquals(true, $assign->testable_submissions_open($student->id));
2106
2107        $assign = $this->create_instance($course, ['duedate' => $yesterday, 'cutoffdate' => $tomorrow]);
2108        $this->assertEquals(true, $assign->testable_submissions_open($student->id));
2109
2110        $assign = $this->create_instance($course, ['duedate' => $yesterday, 'cutoffdate' => $yesterday]);
2111        $this->assertEquals(false, $assign->testable_submissions_open($student->id));
2112
2113        $assign->testable_save_user_extension($student->id, $tomorrow);
2114        $this->assertEquals(true, $assign->testable_submissions_open($student->id));
2115
2116        $assign = $this->create_instance($course, ['submissiondrafts' => 1]);
2117        $this->assertEquals(true, $assign->testable_submissions_open($student->id));
2118
2119        $this->setUser($student);
2120        $submission = $assign->get_user_submission($student->id, true);
2121        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2122        $assign->testable_update_submission($submission, $student->id, true, false);
2123
2124        $this->setUser($teacher);
2125        $this->assertEquals(false, $assign->testable_submissions_open($student->id));
2126    }
2127
2128    public function test_get_graders() {
2129        global $DB;
2130
2131        $this->resetAfterTest();
2132
2133        $course = $this->getDataGenerator()->create_course();
2134        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2135        $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2136        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2137
2138        $this->setAdminUser();
2139
2140        // Create an assignment with no groups.
2141        $assign = $this->create_instance($course);
2142        $this->assertCount(2, $assign->testable_get_graders($student->id));
2143    }
2144
2145    public function test_get_graders_separate_groups() {
2146        global $DB;
2147
2148        $this->resetAfterTest();
2149
2150        $course = $this->getDataGenerator()->create_course();
2151        $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2152        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2153        $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2154        $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2155        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2156        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
2157
2158        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
2159        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2160        groups_add_member($group1, $student);
2161
2162        $this->setAdminUser();
2163
2164        // Force create an assignment with SEPARATEGROUPS.
2165        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2166        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
2167
2168        $assign = $this->create_instance($course, [
2169            'groupingid' => $grouping->id,
2170            'groupmode' => SEPARATEGROUPS,
2171        ]);
2172
2173        $this->assertCount(4, $assign->testable_get_graders($student->id));
2174
2175        // Note the second student is in a group that is not in the grouping.
2176        // This means that we get all graders that are not in a group in the grouping.
2177        $this->assertCount(4, $assign->testable_get_graders($otherstudent->id));
2178    }
2179
2180    public function test_get_notified_users() {
2181        global $CFG, $DB;
2182
2183        $this->resetAfterTest();
2184
2185        $course = $this->getDataGenerator()->create_course();
2186        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
2187        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2188        $this->getDataGenerator()->create_grouping_group(array('groupid' => $group1->id, 'groupingid' => $grouping->id));
2189
2190        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2191        groups_add_member($group1, $teacher);
2192
2193        $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2194        groups_add_member($group1, $editingteacher);
2195
2196        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2197        groups_add_member($group1, $student);
2198
2199        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
2200        $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2201
2202        $capability = 'mod/assign:receivegradernotifications';
2203        $coursecontext = context_course::instance($course->id);
2204        $role = $DB->get_record('role', array('shortname' => 'teacher'));
2205
2206        $this->setUser($teacher);
2207
2208        // Create an assignment with no groups.
2209        $assign = $this->create_instance($course);
2210
2211        $this->assertCount(3, $assign->testable_get_notifiable_users($student->id));
2212
2213        // Change nonediting teachers role to not receive grader notifications.
2214        assign_capability($capability, CAP_PROHIBIT, $role->id, $coursecontext);
2215
2216        // Only the editing teachers will be returned.
2217        $this->assertCount(1, $assign->testable_get_notifiable_users($student->id));
2218
2219        // Note the second student is in a group that is not in the grouping.
2220        // This means that we get all graders that are not in a group in the grouping.
2221        $this->assertCount(1, $assign->testable_get_notifiable_users($otherstudent->id));
2222    }
2223
2224    public function test_get_notified_users_in_grouping() {
2225        global $CFG, $DB;
2226
2227        $this->resetAfterTest();
2228
2229        $course = $this->getDataGenerator()->create_course();
2230        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
2231        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2232        $this->getDataGenerator()->create_grouping_group(array('groupid' => $group1->id, 'groupingid' => $grouping->id));
2233
2234        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2235        groups_add_member($group1, $teacher);
2236
2237        $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2238        groups_add_member($group1, $editingteacher);
2239
2240        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2241        groups_add_member($group1, $student);
2242
2243        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
2244        $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2245
2246        // Force create an assignment with SEPARATEGROUPS.
2247        $assign = $this->create_instance($course, [
2248            'groupingid' => $grouping->id,
2249            'groupmode' => SEPARATEGROUPS,
2250        ]);
2251
2252        // Student is in a group - only the tacher and editing teacher in the group shoudl be present.
2253        $this->setUser($student);
2254        $this->assertCount(2, $assign->testable_get_notifiable_users($student->id));
2255
2256        // Note the second student is in a group that is not in the grouping.
2257        // This means that we get all graders that are not in a group in the grouping.
2258        $this->assertCount(1, $assign->testable_get_notifiable_users($otherstudent->id));
2259
2260        // Change nonediting teachers role to not receive grader notifications.
2261        $capability = 'mod/assign:receivegradernotifications';
2262        $coursecontext = context_course::instance($course->id);
2263        $role = $DB->get_record('role', ['shortname' => 'teacher']);
2264        assign_capability($capability, CAP_PROHIBIT, $role->id, $coursecontext);
2265
2266        // Only the editing teachers will be returned.
2267        $this->assertCount(1, $assign->testable_get_notifiable_users($student->id));
2268
2269        // Note the second student is in a group that is not in the grouping.
2270        // This means that we get all graders that are not in a group in the grouping.
2271        // Unfortunately there are no editing teachers who are not in a group.
2272        $this->assertCount(0, $assign->testable_get_notifiable_users($otherstudent->id));
2273    }
2274
2275    public function test_group_members_only() {
2276        global $CFG;
2277
2278        $this->resetAfterTest();
2279
2280        $course = $this->getDataGenerator()->create_course();
2281        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
2282        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2283        $this->getDataGenerator()->create_grouping_group([
2284            'groupid' => $group1->id,
2285            'groupingid' => $grouping->id,
2286        ]);
2287
2288        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2289        $this->getDataGenerator()->create_grouping_group([
2290            'groupid' => $group2->id,
2291            'groupingid' => $grouping->id,
2292        ]);
2293
2294        $group3 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
2295
2296        // Add users in the following groups
2297        // - Teacher - Group 1.
2298        // - Student - Group 1.
2299        // - Student - Group 2.
2300        // - Student - Unrelated Group
2301        // - Student - No group.
2302        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2303        groups_add_member($group1, $teacher);
2304
2305        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2306        groups_add_member($group1, $student);
2307
2308        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
2309        groups_add_member($group2, $otherstudent);
2310
2311        $yetotherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
2312        groups_add_member($group2, $otherstudent);
2313
2314        $this->getDataGenerator()->create_and_enrol($course, 'student');
2315
2316        $this->setAdminUser();
2317
2318        $CFG->enableavailability = true;
2319        $assign = $this->create_instance($course, [], [
2320            'availability' => json_encode(
2321                \core_availability\tree::get_root_json([\availability_grouping\condition::get_json()])
2322            ),
2323            'groupingid' => $grouping->id,
2324        ]);
2325
2326        // The two students in groups should be returned, but not the teacher in the group, or the student not in the
2327        // group, or the student in an unrelated group.
2328        $this->setUser($teacher);
2329        $participants = $assign->list_participants(0, true);
2330        $this->assertCount(2, $participants);
2331        $this->assertTrue(isset($participants[$student->id]));
2332        $this->assertTrue(isset($participants[$otherstudent->id]));
2333    }
2334
2335    public function test_get_uniqueid_for_user() {
2336        $this->resetAfterTest();
2337
2338        $course = $this->getDataGenerator()->create_course();
2339        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2340        $students = [];
2341        for ($i = 0; $i < 10; $i++) {
2342            $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2343            $students[$student->id] = $student;
2344        }
2345
2346        $this->setUser($teacher);
2347        $assign = $this->create_instance($course);
2348
2349        foreach ($students as $student) {
2350            $uniqueid = $assign->get_uniqueid_for_user($student->id);
2351            $this->assertEquals($student->id, $assign->get_user_id_for_uniqueid($uniqueid));
2352        }
2353    }
2354
2355    public function test_show_student_summary() {
2356        global $CFG, $PAGE;
2357
2358        $this->resetAfterTest();
2359
2360        $course = $this->getDataGenerator()->create_course();
2361        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2362        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2363        $this->setUser($teacher);
2364        $assign = $this->create_instance($course);
2365        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2366
2367        // No feedback should be available because this student has not been graded.
2368        $this->setUser($student);
2369        $output = $assign->view_student_summary($student, true);
2370        $this->assertDoesNotMatchRegularExpression('/Feedback/', $output, 'Do not show feedback if there is no grade');
2371
2372        // Simulate adding a grade.
2373        $this->add_submission($student, $assign);
2374        $this->submit_for_grading($student, $assign);
2375        $this->mark_submission($teacher, $assign, $student);
2376
2377        // Now we should see the feedback.
2378        $this->setUser($student);
2379        $output = $assign->view_student_summary($student, true);
2380        $this->assertMatchesRegularExpression('/Feedback/', $output, 'Show feedback if there is a grade');
2381
2382        // Now hide the grade in gradebook.
2383        $this->setUser($teacher);
2384        require_once($CFG->libdir.'/gradelib.php');
2385        $gradeitem = new grade_item(array(
2386            'itemtype'      => 'mod',
2387            'itemmodule'    => 'assign',
2388            'iteminstance'  => $assign->get_instance()->id,
2389            'courseid'      => $course->id));
2390
2391        $gradeitem->set_hidden(1, false);
2392
2393        // No feedback should be available because the grade is hidden.
2394        $this->setUser($student);
2395        $output = $assign->view_student_summary($student, true);
2396        $this->assertDoesNotMatchRegularExpression('/Feedback/', $output,
2397            'Do not show feedback if the grade is hidden in the gradebook');
2398
2399        // Freeze the context.
2400        $this->setAdminUser();
2401        $context = $assign->get_context();
2402        $CFG->contextlocking = true;
2403        $context->set_locked(true);
2404
2405        // No feedback should be available because the grade is hidden.
2406        $this->setUser($student);
2407        $output = $assign->view_student_summary($student, true);
2408        $this->assertDoesNotMatchRegularExpression('/Feedback/', $output, 'Do not show feedback if the grade is hidden in the gradebook');
2409
2410        // Show the feedback again - it should still be visible even in a frozen context.
2411        $this->setUser($teacher);
2412        $gradeitem->set_hidden(0, false);
2413
2414        $this->setUser($student);
2415        $output = $assign->view_student_summary($student, true);
2416        $this->assertMatchesRegularExpression('/Feedback/', $output, 'Show feedback if there is a grade');
2417    }
2418
2419    public function test_show_student_summary_with_feedback() {
2420        global $CFG, $PAGE;
2421
2422        $this->resetAfterTest();
2423
2424        $course = $this->getDataGenerator()->create_course();
2425        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2426        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2427        $this->setUser($teacher);
2428        $assign = $this->create_instance($course, [
2429            'assignfeedback_comments_enabled' => 1
2430        ]);
2431        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2432
2433        // No feedback should be available because this student has not been graded.
2434        $this->setUser($student);
2435        $output = $assign->view_student_summary($student, true);
2436        $this->assertDoesNotMatchRegularExpression('/Feedback/', $output);
2437
2438        // Simulate adding a grade.
2439        $this->add_submission($student, $assign);
2440        $this->submit_for_grading($student, $assign);
2441        $this->mark_submission($teacher, $assign, $student, null, [
2442            'assignfeedbackcomments_editor' => [
2443                'text' => 'Tomato sauce',
2444                'format' => FORMAT_MOODLE,
2445            ],
2446        ]);
2447
2448        // Should have feedback but no grade.
2449        $this->setUser($student);
2450        $output = $assign->view_student_summary($student, true);
2451        $this->assertMatchesRegularExpression('/Feedback/', $output);
2452        $this->assertMatchesRegularExpression('/Tomato sauce/', $output);
2453        $this->assertDoesNotMatchRegularExpression('/Grade/', $output, 'Do not show grade when there is no grade.');
2454        $this->assertDoesNotMatchRegularExpression('/Graded on/', $output, 'Do not show graded date when there is no grade.');
2455
2456        // Add a grade now.
2457        $this->mark_submission($teacher, $assign, $student, 50.0, [
2458            'assignfeedbackcomments_editor' => [
2459                'text' => 'Bechamel sauce',
2460                'format' => FORMAT_MOODLE,
2461            ],
2462        ]);
2463
2464        // Should have feedback but no grade.
2465        $this->setUser($student);
2466        $output = $assign->view_student_summary($student, true);
2467        $this->assertDoesNotMatchRegularExpression('/Tomato sauce/', $output);
2468        $this->assertMatchesRegularExpression('/Bechamel sauce/', $output);
2469        $this->assertMatchesRegularExpression('/Grade/', $output);
2470        $this->assertMatchesRegularExpression('/Graded on/', $output);
2471
2472        // Now hide the grade in gradebook.
2473        $this->setUser($teacher);
2474        $gradeitem = new grade_item(array(
2475            'itemtype'      => 'mod',
2476            'itemmodule'    => 'assign',
2477            'iteminstance'  => $assign->get_instance()->id,
2478            'courseid'      => $course->id));
2479
2480        $gradeitem->set_hidden(1, false);
2481
2482        // No feedback should be available because the grade is hidden.
2483        $this->setUser($student);
2484        $output = $assign->view_student_summary($student, true);
2485        $this->assertDoesNotMatchRegularExpression('/Feedback/', $output,
2486            'Do not show feedback if the grade is hidden in the gradebook');
2487    }
2488
2489    /**
2490     * Test reopen behavior when in "Manual" mode.
2491     */
2492    public function test_attempt_reopen_method_manual() {
2493        global $PAGE;
2494
2495        $this->resetAfterTest();
2496        $course = $this->getDataGenerator()->create_course();
2497        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2498        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2499
2500        $assign = $this->create_instance($course, [
2501            'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
2502            'maxattempts' => 3,
2503            'submissiondrafts' => 1,
2504            'assignsubmission_onlinetext_enabled' => 1,
2505        ]);
2506        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2507
2508        // Student should be able to see an add submission button.
2509        $this->setUser($student);
2510        $output = $assign->view_student_summary($student, true);
2511        $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));
2512
2513        // Add a submission.
2514        $this->add_submission($student, $assign);
2515        $this->submit_for_grading($student, $assign);
2516
2517        // Verify the student cannot make changes to the submission.
2518        $output = $assign->view_student_summary($student, true);
2519        $this->assertEquals(false, strpos($output, get_string('addsubmission', 'assign')));
2520
2521        // Mark the submission.
2522        $this->mark_submission($teacher, $assign, $student);
2523
2524        // Check the student can see the grade.
2525        $this->setUser($student);
2526        $output = $assign->view_student_summary($student, true);
2527        $this->assertNotEquals(false, strpos($output, '50.0'));
2528
2529        // Allow the student another attempt.
2530        $teacher->ignoresesskey = true;
2531        $this->setUser($teacher);
2532        $result = $assign->testable_process_add_attempt($student->id);
2533        $this->assertEquals(true, $result);
2534
2535        // Check that the previous attempt is now in the submission history table.
2536        $this->setUser($student);
2537        $output = $assign->view_student_summary($student, true);
2538        // Need a better check.
2539        $this->assertNotEquals(false, strpos($output, 'Submission text'), 'Contains: Submission text');
2540
2541        // Check that the student now has a button for Add a new attempt".
2542        $this->assertNotEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
2543        // Check that the student now does not have a button for Submit.
2544        $this->assertEquals(false, strpos($output, get_string('submitassignment', 'assign')));
2545
2546        // Check that the student now has a submission history.
2547        $this->assertNotEquals(false, strpos($output, get_string('attempthistory', 'assign')));
2548
2549        $this->setUser($teacher);
2550        // Check that the grading table loads correctly and contains this user.
2551        // This is also testing that we do not get duplicate rows in the grading table.
2552        $gradingtable = new assign_grading_table($assign, 100, '', 0, true);
2553        $output = $assign->get_renderer()->render($gradingtable);
2554        $this->assertEquals(true, strpos($output, $student->lastname));
2555
2556        // Should be 1 not 2.
2557        $this->assertEquals(1, $assign->count_submissions());
2558        $this->assertEquals(1, $assign->count_submissions_with_status('reopened'));
2559        $this->assertEquals(0, $assign->count_submissions_need_grading());
2560        $this->assertEquals(1, $assign->count_grades());
2561
2562        // Change max attempts to unlimited.
2563        $formdata = clone($assign->get_instance());
2564        $formdata->maxattempts = ASSIGN_UNLIMITED_ATTEMPTS;
2565        $formdata->instance = $formdata->id;
2566        $assign->update_instance($formdata);
2567
2568        // Mark the submission again.
2569        $this->mark_submission($teacher, $assign, $student, 60.0, [], 1);
2570
2571        // Check the grade exists.
2572        $this->setUser($teacher);
2573        $grades = $assign->get_user_grades_for_gradebook($student->id);
2574        $this->assertEquals(60, (int) $grades[$student->id]->rawgrade);
2575
2576        // Check we can reopen still.
2577        $result = $assign->testable_process_add_attempt($student->id);
2578        $this->assertEquals(true, $result);
2579
2580        // Should no longer have a grade because there is no grade for the latest attempt.
2581        $grades = $assign->get_user_grades_for_gradebook($student->id);
2582        $this->assertEmpty($grades);
2583    }
2584
2585    /**
2586     * Test reopen behavior when in "Reopen until pass" mode.
2587     */
2588    public function test_attempt_reopen_method_untilpass() {
2589        global $PAGE;
2590
2591        $this->resetAfterTest();
2592        $course = $this->getDataGenerator()->create_course();
2593        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2594        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2595
2596        $assign = $this->create_instance($course, [
2597            'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS,
2598            'maxattempts' => 3,
2599            'submissiondrafts' => 1,
2600            'assignsubmission_onlinetext_enabled' => 1,
2601        ]);
2602        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2603
2604        // Set grade to pass to 80.
2605        $gradeitem = $assign->get_grade_item();
2606        $gradeitem->gradepass = '80.0';
2607        $gradeitem->update();
2608
2609        // Student should be able to see an add submission button.
2610        $this->setUser($student);
2611        $output = $assign->view_student_summary($student, true);
2612        $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));
2613
2614        // Add a submission.
2615        $this->add_submission($student, $assign);
2616        $this->submit_for_grading($student, $assign);
2617
2618        // Verify the student cannot make a new attempt.
2619        $output = $assign->view_student_summary($student, true);
2620        $this->assertEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
2621
2622        // Mark the submission as non-passing.
2623        $this->mark_submission($teacher, $assign, $student, 50.0);
2624
2625        // Check the student can see the grade.
2626        $this->setUser($student);
2627        $output = $assign->view_student_summary($student, true);
2628        $this->assertNotEquals(false, strpos($output, '50.0'));
2629
2630        // Check that the student now has a button for Add a new attempt.
2631        $output = $assign->view_student_summary($student, true);
2632        $this->assertNotEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
2633
2634        // Check that the student now does not have a button for Submit.
2635        $this->assertEquals(false, strpos($output, get_string('submitassignment', 'assign')));
2636
2637        // Check that the student now has a submission history.
2638        $this->assertNotEquals(false, strpos($output, get_string('attempthistory', 'assign')));
2639
2640        // Add a second submission.
2641        $this->add_submission($student, $assign);
2642        $this->submit_for_grading($student, $assign);
2643
2644        // Mark the submission as passing.
2645        $this->mark_submission($teacher, $assign, $student, 80.0);
2646
2647        // Check that the student does not have a button for Add a new attempt.
2648        $this->setUser($student);
2649        $output = $assign->view_student_summary($student, true);
2650        $this->assertEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
2651
2652        // Re-mark the submission as not passing.
2653        $this->mark_submission($teacher, $assign, $student, 40.0, [], 1);
2654
2655        // Check that the student now has a button for Add a new attempt.
2656        $this->setUser($student);
2657        $output = $assign->view_student_summary($student, true);
2658        $this->assertMatchesRegularExpression('/' . get_string('addnewattempt', 'assign') . '/', $output);
2659        $this->assertNotEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
2660    }
2661
2662    public function test_attempt_reopen_method_untilpass_passing() {
2663        global $PAGE;
2664
2665        $this->resetAfterTest();
2666        $course = $this->getDataGenerator()->create_course();
2667        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2668        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2669
2670        $assign = $this->create_instance($course, [
2671            'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS,
2672            'maxattempts' => 3,
2673            'submissiondrafts' => 1,
2674            'assignsubmission_onlinetext_enabled' => 1,
2675        ]);
2676        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2677
2678        // Set grade to pass to 80.
2679        $gradeitem = $assign->get_grade_item();
2680        $gradeitem->gradepass = '80.0';
2681        $gradeitem->update();
2682
2683        // Student should be able to see an add submission button.
2684        $this->setUser($student);
2685        $output = $assign->view_student_summary($student, true);
2686        $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));
2687
2688        // Add a submission as a student.
2689        $this->add_submission($student, $assign);
2690        $this->submit_for_grading($student, $assign);
2691
2692        // Mark the submission as passing.
2693        $this->mark_submission($teacher, $assign, $student, 100.0);
2694
2695        // Check the student can see the grade.
2696        $this->setUser($student);
2697        $output = $assign->view_student_summary($student, true);
2698        $this->assertNotEquals(false, strpos($output, '100.0'));
2699
2700        // Check that the student does not have a button for Add a new attempt.
2701        $output = $assign->view_student_summary($student, true);
2702        $this->assertEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
2703    }
2704
2705    public function test_attempt_reopen_method_untilpass_no_passing_requirement() {
2706        global $PAGE;
2707
2708        $this->resetAfterTest();
2709        $course = $this->getDataGenerator()->create_course();
2710        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2711        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2712
2713        $assign = $this->create_instance($course, [
2714            'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS,
2715            'maxattempts' => 3,
2716            'submissiondrafts' => 1,
2717            'assignsubmission_onlinetext_enabled' => 1,
2718        ]);
2719        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2720
2721        // Set grade to pass to 0, so that no attempts should reopen.
2722        $gradeitem = $assign->get_grade_item();
2723        $gradeitem->gradepass = '0';
2724        $gradeitem->update();
2725
2726        // Student should be able to see an add submission button.
2727        $this->setUser($student);
2728        $output = $assign->view_student_summary($student, true);
2729        $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));
2730
2731        // Add a submission.
2732        $this->add_submission($student, $assign);
2733        $this->submit_for_grading($student, $assign);
2734
2735        // Mark the submission with any grade.
2736        $this->mark_submission($teacher, $assign, $student, 0.0);
2737
2738        // Check the student can see the grade.
2739        $this->setUser($student);
2740        $output = $assign->view_student_summary($student, true);
2741        $this->assertNotEquals(false, strpos($output, '0.0'));
2742
2743        // Check that the student does not have a button for Add a new attempt.
2744        $output = $assign->view_student_summary($student, true);
2745        $this->assertEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
2746    }
2747
2748    /**
2749     * Test student visibility for each stage of the marking workflow.
2750     */
2751    public function test_markingworkflow() {
2752        global $PAGE;
2753
2754        $this->resetAfterTest();
2755        $course = $this->getDataGenerator()->create_course();
2756        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2757        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2758
2759        $assign = $this->create_instance($course, [
2760            'markingworkflow' => 1,
2761        ]);
2762
2763        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2764
2765        // Mark the submission and set to notmarked.
2766        $this->mark_submission($teacher, $assign, $student, 50.0,  [
2767            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED,
2768        ]);
2769
2770        // Check the student can't see the grade.
2771        $this->setUser($student);
2772        $output = $assign->view_student_summary($student, true);
2773        $this->assertEquals(false, strpos($output, '50.0'));
2774
2775        // Make sure the grade isn't pushed to the gradebook.
2776        $grades = $assign->get_user_grades_for_gradebook($student->id);
2777        $this->assertEmpty($grades);
2778
2779        // Mark the submission and set to inmarking.
2780        $this->mark_submission($teacher, $assign, $student, 50.0,  [
2781            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_INMARKING,
2782        ]);
2783
2784        // Check the student can't see the grade.
2785        $this->setUser($student);
2786        $output = $assign->view_student_summary($student, true);
2787        $this->assertEquals(false, strpos($output, '50.0'));
2788
2789        // Make sure the grade isn't pushed to the gradebook.
2790        $grades = $assign->get_user_grades_for_gradebook($student->id);
2791        $this->assertEmpty($grades);
2792
2793        // Mark the submission and set to readyforreview.
2794        $this->mark_submission($teacher, $assign, $student, 50.0,  [
2795            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW,
2796        ]);
2797
2798        // Check the student can't see the grade.
2799        $this->setUser($student);
2800        $output = $assign->view_student_summary($student, true);
2801        $this->assertEquals(false, strpos($output, '50.0'));
2802
2803        // Make sure the grade isn't pushed to the gradebook.
2804        $grades = $assign->get_user_grades_for_gradebook($student->id);
2805        $this->assertEmpty($grades);
2806
2807        // Mark the submission and set to inreview.
2808        $this->mark_submission($teacher, $assign, $student, 50.0,  [
2809            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW,
2810        ]);
2811
2812        // Check the student can't see the grade.
2813        $this->setUser($student);
2814        $output = $assign->view_student_summary($student, true);
2815        $this->assertEquals(false, strpos($output, '50.0'));
2816
2817        // Make sure the grade isn't pushed to the gradebook.
2818        $grades = $assign->get_user_grades_for_gradebook($student->id);
2819        $this->assertEmpty($grades);
2820
2821        // Mark the submission and set to readyforrelease.
2822        $this->mark_submission($teacher, $assign, $student, 50.0,  [
2823            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE,
2824        ]);
2825
2826        // Check the student can't see the grade.
2827        $this->setUser($student);
2828        $output = $assign->view_student_summary($student, true);
2829        $this->assertEquals(false, strpos($output, '50.0'));
2830
2831        // Make sure the grade isn't pushed to the gradebook.
2832        $grades = $assign->get_user_grades_for_gradebook($student->id);
2833        $this->assertEmpty($grades);
2834
2835        // Mark the submission and set to released.
2836        $this->mark_submission($teacher, $assign, $student, 50.0,  [
2837            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
2838        ]);
2839
2840        // Check the student can see the grade.
2841        $this->setUser($student);
2842        $output = $assign->view_student_summary($student, true);
2843        $this->assertNotEquals(false, strpos($output, '50.0'));
2844
2845        // Make sure the grade is pushed to the gradebook.
2846        $grades = $assign->get_user_grades_for_gradebook($student->id);
2847        $this->assertEquals(50, (int)$grades[$student->id]->rawgrade);
2848    }
2849
2850    /**
2851     * Test that a student allocated a specific marker is only shown to that marker.
2852     */
2853    public function test_markerallocation() {
2854        global $PAGE;
2855
2856        $this->resetAfterTest();
2857        $course = $this->getDataGenerator()->create_course();
2858        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2859        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2860        $otherteacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
2861
2862        $assign = $this->create_instance($course, [
2863            'markingworkflow' => 1,
2864            'markingallocation' => 1
2865        ]);
2866
2867        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2868
2869        // Allocate marker to submission.
2870        $this->mark_submission($teacher, $assign, $student, null, [
2871            'allocatedmarker' => $teacher->id,
2872        ]);
2873
2874        // Check the allocated marker can view the submission.
2875        $this->setUser($teacher);
2876        $users = $assign->list_participants(0, true);
2877        $this->assertEquals(1, count($users));
2878        $this->assertTrue(isset($users[$student->id]));
2879
2880        $cm = get_coursemodule_from_instance('assign', $assign->get_instance()->id);
2881        $context = context_module::instance($cm->id);
2882        $assign = new mod_assign_testable_assign($context, $cm, $course);
2883
2884        // Check that other teachers can't view this submission.
2885        $this->setUser($otherteacher);
2886        $users = $assign->list_participants(0, true);
2887        $this->assertEquals(0, count($users));
2888    }
2889
2890    /**
2891     * Ensure that a teacher cannot submit for students as standard.
2892     */
2893    public function test_teacher_submit_for_student() {
2894        global $PAGE;
2895
2896        $this->resetAfterTest();
2897        $course = $this->getDataGenerator()->create_course();
2898        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2899        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2900
2901        $assign = $this->create_instance($course, [
2902            'assignsubmission_onlinetext_enabled' => 1,
2903            'submissiondrafts' => 1,
2904        ]);
2905
2906        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2907
2908        // Add a submission but do not submit.
2909        $this->add_submission($student, $assign, 'Student submission text');
2910
2911        $this->setUser($student);
2912        $output = $assign->view_student_summary($student, true);
2913        $this->assertStringContainsString('Student submission text', $output, 'Contains student submission text');
2914
2915        // Check that a teacher can not edit the submission as they do not have the capability.
2916        $this->setUser($teacher);
2917        $this->expectException('moodle_exception');
2918        $this->expectExceptionMessage('error/nopermission');
2919        $this->add_submission($student, $assign, 'Teacher edited submission text', false);
2920    }
2921
2922    /**
2923     * Ensure that a teacher with the editothersubmission capability can submit on behalf of a student.
2924     */
2925    public function test_teacher_submit_for_student_with_capability() {
2926        global $PAGE;
2927
2928        $this->resetAfterTest();
2929        $course = $this->getDataGenerator()->create_course();
2930        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2931        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2932        $otherteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
2933
2934        $assign = $this->create_instance($course, [
2935            'assignsubmission_onlinetext_enabled' => 1,
2936            'submissiondrafts' => 1,
2937        ]);
2938
2939        // Add the required capability.
2940        $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description');
2941        assign_capability('mod/assign:editothersubmission', CAP_ALLOW, $roleid, $assign->get_context()->id);
2942        role_assign($roleid, $teacher->id, $assign->get_context()->id);
2943        accesslib_clear_all_caches_for_unit_testing();
2944
2945        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
2946
2947        // Add a submission but do not submit.
2948        $this->add_submission($student, $assign, 'Student submission text');
2949
2950        $this->setUser($student);
2951        $output = $assign->view_student_summary($student, true);
2952        $this->assertStringContainsString('Student submission text', $output, 'Contains student submission text');
2953
2954        // Check that a teacher can edit the submission.
2955        $this->setUser($teacher);
2956        $this->add_submission($student, $assign, 'Teacher edited submission text', false);
2957
2958        $this->setUser($student);
2959        $output = $assign->view_student_summary($student, true);
2960        $this->assertStringNotContainsString('Student submission text', $output, 'Contains student submission text');
2961        $this->assertStringContainsString('Teacher edited submission text', $output, 'Contains teacher edited submission text');
2962
2963        // Check that the teacher can submit the students work.
2964        $this->setUser($teacher);
2965        $this->submit_for_grading($student, $assign, [], false);
2966
2967        // Revert to draft so the student can edit it.
2968        $assign->revert_to_draft($student->id);
2969
2970        $this->setUser($student);
2971
2972        // Check that the submission text was saved.
2973        $output = $assign->view_student_summary($student, true);
2974        $this->assertStringContainsString('Teacher edited submission text', $output, 'Contains student submission text');
2975
2976        // Check that the student can submit their work.
2977        $this->submit_for_grading($student, $assign, []);
2978
2979        $output = $assign->view_student_summary($student, true);
2980        $this->assertStringNotContainsString(get_string('addsubmission', 'assign'), $output);
2981
2982        // An editing teacher without the extra role should still be able to revert to draft.
2983        $this->setUser($otherteacher);
2984
2985        // Revert to draft so the submission is editable.
2986        $assign->revert_to_draft($student->id);
2987    }
2988
2989    /**
2990     * Ensure that disabling submit after the cutoff date works as expected.
2991     */
2992    public function test_disable_submit_after_cutoff_date() {
2993        global $PAGE;
2994
2995        $this->resetAfterTest();
2996        $course = $this->getDataGenerator()->create_course();
2997        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
2998
2999        $now = time();
3000        $tomorrow = $now + DAYSECS;
3001        $lastweek = $now - (7 * DAYSECS);
3002        $yesterday = $now - DAYSECS;
3003
3004        $this->setAdminUser();
3005        $assign = $this->create_instance($course, [
3006            'duedate' => $yesterday,
3007            'cutoffdate' => $tomorrow,
3008            'assignsubmission_onlinetext_enabled' => 1,
3009        ]);
3010
3011        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
3012
3013        // Student should be able to see an add submission button.
3014        $this->setUser($student);
3015        $output = $assign->view_student_summary($student, true);
3016        $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));
3017
3018        // Add a submission but don't submit now.
3019        $this->add_submission($student, $assign);
3020
3021        // Create another instance with cut-off and due-date already passed.
3022        $this->setAdminUser();
3023        $assign = $this->create_instance($course, [
3024            'duedate' => $lastweek,
3025            'cutoffdate' => $yesterday,
3026            'assignsubmission_onlinetext_enabled' => 1,
3027        ]);
3028
3029        $this->setUser($student);
3030        $output = $assign->view_student_summary($student, true);
3031        $this->assertStringNotContainsString($output, get_string('editsubmission', 'assign'),
3032            'Should not be able to edit after cutoff date.');
3033        $this->assertStringNotContainsString($output, get_string('submitassignment', 'assign'),
3034            'Should not be able to submit after cutoff date.');
3035    }
3036
3037    /**
3038     * Testing for submission comment plugin settings.
3039     *
3040     * @dataProvider submission_plugin_settings_provider
3041     * @param   bool    $globalenabled
3042     * @param   array   $instanceconfig
3043     * @param   bool    $isenabled
3044     */
3045    public function test_submission_comment_plugin_settings($globalenabled, $instanceconfig, $isenabled) {
3046        global $CFG;
3047
3048        $this->resetAfterTest();
3049        $course = $this->getDataGenerator()->create_course();
3050
3051        $CFG->usecomments = $globalenabled;
3052        $assign = $this->create_instance($course, $instanceconfig);
3053        $plugin = $assign->get_submission_plugin_by_type('comments');
3054        $this->assertEquals($isenabled, (bool) $plugin->is_enabled('enabled'));
3055    }
3056
3057    public function submission_plugin_settings_provider() {
3058        return [
3059            'CFG->usecomments true, empty config => Enabled by default' => [
3060                true,
3061                [],
3062                true,
3063            ],
3064            'CFG->usecomments true, config enabled => Comments enabled' => [
3065                true,
3066                [
3067                    'assignsubmission_comments_enabled' => 1,
3068                ],
3069                true,
3070            ],
3071            'CFG->usecomments true, config idisabled => Comments enabled' => [
3072                true,
3073                [
3074                    'assignsubmission_comments_enabled' => 0,
3075                ],
3076                true,
3077            ],
3078            'CFG->usecomments false, empty config => Disabled by default' => [
3079                false,
3080                [],
3081                false,
3082            ],
3083            'CFG->usecomments false, config enabled => Comments disabled' => [
3084                false,
3085                [
3086                    'assignsubmission_comments_enabled' => 1,
3087                ],
3088                false,
3089            ],
3090            'CFG->usecomments false, config disabled => Comments disabled' => [
3091                false,
3092                [
3093                    'assignsubmission_comments_enabled' => 0,
3094                ],
3095                false,
3096            ],
3097        ];
3098    }
3099
3100    /**
3101     * Testing for comment inline settings
3102     */
3103    public function test_feedback_comment_commentinline() {
3104        global $CFG, $USER;
3105
3106        $this->resetAfterTest();
3107        $course = $this->getDataGenerator()->create_course();
3108        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3109        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3110
3111        $sourcetext = "Hello!
3112
3113I'm writing to you from the Moodle Majlis in Muscat, Oman, where we just had several days of Moodle community goodness.
3114
3115URL outside a tag: https://moodle.org/logo/logo-240x60.gif
3116Plugin url outside a tag: @@PLUGINFILE@@/logo-240x60.gif
3117
3118External link 1:<img src='https://moodle.org/logo/logo-240x60.gif' alt='Moodle'/>
3119External link 2:<img alt=\"Moodle\" src=\"https://moodle.org/logo/logo-240x60.gif\"/>
3120Internal link 1:<img src='@@PLUGINFILE@@/logo-240x60.gif' alt='Moodle'/>
3121Internal link 2:<img alt=\"Moodle\" src=\"@@PLUGINFILE@@logo-240x60.gif\"/>
3122Anchor link 1:<a href=\"@@PLUGINFILE@@logo-240x60.gif\" alt=\"bananas\">Link text</a>
3123Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
3124";
3125
3126        $this->setUser($teacher);
3127        $assign = $this->create_instance($course, [
3128            'assignsubmission_onlinetext_enabled' => 1,
3129            'assignfeedback_comments_enabled' => 1,
3130            'assignfeedback_comments_commentinline' => 1,
3131        ]);
3132
3133        $this->setUser($student);
3134
3135        // Add a submission but don't submit now.
3136        $this->add_submission($student, $assign, $sourcetext);
3137
3138        $this->setUser($teacher);
3139
3140        $data = new stdClass();
3141        require_once($CFG->dirroot . '/mod/assign/gradeform.php');
3142        $pagination = [
3143            'userid' => $student->id,
3144            'rownum' => 0,
3145            'last' => true,
3146            'useridlistid' => $assign->get_useridlist_key_id(),
3147            'attemptnumber' => 0,
3148        ];
3149        $formparams = array($assign, $data, $pagination);
3150        $mform = new mod_assign_grade_form(null, [$assign, $data, $pagination]);
3151
3152        // We need to get the URL these will be transformed to.
3153        $context = context_user::instance($USER->id);
3154        $itemid = $data->assignfeedbackcomments_editor['itemid'];
3155        $url = $CFG->wwwroot . '/draftfile.php/' . $context->id . '/user/draft/' . $itemid;
3156
3157        // Note the internal images have been stripped and the html is purified (quotes fixed in this case).
3158        $filteredtext = "Hello!
3159
3160I'm writing to you from the Moodle Majlis in Muscat, Oman, where we just had several days of Moodle community goodness.
3161
3162URL outside a tag: https://moodle.org/logo/logo-240x60.gif
3163Plugin url outside a tag: $url/logo-240x60.gif
3164
3165External link 1:<img src=\"https://moodle.org/logo/logo-240x60.gif\" alt=\"Moodle\" />
3166External link 2:<img alt=\"Moodle\" src=\"https://moodle.org/logo/logo-240x60.gif\" />
3167Internal link 1:<img src=\"$url/logo-240x60.gif\" alt=\"Moodle\" />
3168Internal link 2:<img alt=\"Moodle\" src=\"@@PLUGINFILE@@logo-240x60.gif\" />
3169Anchor link 1:<a href=\"@@PLUGINFILE@@logo-240x60.gif\">Link text</a>
3170Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
3171";
3172
3173        $this->assertEquals($filteredtext, $data->assignfeedbackcomments_editor['text']);
3174    }
3175
3176    /**
3177     * Testing for feedback comment plugin settings.
3178     *
3179     * @dataProvider feedback_plugin_settings_provider
3180     * @param   array   $instanceconfig
3181     * @param   bool    $isenabled
3182     */
3183    public function test_feedback_plugin_settings($instanceconfig, $isenabled) {
3184        $this->resetAfterTest();
3185        $course = $this->getDataGenerator()->create_course();
3186
3187        $assign = $this->create_instance($course, $instanceconfig);
3188        $plugin = $assign->get_feedback_plugin_by_type('comments');
3189        $this->assertEquals($isenabled, (bool) $plugin->is_enabled('enabled'));
3190    }
3191
3192    public function feedback_plugin_settings_provider() {
3193        return [
3194            'No configuration => disabled' => [
3195                [],
3196                false,
3197            ],
3198            'Actively disabled' => [
3199                [
3200                    'assignfeedback_comments_enabled' => 0,
3201                ],
3202                false,
3203            ],
3204            'Actively enabled' => [
3205                [
3206                    'assignfeedback_comments_enabled' => 1,
3207                ],
3208                true,
3209            ],
3210        ];
3211    }
3212
3213    /**
3214     * Testing if gradebook feedback plugin is enabled.
3215     */
3216    public function test_is_gradebook_feedback_enabled() {
3217        $this->resetAfterTest();
3218        $course = $this->getDataGenerator()->create_course();
3219        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3220        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3221
3222        $adminconfig = get_config('assign');
3223        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
3224
3225        // Create assignment with gradebook feedback enabled and grade = 0.
3226        $assign = $this->create_instance($course, [
3227            "{$gradebookplugin}_enabled" => 1,
3228            'grades' => 0,
3229        ]);
3230
3231        // Get gradebook feedback plugin.
3232        $gradebookplugintype = str_replace('assignfeedback_', '', $gradebookplugin);
3233        $plugin = $assign->get_feedback_plugin_by_type($gradebookplugintype);
3234        $this->assertEquals(1, $plugin->is_enabled('enabled'));
3235        $this->assertEquals(1, $assign->is_gradebook_feedback_enabled());
3236    }
3237
3238    /**
3239     * Testing if gradebook feedback plugin is disabled.
3240     */
3241    public function test_is_gradebook_feedback_disabled() {
3242        $this->resetAfterTest();
3243        $course = $this->getDataGenerator()->create_course();
3244        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3245        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3246
3247        $adminconfig = get_config('assign');
3248        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
3249
3250        // Create assignment with gradebook feedback disabled and grade = 0.
3251        $assign = $this->create_instance($course, [
3252            "{$gradebookplugin}_enabled" => 0,
3253            'grades' => 0,
3254        ]);
3255
3256        $gradebookplugintype = str_replace('assignfeedback_', '', $gradebookplugin);
3257        $plugin = $assign->get_feedback_plugin_by_type($gradebookplugintype);
3258        $this->assertEquals(0, $plugin->is_enabled('enabled'));
3259    }
3260
3261    /**
3262     * Testing can_edit_submission.
3263     */
3264    public function test_can_edit_submission() {
3265        $this->resetAfterTest();
3266        $course = $this->getDataGenerator()->create_course();
3267        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3268        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3269        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
3270
3271        $assign = $this->create_instance($course, [
3272            'assignsubmission_onlinetext_enabled' => 1,
3273            'submissiondrafts' => 1,
3274        ]);
3275
3276        // Check student can edit their own submission.
3277        $this->assertTrue($assign->can_edit_submission($student->id, $student->id));
3278
3279        // Check student cannot edit others submission.
3280        $this->assertFalse($assign->can_edit_submission($otherstudent->id, $student->id));
3281
3282        // Check teacher cannot (by default) edit a students submission.
3283        $this->assertFalse($assign->can_edit_submission($student->id, $teacher->id));
3284    }
3285
3286    /**
3287     * Testing can_edit_submission with the editothersubmission capability.
3288     */
3289    public function test_can_edit_submission_with_editothersubmission() {
3290        $this->resetAfterTest();
3291        $course = $this->getDataGenerator()->create_course();
3292        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3293        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3294        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
3295
3296        $assign = $this->create_instance($course, [
3297            'assignsubmission_onlinetext_enabled' => 1,
3298            'submissiondrafts' => 1,
3299        ]);
3300
3301        // Add the required capability to edit a student submission.
3302        $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description');
3303        assign_capability('mod/assign:editothersubmission', CAP_ALLOW, $roleid, $assign->get_context()->id);
3304        role_assign($roleid, $teacher->id, $assign->get_context()->id);
3305        accesslib_clear_all_caches_for_unit_testing();
3306
3307        // Check student can edit their own submission.
3308        $this->assertTrue($assign->can_edit_submission($student->id, $student->id));
3309
3310        // Check student cannot edit others submission.
3311        $this->assertFalse($assign->can_edit_submission($otherstudent->id, $student->id));
3312
3313        // Retest - should now have access.
3314        $this->assertTrue($assign->can_edit_submission($student->id, $teacher->id));
3315    }
3316
3317    /**
3318     * Testing can_edit_submission
3319     */
3320    public function test_can_edit_submission_separategroups() {
3321        $this->resetAfterTest();
3322        $course = $this->getDataGenerator()->create_course();
3323        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3324
3325        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3326        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3327        $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3328        $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3329
3330        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
3331        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3332        groups_assign_grouping($grouping->id, $group1->id);
3333        groups_add_member($group1, $student1);
3334        groups_add_member($group1, $student2);
3335
3336        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3337        groups_assign_grouping($grouping->id, $group2->id);
3338        groups_add_member($group2, $student3);
3339        groups_add_member($group2, $student4);
3340
3341        $assign = $this->create_instance($course, [
3342            'assignsubmission_onlinetext_enabled' => 1,
3343            'submissiondrafts' => 1,
3344            'groupingid' => $grouping->id,
3345            'groupmode' => SEPARATEGROUPS,
3346        ]);
3347
3348        // Verify a student does not have the ability to edit submissions for other users.
3349        $this->assertTrue($assign->can_edit_submission($student1->id, $student1->id));
3350        $this->assertFalse($assign->can_edit_submission($student2->id, $student1->id));
3351        $this->assertFalse($assign->can_edit_submission($student3->id, $student1->id));
3352        $this->assertFalse($assign->can_edit_submission($student4->id, $student1->id));
3353    }
3354
3355    /**
3356     * Testing can_edit_submission
3357     */
3358    public function test_can_edit_submission_separategroups_with_editothersubmission() {
3359        $this->resetAfterTest();
3360        $course = $this->getDataGenerator()->create_course();
3361        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3362
3363        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3364        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3365        $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3366        $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3367
3368        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
3369        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3370        groups_assign_grouping($grouping->id, $group1->id);
3371        groups_add_member($group1, $student1);
3372        groups_add_member($group1, $student2);
3373
3374        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3375        groups_assign_grouping($grouping->id, $group2->id);
3376        groups_add_member($group2, $student3);
3377        groups_add_member($group2, $student4);
3378
3379        $assign = $this->create_instance($course, [
3380            'assignsubmission_onlinetext_enabled' => 1,
3381            'submissiondrafts' => 1,
3382            'groupingid' => $grouping->id,
3383            'groupmode' => SEPARATEGROUPS,
3384        ]);
3385
3386        // Add the capability to the new assignment for student 1.
3387        $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description');
3388        assign_capability('mod/assign:editothersubmission', CAP_ALLOW, $roleid, $assign->get_context()->id);
3389        role_assign($roleid, $student1->id, $assign->get_context()->id);
3390        accesslib_clear_all_caches_for_unit_testing();
3391
3392        // Verify student1 has the ability to edit submissions for other users in their group, but not other groups.
3393        $this->assertTrue($assign->can_edit_submission($student1->id, $student1->id));
3394        $this->assertTrue($assign->can_edit_submission($student2->id, $student1->id));
3395        $this->assertFalse($assign->can_edit_submission($student3->id, $student1->id));
3396        $this->assertFalse($assign->can_edit_submission($student4->id, $student1->id));
3397
3398        // Verify other students do not have the ability to edit submissions for other users.
3399        $this->assertTrue($assign->can_edit_submission($student2->id, $student2->id));
3400        $this->assertFalse($assign->can_edit_submission($student1->id, $student2->id));
3401        $this->assertFalse($assign->can_edit_submission($student3->id, $student2->id));
3402        $this->assertFalse($assign->can_edit_submission($student4->id, $student2->id));
3403    }
3404
3405    /**
3406     * Test if the view blind details capability works
3407     */
3408    public function test_can_view_blind_details() {
3409        global $DB;
3410
3411        $this->resetAfterTest();
3412        $course = $this->getDataGenerator()->create_course();
3413        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3414        $manager = $this->getDataGenerator()->create_and_enrol($course, 'manager');
3415        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3416
3417        $assign = $this->create_instance($course, [
3418            'blindmarking' => 1,
3419        ]);
3420
3421        $this->assertTrue($assign->is_blind_marking());
3422
3423        // Test student names are hidden to teacher.
3424        $this->setUser($teacher);
3425        $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
3426        $output = $assign->get_renderer()->render($gradingtable);
3427        $this->assertEquals(true, strpos($output, get_string('hiddenuser', 'assign')));    // "Participant" is somewhere on the page.
3428        $this->assertEquals(false, strpos($output, fullname($student)));    // Students full name doesn't appear.
3429
3430        // Test student names are visible to manager.
3431        $this->setUser($manager);
3432        $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
3433        $output = $assign->get_renderer()->render($gradingtable);
3434        $this->assertEquals(true, strpos($output, get_string('hiddenuser', 'assign')));
3435        $this->assertEquals(true, strpos($output, fullname($student)));
3436    }
3437
3438    /**
3439     * Testing get_shared_group_members
3440     */
3441    public function test_get_shared_group_members() {
3442        $this->resetAfterTest();
3443        $course = $this->getDataGenerator()->create_course();
3444        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3445
3446        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3447        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3448        $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3449        $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3450
3451        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
3452        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3453        groups_assign_grouping($grouping->id, $group1->id);
3454        groups_add_member($group1, $student1);
3455        groups_add_member($group1, $student2);
3456
3457        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3458        groups_assign_grouping($grouping->id, $group2->id);
3459        groups_add_member($group2, $student3);
3460        groups_add_member($group2, $student4);
3461
3462        $assign = $this->create_instance($course, [
3463            'groupingid' => $grouping->id,
3464            'groupmode' => SEPARATEGROUPS,
3465        ]);
3466
3467        $cm = $assign->get_course_module();
3468
3469        // Get shared group members for students 0 and 1.
3470        $groupmembers = $assign->get_shared_group_members($cm, $student1->id);
3471        $this->assertCount(2, $groupmembers);
3472        $this->assertContainsEquals($student1->id, $groupmembers);
3473        $this->assertContainsEquals($student2->id, $groupmembers);
3474
3475        $groupmembers = $assign->get_shared_group_members($cm, $student2->id);
3476        $this->assertCount(2, $groupmembers);
3477        $this->assertContainsEquals($student1->id, $groupmembers);
3478        $this->assertContainsEquals($student2->id, $groupmembers);
3479
3480        $groupmembers = $assign->get_shared_group_members($cm, $student3->id);
3481        $this->assertCount(2, $groupmembers);
3482        $this->assertContainsEquals($student3->id, $groupmembers);
3483        $this->assertContainsEquals($student4->id, $groupmembers);
3484
3485        $groupmembers = $assign->get_shared_group_members($cm, $student4->id);
3486        $this->assertCount(2, $groupmembers);
3487        $this->assertContainsEquals($student3->id, $groupmembers);
3488        $this->assertContainsEquals($student4->id, $groupmembers);
3489    }
3490
3491    /**
3492     * Testing get_shared_group_members
3493     */
3494    public function test_get_shared_group_members_override() {
3495        $this->resetAfterTest();
3496        $course = $this->getDataGenerator()->create_course();
3497        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3498
3499        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3500        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3501        $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3502        $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3503
3504        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
3505        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3506        groups_assign_grouping($grouping->id, $group1->id);
3507        groups_add_member($group1, $student1);
3508        groups_add_member($group1, $student2);
3509
3510        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3511        groups_assign_grouping($grouping->id, $group2->id);
3512        groups_add_member($group2, $student3);
3513        groups_add_member($group2, $student4);
3514
3515        $assign = $this->create_instance($course, [
3516            'groupingid' => $grouping->id,
3517            'groupmode' => SEPARATEGROUPS,
3518        ]);
3519
3520        $cm = $assign->get_course_module();
3521
3522        // Add the capability to access allgroups for one of the students.
3523        $roleid = create_role('Access all groups role', 'accessallgroupsrole', '');
3524        assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $assign->get_context()->id);
3525        role_assign($roleid, $student1->id, $assign->get_context()->id);
3526        accesslib_clear_all_caches_for_unit_testing();
3527
3528        // Get shared group members for students 0 and 1.
3529        $groupmembers = $assign->get_shared_group_members($cm, $student1->id);
3530        $this->assertCount(4, $groupmembers);
3531        $this->assertContainsEquals($student1->id, $groupmembers);
3532        $this->assertContainsEquals($student2->id, $groupmembers);
3533        $this->assertContainsEquals($student3->id, $groupmembers);
3534        $this->assertContainsEquals($student4->id, $groupmembers);
3535
3536        $groupmembers = $assign->get_shared_group_members($cm, $student2->id);
3537        $this->assertCount(2, $groupmembers);
3538        $this->assertContainsEquals($student1->id, $groupmembers);
3539        $this->assertContainsEquals($student2->id, $groupmembers);
3540
3541        $groupmembers = $assign->get_shared_group_members($cm, $student3->id);
3542        $this->assertCount(2, $groupmembers);
3543        $this->assertContainsEquals($student3->id, $groupmembers);
3544        $this->assertContainsEquals($student4->id, $groupmembers);
3545
3546        $groupmembers = $assign->get_shared_group_members($cm, $student4->id);
3547        $this->assertCount(2, $groupmembers);
3548        $this->assertContainsEquals($student3->id, $groupmembers);
3549        $this->assertContainsEquals($student4->id, $groupmembers);
3550    }
3551
3552    /**
3553     * Test get plugins file areas
3554     */
3555    public function test_get_plugins_file_areas() {
3556        global $DB;
3557
3558        $this->resetAfterTest();
3559        $course = $this->getDataGenerator()->create_course();
3560        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3561
3562        $assign = $this->create_instance($course);
3563
3564        // Test that all the submission and feedback plugins are returning the expected file aras.
3565        $usingfilearea = 0;
3566        $coreplugins = core_plugin_manager::standard_plugins_list('assignsubmission');
3567        foreach ($assign->get_submission_plugins() as $plugin) {
3568            $type = $plugin->get_type();
3569            if (!in_array($type, $coreplugins)) {
3570                continue;
3571            }
3572            $fileareas = $plugin->get_file_areas();
3573
3574            if ($type == 'onlinetext') {
3575                $this->assertEquals(array('submissions_onlinetext' => 'Online text'), $fileareas);
3576                $usingfilearea++;
3577            } else if ($type == 'file') {
3578                $this->assertEquals(array('submission_files' => 'File submissions'), $fileareas);
3579                $usingfilearea++;
3580            } else {
3581                $this->assertEmpty($fileareas);
3582            }
3583        }
3584        $this->assertEquals(2, $usingfilearea);
3585
3586        $usingfilearea = 0;
3587        $coreplugins = core_plugin_manager::standard_plugins_list('assignfeedback');
3588        foreach ($assign->get_feedback_plugins() as $plugin) {
3589            $type = $plugin->get_type();
3590            if (!in_array($type, $coreplugins)) {
3591                continue;
3592            }
3593            $fileareas = $plugin->get_file_areas();
3594
3595            if ($type == 'editpdf') {
3596                $this->assertEquals(array('download' => 'Annotate PDF'), $fileareas);
3597                $usingfilearea++;
3598            } else if ($type == 'file') {
3599                $this->assertEquals(array('feedback_files' => 'Feedback files'), $fileareas);
3600                $usingfilearea++;
3601            } else if ($type == 'comments') {
3602                $this->assertEquals(array('feedback' => 'Feedback comments'), $fileareas);
3603                $usingfilearea++;
3604            } else {
3605                $this->assertEmpty($fileareas);
3606            }
3607        }
3608        $this->assertEquals(3, $usingfilearea);
3609    }
3610
3611    /**
3612     * Test override exists
3613     *
3614     * This function needs to obey the group override logic as per the assign grading table and
3615     * the overview block.
3616     */
3617    public function test_override_exists() {
3618        global $DB;
3619
3620        $this->resetAfterTest();
3621        $course = $this->getDataGenerator()->create_course();
3622        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3623
3624        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
3625        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3626        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3627
3628        // Data:
3629        // - student1 => group A only
3630        // - student2 => group B only
3631        // - student3 => Group A + Group B (No user override)
3632        // - student4 => Group A + Group B (With user override)
3633        // - student4 => No groups.
3634        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3635        groups_add_member($group1, $student1);
3636
3637        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3638        groups_add_member($group2, $student2);
3639
3640        $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3641        groups_add_member($group1, $student3);
3642        groups_add_member($group2, $student3);
3643
3644        $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3645        groups_add_member($group1, $student4);
3646        groups_add_member($group2, $student4);
3647
3648        $student5 = $this->getDataGenerator()->create_and_enrol($course, 'student');
3649
3650        $assign = $this->create_instance($course);
3651        $instance = $assign->get_instance();
3652
3653        // Overrides for each of the groups, and a user override.
3654        $overrides = [
3655            (object) [
3656                // Override for group 1, highest priority (numerically lowest sortorder).
3657                'assignid' => $instance->id,
3658                'groupid' => $group1->id,
3659                'userid' => null,
3660                'sortorder' => 1,
3661                'allowsubmissionsfromdate' => 1,
3662                'duedate' => 2,
3663                'cutoffdate' => 3
3664            ],
3665            (object) [
3666                // Override for group 2, lower priority (numerically higher sortorder).
3667                'assignid' => $instance->id,
3668                'groupid' => $group2->id,
3669                'userid' => null,
3670                'sortorder' => 2,
3671                'allowsubmissionsfromdate' => 5,
3672                'duedate' => 6,
3673                'cutoffdate' => 6
3674            ],
3675            (object) [
3676                // User override.
3677                'assignid' => $instance->id,
3678                'groupid' => null,
3679                'userid' => $student3->id,
3680                'sortorder' => null,
3681                'allowsubmissionsfromdate' => 7,
3682                'duedate' => 8,
3683                'cutoffdate' => 9
3684            ],
3685        ];
3686
3687        foreach ($overrides as &$override) {
3688            $override->id = $DB->insert_record('assign_overrides', $override);
3689        }
3690
3691        // User only in group 1 should see the group 1 override.
3692        $this->assertEquals($overrides[0], $assign->override_exists($student1->id));
3693
3694        // User only in group 2 should see the group 2 override.
3695        $this->assertEquals($overrides[1], $assign->override_exists($student2->id));
3696
3697        // User only in both groups with an override should see the user override as it has higher priority.
3698        $this->assertEquals($overrides[2], $assign->override_exists($student3->id));
3699
3700        // User only in both groups with no override should see the group 1 override as it has higher priority.
3701        $this->assertEquals($overrides[0], $assign->override_exists($student4->id));
3702
3703        // User with no overrides shoudl get nothing.
3704        $override = $assign->override_exists($student5->id);
3705        $this->assertNull($override->duedate);
3706        $this->assertNull($override->cutoffdate);
3707        $this->assertNull($override->allowsubmissionsfromdate);
3708    }
3709
3710    /**
3711     * Test the quicksave grades processor
3712     */
3713    public function test_process_save_quick_grades() {
3714        $this->resetAfterTest();
3715        $course = $this->getDataGenerator()->create_course();
3716        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3717        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3718
3719        $teacher->ignoresesskey = true;
3720        $this->setUser($teacher);
3721        $assign = $this->create_instance($course, [
3722                'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
3723            ]);
3724
3725        // Initially grade the user.
3726        $grade = (object) [
3727            'attemptnumber' => '',
3728            'timemodified' => '',
3729        ];
3730        $data = [
3731            "grademodified_{$student->id}" => $grade->timemodified,
3732            "gradeattempt_{$student->id}" => $grade->attemptnumber,
3733            "quickgrade_{$student->id}" => '60.0',
3734        ];
3735
3736        $result = $assign->testable_process_save_quick_grades($data);
3737        $this->assertStringContainsString(get_string('quickgradingchangessaved', 'assign'), $result);
3738        $grade = $assign->get_user_grade($student->id, false);
3739        $this->assertEquals(60.0, $grade->grade);
3740
3741        // Attempt to grade with a past attempts grade info.
3742        $assign->testable_process_add_attempt($student->id);
3743        $data = array(
3744            'grademodified_' . $student->id => $grade->timemodified,
3745            'gradeattempt_' . $student->id => $grade->attemptnumber,
3746            'quickgrade_' . $student->id => '50.0'
3747        );
3748        $result = $assign->testable_process_save_quick_grades($data);
3749        $this->assertStringContainsString(get_string('errorrecordmodified', 'assign'), $result);
3750        $grade = $assign->get_user_grade($student->id, false);
3751        $this->assertFalse($grade);
3752
3753        // Attempt to grade a the attempt.
3754        $submission = $assign->get_user_submission($student->id, false);
3755        $data = array(
3756            'grademodified_' . $student->id => '',
3757            'gradeattempt_' . $student->id => $submission->attemptnumber,
3758            'quickgrade_' . $student->id => '40.0'
3759        );
3760        $result = $assign->testable_process_save_quick_grades($data);
3761        $this->assertStringContainsString(get_string('quickgradingchangessaved', 'assign'), $result);
3762        $grade = $assign->get_user_grade($student->id, false);
3763        $this->assertEquals(40.0, $grade->grade);
3764
3765        // Catch grade update conflicts.
3766        // Save old data for later.
3767        $pastdata = $data;
3768        // Update the grade the 'good' way.
3769        $data = array(
3770            'grademodified_' . $student->id => $grade->timemodified,
3771            'gradeattempt_' . $student->id => $grade->attemptnumber,
3772            'quickgrade_' . $student->id => '30.0'
3773        );
3774        $result = $assign->testable_process_save_quick_grades($data);
3775        $this->assertStringContainsString(get_string('quickgradingchangessaved', 'assign'), $result);
3776        $grade = $assign->get_user_grade($student->id, false);
3777        $this->assertEquals(30.0, $grade->grade);
3778
3779        // Now update using 'old' data. Should fail.
3780        $result = $assign->testable_process_save_quick_grades($pastdata);
3781        $this->assertStringContainsString(get_string('errorrecordmodified', 'assign'), $result);
3782        $grade = $assign->get_user_grade($student->id, false);
3783        $this->assertEquals(30.0, $grade->grade);
3784    }
3785
3786    /**
3787     * Test updating activity completion when submitting an assessment.
3788     */
3789    public function test_update_activity_completion_records_solitary_submission() {
3790        $this->resetAfterTest();
3791
3792        $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
3793        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3794        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3795
3796        $this->setUser($teacher);
3797        $assign = $this->create_instance($course, [
3798            'grade' => 100,
3799            'completion' => COMPLETION_TRACKING_AUTOMATIC,
3800            'requireallteammemberssubmit' => 0,
3801        ]);
3802        $cm = $assign->get_course_module();
3803
3804        // Submit the assignment as the student.
3805        $this->add_submission($student, $assign);
3806
3807        // Check that completion is not met yet.
3808        $completion = new completion_info($course);
3809        $completiondata = $completion->get_data($cm, false, $student->id);
3810        $this->assertEquals(0, $completiondata->completionstate);
3811
3812        // Update to mark as complete.
3813        $submission = $assign->get_user_submission($student->id, true);
3814        $assign->testable_update_activity_completion_records(0, 0, $submission,
3815            $student->id, COMPLETION_COMPLETE, $completion);
3816
3817        // Completion should now be met.
3818        $completiondata = $completion->get_data($cm, false, $student->id);
3819        $this->assertEquals(1, $completiondata->completionstate);
3820    }
3821
3822    /**
3823     * Test updating activity completion when submitting an assessment.
3824     */
3825    public function test_update_activity_completion_records_team_submission() {
3826        $this->resetAfterTest();
3827
3828        $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
3829        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3830        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3831        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
3832
3833        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
3834        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3835
3836        groups_add_member($group1, $student);
3837        groups_add_member($group1, $otherstudent);
3838
3839        $assign = $this->create_instance($course, [
3840            'grade' => 100,
3841            'completion' => COMPLETION_TRACKING_AUTOMATIC,
3842            'teamsubmission' => 1,
3843        ]);
3844
3845        $cm = $assign->get_course_module();
3846
3847        $this->add_submission($student, $assign);
3848        $this->submit_for_grading($student, $assign, ['groupid' => $group1->id]);
3849
3850        $completion = new completion_info($course);
3851
3852        // Check that completion is not met yet.
3853        $completiondata = $completion->get_data($cm, false, $student->id);
3854        $this->assertEquals(0, $completiondata->completionstate);
3855
3856        $completiondata = $completion->get_data($cm, false, $otherstudent->id);
3857        $this->assertEquals(0, $completiondata->completionstate);
3858
3859        $submission = $assign->get_user_submission($student->id, true);
3860        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
3861        $submission->groupid = $group1->id;
3862
3863        $assign->testable_update_activity_completion_records(1, 0, $submission, $student->id, COMPLETION_COMPLETE, $completion);
3864
3865        // Completion should now be met.
3866        $completiondata = $completion->get_data($cm, false, $student->id);
3867        $this->assertEquals(1, $completiondata->completionstate);
3868
3869        $completiondata = $completion->get_data($cm, false, $otherstudent->id);
3870        $this->assertEquals(1, $completiondata->completionstate);
3871    }
3872
3873    /**
3874     * Test updating activity completion when submitting an assessment for MDL-67126.
3875     */
3876    public function test_update_activity_completion_records_team_submission_new() {
3877        $this->resetAfterTest();
3878
3879        $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
3880        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3881        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3882        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
3883
3884        $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
3885        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
3886
3887        groups_add_member($group1, $student);
3888        groups_add_member($group1, $otherstudent);
3889
3890        $assign = $this->create_instance($course, [
3891            'submissiondrafts' => 0,
3892            'completion' => COMPLETION_TRACKING_AUTOMATIC,
3893            'completionsubmit' => 1,
3894            'teamsubmission' => 1,
3895            'assignsubmission_onlinetext_enabled' => 1
3896        ]);
3897
3898        $cm = $assign->get_course_module();
3899
3900        $this->add_submission($student, $assign);
3901
3902        $completion = new completion_info($course);
3903
3904        // Completion should now be met.
3905        $completiondata = $completion->get_data($cm, false, $student->id);
3906        $this->assertEquals(1, $completiondata->completionstate);
3907
3908        $completiondata = $completion->get_data($cm, false, $otherstudent->id);
3909        $this->assertEquals(1, $completiondata->completionstate);
3910    }
3911
3912    /**
3913     * Data provider for test_fix_null_grades
3914     * @return array[] Test data for test_fix_null_grades. Each element should contain grade, expectedcount and gradebookvalue
3915     */
3916    public function fix_null_grades_provider() {
3917        return [
3918            'Negative less than one is errant' => [
3919                'grade' => -0.64,
3920                'gradebookvalue' => null,
3921            ],
3922            'Negative more than one is errant' => [
3923                'grade' => -30.18,
3924                'gradebookvalue' => null,
3925            ],
3926            'Negative one exactly is not errant, but shouldn\'t be pushed to gradebook' => [
3927                'grade' => ASSIGN_GRADE_NOT_SET,
3928                'gradebookvalue' => null,
3929            ],
3930            'Positive grade is not errant' => [
3931                'grade' => 1,
3932                'gradebookvalue' => 1,
3933            ],
3934            'Large grade is not errant' => [
3935                'grade' => 100,
3936                'gradebookvalue' => 100,
3937            ],
3938            'Zero grade is not errant' => [
3939                'grade' => 0,
3940                'gradebookvalue' => 0,
3941            ],
3942        ];
3943    }
3944
3945    /**
3946     * Test fix_null_grades
3947     * @param number $grade The grade we should set in the assign grading table.
3948     * @param number $expectedcount The finalgrade we expect in the gradebook after fixing the grades.
3949     * @dataProvider fix_null_grades_provider
3950     */
3951    public function test_fix_null_grades($grade, $gradebookvalue) {
3952        global $DB;
3953
3954        $this->resetAfterTest();
3955
3956        $course = $this->getDataGenerator()->create_course();
3957        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
3958        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
3959
3960        $this->setUser($teacher);
3961        $assign = $this->create_instance($course);
3962
3963        // Try getting a student's grade. This will give a grade of -1.
3964        // Then we can override it with a bad negative grade.
3965        $assign->get_user_grade($student->id, true);
3966
3967        // Set the grade to something errant.
3968        // We don't set the grader here, so we expect it to be -1 as a result.
3969        $DB->set_field(
3970            'assign_grades',
3971            'grade',
3972            $grade,
3973            [
3974                'userid' => $student->id,
3975                'assignment' => $assign->get_instance()->id,
3976            ]
3977        );
3978        $assign->grade = $grade;
3979        $assigntemp = clone $assign->get_instance();
3980        $assigntemp->cmidnumber = $assign->get_course_module()->idnumber;
3981        assign_update_grades($assigntemp);
3982
3983        // Check that the gradebook was updated with the assign grade. So we can guarentee test results later on.
3984        $expectedgrade = $grade == -1 ? null : $grade; // Assign sends null to the gradebook for -1 grades.
3985        $gradegrade = grade_grade::fetch(array('userid' => $student->id, 'itemid' => $assign->get_grade_item()->id));
3986        $this->assertEquals(-1, $gradegrade->usermodified);
3987        $this->assertEquals($expectedgrade, $gradegrade->rawgrade);
3988
3989        // Call fix_null_grades().
3990        $method = new ReflectionMethod(assign::class, 'fix_null_grades');
3991        $method->setAccessible(true);
3992        $result = $method->invoke($assign);
3993
3994        $this->assertSame(true, $result);
3995
3996        $gradegrade = grade_grade::fetch(array('userid' => $student->id, 'itemid' => $assign->get_grade_item()->id));
3997
3998        $this->assertEquals(-1, $gradegrade->usermodified);
3999        $this->assertEquals($gradebookvalue, $gradegrade->finalgrade);
4000
4001        // Check that the grade was updated in the gradebook by fix_null_grades.
4002        $this->assertEquals($gradebookvalue, $gradegrade->finalgrade);
4003    }
4004
4005    /**
4006     * Test grade override displays 'Graded' for students
4007     */
4008    public function test_grade_submission_override() {
4009        global $DB, $PAGE, $OUTPUT;
4010
4011        $this->resetAfterTest();
4012
4013        $course = $this->getDataGenerator()->create_course();
4014        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
4015        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
4016
4017        $this->setUser($teacher);
4018        $assign = $this->create_instance($course, [
4019            'assignsubmission_onlinetext_enabled' => 1,
4020        ]);
4021
4022        // Simulate adding a grade.
4023        $this->setUser($teacher);
4024        $data = new stdClass();
4025        $data->grade = '50.0';
4026        $assign->testable_apply_grade_to_user($data, $student->id, 0);
4027
4028        // Set grade override.
4029        $gradegrade = grade_grade::fetch([
4030            'userid' => $student->id,
4031            'itemid' => $assign->get_grade_item()->id,
4032        ]);
4033
4034        // Check that grade submission is not overridden yet.
4035        $this->assertEquals(false, $gradegrade->is_overridden());
4036
4037        // Simulate a submission.
4038        $this->setUser($student);
4039        $submission = $assign->get_user_submission($student->id, true);
4040
4041        $PAGE->set_url(new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
4042
4043        // Set override grade grade, and check that grade submission has been overridden.
4044        $gradegrade->set_overridden(true);
4045        $this->assertEquals(true, $gradegrade->is_overridden());
4046
4047        // Check that submissionslocked message 'This assignment is not accepting submissions' does not appear for student.
4048        $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
4049        $output = $assign->get_renderer()->render($gradingtable);
4050        $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
4051
4052        $assignsubmissionstatus = $assign->get_assign_submission_status_renderable($student, true);
4053        $output2 = $assign->get_renderer()->render($assignsubmissionstatus);
4054
4055        // Check that submissionslocked 'This assignment is not accepting submissions' message does not appear for student.
4056        $this->assertStringNotContainsString(get_string('submissionslocked', 'assign'), $output2);
4057        // Check that submissionstatus_marked 'Graded' message does appear for student.
4058        $this->assertStringContainsString(get_string('submissionstatus_marked', 'assign'), $output2);
4059    }
4060
4061    /**
4062     * Test the result of get_filters is consistent.
4063     */
4064    public function test_get_filters() {
4065        $this->resetAfterTest();
4066
4067        $course = $this->getDataGenerator()->create_course();
4068        $assign = $this->create_instance($course);
4069        $valid = $assign->get_filters();
4070
4071        $this->assertEquals(count($valid), 6);
4072    }
4073
4074    /**
4075     * Test assign->get_instance() for a number of cases, as defined in the data provider.
4076     *
4077     * @dataProvider assign_get_instance_provider
4078     * @param array $courseconfig the config to use when creating the course.
4079     * @param array $assignconfig the config to use when creating the assignment.
4080     * @param array $enrolconfig the config to use when enrolling the user (this will be the active user).
4081     * @param array $expectedproperties an map containing the expected names and values for the assign instance data.
4082     */
4083    public function test_assign_get_instance(array $courseconfig, array $assignconfig, array $enrolconfig,
4084            array $expectedproperties) {
4085        $this->resetAfterTest();
4086
4087        set_config('enablecourserelativedates', true); // Enable relative dates at site level.
4088
4089        $course = $this->getDataGenerator()->create_course($courseconfig);
4090        $assign = $this->create_instance($course, $assignconfig);
4091        $user = $this->getDataGenerator()->create_and_enrol($course, ...array_values($enrolconfig));
4092
4093        $instance = $assign->get_instance($user->id);
4094
4095        foreach ($expectedproperties as $propertyname => $propertyval) {
4096            $this->assertEquals($propertyval, $instance->$propertyname);
4097        }
4098    }
4099
4100    /**
4101     * The test_assign_get_instance data provider.
4102     */
4103    public function assign_get_instance_provider() {
4104        $timenow = time();
4105
4106        // The get_default_instance() method shouldn't calculate any properties per-user. It should just return the record data.
4107        // We'll confirm this works for a few different user types anyway, just like we do for get_instance().
4108        return [
4109            'Teacher whose enrolment starts after the course start date, relative dates mode enabled' => [
4110                'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
4111                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4112                'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
4113                    'startdate' => $timenow - 8 * DAYSECS],
4114                'expectedproperties' => ['duedate' => $timenow + 6 * DAYSECS]
4115            ],
4116            'Teacher whose enrolment starts before the course start date, relative dates mode enabled' => [
4117                'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
4118                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4119                'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
4120                    'startdate' => $timenow - 12 * DAYSECS],
4121                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4122            ],
4123            'Teacher whose enrolment starts after the course start date, relative dates mode disabled' => [
4124                'courseconfig' => ['relativedatesmode' => false, 'startdate' => $timenow - 10 * DAYSECS],
4125                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4126                'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
4127                    'startdate' => $timenow - 8 * DAYSECS],
4128                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4129            ],
4130            'Student whose enrolment starts after the course start date, relative dates mode enabled' => [
4131                'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
4132                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4133                'enrolconfig' => ['shortname' => 'student', 'userparams' => null, 'method' => 'manual',
4134                    'startdate' => $timenow - 8 * DAYSECS],
4135                'expectedproperties' => ['duedate' => $timenow + 6 * DAYSECS]
4136            ],
4137            'Student whose enrolment starts before the course start date, relative dates mode enabled' => [
4138                'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
4139                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4140                'enrolconfig' => ['shortname' => 'student', 'userparams' => null, 'method' => 'manual',
4141                    'startdate' => $timenow - 12 * DAYSECS],
4142                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4143            ],
4144            'Student whose enrolment starts after the course start date, relative dates mode disabled' => [
4145                'courseconfig' => ['relativedatesmode' => false, 'startdate' => $timenow - 10 * DAYSECS],
4146                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4147                'enrolconfig' => ['shortname' => 'student', 'userparams' => null, 'method' => 'manual',
4148                    'startdate' => $timenow - 8 * DAYSECS],
4149                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4150            ],
4151        ];
4152    }
4153
4154    /**
4155     * Test assign->get_default_instance() for a number of cases, as defined in the date provider.
4156     *
4157     * @dataProvider assign_get_default_instance_provider
4158     * @param array $courseconfig the config to use when creating the course.
4159     * @param array $assignconfig the config to use when creating the assignment.
4160     * @param array $enrolconfig the config to use when enrolling the user (this will be the active user).
4161     * @param array $expectedproperties an map containing the expected names and values for the assign instance data.
4162     */
4163    public function test_assign_get_default_instance(array $courseconfig, array $assignconfig, array $enrolconfig,
4164            array $expectedproperties) {
4165        $this->resetAfterTest();
4166
4167        set_config('enablecourserelativedates', true); // Enable relative dates at site level.
4168
4169        $course = $this->getDataGenerator()->create_course($courseconfig);
4170        $assign = $this->create_instance($course, $assignconfig);
4171        $user = $this->getDataGenerator()->create_and_enrol($course, ...array_values($enrolconfig));
4172
4173        $this->setUser($user);
4174        $defaultinstance = $assign->get_default_instance();
4175
4176        foreach ($expectedproperties as $propertyname => $propertyval) {
4177            $this->assertEquals($propertyval, $defaultinstance->$propertyname);
4178        }
4179    }
4180
4181    /**
4182     * The test_assign_get_default_instance data provider.
4183     */
4184    public function assign_get_default_instance_provider() {
4185        $timenow = time();
4186
4187        // The get_default_instance() method shouldn't calculate any properties per-user. It should just return the record data.
4188        // We'll confirm this works for a few different user types anyway, just like we do for get_instance().
4189        return [
4190            'Teacher whose enrolment starts after the course start date, relative dates mode enabled' => [
4191                'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
4192                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4193                'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
4194                    'startdate' => $timenow - 8 * DAYSECS],
4195                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4196            ],
4197            'Teacher whose enrolment starts before the course start date, relative dates mode enabled' => [
4198                'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
4199                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4200                'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
4201                    'startdate' => $timenow - 12 * DAYSECS],
4202                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4203            ],
4204            'Teacher whose enrolment starts after the course start date, relative dates mode disabled' => [
4205                'courseconfig' => ['relativedatesmode' => false, 'startdate' => $timenow - 10 * DAYSECS],
4206                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4207                'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
4208                    'startdate' => $timenow - 8 * DAYSECS],
4209                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4210            ],
4211            'Student whose enrolment starts after the course start date, relative dates mode enabled' => [
4212                'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
4213                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
4214                'enrolconfig' => ['shortname' => 'student', 'userparams' => null, 'method' => 'manual',
4215                    'startdate' => $timenow - 8 * DAYSECS],
4216                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS]
4217            ],
4218        ];
4219    }
4220
4221    /**
4222     * Data provider for test_view_group_override
4223     *
4224     * @return array Provider data
4225     */
4226    public function view_group_override_provider() {
4227        return [
4228            'Other users should see their duedate' => [
4229                'student',
4230                ['group2'],
4231                'group1',
4232                '7 June 2019, 5:37 PM',
4233            ],
4234            'Teacher should be able to see all group override duedate' => [
4235                'teacher',
4236                ['group1'],
4237                'group1',
4238                '20 September 2019, 10:37 PM',
4239            ],
4240            'Teacher should be able to see all group override duedate even if they are not member' => [
4241                'editingteacher',
4242                ['group1'],
4243                'group2',
4244                '7 June 2019, 5:37 PM',
4245            ]
4246        ];
4247    }
4248
4249    /**
4250     * Test showing group override duedate for admin
4251     *
4252     * @dataProvider view_group_override_provider
4253     * @param string $role the role of the user (teacher, student, etc)
4254     * @param string[] $groups the groups the user are member of
4255     * @param string $activegroup The selected group
4256     * @param string $expecteddate The expected due date
4257     */
4258    public function test_view_group_override(string $role, array $groups, string $activegroup, string $expecteddate) {
4259        global $DB, $PAGE, $SESSION;
4260
4261        $this->resetAfterTest();
4262        $course = $this->getDataGenerator()->create_course();
4263
4264        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
4265        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
4266
4267        $user = $this->getDataGenerator()->create_and_enrol($course, $role);
4268        if (in_array('group1', $groups)) {
4269            groups_add_member($group1, $user);
4270        }
4271        if (in_array('group2', $groups)) {
4272            groups_add_member($group2, $user);
4273        }
4274        $activegroup = $$activegroup;
4275
4276        $assign = $this->create_instance($course, [
4277            'groupmode' => SEPARATEGROUPS,
4278            'duedate' => 1558999899, // 28 May 2019, 7:31 AM.
4279        ]);
4280        $instance = $assign->get_instance();
4281
4282        // Overrides for two groups.
4283        $overrides = [
4284            (object) [
4285                'assignid' => $instance->id,
4286                'groupid' => $group1->id,
4287                'userid' => null,
4288                'sortorder' => 1,
4289                'duedate' => 1568990258, // 20 September 2019, 10:37 PM.
4290            ],
4291            (object) [
4292                'assignid' => $instance->id,
4293                'groupid' => $group2->id,
4294                'userid' => null,
4295                'sortorder' => 2,
4296                'duedate' => 1559900258, // 7 June 2019, 5:37 PM.
4297            ],
4298        ];
4299
4300        foreach ($overrides as &$override) {
4301            $override->id = $DB->insert_record('assign_overrides', $override);
4302        }
4303
4304        $currenturl = new moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]);
4305        $PAGE->set_url($currenturl);
4306
4307        $this->setUser($user);
4308        $_GET['group'] = $activegroup->id;
4309
4310        /** @var assign $assign */
4311        $header = new assign_header(
4312            $assign->get_instance(),
4313            $assign->get_context(),
4314            false,
4315            $assign->get_course_module()->id
4316        );
4317        $output = $assign->get_renderer()->render($header);
4318        $this->assertStringContainsStringIgnoringCase($expecteddate, $output);
4319    }
4320
4321    /**
4322     * Test that cron task uses task API to get its last run time.
4323     */
4324    public function test_cron_use_task_api_to_get_lastruntime() {
4325        global $DB;
4326        $this->resetAfterTest();
4327        $course = $this->getDataGenerator()->create_course();
4328
4329        // Create an assignment which allows submissions from 3 days ago.
4330        $assign1 = $this->create_instance($course, [
4331            'duedate' => time() + DAYSECS,
4332            'alwaysshowdescription' => 0,
4333            'allowsubmissionsfromdate' => time() - 3 * DAYSECS,
4334            'intro' => 'This one should not be re-created',
4335        ]);
4336
4337        // Create an assignment which allows submissions from 1 day ago.
4338        $assign2 = $this->create_instance($course, [
4339            'duedate' => time() + DAYSECS,
4340            'alwaysshowdescription' => 0,
4341            'allowsubmissionsfromdate' => time() - DAYSECS,
4342            'intro' => 'This one should be re-created',
4343        ]);
4344
4345        // Set last run time 2 days ago.
4346        $DB->set_field('task_scheduled', 'lastruntime', time() - 2 * DAYSECS, ['classname' => '\mod_assign\task\cron_task']);
4347
4348        // Remove events to make sure cron will update calendar and re-create one of them.
4349        $params = array('modulename' => 'assign', 'instance' => $assign1->get_instance()->id);
4350        $DB->delete_records('event', $params);
4351        $params = array('modulename' => 'assign', 'instance' => $assign2->get_instance()->id);
4352        $DB->delete_records('event', $params);
4353
4354        // Run cron.
4355        assign::cron();
4356
4357        // Assert that calendar hasn't been updated for the first assignment as it's supposed to be
4358        // updated as part of previous cron runs (allowsubmissionsfromdate is less than lastruntime).
4359        $params = array('modulename' => 'assign', 'instance' => $assign1->get_instance()->id);
4360        $event1 = $DB->get_record('event', $params);
4361        $this->assertEmpty($event1);
4362
4363        // Assert that calendar has been updated for the second assignment
4364        // because its allowsubmissionsfromdate is greater than lastruntime.
4365        $params = array('modulename' => 'assign', 'instance' => $assign2->get_instance()->id);
4366        $event2 = $DB->get_record('event', $params);
4367        $this->assertNotEmpty($event2);
4368        $this->assertSame('This one should be re-created', $event2->description);
4369    }
4370}
4371