[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/grade/ -> grade_item.php (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1