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