Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
namespace core;
18
 
19
defined('MOODLE_INTERNAL') || die();
20
 
21
require_once(__DIR__.'/fixtures/lib.php');
22
 
23
 
24
/**
25
 * Test grade categories
26
 *
27
 * @package    core
28
 * @category   test
29
 * @copyright  nicolas@moodle.com
30
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31
 */
32
class grade_category_test extends \grade_base_testcase {
33
 
11 efrain 34
    public function test_grade_category(): void {
1 efrain 35
        $this->sub_test_grade_category_construct();
36
        $this->sub_test_grade_category_build_path();
37
        $this->sub_test_grade_category_fetch();
38
        $this->sub_test_grade_category_fetch_all();
39
        $this->sub_test_grade_category_update();
40
        $this->sub_test_grade_category_delete();
41
        $this->sub_test_grade_category_insert();
42
        $this->sub_test_grade_category_qualifies_for_regrading();
43
        $this->sub_test_grade_category_force_regrading();
44
        $this->sub_test_grade_category_aggregate_grades();
45
        $this->sub_test_grade_category_apply_limit_rules();
46
        $this->sub_test_grade_category_is_aggregationcoef_used();
47
        $this->sub_test_grade_category_aggregation_uses_aggregationcoef();
48
        $this->sub_test_grade_category_fetch_course_tree();
49
        $this->sub_test_grade_category_get_children();
50
        $this->sub_test_grade_category_load_grade_item();
51
        $this->sub_test_grade_category_get_grade_item();
52
        $this->sub_test_grade_category_load_parent_category();
53
        $this->sub_test_grade_category_get_parent_category();
54
        $this->sub_test_grade_category_get_name_escaped();
55
        $this->sub_test_grade_category_get_name_unescaped();
56
        $this->sub_test_grade_category_generate_grades_aggregationweight();
57
        $this->sub_test_grade_category_set_parent();
58
        $this->sub_test_grade_category_get_final();
59
        $this->sub_test_grade_category_get_sortorder();
60
        $this->sub_test_grade_category_set_sortorder();
61
        $this->sub_test_grade_category_is_editable();
62
        $this->sub_test_grade_category_move_after_sortorder();
63
        $this->sub_test_grade_category_is_course_category();
64
        $this->sub_test_grade_category_fetch_course_category();
65
        $this->sub_test_grade_category_is_locked();
66
        $this->sub_test_grade_category_set_locked();
67
        $this->sub_test_grade_category_is_hidden();
68
        $this->sub_test_grade_category_set_hidden();
69
        $this->sub_test_grade_category_can_control_visibility();
70
        $this->sub_test_grade_category_total_visibility();
71
 
72
        // This won't work until MDL-11837 is complete.
73
        // $this->sub_test_grade_category_generate_grades();
74
 
75
        // Do this last as adding a second course category messes up the data.
76
        $this->sub_test_grade_category_insert_course_category();
77
        $this->sub_test_grade_category_is_extracredit_used();
78
        $this->sub_test_grade_category_aggregation_uses_extracredit();
79
    }
80
 
81
    // Adds 3 new grade categories at various depths.
82
    protected function sub_test_grade_category_construct() {
83
        $course_category = \grade_category::fetch_course_category($this->courseid);
84
 
85
        $params = new \stdClass();
86
 
87
        $params->courseid = $this->courseid;
88
        $params->fullname = 'unittestcategory4';
89
 
90
        $grade_category = new \grade_category($params, false);
91
        $grade_category->insert();
92
        $this->grade_categories[] = $grade_category;
93
 
94
        $this->assertEquals($params->courseid, $grade_category->courseid);
95
        $this->assertEquals($params->fullname, $grade_category->fullname);
96
        $this->assertEquals(2, $grade_category->depth);
97
        $this->assertEquals("/$course_category->id/$grade_category->id/", $grade_category->path);
98
        $parentpath = $grade_category->path;
99
 
100
        // Test a child category.
101
        $params->parent = $grade_category->id;
102
        $params->fullname = 'unittestcategory5';
103
        $grade_category = new \grade_category($params, false);
104
        $grade_category->insert();
105
        $this->grade_categories[] = $grade_category;
106
 
107
        $this->assertEquals(3, $grade_category->depth);
108
        $this->assertEquals($parentpath.$grade_category->id."/", $grade_category->path);
109
        $parentpath = $grade_category->path;
110
 
111
        // Test a third depth category.
112
        $params->parent = $grade_category->id;
113
        $params->fullname = 'unittestcategory6';
114
        $grade_category = new \grade_category($params, false);
115
        $grade_category->insert();
116
        $this->grade_categories[50] = $grade_category;// Going to delete this one later hence the special index.
117
 
118
        $this->assertEquals(4, $grade_category->depth);
119
        $this->assertEquals($parentpath.$grade_category->id."/", $grade_category->path);
120
    }
121
 
122
    protected function sub_test_grade_category_build_path() {
123
        $grade_category = new \grade_category($this->grade_categories[1]);
124
        $this->assertTrue(method_exists($grade_category, 'build_path'));
125
        $path = \grade_category::build_path($grade_category);
126
        $this->assertEquals($grade_category->path, $path);
127
    }
128
 
129
    protected function sub_test_grade_category_fetch() {
130
        $grade_category = new \grade_category();
131
        $this->assertTrue(method_exists($grade_category, 'fetch'));
132
 
133
        $grade_category = \grade_category::fetch(array('id'=>$this->grade_categories[0]->id));
134
        $this->assertEquals($this->grade_categories[0]->id, $grade_category->id);
135
        $this->assertEquals($this->grade_categories[0]->fullname, $grade_category->fullname);
136
    }
137
 
138
    protected function sub_test_grade_category_fetch_all() {
139
        $grade_category = new \grade_category();
140
        $this->assertTrue(method_exists($grade_category, 'fetch_all'));
141
 
142
        $grade_categories = \grade_category::fetch_all(array('courseid'=>$this->courseid));
143
        $this->assertEquals(count($this->grade_categories), count($grade_categories)-1);
144
    }
145
 
146
    protected function sub_test_grade_category_update() {
147
        global $DB;
148
        $grade_category = new \grade_category($this->grade_categories[0]);
149
        $this->assertTrue(method_exists($grade_category, 'update'));
150
 
151
        $grade_category->fullname = 'Updated info for this unittest grade_category';
152
        $grade_category->path = null; // Path must be recalculated if missing.
153
        $grade_category->depth = null;
154
        $grade_category->aggregation = GRADE_AGGREGATE_MAX; // Should force regrading.
155
 
156
        $grade_item = $grade_category->get_grade_item();
157
        $this->assertEquals(0, $grade_item->needsupdate);
158
 
159
        $this->assertTrue($grade_category->update());
160
 
161
        $fullname = $DB->get_field('grade_categories', 'fullname', array('id' => $this->grade_categories[0]->id));
162
        $this->assertEquals($grade_category->fullname, $fullname);
163
 
164
        $path = $DB->get_field('grade_categories', 'path', array('id' => $this->grade_categories[0]->id));
165
        $this->assertEquals($grade_category->path, $path);
166
 
167
        $depth = $DB->get_field('grade_categories', 'depth', array('id' => $this->grade_categories[0]->id));
168
        $this->assertEquals($grade_category->depth, $depth);
169
 
170
        $grade_item = $grade_category->get_grade_item();
171
        $this->assertEquals(1, $grade_item->needsupdate);
172
    }
173
 
174
    protected function sub_test_grade_category_delete() {
175
        global $DB;
176
 
177
        $grade_category = new \grade_category($this->grade_categories[50]);
178
        $this->assertTrue(method_exists($grade_category, 'delete'));
179
 
180
        $this->assertTrue($grade_category->delete());
181
        $this->assertFalse($DB->get_record('grade_categories', array('id' => $grade_category->id)));
182
    }
183
 
184
    protected function sub_test_grade_category_insert() {
185
        $course_category = \grade_category::fetch_course_category($this->courseid);
186
 
187
        $grade_category = new \grade_category();
188
        $this->assertTrue(method_exists($grade_category, 'insert'));
189
 
190
        $grade_category->fullname    = 'unittestcategory4';
191
        $grade_category->courseid    = $this->courseid;
192
        $grade_category->aggregation = GRADE_AGGREGATE_MEAN;
193
        $grade_category->aggregateonlygraded = 1;
194
        $grade_category->keephigh    = 100;
195
        $grade_category->droplow     = 10;
196
        $grade_category->hidden      = 0;
197
        $grade_category->parent      = $this->grade_categories[1]->id; // sub_test_grade_category_delete() removed the category at 0.
198
 
199
        $grade_category->insert();
200
 
201
        $this->assertEquals('/'.$course_category->id.'/'.$this->grade_categories[1]->parent.'/'.$this->grade_categories[1]->id.'/'.$grade_category->id.'/', $grade_category->path);
202
        $this->assertEquals(4, $grade_category->depth);
203
 
204
        $last_grade_category = end($this->grade_categories);
205
 
206
        $this->assertFalse(empty($grade_category->grade_item));
207
        $this->assertEquals($grade_category->id, $grade_category->grade_item->iteminstance);
208
        $this->assertEquals('category', $grade_category->grade_item->itemtype);
209
 
210
        $this->assertEquals($grade_category->id, $last_grade_category->id + 1);
211
        $this->assertFalse(empty($grade_category->timecreated));
212
        $this->assertFalse(empty($grade_category->timemodified));
213
    }
214
 
215
    protected function sub_test_grade_category_qualifies_for_regrading() {
216
        $grade_category = new \grade_category($this->grade_categories[1]);
217
        $this->assertTrue(method_exists($grade_category, 'qualifies_for_regrading'));
218
        $this->assertFalse($grade_category->qualifies_for_regrading());
219
 
220
        $grade_category->aggregation = GRADE_AGGREGATE_MAX;
221
        $this->assertTrue($grade_category->qualifies_for_regrading());
222
 
223
        $grade_category = new \grade_category($this->grade_categories[1]);
224
        $grade_category->droplow = 99;
225
        $this->assertTrue($grade_category->qualifies_for_regrading());
226
 
227
        $grade_category = new \grade_category($this->grade_categories[1]);
228
        $grade_category->keephigh = 99;
229
        $this->assertTrue($grade_category->qualifies_for_regrading());
230
    }
231
 
232
    protected function sub_test_grade_category_force_regrading() {
233
        $grade_category = new \grade_category($this->grade_categories[1]);
234
        $this->assertTrue(method_exists($grade_category, 'force_regrading'));
235
 
236
        $grade_category->load_grade_item();
237
        $this->assertEquals(0, $grade_category->grade_item->needsupdate);
238
 
239
        $grade_category->force_regrading();
240
 
241
        $grade_category->grade_item = null;
242
        $grade_category->load_grade_item();
243
 
244
        $this->assertEquals(1, $grade_category->grade_item->needsupdate);
245
    }
246
 
247
    /**
248
     * Tests the setting of the grade_grades aggregationweight column.
249
     * Currently, this is only a regression test for MDL-51715.
250
     * This must be run before sub_test_grade_category_set_parent(), which alters
251
     * the fixture.
252
     */
253
    protected function sub_test_grade_category_generate_grades_aggregationweight() {
254
        global $DB;
255
 
256
        // Start of regression test for MDL-51715.
257
        // grade_categories [1] and [2] are child categories of [0]
258
        // Ensure that grades have been generated with fixture data.
259
        $childcat1 = new \grade_category($this->grade_categories[1]);
260
        $childcat1itemid = $childcat1->load_grade_item()->id;
261
        $childcat1->generate_grades();
262
        $childcat2 = new \grade_category($this->grade_categories[2]);
263
        $childcat2itemid = $childcat2->load_grade_item()->id;
264
        $childcat2->generate_grades();
265
        $parentcat = new \grade_category($this->grade_categories[0]);
266
        $parentcat->generate_grades();
267
 
268
        // Drop low and and re-generate to produce 'dropped' aggregation status.
269
        $parentcat->droplow = 1;
270
        $parentcat->generate_grades();
271
 
272
        $this->assertTrue($DB->record_exists_select(
273
                                     'grade_grades',
274
                                     "aggregationstatus='dropped' and itemid in (?,?)",
275
                                     array($childcat1itemid, $childcat2itemid)));
276
        $this->assertFalse($DB->record_exists_select(
277
                                     'grade_grades',
278
                                     "aggregationstatus='dropped' and aggregationweight > 0.00"),
279
                           "aggregationweight should be 0.00 if aggregationstatus=='dropped'");
280
 
281
        // Reset grade data to be consistent with fixture data.
282
        $parentcat->droplow = 0;
283
        $parentcat->generate_grades();
284
 
285
        // Blank out the final grade for one of the child categories and re-generate
286
        // to produce 'novalue' aggregationstatus.  Direct DB update is testing shortcut.
287
        $DB->set_field('grade_grades', 'finalgrade', null, array('itemid'=>$childcat1itemid));
288
        $parentcat->generate_grades();
289
 
290
        $this->assertFalse($DB->record_exists_select(
291
                                     'grade_grades',
292
                                     "aggregationstatus='dropped' and itemid in (?,?)",
293
                                     array($childcat1itemid, $childcat2itemid)));
294
        $this->assertTrue($DB->record_exists_select(
295
                                     'grade_grades',
296
                                     "aggregationstatus='novalue' and itemid = ?",
297
                                     array($childcat1itemid)));
298
        $this->assertFalse($DB->record_exists_select(
299
                                     'grade_grades',
300
                                     "aggregationstatus='novalue' and aggregationweight > 0.00"),
301
                           "aggregationweight should be 0.00 if aggregationstatus=='novalue'");
302
 
303
        // Re-generate to be consistent with fixture data.
304
        $childcat1->generate_grades();
305
        $parentcat->generate_grades();
306
        // End of regression test for MDL-51715.
307
    }
308
 
309
    /**
310
     * Tests the calculation of grades using the various aggregation methods with and without hidden grades
311
     * This will not work entirely until MDL-11837 is done
312
     */
313
    protected function sub_test_grade_category_generate_grades() {
314
        global $DB;
315
 
316
        // Inserting some special grade items to make testing the final grade calculation easier.
317
        $params = new \stdClass();
318
        $params->courseid = $this->courseid;
319
        $params->fullname = 'unittestgradecalccategory';
320
        $params->aggregation = GRADE_AGGREGATE_MEAN;
321
        $params->aggregateonlygraded = 0;
322
        $grade_category = new \grade_category($params, false);
323
        $grade_category->insert();
324
 
325
        $this->assertTrue(method_exists($grade_category, 'generate_grades'));
326
 
327
        $grade_category->load_grade_item();
328
        $cgi = $grade_category->get_grade_item();
329
        $cgi->grademin = 0;
330
        $cgi->grademax = 20; // 3 grade items out of 10 but category is out of 20 to force scaling to occur.
331
        $cgi->update();
332
 
333
        // 3 grade items each with a maximum grade of 10.
334
        $grade_items = array();
335
        for ($i=0; $i<3; $i++) {
336
            $grade_items[$i] = new \grade_item();
337
            $grade_items[$i]->courseid = $this->courseid;
338
            $grade_items[$i]->categoryid = $grade_category->id;
339
            $grade_items[$i]->itemname = 'manual grade_item '.$i;
340
            $grade_items[$i]->itemtype = 'manual';
341
            $grade_items[$i]->itemnumber = 0;
342
            $grade_items[$i]->needsupdate = false;
343
            $grade_items[$i]->gradetype = GRADE_TYPE_VALUE;
344
            $grade_items[$i]->grademin = 0;
345
            $grade_items[$i]->grademax = 10;
346
            $grade_items[$i]->iteminfo = 'Manual grade item used for unit testing';
347
            $grade_items[$i]->timecreated = time();
348
            $grade_items[$i]->timemodified = time();
349
 
350
            // Used as the weight by weighted mean and as extra credit by mean with extra credit.
351
            // Will be 0, 1 and 2.
352
            $grade_items[$i]->aggregationcoef = $i;
353
 
354
            $grade_items[$i]->insert();
355
        }
356
 
357
        // A grade for each grade item.
358
        $grade_grades = array();
359
        for ($i=0; $i<3; $i++) {
360
            $grade_grades[$i] = new \grade_grade();
361
            $grade_grades[$i]->itemid = $grade_items[$i]->id;
362
            $grade_grades[$i]->userid = $this->userid;
363
            $grade_grades[$i]->rawgrade = ($i+1)*2; // Produce grade grades of 2, 4 and 6.
364
            $grade_grades[$i]->finalgrade = ($i+1)*2;
365
            $grade_grades[$i]->timecreated = time();
366
            $grade_grades[$i]->timemodified = time();
367
            $grade_grades[$i]->information = '1 of 2 grade_grades';
368
            $grade_grades[$i]->informationformat = FORMAT_PLAIN;
369
            $grade_grades[$i]->feedback = 'Good, but not good enough..';
370
            $grade_grades[$i]->feedbackformat = FORMAT_PLAIN;
371
 
372
            $grade_grades[$i]->insert();
373
        }
374
 
375
        // 3 grade items with 1 grade_grade each.
376
        // grade grades have the values 2, 4 and 6.
377
 
378
        // First correct answer is the aggregate with all 3 grades.
379
        // Second correct answer is with the first grade (value 2) hidden.
380
 
381
        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MEDIAN, 'GRADE_AGGREGATE_MEDIAN', 8, 8);
382
        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MAX, 'GRADE_AGGREGATE_MAX', 12, 12);
