Search moodle.org's
Developer Documentation

  • 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 37 and 311] [Versions 38 and 311]

       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  }