Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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