Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

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