383
        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MODE, 'GRADE_AGGREGATE_MODE', 12, 12);
384
 
385
        // Weighted mean. note grade totals are rounded to an int to prevent rounding discrepancies. correct final grade isnt actually exactly 10
386
        // 3 items with grades 2, 4 and 6 with weights 0, 1 and 2 and all out of 10. then doubled to be out of 20.
387
        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_WEIGHTED_MEAN, 'GRADE_AGGREGATE_WEIGHTED_MEAN', 10, 10);
388
 
389
        // Simple weighted mean.
390
        // 3 items with grades 2, 4 and 6 equally weighted and all out of 10. then doubled to be out of 20.
391
        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_WEIGHTED_MEAN2, 'GRADE_AGGREGATE_WEIGHTED_MEAN2', 8, 10);
392
 
393
        // Mean of grades with extra credit.
394
        // 3 items with grades 2, 4 and 6 with extra credit 0, 1 and 2 equally weighted and all out of 10. then doubled to be out of 20.
395
        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_EXTRACREDIT_MEAN, 'GRADE_AGGREGATE_EXTRACREDIT_MEAN', 10, 13);
396
 
397
        // Aggregation tests the are affected by a hidden grade currently dont work as we dont store the altered grade in the database
398
        // instead an in memory recalculation is done. This should be remedied by MDL-11837.
