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 311 and 401] [Versions 39 and 401] [Versions 400 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  /**
  18   * Definition of a class to represent a grade category
  19   *
  20   * @package   core_grades
  21   * @copyright 2006 Nicolas Connault
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  require_once (__DIR__ . '/grade_object.php');
  28  
  29  /**
  30   * grade_category is an object mapped to DB table {prefix}grade_categories
  31   *
  32   * @package   core_grades
  33   * @category  grade
  34   * @copyright 2007 Nicolas Connault
  35   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class grade_category extends grade_object {
  38      /**
  39       * The DB table.
  40       * @var string $table
  41       */
  42      public $table = 'grade_categories';
  43  
  44      /**
  45       * Array of required table fields, must start with 'id'.
  46       * @var array $required_fields
  47       */
  48      public $required_fields = array('id', 'courseid', 'parent', 'depth', 'path', 'fullname', 'aggregation',
  49                                   'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes',
  50                                   'timecreated', 'timemodified', 'hidden');
  51  
  52      /**
  53       * The course this category belongs to.
  54       * @var int $courseid
  55       */
  56      public $courseid;
  57  
  58      /**
  59       * The category this category belongs to (optional).
  60       * @var int $parent
  61       */
  62      public $parent;
  63  
  64      /**
  65       * The grade_category object referenced by $this->parent (PK).
  66       * @var grade_category $parent_category
  67       */
  68      public $parent_category;
  69  
  70      /**
  71       * The number of parents this category has.
  72       * @var int $depth
  73       */
  74      public $depth = 0;
  75  
  76      /**
  77       * Shows the hierarchical path for this category as /1/2/3/ (like course_categories), the last number being
  78       * this category's autoincrement ID number.
  79       * @var string $path
  80       */
  81      public $path;
  82  
  83      /**
  84       * The name of this category.
  85       * @var string $fullname
  86       */
  87      public $fullname;
  88  
  89      /**
  90       * A constant pointing to one of the predefined aggregation strategies (none, mean, median, sum etc) .
  91       * @var int $aggregation
  92       */
  93      public $aggregation = GRADE_AGGREGATE_SUM;
  94  
  95      /**
  96       * Keep only the X highest items.
  97       * @var int $keephigh
  98       */
  99      public $keephigh = 0;
 100  
 101      /**
 102       * Drop the X lowest items.
 103       * @var int $droplow
 104       */
 105      public $droplow = 0;
 106  
 107      /**
 108       * Aggregate only graded items
 109       * @var int $aggregateonlygraded
 110       */
 111      public $aggregateonlygraded = 0;
 112  
 113      /**
 114       * Aggregate outcomes together with normal items
 115       * @var int $aggregateoutcomes
 116       */
 117      public $aggregateoutcomes = 0;
 118  
 119      /**
 120       * Array of grade_items or grade_categories nested exactly 1 level below this category
 121       * @var array $children
 122       */
 123      public $children;
 124  
 125      /**
 126       * A hierarchical array of all children below this category. This is stored separately from
 127       * $children because it is more memory-intensive and may not be used as often.
 128       * @var array $all_children
 129       */
 130      public $all_children;
 131  
 132      /**
 133       * An associated grade_item object, with itemtype=category, used to calculate and cache a set of grade values
 134       * for this category.
 135       * @var grade_item $grade_item
 136       */
 137      public $grade_item;
 138  
 139      /**
 140       * Temporary sortorder for speedup of children resorting
 141       * @var int $sortorder
 142       */
 143      public $sortorder;
 144  
 145      /**
 146       * List of options which can be "forced" from site settings.
 147       * @var array $forceable
 148       */
 149      public $forceable = array('aggregation', 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes');
 150  
 151      /**
 152       * String representing the aggregation coefficient. Variable is used as cache.
 153       * @var string $coefstring
 154       */
 155      public $coefstring = null;
 156  
 157      /**
 158       * Static variable storing the result from {@link self::can_apply_limit_rules}.
 159       * @var bool
 160       */
 161      protected $canapplylimitrules;
 162  
 163      /**
 164       * Builds this category's path string based on its parents (if any) and its own id number.
 165       * This is typically done just before inserting this object in the DB for the first time,
 166       * or when a new parent is added or changed. It is a recursive function: once the calling
 167       * object no longer has a parent, the path is complete.
 168       *
 169       * @param grade_category $grade_category A Grade_Category object
 170       * @return string The category's path string
 171       */
 172      public static function build_path($grade_category) {
 173          global $DB;
 174  
 175          if (empty($grade_category->parent)) {
 176              return '/'.$grade_category->id.'/';
 177  
 178          } else {
 179              $parent = $DB->get_record('grade_categories', array('id' => $grade_category->parent));
 180              return grade_category::build_path($parent).$grade_category->id.'/';
 181          }
 182      }
 183  
 184      /**
 185       * Finds and returns a grade_category instance based on params.
 186       *
 187       * @param array $params associative arrays varname=>value
 188       * @return grade_category The retrieved grade_category instance or false if none found.
 189       */
 190      public static function fetch($params) {
 191          if ($records = self::retrieve_record_set($params)) {
 192              return reset($records);
 193          }
 194  
 195          $record = grade_object::fetch_helper('grade_categories', 'grade_category', $params);
 196  
 197          // We store it as an array to keep a key => result set interface in the cache, grade_object::fetch_helper is
 198          // managing exceptions. We return only the first element though.
 199          $records = false;
 200          if ($record) {
 201              $records = array($record->id => $record);
 202          }
 203  
 204          self::set_record_set($params, $records);
 205  
 206          return $record;
 207      }
 208  
 209      /**
 210       * Finds and returns all grade_category instances based on params.
 211       *
 212       * @param array $params associative arrays varname=>value
 213       * @return array array of grade_category insatnces or false if none found.
 214       */
 215      public static function fetch_all($params) {
 216          if ($records = self::retrieve_record_set($params)) {
 217              return $records;
 218          }
 219  
 220          $records = grade_object::fetch_all_helper('grade_categories', 'grade_category', $params);
 221          self::set_record_set($params, $records);
 222  
 223          return $records;
 224      }
 225  
 226      /**
 227       * In addition to update() as defined in grade_object, call force_regrading of parent categories, if applicable.
 228       *
 229       * @param string $source from where was the object updated (mod/forum, manual, etc.)
 230       * @param bool $isbulkupdate If bulk grade update is happening.
 231       * @return bool success
 232       */
 233      public function update($source = null, $isbulkupdate = false) {
 234          // load the grade item or create a new one
 235          $this->load_grade_item();
 236  
 237          // force recalculation of path;
 238          if (empty($this->path)) {
 239              $this->path  = grade_category::build_path($this);
 240              $this->depth = substr_count($this->path, '/') - 1;
 241              $updatechildren = true;
 242  
 243          } else {
 244              $updatechildren = false;
 245          }
 246  
 247          $this->apply_forced_settings();
 248  
 249          // these are exclusive
 250          if ($this->droplow > 0) {
 251              $this->keephigh = 0;
 252  
 253          } else if ($this->keephigh > 0) {
 254              $this->droplow = 0;
 255          }
 256  
 257          // Recalculate grades if needed
 258          if ($this->qualifies_for_regrading()) {
 259              $this->force_regrading();
 260          }
 261  
 262          $this->timemodified = time();
 263  
 264          $result = parent::update($source);
 265  
 266          // now update paths in all child categories
 267          if ($result and $updatechildren) {
 268  
 269              if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
 270  
 271                  foreach ($children as $child) {
 272                      $child->path  = null;
 273                      $child->depth = 0;
 274                      $child->update($source);
 275                  }
 276              }
 277          }
 278  
 279          return $result;
 280      }
 281  
 282      /**
 283       * If parent::delete() is successful, send force_regrading message to parent category.
 284       *
 285       * @param string $source from where was the object deleted (mod/forum, manual, etc.)
 286       * @return bool success
 287       */
 288      public function delete($source=null) {
 289          global $DB;
 290  
 291          $transaction = $DB->start_delegated_transaction();
 292          $grade_item = $this->load_grade_item();
 293  
 294          if ($this->is_course_category()) {
 295  
 296              if ($categories = grade_category::fetch_all(array('courseid'=>$this->courseid))) {
 297  
 298                  foreach ($categories as $category) {
 299  
 300                      if ($category->id == $this->id) {
 301                          continue; // do not delete course category yet
 302                      }
 303                      $category->delete($source);
 304                  }
 305              }
 306  
 307              if ($items = grade_item::fetch_all(array('courseid'=>$this->courseid))) {
 308  
 309                  foreach ($items as $item) {
 310  
 311                      if ($item->id == $grade_item->id) {
 312                          continue; // do not delete course item yet
 313                      }
 314                      $item->delete($source);
 315                  }
 316              }
 317  
 318          } else {
 319              $this->force_regrading();
 320  
 321              $parent = $this->load_parent_category();
 322  
 323              // Update children's categoryid/parent field first
 324              if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
 325                  foreach ($children as $child) {
 326                      $child->set_parent($parent->id);
 327                  }
 328              }
 329  
 330              if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
 331                  foreach ($children as $child) {
 332                      $child->set_parent($parent->id);
 333                  }
 334              }
 335          }
 336  
 337          // first delete the attached grade item and grades
 338          $grade_item->delete($source);
 339  
 340          // delete category itself
 341          $success = parent::delete($source);
 342  
 343          $transaction->allow_commit();
 344          return $success;
 345      }
 346  
 347      /**
 348       * In addition to the normal insert() defined in grade_object, this method sets the depth
 349       * and path for this object, and update the record accordingly.
 350       *
 351       * We do this here instead of in the constructor as they both need to know the record's
 352       * ID number, which only gets created at insertion time.
 353       * This method also creates an associated grade_item if this wasn't done during construction.
 354       *
 355       * @param string $source from where was the object inserted (mod/forum, manual, etc.)
 356       * @param bool $isbulkupdate If bulk grade update is happening.
 357       * @return int PK ID if successful, false otherwise
 358       */
 359      public function insert($source = null, $isbulkupdate = false) {
 360  
 361          if (empty($this->courseid)) {
 362              throw new \moodle_exception('cannotinsertgrade');
 363          }
 364  
 365          if (empty($this->parent)) {
 366              $course_category = grade_category::fetch_course_category($this->courseid);
 367              $this->parent = $course_category->id;
 368          }
 369  
 370          $this->path = null;
 371  
 372          $this->timecreated = $this->timemodified = time();
 373  
 374          if (!parent::insert($source)) {
 375              debugging("Could not insert this category: " . print_r($this, true));
 376              return false;
 377          }
 378  
 379          $this->force_regrading();
 380  
 381          // build path and depth
 382          $this->update($source);
 383  
 384          return $this->id;
 385      }
 386  
 387      /**
 388       * Internal function - used only from fetch_course_category()
 389       * Normal insert() can not be used for course category
 390       *
 391       * @param int $courseid The course ID
 392       * @return int The ID of the new course category
 393       */
 394      public function insert_course_category($courseid) {
 395          $this->courseid    = $courseid;
 396          $this->fullname    = '?';
 397          $this->path        = null;
 398          $this->parent      = null;
 399          $this->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2;
 400  
 401          $this->apply_default_settings();
 402          $this->apply_forced_settings();
 403  
 404          $this->timecreated = $this->timemodified = time();
 405  
 406          if (!parent::insert('system')) {
 407              debugging("Could not insert this category: " . print_r($this, true));
 408              return false;
 409          }
 410  
 411          // build path and depth
 412          $this->update('system');
 413  
 414          return $this->id;
 415      }
 416  
 417      /**
 418       * Compares the values held by this object with those of the matching record in DB, and returns
 419       * whether or not these differences are sufficient to justify an update of all parent objects.
 420       * This assumes that this object has an ID number and a matching record in DB. If not, it will return false.
 421       *
 422       * @return bool
 423       */
 424      public function qualifies_for_regrading() {
 425          if (empty($this->id)) {
 426              debugging("Can not regrade non existing category");
 427              return false;
 428          }
 429  
 430          $db_item = grade_category::fetch(array('id'=>$this->id));
 431  
 432          $aggregationdiff = $db_item->aggregation         != $this->aggregation;
 433          $keephighdiff    = $db_item->keephigh            != $this->keephigh;
 434          $droplowdiff     = $db_item->droplow             != $this->droplow;
 435          $aggonlygrddiff  = $db_item->aggregateonlygraded != $this->aggregateonlygraded;
 436          $aggoutcomesdiff = $db_item->aggregateoutcomes   != $this->aggregateoutcomes;
 437  
 438          return ($aggregationdiff || $keephighdiff || $droplowdiff || $aggonlygrddiff || $aggoutcomesdiff);
 439      }
 440  
 441      /**
 442       * Marks this grade categories' associated grade item as needing regrading
 443       */
 444      public function force_regrading() {
 445          $grade_item = $this->load_grade_item();
 446          $grade_item->force_regrading();
 447      }
 448  
 449      /**
 450       * Something that should be called before we start regrading the whole course.
 451       *
 452       * @return void
 453       */
 454      public function pre_regrade_final_grades() {
 455          $this->auto_update_weights();
 456          $this->auto_update_max();
 457      }
 458  
 459      /**
 460       * Generates and saves final grades in associated category grade item.
 461       * These immediate children must already have their own final grades.
 462       * The category's aggregation method is used to generate final grades.
 463       *
 464       * Please note that category grade is either calculated or aggregated, not both at the same time.
 465       *
 466       * This method must be used ONLY from grade_item::regrade_final_grades(),
 467       * because the calculation must be done in correct order!
 468       *
 469       * Steps to follow:
 470       *  1. Get final grades from immediate children
 471       *  3. Aggregate these grades
 472       *  4. Save them in final grades of associated category grade item
 473       *
 474       * @param int $userid The user ID if final grade generation should be limited to a single user
 475       * @param \core\progress\base|null $progress Optional progress indicator
 476       * @return bool
 477       */
 478      public function generate_grades($userid=null, ?\core\progress\base $progress = null) {
 479          global $CFG, $DB;
 480  
 481          $this->load_grade_item();
 482  
 483          if ($this->grade_item->is_locked()) {
 484              return true; // no need to recalculate locked items
 485          }
 486  
 487          // find grade items of immediate children (category or grade items) and force site settings
 488          $depends_on = $this->grade_item->depends_on();
 489  
 490          if (empty($depends_on)) {
 491              $items = false;
 492  
 493          } else {
 494              list($usql, $params) = $DB->get_in_or_equal($depends_on);
 495              $sql = "SELECT *
 496                        FROM {grade_items}
 497                       WHERE id $usql";
 498              $items = $DB->get_records_sql($sql, $params);
 499              foreach ($items as $id => $item) {
 500                  $items[$id] = new grade_item($item, false);
 501              }
 502          }
 503  
 504          $grade_inst = new grade_grade();
 505          $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
 506  
 507          // where to look for final grades - include grade of this item too, we will store the results there
 508          $gis = array_merge($depends_on, array($this->grade_item->id));
 509          list($usql, $params) = $DB->get_in_or_equal($gis);
 510  
 511          if ($userid) {
 512              $usersql = "AND g.userid=?";
 513              $params[] = $userid;
 514  
 515          } else {
 516              $usersql = "";
 517          }
 518  
 519          $sql = "SELECT $fields
 520                    FROM {grade_grades} g, {grade_items} gi
 521                   WHERE gi.id = g.itemid AND gi.id $usql $usersql
 522                ORDER BY g.userid";
 523  
 524          // group the results by userid and aggregate the grades for this user
 525          $rs = $DB->get_recordset_sql($sql, $params);
 526          if ($rs->valid()) {
 527              $prevuser = 0;
 528              $grade_values = array();
 529              $excluded     = array();
 530              $oldgrade     = null;
 531              $grademaxoverrides = array();
 532              $grademinoverrides = array();
 533  
 534              foreach ($rs as $used) {
 535                  $grade = new grade_grade($used, false);
 536                  if (isset($items[$grade->itemid])) {
 537                      // Prevent grade item to be fetched from DB.
 538                      $grade->grade_item =& $items[$grade->itemid];
 539                  } else if ($grade->itemid == $this->grade_item->id) {
 540                      // This grade's grade item is not in $items.
 541                      $grade->grade_item =& $this->grade_item;
 542                  }
 543                  if ($grade->userid != $prevuser) {
 544                      $this->aggregate_grades($prevuser,
 545                                              $items,
 546                                              $grade_values,
 547                                              $oldgrade,
 548                                              $excluded,
 549                                              $grademinoverrides,
 550                                              $grademaxoverrides);
 551                      $prevuser = $grade->userid;
 552                      $grade_values = array();
 553                      $excluded     = array();
 554                      $oldgrade     = null;
 555                      $grademaxoverrides = array();
 556                      $grademinoverrides = array();
 557                  }
 558                  $grade_values[$grade->itemid] = $grade->finalgrade;
 559                  $grademaxoverrides[$grade->itemid] = $grade->get_grade_max();
 560                  $grademinoverrides[$grade->itemid] = $grade->get_grade_min();
 561  
 562                  if ($grade->excluded) {
 563                      $excluded[] = $grade->itemid;
 564                  }
 565  
 566                  if ($this->grade_item->id == $grade->itemid) {
 567                      $oldgrade = $grade;
 568                  }
 569  
 570                  if ($progress) {
 571                      // Incrementing the progress by nothing causes it to send an update (once per second)
 572                      // to the web browser so as to prevent the connection timing out.
 573                      $progress->increment_progress(0);
 574                  }
 575              }
 576              $this->aggregate_grades($prevuser,
 577                                      $items,
 578                                      $grade_values,
 579                                      $oldgrade,
 580                                      $excluded,
 581                                      $grademinoverrides,
 582                                      $grademaxoverrides);//the last one
 583          }
 584          $rs->close();
 585  
 586          return true;
 587      }
 588  
 589      /**
 590       * Internal function for grade category grade aggregation
 591       *
 592       * @param int    $userid The User ID
 593       * @param array  $items Grade items
 594       * @param array  $grade_values Array of grade values
 595       * @param object $oldgrade Old grade
 596       * @param array  $excluded Excluded
 597       * @param array  $grademinoverrides User specific grademin values if different to the grade_item grademin (key is itemid)
 598       * @param array  $grademaxoverrides User specific grademax values if different to the grade_item grademax (key is itemid)
 599       */
 600      private function aggregate_grades($userid,
 601                                        $items,
 602                                        $grade_values,
 603                                        $oldgrade,
 604                                        $excluded,
 605                                        $grademinoverrides,
 606                                        $grademaxoverrides) {
 607          global $CFG, $DB;
 608  
 609          // Remember these so we can set flags on them to describe how they were used in the aggregation.
 610          $novalue = array();
 611          $dropped = array();
 612          $extracredit = array();
 613          $usedweights = array();
 614  
 615          if (empty($userid)) {
 616              //ignore first call
 617              return;
 618          }
 619  
 620          if ($oldgrade) {
 621              $oldfinalgrade = $oldgrade->finalgrade;
 622              $grade = new grade_grade($oldgrade, false);
 623              $grade->grade_item =& $this->grade_item;
 624  
 625          } else {
 626              // insert final grade - it will be needed later anyway
 627              $grade = new grade_grade(array('itemid'=>$this->grade_item->id, 'userid'=>$userid), false);
 628              $grade->grade_item =& $this->grade_item;
 629              $grade->insert('system');
 630              $oldfinalgrade = null;
 631          }
 632  
 633          // no need to recalculate locked or overridden grades
 634          if ($grade->is_locked() or $grade->is_overridden()) {
 635              return;
 636          }
 637  
 638          // can not use own final category grade in calculation
 639          unset($grade_values[$this->grade_item->id]);
 640  
 641          // Make sure a grade_grade exists for every grade_item.
 642          // We need to do this so we can set the aggregationstatus
 643          // with a set_field call instead of checking if each one exists and creating/updating.
 644          if (!empty($items)) {
 645              list($ggsql, $params) = $DB->get_in_or_equal(array_keys($items), SQL_PARAMS_NAMED, 'g');
 646  
 647  
 648              $params['userid'] = $userid;
 649              $sql = "SELECT itemid
 650                        FROM {grade_grades}
 651                       WHERE itemid $ggsql AND userid = :userid";
 652              $existingitems = $DB->get_records_sql($sql, $params);
 653  
 654              $notexisting = array_diff(array_keys($items), array_keys($existingitems));
 655              foreach ($notexisting as $itemid) {
 656                  $gradeitem = $items[$itemid];
 657                  $gradegrade = new grade_grade(array('itemid' => $itemid,
 658                                                      'userid' => $userid,
 659                                                      'rawgrademin' => $gradeitem->grademin,
 660                                                      'rawgrademax' => $gradeitem->grademax), false);
 661                  $gradegrade->grade_item = $gradeitem;
 662                  $gradegrade->insert('system');
 663              }
 664          }
 665  
 666          // if no grades calculation possible or grading not allowed clear final grade
 667          if (empty($grade_values) or empty($items) or ($this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE)) {
 668              $grade->finalgrade = null;
 669  
 670              if (!is_null($oldfinalgrade)) {
 671                  $grade->timemodified = time();
 672                  $success = $grade->update('aggregation');
 673  
 674                  // If successful trigger a user_graded event.
 675                  if ($success) {
 676                      \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger();
 677                  }
 678              }
 679              $dropped = $grade_values;
 680              $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit);
 681              return;
 682          }
 683  
 684          // Normalize the grades first - all will have value 0...1
 685          // ungraded items are not used in aggregation.
 686          foreach ($grade_values as $itemid=>$v) {
 687              if (is_null($v)) {
 688                  // If null, it means no grade.
 689                  if ($this->aggregateonlygraded) {
 690                      unset($grade_values[$itemid]);
 691                      // Mark this item as "excluded empty" because it has no grade.
 692                      $novalue[$itemid] = 0;
 693                      continue;
 694                  }
 695              }
 696              if (in_array($itemid, $excluded)) {
 697                  unset($grade_values[$itemid]);
 698                  $dropped[$itemid] = 0;
 699                  continue;
 700              }
 701              // Check for user specific grade min/max overrides.
 702              $usergrademin = $items[$itemid]->grademin;
 703              $usergrademax = $items[$itemid]->grademax;
 704              if (isset($grademinoverrides[$itemid])) {
 705                  $usergrademin = $grademinoverrides[$itemid];
 706              }
 707              if (isset($grademaxoverrides[$itemid])) {
 708                  $usergrademax = $grademaxoverrides[$itemid];
 709              }
 710              if ($this->aggregation == GRADE_AGGREGATE_SUM) {
 711                  // Assume that the grademin is 0 when standardising the score, to preserve negative grades.
 712                  $grade_values[$itemid] = grade_grade::standardise_score($v, 0, $usergrademax, 0, 1);
 713              } else {
 714                  $grade_values[$itemid] = grade_grade::standardise_score($v, $usergrademin, $usergrademax, 0, 1);
 715              }
 716  
 717          }
 718  
 719          // First, check if all grades are null, because the final grade will be null
 720          // even when aggreateonlygraded is true.
 721          $allnull = true;
 722          foreach ($grade_values as $v) {
 723              if (!is_null($v)) {
 724                  $allnull = false;
 725                  break;
 726              }
 727          }
 728  
 729          // For items with no value, and not excluded - either set their grade to 0 or exclude them.
 730          foreach ($items as $itemid=>$value) {
 731              if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
 732                  if (!$this->aggregateonlygraded) {
 733                      $grade_values[$itemid] = 0;
 734                  } else {
 735                      // We are specifically marking these items as "excluded empty".
 736                      $novalue[$itemid] = 0;
 737                  }
 738              }
 739          }
 740  
 741          // limit and sort
 742          $allvalues = $grade_values;
 743          if ($this->can_apply_limit_rules()) {
 744              $this->apply_limit_rules($grade_values, $items);
 745          }
 746  
 747          $moredropped = array_diff($allvalues, $grade_values);
 748          foreach ($moredropped as $drop => $unused) {
 749              $dropped[$drop] = 0;
 750          }
 751  
 752          foreach ($grade_values as $itemid => $val) {
 753              if (self::is_extracredit_used() && ($items[$itemid]->aggregationcoef > 0)) {
 754                  $extracredit[$itemid] = 0;
 755              }
 756          }
 757  
 758          asort($grade_values, SORT_NUMERIC);
 759  
 760          // let's see we have still enough grades to do any statistics
 761          if (count($grade_values) == 0) {
 762              // not enough attempts yet
 763              $grade->finalgrade = null;
 764  
 765              if (!is_null($oldfinalgrade)) {
 766                  $grade->timemodified = time();
 767                  $success = $grade->update('aggregation');
 768  
 769                  // If successful trigger a user_graded event.
 770                  if ($success) {
 771                      \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger();
 772                  }
 773              }
 774              $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit);
 775              return;
 776          }
 777  
 778          // do the maths
 779          $result = $this->aggregate_values_and_adjust_bounds($grade_values,
 780                                                              $items,
 781                                                              $usedweights,
 782                                                              $grademinoverrides,
 783                                                              $grademaxoverrides);
 784          $agg_grade = $result['grade'];
 785  
 786          // Set the actual grademin and max to bind the grade properly.
 787          $this->grade_item->grademin = $result['grademin'];
 788          $this->grade_item->grademax = $result['grademax'];
 789  
 790          if ($this->aggregation == GRADE_AGGREGATE_SUM) {
 791              // The natural aggregation always displays the range as coming from 0 for categories.
 792              // However, when we bind the grade we allow for negative values.
 793              $result['grademin'] = 0;
 794          }
 795  
 796          if ($allnull) {
 797              $grade->finalgrade = null;
 798          } else {
 799              // Recalculate the grade back to requested range.
 800              $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $result['grademin'], $result['grademax']);
 801              $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade);
 802          }
 803  
 804          $oldrawgrademin = $grade->rawgrademin;
 805          $oldrawgrademax = $grade->rawgrademax;
 806          $grade->rawgrademin = $result['grademin'];
 807          $grade->rawgrademax = $result['grademax'];
 808  
 809          // Update in db if changed.
 810          if (grade_floats_different($grade->finalgrade, $oldfinalgrade) ||
 811                  grade_floats_different($grade->rawgrademax, $oldrawgrademax) ||
 812                  grade_floats_different($grade->rawgrademin, $oldrawgrademin)) {
 813              $grade->timemodified = time();
 814              $success = $grade->update('aggregation');
 815  
 816              // If successful trigger a user_graded event.
 817              if ($success) {
 818                  \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger();
 819              }
 820          }
 821  
 822          $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit);
 823  
 824          return;
 825      }
 826  
 827      /**
 828       * Set the flags on the grade_grade items to indicate how individual grades are used
 829       * in the aggregation.
 830       *
 831       * WARNING: This function is called a lot during gradebook recalculation, be very performance considerate.
 832       *
 833       * @param int $userid The user we have aggregated the grades for.
 834       * @param array $usedweights An array with keys for each of the grade_item columns included in the aggregation. The value are the relative weight.
 835       * @param array $novalue An array with keys for each of the grade_item columns skipped because
 836       *                       they had no value in the aggregation.
 837       * @param array $dropped An array with keys for each of the grade_item columns dropped
 838       *                       because of any drop lowest/highest settings in the aggregation.
 839       * @param array $extracredit An array with keys for each of the grade_item columns
 840       *                       considered extra credit by the aggregation.
 841       */
 842      private function set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit) {
 843          global $DB;
 844  
 845          // We want to know all current user grades so we can decide whether they need to be updated or they already contain the
 846          // expected value.
 847          $sql = "SELECT gi.id, gg.aggregationstatus, gg.aggregationweight FROM {grade_grades} gg
 848                    JOIN {grade_items} gi ON (gg.itemid = gi.id)
 849                   WHERE gg.userid = :userid";
 850          $params = array('categoryid' => $this->id, 'userid' => $userid);
 851  
 852          // These are all grade_item ids which grade_grades will NOT end up being 'unknown' (because they are not unknown or
 853          // because we will update them to something different that 'unknown').
 854          $giids = array_keys($usedweights + $novalue + $dropped + $extracredit);
 855  
 856          if ($giids) {
 857              // We include grade items that might not be in categoryid.
 858              list($itemsql, $itemlist) = $DB->get_in_or_equal($giids, SQL_PARAMS_NAMED, 'gg');
 859              $sql .= ' AND (gi.categoryid = :categoryid OR gi.id ' . $itemsql . ')';
 860              $params = $params + $itemlist;
 861          } else {
 862              $sql .= ' AND gi.categoryid = :categoryid';
 863          }
 864          $currentgrades = $DB->get_recordset_sql($sql, $params);
 865  
 866          // We will store here the grade_item ids that need to be updated on db.
 867          $toupdate = array();
 868  
 869          if ($currentgrades->valid()) {
 870  
 871              // Iterate through the user grades to see if we really need to update any of them.
 872              foreach ($currentgrades as $currentgrade) {
 873  
 874                  // Unset $usedweights that we do not need to update.
 875                  if (!empty($usedweights) && isset($usedweights[$currentgrade->id]) && $currentgrade->aggregationstatus === 'used') {
 876                      // We discard the ones that already have the contribution specified in $usedweights and are marked as 'used'.
 877                      if (grade_floats_equal($currentgrade->aggregationweight, $usedweights[$currentgrade->id])) {
 878                          unset($usedweights[$currentgrade->id]);
 879                      }
 880                      // Used weights can be present in multiple set_usedinaggregation arguments.
 881                      if (!isset($novalue[$currentgrade->id]) && !isset($dropped[$currentgrade->id]) &&
 882                              !isset($extracredit[$currentgrade->id])) {
 883                          continue;
 884                      }
 885                  }
 886  
 887                  // No value grades.
 888                  if (!empty($novalue) && isset($novalue[$currentgrade->id])) {
 889                      if ($currentgrade->aggregationstatus !== 'novalue' ||
 890                              grade_floats_different($currentgrade->aggregationweight, 0)) {
 891                          $toupdate['novalue'][] = $currentgrade->id;
 892                      }
 893                      continue;
 894                  }
 895  
 896                  // Dropped grades.
 897                  if (!empty($dropped) && isset($dropped[$currentgrade->id])) {
 898                      if ($currentgrade->aggregationstatus !== 'dropped' ||
 899                              grade_floats_different($currentgrade->aggregationweight, 0)) {
 900                          $toupdate['dropped'][] = $currentgrade->id;
 901                      }
 902                      continue;
 903                  }
 904  
 905                  // Extra credit grades.
 906                  if (!empty($extracredit) && isset($extracredit[$currentgrade->id])) {
 907  
 908                      // If this grade item is already marked as 'extra' and it already has the provided $usedweights value would be
 909                      // silly to update to 'used' to later update to 'extra'.
 910                      if (!empty($usedweights) && isset($usedweights[$currentgrade->id]) &&
 911                              grade_floats_equal($currentgrade->aggregationweight, $usedweights[$currentgrade->id])) {
 912                          unset($usedweights[$currentgrade->id]);
 913                      }
 914  
 915                      // Update the item to extra if it is not already marked as extra in the database or if the item's
 916                      // aggregationweight will be updated when going through $usedweights items.
 917                      if ($currentgrade->aggregationstatus !== 'extra' ||
 918                              (!empty($usedweights) && isset($usedweights[$currentgrade->id]))) {
 919                          $toupdate['extracredit'][] = $currentgrade->id;
 920                      }
 921                      continue;
 922                  }
 923  
 924                  // If is not in any of the above groups it should be set to 'unknown', checking that the item is not already
 925                  // unknown, if it is we don't need to update it.
 926                  if ($currentgrade->aggregationstatus !== 'unknown' || grade_floats_different($currentgrade->aggregationweight, 0)) {
 927                      $toupdate['unknown'][] = $currentgrade->id;
 928                  }
 929              }
 930              $currentgrades->close();
 931          }
 932  
 933          // Update items to 'unknown' status.
 934          if (!empty($toupdate['unknown'])) {
 935              list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['unknown'], SQL_PARAMS_NAMED, 'g');
 936  
 937              $itemlist['userid'] = $userid;
 938  
 939              $sql = "UPDATE {grade_grades}
 940                         SET aggregationstatus = 'unknown',
 941                             aggregationweight = 0
 942                       WHERE itemid $itemsql AND userid = :userid";
 943              $DB->execute($sql, $itemlist);
 944          }
 945  
 946          // Update items to 'used' status and setting the proper weight.
 947          if (!empty($usedweights)) {
 948              // The usedweights items are updated individually to record the weights.
 949              foreach ($usedweights as $gradeitemid => $contribution) {
 950                  $sql = "UPDATE {grade_grades}
 951                             SET aggregationstatus = 'used',
 952                                 aggregationweight = :contribution
 953                           WHERE itemid = :itemid AND userid = :userid";
 954  
 955                  $params = array('contribution' => $contribution, 'itemid' => $gradeitemid, 'userid' => $userid);
 956                  $DB->execute($sql, $params);
 957              }
 958          }
 959  
 960          // Update items to 'novalue' status.
 961          if (!empty($toupdate['novalue'])) {
 962              list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['novalue'], SQL_PARAMS_NAMED, 'g');
 963  
 964              $itemlist['userid'] = $userid;
 965  
 966              $sql = "UPDATE {grade_grades}
 967                         SET aggregationstatus = 'novalue',
 968                             aggregationweight = 0
 969                       WHERE itemid $itemsql AND userid = :userid";
 970  
 971              $DB->execute($sql, $itemlist);
 972          }
 973  
 974          // Update items to 'dropped' status.
 975          if (!empty($toupdate['dropped'])) {
 976              list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['dropped'], SQL_PARAMS_NAMED, 'g');
 977  
 978              $itemlist['userid'] = $userid;
 979  
 980              $sql = "UPDATE {grade_grades}
 981                         SET aggregationstatus = 'dropped',
 982                             aggregationweight = 0
 983                       WHERE itemid $itemsql AND userid = :userid";
 984  
 985              $DB->execute($sql, $itemlist);
 986          }
 987  
 988          // Update items to 'extracredit' status.
 989          if (!empty($toupdate['extracredit'])) {
 990              list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['extracredit'], SQL_PARAMS_NAMED, 'g');
 991  
 992              $itemlist['userid'] = $userid;
 993  
 994              $DB->set_field_select('grade_grades',
 995                                    'aggregationstatus',
 996                                    'extra',
 997                                    "itemid $itemsql AND userid = :userid",
 998                                    $itemlist);
 999          }
