Search moodle.org's
Developer Documentation

See Release Notes

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

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

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