399
 
400
        // Fails with 1 grade hidden. still reports 8 as being correct.
401
        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MEAN, 'GRADE_AGGREGATE_MEAN', 8, 10);
402
 
403
        // Fails with 1 grade hidden. still reports 4 as being correct.
404
        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MIN, 'GRADE_AGGREGATE_MIN', 4, 8);
405
 
406
        // Fails with 1 grade hidden. still reports 12 as being correct.
407
        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_SUM, 'GRADE_AGGREGATE_SUM', 12, 10);
408
    }
409
 
410
    /**
411
     * Test grade category aggregation using the supplied grade objects and aggregation method
412
     * @param \grade_category $grade_category the category to be tested
413
     * @param array $grade_items array of instance of grade_item
414
     * @param array $grade_grades array of instances of grade_grade
415
     * @param int $aggmethod the aggregation method to apply ie GRADE_AGGREGATE_MEAN
416
     * @param string $aggmethodname the name of the aggregation method to apply. Used to display any test failure messages
417
     * @param int $correct1 the correct final grade for the category with NO items hidden
418
     * @param int $correct2 the correct final grade for the category with the grade at $grade_grades[0] hidden
419
     * @return void
420
     */
421
    protected function helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, $aggmethod, $aggmethodname, $correct1, $correct2) {
422
        $grade_category->aggregation = $aggmethod;
423
        $grade_category->update();
424
 
425
        // Check grade_item isnt hidden from a previous test.
426
        $grade_items[0]->set_hidden(0, true);
427
        $this->helper_test_grade_aggregation_result($grade_category, $correct1, 'Testing aggregation method('.$aggmethodname.') with no items hidden %s');
428
 
429
        // Hide the grade item with grade of 2.
430
        $grade_items[0]->set_hidden(1, true);
431
        $this->helper_test_grade_aggregation_result($grade_category, $correct2, 'Testing aggregation method('.$aggmethodname.') with 1 item hidden %s');
432
    }