1000      }
1001  
1002      /**
1003       * Internal function that calculates the aggregated grade and new min/max for this grade category
1004       *
1005       * Must be public as it is used by grade_grade::get_hiding_affected()
1006       *
1007       * @param array $grade_values An array of values to be aggregated
1008       * @param array $items The array of grade_items
1009       * @since Moodle 2.6.5, 2.7.2
1010       * @param array & $weights If provided, will be filled with the normalized weights
1011       *                         for each grade_item as used in the aggregation.
1012       *                         Some rules for the weights are:
1013       *                         1. The weights must add up to 1 (unless there are extra credit)
1014       *                         2. The contributed points column must add up to the course
1015       *                         final grade and this column is calculated from these weights.
1016       * @param array  $grademinoverrides User specific grademin values if different to the grade_item grademin (key is itemid)
1017       * @param array  $grademaxoverrides User specific grademax values if different to the grade_item grademax (key is itemid)
1018       * @return array containing values for:
1019       *                'grade' => the new calculated grade
1020       *                'grademin' => the new calculated min grade for the category
1021       *                'grademax' => the new calculated max grade for the category
1022       */
1023      public function aggregate_values_and_adjust_bounds($grade_values,
1024                                                         $items,
1025                                                         & $weights = null,
1026                                                         $grademinoverrides = array(),
1027                                                         $grademaxoverrides = array()) {
1028          global $CFG;
1029  
1030          $category_item = $this->load_grade_item();
1031          $grademin = $category_item->grademin;
1032          $grademax = $category_item->grademax;
1033  
1034          switch ($this->aggregation) {
1035  
1036              case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies
1037                  $num = count($grade_values);
1038                  $grades = array_values($grade_values);
1039  
1040                  // The median gets 100% - others get 0.
1041                  if ($weights !== null && $num > 0) {
1042                      $count = 0;
1043                      foreach ($grade_values as $itemid=>$grade_value) {
1044                          if (($num % 2 == 0) && ($count == intval($num/2)-1 || $count == intval($num/2))) {
1045                              $weights[$itemid] = 0.5;
1046                          } else if (($num % 2 != 0) && ($count == intval(($num/2)-0.5))) {
1047                              $weights[$itemid] = 1.0;
1048                          } else {
1049                              $weights[$itemid] = 0;
1050                          }
1051                          $count++;
1052                      }
1053                  }
1054                  if ($num % 2 == 0) {
1055                      $agg_grade = ($grades[intval($num/2)-1] + $grades[intval($num/2)]) / 2;
1056                  } else {
1057                      $agg_grade = $grades[intval(($num/2)-0.5)];
1058                  }
1059  
1060                  break;
1061  
1062              case GRADE_AGGREGATE_MIN:
1063                  $agg_grade = reset($grade_values);
1064                  // Record the weights as used.
1065                  if ($weights !== null) {
1066                      foreach ($grade_values as $itemid=>$grade_value) {
1067                          $weights[$itemid] = 0;
1068                      }
1069                  }
1070                  // Set the first item to 1.
1071                  $itemids = array_keys($grade_values);
1072                  $weights[reset($itemids)] = 1;
1073                  break;
1074  
1075              case GRADE_AGGREGATE_MAX:
1076                  // Record the weights as used.
1077                  if ($weights !== null) {
1078                      foreach ($grade_values as $itemid=>$grade_value) {
1079                          $weights[$itemid] = 0;
1080                      }
1081                  }
1082                  // Set the last item to 1.
1083                  $itemids = array_keys($grade_values);
1084                  $weights[end($itemids)] = 1;
1085                  $agg_grade = end($grade_values);
1086                  break;
1087  
1088              case GRADE_AGGREGATE_MODE:       // the most common value
1089                  // array_count_values only counts INT and STRING, so if grades are floats we must convert them to string
1090                  $converted_grade_values = array();
1091  
1092                  foreach ($grade_values as $k => $gv) {
1093  
1094                      if (!is_int($gv) && !is_string($gv)) {
1095                          $converted_grade_values[$k] = (string) $gv;
1096  
1097                      } else {
1098                          $converted_grade_values[$k] = $gv;
1099                      }
1100                      if ($weights !== null) {
1101                          $weights[$k] = 0;
1102                      }
1103                  }
1104  
1105                  $freq = array_count_values($converted_grade_values);
1106                  arsort($freq);                      // sort by frequency keeping keys
1107                  $top = reset($freq);               // highest frequency count
1108                  $modes = array_keys($freq, $top);  // search for all modes (have the same highest count)
1109                  rsort($modes, SORT_NUMERIC);       // get highest mode
1110                  $agg_grade = reset($modes);
1111                  // Record the weights as used.
1112                  if ($weights !== null && $top > 0) {
1113                      foreach ($grade_values as $k => $gv) {
1114                          if ($gv == $agg_grade) {
1115                              $weights[$k] = 1.0 / $top;
1116                          }
1117                      }
1118                  }
1119                  break;
1120  
1121              case GRADE_AGGREGATE_WEIGHTED_MEAN: // Weighted average of all existing final grades, weight specified in coef
1122                  $weightsum = 0;
1123                  $sum       = 0;
1124  
1125                  foreach ($grade_values as $itemid=>$grade_value) {
1126                      if ($weights !== null) {
1127                          $weights[$itemid] = $items[$itemid]->aggregationcoef;
1128                      }
1129                      if ($items[$itemid]->aggregationcoef <= 0) {
1130                          continue;
1131                      }
1132                      $weightsum += $items[$itemid]->aggregationcoef;
1133                      $sum       += $items[$itemid]->aggregationcoef * $grade_value;
1134                  }
1135                  if ($weightsum == 0) {
1136                      $agg_grade = null;
1137  
1138                  } else {
1139                      $agg_grade = $sum / $weightsum;
1140                      if ($weights !== null) {
1141                          // Normalise the weights.
1142                          foreach ($weights as $itemid => $weight) {
1143                              $weights[$itemid] = $weight / $weightsum;
1144                          }
1145                      }
1146  
1147                  }
1148                  break;
1149  
1150              case GRADE_AGGREGATE_WEIGHTED_MEAN2:
1151                  // Weighted average of all existing final grades with optional extra credit flag,
1152                  // weight is the range of grade (usually grademax)
1153                  $this->load_grade_item();
1154                  $weightsum = 0;
1155                  $sum       = null;
1156  
1157                  foreach ($grade_values as $itemid=>$grade_value) {
1158                      if ($items[$itemid]->aggregationcoef > 0) {
1159                          continue;
1160                      }
1161  
1162                      $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
1163                      if ($weight <= 0) {
1164                          continue;
1165                      }
1166  
1167                      $weightsum += $weight;
1168                      $sum += $weight * $grade_value;
1169                  }
1170  
1171                  // Handle the extra credit items separately to calculate their weight accurately.
1172                  foreach ($grade_values as $itemid => $grade_value) {
1173                      if ($items[$itemid]->aggregationcoef <= 0) {
1174                          continue;
1175                      }
1176  
1177                      $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
1178                      if ($weight <= 0) {
1179                          $weights[$itemid] = 0;
1180                          continue;
1181                      }
1182  
1183                      $oldsum = $sum;
1184                      $weightedgrade = $weight * $grade_value;
1185                      $sum += $weightedgrade;
1186  
1187                      if ($weights !== null) {
1188                          if ($weightsum <= 0) {
1189                              $weights[$itemid] = 0;
1190                              continue;
1191                          }
1192  
1193                          $oldgrade = $oldsum / $weightsum;
1194                          $grade = $sum / $weightsum;
1195                          $normoldgrade = grade_grade::standardise_score($oldgrade, 0, 1, $grademin, $grademax);
1196                          $normgrade = grade_grade::standardise_score($grade, 0, 1, $grademin, $grademax);
1197                          $boundedoldgrade = $this->grade_item->bounded_grade($normoldgrade);
1198                          $boundedgrade = $this->grade_item->bounded_grade($normgrade);
1199  
1200                          if ($boundedgrade - $boundedoldgrade <= 0) {
1201                              // Nothing new was added to the grade.
1202                              $weights[$itemid] = 0;
1203                          } else if ($boundedgrade < $normgrade) {
1204                              // The grade has been bounded, the extra credit item needs to have a different weight.
1205                              $gradediff = $boundedgrade - $normoldgrade;
1206                              $gradediffnorm = grade_grade::standardise_score($gradediff, $grademin, $grademax, 0, 1);
1207                              $weights[$itemid] = $gradediffnorm / $grade_value;
1208                          } else {
1209                              // Default weighting.
1210                              $weights[$itemid] = $weight / $weightsum;
1211                          }
1212                      }
1213                  }
1214  
1215                  if ($weightsum == 0) {
1216                      $agg_grade = $sum; // only extra credits
1217  
1218                  } else {
1219                      $agg_grade = $sum / $weightsum;
1220                  }
1221  
1222                  // Record the weights as used.
1223                  if ($weights !== null) {
1224                      foreach ($grade_values as $itemid=>$grade_value) {
1225                          if ($items[$itemid]->aggregationcoef > 0) {
1226                              // Ignore extra credit items, the weights have already been computed.
1227                              continue;
1228                          }
1229                          if ($weightsum > 0) {
1230                              $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
1231                              $weights[$itemid] = $weight / $weightsum;
1232                          } else {
1233                              $weights[$itemid] = 0;
1234                          }
1235                      }
1236                  }
1237                  break;
1238  
1239              case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average
1240                  $this->load_grade_item();
1241                  $num = 0;
1242                  $sum = null;
1243  
1244                  foreach ($grade_values as $itemid=>$grade_value) {
1245                      if ($items[$itemid]->aggregationcoef == 0) {
1246                          $num += 1;
1247                          $sum += $grade_value;
1248                          if ($weights !== null) {
1249                              $weights[$itemid] = 1;
1250                          }
1251                      }
1252                  }
1253  
1254                  // Treating the extra credit items separately to get a chance to calculate their effective weights.
1255                  foreach ($grade_values as $itemid=>$grade_value) {
1256                      if ($items[$itemid]->aggregationcoef > 0) {
1257                          $oldsum = $sum;
1258                          $sum += $items[$itemid]->aggregationcoef * $grade_value;
1259  
1260                          if ($weights !== null) {
1261                              if ($num <= 0) {
1262                                  // The category only contains extra credit items, not setting the weight.
1263                                  continue;
1264                              }
1265  
1266                              $oldgrade = $oldsum / $num;
1267                              $grade = $sum / $num;
1268                              $normoldgrade = grade_grade::standardise_score($oldgrade, 0, 1, $grademin, $grademax);
1269                              $normgrade = grade_grade::standardise_score($grade, 0, 1, $grademin, $grademax);
1270                              $boundedoldgrade = $this->grade_item->bounded_grade($normoldgrade);
1271                              $boundedgrade = $this->grade_item->bounded_grade($normgrade);
1272  
1273                              if ($boundedgrade - $boundedoldgrade <= 0) {
1274                                  // Nothing new was added to the grade.
1275                                  $weights[$itemid] = 0;
1276                              } else if ($boundedgrade < $normgrade) {
1277                                  // The grade has been bounded, the extra credit item needs to have a different weight.
1278                                  $gradediff = $boundedgrade - $normoldgrade;
1279                                  $gradediffnorm = grade_grade::standardise_score($gradediff, $grademin, $grademax, 0, 1);
1280                                  $weights[$itemid] = $gradediffnorm / $grade_value;
1281                              } else {
1282                                  // Default weighting.
1283                                  $weights[$itemid] = 1.0 / $num;
1284                              }
1285                          }
1286                      }
1287                  }
1288  
1289                  if ($weights !== null && $num > 0) {
1290                      foreach ($grade_values as $itemid=>$grade_value) {
1291                          if ($items[$itemid]->aggregationcoef > 0) {
1292                              // Extra credit weights were already calculated.
1293                              continue;
1294                          }
1295                          if ($weights[$itemid]) {
1296                              $weights[$itemid] = 1.0 / $num;
1297                          }
1298                      }
1299                  }
1300  
1301                  if ($num == 0) {
1302                      $agg_grade = $sum; // only extra credits or wrong coefs
1303  
1304                  } else {
1305                      $agg_grade = $sum / $num;
1306                  }
1307  
1308                  break;
1309  
1310              case GRADE_AGGREGATE_SUM:    // Add up all the items.
1311                  $this->load_grade_item();
1312                  $num = count($grade_values);
1313                  $sum = 0;
1314  
1315                  // This setting indicates if we should use algorithm prior to MDL-49257 fix for calculating extra credit weights.
1316                  // Even though old algorith has bugs in it, we need to preserve existing grades.
1317                  $gradebookcalculationfreeze = 'gradebook_calculations_freeze_' . $this->courseid;
1318                  $oldextracreditcalculation = isset($CFG->$gradebookcalculationfreeze)
1319                          && ($CFG->$gradebookcalculationfreeze <= 20150619);
1320  
1321                  $sumweights = 0;
1322                  $grademin = 0;
1323                  $grademax = 0;
1324                  $extracredititems = array();
1325                  foreach ($grade_values as $itemid => $gradevalue) {
1326                      // We need to check if the grademax/min was adjusted per user because of excluded items.
1327                      $usergrademin = $items[$itemid]->grademin;
1328                      $usergrademax = $items[$itemid]->grademax;
1329                      if (isset($grademinoverrides[$itemid])) {
1330                          $usergrademin = $grademinoverrides[$itemid];
1331                      }
1332                      if (isset($grademaxoverrides[$itemid])) {
1333                          $usergrademax = $grademaxoverrides[$itemid];
1334                      }
1335  
1336                      // Keep track of the extra credit items, we will need them later on.
1337                      if ($items[$itemid]->aggregationcoef > 0) {
1338                          $extracredititems[$itemid] = $items[$itemid];
1339                      }
1340  
1341                      // Ignore extra credit and items with a weight of 0.
1342                      if (!isset($extracredititems[$itemid]) && $items[$itemid]->aggregationcoef2 > 0) {
1343                          $grademin += $usergrademin;
1344                          $grademax += $usergrademax;
1345                          $sumweights += $items[$itemid]->aggregationcoef2;
1346                      }
1347                  }
1348                  $userweights = array();
1349                  $totaloverriddenweight = 0;
1350                  $totaloverriddengrademax = 0;
1351                  // We first need to rescale all manually assigned weights down by the
1352                  // percentage of weights missing from the category.
1353                  foreach ($grade_values as $itemid => $gradevalue) {
1354                      if ($items[$itemid]->weightoverride) {
1355                          if ($items[$itemid]->aggregationcoef2 <= 0) {
1356                              // Records the weight of 0 and continue.
1357                              $userweights[$itemid] = 0;
1358                              continue;
1359                          }
1360                          $userweights[$itemid] = $sumweights ? ($items[$itemid]->aggregationcoef2 / $sumweights) : 0;
1361                          if (!$oldextracreditcalculation && isset($extracredititems[$itemid])) {
1362                              // Extra credit items do not affect totals.
1363                              continue;
1364                          }
1365                          $totaloverriddenweight += $userweights[$itemid];
1366                          $usergrademax = $items[$itemid]->grademax;
1367                          if (isset($grademaxoverrides[$itemid])) {
1368                              $usergrademax = $grademaxoverrides[$itemid];
1369                          }
1370                          $totaloverriddengrademax += $usergrademax;
1371                      }
1372                  }
1373                  $nonoverriddenpoints = $grademax - $totaloverriddengrademax;
1374  
1375                  // Then we need to recalculate the automatic weights except for extra credit items.
1376                  foreach ($grade_values as $itemid => $gradevalue) {
1377                      if (!$items[$itemid]->weightoverride && ($oldextracreditcalculation || !isset($extracredititems[$itemid]))) {
1378                          $usergrademax = $items[$itemid]->grademax;
1379                          if (isset($grademaxoverrides[$itemid])) {
1380                              $usergrademax = $grademaxoverrides[$itemid];
1381                          }
1382                          if ($nonoverriddenpoints > 0) {
1383                              $userweights[$itemid] = ($usergrademax/$nonoverriddenpoints) * (1 - $totaloverriddenweight);
1384                          } else {
1385                              $userweights[$itemid] = 0;
1386                              if ($items[$itemid]->aggregationcoef2 > 0) {
1387                                  // Items with a weight of 0 should not count for the grade max,
1388                                  // though this only applies if the weight was changed to 0.
1389                                  $grademax -= $usergrademax;
1390                              }
1391                          }
1392                      }
1393                  }
1394  
1395                  // Now when we finally know the grademax we can adjust the automatic weights of extra credit items.
1396                  if (!$oldextracreditcalculation) {
1397                      foreach ($grade_values as $itemid => $gradevalue) {
1398                          if (!$items[$itemid]->weightoverride && isset($extracredititems[$itemid])) {
1399                              $usergrademax = $items[$itemid]->grademax;
1400                              if (isset($grademaxoverrides[$itemid])) {
1401                                  $usergrademax = $grademaxoverrides[$itemid];
1402                              }
1403                              $userweights[$itemid] = $grademax ? ($usergrademax / $grademax) : 0;
1404                          }
1405                      }
1406                  }
1407  
1408                  // We can use our freshly corrected weights below.
1409                  foreach ($grade_values as $itemid => $gradevalue) {
1410                      if (isset($extracredititems[$itemid])) {
1411                          // We skip the extra credit items first.
1412                          continue;
1413                      }
1414                      $sum += $gradevalue * $userweights[$itemid] * $grademax;
1415                      if ($weights !== null) {
1416                          $weights[$itemid] = $userweights[$itemid];
1417                      }
1418                  }
1419  
1420                  // No we proceed with the extra credit items. They might have a different final
1421                  // weight in case the final grade was bounded. So we need to treat them different.
1422                  // Also, as we need to use the bounded_grade() method, we have to inject the
1423                  // right values there, and restore them afterwards.
1424                  $oldgrademax = $this->grade_item->grademax;
1425                  $oldgrademin = $this->grade_item->grademin;
1426                  foreach ($grade_values as $itemid => $gradevalue) {
1427                      if (!isset($extracredititems[$itemid])) {
1428                          continue;
1429                      }
1430                      $oldsum = $sum;
1431                      $weightedgrade = $gradevalue * $userweights[$itemid] * $grademax;
1432                      $sum += $weightedgrade;
1433  
1434                      // Only go through this when we need to record the weights.
1435                      if ($weights !== null) {
1436                          if ($grademax <= 0) {
1437                              // There are only extra credit items in this category,
1438                              // all the weights should be accurate (and be 0).
1439                              $weights[$itemid] = $userweights[$itemid];
1440                              continue;
1441                          }
1442  
1443                          $oldfinalgrade = $this->grade_item->bounded_grade($oldsum);
1444                          $newfinalgrade = $this->grade_item->bounded_grade($sum);
1445                          $finalgradediff = $newfinalgrade - $oldfinalgrade;
1446                          if ($finalgradediff <= 0) {
1447                              // This item did not contribute to the category total at all.
1448                              $weights[$itemid] = 0;
1449                          } else if ($finalgradediff < $weightedgrade) {
1450                              // The weight needs to be adjusted because only a portion of the
1451                              // extra credit item contributed to the category total.
1452                              $weights[$itemid] = $finalgradediff / ($gradevalue * $grademax);
1453                          } else {
1454                              // The weight was accurate.
1455                              $weights[$itemid] = $userweights[$itemid];
1456                          }
1457                      }
1458                  }
1459                  $this->grade_item->grademax = $oldgrademax;
1460                  $this->grade_item->grademin = $oldgrademin;
1461  
1462                  if ($grademax > 0) {
1463                      $agg_grade = $sum / $grademax; // Re-normalize score.
1464                  } else {
1465                      // Every item in the category is extra credit.
1466                      $agg_grade = $sum;
1467                      $grademax = $sum;
1468                  }
1469  
1470                  break;
1471  
1472              case GRADE_AGGREGATE_MEAN:    // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum)
1473              default:
1474                  $num = count($grade_values);
1475                  $sum = array_sum($grade_values);
1476                  $agg_grade = $sum / $num;
1477                  // Record the weights evenly.
1478                  if ($weights !== null && $num > 0) {
1479                      foreach ($grade_values as $itemid=>$grade_value) {
1480                          $weights[$itemid] = 1.0 / $num;
1481                      }
1482                  }
1483                  break;
1484          }
1485  
1486          return array('grade' => $agg_grade, 'grademin' => $grademin, 'grademax' => $grademax);
1487      }
1488  
1489      /**
1490       * Internal function that calculates the aggregated grade for this grade category
1491       *
1492       * Must be public as it is used by grade_grade::get_hiding_affected()
1493       *
1494       * @deprecated since Moodle 2.8
1495       * @param array $grade_values An array of values to be aggregated
1496       * @param array $items The array of grade_items
1497       * @return float The aggregate grade for this grade category
1498       */
1499      public function aggregate_values($grade_values, $items) {
1500          debugging('grade_category::aggregate_values() is deprecated.
1501                     Call grade_category::aggregate_values_and_adjust_bounds() instead.', DEBUG_DEVELOPER);
1502          $result = $this->aggregate_values_and_adjust_bounds($grade_values, $items);
1503          return $result['grade'];
1504      }
1505  
1506      /**
1507       * Some aggregation types may need to update their max grade.
1508       *
1509       * This must be executed after updating the weights as it relies on them.
1510       *
1511       * @return void
1512       */
1513      private function auto_update_max() {
1514          global $CFG, $DB;
1515          if ($this->aggregation != GRADE_AGGREGATE_SUM) {
1516              // not needed at all
1517              return;
1518          }
1519  
1520          // Find grade items of immediate children (category or grade items) and force site settings.
1521          $this->load_grade_item();
1522          $depends_on = $this->grade_item->depends_on();
1523  
1524          // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
1525          // wish to update the grades.
1526          $gradebookcalculationfreeze = 'gradebook_calculations_freeze_' . $this->courseid;
1527          $oldextracreditcalculation = isset($CFG->$gradebookcalculationfreeze) && ($CFG->$gradebookcalculationfreeze <= 20150627);
1528          // Only run if the gradebook isn't frozen.
1529          if (!$oldextracreditcalculation) {
1530              // Don't automatically update the max for calculated items.
1531              if ($this->grade_item->is_calculated()) {
1532                  return;
1533              }
1534          }
1535  
1536          $items = false;
1537          if (!empty($depends_on)) {
1538              list($usql, $params) = $DB->get_in_or_equal($depends_on);
1539              $sql = "SELECT *
1540                        FROM {grade_items}
1541                       WHERE id $usql";
1542              $items = $DB->get_records_sql($sql, $params);
1543          }
1544  
1545          if (!$items) {
1546  
1547              if ($this->grade_item->grademax != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
1548                  $this->grade_item->grademax  = 0;
1549                  $this->grade_item->grademin  = 0;
1550                  $this->grade_item->gradetype = GRADE_TYPE_VALUE;
1551                  $this->grade_item->update('aggregation');
1552              }
1553              return;
1554          }
1555  
1556          //find max grade possible
1557          $maxes = array();
1558  
1559          foreach ($items as $item) {
1560  
1561              if ($item->aggregationcoef > 0) {
1562                  // extra credit from this activity - does not affect total
1563                  continue;
1564              } else if ($item->aggregationcoef2 <= 0) {
1565                  // Items with a weight of 0 do not affect the total.
1566                  continue;
1567              }
1568  
1569              if ($item->gradetype == GRADE_TYPE_VALUE) {
1570                  $maxes[$item->id] = $item->grademax;
1571  
1572              } else if ($item->gradetype == GRADE_TYPE_SCALE) {
1573                  $maxes[$item->id] = $item->grademax; // 0 = nograde, 1 = first scale item, 2 = second scale item
1574              }
1575          }
1576  
1577          if ($this->can_apply_limit_rules()) {
1578              // Apply droplow and keephigh.
1579              $this->apply_limit_rules($maxes, $items);
1580          }
1581          $max = array_sum($maxes);
1582  
1583          // update db if anything changed
1584          if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
1585              $this->grade_item->grademax  = $max;
1586              $this->grade_item->grademin  = 0;
1587              $this->grade_item->gradetype = GRADE_TYPE_VALUE;
1588              $this->grade_item->update('aggregation');
1589          }
1590      }
1591  
1592      /**
1593       * Recalculate the weights of the grade items in this category.
1594       *
1595       * The category total is not updated here, a further call to
1596       * {@link self::auto_update_max()} is required.
1597       *
1598       * @return void
1599       */
1600      private function auto_update_weights() {
1601          global $CFG;
1602          if ($this->aggregation != GRADE_AGGREGATE_SUM) {
1603              // This is only required if we are using natural weights.
1604              return;
1605          }
1606          $children = $this->get_children();
1607  
1608          $gradeitem = null;
1609  
1610          // Calculate the sum of the grademax's of all the items within this category.
1611          $totalnonoverriddengrademax = 0;
1612          $totalgrademax = 0;
1613  
1614          // Out of 1, how much weight has been manually overriden by a user?
1615          $totaloverriddenweight  = 0;
1616          $totaloverriddengrademax  = 0;
1617  
1618          // Has every assessment in this category been overridden?
1619          $automaticgradeitemspresent = false;
1620          // Does the grade item require normalising?
1621          $requiresnormalising = false;
1622  
1623          // This array keeps track of the id and weight of every grade item that has been overridden.
1624          $overridearray = array();
1625          foreach ($children as $sortorder => $child) {
1626              $gradeitem = null;
1627  
1628              if ($child['type'] == 'item') {
1629                  $gradeitem = $child['object'];
1630              } else if ($child['type'] == 'category') {
1631                  $gradeitem = $child['object']->load_grade_item();
1632              }
1633  
1634              if ($gradeitem->gradetype == GRADE_TYPE_NONE || $gradeitem->gradetype == GRADE_TYPE_TEXT) {
1635                  // Text items and none items do not have a weight.
1636                  continue;
1637              } else if (!$this->aggregateoutcomes && $gradeitem->is_outcome_item()) {
1638                  // We will not aggregate outcome items, so we can ignore them.
1639                  continue;
1640              } else if (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE) {
1641                  // The scales are not included in the aggregation, ignore them.
1642                  continue;
1643              }
1644  
1645              // Record the ID and the weight for this grade item.
1646              $overridearray[$gradeitem->id] = array();
1647              $overridearray[$gradeitem->id]['extracredit'] = intval($gradeitem->aggregationcoef);
1648              $overridearray[$gradeitem->id]['weight'] = $gradeitem->aggregationcoef2;
1649              $overridearray[$gradeitem->id]['weightoverride'] = intval($gradeitem->weightoverride);
1650              // If this item has had its weight overridden then set the flag to true, but
1651              // only if all previous items were also overridden. Note that extra credit items
1652              // are counted as overridden grade items.
1653              if (!$gradeitem->weightoverride && $gradeitem->aggregationcoef == 0) {
1654                  $automaticgradeitemspresent = true;
1655              }
1656  
1657              if ($gradeitem->aggregationcoef > 0) {
1658                  // An extra credit grade item doesn't contribute to $totaloverriddengrademax.
1659                  continue;
1660              } else if ($gradeitem->weightoverride > 0 && $gradeitem->aggregationcoef2 <= 0) {
1661                  // An overriden item that defines a weight of 0 does not contribute to $totaloverriddengrademax.
1662                  continue;
1663              }
1664  
1665              $totalgrademax += $gradeitem->grademax;
1666              if ($gradeitem->weightoverride > 0) {
1667                  $totaloverriddenweight += $gradeitem->aggregationcoef2;
1668                  $totaloverriddengrademax += $gradeitem->grademax;
1669              }
1670          }
1671  
1672          // Initialise this variable (used to keep track of the weight override total).
1673          $normalisetotal = 0;
1674          // Keep a record of how much the override total is to see if it is above 100. It it is then we need to set the
1675          // other weights to zero and normalise the others.
1676          $overriddentotal = 0;
1677          // If the overridden weight total is higher than 1 then set the other untouched weights to zero.
1678          $setotherweightstozero = false;
1679          // Total up all of the weights.
1680          foreach ($overridearray as $gradeitemdetail) {
1681              // If the grade item has extra credit, then don't add it to the normalisetotal.
1682              if (!$gradeitemdetail['extracredit']) {
1683                  $normalisetotal += $gradeitemdetail['weight'];
1684              }
1685              // The overridden total comprises of items that are set as overridden, that aren't extra credit and have a value
1686              // greater than zero.
1687              if ($gradeitemdetail['weightoverride'] && !$gradeitemdetail['extracredit'] && $gradeitemdetail['weight'] > 0) {
1688                  // Add overriden weights up to see if they are greater than 1.
1689                  $overriddentotal += $gradeitemdetail['weight'];
1690              }
1691          }
1692          if ($overriddentotal > 1) {
1693              // Make sure that this catergory of weights gets normalised.
1694              $requiresnormalising = true;
1695              // The normalised weights are only the overridden weights, so we just use the total of those.
1696              $normalisetotal = $overriddentotal;
1697          }
1698  
1699          $totalnonoverriddengrademax = $totalgrademax - $totaloverriddengrademax;
1700  
1701          // This setting indicates if we should use algorithm prior to MDL-49257 fix for calculating extra credit weights.
1702          // Even though old algorith has bugs in it, we need to preserve existing grades.
1703          $gradebookcalculationfreeze = (int)get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);
1704          $oldextracreditcalculation = $gradebookcalculationfreeze && ($gradebookcalculationfreeze <= 20150619);
1705  
1706          reset($children);
1707          foreach ($children as $sortorder => $child) {
1708              $gradeitem = null;
1709  
1710              if ($child['type'] == 'item') {
1711                  $gradeitem = $child['object'];
1712              } else if ($child['type'] == 'category') {
1713                  $gradeitem = $child['object']->load_grade_item();
1714              }
1715  
1716              if ($gradeitem->gradetype == GRADE_TYPE_NONE || $gradeitem->gradetype == GRADE_TYPE_TEXT) {
1717                  // Text items and none items do not have a weight, no need to set their weight to
1718                  // zero as they must never be used during aggregation.
1719                  continue;
1720              } else if (!$this->aggregateoutcomes && $gradeitem->is_outcome_item()) {
1721                  // We will not aggregate outcome items, so we can ignore updating their weights.
1722                  continue;
1723              } else if (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE) {
1724                  // We will not aggregate the scales, so we can ignore upating their weights.
1725                  continue;
1726              } else if (!$oldextracreditcalculation && $gradeitem->aggregationcoef > 0 && $gradeitem->weightoverride) {
1727                  // For an item with extra credit ignore other weigths and overrides but do not change anything at all
1728                  // if it's weight was already overridden.
1729                  continue;
1730              }
1731  
1732              // Store the previous value here, no need to update if it is the same value.
1733              $prevaggregationcoef2 = $gradeitem->aggregationcoef2;
1734  
1735              if (!$oldextracreditcalculation && $gradeitem->aggregationcoef > 0 && !$gradeitem->weightoverride) {
1736                  // For an item with extra credit ignore other weigths and overrides.
1737                  $gradeitem->aggregationcoef2 = $totalgrademax ? ($gradeitem->grademax / $totalgrademax) : 0;
1738  
1739              } else if (!$gradeitem->weightoverride) {
1740                  // Calculations with a grade maximum of zero will cause problems. Just set the weight to zero.
1741                  if ($totaloverriddenweight >= 1 || $totalnonoverriddengrademax == 0 || $gradeitem->grademax == 0) {
1742                      // There is no more weight to distribute.
1743                      $gradeitem->aggregationcoef2 = 0;
1744                  } else {
1745                      // Calculate this item's weight as a percentage of the non-overridden total grade maxes
1746                      // then convert it to a proportion of the available non-overriden weight.
1747                      $gradeitem->aggregationcoef2 = ($gradeitem->grademax/$totalnonoverriddengrademax) *
1748                              (1 - $totaloverriddenweight);
1749                  }
1750  
1751              } else if ((!$automaticgradeitemspresent && $normalisetotal != 1) || ($requiresnormalising)
1752                      || $overridearray[$gradeitem->id]['weight'] < 0) {
1753                  // Just divide the overriden weight for this item against the total weight override of all
1754                  // items in this category.
1755                  if ($normalisetotal == 0 || $overridearray[$gradeitem->id]['weight'] < 0) {
1756                      // If the normalised total equals zero, or the weight value is less than zero,
1757                      // set the weight for the grade item to zero.
1758                      $gradeitem->aggregationcoef2 = 0;
1759                  } else {
1760                      $gradeitem->aggregationcoef2 = $overridearray[$gradeitem->id]['weight'] / $normalisetotal;
1761                  }
1762              }
1763  
1764              if (grade_floatval($prevaggregationcoef2) !== grade_floatval($gradeitem->aggregationcoef2)) {
1765                  // Update the grade item to reflect these changes.
1766                  $gradeitem->update();
1767              }
1768          }
1769      }
1770  
1771      /**
1772       * Given an array of grade values (numerical indices) applies droplow or keephigh rules to limit the final array.
1773       *
1774       * @param array $grade_values itemid=>$grade_value float
1775       * @param array $items grade item objects
1776       * @return array Limited grades.
1777       */
1778      public function apply_limit_rules(&$grade_values, $items) {
1779          $extraused = $this->is_extracredit_used();
1780  
1781          if (!empty($this->droplow)) {
1782              asort($grade_values, SORT_NUMERIC);
1783              $dropped = 0;
1784  
1785              // If we have fewer grade items available to drop than $this->droplow, use this flag to escape the loop
1786              // May occur because of "extra credit" or if droplow is higher than the number of grade items
1787              $droppedsomething = true;
1788  
1789              while ($dropped < $this->droplow && $droppedsomething) {
1790                  $droppedsomething = false;
1791  
1792                  $grade_keys = array_keys($grade_values);
1793                  $gradekeycount = count($grade_keys);
1794  
1795                  if ($gradekeycount === 0) {
1796                      //We've dropped all grade items
1797                      break;
1798                  }
1799  
1800                  $originalindex = $founditemid = $foundmax = null;
1801  
1802                  // Find the first remaining grade item that is available to be dropped
1803                  foreach ($grade_keys as $gradekeyindex=>$gradekey) {
1804                      if (!$extraused || $items[$gradekey]->aggregationcoef <= 0) {
1805                          // Found a non-extra credit grade item that is eligible to be dropped
1806                          $originalindex = $gradekeyindex;
1807                          $founditemid = $grade_keys[$originalindex];
1808                          $foundmax = $items[$founditemid]->grademax;
1809                          break;
1810                      }
1811                  }
1812  
1813                  if (empty($founditemid)) {
1814                      // No grade items available to drop
1815                      break;
1816                  }
1817  
1818                  // Now iterate over the remaining grade items
1819                  // We're looking for other grade items with the same grade value but a higher grademax
1820                  $i = 1;
1821                  while ($originalindex + $i < $gradekeycount) {
1822  
1823                      $possibleitemid = $grade_keys[$originalindex+$i];
1824                      $i++;
1825  
1826                      if ($grade_values[$founditemid] != $grade_values[$possibleitemid]) {
1827                          // The next grade item has a different grade value. Stop looking.
1828                          break;
1829                      }
1830  
1831                      if ($extraused && $items[$possibleitemid]->aggregationcoef > 0) {
1832                          // Don't drop extra credit grade items. Continue the search.
1833                          continue;
1834                      }
1835  
1836                      if ($foundmax < $items[$possibleitemid]->grademax) {
1837                          // Found a grade item with the same grade value and a higher grademax
1838                          $foundmax = $items[$possibleitemid]->grademax;
1839                          $founditemid = $possibleitemid;
1840                          // Continue searching to see if there is an even higher grademax
1841                      }
1842                  }
1843  
1844                  // Now drop whatever grade item we have found
1845                  unset($grade_values[$founditemid]);
1846                  $dropped++;
1847                  $droppedsomething = true;
1848              }
1849  
1850          } else if (!empty($this->keephigh)) {
1851              arsort($grade_values, SORT_NUMERIC);
1852              $kept = 0;
1853  
1854              foreach ($grade_values as $itemid=>$value) {
1855  
1856                  if ($extraused and $items[$itemid]->aggregationcoef > 0) {
1857                      // we keep all extra credits
1858  
1859                  } else if ($kept < $this->keephigh) {
1860                      $kept++;
1861  
1862                  } else {
1863                      unset($grade_values[$itemid]);
1864                  }
1865              }
1866          }
1867      }
1868  
1869      /**
1870       * Returns whether or not we can apply the limit rules.
1871       *
1872       * There are cases where drop lowest or keep highest should not be used
1873       * at all. This method will determine whether or not this logic can be
1874       * applied considering the current setup of the category.
1875       *
1876       * @return bool
1877       */
1878      public function can_apply_limit_rules() {
1879          if ($this->canapplylimitrules !== null) {
1880              return $this->canapplylimitrules;
1881          }
1882  
1883          // Set it to be supported by default.
1884          $this->canapplylimitrules = true;
1885  
1886          // Natural aggregation.
1887          if ($this->aggregation == GRADE_AGGREGATE_SUM) {
1888              $canapply = true;
1889  
1890              // Check until one child breaks the rules.
1891              $gradeitems = $this->get_children();
1892              $validitems = 0;
1893              $lastweight = null;
1894              $lastmaxgrade = null;
1895              foreach ($gradeitems as $gradeitem) {
1896                  $gi = $gradeitem['object'];
1897  
1898                  if ($gradeitem['type'] == 'category') {
1899                      // Sub categories are not allowed because they can have dynamic weights/maxgrades.
1900                      $canapply = false;
1901                      break;
1902                  }
1903  
1904                  if ($gi->aggregationcoef > 0) {
1905                      // Extra credit items are not allowed.
1906                      $canapply = false;
1907                      break;
1908                  }
1909  
1910                  if ($lastweight !== null && $lastweight != $gi->aggregationcoef2) {
1911                      // One of the weight differs from another item.
1912                      $canapply = false;
1913                      break;
1914                  }
1915  
1916                  if ($lastmaxgrade !== null && $lastmaxgrade != $gi->grademax) {
1917                      // One of the max grade differ from another item. This is not allowed for now
1918                      // because we could be end up with different max grade between users for this category.
1919                      $canapply = false;
1920                      break;
1921                  }
1922  
1923                  $lastweight = $gi->aggregationcoef2;
1924                  $lastmaxgrade = $gi->grademax;
1925              }
1926  
1927              $this->canapplylimitrules = $canapply;
1928          }
1929  
1930          return $this->canapplylimitrules;
1931      }
1932  
1933      /**
1934       * Returns true if category uses extra credit of any kind
1935       *
1936       * @return bool True if extra credit used
1937       */
1938      public function is_extracredit_used() {
1939          return self::aggregation_uses_extracredit($this->aggregation);
1940      }
1941  
1942      /**
1943       * Returns true if aggregation passed is using extracredit.
1944       *
1945       * @param int $aggregation Aggregation const.
1946       * @return bool True if extra credit used
1947       */
1948      public static function aggregation_uses_extracredit($aggregation) {
1949          return ($aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2
1950               or $aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
1951               or $aggregation == GRADE_AGGREGATE_SUM);
1952      }
1953  
1954      /**
1955       * Returns true if category uses special aggregation coefficient
1956       *
1957       * @return bool True if an aggregation coefficient is being used
1958       */
1959      public function is_aggregationcoef_used() {
1960          return self::aggregation_uses_aggregationcoef($this->aggregation);
1961  
1962      }
1963  
1964      /**
1965       * Returns true if aggregation uses aggregationcoef
1966       *
1967       * @param int $aggregation Aggregation const.
1968       * @return bool True if an aggregation coefficient is being used
1969       */
1970      public static function aggregation_uses_aggregationcoef($aggregation) {
1971          return ($aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN
1972               or $aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2
1973               or $aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
1974               or $aggregation == GRADE_AGGREGATE_SUM);
1975  
1976      }
1977  
1978      /**
1979       * Recursive function to find which weight/extra credit field to use in the grade item form.
1980       *
1981       * @param string $first Whether or not this is the first item in the recursion
1982       * @return string
1983       */
1984      public function get_coefstring($first=true) {
1985          if (!is_null($this->coefstring)) {
1986              return $this->coefstring;
1987          }
1988  
1989          $overriding_coefstring = null;
1990  
1991          // Stop recursing upwards if this category has no parent
1992          if (!$first) {
1993  
1994              if ($parent_category = $this->load_parent_category()) {
1995                  return $parent_category->get_coefstring(false);
1996  
1997              } else {
1998                  return null;
1999              }
2000  
2001          } else if ($first) {
2002  
2003              if ($parent_category = $this->load_parent_category()) {
2004                  $overriding_coefstring = $parent_category->get_coefstring(false);
2005              }
2006          }
2007  
2008          // If an overriding coefstring has trickled down from one of the parent categories, return it. Otherwise, return self.
2009          if (!is_null($overriding_coefstring)) {
2010              return $overriding_coefstring;
2011          }
2012  
2013          // No parent category is overriding this category's aggregation, return its string
2014          if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
2015              $this->coefstring = 'aggregationcoefweight';
2016  
2017          } else if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
2018              $this->coefstring = 'aggregationcoefextrasum';
2019  
2020          } else if ($this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
2021              $this->coefstring = 'aggregationcoefextraweight';
2022  
2023          } else if ($this->aggregation == GRADE_AGGREGATE_SUM) {
2024              $this->coefstring = 'aggregationcoefextraweightsum';
2025  
2026          } else {
2027              $this->coefstring = 'aggregationcoef';
2028          }
2029          return $this->coefstring;
2030      }
2031  
2032      /**
2033       * Returns tree with all grade_items and categories as elements
2034       *
2035       * @param int $courseid The course ID
2036       * @param bool $include_category_items as category children
2037       * @return array
2038       */
2039      public static function fetch_course_tree($courseid, $include_category_items=false) {
2040          $course_category = grade_category::fetch_course_category($courseid);
2041          $category_array = array('object'=>$course_category, 'type'=>'category', 'depth'=>1,
2042                                  'children'=>$course_category->get_children($include_category_items));
2043  
2044          $course_category->sortorder = $course_category->get_sortorder();
2045          $sortorder = $course_category->get_sortorder();
2046          return grade_category::_fetch_course_tree_recursion($category_array, $sortorder);
2047      }
2048  
2049      /**
2050       * An internal function that recursively sorts grade categories within a course
2051       *
2052       * @param array $category_array The seed of the recursion
2053       * @param int   $sortorder The current sortorder
2054       * @return array An array containing 'object', 'type', 'depth' and optionally 'children'
2055       */
2056      static private function _fetch_course_tree_recursion($category_array, &$sortorder) {
2057          if (isset($category_array['object']->gradetype) && $category_array['object']->gradetype==GRADE_TYPE_NONE) {
2058              return null;
2059          }
2060  
2061          // store the grade_item or grade_category instance with extra info
2062          $result = array('object'=>$category_array['object'], 'type'=>$category_array['type'], 'depth'=>$category_array['depth']);
2063  
2064          // reuse final grades if there
2065          if (array_key_exists('finalgrades', $category_array)) {
2066              $result['finalgrades'] = $category_array['finalgrades'];
2067          }
2068  
2069          // recursively resort children
2070          if (!empty($category_array['children'])) {
2071              $result['children'] = array();
2072              //process the category item first
2073              $child = null;
2074  
2075              foreach ($category_array['children'] as $oldorder=>$child_array) {
2076  
2077                  if ($child_array['type'] == 'courseitem' or $child_array['type'] == 'categoryitem') {
2078                      $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
2079                      if (!empty($child)) {
2080                          $result['children'][$sortorder] = $child;
2081                      }
2082                  }
2083              }
2084  
2085              foreach ($category_array['children'] as $oldorder=>$child_array) {
2086  
2087                  if ($child_array['type'] != 'courseitem' and $child_array['type'] != 'categoryitem') {
2088                      $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
2089                      if (!empty($child)) {
2090                          $result['children'][++$sortorder] = $child;
2091                      }
2092                  }
2093              }
2094          }
2095  
2096          return $result;
2097      }
2098  
2099      /**
2100       * Fetches and returns all the children categories and/or grade_items belonging to this category.
2101       * By default only returns the immediate children (depth=1), but deeper levels can be requested,
2102       * as well as all levels (0). The elements are indexed by sort order.
2103       *
2104       * @param bool $include_category_items Whether or not to include category grade_items in the children array
2105       * @return array Array of child objects (grade_category and grade_item).
2106       */
2107      public function get_children($include_category_items=false) {
2108          global $DB;
2109  
2110          // This function must be as fast as possible ;-)
2111          // fetch all course grade items and categories into memory - we do not expect hundreds of these in course
2112          // we have to limit the number of queries though, because it will be used often in grade reports
2113  
2114          $cats  = $DB->get_records('grade_categories', array('courseid' => $this->courseid));
2115          $items = $DB->get_records('grade_items', array('courseid' => $this->courseid));
2116  
2117          // init children array first
2118          foreach ($cats as $catid=>$cat) {
2119              $cats[$catid]->children = array();
2120          }
2121  
2122          //first attach items to cats and add category sortorder
2123          foreach ($items as $item) {
2124  
2125              if ($item->itemtype == 'course' or $item->itemtype == 'category') {
2126                  $cats[$item->iteminstance]->sortorder = $item->sortorder;
2127  
2128                  if (!$include_category_items) {
2129                      continue;
2130                  }
2131                  $categoryid = $item->iteminstance;
2132  
2133              } else {
2134                  $categoryid = $item->categoryid;
2135                  if (empty($categoryid)) {
2136                      debugging('Found a grade item that isnt in a category');
2137                  }
2138              }
2139  
2140              // prevent problems with duplicate sortorders in db
2141              $sortorder = $item->sortorder;
2142  
2143              while (array_key_exists($categoryid, $cats)
2144                  && array_key_exists($sortorder, $cats[$categoryid]->children)) {
2145  
2146                  $sortorder++;
2147              }
2148  
2149              $cats[$categoryid]->children[$sortorder] = $item;
2150  
2151          }
2152  
2153          // now find the requested category and connect categories as children
2154          $category = false;
2155  
2156          foreach ($cats as $catid=>$cat) {
2157  
2158              if (empty($cat->parent)) {
2159  
2160                  if ($cat->path !== '/'.$cat->id.'/') {
2161                      $grade_category = new grade_category($cat, false);
2162                      $grade_category->path  = '/'.$cat->id.'/';
2163                      $grade_category->depth = 1;
2164                      $grade_category->update('system');
2165                      return $this->get_children($include_category_items);
2166                  }
2167  
2168              } else {
2169  
2170                  if (empty($cat->path) or !preg_match('|/'.$cat->parent.'/'.$cat->id.'/$|', $cat->path)) {
2171                      //fix paths and depts
2172                      static $recursioncounter = 0; // prevents infinite recursion
2173                      $recursioncounter++;
2174  
2175                      if ($recursioncounter < 5) {
2176                          // fix paths and depths!
2177                          $grade_category = new grade_category($cat, false);
2178                          $grade_category->depth = 0;
2179                          $grade_category->path  = null;
2180                          $grade_category->update('system');
2181                          return $this->get_children($include_category_items);
2182                      }
2183                  }
2184                  // prevent problems with duplicate sortorders in db
2185                  $sortorder = $cat->sortorder;
2186  
2187                  while (array_key_exists($sortorder, $cats[$cat->parent]->children)) {
2188                      //debugging("$sortorder exists in cat loop");
2189                      $sortorder++;
2190                  }
2191  
2192                  $cats[$cat->parent]->children[$sortorder] = &$cats[$catid];
2193              }
2194  
2195              if ($catid == $this->id) {
2196                  $category = &$cats[$catid];
2197              }
2198          }
2199  
2200          unset($items); // not needed
2201          unset($cats); // not needed
2202  
2203          $children_array = array();
2204          if (is_object($category)) {
2205              $children_array = grade_category::_get_children_recursion($category);
2206              ksort($children_array);
2207          }
2208  
2209          return $children_array;
2210  
2211      }
2212  
2213      /**
2214       * Private method used to retrieve all children of this category recursively
2215       *
2216       * @param grade_category $category Source of current recursion
2217       * @return array An array of child grade categories
2218       */
2219      private static function _get_children_recursion($category) {
2220  
2221          $children_array = array();
2222          foreach ($category->children as $sortorder=>$child) {
2223  
2224              if (property_exists($child, 'itemtype')) {
2225                  $grade_item = new grade_item($child, false);
2226  
2227                  if (in_array($grade_item->itemtype, array('course', 'category'))) {
2228                      $type  = $grade_item->itemtype.'item';
2229                      $depth = $category->depth;
2230  
2231                  } else {
2232                      $type  = 'item';
2233                      $depth = $category->depth; // we use this to set the same colour
2234                  }
2235                  $children_array[$sortorder] = array('object'=>$grade_item, 'type'=>$type, 'depth'=>$depth);
2236  
2237              } else {
2238                  $children = grade_category::_get_children_recursion($child);
2239                  $grade_category = new grade_category($child, false);
2240  
2241                  if (empty($children)) {
2242                      $children = array();
2243                  }
2244                  $children_array[$sortorder] = array('object'=>$grade_category, 'type'=>'category', 'depth'=>$grade_category->depth, 'children'=>$children);
2245              }
2246          }
2247  
2248          // sort the array
2249          ksort($children_array);
2250  
2251          return $children_array;
2252      }
2253  
2254      /**
2255       * Uses {@link get_grade_item()} to load or create a grade_item, then saves it as $this->grade_item.
2256       *
2257       * @return grade_item
2258       */
2259      public function load_grade_item() {
2260          if (empty($this->grade_item)) {
2261              $this->grade_item = $this->get_grade_item();
2262          }
2263          return $this->grade_item;
2264      }
2265  
2266      /**
2267       * Retrieves this grade categories' associated grade_item from the database
2268       *
2269       * If no grade_item exists yet, creates one.
2270       *
2271       * @return grade_item
2272       */
2273      public function get_grade_item() {
2274          if (empty($this->id)) {
2275              debugging("Attempt to obtain a grade_category's associated grade_item without the category's ID being set.");
2276              return false;
2277          }
2278  
2279          if (empty($this->parent)) {
2280              $params = array('courseid'=>$this->courseid, 'itemtype'=>'course', 'iteminstance'=>$this->id);
2281  
2282          } else {
2283              $params = array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id);
2284          }
2285  
2286          if (!$grade_items = grade_item::fetch_all($params)) {
2287              // create a new one
2288              $grade_item = new grade_item($params, false);
2289              $grade_item->gradetype = GRADE_TYPE_VALUE;
2290              $grade_item->insert('system');
2291  
2292          } else if (count($grade_items) == 1) {
2293              // found existing one
2294              $grade_item = reset($grade_items);
2295  
2296          } else {
2297              debugging("Found more than one grade_item attached to category id:".$this->id);
2298              // return first one
2299              $grade_item = reset($grade_items);
2300          }
2301  
2302          return $grade_item;
2303      }
2304  
2305      /**
2306       * Uses $this->parent to instantiate $this->parent_category based on the referenced record in the DB
2307       *
2308       * @return grade_category The parent category
2309       */
2310      public function load_parent_category() {
2311          if (empty($this->parent_category) && !empty($this->parent)) {
2312              $this->parent_category = $this->get_parent_category();
2313          }
2314          return $this->parent_category;
2315      }
2316  
2317      /**
2318       * Uses $this->parent to instantiate and return a grade_category object
2319       *
2320       * @return grade_category Returns the parent category or null if this category has no parent
2321       */
2322      public function get_parent_category() {
2323          if (!empty($this->parent)) {
2324              $parent_category = new grade_category(array('id' => $this->parent));
2325              return $parent_category;
2326          } else {
2327              return null;
2328          }
2329      }
2330  
2331      /**
2332       * Returns the most descriptive field for this grade category
2333       *
2334       * @return string name
2335       * @param bool $escape Whether the returned category name is to be HTML escaped or not.
2336       */
2337      public function get_name($escape = true) {
2338          global $DB;
2339          // For a course category, we return the course name if the fullname is set to '?' in the DB (empty in the category edit form)
2340          if (empty($this->parent) && $this->fullname == '?') {
2341              $course = $DB->get_record('course', array('id'=> $this->courseid));
2342              return format_string($course->fullname, false, ['context' => context_course::instance($this->courseid),
2343                  'escape' => $escape]);
2344  
2345          } else {
2346              // Grade categories can't be set up at system context (unlike scales and outcomes)
2347              // We therefore must have a courseid, and don't need to handle system contexts when filtering.
2348              return format_string($this->fullname, false, ['context' => context_course::instance($this->courseid),
2349                  'escape' => $escape]);
2350          }
2351      }
2352  
2353      /**
2354       * Describe the aggregation settings for this category so the reports make more sense.
2355       *
2356       * @return string description
2357       */
2358      public function get_description() {
2359          $allhelp = array();
2360          if ($this->aggregation != GRADE_AGGREGATE_SUM) {
2361              $aggrstrings = grade_helper::get_aggregation_strings();
2362              $allhelp[] = $aggrstrings[$this->aggregation];
2363          }
2364  
2365          if ($this->droplow && $this->can_apply_limit_rules()) {
2366              $allhelp[] = get_string('droplowestvalues', 'grades', $this->droplow);
2367          }
2368          if ($this->keephigh && $this->can_apply_limit_rules()) {
2369              $allhelp[] = get_string('keephighestvalues', 'grades', $this->keephigh);
2370          }
2371          if (!$this->aggregateonlygraded) {
2372              $allhelp[] = get_string('aggregatenotonlygraded', 'grades');
2373          }
2374          if ($allhelp) {
2375              return implode('. ', $allhelp) . '.';
2376          }
2377          return '';
2378      }
2379  
2380      /**
2381       * Sets this category's parent id
2382       *
2383       * @param int $parentid The ID of the category that is the new parent to $this
2384       * @param string $source From where was the object updated (mod/forum, manual, etc.)
2385       * @return bool success
2386       */
2387      public function set_parent($parentid, $source=null) {
2388          if ($this->parent == $parentid) {
2389              return true;
2390          }
2391  
2392          if ($parentid == $this->id) {
2393              throw new \moodle_exception('cannotassignselfasparent');
2394          }
2395  
2396          if (empty($this->parent) and $this->is_course_category()) {
2397              throw new \moodle_exception('cannothaveparentcate');
2398          }
2399  
2400          // find parent and check course id
2401          if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
2402              return false;
2403          }
2404  
2405          $this->force_regrading();
2406  
2407          // set new parent category
2408          $this->parent          = $parent_category->id;
2409          $this->parent_category =& $parent_category;
2410          $this->path            = null;       // remove old path and depth - will be recalculated in update()
2411          $this->depth           = 0;          // remove old path and depth - will be recalculated in update()
2412          $this->update($source);
2413  
2414          return $this->update($source);
2415      }
2416  
2417      /**
2418       * Returns the final grade values for this grade category.
2419       *
2420       * @param int $userid Optional user ID to retrieve a single user's final grade
2421       * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
2422       */
2423      public function get_final($userid=null) {
2424          $this->load_grade_item();
2425          return $this->grade_item->get_final($userid);
2426      }
2427  
2428      /**
2429       * Returns the sortorder of the grade categories' associated grade_item
2430       *
2431       * This method is also available in grade_item for cases where the object type is not known.
2432       *
2433       * @return int Sort order
2434       */
2435      public function get_sortorder() {
2436          $this->load_grade_item();
2437          return $this->grade_item->get_sortorder();
2438      }
2439  
2440      /**
2441       * Returns the idnumber of the grade categories' associated grade_item.
2442       *
2443       * This method is also available in grade_item for cases where the object type is not known.
2444       *
2445       * @return string idnumber
2446       */
2447      public function get_idnumber() {
2448          $this->load_grade_item();
2449          return $this->grade_item->get_idnumber();
2450      }
2451  
2452      /**
2453       * Sets the sortorder variable for this category.
2454       *
2455       * This method is also available in grade_item, for cases where the object type is not know.
2456       *
2457       * @param int $sortorder The sortorder to assign to this category
2458       */
2459      public function set_sortorder($sortorder) {
2460          $this->load_grade_item();
2461          $this->grade_item->set_sortorder($sortorder);
2462      }
2463  
2464      /**
2465       * Move this category after the given sortorder
2466       *
2467       * Does not change the parent
2468       *
2469       * @param int $sortorder to place after.
2470       * @return void
2471       */
2472      public function move_after_sortorder($sortorder) {
2473          $this->load_grade_item();
2474          $this->grade_item->move_after_sortorder($sortorder);
2475      }
2476  
2477      /**
2478       * Return true if this is the top most category that represents the total course grade.
2479       *
2480       * @return bool
2481       */
2482      public function is_course_category() {
2483          $this->load_grade_item();
2484          return $this->grade_item->is_course_item();
2485      }
2486  
2487      /**
2488       * Return the course level grade_category object
2489       *
2490       * @param int $courseid The Course ID
2491       * @return grade_category Returns the course level grade_category instance
2492       */
2493      public static function fetch_course_category($courseid) {
2494          if (empty($courseid)) {
2495              debugging('Missing course id!');
2496              return false;
2497          }
2498  
2499          // course category has no parent
2500          if ($course_category = grade_category::fetch(array('courseid'=>$courseid, 'parent'=>null))) {
2501              return $course_category;
2502          }
2503  
2504          // create a new one
2505          $course_category = new grade_category();
2506          $course_category->insert_course_category($courseid);
2507  
2508          return $course_category;
2509      }
2510  
2511      /**
2512       * Is grading object editable?
2513       *
2514       * @return bool
2515       */
2516      public function is_editable() {
2517          return true;
2518      }
2519  
2520      /**
2521       * Returns the locked state/date of the grade categories' associated grade_item.
2522       *
2523       * This method is also available in grade_item, for cases where the object type is not known.
2524       *
2525       * @return bool
2526       */
2527      public function is_locked() {
2528          $this->load_grade_item();
2529          return $this->grade_item->is_locked();
2530      }
2531  
2532      /**
2533       * Sets the grade_item's locked variable and updates the grade_item.
2534       *
2535       * Calls set_locked() on the categories' grade_item
2536       *
2537       * @param int  $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
2538       * @param bool $cascade lock/unlock child objects too
2539       * @param bool $refresh refresh grades when unlocking
2540       * @return bool success if category locked (not all children mayb be locked though)
2541       */
2542      public function set_locked($lockedstate, $cascade=false, $refresh=true) {
2543          $this->load_grade_item();
2544  
2545          $result = $this->grade_item->set_locked($lockedstate, $cascade, true);
2546  
2547          if ($cascade) {
2548              //process all children - items and categories
2549              if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
2550  
2551                  foreach ($children as $child) {
2552                      $child->set_locked($lockedstate, true, false);
2553  
2554                      if (empty($lockedstate) and $refresh) {
2555                          //refresh when unlocking
2556                          $child->refresh_grades();
2557                      }
2558                  }
2559              }
2560  
2561              if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
2562  
2563                  foreach ($children as $child) {
2564                      $child->set_locked($lockedstate, true, true);
2565                  }
2566              }
2567          }
2568  
2569          return $result;
2570      }
2571  
2572      /**
2573       * Overrides grade_object::set_properties() to add special handling for changes to category aggregation types
2574       *
2575       * @param stdClass $instance the object to set the properties on
2576       * @param array|stdClass $params Either an associative array or an object containing property name, property value pairs
2577       */
2578      public static function set_properties(&$instance, $params) {
2579          global $DB;
2580  
2581          $fromaggregation = $instance->aggregation;
2582  
2583          parent::set_properties($instance, $params);
2584  
2585          // The aggregation method is changing and this category has already been saved.
2586          if (isset($params->aggregation) && !empty($instance->id)) {
2587              $achildwasdupdated = false;
2588  
2589              // Get all its children.
2590              $children = $instance->get_children();
2591              foreach ($children as $child) {
2592                  $item = $child['object'];
2593                  if ($child['type'] == 'category') {
2594                      $item = $item->load_grade_item();
2595                  }
2596  
2597                  // Set the new aggregation fields.
2598                  if ($item->set_aggregation_fields_for_aggregation($fromaggregation, $params->aggregation)) {
2599                      $item->update();
2600                      $achildwasdupdated = true;
2601                  }
2602              }
2603  
2604              // If this is the course category, it is possible that its grade item was set as needsupdate
2605              // by one of its children. If we keep a reference to that stale object we might cause the
2606              // needsupdate flag to be lost. It's safer to just reload the grade_item from the database.
2607              if ($achildwasdupdated && !empty($instance->grade_item) && $instance->is_course_category()) {
2608                  $instance->grade_item = null;
2609                  $instance->load_grade_item();
2610              }
2611          }
2612      }
2613  
2614      /**
2615       * Sets the grade_item's hidden variable and updates the grade_item.
2616       *
2617       * Overrides grade_item::set_hidden() to add cascading of the hidden value to grade items in this grade category
2618       *
2619       * @param int $hidden 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
2620       * @param bool $cascade apply to child objects too
2621       */
2622      public function set_hidden($hidden, $cascade=false) {
2623          $this->load_grade_item();
2624          //this hides the category itself and everything it contains
2625          parent::set_hidden($hidden, $cascade);
2626  
2627          if ($cascade) {
2628  
2629              // This hides the associated grade item (the course/category total).
2630              $this->grade_item->set_hidden($hidden, $cascade);
2631  
2632              if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
2633  
2634                  foreach ($children as $child) {
2635                      if ($child->can_control_visibility()) {
2636                          $child->set_hidden($hidden, $cascade);
2637                      }
2638                  }
2639              }
2640  
2641              if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
2642  
2643                  foreach ($children as $child) {
2644                      $child->set_hidden($hidden, $cascade);
2645                  }
2646              }
2647          }
2648  
2649          //if marking category visible make sure parent category is visible MDL-21367
2650          if( !$hidden ) {
2651              $category_array = grade_category::fetch_all(array('id'=>$this->parent));
2652              if ($category_array && array_key_exists($this->parent, $category_array)) {
2653                  $category = $category_array[$this->parent];
2654                  //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
2655                  $category->set_hidden($hidden, false);
2656              }
2657          }
2658      }
2659  
2660      /**
2661       * Applies default settings on this category
2662       *
2663       * @return bool True if anything changed
2664       */
2665      public function apply_default_settings() {
2666          global $CFG;
2667  
2668          foreach ($this->forceable as $property) {
2669  
2670              if (isset($CFG->{"grade_$property"})) {
2671  
2672                  if ($CFG->{"grade_$property"} == -1) {
2673                      continue; //temporary bc before version bump
2674                  }
2675                  $this->$property = $CFG->{"grade_$property"};
2676              }
2677          }
2678      }
2679  
2680      /**
2681       * Applies forced settings on this category
2682       *
2683       * @return bool True if anything changed
2684       */
2685      public function apply_forced_settings() {
2686          global $CFG;
2687  
2688          $updated = false;
2689  
2690          foreach ($this->forceable as $property) {
2691  
2692              if (isset($CFG->{"grade_$property"}) and isset($CFG->{"grade_{$property}_flag"}) and
2693                                                      ((int) $CFG->{"grade_{$property}_flag"} & 1)) {
2694  
2695                  if ($CFG->{"grade_$property"} == -1) {
2696                      continue; //temporary bc before version bump
2697                  }
2698                  $this->$property = $CFG->{"grade_$property"};
2699                  $updated = true;
2700              }
2701          }
2702  
2703          return $updated;
2704      }
2705  
2706      /**
2707       * Notification of change in forced category settings.
2708       *
2709       * Causes all course and category grade items to be marked as needing to be updated
2710       */
2711      public static function updated_forced_settings() {
2712          global $CFG, $DB;
2713          $params = array(1, 'course', 'category');
2714          $sql = "UPDATE {grade_items} SET needsupdate=? WHERE itemtype=? or itemtype=?";
2715          $DB->execute($sql, $params);
2716      }
2717  
2718      /**
2719       * Determine the default aggregation values for a given aggregation method.
2720       *
2721       * @param int $aggregationmethod The aggregation method constant value.
2722       * @return array Containing the keys 'aggregationcoef', 'aggregationcoef2' and 'weightoverride'.
2723       */
2724      public static function get_default_aggregation_coefficient_values($aggregationmethod) {
2725          $defaultcoefficients = array(
2726              'aggregationcoef' => 0,
2727              'aggregationcoef2' => 0,
2728              'weightoverride' => 0
2729          );
2730  
2731          switch ($aggregationmethod) {
2732              case GRADE_AGGREGATE_WEIGHTED_MEAN:
2733                  $defaultcoefficients['aggregationcoef'] = 1;
2734                  break;
2735              case GRADE_AGGREGATE_SUM:
2736                  $defaultcoefficients['aggregationcoef2'] = 1;
2737                  break;
2738          }
2739  
2740          return $defaultcoefficients;
2741      }
2742  
2743      /**
2744       * Cleans the cache.
2745       *
2746       * We invalidate them all so it can be completely reloaded.
2747       *
2748       * Being conservative here, if there is a new grade_category we purge them, the important part
2749       * is that this is not purged when there are no changes in grade_categories.
2750       *
2751       * @param bool $deleted
2752       * @return void
2753       */
2754      protected function notify_changed($deleted) {
2755          self::clean_record_set();
2756      }
2757  
2758      /**
2759       * Generates a unique key per query.
2760       *
2761       * Not unique between grade_object children. self::retrieve_record_set and self::set_record_set will be in charge of
2762       * selecting the appropriate cache.
2763       *
2764       * @param array $params An array of conditions like $fieldname => $fieldvalue
2765       * @return string
2766       */
2767      protected static function generate_record_set_key($params) {
2768          return sha1(json_encode($params));
2769      }
2770  
2771      /**
2772       * Tries to retrieve a record set from the cache.
2773       *
2774       * @param array $params The query params
2775       * @return grade_object[]|bool An array of grade_objects or false if not found.
2776       */
2777      protected static function retrieve_record_set($params) {
2778          $cache = cache::make('core', 'grade_categories');
2779          return $cache->get(self::generate_record_set_key($params));
2780      }
2781  
2782      /**
2783       * Sets a result to the records cache, even if there were no results.
2784       *
2785       * @param string $params The query params
2786       * @param grade_object[]|bool $records An array of grade_objects or false if there are no records matching the $key filters
2787       * @return void
2788       */
2789      protected static function set_record_set($params, $records) {
2790          $cache = cache::make('core', 'grade_categories');
2791          return $cache->set(self::generate_record_set_key($params), $records);
2792      }
2793  
2794      /**
2795       * Cleans the cache.
2796       *
2797       * Aggressive deletion to be conservative given the gradebook design.
2798       * The key is based on the requested params, not easy nor worth to purge selectively.
2799       *
2800       * @return void
2801       */
2802      public static function clean_record_set() {
2803          cache_helper::purge_by_event('changesingradecategories');
2804      }
2805  }