Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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

   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  
  34      public function test_grade_category() {
  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          // Will return false as cannot lock a grade that needs updating.
 788          $this->assertFalse($category->set_locked(1));
 789          grade_regrade_final_grades($this->courseid);
 790  
 791          // Get the category from the db again.
 792          $category = new \grade_category($this->grade_categories[0]);
 793          $this->assertTrue($category->set_locked(1));
 794      }
 795  
 796      protected function sub_test_grade_category_is_hidden() {
 797          $category = new \grade_category($this->grade_categories[0]);
 798          $this->assertTrue(method_exists($category, 'is_hidden'));
 799          $category->load_grade_item();
 800          $this->assertEquals($category->is_hidden(), $category->grade_item->is_hidden());
 801      }
 802  
 803      protected function sub_test_grade_category_set_hidden() {
 804          $category = new \grade_category($this->grade_categories[0]);
 805          $this->assertTrue(method_exists($category, 'set_hidden'));
 806          $category->set_hidden(1, true);
 807          $category->load_grade_item();
 808          $this->assertEquals(true, $category->grade_item->is_hidden());
 809      }
 810  
 811      protected function sub_test_grade_category_can_control_visibility() {
 812          $category = new \grade_category($this->grade_categories[0]);
 813          $this->assertTrue($category->can_control_visibility());
 814      }
 815  
 816      protected function sub_test_grade_category_insert_course_category() {
 817          // Beware: adding a duplicate course category messes up the data in a way that's hard to recover from.
 818          $grade_category = new \grade_category();
 819          $this->assertTrue(method_exists($grade_category, 'insert_course_category'));
 820  
 821          $id = $grade_category->insert_course_category($this->courseid);
 822          $this->assertNotNull($id);
 823          $this->assertEquals('?', $grade_category->fullname);
 824          $this->assertEquals(GRADE_AGGREGATE_WEIGHTED_MEAN2, $grade_category->aggregation);
 825          $this->assertEquals("/$id/", $grade_category->path);
 826          $this->assertEquals(1, $grade_category->depth);
 827          $this->assertNull($grade_category->parent);
 828      }
 829  
 830      protected function generate_random_raw_grade($item, $userid) {
 831          $grade = new \grade_grade();
 832          $grade->itemid = $item->id;
 833          $grade->userid = $userid;
 834          $grade->grademin = 0;
 835          $grade->grademax = 1;
 836          $valuetype = "grade$item->gradetype";
 837          $grade->rawgrade = rand(0, 1000) / 1000;
 838          $grade->insert();
 839          return $grade->rawgrade;
 840      }
 841  
 842      protected function sub_test_grade_category_is_extracredit_used() {
 843          $category = new \grade_category();
 844          // Following use aggregationcoef.
 845          $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2;
 846          $this->assertTrue($category->is_extracredit_used());
 847          $category->aggregation = GRADE_AGGREGATE_EXTRACREDIT_MEAN;
 848          $this->assertTrue($category->is_extracredit_used());
 849          $category->aggregation = GRADE_AGGREGATE_SUM;
 850          $this->assertTrue($category->is_extracredit_used());
 851  
 852          // Following don't use aggregationcoef.
 853          $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN;
 854          $this->assertFalse($category->is_extracredit_used());
 855          $category->aggregation = GRADE_AGGREGATE_MAX;
 856          $this->assertFalse($category->is_extracredit_used());
 857          $category->aggregation = GRADE_AGGREGATE_MEAN;
 858          $this->assertFalse($category->is_extracredit_used());
 859          $category->aggregation = GRADE_AGGREGATE_MEDIAN;
 860          $this->assertFalse($category->is_extracredit_used());
 861          $category->aggregation = GRADE_AGGREGATE_MIN;
 862          $this->assertFalse($category->is_extracredit_used());
 863          $category->aggregation = GRADE_AGGREGATE_MODE;
 864          $this->assertFalse($category->is_extracredit_used());
 865      }
 866  
 867      protected function sub_test_grade_category_aggregation_uses_extracredit() {
 868  
 869          $this->assertTrue(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_WEIGHTED_MEAN2));
 870          $this->assertTrue(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_EXTRACREDIT_MEAN));
 871          $this->assertTrue(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_SUM));
 872  
 873          $this->assertFalse(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_WEIGHTED_MEAN));
 874          $this->assertFalse(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MAX));
 875          $this->assertFalse(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MEAN));
 876          $this->assertFalse(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MEDIAN));
 877          $this->assertFalse(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MIN));
 878          $this->assertFalse(\grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MODE));
 879      }
 880  
 881      /**
 882       * Test for category total visibility.
 883       */
 884      protected function sub_test_grade_category_total_visibility() {
 885          // 15 is a manual grade item in grade_categories[5].
 886          $category = new \grade_category($this->grade_categories[5], true);
 887          $gradeitem = new \grade_item($this->grade_items[15], true);
 888  
 889          // Hide grade category.
 890          $category->set_hidden(true, true);
 891          $this->assertTrue($category->is_hidden());
 892          // Category total is hidden.
 893          $categorytotal = $category->get_grade_item();
 894          $this->assertTrue($categorytotal->is_hidden());
 895          // Manual grade is hidden.
 896          $gradeitem->update_from_db();
 897          $this->assertTrue($gradeitem->is_hidden());
 898  
 899          // Unhide manual grade item.
 900          $gradeitem->set_hidden(false);
 901          $this->assertFalse($gradeitem->is_hidden());
 902          // Category is unhidden.
 903          $category->update_from_db();
 904          $this->assertFalse($category->is_hidden());
 905          // Category total remain hidden.
 906          $categorytotal = $category->get_grade_item();
 907          $this->assertTrue($categorytotal->is_hidden());
 908  
 909          // Edit manual grade item.
 910          $this->assertFalse($gradeitem->is_locked());
 911          $gradeitem->set_locked(true);
 912          $gradeitem->update_from_db();
 913          $this->assertTrue($gradeitem->is_locked());
 914          // Category total should still be hidden.
 915          $category->update_from_db();
 916          $categorytotal = $category->get_grade_item();
 917          $this->assertTrue($categorytotal->is_hidden());
 918      }
 919  }