433
 
434
    /**
435
     * Verify the value of the category grade item for $this->userid
436
     * @param \grade_category $grade_category the category to be tested
437
     * @param int $correctgrade the expected grade
438
     * @param string $msg The message that should be displayed if the correct grade is not found
439
     * @return void
440
     */
441
    protected function helper_test_grade_aggregation_result($grade_category, $correctgrade, $msg) {
442
        global $DB;
443
 
444
        $category_grade_item = $grade_category->get_grade_item();
445
 
446
        // This creates all the grade_grades we need.
447
        grade_regrade_final_grades($this->courseid);
448
 
449
        $grade = $DB->get_record('grade_grades', array('itemid'=>$category_grade_item->id, 'userid'=>$this->userid));
450
        $this->assertWithinMargin($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
451
        $this->assertEquals(intval($correctgrade), intval($grade->finalgrade), $msg);
452
 
453
        /*
454
         * TODO this doesnt work as the grade_grades created by $grade_category->generate_grades(); dont
455
         * observe the category's max grade
456
        // delete the grade_grades for the category itself and check they get recreated correctly.
457
        $DB->delete_records('grade_grades', array('itemid'=>$category_grade_item->id));
458
        $grade_category->generate_grades();
459
 
460
        $grade = $DB->get_record('grade_grades', array('itemid'=>$category_grade_item->id, 'userid'=>$this->userid));
461
        $this->assertWithinMargin($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
462
        $this->assertEquals(intval($correctgrade), intval($grade->finalgrade), $msg);
463
         *
464
         */
465
    }
466
 
467
    protected function sub_test_grade_category_aggregate_grades() {
468
        $category = new \grade_category($this->grade_categories[0]);
469
        $this->assertTrue(method_exists($category, 'aggregate_grades'));
470
        // Tested more fully via test_grade_category_generate_grades().
471
    }
472
 
473
    protected function sub_test_grade_category_apply_limit_rules() {
474
        $items[$this->grade_items[0]->id] = new \grade_item($this->grade_items[0], false);
475
        $items[$this->grade_items[1]->id] = new \grade_item($this->grade_items[1], false);
476
        $items[$this->grade_items[2]->id] = new \grade_item($this->grade_items[2], false);
477
        $items[$this->grade_items[4]->id] = new \grade_item($this->grade_items[4], false);
478
 
479
        // Test excluding the lowest 2 out of 4 grades from aggregation with no 0 grades.
480
        $category = new \grade_category();
481
        $category->droplow = 2;
482
        $grades = array($this->grade_items[0]->id=>5.374,
483
                        $this->grade_items[1]->id=>9.4743,
484
                        $this->grade_items[2]->id=>2.5474,
485
                        $this->grade_items[4]->id=>7.3754);
486
        $category->apply_limit_rules($grades, $items);
487
        $this->assertEquals(count($grades), 2);
488
        $this->assertEquals($grades[$this->grade_items[1]->id], 9.4743);
489
        $this->assertEquals($grades[$this->grade_items[4]->id], 7.3754);
490
 
491
        // Test aggregating only the highest 1 out of 4 grades.
492
        $category = new \grade_category();
493
        $category->keephigh = 1;
494
        $category->droplow = 0;
495
        $grades = array($this->grade_items[0]->id=>5.374,
496
                        $this->grade_items[1]->id=>9.4743,
497
                        $this->grade_items[2]->id=>2.5474,
498
                        $this->grade_items[4]->id=>7.3754);
499
        $category->apply_limit_rules($grades, $items);
500
        $this->assertEquals(count($grades), 1);
501
        $grade = reset($grades);
502
        $this->assertEquals(9.4743, $grade);
503
 
504
        // Test excluding the lowest 2 out of 4 grades from aggregation with no 0 grades.
505
        // An extra credit grade item should be kept even if droplow means it would otherwise be excluded.
506
        $category = new \grade_category();
507
        $category->droplow     = 2;
508
        $category->aggregation = GRADE_AGGREGATE_SUM;
509
        $items[$this->grade_items[2]->id]->aggregationcoef = 1; // Mark grade item 2 as "extra credit".
510
        $grades = array($this->grade_items[0]->id=>5.374,
511
                        $this->grade_items[1]->id=>9.4743,
512
                        $this->grade_items[2]->id=>2.5474,
513
                        $this->grade_items[4]->id=>7.3754);
514
        $category->apply_limit_rules($grades, $items);
515
        $this->assertEquals(count($grades), 2);
516
        $this->assertEquals($grades[$this->grade_items[1]->id], 9.4743);
517
        $this->assertEquals($grades[$this->grade_items[2]->id], 2.5474);
518
 
519
        // Test only aggregating the highest 1 out of 4 grades.
520
        // An extra credit grade item is retained in addition to the highest grade.
521
        $category = new \grade_category();
522
        $category->keephigh = 1;
523
        $category->droplow = 0;
524
        $category->aggregation = GRADE_AGGREGATE_SUM;
525
        $items[$this->grade_items[2]->id]->aggregationcoef = 1; // Mark grade item 2 as "extra credit".
526
        $grades = array($this->grade_items[0]->id=>5.374,
527
                        $this->grade_items[1]->id=>9.4743,
528
                        $this->grade_items[2]->id=>2.5474,
529
                        $this->grade_items[4]->id=>7.3754);
530
        $category->apply_limit_rules($grades, $items);
531
        $this->assertEquals(count($grades), 2);
532
        $this->assertEquals($grades[$this->grade_items[1]->id], 9.4743);
533
        $this->assertEquals($grades[$this->grade_items[2]->id], 2.5474);
534
 
535
        // Test excluding the lowest 1 out of 4 grades from aggregation with two 0 grades.
536
        $items[$this->grade_items[2]->id]->aggregationcoef = 0; // Undo marking grade item 2 as "extra credit".
537
        $category = new \grade_category();
538
        $category->droplow     = 1;
539
        $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; // Simple weighted mean.
540
        $grades = array($this->grade_items[0]->id=>0, // 0 out of 110. Should be excluded from aggregation.
541
                        $this->grade_items[1]->id=>5, // 5 out of 100.
542
                        $this->grade_items[2]->id=>2, // 0 out of 6.
543
                        $this->grade_items[4]->id=>0); // 0 out of 100.
544
        $category->apply_limit_rules($grades, $items);
545
        $this->assertEquals(count($grades), 3);
546
        $this->assertEquals($grades[$this->grade_items[1]->id], 5);
547
        $this->assertEquals($grades[$this->grade_items[2]->id], 2);
548
        $this->assertEquals($grades[$this->grade_items[4]->id], 0);
549
 
550
        // Test excluding the lowest 2 out of 4 grades from aggregation with three 0 grades.
551
        $category = new \grade_category();
552
        $category->droplow     = 2;
553
        $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; // Simple weighted mean.
554
        $grades = array($this->grade_items[0]->id=>0, // 0 out of 110. Should be excluded from aggregation.
555
                        $this->grade_items[1]->id=>5, // 5 out of 100.
556
                        $this->grade_items[2]->id=>0, // 0 out of 6.
557
                        $this->grade_items[4]->id=>0); // 0 out of 100. Should be excluded from aggregation.
558
        $category->apply_limit_rules($grades, $items);
559
        $this->assertEquals(count($grades), 2);
560
        $this->assertEquals($grades[$this->grade_items[1]->id], 5);
561
        $this->assertEquals($grades[$this->grade_items[2]->id], 0);
562
 
563
        // Test excluding the lowest 5 out of 4 grades from aggregation.
564
        // Just to check we handle this sensibly.
565
        $category = new \grade_category();
566
        $category->droplow     = 5;
567
        $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; // Simple weighted mean.
568
        $grades = array($this->grade_items[0]->id=>0, // 0 out of 110. Should be excluded from aggregation.
569
                        $this->grade_items[1]->id=>5, // 5 out of 100.
570
                        $this->grade_items[2]->id=>6, // 6 out of 6.
571
                        $this->grade_items[4]->id=>1);// 1 out of 100. Should be excluded from aggregation.
572
        $category->apply_limit_rules($grades, $items);
573
        $this->assertEquals(count($grades), 0);
574
 
575
        // Test excluding the lowest 4 out of 4 grades from aggregation with one marked as extra credit.
576
        $category = new \grade_category();
577
        $category->droplow     = 4;
578
        $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; // Simple weighted mean.
579
        $items[$this->grade_items[2]->id]->aggregationcoef = 1; // Mark grade item 2 as "extra credit".
580
        $grades = array($this->grade_items[0]->id=>0, // 0 out of 110. Should be excluded from aggregation.
581
                        $this->grade_items[1]->id=>5, // 5 out of 100. Should be excluded from aggregation.
582
                        $this->grade_items[2]->id=>6, // 6 out of 6. Extra credit. Should be retained.
583
                        $this->grade_items[4]->id=>1);// 1 out of 100. Should be excluded from aggregation.
584
        $category->apply_limit_rules($grades, $items);
585
        $this->assertEquals(count($grades), 1);
586
        $this->assertEquals($grades[$this->grade_items[2]->id], 6);
587
 
588
        // MDL-35667 - There was an infinite loop if several items had the same grade and at least one was extra credit.
589
        $category = new \grade_category();
590
        $category->droplow     = 1;
591
        $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; // Simple weighted mean.
592
        $items[$this->grade_items[1]->id]->aggregationcoef = 1; // Mark grade item 1 as "extra credit".
593
        $grades = array($this->grade_items[0]->id=>1, // 1 out of 110. Should be excluded from aggregation.
594
                        $this->grade_items[1]->id=>1, // 1 out of 100. Extra credit. Should be retained.
595
                        $this->grade_items[2]->id=>1, // 1 out of 6. Should be retained.
596
                        $this->grade_items[4]->id=>1);// 1 out of 100. Should be retained.
597
        $category->apply_limit_rules($grades, $items);
598
        $this->assertEquals(count($grades), 3);
599
        $this->assertEquals($grades[$this->grade_items[1]->id], 1);
600
        $this->assertEquals($grades[$this->grade_items[2]->id], 1);
601
        $this->assertEquals($grades[$this->grade_items[4]->id], 1);
602
 
603
    }
604
 
605
    protected function sub_test_grade_category_is_aggregationcoef_used() {
606
        $category = new \grade_category();
607
        // Following use aggregationcoef.
608
        $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN;
609
        $this->assertTrue($category->is_aggregationcoef_used());
610
        $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2;
611
        $this->assertTrue($category->is_aggregationcoef_used());
612
        $category->aggregation = GRADE_AGGREGATE_EXTRACREDIT_MEAN;
613
        $this->assertTrue($category->is_aggregationcoef_used());
614
        $category->aggregation = GRADE_AGGREGATE_SUM;
615
        $this->assertTrue($category->is_aggregationcoef_used());
616
 
617
        // Following don't use aggregationcoef.
618
        $category->aggregation = GRADE_AGGREGATE_MAX;
619
        $this->assertFalse($category->is_aggregationcoef_used());
620
        $category->aggregation = GRADE_AGGREGATE_MEAN;
621
        $this->assertFalse($category->is_aggregationcoef_used());
622
        $category->aggregation = GRADE_AGGREGATE_MEDIAN;
623
        $this->assertFalse($category->is_aggregationcoef_used());
624
        $category->aggregation = GRADE_AGGREGATE_MIN;
625
        $this->assertFalse($category->is_aggregationcoef_used());
626
        $category->aggregation = GRADE_AGGREGATE_MODE;
627
        $this->assertFalse($category->is_aggregationcoef_used());
628
    }
629
 
630
    protected function sub_test_grade_category_aggregation_uses_aggregationcoef() {
631
 
632
        $this->assertTrue(\grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_WEIGHTED_MEAN));
633
        $this->assertTrue(\grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_WEIGHTED_MEAN2));
