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 * mod_h5pactivity grader tests
19 *
20 * @package    mod_h5pactivity
21 * @category   test
22 * @copyright  2020 Ferran Recio <ferran@moodle.com>
23 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26namespace mod_h5pactivity\local;
27
28use grade_item;
29use stdClass;
30
31/**
32 * Grader tests class for mod_h5pactivity.
33 *
34 * @package    mod_h5pactivity
35 * @category   test
36 * @copyright  2020 Ferran Recio <ferran@moodle.com>
37 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38 */
39class grader_testcase extends \advanced_testcase {
40
41    /**
42     * Setup to ensure that fixtures are loaded.
43     */
44    public static function setupBeforeClass(): void {
45        global $CFG;
46        require_once($CFG->libdir.'/gradelib.php');
47    }
48
49    /**
50     * Test for grade item delete.
51     */
52    public function test_grade_item_delete() {
53
54        $this->resetAfterTest();
55        $this->setAdminUser();
56
57        $course = $this->getDataGenerator()->create_course();
58        $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
59        $user = $this->getDataGenerator()->create_and_enrol($course, 'student');
60
61        $grader = new grader($activity);
62
63        // Force a user grade.
64        $this->generate_fake_attempt($activity, $user, 5, 10);
65        $grader->update_grades($user->id);
66
67        $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
68        $this->assertNotEquals(0, count($gradeinfo->items));
69        $this->assertArrayHasKey($user->id, $gradeinfo->items[0]->grades);
70
71        $grader->grade_item_delete();
72
73        $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
74        $this->assertEquals(0, count($gradeinfo->items));
75    }
76
77    /**
78     * Test for grade item update.
79     *
80     * @dataProvider grade_item_update_data
81     * @param int $newgrade new activity grade
82     * @param bool $reset if has to reset grades
83     * @param string $idnumber the new idnumber
84     */
85    public function test_grade_item_update(int $newgrade, bool $reset, string $idnumber) {
86
87        $this->resetAfterTest();
88        $this->setAdminUser();
89
90        $course = $this->getDataGenerator()->create_course();
91        $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
92        $user = $this->getDataGenerator()->create_and_enrol($course, 'student');
93
94        // Force a user initial grade.
95        $grader = new grader($activity);
96        $this->generate_fake_attempt($activity, $user, 5, 10);
97        $grader->update_grades($user->id);
98
99        $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
100        $this->assertNotEquals(0, count($gradeinfo->items));
101        $item = array_shift($gradeinfo->items);
102        $this->assertArrayHasKey($user->id, $item->grades);
103        $this->assertEquals(50, round($item->grades[$user->id]->grade));
104
105        // Module grade value determine the way gradebook acts. That means that the expected
106        // result depends on this value.
107        // - Grade > 0: regular max grade value.
108        // - Grade = 0: no grading is used (but grademax remains the same).
109        // - Grade < 0: a scaleid is used (value = -scaleid).
110        if ($newgrade > 0) {
111            $grademax = $newgrade;
112            $scaleid = null;
113            $usergrade = ($newgrade > 50) ? 50 : $newgrade;
114        } else if ($newgrade == 0) {
115            $grademax = 100;
116            $scaleid = null;
117            $usergrade = null; // No user grades expected.
118        } else if ($newgrade < 0) {
119            $scale = $this->getDataGenerator()->create_scale(array("scale" => "value1, value2, value3"));
120            $newgrade = -1 * $scale->id;
121            $grademax = 3;
122            $scaleid = $scale->id;
123            $usergrade = 3; // 50 value will ve converted to "value 3" on scale.
124        }
125
126        // Update grade item.
127        $activity->grade = $newgrade;
128
129        // In case a reset is need, usergrade will be empty.
130        if ($reset) {
131            $param = 'reset';
132            $usergrade = null;
133        } else {
134            // Individual user gradings will be tested as a subcall of update_grades.
135            $param = null;
136        }
137
138        $grader = new grader($activity, $idnumber);
139        $grader->grade_item_update($param);
140
141        // Check new grade item and grades.
142        $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
143        $item = array_shift($gradeinfo->items);
144        $this->assertEquals($scaleid, $item->scaleid);
145        $this->assertEquals($grademax, $item->grademax);
146        $this->assertArrayHasKey($user->id, $item->grades);
147        if ($usergrade) {
148            $this->assertEquals($usergrade, round($item->grades[$user->id]->grade));
149        } else {
150            $this->assertEmpty($item->grades[$user->id]->grade);
151        }
152        if (!empty($idnumber)) {
153            $gradeitem = grade_item::fetch(['idnumber' => $idnumber, 'courseid' => $course->id]);
154            $this->assertInstanceOf('grade_item', $gradeitem);
155        }
156    }
157
158    /**
159     * Data provider for test_grade_item_update.
160     *
161     * @return array
162     */
163    public function grade_item_update_data(): array {
164        return [
165            'Change idnumber' => [
166                100, false, 'newidnumber'
167            ],
168            'Increase max grade to 110' => [
169                110, false, ''
170            ],
171            'Decrease max grade to 80' => [
172                40, false, ''
173            ],
174            'Decrease max grade to 40 (less than actual grades)' => [
175                40, false, ''
176            ],
177            'Reset grades' => [
178                100, true, ''
179            ],
180            'Disable grades' => [
181                0, false, ''
182            ],
183            'Use scales' => [
184                -1, false, ''
185            ],
186            'Use scales with reset' => [
187                -1, true, ''
188            ],
189        ];
190    }
191
192    /**
193     * Test for grade update.
194     *
195     * @dataProvider update_grades_data
196     * @param int $newgrade the new activity grade
197     * @param bool $all if has to be applied to all students or just to one
198     * @param int $completion 1 all student have the activity completed, 0 one have incompleted
199     * @param array $results expected results (user1 grade, user2 grade)
200     */
201    public function test_update_grades(int $newgrade, bool $all, int $completion, array $results) {
202
203        $this->resetAfterTest();
204        $this->setAdminUser();
205
206        $course = $this->getDataGenerator()->create_course();
207        $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
208        $user1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
209        $user2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
210
211        // Force a user initial grade.
212        $grader = new grader($activity);
213        $this->generate_fake_attempt($activity, $user1, 5, 10);
214        $this->generate_fake_attempt($activity, $user2, 3, 12, $completion);
215        $grader->update_grades();
216
217        $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, [$user1->id, $user2->id]);
218        $this->assertNotEquals(0, count($gradeinfo->items));
219        $item = array_shift($gradeinfo->items);
220        $this->assertArrayHasKey($user1->id, $item->grades);
221        $this->assertArrayHasKey($user2->id, $item->grades);
222        $this->assertEquals(50, $item->grades[$user1->id]->grade);
223        // Uncompleted attempts does not generate grades.
224        if ($completion) {
225            $this->assertEquals(25, $item->grades[$user2->id]->grade);
226        } else {
227            $this->assertNull($item->grades[$user2->id]->grade);
228
229        }
230
231        // Module grade value determine the way gradebook acts. That means that the expected
232        // result depends on this value.
233        // - Grade > 0: regular max grade value.
234        // - Grade <= 0: no grade calculation is used (scale and no grading).
235        if ($newgrade < 0) {
236            $scale = $this->getDataGenerator()->create_scale(array("scale" => "value1, value2, value3"));
237            $activity->grade = -1 * $scale->id;
238        } else {
239            $activity->grade = $newgrade;
240        }
241
242        $userid = ($all) ? 0 : $user1->id;
243
244        $grader = new grader($activity);
245        $grader->update_grades($userid);
246
247        // Check new grade item and grades.
248        $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, [$user1->id, $user2->id]);
249        $item = array_shift($gradeinfo->items);
250        $this->assertArrayHasKey($user1->id, $item->grades);
251        $this->assertArrayHasKey($user2->id, $item->grades);
252        $this->assertEquals($results[0], $item->grades[$user1->id]->grade);
253        $this->assertEquals($results[1], $item->grades[$user2->id]->grade);
254    }
255
256    /**
257     * Data provider for test_grade_item_update.
258     *
259     * @return array
260     */
261    public function update_grades_data(): array {
262        return [
263            // Quantitative grade, all attempts completed.
264            'Same grademax, all users, all completed' => [
265                100, true, 1, [50, 25]
266            ],
267            'Same grademax, one user, all completed' => [
268                100, false, 1, [50, 25]
269            ],
270            'Increade max, all users, all completed' => [
271                200, true, 1, [100, 50]
272            ],
273            'Increade max, one user, all completed' => [
274                200, false, 1, [100, 25]
275            ],
276            'Decrease max, all users, all completed' => [
277                50, true, 1, [25, 12.5]
278            ],
279            'Decrease max, one user, all completed' => [
280                50, false, 1, [25, 25]
281            ],
282            // Quantitative grade, some attempts not completed.
283            'Same grademax, all users, not completed' => [
284                100, true, 0, [50, null]
285            ],
286            'Same grademax, one user, not completed' => [
287                100, false, 0, [50, null]
288            ],
289            'Increade max, all users, not completed' => [
290                200, true, 0, [100, null]
291            ],
292            'Increade max, one user, not completed' => [
293                200, false, 0, [100, null]
294            ],
295            'Decrease max, all users, not completed' => [
296                50, true, 0, [25, null]
297            ],
298            'Decrease max, one user, not completed' => [
299                50, false, 0, [25, null]
300            ],
301            // No grade (no grade will be used).
302            'No grade, all users, all completed' => [
303                0, true, 1, [null, null]
304            ],
305            'No grade, one user, all completed' => [
306                0, false, 1, [null, null]
307            ],
308            'No grade, all users, not completed' => [
309                0, true, 0, [null, null]
310            ],
311            'No grade, one user, not completed' => [
312                0, false, 0, [null, null]
313            ],
314            // Scale (grate item will updated but without regrading).
315            'Scale, all users, all completed' => [
316                -1, true, 1, [3, 3]
317            ],
318            'Scale, one user, all completed' => [
319                -1, false, 1, [3, 3]
320            ],
321            'Scale, all users, not completed' => [
322                -1, true, 0, [3, null]
323            ],
324            'Scale, one user, not completed' => [
325                -1, false, 0, [3, null]
326            ],
327        ];
328    }
329
330    /**
331     * Create a fake attempt for a specific user.
332     *
333     * @param stdClass $activity activity instance record.
334     * @param stdClass $user user record
335     * @param int $rawscore score obtained
336     * @param int $maxscore attempt max score
337     * @param int $completion 1 for activity completed, 0 for not completed yet
338     * @return stdClass the attempt record
339     */
340    private function generate_fake_attempt(stdClass $activity, stdClass $user,
341            int $rawscore, int $maxscore, int $completion = 1): stdClass {
342        global $DB;
343
344        $attempt = (object)[
345            'h5pactivityid' => $activity->id,
346            'userid' => $user->id,
347            'timecreated' => 10,
348            'timemodified' => 20,
349            'attempt' => 1,
350            'rawscore' => $rawscore,
351            'maxscore' => $maxscore,
352            'duration' => 2,
353            'completion' => $completion,
354            'success' => 0,
355        ];
356        $attempt->scaled = $attempt->rawscore / $attempt->maxscore;
357        $attempt->id = $DB->insert_record('h5pactivity_attempts', $attempt);
358        return $attempt;
359    }
360}
361