Search moodle.org's
Developer Documentation

See Release Notes

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

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Library of functions for gradebook - both public and internal
  19   *
  20   * @package   core_grades
  21   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  global $CFG;
  28  
  29  /** Include essential files */
  30  require_once($CFG->libdir . '/grade/constants.php');
  31  
  32  require_once($CFG->libdir . '/grade/grade_category.php');
  33  require_once($CFG->libdir . '/grade/grade_item.php');
  34  require_once($CFG->libdir . '/grade/grade_grade.php');
  35  require_once($CFG->libdir . '/grade/grade_scale.php');
  36  require_once($CFG->libdir . '/grade/grade_outcome.php');
  37  
  38  /////////////////////////////////////////////////////////////////////
  39  ///// Start of public API for communication with modules/blocks /////
  40  /////////////////////////////////////////////////////////////////////
  41  
  42  /**
  43   * Submit new or update grade; update/create grade_item definition. Grade must have userid specified,
  44   * rawgrade and feedback with format are optional. rawgrade NULL means 'Not graded'.
  45   * Missing property or key means does not change the existing value.
  46   *
  47   * Only following grade item properties can be changed 'itemname', 'idnumber', 'gradetype', 'grademax',
  48   * 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted' and 'hidden'. 'reset' means delete all current grades including locked ones.
  49   *
  50   * Manual, course or category items can not be updated by this function.
  51   *
  52   * @category grade
  53   * @param string $source Source of the grade such as 'mod/assignment'
  54   * @param int    $courseid ID of course
  55   * @param string $itemtype Type of grade item. For example, mod or block
  56   * @param string $itemmodule More specific then $itemtype. For example, assignment or forum. May be NULL for some item types
  57   * @param int    $iteminstance Instance ID of graded item
  58   * @param int    $itemnumber Most probably 0. Modules can use other numbers when having more than one grade for each user
  59   * @param mixed  $grades Grade (object, array) or several grades (arrays of arrays or objects), NULL if updating grade_item definition only
  60   * @param mixed  $itemdetails Object or array describing the grading item, NULL if no change
  61   * @param bool   $isbulkupdate If bulk grade update is happening.
  62   * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED
  63   */
  64  function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $grades = null,
  65          $itemdetails = null, $isbulkupdate = false) {
  66      global $USER, $CFG, $DB;
  67  
  68      // only following grade_item properties can be changed in this function
  69      $allowed = array('itemname', 'idnumber', 'gradetype', 'grademax', 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted', 'hidden');
  70      // list of 10,5 numeric fields
  71      $floats  = array('grademin', 'grademax', 'multfactor', 'plusfactor');
  72  
  73      // grade item identification
  74      $params = compact('courseid', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber');
  75  
  76      if (is_null($courseid) or is_null($itemtype)) {
  77          debugging('Missing courseid or itemtype');
  78          return GRADE_UPDATE_FAILED;
  79      }
  80  
  81      if (!$gradeitems = grade_item::fetch_all($params)) {
  82          // create a new one
  83          $gradeitem = false;
  84      } else if (count($gradeitems) == 1) {
  85          $gradeitem = reset($gradeitems);
  86          unset($gradeitems); // Release memory.
  87      } else {
  88          debugging('Found more than one grade item');
  89          return GRADE_UPDATE_MULTIPLE;
  90      }
  91  
  92      if (!empty($itemdetails['deleted'])) {
  93          if ($gradeitem) {
  94              if ($gradeitem->delete($source)) {
  95                  return GRADE_UPDATE_OK;
  96              } else {
  97                  return GRADE_UPDATE_FAILED;
  98              }
  99          }
 100          return GRADE_UPDATE_OK;
 101      }
 102  
 103  /// Create or update the grade_item if needed
 104  
 105      if (!$gradeitem) {
 106          if ($itemdetails) {
 107              $itemdetails = (array)$itemdetails;
 108  
 109              // grademin and grademax ignored when scale specified
 110              if (array_key_exists('scaleid', $itemdetails)) {
 111                  if ($itemdetails['scaleid']) {
 112                      unset($itemdetails['grademin']);
 113                      unset($itemdetails['grademax']);
 114                  }
 115              }
 116  
 117              foreach ($itemdetails as $k=>$v) {
 118                  if (!in_array($k, $allowed)) {
 119                      // ignore it
 120                      continue;
 121                  }
 122                  if ($k == 'gradetype' and $v == GRADE_TYPE_NONE) {
 123                      // no grade item needed!
 124                      return GRADE_UPDATE_OK;
 125                  }
 126                  $params[$k] = $v;
 127              }
 128          }
 129          $gradeitem = new grade_item($params);
 130          $gradeitem->insert(null, $isbulkupdate);
 131  
 132      } else {
 133          if ($gradeitem->is_locked()) {
 134              // no notice() here, test returned value instead!
 135              return GRADE_UPDATE_ITEM_LOCKED;
 136          }
 137  
 138          if ($itemdetails) {
 139              $itemdetails = (array)$itemdetails;
 140              $update = false;
 141              foreach ($itemdetails as $k=>$v) {
 142                  if (!in_array($k, $allowed)) {
 143                      // ignore it
 144                      continue;
 145                  }
 146                  if (in_array($k, $floats)) {
 147                      if (grade_floats_different($gradeitem->{$k}, $v)) {
 148                          $gradeitem->{$k} = $v;
 149                          $update = true;
 150                      }
 151  
 152                  } else {
 153                      if ($gradeitem->{$k} != $v) {
 154                          $gradeitem->{$k} = $v;
 155                          $update = true;
 156                      }
 157                  }
 158              }
 159              if ($update) {
 160                  $gradeitem->update(null, $isbulkupdate);
 161              }
 162          }
 163      }
 164  
 165  /// reset grades if requested
 166      if (!empty($itemdetails['reset'])) {
 167          $gradeitem->delete_all_grades('reset');
 168          return GRADE_UPDATE_OK;
 169      }
 170  
 171  /// Some extra checks
 172      // do we use grading?
 173      if ($gradeitem->gradetype == GRADE_TYPE_NONE) {
 174          return GRADE_UPDATE_OK;
 175      }
 176  
 177      // no grade submitted
 178      if (empty($grades)) {
 179          return GRADE_UPDATE_OK;
 180      }
 181  
 182  /// Finally start processing of grades
 183      if (is_object($grades)) {
 184          $grades = array($grades->userid=>$grades);
 185      } else {
 186          if (array_key_exists('userid', $grades)) {
 187              $grades = array($grades['userid']=>$grades);
 188          }
 189      }
 190  
 191  /// normalize and verify grade array
 192      foreach($grades as $k=>$g) {
 193          if (!is_array($g)) {
 194              $g = (array)$g;
 195              $grades[$k] = $g;
 196          }
 197  
 198          if (empty($g['userid']) or $k != $g['userid']) {
 199              debugging('Incorrect grade array index, must be user id! Grade ignored.');
 200              unset($grades[$k]);
 201          }
 202      }
 203  
 204      if (empty($grades)) {
 205          return GRADE_UPDATE_FAILED;
 206      }
 207  
 208      $count = count($grades);
 209      if ($count > 0 and $count < 200) {
 210          list($uids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED, $start='uid');
 211          $params['gid'] = $gradeitem->id;
 212          $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid AND userid $uids";
 213  
 214      } else {
 215          $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid";
 216          $params = array('gid' => $gradeitem->id);
 217      }
 218  
 219      $rs = $DB->get_recordset_sql($sql, $params);
 220  
 221      $failed = false;
 222  
 223      while (count($grades) > 0) {
 224          $gradegrade = null;
 225          $grade       = null;
 226  
 227          foreach ($rs as $gd) {
 228  
 229              $userid = $gd->userid;
 230              if (!isset($grades[$userid])) {
 231                  // this grade not requested, continue
 232                  continue;
 233              }
 234              // existing grade requested
 235              $grade       = $grades[$userid];
 236              $gradegrade = new grade_grade($gd, false);
 237              unset($grades[$userid]);
 238              break;
 239          }
 240  
 241          if (is_null($gradegrade)) {
 242              if (count($grades) == 0) {
 243                  // No more grades to process.
 244                  break;
 245              }
 246  
 247              $grade       = reset($grades);
 248              $userid      = $grade['userid'];
 249              $gradegrade = new grade_grade(array('itemid' => $gradeitem->id, 'userid' => $userid), false);
 250              $gradegrade->load_optional_fields(); // add feedback and info too
 251              unset($grades[$userid]);
 252          }
 253  
 254          $rawgrade       = false;
 255          $feedback       = false;
 256          $feedbackformat = FORMAT_MOODLE;
 257          $feedbackfiles = [];
 258          $usermodified   = $USER->id;
 259          $datesubmitted  = null;
 260          $dategraded     = null;
 261  
 262          if (array_key_exists('rawgrade', $grade)) {
 263              $rawgrade = $grade['rawgrade'];
 264          }
 265  
 266          if (array_key_exists('feedback', $grade)) {
 267              $feedback = $grade['feedback'];
 268          }
 269  
 270          if (array_key_exists('feedbackformat', $grade)) {
 271              $feedbackformat = $grade['feedbackformat'];
 272          }
 273  
 274          if (array_key_exists('feedbackfiles', $grade)) {
 275              $feedbackfiles = $grade['feedbackfiles'];
 276          }
 277  
 278          if (array_key_exists('usermodified', $grade)) {
 279              $usermodified = $grade['usermodified'];
 280          }
 281  
 282          if (array_key_exists('datesubmitted', $grade)) {
 283              $datesubmitted = $grade['datesubmitted'];
 284          }
 285  
 286          if (array_key_exists('dategraded', $grade)) {
 287              $dategraded = $grade['dategraded'];
 288          }
 289  
 290          // update or insert the grade
 291          if (!$gradeitem->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified,
 292                  $dategraded, $datesubmitted, $gradegrade, $feedbackfiles, $isbulkupdate)) {
 293              $failed = true;
 294          }
 295      }
 296  
 297      if ($rs) {
 298          $rs->close();
 299      }
 300  
 301      if (!$failed) {
 302          return GRADE_UPDATE_OK;
 303      } else {
 304          return GRADE_UPDATE_FAILED;
 305      }
 306  }
 307  
 308  /**
 309   * Updates a user's outcomes. Manual outcomes can not be updated.
 310   *
 311   * @category grade
 312   * @param string $source Source of the grade such as 'mod/assignment'
 313   * @param int    $courseid ID of course
 314   * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
 315   * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
 316   * @param int    $iteminstance Instance ID of graded item. For example the forum ID.
 317   * @param int    $userid ID of the graded user
 318   * @param array  $data Array consisting of grade item itemnumber ({@link grade_update()}) => outcomegrade
 319   * @return bool returns true if grade items were found and updated successfully
 320   */
 321  function grade_update_outcomes($source, $courseid, $itemtype, $itemmodule, $iteminstance, $userid, $data) {
 322      if ($items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
 323          $result = true;
 324          foreach ($items as $item) {
 325              if (!array_key_exists($item->itemnumber, $data)) {
 326                  continue;
 327              }
 328              $grade = $data[$item->itemnumber] < 1 ? null : $data[$item->itemnumber];
 329              $result = ($item->update_final_grade($userid, $grade, $source) && $result);
 330          }
 331          return $result;
 332      }
 333      return false; //grade items not found
 334  }
 335  
 336  /**
 337   * Return true if the course needs regrading.
 338   *
 339   * @param int $courseid The course ID
 340   * @return bool true if course grades need updating.
 341   */
 342  function grade_needs_regrade_final_grades($courseid) {
 343      $course_item = grade_item::fetch_course_item($courseid);
 344      return $course_item->needsupdate;
 345  }
 346  
 347  /**
 348   * Return true if the regrade process is likely to be time consuming and
 349   * will therefore require the progress bar.
 350   *
 351   * @param int $courseid The course ID
 352   * @return bool Whether the regrade process is likely to be time consuming
 353   */
 354  function grade_needs_regrade_progress_bar($courseid) {
 355      global $DB;
 356      $grade_items = grade_item::fetch_all(array('courseid' => $courseid));
 357      if (!$grade_items) {
 358          // If there are no grade items then we definitely don't need a progress bar!
 359          return false;
 360      }
 361  
 362      list($sql, $params) = $DB->get_in_or_equal(array_keys($grade_items), SQL_PARAMS_NAMED, 'gi');
 363      $gradecount = $DB->count_records_select('grade_grades', 'itemid ' . $sql, $params);
 364  
 365      // This figure may seem arbitrary, but after analysis it seems that 100 grade_grades can be calculated in ~= 0.5 seconds.
 366      // Any longer than this and we want to show the progress bar.
 367      return $gradecount > 100;
 368  }
 369  
 370  /**
 371   * Check whether regarding of final grades is required and, if so, perform the regrade.
 372   *
 373   * If the regrade is expected to be time consuming (see grade_needs_regrade_progress_bar), then this
 374   * function will output the progress bar, and redirect to the current PAGE->url after regrading
 375   * completes. Otherwise the regrading will happen immediately and the page will be loaded as per
 376   * normal.
 377   *
 378   * A callback may be specified, which is called if regrading has taken place.
 379   * The callback may optionally return a URL which will be redirected to when the progress bar is present.
 380   *
 381   * @param stdClass $course The course to regrade
 382   * @param callable $callback A function to call if regrading took place
 383   * @return moodle_url The URL to redirect to if redirecting
 384   */
 385  function grade_regrade_final_grades_if_required($course, callable $callback = null) {
 386      global $PAGE, $OUTPUT;
 387  
 388      if (!grade_needs_regrade_final_grades($course->id)) {
 389          return false;
 390      }
 391  
 392      if (grade_needs_regrade_progress_bar($course->id)) {
 393          if ($PAGE->state !== moodle_page::STATE_IN_BODY) {
 394              $PAGE->set_heading($course->fullname);
 395              echo $OUTPUT->header();
 396          }
 397          echo $OUTPUT->heading(get_string('recalculatinggrades', 'grades'));
 398          $progress = new \core\progress\display(true);
 399          $status = grade_regrade_final_grades($course->id, null, null, $progress);
 400  
 401          // Show regrade errors and set the course to no longer needing regrade (stop endless loop).
 402          if (is_array($status)) {
 403              foreach ($status as $error) {
 404                  $errortext = new \core\output\notification($error, \core\output\notification::NOTIFY_ERROR);
 405                  echo $OUTPUT->render($errortext);
 406              }
 407              $courseitem = grade_item::fetch_course_item($course->id);
 408              $courseitem->regrading_finished();
 409          }
 410  
 411          if ($callback) {
 412              //
 413              $url = call_user_func($callback);
 414          }
 415  
 416          if (empty($url)) {
 417              $url = $PAGE->url;
 418          }
 419  
 420          echo $OUTPUT->continue_button($url);
 421          echo $OUTPUT->footer();
 422          die();
 423      } else {
 424          $result = grade_regrade_final_grades($course->id);
 425          if ($callback) {
 426              call_user_func($callback);
 427          }
 428          return $result;
 429      }
 430  }
 431  
 432  /**
 433   * Returns grading information for given activity, optionally with user grades
 434   * Manual, course or category items can not be queried.
 435   *
 436   * @category grade
 437   * @param int    $courseid ID of course
 438   * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
 439   * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
 440   * @param int    $iteminstance ID of the item module
 441   * @param mixed  $userid_or_ids Either a single user ID, an array of user IDs or null. If user ID or IDs are not supplied returns information about grade_item
 442   * @return stdClass Object with keys {items, outcomes, errors}, where 'items' is an array of grade
 443   *               information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
 444   */
 445  function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) {
 446      global $CFG;
 447  
 448      $return = new stdClass();
 449      $return->items    = array();
 450      $return->outcomes = array();
 451      $return->errors = [];
 452  
 453      $courseitem = grade_item::fetch_course_item($courseid);
 454      $needsupdate = array();
 455      if ($courseitem->needsupdate) {
 456          $result = grade_regrade_final_grades($courseid);
 457          if ($result !== true) {
 458              $needsupdate = array_keys($result);
 459              // Return regrade errors if the user has capability.
 460              $context = context_course::instance($courseid);
 461              if (has_capability('moodle/grade:edit', $context)) {
 462                  $return->errors = $result;
 463              }
 464              $courseitem->regrading_finished();
 465          }
 466      }
 467  
 468      if ($grade_items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
 469          foreach ($grade_items as $grade_item) {
 470              $decimalpoints = null;
 471  
 472              if (empty($grade_item->outcomeid)) {
 473                  // prepare information about grade item
 474                  $item = new stdClass();
 475                  $item->id = $grade_item->id;
 476                  $item->itemnumber = $grade_item->itemnumber;
 477                  $item->itemtype  = $grade_item->itemtype;
 478                  $item->itemmodule = $grade_item->itemmodule;
 479                  $item->iteminstance = $grade_item->iteminstance;
 480                  $item->scaleid    = $grade_item->scaleid;
 481                  $item->name       = $grade_item->get_name();
 482                  $item->grademin   = $grade_item->grademin;
 483                  $item->grademax   = $grade_item->grademax;
 484                  $item->gradepass  = $grade_item->gradepass;
 485                  $item->locked     = $grade_item->is_locked();
 486                  $item->hidden     = $grade_item->is_hidden();
 487                  $item->grades     = array();
 488  
 489                  switch ($grade_item->gradetype) {
 490                      case GRADE_TYPE_NONE:
 491                          break;
 492  
 493                      case GRADE_TYPE_VALUE:
 494                          $item->scaleid = 0;
 495                          break;
 496  
 497                      case GRADE_TYPE_TEXT:
 498                          $item->scaleid   = 0;
 499                          $item->grademin   = 0;
 500                          $item->grademax   = 0;
 501                          $item->gradepass  = 0;
 502                          break;
 503                  }
 504  
 505                  if (empty($userid_or_ids)) {
 506                      $userids = array();
 507  
 508                  } else if (is_array($userid_or_ids)) {
 509                      $userids = $userid_or_ids;
 510  
 511                  } else {
 512                      $userids = array($userid_or_ids);
 513                  }
 514  
 515                  if ($userids) {
 516                      $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
 517                      foreach ($userids as $userid) {
 518                          $grade_grades[$userid]->grade_item =& $grade_item;
 519  
 520                          $grade = new stdClass();
 521                          $grade->grade          = $grade_grades[$userid]->finalgrade;
 522                          $grade->locked         = $grade_grades[$userid]->is_locked();
 523                          $grade->hidden         = $grade_grades[$userid]->is_hidden();
 524                          $grade->overridden     = $grade_grades[$userid]->overridden;
 525                          $grade->feedback       = $grade_grades[$userid]->feedback;
 526                          $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
 527                          $grade->usermodified   = $grade_grades[$userid]->usermodified;
 528                          $grade->datesubmitted  = $grade_grades[$userid]->get_datesubmitted();
 529                          $grade->dategraded     = $grade_grades[$userid]->get_dategraded();
 530  
 531                          // create text representation of grade
 532                          if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) {
 533                              $grade->grade          = null;
 534                              $grade->str_grade      = '-';
 535                              $grade->str_long_grade = $grade->str_grade;
 536  
 537                          } else if (in_array($grade_item->id, $needsupdate)) {
 538                              $grade->grade          = false;
 539                              $grade->str_grade      = get_string('error');
 540                              $grade->str_long_grade = $grade->str_grade;
 541  
 542                          } else if (is_null($grade->grade)) {
 543                              $grade->str_grade      = '-';
 544                              $grade->str_long_grade = $grade->str_grade;
 545  
 546                          } else {
 547                              $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
 548                              if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
 549                                  $grade->str_long_grade = $grade->str_grade;
 550                              } else {
 551                                  $a = new stdClass();
 552                                  $a->grade = $grade->str_grade;
 553                                  $a->max   = grade_format_gradevalue($grade_item->grademax, $grade_item);
 554                                  $grade->str_long_grade = get_string('gradelong', 'grades', $a);
 555                              }
 556                          }
 557  
 558                          // create html representation of feedback
 559                          if (is_null($grade->feedback)) {
 560                              $grade->str_feedback = '';
 561                          } else {
 562                              $feedback = file_rewrite_pluginfile_urls(
 563                                  $grade->feedback,
 564                                  'pluginfile.php',
 565                                  $grade_grades[$userid]->get_context()->id,
 566                                  GRADE_FILE_COMPONENT,
 567                                  GRADE_FEEDBACK_FILEAREA,
 568                                  $grade_grades[$userid]->id
 569                              );
 570  
 571                              $grade->str_feedback = format_text($feedback, $grade->feedbackformat,
 572                                  ['context' => $grade_grades[$userid]->get_context()]);
 573                          }
 574  
 575                          $item->grades[$userid] = $grade;
 576                      }
 577                  }
 578                  $return->items[$grade_item->itemnumber] = $item;
 579  
 580              } else {
 581                  if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
 582                      debugging('Incorect outcomeid found');
 583                      continue;
 584                  }
 585  
 586                  // outcome info
 587                  $outcome = new stdClass();
 588                  $outcome->id = $grade_item->id;
 589                  $outcome->itemnumber = $grade_item->itemnumber;
 590                  $outcome->itemtype   = $grade_item->itemtype;
 591                  $outcome->itemmodule = $grade_item->itemmodule;
 592                  $outcome->iteminstance = $grade_item->iteminstance;
 593                  $outcome->scaleid    = $grade_outcome->scaleid;
 594                  $outcome->name       = $grade_outcome->get_name();
 595                  $outcome->locked     = $grade_item->is_locked();
 596                  $outcome->hidden     = $grade_item->is_hidden();
 597  
 598                  if (empty($userid_or_ids)) {
 599                      $userids = array();
 600                  } else if (is_array($userid_or_ids)) {
 601                      $userids = $userid_or_ids;
 602                  } else {
 603                      $userids = array($userid_or_ids);
 604                  }
 605  
 606                  if ($userids) {
 607                      $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
 608                      foreach ($userids as $userid) {
 609                          $grade_grades[$userid]->grade_item =& $grade_item;
 610  
 611                          $grade = new stdClass();
 612                          $grade->grade          = $grade_grades[$userid]->finalgrade;
 613                          $grade->locked         = $grade_grades[$userid]->is_locked();
 614                          $grade->hidden         = $grade_grades[$userid]->is_hidden();
 615                          $grade->feedback       = $grade_grades[$userid]->feedback;
 616                          $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
 617                          $grade->usermodified   = $grade_grades[$userid]->usermodified;
 618                          $grade->datesubmitted  = $grade_grades[$userid]->get_datesubmitted();
 619                          $grade->dategraded     = $grade_grades[$userid]->get_dategraded();
 620  
 621                          // create text representation of grade
 622                          if (in_array($grade_item->id, $needsupdate)) {
 623                              $grade->grade     = false;
 624                              $grade->str_grade = get_string('error');
 625  
 626                          } else if (is_null($grade->grade)) {
 627                              $grade->grade = 0;
 628                              $grade->str_grade = get_string('nooutcome', 'grades');
 629  
 630                          } else {
 631                              $grade->grade = (int)$grade->grade;
 632                              $scale = $grade_item->load_scale();
 633                              $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
 634                          }
 635  
 636                          // create html representation of feedback
 637                          if (is_null($grade->feedback)) {
 638                              $grade->str_feedback = '';
 639                          } else {
 640                              $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
 641                          }
 642  
 643                          $outcome->grades[$userid] = $grade;
 644                      }
 645                  }
 646  
 647                  if (isset($return->outcomes[$grade_item->itemnumber])) {
 648                      // itemnumber duplicates - lets fix them!
 649                      $newnumber = $grade_item->itemnumber + 1;
 650                      while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
 651                          $newnumber++;
 652                      }
 653                      $outcome->itemnumber    = $newnumber;
 654                      $grade_item->itemnumber = $newnumber;
 655                      $grade_item->update('system');
 656                  }
 657  
 658                  $return->outcomes[$grade_item->itemnumber] = $outcome;
 659  
 660              }
 661          }
 662      }
 663  
 664      // sort results using itemnumbers
 665      ksort($return->items, SORT_NUMERIC);
 666      ksort($return->outcomes, SORT_NUMERIC);
 667  
 668      return $return;
 669  }
 670  
 671  ///////////////////////////////////////////////////////////////////
 672  ///// End of public API for communication with modules/blocks /////
 673  ///////////////////////////////////////////////////////////////////
 674  
 675  
 676  
 677  ///////////////////////////////////////////////////////////////////
 678  ///// Internal API: used by gradebook plugins and Moodle core /////
 679  ///////////////////////////////////////////////////////////////////
 680  
 681  /**
 682   * Returns a  course gradebook setting
 683   *
 684   * @param int $courseid
 685   * @param string $name of setting, maybe null if reset only
 686   * @param string $default value to return if setting is not found
 687   * @param bool $resetcache force reset of internal static cache
 688   * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
 689   */
 690  function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
 691      global $DB;
 692  
 693      $cache = cache::make('core', 'gradesetting');
 694      $gradesetting = $cache->get($courseid) ?: array();
 695  
 696      if ($resetcache or empty($gradesetting)) {
 697          $gradesetting = array();
 698          $cache->set($courseid, $gradesetting);
 699  
 700      } else if (is_null($name)) {
 701          return null;
 702  
 703      } else if (array_key_exists($name, $gradesetting)) {
 704          return $gradesetting[$name];
 705      }
 706  
 707      if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
 708          $result = null;
 709      } else {
 710          $result = $data->value;
 711      }
 712  
 713      if (is_null($result)) {
 714          $result = $default;
 715      }
 716  
 717      $gradesetting[$name] = $result;
 718      $cache->set($courseid, $gradesetting);
 719      return $result;
 720  }
 721  
 722  /**
 723   * Returns all course gradebook settings as object properties
 724   *
 725   * @param int $courseid
 726   * @return object
 727   */
 728  function grade_get_settings($courseid) {
 729      global $DB;
 730  
 731       $settings = new stdClass();
 732       $settings->id = $courseid;
 733  
 734      if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
 735          foreach ($records as $record) {
 736              $settings->{$record->name} = $record->value;
 737          }
 738      }
 739  
 740      return $settings;
 741  }
 742  
 743  /**
 744   * Add, update or delete a course gradebook setting
 745   *
 746   * @param int $courseid The course ID
 747   * @param string $name Name of the setting
 748   * @param string $value Value of the setting. NULL means delete the setting.
 749   */
 750  function grade_set_setting($courseid, $name, $value) {
 751      global $DB;
 752  
 753      if (is_null($value)) {
 754          $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
 755  
 756      } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
 757          $data = new stdClass();
 758          $data->courseid = $courseid;
 759          $data->name     = $name;
 760          $data->value    = $value;
 761          $DB->insert_record('grade_settings', $data);
 762  
 763      } else {
 764          $data = new stdClass();
 765          $data->id       = $existing->id;
 766          $data->value    = $value;
 767          $DB->update_record('grade_settings', $data);
 768      }
 769  
 770      grade_get_setting($courseid, null, null, true); // reset the cache
 771  }
 772  
 773  /**
 774   * Returns string representation of grade value
 775   *
 776   * @param float|null $value The grade value
 777   * @param object $grade_item Grade item object passed by reference to prevent scale reloading
 778   * @param bool $localized use localised decimal separator
 779   * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
 780   * @param int $decimals The number of decimal places when displaying float values
 781   * @return string
 782   */
 783  function grade_format_gradevalue(?float $value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
 784      if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
 785          return '';
 786      }
 787  
 788      // no grade yet?
 789      if (is_null($value)) {
 790          return '-';
 791      }
 792  
 793      if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
 794          //unknown type??
 795          return '';
 796      }
 797  
 798      if (is_null($displaytype)) {
 799          $displaytype = $grade_item->get_displaytype();
 800      }
 801  
 802      if (is_null($decimals)) {
 803          $decimals = $grade_item->get_decimals();
 804      }
 805  
 806      switch ($displaytype) {
 807          case GRADE_DISPLAY_TYPE_REAL:
 808              return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
 809  
 810          case GRADE_DISPLAY_TYPE_PERCENTAGE:
 811              return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
 812  
 813          case GRADE_DISPLAY_TYPE_LETTER:
 814              return grade_format_gradevalue_letter($value, $grade_item);
 815  
 816          case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE:
 817              return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
 818                      grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
 819  
 820          case GRADE_DISPLAY_TYPE_REAL_LETTER:
 821              return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
 822                      grade_format_gradevalue_letter($value, $grade_item) . ')';
 823  
 824          case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL:
 825              return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
 826                      grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
 827  
 828          case GRADE_DISPLAY_TYPE_LETTER_REAL:
 829              return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
 830                      grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
 831  
 832          case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE:
 833              return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
 834                      grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
 835  
 836          case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER:
 837              return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
 838                      grade_format_gradevalue_letter($value, $grade_item) . ')';
 839          default:
 840              return '';
 841      }
 842  }
 843  
 844  /**
 845   * Returns a float representation of a grade value
 846   *
 847   * @param float|null $value The grade value
 848   * @param object $grade_item Grade item object
 849   * @param int $decimals The number of decimal places
 850   * @param bool $localized use localised decimal separator
 851   * @return string
 852   */
 853  function grade_format_gradevalue_real(?float $value, $grade_item, $decimals, $localized) {
 854      if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
 855          if (!$scale = $grade_item->load_scale()) {
 856              return get_string('error');
 857          }
 858  
 859          $value = $grade_item->bounded_grade($value);
 860          return format_string($scale->scale_items[$value-1]);
 861  
 862      } else {
 863          return format_float($value, $decimals, $localized);
 864      }
 865  }
 866  
 867  /**
 868   * Returns a percentage representation of a grade value
 869   *
 870   * @param float|null $value The grade value
 871   * @param object $grade_item Grade item object
 872   * @param int $decimals The number of decimal places
 873   * @param bool $localized use localised decimal separator
 874   * @return string
 875   */
 876  function grade_format_gradevalue_percentage(?float $value, $grade_item, $decimals, $localized) {
 877      $min = $grade_item->grademin;
 878      $max = $grade_item->grademax;
 879      if ($min == $max) {
 880          return '';
 881      }
 882      $value = $grade_item->bounded_grade($value);
 883      $percentage = (($value-$min)*100)/($max-$min);
 884      return format_float($percentage, $decimals, $localized).' %';
 885  }
 886  
 887  /**
 888   * Returns a letter grade representation of a grade value
 889   * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
 890   *
 891   * @param float|null $value The grade value
 892   * @param object $grade_item Grade item object
 893   * @return string
 894   */
 895  function grade_format_gradevalue_letter(?float $value, $grade_item) {
 896      global $CFG;
 897      $context = context_course::instance($grade_item->courseid, IGNORE_MISSING);
 898      if (!$letters = grade_get_letters($context)) {
 899          return ''; // no letters??
 900      }
 901  
 902      if (is_null($value)) {
 903          return '-';
 904      }
 905  
 906      $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
 907      $value = bounded_number(0, $value, 100); // just in case
 908  
 909      $gradebookcalculationsfreeze = 'gradebook_calculations_freeze_' . $grade_item->courseid;
 910  
 911      foreach ($letters as $boundary => $letter) {
 912          if (property_exists($CFG, $gradebookcalculationsfreeze) && (int)$CFG->{$gradebookcalculationsfreeze} <= 20160518) {
 913              // Do nothing.
 914          } else {
 915              // The boundary is a percentage out of 100 so use 0 as the min and 100 as the max.
 916              $boundary = grade_grade::standardise_score($boundary, 0, 100, 0, 100);
 917          }
 918          if ($value >= $boundary) {
 919              return format_string($letter);
 920          }
 921      }
 922      return '-'; // no match? maybe '' would be more correct
 923  }
 924  
 925  
 926  /**
 927   * Returns grade options for gradebook grade category menu
 928   *
 929   * @param int $courseid The course ID
 930   * @param bool $includenew Include option for new category at array index -1
 931   * @return array of grade categories in course
 932   */
 933  function grade_get_categories_menu($courseid, $includenew=false) {
 934      $result = array();
 935      if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
 936          //make sure course category exists
 937          if (!grade_category::fetch_course_category($courseid)) {
 938              debugging('Can not create course grade category!');
 939              return $result;
 940          }
 941          $categories = grade_category::fetch_all(array('courseid'=>$courseid));
 942      }
 943      foreach ($categories as $key=>$category) {
 944          if ($category->is_course_category()) {
 945              $result[$category->id] = get_string('uncategorised', 'grades');
 946              unset($categories[$key]);
 947          }
 948      }
 949      if ($includenew) {
 950          $result[-1] = get_string('newcategory', 'grades');
 951      }
 952      $cats = array();
 953      foreach ($categories as $category) {
 954          $cats[$category->id] = $category->get_name();
 955      }
 956      core_collator::asort($cats);
 957  
 958      return ($result+$cats);
 959  }
 960  
 961  /**
 962   * Returns the array of grade letters to be used in the supplied context
 963   *
 964   * @param object $context Context object or null for defaults
 965   * @return array of grade_boundary (minimum) => letter_string
 966   */
 967  function grade_get_letters($context=null) {
 968      global $DB;
 969  
 970      if (empty($context)) {
 971          //default grading letters
 972          return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
 973      }
 974  
 975      $cache = cache::make('core', 'grade_letters');
 976      $data = $cache->get($context->id);
 977  
 978      if (!empty($data)) {
 979          return $data;
 980      }
 981  
 982      $letters = array();
 983  
 984      $contexts = $context->get_parent_context_ids();
 985      array_unshift($contexts, $context->id);
 986  
 987      foreach ($contexts as $ctxid) {
 988          if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
 989              foreach ($records as $record) {
 990                  $letters[$record->lowerboundary] = $record->letter;
 991              }
 992          }
 993  
 994          if (!empty($letters)) {
 995              // Cache the grade letters for this context.
 996              $cache->set($context->id, $letters);
 997              return $letters;
 998          }
 999      }