634
        $this->assertTrue(\grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_EXTRACREDIT_MEAN));
635
        $this->assertTrue(\grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_SUM));
636
 
637
        $this->assertFalse(\grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_MAX));
638
        $this->assertFalse(\grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_MEAN));
639
        $this->assertFalse(\grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_MEDIAN));
640
        $this->assertFalse(\grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_MIN));
641
        $this->assertFalse(\grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_MODE));
642
    }
643
 
644
    protected function sub_test_grade_category_fetch_course_tree() {
645
        $category = new \grade_category();
646
        $this->assertTrue(method_exists($category, 'fetch_course_tree'));
647
        // TODO: add some tests.
648
    }
649
 
650
    protected function sub_test_grade_category_get_children() {
651
        $course_category = \grade_category::fetch_course_category($this->courseid);
652
 
653
        $category = new \grade_category($this->grade_categories[0]);
654
        $this->assertTrue(method_exists($category, 'get_children'));
655
 
656
        $children_array = $category->get_children(0);
657
 
658
        $this->assertTrue(is_array($children_array));
659
        $this->assertFalse(empty($children_array[2]));
660
        $this->assertFalse(empty($children_array[2]['object']));
661
        $this->assertFalse(empty($children_array[2]['children']));
662
        $this->assertEquals($this->grade_categories[1]->id, $children_array[2]['object']->id);
663
        $this->assertEquals($this->grade_categories[2]->id, $children_array[5]['object']->id);
664
        $this->assertEquals($this->grade_items[0]->id, $children_array[2]['children'][3]['object']->id);
665
        $this->assertEquals($this->grade_items[1]->id, $children_array[2]['children'][4]['object']->id);
666
        $this->assertEquals($this->grade_items[2]->id, $children_array[5]['children'][6]['object']->id);
667
    }
