Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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              print_error('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          // For items with no value, and not excluded - either set their grade to 0 or exclude them.
 720          foreach ($items as $itemid=>$value) {
 721              if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
 722                  if (!$this->aggregateonlygraded) {
 723                      $grade_values[$itemid] = 0;
 724                  } else {
 725                      // We are specifically marking these items as "excluded empty".
 726                      $novalue[$itemid] = 0;
 727                  }
 728              }
 729          }
 730  
 731          // limit and sort
 732          $allvalues = $grade_values;
 733          if ($this->can_apply_limit_rules()) {
 734              $this->apply_limit_rules($grade_values, $items);
 735          }
 736  
 737          $moredropped = array_diff($allvalues, $grade_values);
 738          foreach ($moredropped as $drop => $unused) {
 739              $dropped[$drop] = 0;
 740          }
 741  
 742          foreach ($grade_values as $itemid => $val) {
 743              if (self::is_extracredit_used() && ($items[$itemid]->aggregationcoef > 0)) {
 744                  $extracredit[$itemid] = 0;
 745              }
 746          }
 747  
 748          asort($grade_values, SORT_NUMERIC);
 749  
 750          // let's see we have still enough grades to do any statistics
 751          if (count($grade_values) == 0) {
 752              // not enough attempts yet
 753              $grade->finalgrade = null;
 754  
 755              if (!is_null($oldfinalgrade)) {
 756                  $grade->timemodified = time();
 757                  $success = $grade->update('aggregation');
 758  
 759                  // If successful trigger a user_graded event.
 760                  if ($success) {
 761                      \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger();
 762                  }
 763              }
 764              $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit);
 765              return;
 766          }
 767  
 768          // do the maths
 769          $result = $this->aggregate_values_and_adjust_bounds($grade_values,
 770                                                              $items,
 771                                                              $usedweights,
 772                                                              $grademinoverrides,
 773                                                              $grademaxoverrides);
 774          $agg_grade = $result['grade'];
 775  
 776          // Set the actual grademin and max to bind the grade properly.
 777          $this->grade_item->grademin = $result['grademin'];
 778          $this->grade_item->grademax = $result['grademax'];
 779  
 780          if ($this->aggregation == GRADE_AGGREGATE_SUM) {
 781              // The natural aggregation always displays the range as coming from 0 for categories.
 782              // However, when we bind the grade we allow for negative values.
 783              $result['grademin'] = 0;
 784          }
 785  
 786          // Recalculate the grade back to requested range.
 787          $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $result['grademin'], $result['grademax']);
 788          $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade);
 789  
 790          $oldrawgrademin = $grade->rawgrademin;
 791          $oldrawgrademax = $grade->rawgrademax;
 792          $grade->rawgrademin = $result['grademin'];
 793          $grade->rawgrademax = $result['grademax'];
 794  
 795          // Update in db if changed.
 796          if (grade_floats_different($grade->finalgrade, $oldfinalgrade) ||
 797                  grade_floats_different($grade->rawgrademax, $oldrawgrademax) ||
 798                  grade_floats_different($grade->rawgrademin, $oldrawgrademin)) {
 799              $grade->timemodified = time();
 800              $success = $grade->update('aggregation');
 801  
 802              // If successful trigger a user_graded event.
 803              if ($success) {
 804                  \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger();
 805              }
 806          }
 807  
 808          $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit);
 809  
 810          return;
 811      }
 812  
 813      /**
 814       * Set the flags on the grade_grade items to indicate how individual grades are used
 815       * in the aggregation.
 816       *
 817       * WARNING: This function is called a lot during gradebook recalculation, be very performance considerate.
 818       *
 819       * @param int $userid The user we have aggregated the grades for.
 820       * @param array $usedweights An array with keys for each of the grade_item columns included in the aggregation. The value are the relative weight.
 821       * @param array $novalue An array with keys for each of the grade_item columns skipped because
 822       *                       they had no value in the aggregation.
 823       * @param array $dropped An array with keys for each of the grade_item columns dropped
 824       *                       because of any drop lowest/highest settings in the aggregation.
 825       * @param array $extracredit An array with keys for each of the grade_item columns
 826       *                       considered extra credit by the aggregation.
 827       */
 828      private function set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit) {
 829          global $DB;
 830  
 831          // We want to know all current user grades so we can decide whether they need to be updated or they already contain the
 832          // expected value.
 833          $sql = "SELECT gi.id, gg.aggregationstatus, gg.aggregationweight FROM {grade_grades} gg
 834                    JOIN {grade_items} gi ON (gg.itemid = gi.id)
 835                   WHERE gg.userid = :userid";
 836          $params = array('categoryid' => $this->id, 'userid' => $userid);
 837  
 838          // These are all grade_item ids which grade_grades will NOT end up being 'unknown' (because they are not unknown or
 839          // because we will update them to something different that 'unknown').
 840          $giids = array_keys($usedweights + $novalue + $dropped + $extracredit);
 841  
 842          if ($giids) {
 843              // We include grade items that might not be in categoryid.
 844              list($itemsql, $itemlist) = $DB->get_in_or_equal($giids, SQL_PARAMS_NAMED, 'gg');
 845              $sql .= ' AND (gi.categoryid = :categoryid OR gi.id ' . $itemsql . ')';
 846              $params = $params + $itemlist;
 847          } else {
 848              $sql .= ' AND gi.categoryid = :categoryid';
 849          }
 850          $currentgrades = $DB->get_recordset_sql($sql, $params);
 851  
 852          // We will store here the grade_item ids that need to be updated on db.
 853          $toupdate = array();
 854  
 855          if ($currentgrades->valid()) {
 856  
 857              // Iterate through the user grades to see if we really need to update any of them.
 858              foreach ($currentgrades as $currentgrade) {
 859  
 860                  // Unset $usedweights that we do not need to update.
 861                  if (!empty($usedweights) && isset($usedweights[$currentgrade->id]) && $currentgrade->aggregationstatus === 'used') {
 862                      // We discard the ones that already have the contribution specified in $usedweights and are marked as 'used'.
 863                      if (grade_floats_equal($currentgrade->aggregationweight, $usedweights[$currentgrade->id])) {
 864                          unset($usedweights[$currentgrade->id]);
 865                      }
 866                      // Used weights can be present in multiple set_usedinaggregation arguments.
 867                      if (!isset($novalue[$currentgrade->id]) && !isset($dropped[$currentgrade->id]) &&
 868                              !isset($extracredit[$currentgrade->id])) {
 869                          continue;
 870                      }
 871                  }
 872  
 873                  // No value grades.
 874                  if (!empty($novalue) && isset($novalue[$currentgrade->id])) {
 875                      if ($currentgrade->aggregationstatus !== 'novalue' ||
 876                              grade_floats_different($currentgrade->aggregationweight, 0)) {
 877                          $toupdate['novalue'][] = $currentgrade->id;
 878                      }
 879                      continue;
 880                  }
 881  
 882                  // Dropped grades.
 883                  if (!empty($dropped) && isset($dropped[$currentgrade->id])) {
 884                      if ($currentgrade->aggregationstatus !== 'dropped' ||
 885                              grade_floats_different($currentgrade->aggregationweight, 0)) {
 886                          $toupdate['dropped'][] = $currentgrade->id;
 887                      }
 888                      continue;
 889                  }
 890  
 891                  // Extra credit grades.
 892                  if (!empty($extracredit) && isset($extracredit[$currentgrade->id])) {
 893  
 894                      // If this grade item is already marked as 'extra' and it already has the provided $usedweights value would be
 895                      // silly to update to 'used' to later update to 'extra'.
 896                      if (!empty($usedweights) && isset($usedweights[$currentgrade->id]) &&
 897                              grade_floats_equal($currentgrade->aggregationweight, $usedweights[$currentgrade->id])) {
 898                          unset($usedweights[$currentgrade->id]);
 899                      }
 900  
 901                      // Update the item to extra if it is not already marked as extra in the database or if the item's
 902                      // aggregationweight will be updated when going through $usedweights items.
 903                      if ($currentgrade->aggregationstatus !== 'extra' ||
 904                              (!empty($usedweights) && isset($usedweights[$currentgrade->id]))) {
 905                          $toupdate['extracredit'][] = $currentgrade->id;
 906                      }
 907                      continue;
 908                  }
 909  
 910                  // If is not in any of the above groups it should be set to 'unknown', checking that the item is not already
 911                  // unknown, if it is we don't need to update it.
 912                  if ($currentgrade->aggregationstatus !== 'unknown' || grade_floats_different($currentgrade->aggregationweight, 0)) {
 913                      $toupdate['unknown'][] = $currentgrade->id;
 914                  }
 915              }
 916              $currentgrades->close();
 917          }
 918  
 919          // Update items to 'unknown' status.
 920          if (!empty($toupdate['unknown'])) {
 921              list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['unknown'], SQL_PARAMS_NAMED, 'g');
 922  
 923              $itemlist['userid'] = $userid;
 924  
 925              $sql = "UPDATE {grade_grades}
 926                         SET aggregationstatus = 'unknown',
 927                             aggregationweight = 0
 928                       WHERE itemid $itemsql AND userid = :userid";
 929              $DB->execute($sql, $itemlist);
 930          }
 931  
 932          // Update items to 'used' status and setting the proper weight.
 933          if (!empty($usedweights)) {
 934              // The usedweights items are updated individually to record the weights.
 935              foreach ($usedweights as $gradeitemid => $contribution) {
 936                  $sql = "UPDATE {grade_grades}
 937                             SET aggregationstatus = 'used',
 938                                 aggregationweight = :contribution
 939                           WHERE itemid = :itemid AND userid = :userid";
 940  
 941                  $params = array('contribution' => $contribution, 'itemid' => $gradeitemid, 'userid' => $userid);
 942                  $DB->execute($sql, $params);
 943              }
 944          }
 945  
 946          // Update items to 'novalue' status.
 947          if (!empty($toupdate['novalue'])) {
 948              list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['novalue'], SQL_PARAMS_NAMED, 'g');
 949  
 950              $itemlist['userid'] = $userid;
 951  
 952              $sql = "UPDATE {grade_grades}
 953                         SET aggregationstatus = 'novalue',
 954                             aggregationweight = 0
 955                       WHERE itemid $itemsql AND userid = :userid";
 956  
 957              $DB->execute($sql, $itemlist);
 958          }
 959  
 960          // Update items to 'dropped' status.
 961          if (!empty($toupdate['dropped'])) {
 962              list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['dropped'], SQL_PARAMS_NAMED, 'g');
 963  
 964              $itemlist['userid'] = $userid;
 965  
 966              $sql = "UPDATE {grade_grades}
 967                         SET aggregationstatus = 'dropped',
 968                             aggregationweight = 0
 969                       WHERE itemid $itemsql AND userid = :userid";
 970  
 971              $DB->execute($sql, $itemlist);
 972          }
 973  
 974          // Update items to 'extracredit' status.
 975          if (!empty($toupdate['extracredit'])) {
 976              list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['extracredit'], SQL_PARAMS_NAMED, 'g');
 977  
 978              $itemlist['userid'] = $userid;
 979  
 980              $DB->set_field_select('grade_grades',
 981                                    'aggregationstatus',
 982                                    'extra',
 983                                    "itemid $itemsql AND userid = :userid",
 984                                    $itemlist);
 985          }
 986      }
 987  
 988      /**
 989       * Internal function that calculates the aggregated grade and new min/max for this grade category
 990       *
 991       * Must be public as it is used by grade_grade::get_hiding_affected()
 992       *
 993       * @param array $grade_values An array of values to be aggregated
 994       * @param array $items The array of grade_items
 995       * @since Moodle 2.6.5, 2.7.2
 996       * @param array & $weights If provided, will be filled with the normalized weights
 997       *                         for each grade_item as used in the aggregation.
 998       *                         Some rules for the weights are:
 999       *                         1. The weights must add up to 1 (unless there are extra credit)
1000       *                         2. The contributed points column must add up to the course
1001       *                         final grade and this column is calculated from these weights.
1002       * @param array  $grademinoverrides User specific grademin values if different to the grade_item grademin (key is itemid)
1003       * @param array  $grademaxoverrides User specific grademax values if different to the grade_item grademax (key is itemid)
1004       * @return array containing values for:
1005       *                'grade' => the new calculated grade
1006       *                'grademin' => the new calculated min grade for the category
1007       *                'grademax' => the new calculated max grade for the category
1008       */
1009      public function aggregate_values_and_adjust_bounds($grade_values,
1010                                                         $items,
1011                                                         & $weights = null,
1012                                                         $grademinoverrides = array(),
1013                                                         $grademaxoverrides = array()) {
1014          global $CFG;
1015  
1016          $category_item = $this->load_grade_item();
1017          $grademin = $category_item->grademin;
1018          $grademax = $category_item->grademax;
1019  
1020          switch ($this->aggregation) {
1021  
1022              case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies
1023                  $num = count($grade_values);
1024                  $grades = array_values($grade_values);
1025  
1026                  // The median gets 100% - others get 0.
1027                  if ($weights !== null && $num > 0) {
1028                      $count = 0;
1029                      foreach ($grade_values as $itemid=>$grade_value) {
1030                          if (($num % 2 == 0) && ($count == intval($num/2)-1 || $count == intval($num/2))) {
1031                              $weights[$itemid] = 0.5;
1032                          } else if (($num % 2 != 0) && ($count == intval(($num/2)-0.5))) {
1033                              $weights[$itemid] = 1.0;
1034                          } else {
1035                              $weights[$itemid] = 0;
1036                          }
1037                          $count++;
1038                      }
1039                  }
1040                  if ($num % 2 == 0) {
1041                      $agg_grade = ($grades[intval($num/2)-1] + $grades[intval($num/2)]) / 2;
1042                  } else {
1043                      $agg_grade = $grades[intval(($num/2)-0.5)];
1044                  }
1045  
1046                  break;
1047  
1048              case GRADE_AGGREGATE_MIN:
1049                  $agg_grade = reset($grade_values);
1050                  // Record the weights as used.
1051                  if ($weights !== null) {
1052                      foreach ($grade_values as $itemid=>$grade_value) {
1053                          $weights[$itemid] = 0;
1054                      }
1055                  }
1056                  // Set the first item to 1.
1057                  $itemids = array_keys($grade_values);
1058                  $weights[reset($itemids)] = 1;
1059                  break;
1060  
1061              case GRADE_AGGREGATE_MAX:
1062                  // Record the weights as used.
1063                  if ($weights !== null) {
1064                      foreach ($grade_values as $itemid=>$grade_value) {
1065                          $weights[$itemid] = 0;
1066                      }
1067                  }
1068                  // Set the last item to 1.
1069                  $itemids = array_keys($grade_values);
1070                  $weights[end($itemids)] = 1;
1071                  $agg_grade = end($grade_values);
1072                  break;
1073  
1074              case GRADE_AGGREGATE_MODE:       // the most common value
1075                  // array_count_values only counts INT and STRING, so if grades are floats we must convert them to string
1076                  $converted_grade_values = array();
1077  
1078                  foreach ($grade_values as $k => $gv) {
1079  
1080                      if (!is_int($gv) && !is_string($gv)) {
1081                          $converted_grade_values[$k] = (string) $gv;
1082  
1083                      } else {
1084                          $converted_grade_values[$k] = $gv;
1085                      }
1086                      if ($weights !== null) {
1087                          $weights[$k] = 0;
1088                      }
1089                  }
1090  
1091                  $freq = array_count_values($converted_grade_values);
1092                  arsort($freq);                      // sort by frequency keeping keys
1093                  $top = reset($freq);               // highest frequency count
1094                  $modes = array_keys($freq, $top);  // search for all modes (have the same highest count)
1095                  rsort($modes, SORT_NUMERIC);       // get highest mode
1096                  $agg_grade = reset($modes);
1097                  // Record the weights as used.
1098                  if ($weights !== null && $top > 0) {
1099                      foreach ($grade_values as $k => $gv) {
1100                          if ($gv == $agg_grade) {
1101                              $weights[$k] = 1.0 / $top;
1102                          }
1103                      }
1104                  }
1105                  break;
1106  
1107              case GRADE_AGGREGATE_WEIGHTED_MEAN: // Weighted average of all existing final grades, weight specified in coef
1108                  $weightsum = 0;
1109                  $sum       = 0;
1110  
1111                  foreach ($grade_values as $itemid=>$grade_value) {
1112                      if ($weights !== null) {
1113                          $weights[$itemid] = $items[$itemid]->aggregationcoef;
1114                      }
1115                      if ($items[$itemid]->aggregationcoef <= 0) {
1116                          continue;
1117                      }
1118                      $weightsum += $items[$itemid]->aggregationcoef;
1119                      $sum       += $items[$itemid]->aggregationcoef * $grade_value;
1120                  }
1121                  if ($weightsum == 0) {
1122                      $agg_grade = null;
1123  
1124                  } else {
1125                      $agg_grade = $sum / $weightsum;
1126                      if ($weights !== null) {
1127                          // Normalise the weights.
1128                          foreach ($weights as $itemid => $weight) {
1129                              $weights[$itemid] = $weight / $weightsum;
1130                          }
1131                      }
1132  
1133                  }
1134                  break;
1135  
1136              case GRADE_AGGREGATE_WEIGHTED_MEAN2:
1137                  // Weighted average of all existing final grades with optional extra credit flag,
1138                  // weight is the range of grade (usually grademax)
1139                  $this->load_grade_item();
1140                  $weightsum = 0;
1141                  $sum       = null;
1142  
1143                  foreach ($grade_values as $itemid=>$grade_value) {
1144                      if ($items[$itemid]->aggregationcoef > 0) {
1145                          continue;
1146                      }
1147  
1148                      $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
1149                      if ($weight <= 0) {
1150                          continue;
1151                      }
1152  
1153                      $weightsum += $weight;
1154                      $sum += $weight * $grade_value;
1155                  }
1156  
1157                  // Handle the extra credit items separately to calculate their weight accurately.
1158                  foreach ($grade_values as $itemid => $grade_value) {
1159                      if ($items[$itemid]->aggregationcoef <= 0) {
1160                          continue;
1161                      }
1162  
1163                      $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
1164                      if ($weight <= 0) {
1165                          $weights[$itemid] = 0;
1166                          continue;
1167                      }
1168  
1169                      $oldsum = $sum;
1170                      $weightedgrade = $weight * $grade_value;
1171                      $sum += $weightedgrade;
1172  
1173                      if ($weights !== null) {
1174                          if ($weightsum <= 0) {
1175                              $weights[$itemid] = 0;
1176                              continue;
1177                          }
1178  
1179                          $oldgrade = $oldsum / $weightsum;
1180                          $grade = $sum / $weightsum;
1181                          $normoldgrade = grade_grade::standardise_score($oldgrade, 0, 1, $grademin, $grademax);
1182                          $normgrade = grade_grade::standardise_score($grade, 0, 1, $grademin, $grademax);
1183                          $boundedoldgrade = $this->grade_item->bounded_grade($normoldgrade);
1184                          $boundedgrade = $this->grade_item->bounded_grade($normgrade);
1185  
1186                          if ($boundedgrade - $boundedoldgrade <= 0) {
1187                              // Nothing new was added to the grade.
1188                              $weights[$itemid] = 0;
1189                          } else if ($boundedgrade < $normgrade) {
1190                              // The grade has been bounded, the extra credit item needs to have a different weight.
1191                              $gradediff = $boundedgrade - $normoldgrade;
1192                              $gradediffnorm = grade_grade::standardise_score($gradediff, $grademin, $grademax, 0, 1);
1193                              $weights[$itemid] = $gradediffnorm / $grade_value;
1194                          } else {
1195                              // Default weighting.
1196                              $weights[$itemid] = $weight / $weightsum;
1197                          }
1198                      }
1199                  }
1200  
1201                  if ($weightsum == 0) {
1202                      $agg_grade = $sum; // only extra credits
1203  
1204                  } else {
1205                      $agg_grade = $sum / $weightsum;
1206                  }
1207  
1208                  // Record the weights as used.
1209                  if ($weights !== null) {
1210                      foreach ($grade_values as $itemid=>$grade_value) {
1211                          if ($items[$itemid]->aggregationcoef > 0) {
1212                              // Ignore extra credit items, the weights have already been computed.
1213                              continue;
1214                          }
1215                          if ($weightsum > 0) {
1216                              $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
1217                              $weights[$itemid] = $weight / $weightsum;
1218                          } else {
1219                              $weights[$itemid] = 0;
1220                          }
1221                      }
1222                  }
1223                  break;
1224  
1225              case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average
1226                  $this->load_grade_item();
1227                  $num = 0;
1228                  $sum = null;
1229  
1230                  foreach ($grade_values as $itemid=>$grade_value) {
1231                      if ($items[$itemid]->aggregationcoef == 0) {
1232                          $num += 1;
1233                          $sum += $grade_value;
1234                          if ($weights !== null) {
1235                              $weights[$itemid] = 1;
1236                          }
1237                      }
1238                  }
1239  
1240                  // Treating the extra credit items separately to get a chance to calculate their effective weights.
1241                  foreach ($grade_values as $itemid=>$grade_value) {
1242                      if ($items[$itemid]->aggregationcoef > 0) {
1243                          $oldsum = $sum;
1244                          $sum += $items[$itemid]->aggregationcoef * $grade_value;
1245  
1246                          if ($weights !== null) {
1247                              if ($num <= 0) {
1248                                  // The category only contains extra credit items, not setting the weight.
1249                                  continue;
1250                              }
1251  
1252                              $oldgrade = $oldsum / $num;
1253                              $grade = $sum / $num;
1254                              $normoldgrade = grade_grade::standardise_score($oldgrade, 0, 1, $grademin, $grademax);
1255                              $normgrade = grade_grade::standardise_score($grade, 0, 1, $grademin, $grademax);
1256                              $boundedoldgrade = $this->grade_item->bounded_grade($normoldgrade);
1257                              $boundedgrade = $this->grade_item->bounded_grade($normgrade);
1258  
1259                              if ($boundedgrade - $boundedoldgrade <= 0) {
1260                                  // Nothing new was added to the grade.
1261                                  $weights[$itemid] = 0;
1262                              } else if ($boundedgrade < $normgrade) {
1263                                  // The grade has been bounded, the extra credit item needs to have a different weight.
1264                                  $gradediff = $boundedgrade - $normoldgrade;
1265                                  $gradediffnorm = grade_grade::standardise_score($gradediff, $grademin, $grademax, 0, 1);
1266                                  $weights[$itemid] = $gradediffnorm / $grade_value;
1267                              } else {
1268                                  // Default weighting.
1269                                  $weights[$itemid] = 1.0 / $num;
1270                              }
1271                          }
1272                      }
1273                  }
1274  
1275                  if ($weights !== null && $num > 0) {
1276                      foreach ($grade_values as $itemid=>$grade_value) {
1277                          if ($items[$itemid]->aggregationcoef > 0) {
1278                              // Extra credit weights were already calculated.
1279                              continue;
1280                          }
1281                          if ($weights[$itemid]) {
1282                              $weights[$itemid] = 1.0 / $num;
1283                          }
1284                      }
1285                  }
1286  
1287                  if ($num == 0) {
1288                      $agg_grade = $sum; // only extra credits or wrong coefs
1289  
1290                  } else {
1291                      $agg_grade = $sum / $num;
1292                  }
1293  
1294                  break;
1295  
1296              case GRADE_AGGREGATE_SUM:    // Add up all the items.
1297                  $this->load_grade_item();
1298                  $num = count($grade_values);
1299                  $sum = 0;
1300  
1301                  // This setting indicates if we should use algorithm prior to MDL-49257 fix for calculating extra credit weights.
1302                  // Even though old algorith has bugs in it, we need to preserve existing grades.
1303                  $gradebookcalculationfreeze = 'gradebook_calculations_freeze_' . $this->courseid;
1304                  $oldextracreditcalculation = isset($CFG->$gradebookcalculationfreeze)
1305                          && ($CFG->$gradebookcalculationfreeze <= 20150619);
1306  
1307                  $sumweights = 0;
1308                  $grademin = 0;
1309                  $grademax = 0;
1310                  $extracredititems = array();
1311                  foreach ($grade_values as $itemid => $gradevalue) {
1312                      // We need to check if the grademax/min was adjusted per user because of excluded items.
1313                      $usergrademin = $items[$itemid]->grademin;
1314                      $usergrademax = $items[$itemid]->grademax;
1315                      if (isset($grademinoverrides[$itemid])) {
1316                          $usergrademin = $grademinoverrides[$itemid];
1317                      }
1318                      if (isset($grademaxoverrides[$itemid])) {
1319                          $usergrademax = $grademaxoverrides[$itemid];
1320                      }
1321  
1322                      // Keep track of the extra credit items, we will need them later on.
1323                      if ($items[$itemid]->aggregationcoef > 0) {
1324                          $extracredititems[$itemid] = $items[$itemid];
1325                      }
1326  
1327                      // Ignore extra credit and items with a weight of 0.
1328                      if (!isset($extracredititems[$itemid]) && $items[$itemid]->aggregationcoef2 > 0) {
1329                          $grademin += $usergrademin;
1330                          $grademax += $usergrademax;
1331                          $sumweights += $items[$itemid]->aggregationcoef2;
1332                      }
1333                  }
1334                  $userweights = array();
1335                  $totaloverriddenweight = 0;
1336                  $totaloverriddengrademax = 0;
1337                  // We first need to rescale all manually assigned weights down by the
1338                  // percentage of weights missing from the category.
1339                  foreach ($grade_values as $itemid => $gradevalue) {
1340                      if ($items[$itemid]->weightoverride) {
1341                          if ($items[$itemid]->aggregationcoef2 <= 0) {
1342                              // Records the weight of 0 and continue.
1343                              $userweights[$itemid] = 0;
1344                              continue;
1345                          }
1346                          $userweights[$itemid] = $sumweights ? ($items[$itemid]->aggregationcoef2 / $sumweights) : 0;
1347                          if (!$oldextracreditcalculation && isset($extracredititems[$itemid])) {
1348                              // Extra credit items do not affect totals.
1349                              continue;
1350                          }
1351                          $totaloverriddenweight += $userweights[$itemid];
1352                          $usergrademax = $items[$itemid]->grademax;
1353                          if (isset($grademaxoverrides[$itemid])) {
1354                              $usergrademax = $grademaxoverrides[$itemid];
1355                          }
1356                          $totaloverriddengrademax += $usergrademax;
1357                      }
1358                  }
1359                  $nonoverriddenpoints = $grademax - $totaloverriddengrademax;
1360  
1361                  // Then we need to recalculate the automatic weights except for extra credit items.
1362                  foreach ($grade_values as $itemid => $gradevalue) {
1363                      if (!$items[$itemid]->weightoverride && ($oldextracreditcalculation || !isset($extracredititems[$itemid]))) {
1364                          $usergrademax = $items[$itemid]->grademax;
1365                          if (isset($grademaxoverrides[$itemid])) {
1366                              $usergrademax = $grademaxoverrides[$itemid];
1367                          }
1368                          if ($nonoverriddenpoints > 0) {
1369                              $userweights[$itemid] = ($usergrademax/$nonoverriddenpoints) * (1 - $totaloverriddenweight);
1370                          } else {
1371                              $userweights[$itemid] = 0;
1372                              if ($items[$itemid]->aggregationcoef2 > 0) {
1373                                  // Items with a weight of 0 should not count for the grade max,
1374                                  // though this only applies if the weight was changed to 0.
1375                                  $grademax -= $usergrademax;
1376                              }
1377                          }
1378                      }
1379                  }
1380  
1381                  // Now when we finally know the grademax we can adjust the automatic weights of extra credit items.
1382                  if (!$oldextracreditcalculation) {
1383                      foreach ($grade_values as $itemid => $gradevalue) {
1384                          if (!$items[$itemid]->weightoverride && isset($extracredititems[$itemid])) {
1385                              $usergrademax = $items[$itemid]->grademax;
1386                              if (isset($grademaxoverrides[$itemid])) {
1387                                  $usergrademax = $grademaxoverrides[$itemid];
1388                              }
1389                              $userweights[$itemid] = $grademax ? ($usergrademax / $grademax) : 0;
1390                          }
1391                      }
1392                  }
1393  
1394                  // We can use our freshly corrected weights below.
1395                  foreach ($grade_values as $itemid => $gradevalue) {
1396                      if (isset($extracredititems[$itemid])) {
1397                          // We skip the extra credit items first.
1398                          continue;
1399                      }
1400                      $sum += $gradevalue * $userweights[$itemid] * $grademax;
1401                      if ($weights !== null) {
1402                          $weights[$itemid] = $userweights[$itemid];
1403                      }
1404                  }
1405  
1406                  // No we proceed with the extra credit items. They might have a different final
1407                  // weight in case the final grade was bounded. So we need to treat them different.
1408                  // Also, as we need to use the bounded_grade() method, we have to inject the
1409                  // right values there, and restore them afterwards.
1410                  $oldgrademax = $this->grade_item->grademax;
1411                  $oldgrademin = $this->grade_item->grademin;
1412                  foreach ($grade_values as $itemid => $gradevalue) {
1413                      if (!isset($extracredititems[$itemid])) {
1414                          continue;
1415                      }
1416                      $oldsum = $sum;
1417                      $weightedgrade = $gradevalue * $userweights[$itemid] * $grademax;
1418                      $sum += $weightedgrade;
1419  
1420                      // Only go through this when we need to record the weights.
1421                      if ($weights !== null) {
1422                          if ($grademax <= 0) {
1423                              // There are only extra credit items in this category,
1424                              // all the weights should be accurate (and be 0).
1425                              $weights[$itemid] = $userweights[$itemid];
1426                              continue;
1427                          }
1428  
1429                          $oldfinalgrade = $this->grade_item->bounded_grade($oldsum);
1430                          $newfinalgrade = $this->grade_item->bounded_grade($sum);
1431                          $finalgradediff = $newfinalgrade - $oldfinalgrade;
1432                          if ($finalgradediff <= 0) {
1433                              // This item did not contribute to the category total at all.
1434                              $weights[$itemid] = 0;
1435                          } else if ($finalgradediff < $weightedgrade) {
1436                              // The weight needs to be adjusted because only a portion of the
1437                              // extra credit item contributed to the category total.
1438                              $weights[$itemid] = $finalgradediff / ($gradevalue * $grademax);
1439                          } else {
1440                              // The weight was accurate.
1441                              $weights[$itemid] = $userweights[$itemid];
1442                          }
1443                      }
1444                  }
1445                  $this->grade_item->grademax = $oldgrademax;
1446                  $this->grade_item->grademin = $oldgrademin;
1447  
1448                  if ($grademax > 0) {
1449                      $agg_grade = $sum / $grademax; // Re-normalize score.
1450                  } else {
1451                      // Every item in the category is extra credit.
1452                      $agg_grade = $sum;
1453                      $grademax = $sum;
1454                  }
1455  
1456                  break;
1457  
1458              case GRADE_AGGREGATE_MEAN:    // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum)
1459              default:
1460                  $num = count($grade_values);
1461                  $sum = array_sum($grade_values);
1462                  $agg_grade = $sum / $num;
1463                  // Record the weights evenly.
1464                  if ($weights !== null && $num > 0) {
1465                      foreach ($grade_values as $itemid=>$grade_value) {
1466                          $weights[$itemid] = 1.0 / $num;
1467                      }
1468                  }
1469                  break;
1470          }
1471  
1472          return array('grade' => $agg_grade, 'grademin' => $grademin, 'grademax' => $grademax);
1473      }
1474  
1475      /**
1476       * Internal function that calculates the aggregated grade for this grade category
1477       *
1478       * Must be public as it is used by grade_grade::get_hiding_affected()
1479       *
1480       * @deprecated since Moodle 2.8
1481       * @param array $grade_values An array of values to be aggregated
1482       * @param array $items The array of grade_items
1483       * @return float The aggregate grade for this grade category
1484       */
1485      public function aggregate_values($grade_values, $items) {
1486          debugging('grade_category::aggregate_values() is deprecated.
1487                     Call grade_category::aggregate_values_and_adjust_bounds() instead.', DEBUG_DEVELOPER);
1488          $result = $this->aggregate_values_and_adjust_bounds($grade_values, $items);
1489          return $result['grade'];
1490      }
1491  
1492      /**
1493       * Some aggregation types may need to update their max grade.
1494       *
1495       * This must be executed after updating the weights as it relies on them.
1496       *
1497       * @return void
1498       */
1499      private function auto_update_max() {
1500          global $CFG, $DB;
1501          if ($this->aggregation != GRADE_AGGREGATE_SUM) {
1502              // not needed at all
1503              return;
1504          }
1505  
1506          // Find grade items of immediate children (category or grade items) and force site settings.
1507          $this->load_grade_item();
1508          $depends_on = $this->grade_item->depends_on();
1509  
1510          // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
1511          // wish to update the grades.
1512          $gradebookcalculationfreeze = 'gradebook_calculations_freeze_' . $this->courseid;
1513          $oldextracreditcalculation = isset($CFG->$gradebookcalculationfreeze) && ($CFG->$gradebookcalculationfreeze <= 20150627);
1514          // Only run if the gradebook isn't frozen.
1515          if (!$oldextracreditcalculation) {
1516              // Don't automatically update the max for calculated items.
1517              if ($this->grade_item->is_calculated()) {
1518                  return;
1519              }
1520          }
1521  
1522          $items = false;
1523          if (!empty($depends_on)) {
1524              list($usql, $params) = $DB->get_in_or_equal($depends_on);
1525              $sql = "SELECT *
1526                        FROM {grade_items}
1527                       WHERE id $usql";
1528              $items = $DB->get_records_sql($sql, $params);
1529          }
1530  
1531          if (!$items) {
1532  
1533              if ($this->grade_item->grademax != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
1534                  $this->grade_item->grademax  = 0;
1535                  $this->grade_item->grademin  = 0;
1536                  $this->grade_item->gradetype = GRADE_TYPE_VALUE;
1537                  $this->grade_item->update('aggregation');
1538              }
1539              return;
1540          }
1541  
1542          //find max grade possible
1543          $maxes = array();
1544  
1545          foreach ($items as $item) {
1546  
1547              if ($item->aggregationcoef > 0) {
1548                  // extra credit from this activity - does not affect total
1549                  continue;
1550              } else if ($item->aggregationcoef2 <= 0) {
1551                  // Items with a weight of 0 do not affect the total.
1552                  continue;
1553              }
1554  
1555              if ($item->gradetype == GRADE_TYPE_VALUE) {
1556                  $maxes[$item->id] = $item->grademax;
1557  
1558              } else if ($item->gradetype == GRADE_TYPE_SCALE) {
1559                  $maxes[$item->id] = $item->grademax; // 0 = nograde, 1 = first scale item, 2 = second scale item
1560              }
1561          }
1562  
1563          if ($this->can_apply_limit_rules()) {
1564              // Apply droplow and keephigh.
1565              $this->apply_limit_rules($maxes, $items);
1566          }
1567          $max = array_sum($maxes);
1568  
1569          // update db if anything changed
1570          if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
1571              $this->grade_item->grademax  = $max;
1572              $this->grade_item->grademin  = 0;
1573              $this->grade_item->gradetype = GRADE_TYPE_VALUE;
1574              $this->grade_item->update('aggregation');
1575          }
1576      }
1577  
1578      /**
1579       * Recalculate the weights of the grade items in this category.
1580       *
1581       * The category total is not updated here, a further call to
1582       * {@link self::auto_update_max()} is required.
1583       *
1584       * @return void
1585       */
1586      private function auto_update_weights() {
1587          global $CFG;
1588          if ($this->aggregation != GRADE_AGGREGATE_SUM) {
1589              // This is only required if we are using natural weights.
1590              return;
1591          }
1592          $children = $this->get_children();
1593  
1594          $gradeitem = null;
1595  
1596          // Calculate the sum of the grademax's of all the items within this category.
1597          $totalnonoverriddengrademax = 0;
1598          $totalgrademax = 0;
1599  
1600          // Out of 1, how much weight has been manually overriden by a user?
1601          $totaloverriddenweight  = 0;
1602          $totaloverriddengrademax  = 0;
1603  
1604          // Has every assessment in this category been overridden?
1605          $automaticgradeitemspresent = false;
1606          // Does the grade item require normalising?
1607          $requiresnormalising = false;
1608  
1609          // This array keeps track of the id and weight of every grade item that has been overridden.
1610          $overridearray = array();
1611          foreach ($children as $sortorder => $child) {
1612              $gradeitem = null;
1613  
1614              if ($child['type'] == 'item') {
1615                  $gradeitem = $child['object'];
1616              } else if ($child['type'] == 'category') {
1617                  $gradeitem = $child['object']->load_grade_item();
1618              }
1619  
1620              if ($gradeitem->gradetype == GRADE_TYPE_NONE || $gradeitem->gradetype == GRADE_TYPE_TEXT) {
1621                  // Text items and none items do not have a weight.
1622                  continue;
1623              } else if (!$this->aggregateoutcomes && $gradeitem->is_outcome_item()) {
1624                  // We will not aggregate outcome items, so we can ignore them.
1625                  continue;
1626              } else if (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE) {
1627                  // The scales are not included in the aggregation, ignore them.
1628                  continue;
1629              }
1630  
1631              // Record the ID and the weight for this grade item.
1632              $overridearray[$gradeitem->id] = array();
1633              $overridearray[$gradeitem->id]['extracredit'] = intval($gradeitem->aggregationcoef);
1634              $overridearray[$gradeitem->id]['weight'] = $gradeitem->aggregationcoef2;
1635              $overridearray[$gradeitem->id]['weightoverride'] = intval($gradeitem->weightoverride);
1636              // If this item has had its weight overridden then set the flag to true, but
1637              // only if all previous items were also overridden. Note that extra credit items
1638              // are counted as overridden grade items.
1639              if (!$gradeitem->weightoverride && $gradeitem->aggregationcoef == 0) {
1640                  $automaticgradeitemspresent = true;
1641              }
1642  
1643              if ($gradeitem->aggregationcoef > 0) {
1644                  // An extra credit grade item doesn't contribute to $totaloverriddengrademax.
1645                  continue;
1646              } else if ($gradeitem->weightoverride > 0 && $gradeitem->aggregationcoef2 <= 0) {
1647                  // An overriden item that defines a weight of 0 does not contribute to $totaloverriddengrademax.
1648                  continue;
1649              }
1650  
1651              $totalgrademax += $gradeitem->grademax;
1652              if ($gradeitem->weightoverride > 0) {
1653                  $totaloverriddenweight += $gradeitem->aggregationcoef2;
1654                  $totaloverriddengrademax += $gradeitem->grademax;
1655              }
1656          }
1657  
1658          // Initialise this variable (used to keep track of the weight override total).
1659          $normalisetotal = 0;
1660          // 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
1661          // other weights to zero and normalise the others.
1662          $overriddentotal = 0;
1663          // If the overridden weight total is higher than 1 then set the other untouched weights to zero.
1664          $setotherweightstozero = false;
1665          // Total up all of the weights.
1666          foreach ($overridearray as $gradeitemdetail) {
1667              // If the grade item has extra credit, then don't add it to the normalisetotal.
1668              if (!$gradeitemdetail['extracredit']) {
1669                  $normalisetotal += $gradeitemdetail['weight'];
1670              }
1671              // The overridden total comprises of items that are set as overridden, that aren't extra credit and have a value
1672              // greater than zero.
1673              if ($gradeitemdetail['weightoverride'] && !$gradeitemdetail['extracredit'] && $gradeitemdetail['weight'] > 0) {
1674                  // Add overriden weights up to see if they are greater than 1.
1675                  $overriddentotal += $gradeitemdetail['weight'];
1676              }
1677          }
1678          if ($overriddentotal > 1) {
1679              // Make sure that this catergory of weights gets normalised.
1680              $requiresnormalising = true;
1681              // The normalised weights are only the overridden weights, so we just use the total of those.
1682              $normalisetotal = $overriddentotal;
1683          }
1684  
1685          $totalnonoverriddengrademax = $totalgrademax - $totaloverriddengrademax;
1686  
1687          // This setting indicates if we should use algorithm prior to MDL-49257 fix for calculating extra credit weights.
1688          // Even though old algorith has bugs in it, we need to preserve existing grades.
1689          $gradebookcalculationfreeze = (int)get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);
1690          $oldextracreditcalculation = $gradebookcalculationfreeze && ($gradebookcalculationfreeze <= 20150619);
1691  
1692          reset($children);
1693          foreach ($children as $sortorder => $child) {
1694              $gradeitem = null;
1695  
1696              if ($child['type'] == 'item') {
1697                  $gradeitem = $child['object'];
1698              } else if ($child['type'] == 'category') {
1699                  $gradeitem = $child['object']->load_grade_item();
1700              }
1701  
1702              if ($gradeitem->gradetype == GRADE_TYPE_NONE || $gradeitem->gradetype == GRADE_TYPE_TEXT) {
1703                  // Text items and none items do not have a weight, no need to set their weight to
1704                  // zero as they must never be used during aggregation.
1705                  continue;
1706              } else if (!$this->aggregateoutcomes && $gradeitem->is_outcome_item()) {
1707                  // We will not aggregate outcome items, so we can ignore updating their weights.
1708                  continue;
1709              } else if (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE) {
1710                  // We will not aggregate the scales, so we can ignore upating their weights.
1711                  continue;
1712              } else if (!$oldextracreditcalculation && $gradeitem->aggregationcoef > 0 && $gradeitem->weightoverride) {
1713                  // For an item with extra credit ignore other weigths and overrides but do not change anything at all
1714                  // if it's weight was already overridden.
1715                  continue;
1716              }
1717  
1718              // Store the previous value here, no need to update if it is the same value.
1719              $prevaggregationcoef2 = $gradeitem->aggregationcoef2;
1720  
1721              if (!$oldextracreditcalculation && $gradeitem->aggregationcoef > 0 && !$gradeitem->weightoverride) {
1722                  // For an item with extra credit ignore other weigths and overrides.
1723                  $gradeitem->aggregationcoef2 = $totalgrademax ? ($gradeitem->grademax / $totalgrademax) : 0;
1724  
1725              } else if (!$gradeitem->weightoverride) {
1726                  // Calculations with a grade maximum of zero will cause problems. Just set the weight to zero.
1727                  if ($totaloverriddenweight >= 1 || $totalnonoverriddengrademax == 0 || $gradeitem->grademax == 0) {
1728                      // There is no more weight to distribute.
1729                      $gradeitem->aggregationcoef2 = 0;
1730                  } else {
1731                      // Calculate this item's weight as a percentage of the non-overridden total grade maxes
1732                      // then convert it to a proportion of the available non-overriden weight.
1733                      $gradeitem->aggregationcoef2 = ($gradeitem->grademax/$totalnonoverriddengrademax) *
1734                              (1 - $totaloverriddenweight);
1735                  }
1736  
1737              } else if ((!$automaticgradeitemspresent && $normalisetotal != 1) || ($requiresnormalising)
1738                      || $overridearray[$gradeitem->id]['weight'] < 0) {
1739                  // Just divide the overriden weight for this item against the total weight override of all
1740                  // items in this category.
1741                  if ($normalisetotal == 0 || $overridearray[$gradeitem->id]['weight'] < 0) {
1742                      // If the normalised total equals zero, or the weight value is less than zero,
1743                      // set the weight for the grade item to zero.
1744                      $gradeitem->aggregationcoef2 = 0;
1745                  } else {
1746                      $gradeitem->aggregationcoef2 = $overridearray[$gradeitem->id]['weight'] / $normalisetotal;
1747                  }
1748              }
1749  
1750              if (grade_floatval($prevaggregationcoef2) !== grade_floatval($gradeitem->aggregationcoef2)) {
1751                  // Update the grade item to reflect these changes.
1752                  $gradeitem->update();
1753              }
1754          }
1755      }
1756  
1757      /**
1758       * Given an array of grade values (numerical indices) applies droplow or keephigh rules to limit the final array.
1759       *
1760       * @param array $grade_values itemid=>$grade_value float
1761       * @param array $items grade item objects
1762       * @return array Limited grades.
1763       */
1764      public function apply_limit_rules(&$grade_values, $items) {
1765          $extraused = $this->is_extracredit_used();
1766  
1767          if (!empty($this->droplow)) {
1768              asort($grade_values, SORT_NUMERIC);
1769              $dropped = 0;
1770  
1771              // If we have fewer grade items available to drop than $this->droplow, use this flag to escape the loop
1772              // May occur because of "extra credit" or if droplow is higher than the number of grade items
1773              $droppedsomething = true;
1774  
1775              while ($dropped < $this->droplow && $droppedsomething) {
1776                  $droppedsomething = false;
1777  
1778                  $grade_keys = array_keys($grade_values);
1779                  $gradekeycount = count($grade_keys);
1780  
1781                  if ($gradekeycount === 0) {
1782                      //We've dropped all grade items
1783                      break;
1784                  }
1785  
1786                  $originalindex = $founditemid = $foundmax = null;
1787  
1788                  // Find the first remaining grade item that is available to be dropped
1789                  foreach ($grade_keys as $gradekeyindex=>$gradekey) {
1790                      if (!$extraused || $items[$gradekey]->aggregationcoef <= 0) {
1791                          // Found a non-extra credit grade item that is eligible to be dropped
1792                          $originalindex = $gradekeyindex;
1793                          $founditemid = $grade_keys[$originalindex];
1794                          $foundmax = $items[$founditemid]->grademax;
1795                          break;
1796                      }
1797                  }
1798  
1799                  if (empty($founditemid)) {
1800                      // No grade items available to drop
1801                      break;
1802                  }
1803  
1804                  // Now iterate over the remaining grade items
1805                  // We're looking for other grade items with the same grade value but a higher grademax
1806                  $i = 1;
1807                  while ($originalindex + $i < $gradekeycount) {
1808  
1809                      $possibleitemid = $grade_keys[$originalindex+$i];
1810                      $i++;
1811  
1812                      if ($grade_values[$founditemid] != $grade_values[$possibleitemid]) {
1813                          // The next grade item has a different grade value. Stop looking.
1814                          break;
1815                      }
1816  
1817                      if ($extraused && $items[$possibleitemid]->aggregationcoef > 0) {
1818                          // Don't drop extra credit grade items. Continue the search.
1819                          continue;
1820                      }
1821  
1822                      if ($foundmax < $items[$possibleitemid]->grademax) {
1823                          // Found a grade item with the same grade value and a higher grademax
1824                          $foundmax = $items[$possibleitemid]->grademax;
1825                          $founditemid = $possibleitemid;
1826                          // Continue searching to see if there is an even higher grademax
1827                      }
1828                  }
1829  
1830                  // Now drop whatever grade item we have found
1831                  unset($grade_values[$founditemid]);
1832                  $dropped++;
1833                  $droppedsomething = true;
1834              }
1835  
1836          } else if (!empty($this->keephigh)) {
1837              arsort($grade_values, SORT_NUMERIC);
1838              $kept = 0;
1839  
1840              foreach ($grade_values as $itemid=>$value) {
1841  
1842                  if ($extraused and $items[$itemid]->aggregationcoef > 0) {
1843                      // we keep all extra credits
1844  
1845                  } else if ($kept < $this->keephigh) {
1846                      $kept++;
1847  
1848                  } else {
1849                      unset($grade_values[$itemid]);
1850                  }
1851              }
1852          }
1853      }
1854  
1855      /**
1856       * Returns whether or not we can apply the limit rules.
1857       *
1858       * There are cases where drop lowest or keep highest should not be used
1859       * at all. This method will determine whether or not this logic can be
1860       * applied considering the current setup of the category.
1861       *
1862       * @return bool
1863       */
1864      public function can_apply_limit_rules() {
1865          if ($this->canapplylimitrules !== null) {
1866              return $this->canapplylimitrules;
1867          }
1868  
1869          // Set it to be supported by default.
1870          $this->canapplylimitrules = true;
1871  
1872          // Natural aggregation.
1873          if ($this->aggregation == GRADE_AGGREGATE_SUM) {
1874              $canapply = true;
1875  
1876              // Check until one child breaks the rules.
1877              $gradeitems = $this->get_children();
1878              $validitems = 0;
1879              $lastweight = null;
1880              $lastmaxgrade = null;
1881              foreach ($gradeitems as $gradeitem) {
1882                  $gi = $gradeitem['object'];
1883  
1884                  if ($gradeitem['type'] == 'category') {
1885                      // Sub categories are not allowed because they can have dynamic weights/maxgrades.
1886                      $canapply = false;
1887                      break;
1888                  }
1889  
1890                  if ($gi->aggregationcoef > 0) {
1891                      // Extra credit items are not allowed.
1892                      $canapply = false;
1893                      break;
1894                  }
1895  
1896                  if ($lastweight !== null && $lastweight != $gi->aggregationcoef2) {
1897                      // One of the weight differs from another item.
1898                      $canapply = false;
1899                      break;
1900                  }
1901  
1902                  if ($lastmaxgrade !== null && $lastmaxgrade != $gi->grademax) {
1903                      // One of the max grade differ from another item. This is not allowed for now
1904                      // because we could be end up with different max grade between users for this category.
1905                      $canapply = false;
1906                      break;
1907                  }
1908  
1909                  $lastweight = $gi->aggregationcoef2;
1910                  $lastmaxgrade = $gi->grademax;
1911              }
1912  
1913              $this->canapplylimitrules = $canapply;
1914          }
1915  
1916          return $this->canapplylimitrules;
1917      }
1918  
1919      /**
1920       * Returns true if category uses extra credit of any kind
1921       *
1922       * @return bool True if extra credit used
1923       */
1924      public function is_extracredit_used() {
1925          return self::aggregation_uses_extracredit($this->aggregation);
1926      }
1927  
1928      /**
1929       * Returns true if aggregation passed is using extracredit.
1930       *
1931       * @param int $aggregation Aggregation const.
1932       * @return bool True if extra credit used
1933       */
1934      public static function aggregation_uses_extracredit($aggregation) {
1935          return ($aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2
1936               or $aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
1937               or $aggregation == GRADE_AGGREGATE_SUM);
1938      }
1939  
1940      /**
1941       * Returns true if category uses special aggregation coefficient
1942       *
1943       * @return bool True if an aggregation coefficient is being used
1944       */
1945      public function is_aggregationcoef_used() {
1946          return self::aggregation_uses_aggregationcoef($this->aggregation);
1947  
1948      }
1949  
1950      /**
1951       * Returns true if aggregation uses aggregationcoef
1952       *
1953       * @param int $aggregation Aggregation const.
1954       * @return bool True if an aggregation coefficient is being used
1955       */
1956      public static function aggregation_uses_aggregationcoef($aggregation) {
1957          return ($aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN
1958               or $aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2
1959               or $aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
1960               or $aggregation == GRADE_AGGREGATE_SUM);
1961  
1962      }
1963  
1964      /**
1965       * Recursive function to find which weight/extra credit field to use in the grade item form.
1966       *
1967       * @param string $first Whether or not this is the first item in the recursion
1968       * @return string
1969       */
1970      public function get_coefstring($first=true) {
1971          if (!is_null($this->coefstring)) {
1972              return $this->coefstring;
1973          }
1974  
1975          $overriding_coefstring = null;
1976  
1977          // Stop recursing upwards if this category has no parent
1978          if (!$first) {
1979  
1980              if ($parent_category = $this->load_parent_category()) {
1981                  return $parent_category->get_coefstring(false);
1982  
1983              } else {
1984                  return null;
1985              }
1986  
1987          } else if ($first) {
1988  
1989              if ($parent_category = $this->load_parent_category()) {
1990                  $overriding_coefstring = $parent_category->get_coefstring(false);
1991              }
1992          }
1993  
1994          // If an overriding coefstring has trickled down from one of the parent categories, return it. Otherwise, return self.
1995          if (!is_null($overriding_coefstring)) {
1996              return $overriding_coefstring;
1997          }
1998  
1999          // No parent category is overriding this category's aggregation, return its string
2000          if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
2001              $this->coefstring = 'aggregationcoefweight';
2002  
2003          } else if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
2004              $this->coefstring = 'aggregationcoefextrasum';
2005  
2006          } else if ($this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
2007              $this->coefstring = 'aggregationcoefextraweight';
2008  
2009          } else if ($this->aggregation == GRADE_AGGREGATE_SUM) {
2010              $this->coefstring = 'aggregationcoefextraweightsum';
2011  
2012          } else {
2013              $this->coefstring = 'aggregationcoef';
2014          }
2015          return $this->coefstring;
2016      }
2017  
2018      /**
2019       * Returns tree with all grade_items and categories as elements
2020       *
2021       * @param int $courseid The course ID
2022       * @param bool $include_category_items as category children
2023       * @return array
2024       */
2025      public static function fetch_course_tree($courseid, $include_category_items=false) {
2026          $course_category = grade_category::fetch_course_category($courseid);
2027          $category_array = array('object'=>$course_category, 'type'=>'category', 'depth'=>1,
2028                                  'children'=>$course_category->get_children($include_category_items));
2029  
2030          $course_category->sortorder = $course_category->get_sortorder();
2031          $sortorder = $course_category->get_sortorder();
2032          return grade_category::_fetch_course_tree_recursion($category_array, $sortorder);
2033      }
2034  
2035      /**
2036       * An internal function that recursively sorts grade categories within a course
2037       *
2038       * @param array $category_array The seed of the recursion
2039       * @param int   $sortorder The current sortorder
2040       * @return array An array containing 'object', 'type', 'depth' and optionally 'children'
2041       */
2042      static private function _fetch_course_tree_recursion($category_array, &$sortorder) {
2043          if (isset($category_array['object']->gradetype) && $category_array['object']->gradetype==GRADE_TYPE_NONE) {
2044              return null;
2045          }
2046  
2047          // store the grade_item or grade_category instance with extra info
2048          $result = array('object'=>$category_array['object'], 'type'=>$category_array['type'], 'depth'=>$category_array['depth']);
2049  
2050          // reuse final grades if there
2051          if (array_key_exists('finalgrades', $category_array)) {
2052              $result['finalgrades'] = $category_array['finalgrades'];
2053          }
2054  
2055          // recursively resort children
2056          if (!empty($category_array['children'])) {
2057              $result['children'] = array();
2058              //process the category item first
2059              $child = null;
2060  
2061              foreach ($category_array['children'] as $oldorder=>$child_array) {
2062  
2063                  if ($child_array['type'] == 'courseitem' or $child_array['type'] == 'categoryitem') {
2064                      $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
2065                      if (!empty($child)) {
2066                          $result['children'][$sortorder] = $child;
2067                      }
2068                  }
2069              }
2070  
2071              foreach ($category_array['children'] as $oldorder=>$child_array) {
2072  
2073                  if ($child_array['type'] != 'courseitem' and $child_array['type'] != 'categoryitem') {
2074                      $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
2075                      if (!empty($child)) {
2076                          $result['children'][++$sortorder] = $child;
2077                      }
2078                  }
2079              }
2080          }
2081  
2082          return $result;
2083      }
2084  
2085      /**
2086       * Fetches and returns all the children categories and/or grade_items belonging to this category.
2087       * By default only returns the immediate children (depth=1), but deeper levels can be requested,
2088       * as well as all levels (0). The elements are indexed by sort order.
2089       *
2090       * @param bool $include_category_items Whether or not to include category grade_items in the children array
2091       * @return array Array of child objects (grade_category and grade_item).
2092       */
2093      public function get_children($include_category_items=false) {
2094          global $DB;
2095  
2096          // This function must be as fast as possible ;-)
2097          // fetch all course grade items and categories into memory - we do not expect hundreds of these in course
2098          // we have to limit the number of queries though, because it will be used often in grade reports
2099  
2100          $cats  = $DB->get_records('grade_categories', array('courseid' => $this->courseid));
2101          $items = $DB->get_records('grade_items', array('courseid' => $this->courseid));
2102  
2103          // init children array first
2104          foreach ($cats as $catid=>$cat) {
2105              $cats[$catid]->children = array();
2106          }
2107  
2108          //first attach items to cats and add category sortorder
2109          foreach ($items as $item) {
2110  
2111              if ($item->itemtype == 'course' or $item->itemtype == 'category') {
2112                  $cats[$item->iteminstance]->sortorder = $item->sortorder;
2113  
2114                  if (!$include_category_items) {
2115                      continue;
2116                  }
2117                  $categoryid = $item->iteminstance;
2118  
2119              } else {
2120                  $categoryid = $item->categoryid;
2121                  if (empty($categoryid)) {
2122                      debugging('Found a grade item that isnt in a category');
2123                  }
2124              }
2125  
2126              // prevent problems with duplicate sortorders in db
2127              $sortorder = $item->sortorder;
2128  
2129              while (array_key_exists($categoryid, $cats)
2130                  && array_key_exists($sortorder, $cats[$categoryid]->children)) {
2131  
2132                  $sortorder++;
2133              }
2134  
2135              $cats[$categoryid]->children[$sortorder] = $item;
2136  
2137          }
2138  
2139          // now find the requested category and connect categories as children
2140          $category = false;
2141  
2142          foreach ($cats as $catid=>$cat) {
2143  
2144              if (empty($cat->parent)) {
2145  
2146                  if ($cat->path !== '/'.$cat->id.'/') {
2147                      $grade_category = new grade_category($cat, false);
2148                      $grade_category->path  = '/'.$cat->id.'/';
2149                      $grade_category->depth = 1;
2150                      $grade_category->update('system');
2151                      return $this->get_children($include_category_items);
2152                  }
2153  
2154              } else {
2155  
2156                  if (empty($cat->path) or !preg_match('|/'.$cat->parent.'/'.$cat->id.'/$|', $cat->path)) {
2157                      //fix paths and depts
2158                      static $recursioncounter = 0; // prevents infinite recursion
2159                      $recursioncounter++;
2160  
2161                      if ($recursioncounter < 5) {
2162                          // fix paths and depths!
2163                          $grade_category = new grade_category($cat, false);
2164                          $grade_category->depth = 0;
2165                          $grade_category->path  = null;
2166                          $grade_category->update('system');
2167                          return $this->get_children($include_category_items);
2168                      }
2169                  }
2170                  // prevent problems with duplicate sortorders in db
2171                  $sortorder = $cat->sortorder;
2172  
2173                  while (array_key_exists($sortorder, $cats[$cat->parent]->children)) {
2174                      //debugging("$sortorder exists in cat loop");
2175                      $sortorder++;
2176                  }
2177  
2178                  $cats[$cat->parent]->children[$sortorder] = &$cats[$catid];
2179              }
2180  
2181              if ($catid == $this->id) {
2182                  $category = &$cats[$catid];
2183              }
2184          }
2185  
2186          unset($items); // not needed
2187          unset($cats); // not needed
2188  
2189          $children_array = array();
2190          if (is_object($category)) {
2191              $children_array = grade_category::_get_children_recursion($category);
2192              ksort($children_array);
2193          }
2194  
2195          return $children_array;
2196  
2197      }
2198  
2199      /**
2200       * Private method used to retrieve all children of this category recursively
2201       *
2202       * @param grade_category $category Source of current recursion
2203       * @return array An array of child grade categories
2204       */
2205      private static function _get_children_recursion($category) {
2206  
2207          $children_array = array();
2208          foreach ($category->children as $sortorder=>$child) {
2209  
2210              if (property_exists($child, 'itemtype')) {
2211                  $grade_item = new grade_item($child, false);
2212  
2213                  if (in_array($grade_item->itemtype, array('course', 'category'))) {
2214                      $type  = $grade_item->itemtype.'item';
2215                      $depth = $category->depth;
2216  
2217                  } else {
2218                      $type  = 'item';
2219                      $depth = $category->depth; // we use this to set the same colour
2220                  }
2221                  $children_array[$sortorder] = array('object'=>$grade_item, 'type'=>$type, 'depth'=>$depth);
2222  
2223              } else {
2224                  $children = grade_category::_get_children_recursion($child);
2225                  $grade_category = new grade_category($child, false);
2226  
2227                  if (empty($children)) {
2228                      $children = array();
2229                  }
2230                  $children_array[$sortorder] = array('object'=>$grade_category, 'type'=>'category', 'depth'=>$grade_category->depth, 'children'=>$children);
2231              }
2232          }
2233  
2234          // sort the array
2235          ksort($children_array);
2236  
2237          return $children_array;
2238      }
2239  
2240      /**
2241       * Uses {@link get_grade_item()} to load or create a grade_item, then saves it as $this->grade_item.
2242       *
2243       * @return grade_item
2244       */
2245      public function load_grade_item() {
2246          if (empty($this->grade_item)) {
2247              $this->grade_item = $this->get_grade_item();
2248          }
2249          return $this->grade_item;
2250      }
2251  
2252      /**
2253       * Retrieves this grade categories' associated grade_item from the database
2254       *
2255       * If no grade_item exists yet, creates one.
2256       *
2257       * @return grade_item
2258       */
2259      public function get_grade_item() {
2260          if (empty($this->id)) {
2261              debugging("Attempt to obtain a grade_category's associated grade_item without the category's ID being set.");
2262              return false;
2263          }
2264  
2265          if (empty($this->parent)) {
2266              $params = array('courseid'=>$this->courseid, 'itemtype'=>'course', 'iteminstance'=>$this->id);
2267  
2268          } else {
2269              $params = array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id);
2270          }
2271  
2272          if (!$grade_items = grade_item::fetch_all($params)) {
2273              // create a new one
2274              $grade_item = new grade_item($params, false);
2275              $grade_item->gradetype = GRADE_TYPE_VALUE;
2276              $grade_item->insert('system');
2277  
2278          } else if (count($grade_items) == 1) {
2279              // found existing one
2280              $grade_item = reset($grade_items);
2281  
2282          } else {
2283              debugging("Found more than one grade_item attached to category id:".$this->id);
2284              // return first one
2285              $grade_item = reset($grade_items);
2286          }
2287  
2288          return $grade_item;
2289      }
2290  
2291      /**
2292       * Uses $this->parent to instantiate $this->parent_category based on the referenced record in the DB
2293       *
2294       * @return grade_category The parent category
2295       */
2296      public function load_parent_category() {
2297          if (empty($this->parent_category) && !empty($this->parent)) {
2298              $this->parent_category = $this->get_parent_category();
2299          }
2300          return $this->parent_category;
2301      }
2302  
2303      /**
2304       * Uses $this->parent to instantiate and return a grade_category object
2305       *
2306       * @return grade_category Returns the parent category or null if this category has no parent
2307       */
2308      public function get_parent_category() {
2309          if (!empty($this->parent)) {
2310              $parent_category = new grade_category(array('id' => $this->parent));
2311              return $parent_category;
2312          } else {
2313              return null;
2314          }
2315      }
2316  
2317      /**
2318       * Returns the most descriptive field for this grade category
2319       *
2320       * @return string name
2321       * @param bool $escape Whether the returned category name is to be HTML escaped or not.
2322       */
2323      public function get_name($escape = true) {
2324          global $DB;
2325          // For a course category, we return the course name if the fullname is set to '?' in the DB (empty in the category edit form)
2326          if (empty($this->parent) && $this->fullname == '?') {
2327              $course = $DB->get_record('course', array('id'=> $this->courseid));
2328              return format_string($course->fullname, false, ['context' => context_course::instance($this->courseid),
2329                  'escape' => $escape]);
2330  
2331          } else {
2332              // Grade categories can't be set up at system context (unlike scales and outcomes)
2333              // We therefore must have a courseid, and don't need to handle system contexts when filtering.
2334              return format_string($this->fullname, false, ['context' => context_course::instance($this->courseid),
2335                  'escape' => $escape]);
2336          }
2337      }
2338  
2339      /**
2340       * Describe the aggregation settings for this category so the reports make more sense.
2341       *
2342       * @return string description
2343       */
2344      public function get_description() {
2345          $allhelp = array();
2346          if ($this->aggregation != GRADE_AGGREGATE_SUM) {
2347              $aggrstrings = grade_helper::get_aggregation_strings();
2348              $allhelp[] = $aggrstrings[$this->aggregation];
2349          }
2350  
2351          if ($this->droplow && $this->can_apply_limit_rules()) {
2352              $allhelp[] = get_string('droplowestvalues', 'grades', $this->droplow);
2353          }
2354          if ($this->keephigh && $this->can_apply_limit_rules()) {
2355              $allhelp[] = get_string('keephighestvalues', 'grades', $this->keephigh);
2356          }
2357          if (!$this->aggregateonlygraded) {
2358              $allhelp[] = get_string('aggregatenotonlygraded', 'grades');
2359          }
2360          if ($allhelp) {
2361              return implode('. ', $allhelp) . '.';
2362          }
2363          return '';
2364      }
2365  
2366      /**
2367       * Sets this category's parent id
2368       *
2369       * @param int $parentid The ID of the category that is the new parent to $this
2370       * @param string $source From where was the object updated (mod/forum, manual, etc.)
2371       * @return bool success
2372       */
2373      public function set_parent($parentid, $source=null) {
2374          if ($this->parent == $parentid) {
2375              return true;
2376          }
2377  
2378          if ($parentid == $this->id) {
2379              print_error('cannotassignselfasparent');
2380          }
2381  
2382          if (empty($this->parent) and $this->is_course_category()) {
2383              print_error('cannothaveparentcate');
2384          }
2385  
2386          // find parent and check course id
2387          if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
2388              return false;
2389          }
2390  
2391          $this->force_regrading();
2392  
2393          // set new parent category
2394          $this->parent          = $parent_category->id;
2395          $this->parent_category =& $parent_category;
2396          $this->path            = null;       // remove old path and depth - will be recalculated in update()
2397          $this->depth           = 0;          // remove old path and depth - will be recalculated in update()
2398          $this->update($source);
2399  
2400          return $this->update($source);
2401      }
2402  
2403      /**
2404       * Returns the final grade values for this grade category.
2405       *
2406       * @param int $userid Optional user ID to retrieve a single user's final grade
2407       * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
2408       */
2409      public function get_final($userid=null) {
2410          $this->load_grade_item();
2411          return $this->grade_item->get_final($userid);
2412      }
2413  
2414      /**
2415       * Returns the sortorder of the grade categories' associated grade_item
2416       *
2417       * This method is also available in grade_item for cases where the object type is not known.
2418       *
2419       * @return int Sort order
2420       */
2421      public function get_sortorder() {
2422          $this->load_grade_item();
2423          return $this->grade_item->get_sortorder();
2424      }
2425  
2426      /**
2427       * Returns the idnumber of the grade categories' associated grade_item.
2428       *
2429       * This method is also available in grade_item for cases where the object type is not known.
2430       *
2431       * @return string idnumber
2432       */
2433      public function get_idnumber() {
2434          $this->load_grade_item();
2435          return $this->grade_item->get_idnumber();
2436      }
2437  
2438      /**
2439       * Sets the sortorder variable for this category.
2440       *
2441       * This method is also available in grade_item, for cases where the object type is not know.
2442       *
2443       * @param int $sortorder The sortorder to assign to this category
2444       */
2445      public function set_sortorder($sortorder) {
2446          $this->load_grade_item();
2447          $this->grade_item->set_sortorder($sortorder);
2448      }
2449  
2450      /**
2451       * Move this category after the given sortorder
2452       *
2453       * Does not change the parent
2454       *
2455       * @param int $sortorder to place after.
2456       * @return void
2457       */
2458      public function move_after_sortorder($sortorder) {
2459          $this->load_grade_item();
2460          $this->grade_item->move_after_sortorder($sortorder);
2461      }
2462  
2463      /**
2464       * Return true if this is the top most category that represents the total course grade.
2465       *
2466       * @return bool
2467       */
2468      public function is_course_category() {
2469          $this->load_grade_item();
2470          return $this->grade_item->is_course_item();
2471      }
2472  
2473      /**
2474       * Return the course level grade_category object
2475       *
2476       * @param int $courseid The Course ID
2477       * @return grade_category Returns the course level grade_category instance
2478       */
2479      public static function fetch_course_category($courseid) {
2480          if (empty($courseid)) {
2481              debugging('Missing course id!');
2482              return false;
2483          }
2484  
2485          // course category has no parent
2486          if ($course_category = grade_category::fetch(array('courseid'=>$courseid, 'parent'=>null))) {
2487              return $course_category;
2488          }
2489  
2490          // create a new one
2491          $course_category = new grade_category();
2492          $course_category->insert_course_category($courseid);
2493  
2494          return $course_category;
2495      }
2496  
2497      /**
2498       * Is grading object editable?
2499       *
2500       * @return bool
2501       */
2502      public function is_editable() {
2503          return true;
2504      }
2505  
2506      /**
2507       * Returns the locked state/date of the grade categories' associated grade_item.
2508       *
2509       * This method is also available in grade_item, for cases where the object type is not known.
2510       *
2511       * @return bool
2512       */
2513      public function is_locked() {
2514          $this->load_grade_item();
2515          return $this->grade_item->is_locked();
2516      }
2517  
2518      /**
2519       * Sets the grade_item's locked variable and updates the grade_item.
2520       *
2521       * Calls set_locked() on the categories' grade_item
2522       *
2523       * @param int  $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
2524       * @param bool $cascade lock/unlock child objects too
2525       * @param bool $refresh refresh grades when unlocking
2526       * @return bool success if category locked (not all children mayb be locked though)
2527       */
2528      public function set_locked($lockedstate, $cascade=false, $refresh=true) {
2529          $this->load_grade_item();
2530  
2531          $result = $this->grade_item->set_locked($lockedstate, $cascade, true);
2532  
2533          if ($cascade) {
2534              //process all children - items and categories
2535              if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
2536  
2537                  foreach ($children as $child) {
2538                      $child->set_locked($lockedstate, true, false);
2539  
2540                      if (empty($lockedstate) and $refresh) {
2541                          //refresh when unlocking
2542                          $child->refresh_grades();
2543                      }
2544                  }
2545              }
2546  
2547              if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
2548  
2549                  foreach ($children as $child) {
2550                      $child->set_locked($lockedstate, true, true);
2551                  }
2552              }
2553          }
2554  
2555          return $result;
2556      }
2557  
2558      /**
2559       * Overrides grade_object::set_properties() to add special handling for changes to category aggregation types
2560       *
2561       * @param stdClass $instance the object to set the properties on
2562       * @param array|stdClass $params Either an associative array or an object containing property name, property value pairs
2563       */
2564      public static function set_properties(&$instance, $params) {
2565          global $DB;
2566  
2567          $fromaggregation = $instance->aggregation;
2568  
2569          parent::set_properties($instance, $params);
2570  
2571          // The aggregation method is changing and this category has already been saved.
2572          if (isset($params->aggregation) && !empty($instance->id)) {
2573              $achildwasdupdated = false;
2574  
2575              // Get all its children.
2576              $children = $instance->get_children();
2577              foreach ($children as $child) {
2578                  $item = $child['object'];
2579                  if ($child['type'] == 'category') {
2580                      $item = $item->load_grade_item();
2581                  }
2582  
2583                  // Set the new aggregation fields.
2584                  if ($item->set_aggregation_fields_for_aggregation($fromaggregation, $params->aggregation)) {
2585                      $item->update();
2586                      $achildwasdupdated = true;
2587                  }
2588              }
2589  
2590              // If this is the course category, it is possible that its grade item was set as needsupdate
2591              // by one of its children. If we keep a reference to that stale object we might cause the
2592              // needsupdate flag to be lost. It's safer to just reload the grade_item from the database.
2593              if ($achildwasdupdated && !empty($instance->grade_item) && $instance->is_course_category()) {
2594                  $instance->grade_item = null;
2595                  $instance->load_grade_item();
2596              }
2597          }
2598      }
2599  
2600      /**
2601       * Sets the grade_item's hidden variable and updates the grade_item.
2602       *
2603       * Overrides grade_item::set_hidden() to add cascading of the hidden value to grade items in this grade category
2604       *
2605       * @param int $hidden 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
2606       * @param bool $cascade apply to child objects too
2607       */
2608      public function set_hidden($hidden, $cascade=false) {
2609          $this->load_grade_item();
2610          //this hides the category itself and everything it contains
2611          parent::set_hidden($hidden, $cascade);
2612  
2613          if ($cascade) {
2614  
2615              // This hides the associated grade item (the course/category total).
2616              $this->grade_item->set_hidden($hidden, $cascade);
2617  
2618              if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
2619  
2620                  foreach ($children as $child) {
2621                      if ($child->can_control_visibility()) {
2622                          $child->set_hidden($hidden, $cascade);
2623                      }
2624                  }
2625              }
2626  
2627              if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
2628  
2629                  foreach ($children as $child) {
2630                      $child->set_hidden($hidden, $cascade);
2631                  }
2632              }
2633          }
2634  
2635          //if marking category visible make sure parent category is visible MDL-21367
2636          if( !$hidden ) {
2637              $category_array = grade_category::fetch_all(array('id'=>$this->parent));
2638              if ($category_array && array_key_exists($this->parent, $category_array)) {
2639                  $category = $category_array[$this->parent];
2640                  //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
2641                  $category->set_hidden($hidden, false);
2642              }
2643          }
2644      }
2645  
2646      /**
2647       * Applies default settings on this category
2648       *
2649       * @return bool True if anything changed
2650       */
2651      public function apply_default_settings() {
2652          global $CFG;
2653  
2654          foreach ($this->forceable as $property) {
2655  
2656              if (isset($CFG->{"grade_$property"})) {
2657  
2658                  if ($CFG->{"grade_$property"} == -1) {
2659                      continue; //temporary bc before version bump
2660                  }
2661                  $this->$property = $CFG->{"grade_$property"};
2662              }
2663          }
2664      }
2665  
2666      /**
2667       * Applies forced settings on this category
2668       *
2669       * @return bool True if anything changed
2670       */
2671      public function apply_forced_settings() {
2672          global $CFG;
2673  
2674          $updated = false;
2675  
2676          foreach ($this->forceable as $property) {
2677  
2678              if (isset($CFG->{"grade_$property"}) and isset($CFG->{"grade_{$property}_flag"}) and
2679                                                      ((int) $CFG->{"grade_{$property}_flag"} & 1)) {
2680  
2681                  if ($CFG->{"grade_$property"} == -1) {
2682                      continue; //temporary bc before version bump
2683                  }
2684                  $this->$property = $CFG->{"grade_$property"};
2685                  $updated = true;
2686              }
2687          }
2688  
2689          return $updated;
2690      }
2691  
2692      /**
2693       * Notification of change in forced category settings.
2694       *
2695       * Causes all course and category grade items to be marked as needing to be updated
2696       */
2697      public static function updated_forced_settings() {
2698          global $CFG, $DB;
2699          $params = array(1, 'course', 'category');
2700          $sql = "UPDATE {grade_items} SET needsupdate=? WHERE itemtype=? or itemtype=?";
2701          $DB->execute($sql, $params);
2702      }
2703  
2704      /**
2705       * Determine the default aggregation values for a given aggregation method.
2706       *
2707       * @param int $aggregationmethod The aggregation method constant value.
2708       * @return array Containing the keys 'aggregationcoef', 'aggregationcoef2' and 'weightoverride'.
2709       */
2710      public static function get_default_aggregation_coefficient_values($aggregationmethod) {
2711          $defaultcoefficients = array(
2712              'aggregationcoef' => 0,
2713              'aggregationcoef2' => 0,
2714              'weightoverride' => 0
2715          );
2716  
2717          switch ($aggregationmethod) {
2718              case GRADE_AGGREGATE_WEIGHTED_MEAN:
2719                  $defaultcoefficients['aggregationcoef'] = 1;
2720                  break;
2721              case GRADE_AGGREGATE_SUM:
2722                  $defaultcoefficients['aggregationcoef2'] = 1;
2723                  break;
2724          }
2725  
2726          return $defaultcoefficients;
2727      }
2728  
2729      /**
2730       * Cleans the cache.
2731       *
2732       * We invalidate them all so it can be completely reloaded.
2733       *
2734       * Being conservative here, if there is a new grade_category we purge them, the important part
2735       * is that this is not purged when there are no changes in grade_categories.
2736       *
2737       * @param bool $deleted
2738       * @return void
2739       */
2740      protected function notify_changed($deleted) {
2741          self::clean_record_set();
2742      }
2743  
2744      /**
2745       * Generates a unique key per query.
2746       *
2747       * Not unique between grade_object children. self::retrieve_record_set and self::set_record_set will be in charge of
2748       * selecting the appropriate cache.
2749       *
2750       * @param array $params An array of conditions like $fieldname => $fieldvalue
2751       * @return string
2752       */
2753      protected static function generate_record_set_key($params) {
2754          return sha1(json_encode($params));
2755      }
2756  
2757      /**
2758       * Tries to retrieve a record set from the cache.
2759       *
2760       * @param array $params The query params
2761       * @return grade_object[]|bool An array of grade_objects or false if not found.
2762       */
2763      protected static function retrieve_record_set($params) {
2764          $cache = cache::make('core', 'grade_categories');
2765          return $cache->get(self::generate_record_set_key($params));
2766      }
2767  
2768      /**
2769       * Sets a result to the records cache, even if there were no results.
2770       *
2771       * @param string $params The query params
2772       * @param grade_object[]|bool $records An array of grade_objects or false if there are no records matching the $key filters
2773       * @return void
2774       */
2775      protected static function set_record_set($params, $records) {
2776          $cache = cache::make('core', 'grade_categories');
2777          return $cache->set(self::generate_record_set_key($params), $records);
2778      }
2779  
2780      /**
2781       * Cleans the cache.
2782       *
2783       * Aggressive deletion to be conservative given the gradebook design.
2784       * The key is based on the requested params, not easy nor worth to purge selectively.
2785       *
2786       * @return void
2787       */
2788      public static function clean_record_set() {
2789          cache_helper::purge_by_event('changesingradecategories');
2790      }
2791  }