[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/lib/grade/ -> grade_category.php (source)

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1