Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

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

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Definition of a class to represent a grade item
  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  require_once ('grade_object.php');
  28  
  29  /**
  30   * Class representing a grade item.
  31   *
  32   * It is responsible for handling its DB representation, modifying and returning its metadata.
  33   *
  34   * @package   core_grades
  35   * @category  grade
  36   * @copyright 2006 Nicolas Connault
  37   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class grade_item extends grade_object {
  40      /**
  41       * DB Table (used by grade_object).
  42       * @var string $table
  43       */
  44      public $table = 'grade_items';
  45  
  46      /**
  47       * Array of required table fields, must start with 'id'.
  48       * @var array $required_fields
  49       */
  50      public $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',
  51                                   'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',
  52                                   'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',
  53                                   'aggregationcoef2', 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime',
  54                                   'needsupdate', 'weightoverride', 'timecreated', 'timemodified');
  55  
  56      /**
  57       * The course this grade_item belongs to.
  58       * @var int $courseid
  59       */
  60      public $courseid;
  61  
  62      /**
  63       * The category this grade_item belongs to (optional).
  64       * @var int $categoryid
  65       */
  66      public $categoryid;
  67  
  68      /**
  69       * The grade_category object referenced $this->iteminstance if itemtype == 'category' or == 'course'.
  70       * @var grade_category $item_category
  71       */
  72      public $item_category;
  73  
  74      /**
  75       * The grade_category object referenced by $this->categoryid.
  76       * @var grade_category $parent_category
  77       */
  78      public $parent_category;
  79  
  80  
  81      /**
  82       * The name of this grade_item (pushed by the module).
  83       * @var string $itemname
  84       */
  85      public $itemname;
  86  
  87      /**
  88       * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
  89       * @var string $itemtype
  90       */
  91      public $itemtype;
  92  
  93      /**
  94       * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
  95       * @var string $itemmodule
  96       */
  97      public $itemmodule;
  98  
  99      /**
 100       * ID of the item module
 101       * @var int $iteminstance
 102       */
 103      public $iteminstance;
 104  
 105      /**
 106       * Number of the item in a series of multiple grades pushed by an activity.
 107       * @var int $itemnumber
 108       */
 109      public $itemnumber;
 110  
 111      /**
 112       * Info and notes about this item.
 113       * @var string $iteminfo
 114       */
 115      public $iteminfo;
 116  
 117      /**
 118       * Arbitrary idnumber provided by the module responsible.
 119       * @var string $idnumber
 120       */
 121      public $idnumber;
 122  
 123      /**
 124       * Calculation string used for this item.
 125       * @var string $calculation
 126       */
 127      public $calculation;
 128  
 129      /**
 130       * Indicates if we already tried to normalize the grade calculation formula.
 131       * This flag helps to minimize db access when broken formulas used in calculation.
 132       * @var bool
 133       */
 134      public $calculation_normalized;
 135      /**
 136       * Math evaluation object
 137       * @var calc_formula A formula object
 138       */
 139      public $formula;
 140  
 141      /**
 142       * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
 143       * @var int $gradetype
 144       */
 145      public $gradetype = GRADE_TYPE_VALUE;
 146  
 147      /**
 148       * Maximum allowable grade.
 149       * @var float $grademax
 150       */
 151      public $grademax = 100;
 152  
 153      /**
 154       * Minimum allowable grade.
 155       * @var float $grademin
 156       */
 157      public $grademin = 0;
 158  
 159      /**
 160       * id of the scale, if this grade is based on a scale.
 161       * @var int $scaleid
 162       */
 163      public $scaleid;
 164  
 165      /**
 166       * The grade_scale object referenced by $this->scaleid.
 167       * @var grade_scale $scale
 168       */
 169      public $scale;
 170  
 171      /**
 172       * The id of the optional grade_outcome associated with this grade_item.
 173       * @var int $outcomeid
 174       */
 175      public $outcomeid;
 176  
 177      /**
 178       * The grade_outcome this grade is associated with, if applicable.
 179       * @var grade_outcome $outcome
 180       */
 181      public $outcome;
 182  
 183      /**
 184       * grade required to pass. (grademin <= gradepass <= grademax)
 185       * @var float $gradepass
 186       */
 187      public $gradepass = 0;
 188  
 189      /**
 190       * Multiply all grades by this number.
 191       * @var float $multfactor
 192       */
 193      public $multfactor = 1.0;
 194  
 195      /**
 196       * Add this to all grades.
 197       * @var float $plusfactor
 198       */
 199      public $plusfactor = 0;
 200  
 201      /**
 202       * Aggregation coeficient used for weighted averages or extra credit
 203       * @var float $aggregationcoef
 204       */
 205      public $aggregationcoef = 0;
 206  
 207      /**
 208       * Aggregation coeficient used for weighted averages only
 209       * @var float $aggregationcoef2
 210       */
 211      public $aggregationcoef2 = 0;
 212  
 213      /**
 214       * Sorting order of the columns.
 215       * @var int $sortorder
 216       */
 217      public $sortorder = 0;
 218  
 219      /**
 220       * Display type of the grades (Real, Percentage, Letter, or default).
 221       * @var int $display
 222       */
 223      public $display = GRADE_DISPLAY_TYPE_DEFAULT;
 224  
 225      /**
 226       * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
 227       * @var int $decimals
 228       */
 229      public $decimals = null;
 230  
 231      /**
 232       * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
 233       * @var int $locked
 234       */
 235      public $locked = 0;
 236  
 237      /**
 238       * Date after which the grade will be locked. Empty means no automatic locking.
 239       * @var int $locktime
 240       */
 241      public $locktime = 0;
 242  
 243      /**
 244       * If set, the whole column will be recalculated, then this flag will be switched off.
 245       * @var bool $needsupdate
 246       */
 247      public $needsupdate = 1;
 248  
 249      /**
 250       * If set, the grade item's weight has been overridden by a user and should not be automatically adjusted.
 251       */
 252      public $weightoverride = 0;
 253  
 254      /**
 255       * Cached dependson array
 256       * @var array An array of cached grade item dependencies.
 257       */
 258      public $dependson_cache = null;
 259  
 260      /**
 261       * @var bool If we regrade this item should we mark it as overridden?
 262       */
 263      public $markasoverriddenwhengraded = true;
 264  
 265      /**
 266       * Constructor. Optionally (and by default) attempts to fetch corresponding row from the database
 267       *
 268       * @param array $params An array with required parameters for this grade object.
 269       * @param bool $fetch Whether to fetch corresponding row from the database or not,
 270       *        optional fields might not be defined if false used
 271       */
 272      public function __construct($params = null, $fetch = true) {
 273          global $CFG;
 274          // Set grademax from $CFG->gradepointdefault .
 275          self::set_properties($this, array('grademax' => $CFG->gradepointdefault));
 276          parent::__construct($params, $fetch);
 277      }
 278  
 279      /**
 280       * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
 281       * Force regrading if necessary, rounds the float numbers using php function,
 282       * the reason is we need to compare the db value with computed number to skip regrading if possible.
 283       *
 284       * @param string $source from where was the object inserted (mod/forum, manual, etc.)
 285       * @param bool $isbulkupdate If bulk grade update is happening.
 286       * @return bool success
 287       */
 288      public function update($source = null, $isbulkupdate = false) {
 289          // reset caches
 290          $this->dependson_cache = null;
 291  
 292          // Retrieve scale and infer grademax/min from it if needed
 293          $this->load_scale();
 294  
 295          // make sure there is not 0 in outcomeid
 296          if (empty($this->outcomeid)) {
 297              $this->outcomeid = null;
 298          }
 299  
 300          if ($this->qualifies_for_regrading()) {
 301              $this->force_regrading();
 302          }
 303  
 304          $this->timemodified = time();
 305  
 306          $this->grademin        = grade_floatval($this->grademin);
 307          $this->grademax        = grade_floatval($this->grademax);
 308          $this->multfactor      = grade_floatval($this->multfactor);
 309          $this->plusfactor      = grade_floatval($this->plusfactor);
 310          $this->aggregationcoef = grade_floatval($this->aggregationcoef);
 311          $this->aggregationcoef2 = grade_floatval($this->aggregationcoef2);
 312  
 313          $result = parent::update($source, $isbulkupdate);
 314  
 315          if ($result) {
 316              $event = \core\event\grade_item_updated::create_from_grade_item($this);
 317              $event->trigger();
 318          }
 319  
 320          return $result;
 321      }
 322  
 323      /**
 324       * Compares the values held by this object with those of the matching record in DB, and returns
 325       * whether or not these differences are sufficient to justify an update of all parent objects.
 326       * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
 327       *
 328       * @return bool
 329       */
 330      public function qualifies_for_regrading() {
 331          if (empty($this->id)) {
 332              return false;
 333          }
 334  
 335          $db_item = new grade_item(array('id' => $this->id));
 336  
 337          $calculationdiff = $db_item->calculation != $this->calculation;
 338          $categorydiff    = $db_item->categoryid  != $this->categoryid;
 339          $gradetypediff   = $db_item->gradetype   != $this->gradetype;
 340          $scaleiddiff     = $db_item->scaleid     != $this->scaleid;
 341          $outcomeiddiff   = $db_item->outcomeid   != $this->outcomeid;
 342          $locktimediff    = $db_item->locktime    != $this->locktime;
 343          $grademindiff    = grade_floats_different($db_item->grademin,        $this->grademin);
 344          $grademaxdiff    = grade_floats_different($db_item->grademax,        $this->grademax);
 345          $multfactordiff  = grade_floats_different($db_item->multfactor,      $this->multfactor);
 346          $plusfactordiff  = grade_floats_different($db_item->plusfactor,      $this->plusfactor);
 347          $acoefdiff       = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
 348          $acoefdiff2      = grade_floats_different($db_item->aggregationcoef2, $this->aggregationcoef2);
 349          $weightoverride  = grade_floats_different($db_item->weightoverride, $this->weightoverride);
 350  
 351          $needsupdatediff = !$db_item->needsupdate &&  $this->needsupdate;    // force regrading only if setting the flag first time
 352          $lockeddiff      = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
 353  
 354          return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
 355               || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
 356               || $lockeddiff || $acoefdiff || $acoefdiff2 || $weightoverride || $locktimediff);
 357      }
 358  
 359      /**
 360       * Finds and returns a grade_item instance based on params.
 361       *
 362       * @static
 363       * @param array $params associative arrays varname=>value
 364       * @return grade_item|bool Returns a grade_item instance or false if none found
 365       */
 366      public static function fetch($params) {
 367          return grade_object::fetch_helper('grade_items', 'grade_item', $params);
 368      }
 369  
 370      /**
 371       * Check to see if there are any existing grades for this grade_item.
 372       *
 373       * @return boolean - true if there are valid grades for this grade_item.
 374       */
 375      public function has_grades() {
 376          global $DB;
 377  
 378          $count = $DB->count_records_select('grade_grades',
 379                                             'itemid = :gradeitemid AND finalgrade IS NOT NULL',
 380                                             array('gradeitemid' => $this->id));
 381          return $count > 0;
 382      }
 383  
 384      /**
 385       * Check to see if there are existing overridden grades for this grade_item.
 386       *
 387       * @return boolean - true if there are overridden grades for this grade_item.
 388       */
 389      public function has_overridden_grades() {
 390          global $DB;
 391  
 392          $count = $DB->count_records_select('grade_grades',
 393                                             'itemid = :gradeitemid AND finalgrade IS NOT NULL AND overridden > 0',
 394                                             array('gradeitemid' => $this->id));
 395          return $count > 0;
 396      }
 397  
 398      /**
 399       * Finds and returns all grade_item instances based on params.
 400       *
 401       * @static
 402       * @param array $params associative arrays varname=>value
 403       * @return array array of grade_item instances or false if none found.
 404       */
 405      public static function fetch_all($params) {
 406          return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
 407      }
 408  
 409      /**
 410       * Delete all grades and force_regrading of parent category.
 411       *
 412       * @param string $source from where was the object deleted (mod/forum, manual, etc.)
 413       * @return bool success
 414       */
 415      public function delete($source=null) {
 416          global $DB;
 417  
 418          $transaction = $DB->start_delegated_transaction();
 419          $this->delete_all_grades($source);
 420          $success = parent::delete($source);
 421          $transaction->allow_commit();
 422  
 423          if ($success) {
 424              $event = \core\event\grade_item_deleted::create_from_grade_item($this);
 425              $event->trigger();
 426          }
 427  
 428          return $success;
 429      }
 430  
 431      /**
 432       * Delete all grades
 433       *
 434       * @param string $source from where was the object deleted (mod/forum, manual, etc.)
 435       * @return bool
 436       */
 437      public function delete_all_grades($source=null) {
 438          global $DB;
 439  
 440          $transaction = $DB->start_delegated_transaction();
 441  
 442          if (!$this->is_course_item()) {
 443              $this->force_regrading();
 444          }
 445  
 446          if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
 447              foreach ($grades as $grade) {
 448                  $grade->delete($source);
 449              }
 450          }
 451  
 452          // Delete all the historical files.
 453          // We only support feedback files for modules atm.
 454          if ($this->is_external_item()) {
 455              $fs = new file_storage();
 456              $fs->delete_area_files($this->get_context()->id, GRADE_FILE_COMPONENT, GRADE_HISTORY_FEEDBACK_FILEAREA);
 457          }
 458  
 459          $transaction->allow_commit();
 460  
 461          return true;
 462      }
 463  
 464      /**
 465       * Duplicate grade item.
 466       *
 467       * @return grade_item The duplicate grade item
 468       */
 469      public function duplicate() {
 470          // Convert current object to array.
 471          $copy = (array) $this;
 472  
 473          if (empty($copy["id"])) {
 474              throw new moodle_exception('invalidgradeitemid');
 475          }
 476  
 477          // Remove fields that will be either unique or automatically filled.
 478          $removekeys = array();
 479          $removekeys[] = 'id';
 480          $removekeys[] = 'idnumber';
 481          $removekeys[] = 'timecreated';
 482          $removekeys[] = 'sortorder';
 483          foreach ($removekeys as $key) {
 484              unset($copy[$key]);
 485          }
 486  
 487          // Addendum to name.
 488          $copy["itemname"] = get_string('duplicatedgradeitem', 'grades', $copy["itemname"]);
 489  
 490          // Create new grade item.
 491          $gradeitem = new grade_item($copy);
 492  
 493          // Insert grade item into database.
 494          $gradeitem->insert();
 495  
 496          return $gradeitem;
 497      }
 498  
 499      /**
 500       * In addition to perform parent::insert(), calls force_regrading() method too.
 501       *
 502       * @param string $source From where was the object inserted (mod/forum, manual, etc.)
 503       * @param string $isbulkupdate If bulk grade update is happening.
 504       * @return int PK ID if successful, false otherwise
 505       */
 506      public function insert($source = null, $isbulkupdate = false) {
 507          global $CFG, $DB;
 508  
 509          if (empty($this->courseid)) {
 510              throw new \moodle_exception('cannotinsertgrade');
 511          }
 512  
 513          // load scale if needed
 514          $this->load_scale();
 515  
 516          // add parent category if needed
 517          if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
 518              $course_category = grade_category::fetch_course_category($this->courseid);
 519              $this->categoryid = $course_category->id;
 520  
 521          }
 522  
 523          // always place the new items at the end, move them after insert if needed
 524          $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid));
 525          if (!empty($last_sortorder)) {
 526              $this->sortorder = $last_sortorder + 1;
 527          } else {
 528              $this->sortorder = 1;
 529          }
 530  
 531          // add proper item numbers to manual items
 532          if ($this->itemtype == 'manual') {
 533              if (empty($this->itemnumber)) {
 534                  $this->itemnumber = 0;
 535              }
 536          }
 537  
 538          // make sure there is not 0 in outcomeid
 539          if (empty($this->outcomeid)) {
 540              $this->outcomeid = null;
 541          }
 542  
 543          $this->timecreated = $this->timemodified = time();
 544  
 545          if (parent::insert($source, $isbulkupdate)) {
 546              // force regrading of items if needed
 547              $this->force_regrading();
 548  
 549              $event = \core\event\grade_item_created::create_from_grade_item($this);
 550              $event->trigger();
 551  
 552              return $this->id;
 553  
 554          } else {
 555              debugging("Could not insert this grade_item in the database!");
 556              return false;
 557          }
 558      }
 559  
 560      /**
 561       * Set idnumber of grade item, updates also course_modules table
 562       *
 563       * @param string $idnumber (without magic quotes)
 564       * @return bool success
 565       */
 566      public function add_idnumber($idnumber) {
 567          global $DB;
 568          if (!empty($this->idnumber)) {
 569              return false;
 570          }
 571  
 572          if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
 573              if ($this->itemnumber == 0) {
 574                  // for activity modules, itemnumber 0 is synced with the course_modules
 575                  if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
 576                      return false;
 577                  }
 578                  if (!empty($cm->idnumber)) {
 579                      return false;
 580                  }
 581                  $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
 582                  $this->idnumber = $idnumber;
 583                  return $this->update();
 584              } else {
 585                  $this->idnumber = $idnumber;
 586                  return $this->update();
 587              }
 588  
 589          } else {
 590              $this->idnumber = $idnumber;
 591              return $this->update();
 592          }
 593      }
 594  
 595      /**
 596       * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
 597       * $userid is given) or the locked state of a specific grade within this item if a specific
 598       * $userid is given and the grade_item is unlocked.
 599       *
 600       * @param int $userid The user's ID
 601       * @return bool Locked state
 602       */
 603      public function is_locked($userid=NULL) {
 604          global $CFG;
 605  
 606          // Override for any grade items belonging to activities which are in the process of being deleted.
 607          require_once($CFG->dirroot . '/course/lib.php');
 608          if (course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance)) {
 609              return true;
 610          }
 611  
 612          if (!empty($this->locked)) {
 613              return true;
 614          }
 615  
 616          if (!empty($userid)) {
 617              if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
 618                  $grade->grade_item =& $this; // prevent db fetching of cached grade_item
 619                  return $grade->is_locked();
 620              }
 621          }
 622  
 623          return false;
 624      }
 625  
 626      /**
 627       * Locks or unlocks this grade_item and (optionally) all its associated final grades.
 628       *
 629       * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
 630       * @param bool $cascade Lock/unlock child objects too
 631       * @param bool $refresh Refresh grades when unlocking
 632       * @return bool True if grade_item all grades updated, false if at least one update fails
 633       */
 634      public function set_locked($lockedstate, $cascade=false, $refresh=true) {
 635          if ($lockedstate) {
 636          /// setting lock
 637              if ($this->needsupdate) {
 638                  return false; // can not lock grade without first having final grade
 639              }
 640  
 641              $this->locked = time();
 642              $this->update();
 643  
 644              if ($cascade) {
 645                  $grades = $this->get_final();
 646                  foreach($grades as $g) {
 647                      $grade = new grade_grade($g, false);
 648                      $grade->grade_item =& $this;
 649                      $grade->set_locked(1, null, false);
 650                  }
 651              }
 652  
 653              return true;
 654  
 655          } else {
 656          /// removing lock
 657              if (!empty($this->locked) and $this->locktime < time()) {
 658                  //we have to reset locktime or else it would lock up again
 659                  $this->locktime = 0;
 660              }
 661  
 662              $this->locked = 0;
 663              $this->update();
 664  
 665              if ($cascade) {
 666                  if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
 667                      foreach($grades as $grade) {
 668                          $grade->grade_item =& $this;
 669                          $grade->set_locked(0, null, false);
 670                      }
 671                  }
 672              }
 673  
 674              if ($refresh) {
 675                  //refresh when unlocking
 676                  $this->refresh_grades();
 677              }
 678  
 679              return true;
 680          }
 681      }
 682  
 683      /**
 684       * Lock the grade if needed. Make sure this is called only when final grades are valid
 685       */
 686      public function check_locktime() {
 687          if (!empty($this->locked)) {
 688              return; // already locked
 689          }
 690  
 691          if ($this->locktime and $this->locktime < time()) {
 692              $this->locked = time();
 693              $this->update('locktime');
 694          }
 695      }
 696  
 697      /**
 698       * Set the locktime for this grade item.
 699       *
 700       * @param int $locktime timestamp for lock to activate
 701       * @return void
 702       */
 703      public function set_locktime($locktime) {
 704          $this->locktime = $locktime;
 705          $this->update();
 706      }
 707  
 708      /**
 709       * Set the locktime for this grade item.
 710       *
 711       * @return int $locktime timestamp for lock to activate
 712       */
 713      public function get_locktime() {
 714          return $this->locktime;
 715      }
 716  
 717      /**
 718       * Set the hidden status of grade_item and all grades.
 719       *
 720       * 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
 721       *
 722       * @param int $hidden new hidden status
 723       * @param bool $cascade apply to child objects too
 724       */
 725      public function set_hidden($hidden, $cascade=false) {
 726          parent::set_hidden($hidden, $cascade);
 727  
 728          if ($cascade) {
 729              if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
 730                  foreach($grades as $grade) {
 731                      $grade->grade_item =& $this;
 732                      $grade->set_hidden($hidden, $cascade);
 733                  }
 734              }
 735          }
 736  
 737          //if marking item visible make sure category is visible MDL-21367
 738          if( !$hidden ) {
 739              $category_array = grade_category::fetch_all(array('id'=>$this->categoryid));
 740              if ($category_array && array_key_exists($this->categoryid, $category_array)) {
 741                  $category = $category_array[$this->categoryid];
 742                  //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
 743                  $category->set_hidden($hidden, false);
 744              }
 745          }
 746      }
 747  
 748      /**
 749       * Returns the number of grades that are hidden
 750       *
 751       * @param string $groupsql SQL to limit the query by group
 752       * @param array $params SQL params for $groupsql
 753       * @param string $groupwheresql Where conditions for $groupsql
 754       * @return int The number of hidden grades
 755       */
 756      public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
 757          global $DB;
 758          $params = (array)$params;
 759          $params['itemid'] = $this->id;
 760  
 761          return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
 762                              ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
 763      }
 764  
 765      /**
 766       * Mark regrading as finished successfully. This will also be called when subsequent regrading will not change any grades.
 767       * Situations such as an error being found will still result in the regrading being finished.
 768       */
 769      public function regrading_finished() {
 770          global $DB;
 771          $this->needsupdate = 0;
 772          //do not use $this->update() because we do not want this logged in grade_item_history
 773          $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id));
 774      }
 775  
 776      /**
 777       * Performs the necessary calculations on the grades_final referenced by this grade_item.
 778       * Also resets the needsupdate flag once successfully performed.
 779       *
 780       * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
 781       * because the regrading must be done in correct order!!
 782       *
 783       * @param int $userid Supply a user ID to limit the regrading to a single user
 784       * @param \core\progress\base|null $progress Optional progress object, will be updated per user
 785       * @return bool true if ok, error string otherwise
 786       */
 787      public function regrade_final_grades($userid=null, ?\core\progress\base $progress = null) {
 788          global $CFG, $DB;
 789  
 790          // locked grade items already have correct final grades
 791          if ($this->is_locked()) {
 792              return true;
 793          }
 794  
 795          // calculation produces final value using formula from other final values
 796          if ($this->is_calculated()) {
 797              if ($this->compute($userid)) {
 798                  return true;
 799              } else {
 800                  return "Could not calculate grades for grade item"; // TODO: improve and localize
 801              }
 802  
 803          // noncalculated outcomes already have final values - raw grades not used
 804          } else if ($this->is_outcome_item()) {
 805              return true;
 806  
 807          // aggregate the category grade
 808          } else if ($this->is_category_item() or $this->is_course_item()) {
 809              // aggregate category grade item
 810              $category = $this->load_item_category();
 811              $category->grade_item =& $this;
 812              if ($category->generate_grades($userid, $progress)) {
 813                  return true;
 814              } else {
 815                  return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
 816              }
 817  
 818          } else if ($this->is_manual_item()) {
 819              // manual items track only final grades, no raw grades
 820              return true;
 821  
 822          } else if (!$this->is_raw_used()) {
 823              // hmm - raw grades are not used- nothing to regrade
 824              return true;
 825          }
 826  
 827          // normal grade item - just new final grades
 828          $result = true;
 829          $grade_inst = new grade_grade();
 830          $fields = implode(',', $grade_inst->required_fields);
 831          if ($userid) {
 832              $params = array($this->id, $userid);
 833              $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
 834          } else {
 835              $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields);
 836          }
 837          if ($rs) {
 838              foreach ($rs as $grade_record) {
 839                  $grade = new grade_grade($grade_record, false);
 840  
 841                  // Incrementing the progress by nothing causes it to send an update (once per second)
 842                  // to the web browser so as to prevent the connection timing out.
 843                  if ($progress) {
 844                      $progress->increment_progress(0);
 845                  }
 846  
 847                  if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
 848                      // this grade is locked - final grade must be ok
 849                      continue;
 850                  }
 851  
 852                  $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
 853  
 854                  if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
 855                      $success = $grade->update('system');
 856  
 857                      // If successful trigger a user_graded event.
 858                      if ($success) {
 859                          $grade->load_grade_item();
 860                          \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger();
 861                      } else {
 862                          $result = "Internal error updating final grade";
 863                      }
 864                  }
 865              }
 866              $rs->close();
 867          }
 868  
 869          return $result;
 870      }
 871  
 872      /**
 873       * Given a float grade value or integer grade scale, applies a number of adjustment based on
 874       * grade_item variables and returns the result.
 875       *
 876       * @param float $rawgrade The raw grade value
 877       * @param float $rawmin original rawmin
 878       * @param float $rawmax original rawmax
 879       * @return mixed
 880       */
 881      public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
 882          if (is_null($rawgrade)) {
 883              return null;
 884          }
 885  
 886          if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
 887  
 888              if ($this->grademax < $this->grademin) {
 889                  return null;
 890              }
 891  
 892              if ($this->grademax == $this->grademin) {
 893                  return $this->grademax; // no range
 894              }
 895  
 896              // Standardise score to the new grade range
 897              // NOTE: skip if the activity provides a manual rescaling option.
 898              $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
 899              if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
 900                  $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
 901              }
 902  
 903              // Apply other grade_item factors
 904              $rawgrade *= $this->multfactor;
 905              $rawgrade += $this->plusfactor;
 906  
 907              return $this->bounded_grade($rawgrade);
 908  
 909          } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
 910              if (empty($this->scale)) {
 911                  $this->load_scale();
 912              }
 913  
 914              if ($this->grademax < 0) {
 915                  return null; // scale not present - no grade
 916              }
 917  
 918              if ($this->grademax == 0) {
 919                  return $this->grademax; // only one option
 920              }
 921  
 922              // Convert scale if needed
 923              // NOTE: skip if the activity provides a manual rescaling option.
 924              $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
 925              if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
 926                  // This should never happen because scales are locked if they are in use.
 927                  $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
 928              }
 929  
 930              return $this->bounded_grade($rawgrade);
 931  
 932  
 933          } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
 934              // somebody changed the grading type when grades already existed
 935              return null;
 936  
 937          } else {
 938              debugging("Unknown grade type");
 939              return null;
 940          }
 941      }
 942  
 943      /**
 944       * Update the rawgrademax and rawgrademin for all grade_grades records for this item.
 945       * Scale every rawgrade to maintain the percentage. This function should be called
 946       * after the gradeitem has been updated to the new min and max values.
 947       *
 948       * @param float $oldgrademin The previous grade min value
 949       * @param float $oldgrademax The previous grade max value
 950       * @param float $newgrademin The new grade min value
 951       * @param float $newgrademax The new grade max value
 952       * @param string $source from where was the object inserted (mod/forum, manual, etc.)
 953       * @return bool True on success
 954       */
 955      public function rescale_grades_keep_percentage($oldgrademin, $oldgrademax, $newgrademin, $newgrademax, $source = null) {
 956          global $DB;
 957  
 958          if (empty($this->id)) {
 959              return false;
 960          }
 961  
 962          if ($oldgrademax <= $oldgrademin) {
 963              // Grades cannot be scaled.
 964              return false;
 965          }
 966          $scale = ($newgrademax - $newgrademin) / ($oldgrademax - $oldgrademin);
 967          if (($newgrademax - $newgrademin) <= 1) {
 968              // We would lose too much precision, lets bail.
 969              return false;
 970          }
 971  
 972          $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id));
 973  
 974          foreach ($rs as $graderecord) {
 975              // For each record, create an object to work on.
 976              $grade = new grade_grade($graderecord, false);
 977              // Set this object in the item so it doesn't re-fetch it.
 978              $grade->grade_item = $this;
 979  
 980              if (!$this->is_category_item() || ($this->is_category_item() && $grade->is_overridden())) {
 981                  // Updating the raw grade automatically updates the min/max.
 982                  if ($this->is_raw_used()) {
 983                      $rawgrade = (($grade->rawgrade - $oldgrademin) * $scale) + $newgrademin;
 984                      $this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE, null, null, null, $grade);
 985                  } else {
 986                      $finalgrade = (($grade->finalgrade - $oldgrademin) * $scale) + $newgrademin;
 987                      $this->update_final_grade($grade->userid, $finalgrade, $source);
 988                  }
 989              }
 990          }
 991          $rs->close();
 992  
 993          // Mark this item for regrading.
 994          $this->force_regrading();
 995  
 996          return true;
 997      }
 998  
 999      /**
1000       * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
1001       *
1002       * @return void
1003       */
1004      public function force_regrading() {
1005          global $DB;
1006          $this->needsupdate = 1;
1007          //mark this item and course item only - categories and calculated items are always regraded
1008          $wheresql = "(itemtype='course' OR id=?) AND courseid=?";
1009          $params   = array($this->id, $this->courseid);
1010          $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
1011      }
1012  
1013      /**
1014       * Instantiates a grade_scale object from the DB if this item's scaleid variable is set
1015       *
1016       * @return grade_scale Returns a grade_scale object or null if no scale used
1017       */
1018      public function load_scale() {
1019          if ($this->gradetype != GRADE_TYPE_SCALE) {
1020              $this->scaleid = null;
1021          }
1022  
1023          if (!empty($this->scaleid)) {
1024              //do not load scale if already present
1025              if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
1026                  $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
1027                  if (!$this->scale) {
1028                      debugging('Incorrect scale id: '.$this->scaleid);
1029                      $this->scale = null;
1030                      return null;
1031                  }
1032                  $this->scale->load_items();
1033              }
1034  
1035              // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
1036              // stay with the current min=1 max=count(scaleitems)
1037              $this->grademax = count($this->scale->scale_items);
1038              $this->grademin = 1;
1039  
1040          } else {
1041              $this->scale = null;
1042          }
1043  
1044          return $this->scale;
1045      }
1046  
1047      /**
1048       * Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set
1049       *
1050       * @return grade_outcome This grade item's associated grade_outcome or null
1051       */
1052      public function load_outcome() {
1053          if (!empty($this->outcomeid)) {
1054              $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
1055          }
1056          return $this->outcome;
1057      }
1058  
1059      /**
1060       * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
1061       * or category attached to category item.
1062       *
1063       * @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item
1064       */
1065      public function get_parent_category() {
1066          if ($this->is_category_item() or $this->is_course_item()) {
1067              return $this->get_item_category();
1068  
1069          } else {
1070              return grade_category::fetch(array('id'=>$this->categoryid));
1071          }
1072      }
1073  
1074      /**
1075       * Calls upon the get_parent_category method to retrieve the grade_category object
1076       * from the DB and assigns it to $this->parent_category. It also returns the object.
1077       *
1078       * @return grade_category This grade item's parent grade_category.
1079       */
1080      public function load_parent_category() {
1081          if (empty($this->parent_category->id)) {
1082              $this->parent_category = $this->get_parent_category();
1083          }
1084          return $this->parent_category;
1085      }
1086  
1087      /**
1088       * Returns the grade_category for a grade category grade item
1089       *
1090       * @return grade_category|bool Returns a grade_category instance if applicable or false otherwise
1091       */
1092      public function get_item_category() {
1093          if (!$this->is_course_item() and !$this->is_category_item()) {
1094              return false;
1095          }
1096          return grade_category::fetch(array('id'=>$this->iteminstance));
1097      }
1098  
1099      /**
1100       * Calls upon the get_item_category method to retrieve the grade_category object
1101       * from the DB and assigns it to $this->item_category. It also returns the object.
1102       *
1103       * @return grade_category
1104       */
1105      public function load_item_category() {
1106          if (empty($this->item_category->id)) {
1107              $this->item_category = $this->get_item_category();
1108          }
1109          return $this->item_category;
1110      }
1111  
1112      /**
1113       * Is the grade item associated with category?
1114       *
1115       * @return bool
1116       */
1117      public function is_category_item() {
1118          return ($this->itemtype == 'category');
1119      }
1120  
1121      /**
1122       * Is the grade item associated with course?
1123       *
1124       * @return bool
1125       */
1126      public function is_course_item() {
1127          return ($this->itemtype == 'course');
1128      }
1129  
1130      /**
1131       * Is this a manually graded item?
1132       *
1133       * @return bool
1134       */
1135      public function is_manual_item() {
1136          return ($this->itemtype == 'manual');
1137      }
1138  
1139      /**
1140       * Is this an outcome item?
1141       *
1142       * @return bool
1143       */
1144      public function is_outcome_item() {
1145          return !empty($this->outcomeid);
1146      }
1147  
1148      /**
1149       * Is the grade item external - associated with module, plugin or something else?
1150       *
1151       * @return bool
1152       */
1153      public function is_external_item() {
1154          return ($this->itemtype == 'mod');
1155      }
1156  
1157      /**
1158       * Is the grade item overridable
1159       *
1160       * @return bool
1161       */
1162      public function is_overridable_item() {
1163          if ($this->is_course_item() or $this->is_category_item()) {
1164              $overridable = (bool) get_config('moodle', 'grade_overridecat');
1165          } else {
1166              $overridable = false;
1167          }
1168  
1169          return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $overridable);
1170      }
1171  
1172      /**
1173       * Is the grade item feedback overridable
1174       *
1175       * @return bool
1176       */
1177      public function is_overridable_item_feedback() {
1178          return !$this->is_outcome_item() and $this->is_external_item();
1179      }
1180  
1181      /**
1182       * Returns true if grade items uses raw grades
1183       *
1184       * @return bool
1185       */
1186      public function is_raw_used() {
1187          return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
1188      }
1189  
1190      /**
1191       * Returns true if the grade item is an aggreggated type grade.
1192       *
1193       * @since  Moodle 2.8.7, 2.9.1
1194       * @return bool
1195       */
1196      public function is_aggregate_item() {
1197          return ($this->is_category_item() || $this->is_course_item());
1198      }
1199  
1200      /**
1201       * Returns the grade item associated with the course
1202       *
1203       * @param int $courseid
1204       * @return grade_item Course level grade item object
1205       */
1206      public static function fetch_course_item($courseid) {
1207          if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
1208              return $course_item;
1209          }
1210  
1211          // first get category - it creates the associated grade item
1212          $course_category = grade_category::fetch_course_category($courseid);
1213          return $course_category->get_grade_item();
1214      }
1215  
1216      /**
1217       * Is grading object editable?
1218       *
1219       * @return bool
1220       */
1221      public function is_editable() {
1222          return true;
1223      }
1224  
1225      /**
1226       * Checks if grade calculated. Returns this object's calculation.
1227       *
1228       * @return bool true if grade item calculated.
1229       */
1230      public function is_calculated() {
1231          if (empty($this->calculation)) {
1232              return false;
1233          }
1234  
1235          /*
1236           * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
1237           * we would have to fetch all course grade items to find out the ids.
1238           * Also if user changes the idnumber the formula does not need to be updated.
1239           */
1240  
1241          // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
1242          if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
1243              $this->set_calculation($this->calculation);
1244          }
1245  
1246          return !empty($this->calculation);
1247      }
1248  
1249      /**
1250       * Returns calculation string if grade calculated.
1251       *
1252       * @return string Returns the grade item's calculation if calculation is used, null if not
1253       */
1254      public function get_calculation() {
1255          if ($this->is_calculated()) {
1256              return grade_item::denormalize_formula($this->calculation, $this->courseid);
1257  
1258          } else {
1259              return NULL;
1260          }
1261      }
1262  
1263      /**
1264       * Sets this item's calculation (creates it) if not yet set, or
1265       * updates it if already set (in the DB). If no calculation is given,
1266       * the calculation is removed.
1267       *
1268       * @param string $formula string representation of formula used for calculation
1269       * @return bool success
1270       */
1271      public function set_calculation($formula) {
1272          $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
1273          $this->calculation_normalized = true;
1274          return $this->update();
1275      }
1276  
1277      /**
1278       * Denormalizes the calculation formula to [idnumber] form
1279       *
1280       * @param string $formula A string representation of the formula
1281       * @param int $courseid The course ID
1282       * @return string The denormalized formula as a string
1283       */
1284      public static function denormalize_formula($formula, $courseid) {
1285          if (empty($formula)) {
1286              return '';
1287          }
1288  
1289          // denormalize formula - convert ##giXX## to [[idnumber]]
1290          if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
1291              foreach ($matches[1] as $id) {
1292                  if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
1293                      if (!empty($grade_item->idnumber)) {
1294                          $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
1295                      }
1296                  }
1297              }
1298          }
1299  
1300          return $formula;
1301  
1302      }
1303  
1304      /**
1305       * Normalizes the calculation formula to [#giXX#] form
1306       *
1307       * @param string $formula The formula
1308       * @param int $courseid The course ID
1309       * @return string The normalized formula as a string
1310       */
1311      public static function normalize_formula($formula, $courseid) {
1312          $formula = trim($formula);
1313  
1314          if (empty($formula)) {
1315              return NULL;
1316  
1317          }
1318  
1319          // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
1320          if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1321              foreach ($grade_items as $grade_item) {
1322                  $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
1323              }
1324          }
1325  
1326          return $formula;
1327      }
1328  
1329      /**
1330       * Returns the final values for this grade item (as imported by module or other source).
1331       *
1332       * @param int $userid Optional: to retrieve a single user's final grade
1333       * @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance.
1334       */
1335      public function get_final($userid=NULL) {
1336          global $DB;
1337          if ($userid) {
1338              if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) {
1339                  return $user;
1340              }
1341  
1342          } else {
1343              if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) {
1344                  //TODO: speed up with better SQL (MDL-31380)
1345                  $result = array();
1346                  foreach ($grades as $grade) {
1347                      $result[$grade->userid] = $grade;
1348                  }
1349                  return $result;
1350              } else {
1351                  return array();
1352              }
1353          }
1354      }
1355  
1356      /**
1357       * Get (or create if not exist yet) grade for this user
1358       *
1359       * @param int $userid The user ID
1360       * @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted
1361       * @return grade_grade The grade_grade instance for the user for this grade item
1362       */
1363      public function get_grade($userid, $create=true) {
1364          if (empty($this->id)) {
1365              debugging('Can not use before insert');
1366              return false;
1367          }
1368  
1369          $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
1370          if (empty($grade->id) and $create) {
1371              $grade->insert();
1372          }
1373  
1374          return $grade;
1375      }
1376  
1377      /**
1378       * Returns the sortorder of this grade_item. This method is also available in
1379       * grade_category, for cases where the object type is not know.
1380       *
1381       * @return int Sort order
1382       */
1383      public function get_sortorder() {
1384          return $this->sortorder;
1385      }
1386  
1387      /**
1388       * Returns the idnumber of this grade_item. This method is also available in
1389       * grade_category, for cases where the object type is not know.
1390       *
1391       * @return string The grade item idnumber
1392       */
1393      public function get_idnumber() {
1394          return $this->idnumber;
1395      }
1396  
1397      /**
1398       * Returns this grade_item. This method is also available in
1399       * grade_category, for cases where the object type is not know.
1400       *
1401       * @return grade_item
1402       */
1403      public function get_grade_item() {
1404          return $this;
1405      }
1406  
1407      /**
1408       * Sets the sortorder of this grade_item. This method is also available in
1409       * grade_category, for cases where the object type is not know.
1410       *
1411       * @param int $sortorder
1412       */
1413      public function set_sortorder($sortorder) {
1414          if ($this->sortorder == $sortorder) {
1415              return;
1416          }
1417          $this->sortorder = $sortorder;
1418          $this->update();
1419      }
1420  
1421      /**
1422       * Update this grade item's sortorder so that it will appear after $sortorder
1423       *
1424       * @param int $sortorder The sort order to place this grade item after
1425       */
1426      public function move_after_sortorder($sortorder) {
1427          global $CFG, $DB;
1428  
1429          //make some room first
1430          $params = array($sortorder, $this->courseid);
1431          $sql = "UPDATE {grade_items}
1432                     SET sortorder = sortorder + 1
1433                   WHERE sortorder > ? AND courseid = ?";
1434          $DB->execute($sql, $params);
1435  
1436          $this->set_sortorder($sortorder + 1);
1437      }
1438  
1439      /**
1440       * Detect duplicate grade item's sortorder and re-sort them.
1441       * Note: Duplicate sortorder will be introduced while duplicating activities or
1442       * merging two courses.
1443       *
1444       * @param int $courseid id of the course for which grade_items sortorder need to be fixed.
1445       */
1446      public static function fix_duplicate_sortorder($courseid) {
1447          global $DB;
1448  
1449          $transaction = $DB->start_delegated_transaction();
1450  
1451          $sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder
1452                      FROM {grade_items} g1
1453                      JOIN {grade_items} g2 ON g1.courseid = g2.courseid
1454                  WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid
1455                  ORDER BY g1.sortorder DESC, g1.id DESC";
1456  
1457          // Get all duplicates in course highest sort order, and higest id first so that we can make space at the
1458          // bottom higher end of the sort orders and work down by id.
1459          $rs = $DB->get_recordset_sql($sql, array('courseid' => $courseid));
1460  
1461          foreach($rs as $duplicate) {
1462              $DB->execute("UPDATE {grade_items}
1463                              SET sortorder = sortorder + 1
1464                            WHERE courseid = :courseid AND
1465                            (sortorder > :sortorder OR (sortorder = :sortorder2 AND id > :id))",
1466                  array('courseid' => $duplicate->courseid,
1467                      'sortorder' => $duplicate->sortorder,
1468                      'sortorder2' => $duplicate->sortorder,
1469                      'id' => $duplicate->id));
1470          }
1471          $rs->close();
1472          $transaction->allow_commit();
1473      }
1474  
1475      /**
1476       * Returns the most descriptive field for this object.
1477       *
1478       * Determines what type of grade item it is then returns the appropriate string
1479       *
1480       * @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
1481       * @param bool $escape Whether the returned category name is to be HTML escaped or not.
1482       * @return string name
1483       */
1484      public function get_name($fulltotal=false, $escape = true) {
1485          global $CFG;
1486          require_once($CFG->dirroot . '/course/lib.php');
1487          if (strval($this->itemname) !== '') {
1488              // MDL-10557
1489  
1490              // Make it obvious to users if the course module to which this grade item relates, is currently being removed.
1491              $deletionpending = course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance);
1492              $deletionnotice = get_string('gradesmoduledeletionprefix', 'grades');
1493  
1494              $options = ['context' => context_course::instance($this->courseid), 'escape' => $escape];
1495              return $deletionpending ?
1496                  format_string($deletionnotice . ' ' . $this->itemname, true, $options) :
1497                  format_string($this->itemname, true, $options);
1498  
1499          } else if ($this->is_course_item()) {
1500              return get_string('coursetotal', 'grades');
1501  
1502          } else if ($this->is_category_item()) {
1503              if ($fulltotal) {
1504                  $category = $this->load_parent_category();
1505                  $a = new stdClass();
1506                  $a->category = $category->get_name($escape);
1507                  return get_string('categorytotalfull', 'grades', $a);
1508              } else {
1509              return get_string('categorytotal', 'grades');
1510              }
1511  
1512          } else {
1513              return get_string('gradenoun');
1514          }
1515      }
1516  
1517      /**
1518       * A grade item can return a more detailed description which will be added to the header of the column/row in some reports.
1519       *
1520       * @return string description
1521       */
1522      public function get_description() {
1523          if ($this->is_course_item() || $this->is_category_item()) {
1524              $categoryitem = $this->load_item_category();
1525              return $categoryitem->get_description();
1526          }
1527          return '';
1528      }
1529  
1530      /**
1531       * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1532       *
1533       * @param int $parentid The ID of the new parent
1534       * @param bool $updateaggregationfields Whether or not to convert the aggregation fields when switching between category.
1535       *                          Set this to false when the aggregation fields have been updated in prevision of the new
1536       *                          category, typically when the item is freshly created.
1537       * @return bool True if success
1538       */
1539      public function set_parent($parentid, $updateaggregationfields = true) {
1540          if ($this->is_course_item() or $this->is_category_item()) {
1541              throw new \moodle_exception('cannotsetparentforcatoritem');
1542          }
1543  
1544          if ($this->categoryid == $parentid) {
1545              return true;
1546          }
1547  
1548          // find parent and check course id
1549          if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
1550              return false;
1551          }
1552  
1553          $currentparent = $this->load_parent_category();
1554  
1555          if ($updateaggregationfields) {
1556              $this->set_aggregation_fields_for_aggregation($currentparent->aggregation, $parent_category->aggregation);
1557          }
1558  
1559          $this->force_regrading();
1560  
1561          // set new parent
1562          $this->categoryid = $parent_category->id;
1563          $this->parent_category =& $parent_category;
1564  
1565          return $this->update();
1566      }
1567  
1568      /**
1569       * Update the aggregation fields when the aggregation changed.
1570       *
1571       * This method should always be called when the aggregation has changed, but also when
1572       * the item was moved to another category, even it if uses the same aggregation method.
1573       *
1574       * Some values such as the weight only make sense within a category, once moved the
1575       * values should be reset to let the user adapt them accordingly.
1576       *
1577       * Note that this method does not save the grade item.
1578       * {@link grade_item::update()} has to be called manually after using this method.
1579       *
1580       * @param  int $from Aggregation method constant value.
1581       * @param  int $to   Aggregation method constant value.
1582       * @return boolean   True when at least one field was changed, false otherwise
1583       */
1584      public function set_aggregation_fields_for_aggregation($from, $to) {
1585          $defaults = grade_category::get_default_aggregation_coefficient_values($to);
1586  
1587          $origaggregationcoef = $this->aggregationcoef;
1588          $origaggregationcoef2 = $this->aggregationcoef2;
1589          $origweighoverride = $this->weightoverride;
1590  
1591          if ($from == GRADE_AGGREGATE_SUM && $to == GRADE_AGGREGATE_SUM && $this->weightoverride) {
1592              // Do nothing. We are switching from SUM to SUM and the weight is overriden,
1593              // a teacher would not expect any change in this situation.
1594  
1595          } else if ($from == GRADE_AGGREGATE_WEIGHTED_MEAN && $to == GRADE_AGGREGATE_WEIGHTED_MEAN) {
1596              // Do nothing. The weights can be kept in this case.
1597  
1598          } else if (in_array($from, array(GRADE_AGGREGATE_SUM,  GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))
1599                  && in_array($to, array(GRADE_AGGREGATE_SUM,  GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))) {
1600  
1601              // Reset all but the the extra credit field.
1602              $this->aggregationcoef2 = $defaults['aggregationcoef2'];
1603              $this->weightoverride = $defaults['weightoverride'];
1604  
1605              if ($to != GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
1606                  // Normalise extra credit, except for 'Mean with extra credit' which supports higher values than 1.
1607                  $this->aggregationcoef = min(1, $this->aggregationcoef);
1608              }
1609          } else {
1610              // Reset all.
1611              $this->aggregationcoef = $defaults['aggregationcoef'];
1612              $this->aggregationcoef2 = $defaults['aggregationcoef2'];
1613              $this->weightoverride = $defaults['weightoverride'];
1614          }
1615  
1616          $acoefdiff       = grade_floats_different($origaggregationcoef, $this->aggregationcoef);
1617          $acoefdiff2      = grade_floats_different($origaggregationcoef2, $this->aggregationcoef2);
1618          $weightoverride  = grade_floats_different($origweighoverride, $this->weightoverride);
1619  
1620          return $acoefdiff || $acoefdiff2 || $weightoverride;
1621      }
1622  
1623      /**
1624       * Makes sure value is a valid grade value.
1625       *
1626       * @param float $gradevalue
1627       * @return mixed float or int fixed grade value
1628       */
1629      public function bounded_grade($gradevalue) {
1630          global $CFG;
1631  
1632          if (is_null($gradevalue)) {
1633              return null;
1634          }
1635  
1636          if ($this->gradetype == GRADE_TYPE_SCALE) {
1637              // no >100% grades hack for scale grades!
1638              // 1.5 is rounded to 2 ;-)
1639              return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax);
1640          }
1641  
1642          $grademax = $this->grademax;
1643  
1644          // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1645          $maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by default
1646  
1647          if (!empty($CFG->unlimitedgrades)) {
1648              // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1649              $grademax = $grademax * $maxcoef;
1650          } else if ($this->is_category_item() or $this->is_course_item()) {
1651              $category = $this->load_item_category();
1652              if ($category->aggregation >= 100) {
1653                  // grade >100% hack
1654                  $grademax = $grademax * $maxcoef;
1655              }
1656          }
1657  
1658          return (float)bounded_number($this->grademin, $gradevalue, $grademax);
1659      }
1660  
1661      /**
1662       * Finds out on which other items does this depend directly when doing calculation or category aggregation
1663       *
1664       * @param bool $reset_cache
1665       * @return array of grade_item IDs this one depends on
1666       */
1667      public function depends_on($reset_cache=false) {
1668          global $CFG, $DB;
1669  
1670          if ($reset_cache) {
1671              $this->dependson_cache = null;
1672          } else if (isset($this->dependson_cache)) {
1673              return $this->dependson_cache;
1674          }
1675  
1676          if ($this->is_locked() && !$this->is_category_item()) {
1677              // locked items do not need to be regraded
1678              $this->dependson_cache = array();
1679              return $this->dependson_cache;
1680          }
1681  
1682          if ($this->is_calculated()) {
1683              if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
1684                  $this->dependson_cache = array_unique($matches[1]); // remove duplicates
1685                  return $this->dependson_cache;
1686              } else {
1687                  $this->dependson_cache = array();
1688                  return $this->dependson_cache;
1689              }
1690  
1691          } else if ($grade_category = $this->load_item_category()) {
1692              $params = array();
1693  
1694              //only items with numeric or scale values can be aggregated
1695              if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
1696                  $this->dependson_cache = array();
1697                  return $this->dependson_cache;
1698              }
1699  
1700              $grade_category->apply_forced_settings();
1701  
1702              if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
1703                  $outcomes_sql = "";
1704              } else {
1705                  $outcomes_sql = "AND gi.outcomeid IS NULL";
1706              }
1707  
1708              if (empty($CFG->grade_includescalesinaggregation)) {
1709                  $gtypes = "gi.gradetype = ?";
1710                  $params[] = GRADE_TYPE_VALUE;
1711              } else {
1712                  $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
1713                  $params[] = GRADE_TYPE_VALUE;
1714                  $params[] = GRADE_TYPE_SCALE;
1715              }
1716  
1717              $params[] = $grade_category->id;
1718              $params[] = $this->courseid;
1719              $params[] = $grade_category->id;
1720              $params[] = $this->courseid;
1721              if (empty($CFG->grade_includescalesinaggregation)) {
1722                  $params[] = GRADE_TYPE_VALUE;
1723              } else {
1724                  $params[] = GRADE_TYPE_VALUE;
1725                  $params[] = GRADE_TYPE_SCALE;
1726              }
1727              $sql = "SELECT gi.id
1728                        FROM {grade_items} gi
1729                       WHERE $gtypes
1730                             AND gi.categoryid = ?
1731                             AND gi.courseid = ?
1732                             $outcomes_sql
1733                      UNION
1734  
1735                      SELECT gi.id
1736                        FROM {grade_items} gi, {grade_categories} gc
1737                       WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
1738                             AND gc.parent = ?
1739                             AND gi.courseid = ?
1740                             AND $gtypes
1741                             $outcomes_sql";
1742  
1743              if ($children = $DB->get_records_sql($sql, $params)) {
1744                  $this->dependson_cache = array_keys($children);
1745                  return $this->dependson_cache;
1746              } else {
1747                  $this->dependson_cache = array();
1748                  return $this->dependson_cache;
1749              }
1750  
1751          } else {
1752              $this->dependson_cache = array();
1753              return $this->dependson_cache;
1754          }
1755      }
1756  
1757      /**
1758       * Refetch grades from modules, plugins.
1759       *
1760       * @param int $userid optional, limit the refetch to a single user
1761       * @return bool Returns true on success or if there is nothing to do
1762       */
1763      public function refresh_grades($userid=0) {
1764          global $DB;
1765          if ($this->itemtype == 'mod') {
1766              if ($this->is_outcome_item()) {
1767                  //nothing to do
1768                  return true;
1769              }
1770  
1771              if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) {
1772                  debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
1773                  return false;
1774              }
1775  
1776              if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
1777                  debugging('Can not find course module');
1778                  return false;
1779              }
1780  
1781              $activity->modname    = $this->itemmodule;
1782              $activity->cmidnumber = $cm->idnumber;
1783  
1784              return grade_update_mod_grades($activity, $userid);
1785          }
1786  
1787          return true;
1788      }
1789  
1790      /**
1791       * Updates final grade value for given user, this is a only way to update final
1792       * grades from gradebook and import because it logs the change in history table
1793       * and deals with overridden flag. This flag is set to prevent later overriding
1794       * from raw grades submitted from modules.
1795       *
1796       * @param int $userid The graded user
1797       * @param float|false $finalgrade The float value of final grade, false means do not change
1798       * @param string $source The modification source
1799       * @param string $feedback Optional teacher feedback
1800       * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1801       * @param int $usermodified The ID of the user making the modification
1802       * @param int $timemodified Optional parameter to set the time modified, if not present current time.
1803       * @param bool $isbulkupdate If bulk grade update is happening.
1804       * @return bool success
1805       */
1806      public function update_final_grade($userid, $finalgrade = false, $source = null, $feedback = false,
1807              $feedbackformat = FORMAT_MOODLE, $usermodified = null, $timemodified = null, $isbulkupdate = false) {
1808          global $USER, $CFG;
1809  
1810          $result = true;
1811  
1812          // no grading used or locked
1813          if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1814              return false;
1815          }
1816  
1817          $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1818          $grade->grade_item =& $this; // prevent db fetching of this grade_item
1819  
1820          if (empty($usermodified)) {
1821              $grade->usermodified = $USER->id;
1822          } else {
1823              $grade->usermodified = $usermodified;
1824          }
1825  
1826          if ($grade->is_locked()) {
1827              // do not update locked grades at all
1828              return false;
1829          }
1830  
1831          $locktime = $grade->get_locktime();
1832          if ($locktime and $locktime < time()) {
1833              // do not update grades that should be already locked, force regrade instead
1834              $this->force_regrading();
1835              return false;
1836          }
1837  
1838          $oldgrade = new stdClass();
1839          $oldgrade->finalgrade     = $grade->finalgrade;
1840          $oldgrade->overridden     = $grade->overridden;
1841          $oldgrade->feedback       = $grade->feedback;
1842          $oldgrade->feedbackformat = $grade->feedbackformat;
1843          $oldgrade->rawgrademin    = $grade->rawgrademin;
1844          $oldgrade->rawgrademax    = $grade->rawgrademax;
1845  
1846          // MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly.
1847          $grade->rawgrademin = $this->grademin;
1848          $grade->rawgrademax = $this->grademax;
1849          $grade->rawscaleid  = $this->scaleid;
1850  
1851          // changed grade?
1852          if ($finalgrade !== false) {
1853              if ($this->is_overridable_item() && $this->markasoverriddenwhengraded) {
1854                  $grade->overridden = time();
1855              }
1856  
1857              $grade->finalgrade = $this->bounded_grade($finalgrade);
1858          }
1859  
1860          // do we have comment from teacher?
1861          if ($feedback !== false) {
1862              if ($this->is_overridable_item_feedback()) {
1863                  // external items (modules, plugins) may have own feedback
1864                  $grade->overridden = time();
1865              }
1866  
1867              $grade->feedback       = $feedback;
1868              $grade->feedbackformat = $feedbackformat;
1869          }
1870  
1871          $gradechanged = false;
1872          if (empty($grade->id)) {
1873              $grade->timecreated = null;   // Hack alert - date submitted - no submission yet.
1874              $grade->timemodified = $timemodified ?? time(); // Hack alert - date graded.
1875              $result = (bool)$grade->insert($source, $isbulkupdate);
1876  
1877              // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
1878              if ($result && !is_null($grade->finalgrade)) {
1879                  \core\event\user_graded::create_from_grade($grade)->trigger();
1880              }
1881              $gradechanged = true;
1882          } else {
1883              // Existing grade_grades.
1884  
1885              if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1886                      or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
1887                      or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
1888                      or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {
1889                  $gradechanged = true;
1890              }
1891  
1892              if ($grade->feedback === $oldgrade->feedback and $grade->feedbackformat == $oldgrade->feedbackformat and
1893                      $gradechanged === false) {
1894                  // No grade nor feedback changed.
1895                  return $result;
1896              }
1897  
1898              $grade->timemodified = $timemodified ?? time(); // Hack alert - date graded.
1899              $result = $grade->update($source, $isbulkupdate);
1900  
1901              // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
1902              if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
1903                  \core\event\user_graded::create_from_grade($grade)->trigger();
1904              }
1905          }
1906  
1907          if (!$result) {
1908              // Something went wrong - better force final grade recalculation.
1909              $this->force_regrading();
1910              return $result;
1911          }
1912  
1913          // If we are not updating grades we don't need to recalculate the whole course.
1914          if (!$gradechanged) {
1915              return $result;
1916          }
1917  
1918          if ($this->is_course_item() and !$this->needsupdate) {
1919              if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1920                  $this->force_regrading();
1921              }
1922  
1923          } else if (!$this->needsupdate) {
1924  
1925              $course_item = grade_item::fetch_course_item($this->courseid);
1926              if (!$course_item->needsupdate) {
1927                  if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1928                      $this->force_regrading();
1929                  }
1930              } else {
1931                  $this->force_regrading();
1932              }
1933          }
1934  
1935          return $result;
1936      }
1937  
1938  
1939      /**
1940       * Updates raw grade value for given user, this is a only way to update raw
1941       * grades from external source (modules, etc.),
1942       * because it logs the change in history table and deals with final grade recalculation.
1943       *
1944       * @param int $userid the graded user
1945       * @param mixed $rawgrade float value of raw grade - false means do not change
1946       * @param string $source modification source
1947       * @param string $feedback optional teacher feedback
1948       * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1949       * @param int $usermodified the ID of the user who did the grading
1950       * @param int $dategraded A timestamp of when the student's work was graded
1951       * @param int $datesubmitted A timestamp of when the student's work was submitted
1952       * @param grade_grade $grade A grade object, useful for bulk upgrades
1953       * @param array $feedbackfiles An array identifying the location of files we want to copy to the gradebook feedback area.
1954       *        Example -
1955       *        [
1956       *            'contextid' => 1,
1957       *            'component' => 'mod_xyz',
1958       *            'filearea' => 'mod_xyz_feedback',
1959       *            'itemid' => 2
1960       *        ];
1961       * @param bool $isbulkupdate If bulk grade update is happening.
1962       * @return bool success
1963       */
1964      public function update_raw_grade($userid, $rawgrade = false, $source = null, $feedback = false,
1965              $feedbackformat = FORMAT_MOODLE, $usermodified = null, $dategraded = null, $datesubmitted=null,
1966              $grade = null, array $feedbackfiles = [], $isbulkupdate = false) {
1967          global $USER;
1968  
1969          $result = true;
1970  
1971          // calculated grades can not be updated; course and category can not be updated  because they are aggregated
1972          if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1973              return false;
1974          }
1975  
1976          if (is_null($grade)) {
1977              //fetch from db
1978              $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1979          }
1980          $grade->grade_item =& $this; // prevent db fetching of this grade_item
1981  
1982          if (empty($usermodified)) {
1983              $grade->usermodified = $USER->id;
1984          } else {
1985              $grade->usermodified = $usermodified;
1986          }
1987  
1988          if ($grade->is_locked()) {
1989              // do not update locked grades at all
1990              return false;
1991          }
1992  
1993          $locktime = $grade->get_locktime();
1994          if ($locktime and $locktime < time()) {
1995              // do not update grades that should be already locked and force regrade
1996              $this->force_regrading();
1997              return false;
1998          }
1999  
2000          $oldgrade = new stdClass();
2001          $oldgrade->finalgrade     = $grade->finalgrade;
2002          $oldgrade->rawgrade       = $grade->rawgrade;
2003          $oldgrade->rawgrademin    = $grade->rawgrademin;
2004          $oldgrade->rawgrademax    = $grade->rawgrademax;
2005          $oldgrade->rawscaleid     = $grade->rawscaleid;
2006          $oldgrade->feedback       = $grade->feedback;
2007          $oldgrade->feedbackformat = $grade->feedbackformat;
2008  
2009          // use new min and max
2010          $grade->rawgrade    = $grade->rawgrade;
2011          $grade->rawgrademin = $this->grademin;
2012          $grade->rawgrademax = $this->grademax;
2013          $grade->rawscaleid  = $this->scaleid;
2014  
2015          // change raw grade?
2016          if ($rawgrade !== false) {
2017              $grade->rawgrade = $rawgrade;
2018          }
2019  
2020          // empty feedback means no feedback at all
2021          if ($feedback === '') {
2022              $feedback = null;
2023          }
2024  
2025          // do we have comment from teacher?
2026          if ($feedback !== false and !$grade->is_overridden()) {
2027              $grade->feedback       = $feedback;
2028              $grade->feedbackformat = $feedbackformat;
2029              $grade->feedbackfiles  = $feedbackfiles;
2030          }
2031  
2032          // update final grade if possible
2033          if (!$grade->is_locked() and !$grade->is_overridden()) {
2034              $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
2035          }
2036  
2037          // TODO: hack alert - create new fields for these in 2.0
2038          $oldgrade->timecreated  = $grade->timecreated;
2039          $oldgrade->timemodified = $grade->timemodified;
2040  
2041          $grade->timecreated = $datesubmitted;
2042  
2043          if ($grade->is_overridden()) {
2044              // keep original graded date - update_final_grade() sets this for overridden grades
2045  
2046          } else if (is_null($grade->rawgrade) and is_null($grade->feedback)) {
2047              // no grade and feedback means no grading yet
2048              $grade->timemodified = null;
2049  
2050          } else if (!empty($dategraded)) {
2051              // fine - module sends info when graded (yay!)
2052              $grade->timemodified = $dategraded;
2053  
2054          } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
2055                     or $grade->feedback !== $oldgrade->feedback) {
2056              // guess - if either grade or feedback changed set new graded date
2057              $grade->timemodified = time();
2058  
2059          } else {
2060              //keep original graded date
2061          }
2062          // end of hack alert
2063  
2064          $gradechanged = false;
2065          if (empty($grade->id)) {
2066              $result = (bool)$grade->insert($source, $isbulkupdate);
2067  
2068              // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
2069              if ($result && !is_null($grade->finalgrade)) {
2070                  \core\event\user_graded::create_from_grade($grade)->trigger();
2071              }
2072              $gradechanged = true;
2073          } else {
2074              // Existing grade_grades.
2075  
2076              if (grade_floats_different($grade->finalgrade,  $oldgrade->finalgrade)
2077                      or grade_floats_different($grade->rawgrade,    $oldgrade->rawgrade)
2078                      or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
2079                      or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
2080                      or $grade->rawscaleid != $oldgrade->rawscaleid) {
2081                  $gradechanged = true;
2082              }
2083  
2084              // The timecreated and timemodified checking is part of the hack above.
2085              if ($gradechanged === false and
2086                      $grade->feedback === $oldgrade->feedback and
2087                      $grade->feedbackformat == $oldgrade->feedbackformat and
2088                      $grade->timecreated == $oldgrade->timecreated and
2089                      $grade->timemodified == $oldgrade->timemodified) {
2090                  // No changes.
2091                  return $result;
2092              }
2093              $result = $grade->update($source, $isbulkupdate);
2094  
2095              // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
2096              if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
2097                  \core\event\user_graded::create_from_grade($grade)->trigger();
2098              }
2099          }
2100  
2101          if (!$result) {
2102              // Something went wrong - better force final grade recalculation.
2103              $this->force_regrading();
2104              return $result;
2105          }
2106  
2107          // If we are not updating grades we don't need to recalculate the whole course.
2108          if (!$gradechanged) {
2109              return $result;
2110          }
2111  
2112          if (!$this->needsupdate) {
2113              $course_item = grade_item::fetch_course_item($this->courseid);
2114              if (!$course_item->needsupdate) {
2115                  if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
2116                      $this->force_regrading();
2117                  }
2118              }
2119          }
2120  
2121          return $result;
2122      }
2123  
2124      /**
2125       * Calculates final grade values using the formula in the calculation property.
2126       * The parameters are taken from final grades of grade items in current course only.
2127       *
2128       * @param int $userid Supply a user ID to limit the calculations to the grades of a single user
2129       * @return bool false if error
2130       */
2131      public function compute($userid=null) {
2132          global $CFG, $DB;
2133  
2134          if (!$this->is_calculated()) {
2135              return false;
2136          }
2137  
2138          require_once($CFG->libdir.'/mathslib.php');
2139  
2140          if ($this->is_locked()) {
2141              return true; // no need to recalculate locked items
2142          }
2143  
2144          // Precreate grades - we need them to exist
2145          if ($userid) {
2146              $missing = array();
2147              if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id, 'userid'=>$userid))) {
2148                  $m = new stdClass();
2149                  $m->userid = $userid;
2150                  $missing[] = $m;
2151              }
2152          } else {
2153              // Find any users who have grades for some but not all grade items in this course
2154              $params = array('gicourseid' => $this->courseid, 'ggitemid' => $this->id);
2155              $sql = "SELECT gg.userid
2156                        FROM {grade_grades} gg
2157                             JOIN {grade_items} gi
2158                             ON (gi.id = gg.itemid AND gi.courseid = :gicourseid)
2159                       GROUP BY gg.userid
2160                       HAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0";
2161              $missing = $DB->get_records_sql($sql, $params);
2162          }
2163  
2164          if ($missing) {
2165              foreach ($missing as $m) {
2166                  $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false);
2167                  $grade->grade_item =& $this;
2168                  $grade->insert('system');
2169              }
2170          }
2171  
2172          // get used items
2173          $useditems = $this->depends_on();
2174  
2175          // prepare formula and init maths library
2176          $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
2177          if (strpos($formula, '[[') !== false) {
2178              // missing item
2179              return false;
2180          }
2181          $this->formula = new calc_formula($formula);
2182  
2183          // where to look for final grades?
2184          // this itemid is added so that we use only one query for source and final grades
2185          $gis = array_merge($useditems, array($this->id));
2186          list($usql, $params) = $DB->get_in_or_equal($gis);
2187  
2188          if ($userid) {
2189              $usersql = "AND g.userid=?";
2190              $params[] = $userid;
2191          } else {
2192              $usersql = "";
2193          }
2194  
2195          $grade_inst = new grade_grade();
2196          $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
2197  
2198          $params[] = $this->courseid;
2199          $sql = "SELECT $fields
2200                    FROM {grade_grades} g, {grade_items} gi
2201                   WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?
2202                   ORDER BY g.userid";
2203  
2204          $return = true;
2205  
2206          // group the grades by userid and use formula on the group
2207          $rs = $DB->get_recordset_sql($sql, $params);
2208          if ($rs->valid()) {
2209              $prevuser = 0;
2210              $grade_records   = array();
2211              $oldgrade    = null;
2212              foreach ($rs as $used) {
2213                  if ($used->userid != $prevuser) {
2214                      if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
2215                          $return = false;
2216                      }
2217                      $prevuser = $used->userid;
2218                      $grade_records   = array();
2219                      $oldgrade    = null;
2220                  }
2221                  if ($used->itemid == $this->id) {
2222                      $oldgrade = $used;
2223                  }
2224                  $grade_records['gi'.$used->itemid] = $used->finalgrade;
2225              }
2226              if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
2227                  $return = false;
2228              }
2229          }
2230          $rs->close();
2231  
2232          return $return;
2233      }
2234  
2235      /**
2236       * Internal function that does the final grade calculation
2237       *
2238       * @param int $userid The user ID
2239       * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade
2240       * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID
2241       * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database
2242       * @return bool False if an error occurred
2243       */
2244      public function use_formula($userid, $params, $useditems, $oldgrade) {
2245          if (empty($userid)) {
2246              return true;
2247          }
2248  
2249          // add missing final grade values
2250          // not graded (null) is counted as 0 - the spreadsheet way
2251          $allinputsnull = true;
2252          foreach($useditems as $gi) {
2253              if (!array_key_exists('gi'.$gi, $params) || is_null($params['gi'.$gi])) {
2254                  $params['gi'.$gi] = 0;
2255              } else {
2256                  $params['gi'.$gi] = (float)$params['gi'.$gi];
2257                  if ($gi != $this->id) {
2258                      $allinputsnull = false;
2259                  }
2260              }
2261          }
2262  
2263          // can not use own final grade during calculation
2264          unset($params['gi'.$this->id]);
2265  
2266          // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
2267          // wish to update the grades.
2268          $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);
2269  
2270          $rawminandmaxchanged = false;
2271          // insert final grade - will be needed later anyway
2272          if ($oldgrade) {
2273              // Only run through this code if the gradebook isn't frozen.
2274              if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2275                  // Do nothing.
2276              } else {
2277                  // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
2278                  // grade_item grade maximum and minimum respectively.
2279                  if ($oldgrade->rawgrademax != $this->grademax || $oldgrade->rawgrademin != $this->grademin) {
2280                      $rawminandmaxchanged = true;
2281                      $oldgrade->rawgrademax = $this->grademax;
2282                      $oldgrade->rawgrademin = $this->grademin;
2283                  }
2284              }
2285              $oldfinalgrade = $oldgrade->finalgrade;
2286              $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
2287              $grade->grade_item =& $this;
2288  
2289          } else {
2290              $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
2291              $grade->grade_item =& $this;
2292              $rawminandmaxchanged = false;
2293              if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2294                  // Do nothing.
2295              } else {
2296                  // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
2297                  // grade_item grade maximum and minimum respectively.
2298                  $rawminandmaxchanged = true;
2299                  $grade->rawgrademax = $this->grademax;
2300                  $grade->rawgrademin = $this->grademin;
2301              }
2302              $grade->insert('system');
2303              $oldfinalgrade = null;
2304          }
2305  
2306          // no need to recalculate locked or overridden grades
2307          if ($grade->is_locked() or $grade->is_overridden()) {
2308              return true;
2309          }
2310  
2311          if ($allinputsnull) {
2312              $grade->finalgrade = null;
2313              $result = true;
2314  
2315          } else {
2316  
2317              // do the calculation
2318              $this->formula->set_params($params);
2319              $result = $this->formula->evaluate();
2320  
2321              if ($result === false) {
2322                  $grade->finalgrade = null;
2323  
2324              } else {
2325                  // normalize
2326                  $grade->finalgrade = $this->bounded_grade($result);
2327              }
2328          }
2329  
2330          // Only run through this code if the gradebook isn't frozen.
2331          if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2332              // Update in db if changed.
2333              if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
2334                  $grade->timemodified = time();
2335                  $success = $grade->update('compute');
2336  
2337                  // If successful trigger a user_graded event.
2338                  if ($success) {
2339                      \core\event\user_graded::create_from_grade($grade)->trigger();
2340                  }
2341              }
2342          } else {
2343              // Update in db if changed.
2344              if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || $rawminandmaxchanged) {
2345                  $grade->timemodified = time();
2346                  $success = $grade->update('compute');
2347  
2348                  // If successful trigger a user_graded event.
2349                  if ($success) {
2350                      \core\event\user_graded::create_from_grade($grade)->trigger();
2351                  }
2352              }
2353          }
2354  
2355          if ($result !== false) {
2356              //lock grade if needed
2357          }
2358  
2359          if ($result === false) {
2360              return false;
2361          } else {
2362              return true;
2363          }
2364  
2365      }
2366  
2367      /**
2368       * Validate the formula.
2369       *
2370       * @param string $formulastr
2371       * @return bool true if calculation possible, false otherwise
2372       */
2373      public function validate_formula($formulastr) {
2374          global $CFG, $DB;
2375          require_once($CFG->libdir.'/mathslib.php');
2376  
2377          $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);
2378  
2379          if (empty($formulastr)) {
2380              return true;
2381          }
2382  
2383          if (strpos($formulastr, '=') !== 0) {
2384              return get_string('errorcalculationnoequal', 'grades');
2385          }
2386  
2387          // get used items
2388          if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
2389              $useditems = array_unique($matches[1]); // remove duplicates
2390          } else {
2391              $useditems = array();
2392          }
2393  
2394          // MDL-11902
2395          // unset the value if formula is trying to reference to itself
2396          // but array keys does not match itemid
2397          if (!empty($this->id)) {
2398              $useditems = array_diff($useditems, array($this->id));
2399              //unset($useditems[$this->id]);
2400          }
2401  
2402          // prepare formula and init maths library
2403          $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
2404          $formula = new calc_formula($formula);
2405  
2406  
2407          if (empty($useditems)) {
2408              $grade_items = array();
2409  
2410          } else {
2411              list($usql, $params) = $DB->get_in_or_equal($useditems);
2412              $params[] = $this->courseid;
2413              $sql = "SELECT gi.*
2414                        FROM {grade_items} gi
2415                       WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
2416  
2417              if (!$grade_items = $DB->get_records_sql($sql, $params)) {
2418                  $grade_items = array();
2419              }
2420          }
2421  
2422          $params = array();
2423          foreach ($useditems as $itemid) {
2424              // make sure all grade items exist in this course
2425              if (!array_key_exists($itemid, $grade_items)) {
2426                  return false;
2427              }
2428              // use max grade when testing formula, this should be ok in 99.9%
2429              // division by 0 is one of possible problems
2430              $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
2431          }
2432  
2433          // do the calculation
2434          $formula->set_params($params);
2435          $result = $formula->evaluate();
2436  
2437          // false as result indicates some problem
2438          if ($result === false) {
2439              // TODO: add more error hints
2440              return get_string('errorcalculationunknown', 'grades');
2441          } else {
2442              return true;
2443          }
2444      }
2445  
2446      /**
2447       * Returns the value of the display type
2448       *
2449       * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2450       *
2451       * @return int Display type
2452       */
2453      public function get_displaytype() {
2454          global $CFG;
2455  
2456          if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
2457              return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);
2458  
2459          } else {
2460              return $this->display;
2461          }
2462      }
2463  
2464      /**
2465       * Returns the value of the decimals field
2466       *
2467       * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2468       *
2469       * @return int Decimals (0 - 5)
2470       */
2471      public function get_decimals() {
2472          global $CFG;
2473  
2474          if (is_null($this->decimals)) {
2475              return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
2476  
2477          } else {
2478              return $this->decimals;
2479          }
2480      }
2481  
2482      /**
2483       * Returns a string representing the range of grademin - grademax for this grade item.
2484       *
2485       * @param int $rangesdisplaytype
2486       * @param int $rangesdecimalpoints
2487       * @return string
2488       */
2489      function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
2490  
2491          global $USER;
2492  
2493          // Determine which display type to use for this average
2494          if (isset($USER->editing) && $USER->editing) {
2495              $displaytype = GRADE_DISPLAY_TYPE_REAL;
2496  
2497          } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs
2498              $displaytype = $this->get_displaytype();
2499  
2500          } else {
2501              $displaytype = $rangesdisplaytype;
2502          }
2503  
2504          // Override grade_item setting if a display preference (not default) was set for the averages
2505          if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
2506              $decimalpoints = $this->get_decimals();
2507  
2508          } else {
2509              $decimalpoints = $rangesdecimalpoints;
2510          }
2511  
2512          if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
2513              $grademin = "0 %";
2514              $grademax = "100 %";
2515  
2516          } else {
2517              $grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);
2518              $grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);
2519          }
2520  
2521          return $grademin.'&ndash;'. $grademax;
2522      }
2523  
2524      /**
2525       * Queries parent categories recursively to find the aggregationcoef type that applies to this grade item.
2526       *
2527       * @return string|false Returns the coefficient string of false is no coefficient is being used
2528       */
2529      public function get_coefstring() {
2530          $parent_category = $this->load_parent_category();
2531          if ($this->is_category_item()) {
2532              $parent_category = $parent_category->load_parent_category();
2533          }
2534  
2535          if ($parent_category->is_aggregationcoef_used()) {
2536              return $parent_category->get_coefstring();
2537          } else {
2538              return false;
2539          }
2540      }
2541  
2542      /**
2543       * Returns whether the grade item can control the visibility of the grades
2544       *
2545       * @return bool
2546       */
2547      public function can_control_visibility() {
2548          if (core_component::get_plugin_directory($this->itemtype, $this->itemmodule)) {
2549              return !plugin_supports($this->itemtype, $this->itemmodule, FEATURE_CONTROLS_GRADE_VISIBILITY, false);
2550          }
2551          return parent::can_control_visibility();
2552      }
2553  
2554      /**
2555       * Used to notify the completion system (if necessary) that a user's grade
2556       * has changed, and clear up a possible score cache.
2557       *
2558       * @param bool $deleted True if grade was actually deleted
2559       */
2560      protected function notify_changed($deleted) {
2561          global $CFG;
2562  
2563          // Condition code may cache the grades for conditional availability of
2564          // modules or sections. (This code should use a hook for communication
2565          // with plugin, but hooks are not implemented at time of writing.)
2566          if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
2567              \availability_grade\callbacks::grade_item_changed($this->courseid);
2568          }
2569      }
2570  
2571      /**
2572       * Helper function to get the accurate context for this grade column.
2573       *
2574       * @return context
2575       */
2576      public function get_context() {
2577          if ($this->itemtype == 'mod') {
2578              $modinfo = get_fast_modinfo($this->courseid);
2579              // Sometimes the course module cache is out of date and needs to be rebuilt.
2580              if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {
2581                  rebuild_course_cache($this->courseid, true);
2582                  $modinfo = get_fast_modinfo($this->courseid);
2583              }
2584              // Even with a rebuilt cache the module does not exist. This means the
2585              // database is in an invalid state - we will log an error and return
2586              // the course context but the calling code should be updated.
2587              if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {
2588                  mtrace(get_string('moduleinstancedoesnotexist', 'error'));
2589                  $context = \context_course::instance($this->courseid);
2590              } else {
2591                  $cm = $modinfo->instances[$this->itemmodule][$this->iteminstance];
2592                  $context = \context_module::instance($cm->id);
2593              }
2594          } else {
2595              $context = \context_course::instance($this->courseid);
2596          }
2597          return $context;
2598      }
2599  }