Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

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

   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 an individual user's grade
  19   *
  20   * @package   core_grades
  21   * @category  grade
  22   * @copyright 2006 Nicolas Connault
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once ('grade_object.php');
  29  
  30  /**
  31   * grade_grades is an object mapped to DB table {prefix}grade_grades
  32   *
  33   * @package   core_grades
  34   * @category  grade
  35   * @copyright 2006 Nicolas Connault
  36   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class grade_grade extends grade_object {
  39  
  40      /**
  41       * The DB table.
  42       * @var string $table
  43       */
  44      public $table = 'grade_grades';
  45  
  46      /**
  47       * Array of required table fields, must start with 'id'.
  48       * @var array $required_fields
  49       */
  50      public $required_fields = array('id', 'itemid', 'userid', 'rawgrade', 'rawgrademax', 'rawgrademin',
  51                                   'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked',
  52                                   'locktime', 'exported', 'overridden', 'excluded', 'timecreated',
  53                                   'timemodified', 'aggregationstatus', 'aggregationweight');
  54  
  55      /**
  56       * Array of optional fields with default values (these should match db defaults)
  57       * @var array $optional_fields
  58       */
  59      public $optional_fields = array('feedback'=>null, 'feedbackformat'=>0, 'information'=>null, 'informationformat'=>0);
  60  
  61      /**
  62       * The id of the grade_item this grade belongs to.
  63       * @var int $itemid
  64       */
  65      public $itemid;
  66  
  67      /**
  68       * The grade_item object referenced by $this->itemid.
  69       * @var grade_item $grade_item
  70       */
  71      public $grade_item;
  72  
  73      /**
  74       * The id of the user this grade belongs to.
  75       * @var int $userid
  76       */
  77      public $userid;
  78  
  79      /**
  80       * The grade value of this raw grade, if such was provided by the module.
  81       * @var float $rawgrade
  82       */
  83      public $rawgrade;
  84  
  85      /**
  86       * The maximum allowable grade when this grade was created.
  87       * @var float $rawgrademax
  88       */
  89      public $rawgrademax = 100;
  90  
  91      /**
  92       * The minimum allowable grade when this grade was created.
  93       * @var float $rawgrademin
  94       */
  95      public $rawgrademin = 0;
  96  
  97      /**
  98       * id of the scale, if this grade is based on a scale.
  99       * @var int $rawscaleid
 100       */
 101      public $rawscaleid;
 102  
 103      /**
 104       * The userid of the person who last modified this grade.
 105       * @var int $usermodified
 106       */
 107      public $usermodified;
 108  
 109      /**
 110       * The final value of this grade.
 111       * @var float $finalgrade
 112       */
 113      public $finalgrade;
 114  
 115      /**
 116       * 0 if visible, 1 always hidden or date not visible until
 117       * @var float $hidden
 118       */
 119      public $hidden = 0;
 120  
 121      /**
 122       * 0 not locked, date when the item was locked
 123       * @var float locked
 124       */
 125      public $locked = 0;
 126  
 127      /**
 128       * 0 no automatic locking, date when to lock the grade automatically
 129       * @var float $locktime
 130       */
 131      public $locktime = 0;
 132  
 133      /**
 134       * Exported flag
 135       * @var bool $exported
 136       */
 137      public $exported = 0;
 138  
 139      /**
 140       * Overridden flag
 141       * @var bool $overridden
 142       */
 143      public $overridden = 0;
 144  
 145      /**
 146       * Grade excluded from aggregation functions
 147       * @var bool $excluded
 148       */
 149      public $excluded = 0;
 150  
 151      /**
 152       * TODO: HACK: create a new field datesubmitted - the date of submission if any (MDL-31377)
 153       * @var bool $timecreated
 154       */
 155      public $timecreated = null;
 156  
 157      /**
 158       * TODO: HACK: create a new field dategraded - the date of grading (MDL-31378)
 159       * @var bool $timemodified
 160       */
 161      public $timemodified = null;
 162  
 163      /**
 164       * Aggregation status flag. Can be one of 'unknown', 'dropped', 'novalue' or 'used'.
 165       * @var string $aggregationstatus
 166       */
 167      public $aggregationstatus = 'unknown';
 168  
 169      /**
 170       * Aggregation weight is the specific weight used in the aggregation calculation for this grade.
 171       * @var float $aggregationweight
 172       */
 173      public $aggregationweight = null;
 174  
 175      /**
 176       * Feedback files to copy.
 177       *
 178       * Example -
 179       *
 180       * [
 181       *     'contextid' => 1,
 182       *     'component' => 'mod_xyz',
 183       *     'filearea' => 'mod_xyz_feedback',
 184       *     'itemid' => 2
 185       * ];
 186       *
 187       * @var array
 188       */
 189      public $feedbackfiles = [];
 190  
 191      /**
 192       * Feedback content.
 193       * @var string $feedback
 194       */
 195      public $feedback;
 196  
 197      /**
 198       * Feedback format.
 199       * @var int $feedbackformat
 200       */
 201      public $feedbackformat = FORMAT_PLAIN;
 202  
 203      /**
 204       * Information text.
 205       * @var string $information
 206       */
 207      public $information;
 208  
 209      /**
 210       * Information text format.
 211       * @var int $informationformat
 212       */
 213      public $informationformat = FORMAT_PLAIN;
 214  
 215      /**
 216       * label text.
 217       * @var string $label
 218       */
 219      public $label;
 220  
 221      /**
 222       * Returns array of grades for given grade_item+users
 223       *
 224       * @param grade_item $grade_item
 225       * @param array $userids
 226       * @param bool $include_missing include grades that do not exist yet
 227       * @return array userid=>grade_grade array
 228       */
 229      public static function fetch_users_grades($grade_item, $userids, $include_missing=true) {
 230          global $DB;
 231  
 232          // hmm, there might be a problem with length of sql query
 233          // if there are too many users requested - we might run out of memory anyway
 234          $limit = 2000;
 235          $count = count($userids);
 236          if ($count > $limit) {
 237              $half = (int)($count/2);
 238              $first  = array_slice($userids, 0, $half);
 239              $second = array_slice($userids, $half);
 240              return grade_grade::fetch_users_grades($grade_item, $first, $include_missing) + grade_grade::fetch_users_grades($grade_item, $second, $include_missing);
 241          }
 242  
 243          list($user_ids_cvs, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uid0');
 244          $params['giid'] = $grade_item->id;
 245          $result = array();
 246          if ($grade_records = $DB->get_records_select('grade_grades', "itemid=:giid AND userid $user_ids_cvs", $params)) {
 247              foreach ($grade_records as $record) {
 248                  $result[$record->userid] = new grade_grade($record, false);
 249              }
 250          }
 251          if ($include_missing) {
 252              foreach ($userids as $userid) {
 253                  if (!array_key_exists($userid, $result)) {
 254                      $grade_grade = new grade_grade();
 255                      $grade_grade->userid = $userid;
 256                      $grade_grade->itemid = $grade_item->id;
 257                      $result[$userid] = $grade_grade;
 258                  }
 259              }
 260          }
 261  
 262          return $result;
 263      }
 264  
 265      /**
 266       * Loads the grade_item object referenced by $this->itemid and saves it as $this->grade_item for easy access
 267       *
 268       * @return grade_item The grade_item instance referenced by $this->itemid
 269       */
 270      public function load_grade_item() {
 271          if (empty($this->itemid)) {
 272              debugging('Missing itemid');
 273              $this->grade_item = null;
 274              return null;
 275          }
 276  
 277          if (empty($this->grade_item)) {
 278              $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
 279  
 280          } else if ($this->grade_item->id != $this->itemid) {
 281              debugging('Itemid mismatch');
 282              $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
 283          }
 284  
 285          if (empty($this->grade_item)) {
 286              debugging("Missing grade item id $this->itemid", DEBUG_DEVELOPER);
 287          }
 288  
 289          return $this->grade_item;
 290      }
 291  
 292      /**
 293       * Is grading object editable?
 294       *
 295       * @return bool
 296       */
 297      public function is_editable() {
 298          if ($this->is_locked()) {
 299              return false;
 300          }
 301  
 302          $grade_item = $this->load_grade_item();
 303  
 304          if ($grade_item->gradetype == GRADE_TYPE_NONE) {
 305              return false;
 306          }
 307  
 308          if ($grade_item->is_course_item() or $grade_item->is_category_item()) {
 309              return (bool)get_config('moodle', 'grade_overridecat');
 310          }
 311  
 312          return true;
 313      }
 314  
 315      /**
 316       * Check grade lock status. Uses both grade item lock and grade lock.
 317       * Internally any date in locked field (including future ones) means locked,
 318       * the date is stored for logging purposes only.
 319       *
 320       * @return bool True if locked, false if not
 321       */
 322      public function is_locked() {
 323          $this->load_grade_item();
 324          if (empty($this->grade_item)) {
 325              return !empty($this->locked);
 326          } else {
 327              return !empty($this->locked) or $this->grade_item->is_locked();
 328          }
 329      }
 330  
 331      /**
 332       * Checks if grade overridden
 333       *
 334       * @return bool True if grade is overriden
 335       */
 336      public function is_overridden() {
 337          return !empty($this->overridden);
 338      }
 339  
 340      /**
 341       * Returns timestamp of submission related to this grade, null if not submitted.
 342       *
 343       * @return int Timestamp
 344       */
 345      public function get_datesubmitted() {
 346          //TODO: HACK - create new fields (MDL-31379)
 347          return $this->timecreated;
 348      }
 349  
 350      /**
 351       * Returns the weight this grade contributed to the aggregated grade
 352       *
 353       * @return float|null
 354       */
 355      public function get_aggregationweight() {
 356          return $this->aggregationweight;
 357      }
 358  
 359      /**
 360       * Set aggregationweight.
 361       *
 362       * @param float $aggregationweight
 363       * @return void
 364       */
 365      public function set_aggregationweight($aggregationweight) {
 366          $this->aggregationweight = $aggregationweight;
 367          $this->update();
 368      }
 369  
 370      /**
 371       * Returns the info on how this value was used in the aggregated grade
 372       *
 373       * @return string One of 'dropped', 'excluded', 'novalue', 'used' or 'extra'
 374       */
 375      public function get_aggregationstatus() {
 376          return $this->aggregationstatus;
 377      }
 378  
 379      /**
 380       * Set aggregationstatus flag
 381       *
 382       * @param string $aggregationstatus
 383       * @return void
 384       */
 385      public function set_aggregationstatus($aggregationstatus) {
 386          $this->aggregationstatus = $aggregationstatus;
 387          $this->update();
 388      }
 389  
 390      /**
 391       * Returns the minimum and maximum number of points this grade is graded with respect to.
 392       *
 393       * @since  Moodle 2.8.7, 2.9.1
 394       * @return array A list containing, in order, the minimum and maximum number of points.
 395       */
 396      protected function get_grade_min_and_max() {
 397          global $CFG;
 398          $this->load_grade_item();
 399  
 400          // When the following setting is turned on we use the grade_grade raw min and max values.
 401          $minmaxtouse = grade_get_setting($this->grade_item->courseid, 'minmaxtouse', $CFG->grade_minmaxtouse);
 402  
 403          // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
 404          // wish to update the grades.
 405          $gradebookcalculationsfreeze = 'gradebook_calculations_freeze_' . $this->grade_item->courseid;
 406          // Gradebook is frozen, run through old code.
 407          if (isset($CFG->$gradebookcalculationsfreeze) && (int)$CFG->$gradebookcalculationsfreeze <= 20150627) {
 408              // Only aggregate items use separate min grades.
 409              if ($minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE || $this->grade_item->is_aggregate_item()) {
 410                  return array($this->rawgrademin, $this->rawgrademax);
 411              } else {
 412                  return array($this->grade_item->grademin, $this->grade_item->grademax);
 413              }
 414          } else {
 415              // Only aggregate items use separate min grades, unless they are calculated grade items.
 416              if (($this->grade_item->is_aggregate_item() && !$this->grade_item->is_calculated())
 417                      || $minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE) {
 418                  return array($this->rawgrademin, $this->rawgrademax);
 419              } else {
 420                  return array($this->grade_item->grademin, $this->grade_item->grademax);
 421              }
 422          }
 423      }
 424  
 425      /**
 426       * Returns the minimum number of points this grade is graded with.
 427       *
 428       * @since  Moodle 2.8.7, 2.9.1
 429       * @return float The minimum number of points
 430       */
 431      public function get_grade_min() {
 432          list($min, $max) = $this->get_grade_min_and_max();
 433  
 434          return $min;
 435      }
 436  
 437      /**
 438       * Returns the maximum number of points this grade is graded with respect to.
 439       *
 440       * @since  Moodle 2.8.7, 2.9.1
 441       * @return float The maximum number of points
 442       */
 443      public function get_grade_max() {
 444          list($min, $max) = $this->get_grade_min_and_max();
 445  
 446          return $max;
 447      }
 448  
 449      /**
 450       * Returns timestamp when last graded, null if no grade present
 451       *
 452       * @return int
 453       */
 454      public function get_dategraded() {
 455          //TODO: HACK - create new fields (MDL-31379)
 456          if (is_null($this->finalgrade) and is_null($this->feedback)) {
 457              return null; // no grade == no date
 458          } else if ($this->overridden) {
 459              return $this->overridden;
 460          } else {
 461              return $this->timemodified;
 462          }
 463      }
 464  
 465      /**
 466       * Set the overridden status of grade
 467       *
 468       * @param bool $state requested overridden state
 469       * @param bool $refresh refresh grades from external activities if needed
 470       * @return bool true is db state changed
 471       */
 472      public function set_overridden($state, $refresh = true) {
 473          if (empty($this->overridden) and $state) {
 474              $this->overridden = time();
 475              $this->update(null, true);
 476              return true;
 477  
 478          } else if (!empty($this->overridden) and !$state) {
 479              $this->overridden = 0;
 480              $this->update(null, true);
 481  
 482              if ($refresh) {
 483                  //refresh when unlocking
 484                  $this->grade_item->refresh_grades($this->userid);
 485              }
 486  
 487              return true;
 488          }
 489          return false;
 490      }
 491  
 492      /**
 493       * Checks if grade excluded from aggregation functions
 494       *
 495       * @return bool True if grade is excluded from aggregation
 496       */
 497      public function is_excluded() {
 498          return !empty($this->excluded);
 499      }
 500  
 501      /**
 502       * Set the excluded status of grade
 503       *
 504       * @param bool $state requested excluded state
 505       * @return bool True is database state changed
 506       */
 507      public function set_excluded($state) {
 508          if (empty($this->excluded) and $state) {
 509              $this->excluded = time();
 510              $this->update();
 511              return true;
 512  
 513          } else if (!empty($this->excluded) and !$state) {
 514              $this->excluded = 0;
 515              $this->update();
 516              return true;
 517          }
 518          return false;
 519      }
 520  
 521      /**
 522       * Lock/unlock this grade.
 523       *
 524       * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
 525       * @param bool $cascade Ignored param
 526       * @param bool $refresh Refresh grades when unlocking
 527       * @return bool True if successful, false if can not set new lock state for grade
 528       */
 529      public function set_locked($lockedstate, $cascade=false, $refresh=true) {
 530          $this->load_grade_item();
 531  
 532          if ($lockedstate) {
 533              if ($this->grade_item->needsupdate) {
 534                  //can not lock grade if final not calculated!
 535                  return false;
 536              }
 537  
 538              $this->locked = time();
 539              $this->update();
 540  
 541              return true;
 542  
 543          } else {
 544              if (!empty($this->locked) and $this->locktime < time()) {
 545                  //we have to reset locktime or else it would lock up again
 546                  $this->locktime = 0;
 547              }
 548  
 549              // remove the locked flag
 550              $this->locked = 0;
 551              $this->update();
 552  
 553              if ($refresh and !$this->is_overridden()) {
 554                  //refresh when unlocking and not overridden
 555                  $this->grade_item->refresh_grades($this->userid);
 556              }
 557  
 558              return true;
 559          }
 560      }
 561  
 562      /**
 563       * Lock the grade if needed. Make sure this is called only when final grades are valid
 564       *
 565       * @param array $items array of all grade item ids
 566       * @return void
 567       */
 568      public static function check_locktime_all($items) {
 569          global $CFG, $DB;
 570  
 571          $now = time(); // no rounding needed, this is not supposed to be called every 10 seconds
 572          list($usql, $params) = $DB->get_in_or_equal($items);
 573          $params[] = $now;
 574          $rs = $DB->get_recordset_select('grade_grades', "itemid $usql AND locked = 0 AND locktime > 0 AND locktime < ?", $params);
 575          foreach ($rs as $grade) {
 576              $grade_grade = new grade_grade($grade, false);
 577              $grade_grade->locked = time();
 578              $grade_grade->update('locktime');
 579          }
 580          $rs->close();
 581      }
 582  
 583      /**
 584       * Set the locktime for this grade.
 585       *
 586       * @param int $locktime timestamp for lock to activate
 587       * @return void
 588       */
 589      public function set_locktime($locktime) {
 590          $this->locktime = $locktime;
 591          $this->update();
 592      }
 593  
 594      /**
 595       * Get the locktime for this grade.
 596       *
 597       * @return int $locktime timestamp for lock to activate
 598       */
 599      public function get_locktime() {
 600          $this->load_grade_item();
 601  
 602          $item_locktime = $this->grade_item->get_locktime();
 603  
 604          if (empty($this->locktime) or ($item_locktime and $item_locktime < $this->locktime)) {
 605              return $item_locktime;
 606  
 607          } else {
 608              return $this->locktime;
 609          }
 610      }
 611  
 612      /**
 613       * Check grade hidden status. Uses data from both grade item and grade.
 614       *
 615       * @return bool true if hidden, false if not
 616       */
 617      public function is_hidden() {
 618          $this->load_grade_item();
 619          if (empty($this->grade_item)) {
 620              return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time());
 621          } else {
 622              return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()) or $this->grade_item->is_hidden();
 623          }
 624      }
 625  
 626      /**
 627       * Check grade hidden status. Uses data from both grade item and grade.
 628       *
 629       * @return bool true if hiddenuntil, false if not
 630       */
 631      public function is_hiddenuntil() {
 632          $this->load_grade_item();
 633  
 634          if ($this->hidden == 1 or $this->grade_item->hidden == 1) {
 635              return false; //always hidden
 636          }
 637  
 638          if ($this->hidden > 1 or $this->grade_item->hidden > 1) {
 639              return true;
 640          }
 641  
 642          return false;
 643      }
 644  
 645      /**
 646       * Check grade hidden status. Uses data from both grade item and grade.
 647       *
 648       * @return int 0 means visible, 1 hidden always, timestamp hidden until
 649       */
 650      public function get_hidden() {
 651          $this->load_grade_item();
 652  
 653          $item_hidden = $this->grade_item->get_hidden();
 654  
 655          if ($item_hidden == 1) {
 656              return 1;
 657  
 658          } else if ($item_hidden == 0) {
 659              return $this->hidden;
 660  
 661          } else {
 662              if ($this->hidden == 0) {
 663                  return $item_hidden;
 664              } else if ($this->hidden == 1) {
 665                  return 1;
 666              } else if ($this->hidden > $item_hidden) {
 667                  return $this->hidden;
 668              } else {
 669                  return $item_hidden;
 670              }
 671          }
 672      }
 673  
 674      /**
 675       * Set the hidden status of grade, 0 mean visible, 1 always hidden, number means date to hide until.
 676       *
 677       * @param int $hidden new hidden status
 678       * @param bool $cascade ignored
 679       */
 680      public function set_hidden($hidden, $cascade=false) {
 681         $this->hidden = $hidden;
 682         $this->update();
 683      }
 684  
 685      /**
 686       * Finds and returns a grade_grade instance based on params.
 687       *
 688       * @param array $params associative arrays varname=>value
 689       * @return grade_grade Returns a grade_grade instance or false if none found
 690       */
 691      public static function fetch($params) {
 692          return grade_object::fetch_helper('grade_grades', 'grade_grade', $params);
 693      }
 694  
 695      /**
 696       * Finds and returns all grade_grade instances based on params.
 697       *
 698       * @param array $params associative arrays varname=>value
 699       * @return array array of grade_grade instances or false if none found.
 700       */
 701      public static function fetch_all($params) {
 702          return grade_object::fetch_all_helper('grade_grades', 'grade_grade', $params);
 703      }
 704  
 705      /**
 706       * Given a float value situated between a source minimum and a source maximum, converts it to the
 707       * corresponding value situated between a target minimum and a target maximum. Thanks to Darlene
 708       * for the formula :-)
 709       *
 710       * @param float $rawgrade
 711       * @param float $source_min
 712       * @param float $source_max
 713       * @param float $target_min
 714       * @param float $target_max
 715       * @return float Converted value
 716       */
 717      public static function standardise_score($rawgrade, $source_min, $source_max, $target_min, $target_max) {
 718          if (is_null($rawgrade)) {
 719            return null;
 720          }
 721  
 722          if ($source_max == $source_min or $target_min == $target_max) {
 723              // prevent division by 0
 724              return $target_max;
 725          }
 726  
 727          $factor = ($rawgrade - $source_min) / ($source_max - $source_min);
 728          $diff = $target_max - $target_min;
 729          $standardised_value = $factor * $diff + $target_min;
 730          return $standardised_value;
 731      }
 732  
 733      /**
 734       * Given an array like this:
 735       * $a = array(1=>array(2, 3),
 736       *            2=>array(4),
 737       *            3=>array(1),
 738       *            4=>array())
 739       * this function fully resolves the dependencies so each value will be an array of
 740       * the all items this item depends on and their dependencies (and their dependencies...).
 741       * It should not explode if there are circular dependencies.
 742       * The dependency depth array will list the number of branches in the tree above each leaf.
 743       *
 744       * @param array $dependson Array to flatten
 745       * @param array $dependencydepth Array of itemids => depth. Initially these should be all set to 1.
 746       * @return array Flattened array
 747       */
 748      protected static function flatten_dependencies_array(&$dependson, &$dependencydepth) {
 749          // Flatten the nested dependencies - this will handle recursion bombs because it removes duplicates.
 750          $somethingchanged = true;
 751          // First of all, delete any incorrect (not array or individual null) dependency, they aren't welcome.
 752          // TODO: Maybe we should report about this happening, it shouldn't if all dependencies are correct and consistent.
 753          foreach ($dependson as $itemid => $depends) {
 754              $depends = is_array($depends) ? $depends : []; // Only arrays are accepted.
 755              $dependson[$itemid] = array_filter($depends, function($val) { // Only not-null values are accepted.
 756                  return !is_null($val);
 757              });
 758          }
 759          while ($somethingchanged) {
 760              $somethingchanged = false;
 761  
 762              foreach ($dependson as $itemid => $depends) {
 763                  // Make a copy so we can tell if it changed.
 764                  $before = $dependson[$itemid];
 765                  foreach ($depends as $subitemid => $subdepends) {
 766                      $dependson[$itemid] = array_unique(array_merge($depends, $dependson[$subdepends] ?? []));
 767                      sort($dependson[$itemid], SORT_NUMERIC);
 768                  }
 769                  if ($before != $dependson[$itemid]) {
 770                      $somethingchanged = true;
 771                      if (!isset($dependencydepth[$itemid])) {
 772                          $dependencydepth[$itemid] = 1;
 773                      } else {
 774                          $dependencydepth[$itemid]++;
 775                      }
 776                  }
 777              }
 778          }
 779      }
 780  
 781      /**
 782       * Return array of grade item ids that are either hidden or indirectly depend
 783       * on hidden grades, excluded grades are not returned.
 784       * THIS IS A REALLY BIG HACK! to be replaced by conditional aggregation of hidden grades in 2.0
 785       *
 786       * @param array $grade_grades all course grades of one user, & used for better internal caching
 787       * @param array $grade_items array of grade items, & used for better internal caching
 788       * @return array This is an array of following arrays:
 789       *      unknown => list of item ids that may be affected by hiding (with the ITEM ID as both the key and the value) - for BC with old gradereport plugins
 790       *      unknowngrades => list of item ids that may be affected by hiding (with the calculated grade as the value)
 791       *      altered => list of item ids that are definitely affected by hiding (with the calculated grade as the value)
 792       *      alteredgrademax => for each item in altered or unknown, the new value of the grademax
 793       *      alteredgrademin => for each item in altered or unknown, the new value of the grademin
 794       *      alteredgradestatus => for each item with a modified status - the value of the new status
 795       *      alteredgradeweight => for each item with a modified weight - the value of the new weight
 796       */
 797      public static function get_hiding_affected(&$grade_grades, &$grade_items) {
 798          global $CFG;
 799  
 800          if (count($grade_grades) !== count($grade_items)) {
 801              throw new \moodle_exception('invalidarraysize', 'debug', '', 'grade_grade::get_hiding_affected()!');
 802          }
 803  
 804          $dependson = array();
 805          $todo = array();
 806          $unknown = array();  // can not find altered
 807          $altered = array();  // altered grades
 808          $alteredgrademax = array();  // Altered grade max values.
 809          $alteredgrademin = array();  // Altered grade min values.
 810          $alteredaggregationstatus = array();  // Altered aggregation status.
 811          $alteredaggregationweight = array();  // Altered aggregation weight.
 812          $dependencydepth = array();
 813  
 814          $hiddenfound = false;
 815          foreach($grade_grades as $itemid=>$unused) {
 816              $grade_grade =& $grade_grades[$itemid];
 817              // We need the immediate dependencies of all every grade_item so we can calculate nested dependencies.
 818              $dependson[$grade_grade->itemid] = $grade_items[$grade_grade->itemid]->depends_on();
 819              if ($grade_grade->is_excluded()) {
 820                  //nothing to do, aggregation is ok
 821                  continue;
 822              } else if ($grade_grade->is_hidden()) {
 823                  $hiddenfound = true;
 824                  $altered[$grade_grade->itemid] = null;
 825                  $alteredaggregationstatus[$grade_grade->itemid] = 'dropped';
 826                  $alteredaggregationweight[$grade_grade->itemid] = 0;
 827              } else if ($grade_grade->is_overridden()) {
 828                  // No need to recalculate overridden grades.
 829                  continue;
 830              } else {
 831                  if (!empty($dependson[$grade_grade->itemid])) {
 832                      $dependencydepth[$grade_grade->itemid] = 1;
 833                      $todo[] = $grade_grade->itemid;
 834                  }
 835              }
 836          }
 837  
 838          // Flatten the dependency tree and count number of branches to each leaf.
 839          self::flatten_dependencies_array($dependson, $dependencydepth);
 840  
 841          if (!$hiddenfound) {
 842              return array('unknown' => array(),
 843                           'unknowngrades' => array(),
 844                           'altered' => array(),
 845                           'alteredgrademax' => array(),
 846                           'alteredgrademin' => array(),
 847                           'alteredaggregationstatus' => array(),
 848                           'alteredaggregationweight' => array());
 849          }
 850          // This line ensures that $dependencydepth has the same number of items as $todo.
 851          $dependencydepth = array_intersect_key($dependencydepth, array_flip($todo));
 852          // We need to resort the todo list by the dependency depth. This guarantees we process the leaves, then the branches.
 853          array_multisort($dependencydepth, $todo);
 854  
 855          $max = count($todo);
 856          $hidden_precursors = null;
 857          for($i=0; $i<$max; $i++) {
 858              $found = false;
 859              foreach($todo as $key=>$do) {
 860                  $hidden_precursors = array_intersect($dependson[$do], array_keys($unknown));
 861                  if ($hidden_precursors) {
 862                      // this item depends on hidden grade indirectly
 863                      $unknown[$do] = $grade_grades[$do]->finalgrade;
 864                      unset($todo[$key]);
 865                      $found = true;
 866                      continue;
 867  
 868                  } else if (!array_intersect($dependson[$do], $todo)) {
 869                      $hidden_precursors = array_intersect($dependson[$do], array_keys($altered));
 870                      // If the dependency is a sum aggregation, we need to process it as if it had hidden items.
 871                      // The reason for this, is that the code will recalculate the maxgrade by removing ungraded
 872                      // items and accounting for 'drop x grades' and then stored back in our virtual grade_items.
 873                      // This recalculation is necessary because there will be a call to:
 874                      //              $grade_category->aggregate_values_and_adjust_bounds
 875                      // for the top level grade that will depend on knowing what that caclulated grademax is
 876                      // and it finds that value by checking the virtual grade_items.
 877                      $issumaggregate = false;
 878                      if ($grade_items[$do]->itemtype == 'category') {
 879                          $issumaggregate = $grade_items[$do]->load_item_category()->aggregation == GRADE_AGGREGATE_SUM;
 880                      }
 881                      if (!$hidden_precursors && !$issumaggregate) {
 882                          unset($todo[$key]);
 883                          $found = true;
 884                          continue;
 885  
 886                      } else {
 887                          // depends on altered grades - we should try to recalculate if possible
 888                          if ($grade_items[$do]->is_calculated() or
 889                              (!$grade_items[$do]->is_category_item() and !$grade_items[$do]->is_course_item()) or
 890                              ($grade_items[$do]->is_category_item() and $grade_items[$do]->is_locked())
 891                          ) {
 892                              // This is a grade item that is not a category or course and has been affected by grade hiding.
 893                              // Or a grade item that is a category and it is locked.
 894                              // I guess this means it is a calculation that needs to be recalculated.
 895                              $unknown[$do] = $grade_grades[$do]->finalgrade;
 896                              unset($todo[$key]);
 897                              $found = true;
 898                              continue;
 899  
 900                          } else {
 901                              // This is a grade category (or course).
 902                              $grade_category = $grade_items[$do]->load_item_category();
 903  
 904                              // Build a new list of the grades in this category.
 905                              $values = array();
 906                              $immediatedepends = $grade_items[$do]->depends_on();
 907                              foreach ($immediatedepends as $itemid) {
 908                                  if (array_key_exists($itemid, $altered)) {
 909                                      //nulling an altered precursor
 910                                      $values[$itemid] = $altered[$itemid];
 911                                      if (is_null($values[$itemid])) {
 912                                          // This means this was a hidden grade item removed from the result.
 913                                          unset($values[$itemid]);
 914                                      }
 915                                  } elseif (empty($values[$itemid])) {
 916                                      $values[$itemid] = $grade_grades[$itemid]->finalgrade;
 917                                  }
 918                              }
 919  
 920                              foreach ($values as $itemid=>$value) {
 921                                  if ($grade_grades[$itemid]->is_excluded()) {
 922                                      unset($values[$itemid]);
 923                                      $alteredaggregationstatus[$itemid] = 'excluded';
 924                                      $alteredaggregationweight[$itemid] = null;
 925                                      continue;
 926                                  }
 927                                  // The grade min/max may have been altered by hiding.
 928                                  $grademin = $grade_items[$itemid]->grademin;
 929                                  if (isset($alteredgrademin[$itemid])) {
 930                                      $grademin = $alteredgrademin[$itemid];
 931                                  }
 932                                  $grademax = $grade_items[$itemid]->grademax;
 933                                  if (isset($alteredgrademax[$itemid])) {
 934                                      $grademax = $alteredgrademax[$itemid];
 935                                  }
 936                                  $values[$itemid] = grade_grade::standardise_score($value, $grademin, $grademax, 0, 1);
 937                              }
 938  
 939                              if ($grade_category->aggregateonlygraded) {
 940                                  foreach ($values as $itemid=>$value) {
 941                                      if (is_null($value)) {
 942                                          unset($values[$itemid]);
 943                                          $alteredaggregationstatus[$itemid] = 'novalue';
 944                                          $alteredaggregationweight[$itemid] = null;
 945                                      }
 946                                  }
 947                              } else {
 948                                  foreach ($values as $itemid=>$value) {
 949                                      if (is_null($value)) {
 950                                          $values[$itemid] = 0;
 951                                      }
 952                                  }
 953                              }
 954  
 955                              // limit and sort
 956                              $allvalues = $values;
 957                              $grade_category->apply_limit_rules($values, $grade_items);
 958  
 959                              $moredropped = array_diff($allvalues, $values);
 960                              foreach ($moredropped as $drop => $unused) {
 961                                  $alteredaggregationstatus[$drop] = 'dropped';
 962                                  $alteredaggregationweight[$drop] = null;
 963                              }
 964  
 965                              foreach ($values as $itemid => $val) {
 966                                  if ($grade_category->is_extracredit_used() && ($grade_items[$itemid]->aggregationcoef > 0)) {
 967                                      $alteredaggregationstatus[$itemid] = 'extra';
 968                                  }
 969                              }
 970  
 971                              asort($values, SORT_NUMERIC);
 972  
 973                              // let's see we have still enough grades to do any statistics
 974                              if (count($values) == 0) {
 975                                  // not enough attempts yet
 976                                  $altered[$do] = null;
 977                                  unset($todo[$key]);
 978                                  $found = true;
 979                                  continue;
 980                              }
 981  
 982                              $usedweights = array();
 983                              $adjustedgrade = $grade_category->aggregate_values_and_adjust_bounds($values, $grade_items, $usedweights);
 984  
 985                              // recalculate the rawgrade back to requested range
 986                              $finalgrade = grade_grade::standardise_score($adjustedgrade['grade'],
 987                                                                           0,
 988                                                                           1,
 989                                                                           $adjustedgrade['grademin'],
 990                                                                           $adjustedgrade['grademax']);
 991  
 992                              foreach ($usedweights as $itemid => $weight) {
 993                                  if (!isset($alteredaggregationstatus[$itemid])) {
 994                                      $alteredaggregationstatus[$itemid] = 'used';
 995                                  }
 996                                  $alteredaggregationweight[$itemid] = $weight;
 997                              }
 998  
 999                              $finalgrade = $grade_items[$do]->bounded_grade($finalgrade);
1000                              $alteredgrademin[$do] = $adjustedgrade['grademin'];
1001                              $alteredgrademax[$do] = $adjustedgrade['grademax'];
1002                              // We need to muck with the "in-memory" grade_items records so
1003                              // that subsequent calculations will use the adjusted grademin and grademax.
1004                              $grade_items[$do]->grademin = $adjustedgrade['grademin'];
1005                              $grade_items[$do]->grademax = $adjustedgrade['grademax'];
1006  
1007                              $altered[$do] = $finalgrade;
1008                              unset($todo[$key]);
1009                              $found = true;
1010                              continue;
1011                          }
1012                      }
1013                  }
1014              }
1015              if (!$found) {
1016                  break;
1017              }
1018          }
1019  
1020          return array('unknown' => array_combine(array_keys($unknown), array_keys($unknown)), // Left for BC in case some gradereport plugins expect it.
1021                       'unknowngrades' => $unknown,
1022                       'altered' => $altered,
1023                       'alteredgrademax' => $alteredgrademax,
1024                       'alteredgrademin' => $alteredgrademin,
1025                       'alteredaggregationstatus' => $alteredaggregationstatus,
1026                       'alteredaggregationweight' => $alteredaggregationweight);
1027      }
1028  
1029      /**
1030       * Returns true if the grade's value is superior or equal to the grade item's gradepass value, false otherwise.
1031       *
1032       * @param grade_item $grade_item An optional grade_item of which gradepass value we can use, saves having to load the grade_grade's grade_item
1033       * @return bool
1034       */
1035      public function is_passed($grade_item = null) {
1036          if (empty($grade_item)) {
1037              if (!isset($this->grade_item)) {
1038                  $this->load_grade_item();
1039              }
1040          } else {
1041              $this->grade_item = $grade_item;
1042              $this->itemid = $grade_item->id;
1043          }
1044  
1045          // Return null if finalgrade is null
1046          if (is_null($this->finalgrade)) {
1047              return null;
1048          }
1049  
1050          // Return null if gradepass == grademin, gradepass is null, or grade item is a scale and gradepass is 0.
1051          if (is_null($this->grade_item->gradepass)) {
1052              return null;
1053          } else if ($this->grade_item->gradepass == $this->grade_item->grademin) {
1054              return null;
1055          } else if ($this->grade_item->gradetype == GRADE_TYPE_SCALE && !grade_floats_different($this->grade_item->gradepass, 0.0)) {
1056              return null;
1057          }
1058  
1059          return $this->finalgrade >= $this->grade_item->gradepass;
1060      }
1061  
1062      /**
1063       * In addition to update() as defined in grade_object rounds the float numbers using php function,
1064       * the reason is we need to compare the db value with computed number to skip updates if possible.
1065       *
1066       * @param string $source from where was the object inserted (mod/forum, manual, etc.)
1067       * @param bool $isbulkupdate If bulk grade update is happening.
1068       * @return bool success
1069       */
1070      public function update($source=null, $isbulkupdate = false) {
1071          $this->rawgrade = grade_floatval($this->rawgrade);
1072          $this->finalgrade = grade_floatval($this->finalgrade);
1073          $this->rawgrademin = grade_floatval($this->rawgrademin);
1074          $this->rawgrademax = grade_floatval($this->rawgrademax);
1075          return parent::update($source, $isbulkupdate);
1076      }
1077  
1078  
1079      /**
1080       * Handles adding feedback files in the gradebook.
1081       *
1082       * @param int|null $historyid
1083       */
1084      protected function add_feedback_files(int $historyid = null) {
1085          global $CFG;
1086  
1087          // We only support feedback files for modules atm.
1088          if ($this->grade_item && $this->grade_item->is_external_item()) {
1089              $context = $this->get_context();
1090              $this->copy_feedback_files($context, GRADE_FEEDBACK_FILEAREA, $this->id);
1091  
1092              if (empty($CFG->disablegradehistory) && $historyid) {
1093                  $this->copy_feedback_files($context, GRADE_HISTORY_FEEDBACK_FILEAREA, $historyid);
1094              }
1095          }
1096  
1097          return $this->id;
1098      }
1099  
1100      /**
1101       * Handles updating feedback files in the gradebook.
1102       *
1103       * @param int|null $historyid
1104       */
1105      protected function update_feedback_files(int $historyid = null) {
1106          global $CFG;
1107  
1108          // We only support feedback files for modules atm.
1109          if ($this->grade_item && $this->grade_item->is_external_item()) {
1110              $context = $this->get_context();
1111  
1112              $fs = new file_storage();
1113              $fs->delete_area_files($context->id, GRADE_FILE_COMPONENT, GRADE_FEEDBACK_FILEAREA, $this->id);
1114  
1115              $this->copy_feedback_files($context, GRADE_FEEDBACK_FILEAREA, $this->id);
1116  
1117              if (empty($CFG->disablegradehistory) && $historyid) {
1118                  $this->copy_feedback_files($context, GRADE_HISTORY_FEEDBACK_FILEAREA, $historyid);
1119              }
1120          }
1121  
1122          return true;
1123      }
1124  
1125      /**
1126       * Handles deleting feedback files in the gradebook.
1127       */
1128      protected function delete_feedback_files() {
1129          // We only support feedback files for modules atm.
1130          if ($this->grade_item && $this->grade_item->is_external_item()) {
1131              $context = $this->get_context();
1132  
1133              $fs = new file_storage();
1134              $fs->delete_area_files($context->id, GRADE_FILE_COMPONENT, GRADE_FEEDBACK_FILEAREA, $this->id);
1135  
1136              // Grade history only gets deleted when we delete the whole grade item.
1137          }
1138  
1139          return true;
1140      }
1141  
1142      /**
1143       * Deletes the grade_grade instance from the database.
1144       *
1145       * @param string $source The location the deletion occurred (mod/forum, manual, etc.).
1146       * @return bool Returns true if the deletion was successful, false otherwise.
1147       */
1148      public function delete($source = null) {
1149          global $DB;
1150  
1151          $transaction = $DB->start_delegated_transaction();
1152          $success = parent::delete($source);
1153  
1154          // If the grade was deleted successfully trigger a grade_deleted event.
1155          if ($success && !empty($this->grade_item)) {
1156              \core\event\grade_deleted::create_from_grade($this)->trigger();
1157          }
1158  
1159          $transaction->allow_commit();
1160          return $success;
1161      }
1162  
1163      /**
1164       * Used to notify the completion system (if necessary) that a user's grade
1165       * has changed, and clear up a possible score cache.
1166       *
1167       * @param bool $deleted True if grade was actually deleted
1168       * @param bool $isbulkupdate If bulk grade update is happening.
1169       */
1170      protected function notify_changed($deleted, $isbulkupdate = false) {
1171          global $CFG;
1172  
1173          // Condition code may cache the grades for conditional availability of
1174          // modules or sections. (This code should use a hook for communication
1175          // with plugin, but hooks are not implemented at time of writing.)
1176          if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
1177              \availability_grade\callbacks::grade_changed($this->userid);
1178          }
1179  
1180          require_once($CFG->libdir.'/completionlib.php');
1181  
1182          // Bail out immediately if completion is not enabled for site (saves loading
1183          // grade item & requiring the restore stuff).
1184          if (!completion_info::is_enabled_for_site()) {
1185              return;
1186          }
1187  
1188          // Ignore during restore, as completion data will be updated anyway and
1189          // doing it now will result in incorrect dates (it will say they got the
1190          // grade completion now, instead of the correct time).
1191          if (class_exists('restore_controller', false) && restore_controller::is_executing()) {
1192              return;
1193          }
1194  
1195          // Load information about grade item, exit if the grade item is missing.
1196          if (!$this->load_grade_item()) {
1197              return;
1198          }
1199  
1200          // Only course-modules have completion data
1201          if ($this->grade_item->itemtype!='mod') {
1202              return;
1203          }
1204  
1205          // Use $COURSE if available otherwise get it via item fields
1206          $course = get_course($this->grade_item->courseid, false);
1207  
1208          // Bail out if completion is not enabled for course
1209          $completion = new completion_info($course);
1210          if (!$completion->is_enabled()) {
1211              return;
1212          }
1213  
1214          // Get course-module
1215          $cm = get_coursemodule_from_instance($this->grade_item->itemmodule,
1216                $this->grade_item->iteminstance, $this->grade_item->courseid);
1217          // If the course-module doesn't exist, display a warning...
1218          if (!$cm) {
1219              // ...unless the grade is being deleted in which case it's likely
1220              // that the course-module was just deleted too, so that's okay.
1221              if (!$deleted) {
1222                  debugging("Couldn't find course-module for module '" .
1223                          $this->grade_item->itemmodule . "', instance '" .
1224                          $this->grade_item->iteminstance . "', course '" .
1225                          $this->grade_item->courseid . "'");
1226              }
1227              return;
1228          }
1229  
1230          // Pass information on to completion system
1231          $completion->inform_grade_changed($cm, $this->grade_item, $this, $deleted, $isbulkupdate);
1232      }
1233  
1234      /**
1235       * Get some useful information about how this grade_grade is reflected in the aggregation
1236       * for the grade_category. For example this could be an extra credit item, and it could be
1237       * dropped because it's in the X lowest or highest.
1238       *
1239       * @return array(status, weight) - A keyword and a numerical weight that represents how this grade was included in the aggregation.
1240       */
1241      function get_aggregation_hint() {
1242          return array('status' => $this->get_aggregationstatus(),
1243                       'weight' => $this->get_aggregationweight());
1244      }
1245  
1246      /**
1247       * Handles copying feedback files to a specified gradebook file area.
1248       *
1249       * @param context $context
1250       * @param string $filearea
1251       * @param int $itemid
1252       */
1253      private function copy_feedback_files(context $context, string $filearea, int $itemid) {
1254          if ($this->feedbackfiles) {
1255              $filestocopycontextid = $this->feedbackfiles['contextid'];
1256              $filestocopycomponent = $this->feedbackfiles['component'];
1257              $filestocopyfilearea = $this->feedbackfiles['filearea'];
1258              $filestocopyitemid = $this->feedbackfiles['itemid'];
1259  
1260              $fs = new file_storage();
1261              if ($filestocopy = $fs->get_area_files($filestocopycontextid, $filestocopycomponent, $filestocopyfilearea,
1262                      $filestocopyitemid)) {
1263                  foreach ($filestocopy as $filetocopy) {
1264                      $destination = [
1265                          'contextid' => $context->id,
1266                          'component' => GRADE_FILE_COMPONENT,
1267                          'filearea' => $filearea,
1268                          'itemid' => $itemid
1269                      ];
1270                      $fs->create_file_from_storedfile($destination, $filetocopy);
1271                  }
1272              }
1273          }
1274      }
1275  
1276      /**
1277       * Determine the correct context for this grade_grade.
1278       *
1279       * @return context
1280       */
1281      public function get_context() {
1282          $this->load_grade_item();
1283          return $this->grade_item->get_context();
1284      }
1285  }