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