668
 
669
    protected function sub_test_grade_category_load_grade_item() {
670
        $category = new \grade_category($this->grade_categories[0]);
671
        $this->assertTrue(method_exists($category, 'load_grade_item'));
672
        $this->assertEquals(null, $category->grade_item);
673
        $category->load_grade_item();
674
        $this->assertEquals($this->grade_items[3]->id, $category->grade_item->id);
675
    }
676
 
677
    protected function sub_test_grade_category_get_grade_item() {
678
        $category = new \grade_category($this->grade_categories[0]);
679
        $this->assertTrue(method_exists($category, 'get_grade_item'));
680
        $grade_item = $category->get_grade_item();
681
        $this->assertEquals($this->grade_items[3]->id, $grade_item->id);
682
    }
683
 
684
    protected function sub_test_grade_category_load_parent_category() {
685
        $category = new \grade_category($this->grade_categories[1]);
686
        $this->assertTrue(method_exists($category, 'load_parent_category'));
687
        $this->assertEquals(null, $category->parent_category);
688
        $category->load_parent_category();
689
        $this->assertEquals($this->grade_categories[0]->id, $category->parent_category->id);
690
    }
691
 
692
    protected function sub_test_grade_category_get_parent_category() {
693
        $category = new \grade_category($this->grade_categories[1]);
694
        $this->assertTrue(method_exists($category, 'get_parent_category'));
695
        $parent_category = $category->get_parent_category();
696
        $this->assertEquals($this->grade_categories[0]->id, $parent_category->id);
697
    }