1000  
1001      $letters = grade_get_letters(null);
1002      // Cache the grade letters for this context.
1003      $cache->set($context->id, $letters);
1004      return $letters;
1005  }
1006  
1007  
1008  /**
1009   * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
1010   *
1011   * @param string $idnumber string (with magic quotes)
1012   * @param int $courseid ID numbers are course unique only
1013   * @param grade_item $grade_item The grade item this idnumber is associated with
1014   * @param stdClass $cm used for course module idnumbers and items attached to modules
1015   * @return bool true means idnumber ok
1016   */
1017  function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
1018      global $DB;
1019  
1020      if ($idnumber == '') {
1021          //we allow empty idnumbers
1022          return true;
1023      }
1024  
1025      // keep existing even when not unique
1026      if ($cm and $cm->idnumber == $idnumber) {
1027          if ($grade_item and $grade_item->itemnumber != 0) {
1028              // grade item with itemnumber > 0 can't have the same idnumber as the main
1029              // itemnumber 0 which is synced with course_modules
1030              return false;
1031          }
1032          return true;
1033      } else if ($grade_item and $grade_item->idnumber == $idnumber) {
1034          return true;
1035      }
1036  
1037      if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
1038          return false;
1039      }
1040  
1041      if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
1042          return false;
1043      }
1044  
1045      return true;
1046  }
1047  
1048  /**
1049   * Force final grade recalculation in all course items
1050   *
1051   * @param int $courseid The course ID to recalculate
1052   */
1053  function grade_force_full_regrading($courseid) {
1054      global $DB;
1055      $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
1056  }
1057  
1058  /**
1059   * Forces regrading of all site grades. Used when changing site setings
1060   */
1061  function grade_force_site_regrading() {
1062      global $CFG, $DB;
1063      $DB->set_field('grade_items', 'needsupdate', 1);
1064  }
1065  
1066  /**
1067   * Recover a user's grades from grade_grades_history
1068   * @param int $userid the user ID whose grades we want to recover
1069   * @param int $courseid the relevant course
1070   * @return bool true if successful or false if there was an error or no grades could be recovered
1071   */
1072  function grade_recover_history_grades($userid, $courseid) {
1073      global $CFG, $DB;
1074  
1075      if ($CFG->disablegradehistory) {
1076          debugging('Attempting to recover grades when grade history is disabled.');
1077          return false;
1078      }
1079  
1080      //Were grades recovered? Flag to return.
1081      $recoveredgrades = false;
1082  
1083      //Check the user is enrolled in this course
1084      //Dont bother checking if they have a gradeable role. They may get one later so recover
1085      //whatever grades they have now just in case.
1086      $course_context = context_course::instance($courseid);
1087      if (!is_enrolled($course_context, $userid)) {
1088          debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
1089          return false;
1090      }
1091  
1092      //Check for existing grades for this user in this course
1093      //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
1094      //In the future we could move the existing grades to the history table then recover the grades from before then
1095      $sql = "SELECT gg.id
1096                FROM {grade_grades} gg
1097                JOIN {grade_items} gi ON gi.id = gg.itemid
1098               WHERE gi.courseid = :courseid AND gg.userid = :userid";
1099      $params = array('userid' => $userid, 'courseid' => $courseid);
1100      if ($DB->record_exists_sql($sql, $params)) {
1101          debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
1102          return false;
1103      } else {
1104          //Retrieve the user's old grades
1105          //have history ID as first column to guarantee we a unique first column
1106          $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
1107                         h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
1108                         h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
1109                    FROM {grade_grades_history} h
1110                    JOIN (SELECT itemid, MAX(id) AS id
1111                            FROM {grade_grades_history}
1112                           WHERE userid = :userid1
1113                        GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
1114                    JOIN {grade_items} gi ON gi.id = h.itemid
1115                    JOIN (SELECT itemid, MAX(timemodified) AS tm
1116                            FROM {grade_grades_history}
1117                           WHERE userid = :userid2 AND action = :insertaction
1118                        GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
1119                   WHERE gi.courseid = :courseid";
1120          $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT, 'courseid' => $courseid);
1121          $oldgrades = $DB->get_records_sql($sql, $params);
1122  
1123          //now move the old grades to the grade_grades table
1124          foreach ($oldgrades as $oldgrade) {
1125              unset($oldgrade->id);
1126  
1127              $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
1128              $grade->insert($oldgrade->source);
1129  
1130              //dont include default empty grades created when activities are created
1131              if (!is_null($oldgrade->finalgrade) || !is_null($oldgrade->feedback)) {
1132                  $recoveredgrades = true;
1133              }
1134          }
1135      }
1136  
1137      //Some activities require manual grade synching (moving grades from the activity into the gradebook)
1138      //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
1139      grade_grab_course_grades($courseid, null, $userid);
1140  
1141      return $recoveredgrades;
1142  }
1143  
1144  /**
1145   * Updates all final grades in course.
1146   *
1147   * @param int $courseid The course ID
1148   * @param int $userid If specified try to do a quick regrading of the grades of this user only
1149   * @param object $updated_item Optional grade item to be marked for regrading. It is required if $userid is set.
1150   * @param \core\progress\base $progress If provided, will be used to update progress on this long operation.
1151   * @return array|true true if ok, array of errors if problems found. Grade item id => error message
1152   */
1153  function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null, $progress=null) {
1154      // This may take a very long time and extra memory.
1155      \core_php_time_limit::raise();
1156      raise_memory_limit(MEMORY_EXTRA);
1157  
1158      $course_item = grade_item::fetch_course_item($courseid);
1159  
1160      if ($progress == null) {
1161          $progress = new \core\progress\none();
1162      }
1163  
1164      if ($userid) {
1165          // one raw grade updated for one user
1166          if (empty($updated_item)) {
1167              throw new \moodle_exception("cannotbenull", 'debug', '', "updated_item");
1168          }
1169          if ($course_item->needsupdate) {
1170              $updated_item->force_regrading();
1171              return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
1172          }
1173  
1174      } else {
1175          if (!$course_item->needsupdate) {
1176              // nothing to do :-)
1177              return true;
1178          }
1179      }
1180  
1181      // Categories might have to run some processing before we fetch the grade items.
1182      // This gives them a final opportunity to update and mark their children to be updated.
1183      // We need to work on the children categories up to the parent ones, so that, for instance,
1184      // if a category total is updated it will be reflected in the parent category.
1185      $cats = grade_category::fetch_all(array('courseid' => $courseid));
1186      $flatcattree = array();
1187      foreach ($cats as $cat) {
1188          if (!isset($flatcattree[$cat->depth])) {
1189              $flatcattree[$cat->depth] = array();
1190          }
1191          $flatcattree[$cat->depth][] = $cat;
1192      }
1193      krsort($flatcattree);
1194      foreach ($flatcattree as $depth => $cats) {
1195          foreach ($cats as $cat) {
1196              $cat->pre_regrade_final_grades();
1197          }
1198      }
1199  
1200      $progresstotal = 0;
1201      $progresscurrent = 0;
1202  
1203      $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1204      $depends_on = array();
1205  
1206      foreach ($grade_items as $gid=>$gitem) {
1207          if ((!empty($updated_item) and $updated_item->id == $gid) ||
1208                  $gitem->is_course_item() || $gitem->is_category_item() || $gitem->is_calculated()) {
1209              $grade_items[$gid]->needsupdate = 1;
1210          }
1211  
1212          // We load all dependencies of these items later we can discard some grade_items based on this.
1213          if ($grade_items[$gid]->needsupdate) {
1214              $depends_on[$gid] = $grade_items[$gid]->depends_on();
1215              $progresstotal++;
1216          }
1217      }
1218  
1219      $progress->start_progress('regrade_course', $progresstotal);
1220  
1221      $errors = array();
1222      $finalids = array();
1223      $updatedids = array();
1224      $gids     = array_keys($grade_items);
1225      $failed = 0;
1226  
1227      while (count($finalids) < count($gids)) { // work until all grades are final or error found
1228          $count = 0;
1229          foreach ($gids as $gid) {
1230              if (in_array($gid, $finalids)) {
1231                  continue; // already final
1232              }
1233  
1234              if (!$grade_items[$gid]->needsupdate) {
1235                  $finalids[] = $gid; // we can make it final - does not need update
1236                  continue;
1237              }
1238              $thisprogress = $progresstotal;
1239              foreach ($grade_items as $item) {
1240                  if ($item->needsupdate) {
1241                      $thisprogress--;
1242                  }
1243              }
1244              // Clip between $progresscurrent and $progresstotal.
1245              $thisprogress = max(min($thisprogress, $progresstotal), $progresscurrent);
1246              $progress->progress($thisprogress);
1247              $progresscurrent = $thisprogress;
1248  
1249              foreach ($depends_on[$gid] as $did) {
1250                  if (!in_array($did, $finalids)) {
1251                      // This item depends on something that is not yet in finals array.
1252                      continue 2;
1253                  }
1254              }
1255  
1256              // If this grade item has no dependancy with any updated item at all, then remove it from being recalculated.
1257  
1258              // When we get here, all of this grade item's decendents are marked as final so they would be marked as updated too
1259              // if they would have been regraded. We don't need to regrade items which dependants (not only the direct ones
1260              // but any dependant in the cascade) have not been updated.
1261  
1262              // If $updated_item was specified we discard the grade items that do not depend on it or on any grade item that
1263              // depend on $updated_item.
1264  
1265              // Here we check to see if the direct decendants are marked as updated.
1266              if (!empty($updated_item) && $gid != $updated_item->id && !in_array($updated_item->id, $depends_on[$gid])) {
1267  
1268                  // We need to ensure that none of this item's dependencies have been updated.
1269                  // If we find that one of the direct decendants of this grade item is marked as updated then this
1270                  // grade item needs to be recalculated and marked as updated.
1271                  // Being marked as updated is done further down in the code.
1272  
1273                  $updateddependencies = false;
1274                  foreach ($depends_on[$gid] as $dependency) {
1275                      if (in_array($dependency, $updatedids)) {
1276                          $updateddependencies = true;
1277                          break;
1278                      }
1279                  }
1280                  if ($updateddependencies === false) {
1281                      // If no direct descendants are marked as updated, then we don't need to update this grade item. We then mark it
1282                      // as final.
1283                      $count++;
1284                      $finalids[] = $gid;
1285                      continue;
1286                  }
1287              }
1288  
1289              // Let's update, calculate or aggregate.
1290              $result = $grade_items[$gid]->regrade_final_grades($userid, $progress);
1291  
1292              if ($result === true) {
1293  
1294                  // We should only update the database if we regraded all users.
1295                  if (empty($userid)) {
1296                      $grade_items[$gid]->regrading_finished();
1297                      // Do the locktime item locking.
1298                      $grade_items[$gid]->check_locktime();
1299                  } else {
1300                      $grade_items[$gid]->needsupdate = 0;
1301                  }
1302                  $count++;
1303                  $finalids[] = $gid;
1304                  $updatedids[] = $gid;
1305  
1306              } else {
1307                  $grade_items[$gid]->force_regrading();
1308                  $errors[$gid] = $result;
1309              }
1310          }
1311  
1312          if ($count == 0) {
1313              $failed++;
1314          } else {
1315              $failed = 0;
1316          }
1317  
1318          if ($failed > 1) {
1319              foreach($gids as $gid) {
1320                  if (in_array($gid, $finalids)) {
1321                      continue; // this one is ok
1322                  }
1323                  $grade_items[$gid]->force_regrading();
1324                  if (!empty($grade_items[$gid]->calculation) && empty($errors[$gid])) {
1325                      $itemname = $grade_items[$gid]->get_name();
1326                      $errors[$gid] = get_string('errorcalculationbroken', 'grades', $itemname);
1327                  }
1328              }
1329              break; // Found error.
1330          }
1331      }
1332      $progress->end_progress();
1333  
1334      if (count($errors) == 0) {
1335          if (empty($userid)) {
1336              // do the locktime locking of grades, but only when doing full regrading
1337              grade_grade::check_locktime_all($gids);
1338          }
1339          return true;
1340      } else {
1341          return $errors;
1342      }
1343  }
1344  
1345  /**
1346   * Refetches grade data from course activities
1347   *
1348   * @param int $courseid The course ID
1349   * @param string $modname Limit the grade fetch to a single module type. For example 'forum'
1350   * @param int $userid limit the grade fetch to a single user
1351   */
1352  function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
1353      global $CFG, $DB;
1354  
1355      if ($modname) {
1356          $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1357                    FROM {".$modname."} a, {course_modules} cm, {modules} m
1358                   WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1359          $params = array('modname'=>$modname, 'courseid'=>$courseid);
1360  
1361          if ($modinstances = $DB->get_records_sql($sql, $params)) {
1362              foreach ($modinstances as $modinstance) {
1363                  grade_update_mod_grades($modinstance, $userid);
1364              }
1365          }
1366          return;
1367      }
1368  
1369      if (!$mods = core_component::get_plugin_list('mod') ) {
1370          throw new \moodle_exception('nomodules', 'debug');
1371      }
1372  
1373      foreach ($mods as $mod => $fullmod) {
1374          if ($mod == 'NEWMODULE') {   // Someone has unzipped the template, ignore it
1375              continue;
1376          }
1377  
1378          // include the module lib once
1379          if (file_exists($fullmod.'/lib.php')) {
1380              // get all instance of the activity
1381              $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1382                        FROM {".$mod."} a, {course_modules} cm, {modules} m
1383                       WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1384              $params = array('mod'=>$mod, 'courseid'=>$courseid);
1385  
1386              if ($modinstances = $DB->get_records_sql($sql, $params)) {
1387                  foreach ($modinstances as $modinstance) {
1388                      grade_update_mod_grades($modinstance, $userid);
1389                  }
1390              }
1391          }
1392      }
1393  }
1394  
1395  /**
1396   * Force full update of module grades in central gradebook
1397   *
1398   * @param object $modinstance Module object with extra cmidnumber and modname property
1399   * @param int $userid Optional user ID if limiting the update to a single user
1400   * @return bool True if success
1401   */
1402  function grade_update_mod_grades($modinstance, $userid=0) {
1403      global $CFG, $DB;
1404  
1405      $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
1406      if (!file_exists($fullmod.'/lib.php')) {
1407          debugging('missing lib.php file in module ' . $modinstance->modname);
1408          return false;
1409      }
1410      include_once($fullmod.'/lib.php');
1411  
1412      $updateitemfunc   = $modinstance->modname.'_grade_item_update';
1413      $updategradesfunc = $modinstance->modname.'_update_grades';
1414  
1415      if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1416          //new grading supported, force updating of grades
1417          $updateitemfunc($modinstance);
1418          $updategradesfunc($modinstance, $userid);
1419      } else if (function_exists($updategradesfunc) xor function_exists($updateitemfunc)) {
1420          // Module does not support grading?
1421          debugging("You have declared one of $updateitemfunc and $updategradesfunc but not both. " .
1422                    "This will cause broken behaviour.", DEBUG_DEVELOPER);
1423      }
1424  
1425      return true;
1426  }
1427  
1428  /**
1429   * Remove grade letters for given context
1430   *
1431   * @param context $context The context
1432   * @param bool $showfeedback If true a success notification will be displayed
1433   */
1434  function remove_grade_letters($context, $showfeedback) {
1435      global $DB, $OUTPUT;
1436  
1437      $strdeleted = get_string('deleted');
1438  
1439      $records = $DB->get_records('grade_letters', array('contextid' => $context->id));
1440      foreach ($records as $record) {
1441          $DB->delete_records('grade_letters', array('id' => $record->id));
1442          // Trigger the letter grade deleted event.
1443          $event = \core\event\grade_letter_deleted::create(array(
1444              'objectid' => $record->id,
1445              'context' => $context,
1446          ));
1447          $event->trigger();
1448      }
1449      if ($showfeedback) {
1450          echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
1451      }
1452  
1453      $cache = cache::make('core', 'grade_letters');
1454      $cache->delete($context->id);
1455  }
1456  
1457  /**
1458   * Remove all grade related course data
1459   * Grade history is kept
1460   *
1461   * @param int $courseid The course ID
1462   * @param bool $showfeedback If true success notifications will be displayed
1463   */
1464  function remove_course_grades($courseid, $showfeedback) {
1465      global $DB, $OUTPUT;
1466  
1467      $fs = get_file_storage();
1468      $strdeleted = get_string('deleted');
1469  
1470      $course_category = grade_category::fetch_course_category($courseid);
1471      $course_category->delete('coursedelete');
1472      $fs->delete_area_files(context_course::instance($courseid)->id, 'grade', 'feedback');
1473      if ($showfeedback) {
1474          echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
1475      }
1476  
1477      if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1478          foreach ($outcomes as $outcome) {
1479              $outcome->delete('coursedelete');
1480          }
1481      }
1482      $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1483      if ($showfeedback) {
1484          echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1485      }
1486  
1487      if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1488          foreach ($scales as $scale) {
1489              $scale->delete('coursedelete');
1490          }
1491      }
1492      if ($showfeedback) {
1493          echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1494      }
1495  
1496      $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1497      if ($showfeedback) {
1498          echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1499      }
1500  }
1501  
1502  /**
1503   * Called when course category is deleted
1504   * Cleans the gradebook of associated data
1505   *
1506   * @param int $categoryid The course category id
1507   * @param int $newparentid If empty everything is deleted. Otherwise the ID of the category where content moved
1508   * @param bool $showfeedback print feedback
1509   */
1510  function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1511      global $DB;
1512  
1513      $context = context_coursecat::instance($categoryid);
1514      $records = $DB->get_records('grade_letters', array('contextid' => $context->id));
1515      foreach ($records as $record) {
1516          $DB->delete_records('grade_letters', array('id' => $record->id));
1517          // Trigger the letter grade deleted event.
1518          $event = \core\event\grade_letter_deleted::create(array(
1519              'objectid' => $record->id,
1520              'context' => $context,
1521          ));
1522          $event->trigger();
1523      }
1524  }
1525  
1526  /**
1527   * Does gradebook cleanup when a module is uninstalled
1528   * Deletes all associated grade items
1529   *
1530   * @param string $modname The grade item module name to remove. For example 'forum'
1531   */
1532  function grade_uninstalled_module($modname) {
1533      global $CFG, $DB;
1534  
1535      $sql = "SELECT *
1536                FROM {grade_items}
1537               WHERE itemtype='mod' AND itemmodule=?";
1538  
1539      // go all items for this module and delete them including the grades
1540      $rs = $DB->get_recordset_sql($sql, array($modname));
1541      foreach ($rs as $item) {
1542          $grade_item = new grade_item($item, false);
1543          $grade_item->delete('moduninstall');
1544      }
1545      $rs->close();
1546  }
1547  
1548  /**
1549   * Deletes all of a user's grade data from gradebook
1550   *
1551   * @param int $userid The user whose grade data should be deleted
1552   */
1553  function grade_user_delete($userid) {
1554      if ($grades = grade_grade::fetch_all(array('userid'=>$userid))) {
1555          foreach ($grades as $grade) {
1556              $grade->delete('userdelete');
1557          }
1558      }
1559  }
1560  
1561  /**
1562   * Purge course data when user unenrolls from a course
1563   *
1564   * @param int $courseid The ID of the course the user has unenrolled from
1565   * @param int $userid The ID of the user unenrolling
1566   */
1567  function grade_user_unenrol($courseid, $userid) {
1568      if ($items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1569          foreach ($items as $item) {
1570              if ($grades = grade_grade::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id))) {
1571                  foreach ($grades as $grade) {
1572                      $grade->delete('userdelete');
1573                  }
1574              }
1575          }
1576      }
1577  }
1578  
1579  /**
1580   * Reset all course grades, refetch from the activities and recalculate
1581   *
1582   * @param int $courseid The course to reset
1583   * @return bool success
1584   */
1585  function grade_course_reset($courseid) {
1586  
1587      // no recalculations
1588      grade_force_full_regrading($courseid);
1589  
1590      $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1591      foreach ($grade_items as $gid=>$grade_item) {
1592          $grade_item->delete_all_grades('reset');
1593      }
1594  
1595      //refetch all grades
1596      grade_grab_course_grades($courseid);
1597  
1598      // recalculate all grades
1599      grade_regrade_final_grades($courseid);
1600      return true;
1601  }
1602  
1603  /**
1604   * Convert a number to 5 decimal point float, null db compatible format
1605   * (we need this to decide if db value changed)
1606   *
1607   * @param float|null $number The number to convert
1608   * @return float|null float or null
1609   */
1610  function grade_floatval(?float $number) {
1611      if (is_null($number)) {
1612          return null;
1613      }
1614      // we must round to 5 digits to get the same precision as in 10,5 db fields
1615      // note: db rounding for 10,5 is different from php round() function
1616      return round($number, 5);
1617  }
1618  
1619  /**
1620   * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}. Nulls accepted too.
1621   * Used for determining if a database update is required
1622   *
1623   * @param float|null $f1 Float one to compare
1624   * @param float|null $f2 Float two to compare
1625   * @return bool True if the supplied values are different
1626   */
1627  function grade_floats_different(?float $f1, ?float $f2): bool {
1628      // note: db rounding for 10,5 is different from php round() function
1629      return (grade_floatval($f1) !== grade_floatval($f2));
1630  }
1631  
1632  /**
1633   * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
1634   *
1635   * Do not use rounding for 10,5 at the database level as the results may be
1636   * different from php round() function.
1637   *
1638   * @since Moodle 2.0
1639   * @param float|null $f1 Float one to compare
1640   * @param float|null $f2 Float two to compare
1641   * @return bool True if the values should be considered as the same grades
1642   */
1643  function grade_floats_equal(?float $f1, ?float $f2): bool {
1644      return (grade_floatval($f1) === grade_floatval($f2));
1645  }
1646  
1647  /**
1648   * Get the most appropriate grade date for a grade item given the user that the grade relates to.
1649   *
1650   * @param \stdClass $grade
1651   * @param \stdClass $user
1652   * @return int|null
1653   */
1654  function grade_get_date_for_user_grade(\stdClass $grade, \stdClass $user): ?int {
1655      // The `datesubmitted` is the time that the grade was created.
1656      // The `dategraded` is the time that it was modified or overwritten.
1657      // If the grade was last modified by the user themselves use the date graded.
1658      // Otherwise use date submitted.
1659      if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
1660          return $grade->dategraded;
1661      } else {
1662          return $grade->datesubmitted;
1663      }
1664  }