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