Search moodle.org's
Developer Documentation

See Release Notes

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

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