698
 
699
    /**
700
     * Tests the getter of the category fullname with escaped HTML.
701
     */
702
    protected function sub_test_grade_category_get_name_escaped() {
703
        $category = new \grade_category($this->grade_categories[0]);
704
        $this->assertTrue(method_exists($category, 'get_name'));
705
        $this->assertEquals(format_string($this->grade_categories[0]->fullname, true, ['escape' => true]),
706
            $category->get_name(true));
707
    }
708
 
709
    /**
710
     * Tests the getter of the category fullname with unescaped HTML.
711
     */
712
    protected function sub_test_grade_category_get_name_unescaped() {
713
        $category = new \grade_category($this->grade_categories[0]);
714
        $this->assertTrue(method_exists($category, 'get_name'));
715
        $this->assertEquals(format_string($this->grade_categories[0]->fullname, true, ['escape' => false]),
716
            $category->get_name(false));
717
    }
718
 
719
    protected function sub_test_grade_category_set_parent() {
720
        $category = new \grade_category($this->grade_categories[1]);
721
        $this->assertTrue(method_exists($category, 'set_parent'));
722
        // TODO: implement detailed tests.
723
 
724
        $course_category = \grade_category::fetch_course_category($this->courseid);
725
        $this->assertTrue($category->set_parent($course_category->id));
726
        $this->assertEquals($course_category->id, $category->parent);
727
    }
728
 
729
    protected function sub_test_grade_category_get_final() {
730
        $category = new \grade_category($this->grade_categories[0]);
731
        $this->assertTrue(method_exists($category, 'get_final'));
732
        $category->load_grade_item();
733
        $this->assertEquals($category->get_final(), $category->grade_item->get_final());
734
    }
735
 
736
    protected function sub_test_grade_category_get_sortorder() {
737
        $category = new \grade_category($this->grade_categories[0]);
738
        $this->assertTrue(method_exists($category, 'get_sortorder'));
739
        $category->load_grade_item();
740
        $this->assertEquals($category->get_sortorder(), $category->grade_item->get_sortorder());
741
    }
742
 
743
    protected function sub_test_grade_category_set_sortorder() {
744
        $category = new \grade_category($this->grade_categories[0]);
745
        $this->assertTrue(method_exists($category, 'set_sortorder'));
746
        $category->load_grade_item();
747
        $this->assertEquals($category->set_sortorder(10), $category->grade_item->set_sortorder(10));
748
    }
749
 
750
    protected function sub_test_grade_category_move_after_sortorder() {
751
        $category = new \grade_category($this->grade_categories[0]);
752
        $this->assertTrue(method_exists($category, 'move_after_sortorder'));
753
        $category->load_grade_item();
754
        $this->assertEquals($category->move_after_sortorder(10), $category->grade_item->move_after_sortorder(10));
755
    }
756
 
757
    protected function sub_test_grade_category_is_course_category() {
758
        $category = \grade_category::fetch_course_category($this->courseid);
759
        $this->assertTrue(method_exists($category, 'is_course_category'));
760
        $this->assertTrue($category->is_course_category());
761
    }
762
 
763
    protected function sub_test_grade_category_fetch_course_category() {
764
        $category = new \grade_category();
765
        $this->assertTrue(method_exists($category, 'fetch_course_category'));
766
        $category = \grade_category::fetch_course_category($this->courseid);
767
        $this->assertTrue(empty($category->parent));
768
    }
769
    /**
770
     * TODO implement
771
     */
772
    protected function sub_test_grade_category_is_editable() {
773
 
774
    }
775
 
776
    protected function sub_test_grade_category_is_locked() {
777
        $category = new \grade_category($this->grade_categories[0]);
778
        $this->assertTrue(method_exists($category, 'is_locked'));
779
        $category->load_grade_item();
780
        $this->assertEquals($category->is_locked(), $category->grade_item->is_locked());
781
    }
782
 
783
    protected function sub_test_grade_category_set_locked() {
784
        $category = new \grade_category($this->grade_categories[0]);
785
        $this->assertTrue(method_exists($category, 'set_locked'));
786
 
787
        // Even though a grade that needs updating cannot be locked, set_locked will return true because it will successfully
788
        // schedule the locking for as soon as final grades are recalculated.
789
        $this->assertTrue($category->set_locked(1));
790
        // The category should not be locked yet as we are waiting for a recalculation.
791
        $this->assertFalse($category->is_locked());
792
        grade_regrade_final_grades($this->courseid);
793
 
794
        // Get the category from the db again.
795
        $category = new \grade_category($this->grade_categories[0]);
796
        // The category is locked now.
797
        $this->assertTrue($category->is_locked());
798
    }
