[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/ -> gradelib.php (source)

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


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