Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.
/lib/ -> gradelib.php (source)

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