799
 
800
    protected function sub_test_grade_category_is_hidden() {
801
        $category = new \grade_category($this->grade_categories[0]);
802
        $this->assertTrue(method_exists($category, 'is_hidden'));
803
        $category->load_grade_item();
804
        $this->assertEquals($category->is_hidden(), $category->grade_item->is_hidden());
805
    }
806
 
807
    protected function sub_test_grade_category_set_hidden() {
808
        $category = new \grade_category($this->grade_categories[0]);
809
        $this->assertTrue(method_exists($category, 'set_hidden'));
810
        $category->set_hidden(1, true);
811
        $category->load_grade_item();
812
        $this->assertEquals(true, $category->grade_item->is_hidden());
813
    }
814
 
815
    protected function sub_test_grade_category_can_control_visibility() {
816
        $category = new \grade_category($this->grade_categories[0]);
817
        $this->assertTrue($category->can_control_visibility());
818
    }
819
 
820
    protected function sub_test_grade_category_insert_course_category() {
821
        // Beware: adding a duplicate course category messes up the data in a way that's hard to recover from.
822
        $grade_category = new \grade_category();
823
        $this->assertTrue(method_exists($grade_category, 'insert_course_category'));
824
 
825
        $id = $grade_category->insert_course_category($this->courseid);
826
        $this->assertNotNull($id);
827
        $this->assertEquals('?', $grade_category->fullname);
828
        $this->assertEquals(GRADE_AGGREGATE_WEIGHTED_MEAN2, $grade_category->aggregation);
829
        $this->assertEquals("/$id/", $grade_category->path);
830
        $this->assertEquals(1, $grade_category->depth);
831
        $this->assertNull($grade_category->parent);
832
    }
833
 
834
    protected function generate_random_raw_grade($item, $userid) {
835
        $grade = new \grade_grade();
836
        $grade->itemid = $item->id;
837
        $grade->userid = $userid;
838
        $grade->grademin = 0;
839
        $grade->grademax = 1;
840
        $valuetype = "grade$item->gradetype";
841
        $grade->rawgrade = rand(0, 1000) / 1000;
842
        $grade->insert();
843
        return $grade->rawgrade;
844
    }
845
 
846
    protected function sub_test_grade_category_is_extracredit_used() {
847
        $category = new \grade_category();
848
        // Following use aggregationcoef.
849
        $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2;
850
        $this->assertTrue($category->is_extracredit_used());
851
        $category->aggregation = GRADE_AGGREGATE_EXTRACREDIT_MEAN;
852
        $this->assertTrue($category->is_extracredit_used());
853
        $category->aggregation = GRADE_AGGREGATE_SUM;
854
        $this->assertTrue($category->is_extracredit_used());
855
 
856
        // Following don't use aggregationcoef.
857
        $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN;
858
        $this->assertFalse($category->is_extracredit_used());
859
        $category->aggregation = GRADE_AGGREGATE_MAX;
860
        $this->assertFalse($category->is_extracredit_used());
861
        $category->aggregation = GRADE_AGGREGATE_MEAN;
862
        $this->assertFalse($category->is_extracredit_used());
863
        $category->aggregation = GRADE_AGGREGATE_MEDIAN;
864
        $this->assertFalse($category->is_extracredit_used());
865
        $category->aggregation = GRADE_AGGREGATE_MIN;
866
        $this->assertFalse($category->is_extracredit_used());
867
        $category->aggregation = GRADE_AGGREGATE_MODE;
868
        $this->assertFalse($category->is_extracredit_used());
869
    }
870
 
871
    protected function sub_test_grade_category_aggregation_uses_extracredit() {
872
 
873
        $this->assertTrue(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_WEIGHTED_MEAN2));
874
        $this->assertTrue(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_EXTRACREDIT_MEAN));
875
        $this->assertTrue(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_SUM));
876
 
877
        $this->assertFalse(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_WEIGHTED_MEAN));
878
        $this->assertFalse(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MAX));
879
        $this->assertFalse(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MEAN));
880
        $this->assertFalse(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MEDIAN));
881
        $this->assertFalse(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MIN));
882
        $this->assertFalse(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MODE));
883
    }
884
 
885
    /**
886
     * Test for category total visibility.
887
     */
888
    protected function sub_test_grade_category_total_visibility() {
889
        // 15 is a manual grade item in grade_categories[5].
890
        $category = new \grade_category($this->grade_categories[5], true);
891
        $gradeitem = new \grade_item($this->grade_items[15], true);
892
 
893
        // Hide grade category.
894
        $category->set_hidden(true, true);
895
        $this->assertTrue($category->is_hidden());
896
        // Category total is hidden.
897
        $categorytotal = $category->get_grade_item();
898
        $this->assertTrue($categorytotal->is_hidden());
899
        // Manual grade is hidden.
900
        $gradeitem->update_from_db();
901
        $this->assertTrue($gradeitem->is_hidden());
902
 
903
        // Unhide manual grade item.
904
        $gradeitem->set_hidden(false);
905
        $this->assertFalse($gradeitem->is_hidden());
906
        // Category is unhidden.
907
        $category->update_from_db();
908
        $this->assertFalse($category->is_hidden());
909
        // Category total remain hidden.
910
        $categorytotal = $category->get_grade_item();
911
        $this->assertTrue($categorytotal->is_hidden());
912
 
913
        // Edit manual grade item.
914
        $this->assertFalse($gradeitem->is_locked());
915
        $gradeitem->set_locked(true);
916
        $gradeitem->update_from_db();
917
        $this->assertTrue($gradeitem->is_locked());
918
        // Category total should still be hidden.
919
        $category->update_from_db();
920
        $categorytotal = $category->get_grade_item();
921
        $this->assertTrue($categorytotal->is_hidden());
922
    }
923
}