Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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