MDL-66722 core_grades: Add simple direct grading to gradingpanel
[moodle.git] / grade / tests / grades_grader_gradingpanel_point_external_store_test.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Unit tests for core_grades\component_gradeitems;
19  *
20  * @package   core_grades
21  * @category  test
22  * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
23  * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
24  */
26 declare(strict_types = 1);
28 namespace core_grades\grades\grader\gradingpanel\point\external;
30 use advanced_testcase;
31 use coding_exception;
32 use core_grades\component_gradeitem;
33 use external_api;
34 use mod_forum\local\entities\forum as forum_entity;
35 use moodle_exception;
36 use grade_grade;
37 use grade_item;
39 /**
40  * Unit tests for core_grades\component_gradeitems;
41  *
42  * @package   core_grades
43  * @category  test
44  * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
45  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46  */
47 class store_test extends advanced_testcase {
49     public static function setupBeforeClass(): void {
50         global $CFG;
51         require_once("{$CFG->libdir}/externallib.php");
52     }
54     /**
55      * Ensure that an execute with an invalid component is rejected.
56      */
57     public function test_execute_invalid_component(): void {
58         $this->resetAfterTest();
59         $user = $this->getDataGenerator()->create_user();
60         $this->setUser($user);
62         $this->expectException(coding_exception::class);
63         $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_invalid' component");
64         store::execute('mod_invalid', 1, 'foo', 2, 'formdata');
65     }
67     /**
68      * Ensure that an execute with an invalid itemname on a valid component is rejected.
69      */
70     public function test_execute_invalid_itemname(): void {
71         $this->resetAfterTest();
72         $user = $this->getDataGenerator()->create_user();
73         $this->setUser($user);
75         $this->expectException(coding_exception::class);
76         $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_forum' component");
77         store::execute('mod_forum', 1, 'foo', 2, 'formdata');
78     }
80     /**
81      * Ensure that an execute against a different grading method is rejected.
82      */
83     public function test_execute_incorrect_type(): void {
84         $this->resetAfterTest();
86         $forum = $this->get_forum_instance([
87             // Negative numbers mean a scale.
88             'grade_forum' => -1,
89         ]);
90         $course = $forum->get_course_record();
91         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
92         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
93         $this->setUser($teacher);
95         $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
97         $this->expectException(moodle_exception::class);
98         $this->expectExceptionMessage("not configured for direct grading");
99         store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, 'formdata');
100     }
102     /**
103      * Ensure that an execute against a different grading method is rejected.
104      */
105     public function test_execute_disabled(): void {
106         $this->resetAfterTest();
108         $forum = $this->get_forum_instance();
109         $course = $forum->get_course_record();
110         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
111         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
112         $this->setUser($teacher);
114         $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
116         $this->expectException(moodle_exception::class);
117         $this->expectExceptionMessage("Grading is not enabled");
118         store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, 'formdata');
119     }
121     /**
122      * Ensure that an execute against the correct grading method returns the current state of the user.
123      */
124     public function test_execute_store_empty(): void {
125         $this->resetAfterTest();
127         $forum = $this->get_forum_instance([
128             // Negative numbers mean a scale.
129             'grade_forum' => 5,
130         ]);
131         $course = $forum->get_course_record();
132         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
133         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
134         $this->setUser($teacher);
136         $formdata = [
137             'grade' => null,
138         ];
140         $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
142         $result = store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, http_build_query($formdata));
143         $result = external_api::clean_returnvalue(store::execute_returns(), $result);
145         // The result should still be empty.
146         $this->assertIsArray($result);
147         $this->assertArrayHasKey('templatename', $result);
149         $this->assertEquals('core_grades/grades/grader/gradingpanel/point', $result['templatename']);
151         $this->assertArrayHasKey('grade', $result);
152         $this->assertIsArray($result['grade']);
153         $this->assertArrayHasKey('grade', $result['grade']);
154         $this->assertEmpty($result['grade']['grade']);
155         $this->assertArrayHasKey('timecreated', $result['grade']);
156         $this->assertIsInt($result['grade']['timecreated']);
157         $this->assertArrayHasKey('timemodified', $result['grade']);
158         $this->assertIsInt($result['grade']['timemodified']);
160         $this->assertArrayHasKey('warnings', $result);
161         $this->assertIsArray($result['warnings']);
162         $this->assertEmpty($result['warnings']);
164         // Compare against the grade stored in the database.
165         $storedgradeitem = grade_item::fetch([
166             'courseid' => $forum->get_course_id(),
167             'itemtype' => 'mod',
168             'itemmodule' => 'forum',
169             'iteminstance' => $forum->get_id(),
170             'itemnumber' => $gradeitem->get_grade_itemid(),
171         ]);
172         $storedgrade = grade_grade::fetch([
173             'userid' => $student->id,
174             'itemid' => $storedgradeitem->id,
175         ]);
177         $this->assertEmpty($storedgrade->rawgrade);
178     }
180     /**
181      * Ensure that an execute against the correct grading method returns the current state of the user.
182      */
183     public function test_execute_store_graded(): void {
184         $this->resetAfterTest();
186         $forum = $this->get_forum_instance([
187             // Negative numbers mean a scale.
188             'grade_forum' => 5,
189         ]);
190         $course = $forum->get_course_record();
191         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
192         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
193         $this->setUser($teacher);
195         $formdata = [
196             'grade' => 4,
197         ];
198         $formattedvalue = grade_floatval(unformat_float(4));
200         $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
202         $result = store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, http_build_query($formdata));
203         $result = external_api::clean_returnvalue(store::execute_returns(), $result);
205         // The result should still be empty.
206         $this->assertIsArray($result);
207         $this->assertArrayHasKey('templatename', $result);
209         $this->assertEquals('core_grades/grades/grader/gradingpanel/point', $result['templatename']);
211         $this->assertArrayHasKey('grade', $result);
212         $this->assertIsArray($result['grade']);
213         $this->assertArrayHasKey('grade', $result['grade']);
214         $this->assertEquals($formattedvalue, $result['grade']['grade']);
215         $this->assertArrayHasKey('timecreated', $result['grade']);
216         $this->assertIsInt($result['grade']['timecreated']);
217         $this->assertArrayHasKey('timemodified', $result['grade']);
218         $this->assertIsInt($result['grade']['timemodified']);
220         $this->assertArrayHasKey('warnings', $result);
221         $this->assertIsArray($result['warnings']);
222         $this->assertEmpty($result['warnings']);
224         // Compare against the grade stored in the database.
225         $storedgradeitem = grade_item::fetch([
226             'courseid' => $forum->get_course_id(),
227             'itemtype' => 'mod',
228             'itemmodule' => 'forum',
229             'iteminstance' => $forum->get_id(),
230             'itemnumber' => $gradeitem->get_grade_itemid(),
231         ]);
232         $storedgrade = grade_grade::fetch([
233             'userid' => $student->id,
234             'itemid' => $storedgradeitem->id,
235         ]);
237         $this->assertEquals($formattedvalue, $storedgrade->rawgrade);
238     }
240     /**
241      * Ensure that an out-of-range value is rejected.
242      *
243      * @dataProvider execute_out_of_range_provider
244      * @param int $maxvalue The max value of the forum
245      * @param int $suppliedvalue The value that was submitted
246      */
247     public function test_execute_store_out_of__range(int $maxvalue, int $suppliedvalue): void {
248         $this->resetAfterTest();
250         $forum = $this->get_forum_instance([
251             // Negative numbers mean a scale.
252             'grade_forum' => $maxvalue,
253         ]);
254         $course = $forum->get_course_record();
255         $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
256         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
257         $this->setUser($teacher);
259         $formdata = [
260             'grade' => $suppliedvalue,
261         ];
263         $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
265         $this->expectException(moodle_exception::class);
266         $this->expectExceptionMessage("Invalid grade '{$suppliedvalue}' provided. Grades must be between 0 and {$maxvalue}.");
267         store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, http_build_query($formdata));
268     }
270     /**
271      * Data provider for out of range tests.
272      *
273      * @return array
274      */
275     public function execute_out_of_range_provider(): array {
276         return [
277             'above' => [
278                 'max' => 100,
279                 'supplied' => 101,
280             ],
281             'above just' => [
282                 'max' => 100,
283                 'supplied' => 101.001,
284             ],
285             'below' => [
286                 'max' => 100,
287                 'supplied' => -100,
288             ],
289             '-1' => [
290                 'max' => 100,
291                 'supplied' => -1,
292             ],
293         ];
294     }
297     /**
298      * Get a forum instance.
299      *
300      * @param array $config
301      * @return forum_entity
302      */
303     protected function get_forum_instance(array $config = []): forum_entity {
304         $this->resetAfterTest();
306         $datagenerator = $this->getDataGenerator();
307         $course = $datagenerator->create_course();
308         $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id]));
310         $vaultfactory = \mod_forum\local\container::get_vault_factory();
311         $vault = $vaultfactory->get_forum_vault();
313         return $vault->get_from_id((int) $forum->id);
314     }