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 * Definition of a class to represent a grade item 19 * 20 * @package core_grades 21 * @category grade 22 * @copyright 2006 Nicolas Connault 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 defined('MOODLE_INTERNAL') || die(); 27 require_once ('grade_object.php'); 28 29 /** 30 * Class representing a grade item. 31 * 32 * It is responsible for handling its DB representation, modifying and returning its metadata. 33 * 34 * @package core_grades 35 * @category grade 36 * @copyright 2006 Nicolas Connault 37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 */ 39 class grade_item extends grade_object { 40 /** 41 * DB Table (used by grade_object). 42 * @var string $table 43 */ 44 public $table = 'grade_items'; 45 46 /** 47 * Array of required table fields, must start with 'id'. 48 * @var array $required_fields 49 */ 50 public $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance', 51 'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin', 52 'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef', 53 'aggregationcoef2', 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime', 54 'needsupdate', 'weightoverride', 'timecreated', 'timemodified'); 55 56 /** 57 * The course this grade_item belongs to. 58 * @var int $courseid 59 */ 60 public $courseid; 61 62 /** 63 * The category this grade_item belongs to (optional). 64 * @var int $categoryid 65 */ 66 public $categoryid; 67 68 /** 69 * The grade_category object referenced $this->iteminstance if itemtype == 'category' or == 'course'. 70 * @var grade_category $item_category 71 */ 72 public $item_category; 73 74 /** 75 * The grade_category object referenced by $this->categoryid. 76 * @var grade_category $parent_category 77 */ 78 public $parent_category; 79 80 81 /** 82 * The name of this grade_item (pushed by the module). 83 * @var string $itemname 84 */ 85 public $itemname; 86 87 /** 88 * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc... 89 * @var string $itemtype 90 */ 91 public $itemtype; 92 93 /** 94 * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc). 95 * @var string $itemmodule 96 */ 97 public $itemmodule; 98 99 /** 100 * ID of the item module 101 * @var int $iteminstance 102 */ 103 public $iteminstance; 104 105 /** 106 * Number of the item in a series of multiple grades pushed by an activity. 107 * @var int $itemnumber 108 */ 109 public $itemnumber; 110 111 /** 112 * Info and notes about this item. 113 * @var string $iteminfo 114 */ 115 public $iteminfo; 116 117 /** 118 * Arbitrary idnumber provided by the module responsible. 119 * @var string $idnumber 120 */ 121 public $idnumber; 122 123 /** 124 * Calculation string used for this item. 125 * @var string $calculation 126 */ 127 public $calculation; 128 129 /** 130 * Indicates if we already tried to normalize the grade calculation formula. 131 * This flag helps to minimize db access when broken formulas used in calculation. 132 * @var bool 133 */ 134 public $calculation_normalized; 135 /** 136 * Math evaluation object 137 * @var calc_formula A formula object 138 */ 139 public $formula; 140 141 /** 142 * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text) 143 * @var int $gradetype 144 */ 145 public $gradetype = GRADE_TYPE_VALUE; 146 147 /** 148 * Maximum allowable grade. 149 * @var float $grademax 150 */ 151 public $grademax = 100; 152 153 /** 154 * Minimum allowable grade. 155 * @var float $grademin 156 */ 157 public $grademin = 0; 158 159 /** 160 * id of the scale, if this grade is based on a scale. 161 * @var int $scaleid 162 */ 163 public $scaleid; 164 165 /** 166 * The grade_scale object referenced by $this->scaleid. 167 * @var grade_scale $scale 168 */ 169 public $scale; 170 171 /** 172 * The id of the optional grade_outcome associated with this grade_item. 173 * @var int $outcomeid 174 */ 175 public $outcomeid; 176 177 /** 178 * The grade_outcome this grade is associated with, if applicable. 179 * @var grade_outcome $outcome 180 */ 181 public $outcome; 182 183 /** 184 * grade required to pass. (grademin <= gradepass <= grademax) 185 * @var float $gradepass 186 */ 187 public $gradepass = 0; 188 189 /** 190 * Multiply all grades by this number. 191 * @var float $multfactor 192 */ 193 public $multfactor = 1.0; 194 195 /** 196 * Add this to all grades. 197 * @var float $plusfactor 198 */ 199 public $plusfactor = 0; 200 201 /** 202 * Aggregation coeficient used for weighted averages or extra credit 203 * @var float $aggregationcoef 204 */ 205 public $aggregationcoef = 0; 206 207 /** 208 * Aggregation coeficient used for weighted averages only 209 * @var float $aggregationcoef2 210 */ 211 public $aggregationcoef2 = 0; 212 213 /** 214 * Sorting order of the columns. 215 * @var int $sortorder 216 */ 217 public $sortorder = 0; 218 219 /** 220 * Display type of the grades (Real, Percentage, Letter, or default). 221 * @var int $display 222 */ 223 public $display = GRADE_DISPLAY_TYPE_DEFAULT; 224 225 /** 226 * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types. 227 * @var int $decimals 228 */ 229 public $decimals = null; 230 231 /** 232 * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating. 233 * @var int $locked 234 */ 235 public $locked = 0; 236 237 /** 238 * Date after which the grade will be locked. Empty means no automatic locking. 239 * @var int $locktime 240 */ 241 public $locktime = 0; 242 243 /** 244 * If set, the whole column will be recalculated, then this flag will be switched off. 245 * @var bool $needsupdate 246 */ 247 public $needsupdate = 1; 248 249 /** 250 * If set, the grade item's weight has been overridden by a user and should not be automatically adjusted. 251 */ 252 public $weightoverride = 0; 253 254 /** 255 * Cached dependson array 256 * @var array An array of cached grade item dependencies. 257 */ 258 public $dependson_cache = null; 259 260 /** 261 * @var bool If we regrade this item should we mark it as overridden? 262 */ 263 public $markasoverriddenwhengraded = true; 264 265 /** 266 * @var int course module ID 267 */ 268 public $cmid; 269 270 /** 271 * @var string average information. 272 */ 273 public $avg; 274 275 /** 276 * Category name. 277 * @var string 278 */ 279 public $category; 280 281 /** 282 * Constructor. Optionally (and by default) attempts to fetch corresponding row from the database 283 * 284 * @param array $params An array with required parameters for this grade object. 285 * @param bool $fetch Whether to fetch corresponding row from the database or not, 286 * optional fields might not be defined if false used 287 */ 288 public function __construct($params = null, $fetch = true) { 289 global $CFG; 290 // Set grademax from $CFG->gradepointdefault . 291 self::set_properties($this, array('grademax' => $CFG->gradepointdefault)); 292 parent::__construct($params, $fetch); 293 } 294 295 /** 296 * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects. 297 * Force regrading if necessary, rounds the float numbers using php function, 298 * the reason is we need to compare the db value with computed number to skip regrading if possible. 299 * 300 * @param string $source from where was the object inserted (mod/forum, manual, etc.) 301 * @param bool $isbulkupdate If bulk grade update is happening. 302 * @return bool success 303 */ 304 public function update($source = null, $isbulkupdate = false) { 305 // reset caches 306 $this->dependson_cache = null; 307 308 // Retrieve scale and infer grademax/min from it if needed 309 $this->load_scale(); 310 311 // make sure there is not 0 in outcomeid 312 if (empty($this->outcomeid)) { 313 $this->outcomeid = null; 314 } 315 316 if ($this->qualifies_for_regrading()) { 317 $this->force_regrading(); 318 } 319 320 $this->timemodified = time(); 321 322 $this->grademin = grade_floatval($this->grademin); 323 $this->grademax = grade_floatval($this->grademax); 324 $this->multfactor = grade_floatval($this->multfactor); 325 $this->plusfactor = grade_floatval($this->plusfactor); 326 $this->aggregationcoef = grade_floatval($this->aggregationcoef); 327 $this->aggregationcoef2 = grade_floatval($this->aggregationcoef2); 328 329 $result = parent::update($source, $isbulkupdate); 330 331 if ($result) { 332 $event = \core\event\grade_item_updated::create_from_grade_item($this); 333 $event->trigger(); 334 } 335 336 return $result; 337 } 338 339 /** 340 * Compares the values held by this object with those of the matching record in DB, and returns 341 * whether or not these differences are sufficient to justify an update of all parent objects. 342 * This assumes that this object has an id number and a matching record in DB. If not, it will return false. 343 * 344 * @return bool 345 */ 346 public function qualifies_for_regrading() { 347 if (empty($this->id)) { 348 return false; 349 } 350 351 $db_item = new grade_item(array('id' => $this->id)); 352 353 $calculationdiff = $db_item->calculation != $this->calculation; 354 $categorydiff = $db_item->categoryid != $this->categoryid; 355 $gradetypediff = $db_item->gradetype != $this->gradetype; 356 $scaleiddiff = $db_item->scaleid != $this->scaleid; 357 $outcomeiddiff = $db_item->outcomeid != $this->outcomeid; 358 $locktimediff = $db_item->locktime != $this->locktime; 359 $grademindiff = grade_floats_different($db_item->grademin, $this->grademin); 360 $grademaxdiff = grade_floats_different($db_item->grademax, $this->grademax); 361 $multfactordiff = grade_floats_different($db_item->multfactor, $this->multfactor); 362 $plusfactordiff = grade_floats_different($db_item->plusfactor, $this->plusfactor); 363 $acoefdiff = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef); 364 $acoefdiff2 = grade_floats_different($db_item->aggregationcoef2, $this->aggregationcoef2); 365 $weightoverride = grade_floats_different($db_item->weightoverride, $this->weightoverride); 366 367 $needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time 368 $lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking 369 370 return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff 371 || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff 372 || $lockeddiff || $acoefdiff || $acoefdiff2 || $weightoverride || $locktimediff); 373 } 374 375 /** 376 * Finds and returns a grade_item instance based on params. 377 * 378 * @static 379 * @param array $params associative arrays varname=>value 380 * @return grade_item|bool Returns a grade_item instance or false if none found 381 */ 382 public static function fetch($params) { 383 return grade_object::fetch_helper('grade_items', 'grade_item', $params); 384 } 385 386 /** 387 * Check to see if there are any existing grades for this grade_item. 388 * 389 * @return boolean - true if there are valid grades for this grade_item. 390 */ 391 public function has_grades() { 392 global $DB; 393 394 $count = $DB->count_records_select('grade_grades', 395 'itemid = :gradeitemid AND finalgrade IS NOT NULL', 396 array('gradeitemid' => $this->id)); 397 return $count > 0; 398 } 399 400 /** 401 * Check to see if there are existing overridden grades for this grade_item. 402 * 403 * @return boolean - true if there are overridden grades for this grade_item. 404 */ 405 public function has_overridden_grades() { 406 global $DB; 407 408 $count = $DB->count_records_select('grade_grades', 409 'itemid = :gradeitemid AND finalgrade IS NOT NULL AND overridden > 0', 410 array('gradeitemid' => $this->id)); 411 return $count > 0; 412 } 413 414 /** 415 * Finds and returns all grade_item instances based on params. 416 * 417 * @static 418 * @param array $params associative arrays varname=>value 419 * @return array array of grade_item instances or false if none found. 420 */ 421 public static function fetch_all($params) { 422 return grade_object::fetch_all_helper('grade_items', 'grade_item', $params); 423 } 424 425 /** 426 * Delete all grades and force_regrading of parent category. 427 * 428 * @param string $source from where was the object deleted (mod/forum, manual, etc.) 429 * @return bool success 430 */ 431 public function delete($source=null) { 432 global $DB; 433 434 $transaction = $DB->start_delegated_transaction(); 435 $this->delete_all_grades($source); 436 $success = parent::delete($source); 437 $transaction->allow_commit(); 438 439 if ($success) { 440 $event = \core\event\grade_item_deleted::create_from_grade_item($this); 441 $event->trigger(); 442 } 443 444 return $success; 445 } 446 447 /** 448 * Delete all grades 449 * 450 * @param string $source from where was the object deleted (mod/forum, manual, etc.) 451 * @return bool 452 */ 453 public function delete_all_grades($source=null) { 454 global $DB; 455 456 $transaction = $DB->start_delegated_transaction(); 457 458 if (!$this->is_course_item()) { 459 $this->force_regrading(); 460 } 461 462 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) { 463 foreach ($grades as $grade) { 464 $grade->delete($source); 465 } 466 } 467 468 // Delete all the historical files. 469 // We only support feedback files for modules atm. 470 if ($this->is_external_item()) { 471 $fs = new file_storage(); 472 $fs->delete_area_files($this->get_context()->id, GRADE_FILE_COMPONENT, GRADE_HISTORY_FEEDBACK_FILEAREA); 473 } 474 475 $transaction->allow_commit(); 476 477 return true; 478 } 479 480 /** 481 * Duplicate grade item. 482 * 483 * @return grade_item The duplicate grade item 484 */ 485 public function duplicate() { 486 // Convert current object to array. 487 $copy = (array) $this; 488 489 if (empty($copy["id"])) { 490 throw new moodle_exception('invalidgradeitemid'); 491 } 492 493 // Remove fields that will be either unique or automatically filled. 494 $removekeys = array(); 495 $removekeys[] = 'id'; 496 $removekeys[] = 'idnumber'; 497 $removekeys[] = 'timecreated'; 498 $removekeys[] = 'sortorder'; 499 foreach ($removekeys as $key) { 500 unset($copy[$key]); 501 } 502 503 // Addendum to name. 504 $copy["itemname"] = get_string('duplicatedgradeitem', 'grades', $copy["itemname"]); 505 506 // Create new grade item. 507 $gradeitem = new grade_item($copy); 508 509 // Insert grade item into database. 510 $gradeitem->insert(); 511 512 return $gradeitem; 513 } 514 515 /** 516 * In addition to perform parent::insert(), calls force_regrading() method too. 517 * 518 * @param string $source From where was the object inserted (mod/forum, manual, etc.) 519 * @param string $isbulkupdate If bulk grade update is happening. 520 * @return int PK ID if successful, false otherwise 521 */ 522 public function insert($source = null, $isbulkupdate = false) { 523 global $CFG, $DB; 524 525 if (empty($this->courseid)) { 526 throw new \moodle_exception('cannotinsertgrade'); 527 } 528 529 // load scale if needed 530 $this->load_scale(); 531 532 // add parent category if needed 533 if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) { 534 $course_category = grade_category::fetch_course_category($this->courseid); 535 $this->categoryid = $course_category->id; 536 537 } 538 539 // always place the new items at the end, move them after insert if needed 540 $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid)); 541 if (!empty($last_sortorder)) { 542 $this->sortorder = $last_sortorder + 1; 543 } else { 544 $this->sortorder = 1; 545 } 546 547 // add proper item numbers to manual items 548 if ($this->itemtype == 'manual') { 549 if (empty($this->itemnumber)) { 550 $this->itemnumber = 0; 551 } 552 } 553 554 // make sure there is not 0 in outcomeid 555 if (empty($this->outcomeid)) { 556 $this->outcomeid = null; 557 } 558 559 $this->timecreated = $this->timemodified = time(); 560 561 if (parent::insert($source, $isbulkupdate)) { 562 // force regrading of items if needed 563 $this->force_regrading(); 564 565 $event = \core\event\grade_item_created::create_from_grade_item($this); 566 $event->trigger(); 567 568 return $this->id; 569 570 } else { 571 debugging("Could not insert this grade_item in the database!"); 572 return false; 573 } 574 } 575 576 /** 577 * Set idnumber of grade item, updates also course_modules table 578 * 579 * @param string $idnumber (without magic quotes) 580 * @return bool success 581 */ 582 public function add_idnumber($idnumber) { 583 global $DB; 584 if (!empty($this->idnumber)) { 585 return false; 586 } 587 588 if ($this->itemtype == 'mod' and !$this->is_outcome_item()) { 589 if ($this->itemnumber == 0) { 590 // for activity modules, itemnumber 0 is synced with the course_modules 591 if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) { 592 return false; 593 } 594 if (!empty($cm->idnumber)) { 595 return false; 596 } 597 $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id)); 598 $this->idnumber = $idnumber; 599 return $this->update(); 600 } else { 601 $this->idnumber = $idnumber; 602 return $this->update(); 603 } 604 605 } else { 606 $this->idnumber = $idnumber; 607 return $this->update(); 608 } 609 } 610 611 /** 612 * Returns the locked state of this grade_item (if the grade_item is locked OR no specific 613 * $userid is given) or the locked state of a specific grade within this item if a specific 614 * $userid is given and the grade_item is unlocked. 615 * 616 * @param int $userid The user's ID 617 * @return bool Locked state 618 */ 619 public function is_locked($userid=NULL) { 620 global $CFG; 621 622 // Override for any grade items belonging to activities which are in the process of being deleted. 623 require_once($CFG->dirroot . '/course/lib.php'); 624 if (course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance)) { 625 return true; 626 } 627 628 if (!empty($this->locked)) { 629 return true; 630 } 631 632 if (!empty($userid)) { 633 if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) { 634 $grade->grade_item =& $this; // prevent db fetching of cached grade_item 635 return $grade->is_locked(); 636 } 637 } 638 639 return false; 640 } 641 642 /** 643 * Locks or unlocks this grade_item and (optionally) all its associated final grades. 644 * 645 * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked. 646 * @param bool $cascade Lock/unlock child objects too 647 * @param bool $refresh Refresh grades when unlocking 648 * @return bool True if grade_item all grades updated, false if at least one update fails 649 */ 650 public function set_locked($lockedstate, $cascade=false, $refresh=true) { 651 if ($lockedstate) { 652 // Setting lock. 653 if (empty($this->id)) { 654 return false; 655 } else if ($this->needsupdate) { 656 // Can not lock grade without first having final grade, 657 // so we schedule it to be locked as soon as regrading is finished. 658 $this->locktime = time() - 1; 659 } else { 660 $this->locked = time(); 661 } 662 $this->update(); 663 664 if ($cascade) { 665 $grades = $this->get_final(); 666 foreach($grades as $g) { 667 $grade = new grade_grade($g, false); 668 $grade->grade_item =& $this; 669 $grade->set_locked(1, null, false); 670 } 671 } 672 673 return true; 674 675 } else { 676 /// removing lock 677 if (!empty($this->locked) and $this->locktime < time()) { 678 //we have to reset locktime or else it would lock up again 679 $this->locktime = 0; 680 } 681 682 $this->locked = 0; 683 $this->update(); 684 685 if ($cascade) { 686 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) { 687 foreach($grades as $grade) { 688 $grade->grade_item =& $this; 689 $grade->set_locked(0, null, false); 690 } 691 } 692 } 693 694 if ($refresh) { 695 //refresh when unlocking 696 $this->refresh_grades(); 697 } 698 699 return true; 700 } 701 } 702 703 /** 704 * Lock the grade if needed. Make sure this is called only when final grades are valid 705 */ 706 public function check_locktime() { 707 if (!empty($this->locked)) { 708 return; // already locked 709 } 710 711 if ($this->locktime and $this->locktime < time()) { 712 $this->locked = time(); 713 $this->update('locktime'); 714 } 715 } 716 717 /** 718 * Set the locktime for this grade item. 719 * 720 * @param int $locktime timestamp for lock to activate 721 * @return void 722 */ 723 public function set_locktime($locktime) { 724 $this->locktime = $locktime; 725 $this->update(); 726 } 727 728 /** 729 * Set the locktime for this grade item. 730 * 731 * @return int $locktime timestamp for lock to activate 732 */ 733 public function get_locktime() { 734 return $this->locktime; 735 } 736 737 /** 738 * Set the hidden status of grade_item and all grades. 739 * 740 * 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until 741 * 742 * @param int $hidden new hidden status 743 * @param bool $cascade apply to child objects too 744 */ 745 public function set_hidden($hidden, $cascade=false) { 746 parent::set_hidden($hidden, $cascade); 747 748 if ($cascade) { 749 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) { 750 foreach($grades as $grade) { 751 $grade->grade_item =& $this; 752 $grade->set_hidden($hidden, $cascade); 753 } 754 } 755 } 756 757 //if marking item visible make sure category is visible MDL-21367 758 if( !$hidden ) { 759 $category_array = grade_category::fetch_all(array('id'=>$this->categoryid)); 760 if ($category_array && array_key_exists($this->categoryid, $category_array)) { 761 $category = $category_array[$this->categoryid]; 762 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden 763 $category->set_hidden($hidden, false); 764 } 765 } 766 } 767 768 /** 769 * Returns the number of grades that are hidden 770 * 771 * @param string $groupsql SQL to limit the query by group 772 * @param array $params SQL params for $groupsql 773 * @param string $groupwheresql Where conditions for $groupsql 774 * @return int The number of hidden grades 775 */ 776 public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") { 777 global $DB; 778 $params = (array)$params; 779 $params['itemid'] = $this->id; 780 781 return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN " 782 ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params); 783 } 784 785 /** 786 * Mark regrading as finished successfully. This will also be called when subsequent regrading will not change any grades. 787 * Situations such as an error being found will still result in the regrading being finished. 788 */ 789 public function regrading_finished() { 790 global $DB; 791 $this->needsupdate = 0; 792 //do not use $this->update() because we do not want this logged in grade_item_history 793 $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id)); 794 } 795 796 /** 797 * Performs the necessary calculations on the grades_final referenced by this grade_item. 798 * Also resets the needsupdate flag once successfully performed. 799 * 800 * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(), 801 * because the regrading must be done in correct order!! 802 * 803 * @param int $userid Supply a user ID to limit the regrading to a single user 804 * @param \core\progress\base|null $progress Optional progress object, will be updated per user 805 * @return bool true if ok, error string otherwise 806 */ 807 public function regrade_final_grades($userid=null, ?\core\progress\base $progress = null) { 808 global $CFG, $DB; 809 810 // locked grade items already have correct final grades 811 if ($this->is_locked()) { 812 return true; 813 } 814 815 // calculation produces final value using formula from other final values 816 if ($this->is_calculated()) { 817 if ($this->compute($userid)) { 818 return true; 819 } else { 820 return "Could not calculate grades for grade item"; // TODO: improve and localize 821 } 822 823 // noncalculated outcomes already have final values - raw grades not used 824 } else if ($this->is_outcome_item()) { 825 return true; 826 827 // aggregate the category grade 828 } else if ($this->is_category_item() or $this->is_course_item()) { 829 // aggregate category grade item 830 $category = $this->load_item_category(); 831 $category->grade_item =& $this; 832 if ($category->generate_grades($userid, $progress)) { 833 return true; 834 } else { 835 return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize 836 } 837 838 } else if ($this->is_manual_item()) { 839 // manual items track only final grades, no raw grades 840 return true; 841 842 } else if (!$this->is_raw_used()) { 843 // hmm - raw grades are not used- nothing to regrade 844 return true; 845 } 846 847 // normal grade item - just new final grades 848 $result = true; 849 $grade_inst = new grade_grade(); 850 $fields = implode(',', $grade_inst->required_fields); 851 if ($userid) { 852 $params = array($this->id, $userid); 853 $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields); 854 } else { 855 $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields); 856 } 857 if ($rs) { 858 foreach ($rs as $grade_record) { 859 $grade = new grade_grade($grade_record, false); 860 861 // Incrementing the progress by nothing causes it to send an update (once per second) 862 // to the web browser so as to prevent the connection timing out. 863 if ($progress) { 864 $progress->increment_progress(0); 865 } 866 867 if (!empty($grade_record->locked) or !empty($grade_record->overridden)) { 868 // this grade is locked - final grade must be ok 869 continue; 870 } 871 872 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax); 873 874 if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) { 875 $success = $grade->update('system'); 876 877 // If successful trigger a user_graded event. 878 if ($success) { 879 $grade->load_grade_item(); 880 \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger(); 881 } else { 882 $result = "Internal error updating final grade"; 883 } 884 } 885 } 886 $rs->close(); 887 } 888 889 return $result; 890 } 891 892 /** 893 * Given a float grade value or integer grade scale, applies a number of adjustment based on 894 * grade_item variables and returns the result. 895 * 896 * @param float $rawgrade The raw grade value 897 * @param float $rawmin original rawmin 898 * @param float $rawmax original rawmax 899 * @return mixed 900 */ 901 public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) { 902 if (is_null($rawgrade)) { 903 return null; 904 } 905 906 if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade 907 908 if ($this->grademax < $this->grademin) { 909 return null; 910 } 911 912 if ($this->grademax == $this->grademin) { 913 return $this->grademax; // no range 914 } 915 916 // Standardise score to the new grade range 917 // NOTE: skip if the activity provides a manual rescaling option. 918 $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false); 919 if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) { 920 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax); 921 } 922 923 // Apply other grade_item factors 924 $rawgrade *= $this->multfactor; 925 $rawgrade += $this->plusfactor; 926 927 return $this->bounded_grade($rawgrade); 928 929 } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value 930 if (empty($this->scale)) { 931 $this->load_scale(); 932 } 933 934 if ($this->grademax < 0) { 935 return null; // scale not present - no grade 936 } 937 938 if ($this->grademax == 0) { 939 return $this->grademax; // only one option 940 } 941 942 // Convert scale if needed 943 // NOTE: skip if the activity provides a manual rescaling option. 944 $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false); 945 if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) { 946 // This should never happen because scales are locked if they are in use. 947 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax); 948 } 949 950 return $this->bounded_grade($rawgrade); 951 952 953 } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value 954 // somebody changed the grading type when grades already existed 955 return null; 956 957 } else { 958 debugging("Unknown grade type"); 959 return null; 960 } 961 } 962 963 /** 964 * Update the rawgrademax and rawgrademin for all grade_grades records for this item. 965 * Scale every rawgrade to maintain the percentage. This function should be called 966 * after the gradeitem has been updated to the new min and max values. 967 * 968 * @param float $oldgrademin The previous grade min value 969 * @param float $oldgrademax The previous grade max value 970 * @param float $newgrademin The new grade min value 971 * @param float $newgrademax The new grade max value 972 * @param string $source from where was the object inserted (mod/forum, manual, etc.) 973 * @return bool True on success 974 */ 975 public function rescale_grades_keep_percentage($oldgrademin, $oldgrademax, $newgrademin, $newgrademax, $source = null) { 976 global $DB; 977 978 if (empty($this->id)) { 979 return false; 980 } 981 982 if ($oldgrademax <= $oldgrademin) { 983 // Grades cannot be scaled. 984 return false; 985 } 986 $scale = ($newgrademax - $newgrademin) / ($oldgrademax - $oldgrademin); 987 if (($newgrademax - $newgrademin) <= 1) { 988 // We would lose too much precision, lets bail. 989 return false; 990 } 991 992 $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id)); 993 994 foreach ($rs as $graderecord) { 995 // For each record, create an object to work on. 996 $grade = new grade_grade($graderecord, false); 997 // Set this object in the item so it doesn't re-fetch it. 998 $grade->grade_item = $this; 999 1000 if (!$this->is_category_item() || ($this->is_category_item() && $grade->is_overridden())) { 1001 // Updating the raw grade automatically updates the min/max. 1002 if ($this->is_raw_used()) { 1003 $rawgrade = (($grade->rawgrade - $oldgrademin) * $scale) + $newgrademin; 1004 $this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE, null, null, null, $grade); 1005 } else { 1006 $finalgrade = (($grade->finalgrade - $oldgrademin) * $scale) + $newgrademin; 1007 $this->update_final_grade($grade->userid, $finalgrade, $source); 1008 } 1009 } 1010 } 1011 $rs->close(); 1012 1013 // Mark this item for regrading. 1014 $this->force_regrading(); 1015 1016 return true; 1017 } 1018 1019 /** 1020 * Sets this grade_item's needsupdate to true. Also marks the course item as needing update. 1021 * 1022 * @return void 1023 */ 1024 public function force_regrading() { 1025 global $DB; 1026 $this->needsupdate = 1; 1027 //mark this item and course item only - categories and calculated items are always regraded 1028 $wheresql = "(itemtype='course' OR id=?) AND courseid=?"; 1029 $params = array($this->id, $this->courseid); 1030 $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params); 1031 } 1032 1033 /** 1034 * Instantiates a grade_scale object from the DB if this item's scaleid variable is set 1035 * 1036 * @return grade_scale Returns a grade_scale object or null if no scale used 1037 */ 1038 public function load_scale() { 1039 if ($this->gradetype != GRADE_TYPE_SCALE) { 1040 $this->scaleid = null; 1041 } 1042 1043 if (!empty($this->scaleid)) { 1044 //do not load scale if already present 1045 if (empty($this->scale->id) or $this->scale->id != $this->scaleid) { 1046 $this->scale = grade_scale::fetch(array('id'=>$this->scaleid)); 1047 if (!$this->scale) { 1048 debugging('Incorrect scale id: '.$this->scaleid); 1049 $this->scale = null; 1050 return null; 1051 } 1052 $this->scale->load_items(); 1053 } 1054 1055 // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we 1056 // stay with the current min=1 max=count(scaleitems) 1057 $this->grademax = count($this->scale->scale_items); 1058 $this->grademin = 1; 1059 1060 } else { 1061 $this->scale = null; 1062 } 1063 1064 return $this->scale; 1065 } 1066 1067 /** 1068 * Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set 1069 * 1070 * @return grade_outcome This grade item's associated grade_outcome or null 1071 */ 1072 public function load_outcome() { 1073 if (!empty($this->outcomeid)) { 1074 $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid)); 1075 } 1076 return $this->outcome; 1077 } 1078 1079 /** 1080 * Returns the grade_category object this grade_item belongs to (referenced by categoryid) 1081 * or category attached to category item. 1082 * 1083 * @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item 1084 */ 1085 public function get_parent_category() { 1086 if ($this->is_category_item() or $this->is_course_item()) { 1087 return $this->get_item_category(); 1088 1089 } else { 1090 return grade_category::fetch(array('id'=>$this->categoryid)); 1091 } 1092 } 1093 1094 /** 1095 * Calls upon the get_parent_category method to retrieve the grade_category object 1096 * from the DB and assigns it to $this->parent_category. It also returns the object. 1097 * 1098 * @return grade_category This grade item's parent grade_category. 1099 */ 1100 public function load_parent_category() { 1101 if (empty($this->parent_category->id)) { 1102 $this->parent_category = $this->get_parent_category(); 1103 } 1104 return $this->parent_category; 1105 } 1106 1107 /** 1108 * Returns the grade_category for a grade category grade item 1109 * 1110 * @return grade_category|bool Returns a grade_category instance if applicable or false otherwise 1111 */ 1112 public function get_item_category() { 1113 if (!$this->is_course_item() and !$this->is_category_item()) { 1114 return false; 1115 } 1116 return grade_category::fetch(array('id'=>$this->iteminstance)); 1117 } 1118 1119 /** 1120 * Calls upon the get_item_category method to retrieve the grade_category object 1121 * from the DB and assigns it to $this->item_category. It also returns the object. 1122 * 1123 * @return grade_category 1124 */ 1125 public function load_item_category() { 1126 if (empty($this->item_category->id)) { 1127 $this->item_category = $this->get_item_category(); 1128 } 1129 return $this->item_category; 1130 } 1131 1132 /** 1133 * Is the grade item associated with category? 1134 * 1135 * @return bool 1136 */ 1137 public function is_category_item() { 1138 return ($this->itemtype == 'category'); 1139 } 1140 1141 /** 1142 * Is the grade item associated with course? 1143 * 1144 * @return bool 1145 */ 1146 public function is_course_item() { 1147 return ($this->itemtype == 'course'); 1148 } 1149 1150 /** 1151 * Is this a manually graded item? 1152 * 1153 * @return bool 1154 */ 1155 public function is_manual_item() { 1156 return ($this->itemtype == 'manual'); 1157 } 1158 1159 /** 1160 * Is this an outcome item? 1161 * 1162 * @return bool 1163 */ 1164 public function is_outcome_item() { 1165 return !empty($this->outcomeid); 1166 } 1167 1168 /** 1169 * Is the grade item external - associated with module, plugin or something else? 1170 * 1171 * @return bool 1172 */ 1173 public function is_external_item() { 1174 return ($this->itemtype == 'mod'); 1175 } 1176 1177 /** 1178 * Is the grade item overridable 1179 * 1180 * @return bool 1181 */ 1182 public function is_overridable_item() { 1183 if ($this->is_course_item() or $this->is_category_item()) { 1184 $overridable = (bool) get_config('moodle', 'grade_overridecat'); 1185 } else { 1186 $overridable = false; 1187 } 1188 1189 return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $overridable); 1190 } 1191 1192 /** 1193 * Is the grade item feedback overridable 1194 * 1195 * @return bool 1196 */ 1197 public function is_overridable_item_feedback() { 1198 return !$this->is_outcome_item() and $this->is_external_item(); 1199 } 1200 1201 /** 1202 * Returns true if grade items uses raw grades 1203 * 1204 * @return bool 1205 */ 1206 public function is_raw_used() { 1207 return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item()); 1208 } 1209 1210 /** 1211 * Returns true if the grade item is an aggreggated type grade. 1212 * 1213 * @since Moodle 2.8.7, 2.9.1 1214 * @return bool 1215 */ 1216 public function is_aggregate_item() { 1217 return ($this->is_category_item() || $this->is_course_item()); 1218 } 1219 1220 /** 1221 * Returns the grade item associated with the course 1222 * 1223 * @param int $courseid 1224 * @return grade_item Course level grade item object 1225 */ 1226 public static function fetch_course_item($courseid) { 1227 if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) { 1228 return $course_item; 1229 } 1230 1231 // first get category - it creates the associated grade item 1232 $course_category = grade_category::fetch_course_category($courseid); 1233 return $course_category->get_grade_item(); 1234 } 1235 1236 /** 1237 * Is grading object editable? 1238 * 1239 * @return bool 1240 */ 1241 public function is_editable() { 1242 return true; 1243 } 1244 1245 /** 1246 * Checks if grade calculated. Returns this object's calculation. 1247 * 1248 * @return bool true if grade item calculated. 1249 */ 1250 public function is_calculated() { 1251 if (empty($this->calculation)) { 1252 return false; 1253 } 1254 1255 /* 1256 * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(), 1257 * we would have to fetch all course grade items to find out the ids. 1258 * Also if user changes the idnumber the formula does not need to be updated. 1259 */ 1260 1261 // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.) 1262 if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) { 1263 $this->set_calculation($this->calculation); 1264 } 1265 1266 return !empty($this->calculation); 1267 } 1268 1269 /** 1270 * Returns calculation string if grade calculated. 1271 * 1272 * @return string Returns the grade item's calculation if calculation is used, null if not 1273 */ 1274 public function get_calculation() { 1275 if ($this->is_calculated()) { 1276 return grade_item::denormalize_formula($this->calculation, $this->courseid); 1277 1278 } else { 1279 return NULL; 1280 } 1281 } 1282 1283 /** 1284 * Sets this item's calculation (creates it) if not yet set, or 1285 * updates it if already set (in the DB). If no calculation is given, 1286 * the calculation is removed. 1287 * 1288 * @param string $formula string representation of formula used for calculation 1289 * @return bool success 1290 */ 1291 public function set_calculation($formula) { 1292 $this->calculation = grade_item::normalize_formula($formula, $this->courseid); 1293 $this->calculation_normalized = true; 1294 return $this->update(); 1295 } 1296 1297 /** 1298 * Denormalizes the calculation formula to [idnumber] form 1299 * 1300 * @param string $formula A string representation of the formula 1301 * @param int $courseid The course ID 1302 * @return string The denormalized formula as a string 1303 */ 1304 public static function denormalize_formula($formula, $courseid) { 1305 if (empty($formula)) { 1306 return ''; 1307 } 1308 1309 // denormalize formula - convert ##giXX## to [[idnumber]] 1310 if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) { 1311 foreach ($matches[1] as $id) { 1312 if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) { 1313 if (!empty($grade_item->idnumber)) { 1314 $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula); 1315 } 1316 } 1317 } 1318 } 1319 1320 return $formula; 1321 1322 } 1323 1324 /** 1325 * Normalizes the calculation formula to [#giXX#] form 1326 * 1327 * @param string $formula The formula 1328 * @param int $courseid The course ID 1329 * @return string The normalized formula as a string 1330 */ 1331 public static function normalize_formula($formula, $courseid) { 1332 $formula = trim($formula); 1333 1334 if (empty($formula)) { 1335 return NULL; 1336 1337 } 1338 1339 // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]] 1340 if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) { 1341 foreach ($grade_items as $grade_item) { 1342 $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula); 1343 } 1344 } 1345 1346 return $formula; 1347 } 1348 1349 /** 1350 * Returns the final values for this grade item (as imported by module or other source). 1351 * 1352 * @param int $userid Optional: to retrieve a single user's final grade 1353 * @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance. 1354 */ 1355 public function get_final($userid=NULL) { 1356 global $DB; 1357 if ($userid) { 1358 if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) { 1359 return $user; 1360 } 1361 1362 } else { 1363 if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) { 1364 //TODO: speed up with better SQL (MDL-31380) 1365 $result = array(); 1366 foreach ($grades as $grade) { 1367 $result[$grade->userid] = $grade; 1368 } 1369 return $result; 1370 } else { 1371 return array(); 1372 } 1373 } 1374 } 1375 1376 /** 1377 * Get (or create if not exist yet) grade for this user 1378 * 1379 * @param int $userid The user ID 1380 * @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted 1381 * @return grade_grade The grade_grade instance for the user for this grade item 1382 */ 1383 public function get_grade($userid, $create=true) { 1384 if (empty($this->id)) { 1385 debugging('Can not use before insert'); 1386 return false; 1387 } 1388 1389 $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id)); 1390 if (empty($grade->id) and $create) { 1391 $grade->insert(); 1392 } 1393 1394 return $grade; 1395 } 1396 1397 /** 1398 * Returns the sortorder of this grade_item. This method is also available in 1399 * grade_category, for cases where the object type is not know. 1400 * 1401 * @return int Sort order 1402 */ 1403 public function get_sortorder() { 1404 return $this->sortorder; 1405 } 1406 1407 /** 1408 * Returns the idnumber of this grade_item. This method is also available in 1409 * grade_category, for cases where the object type is not know. 1410 * 1411 * @return string The grade item idnumber 1412 */ 1413 public function get_idnumber() { 1414 return $this->idnumber; 1415 } 1416 1417 /** 1418 * Returns this grade_item. This method is also available in 1419 * grade_category, for cases where the object type is not know. 1420 * 1421 * @return grade_item 1422 */ 1423 public function get_grade_item() { 1424 return $this; 1425 } 1426 1427 /** 1428 * Sets the sortorder of this grade_item. This method is also available in 1429 * grade_category, for cases where the object type is not know. 1430 * 1431 * @param int $sortorder 1432 */ 1433 public function set_sortorder($sortorder) { 1434 if ($this->sortorder == $sortorder) { 1435 return; 1436 } 1437 $this->sortorder = $sortorder; 1438 $this->update(); 1439 } 1440 1441 /** 1442 * Update this grade item's sortorder so that it will appear after $sortorder 1443 * 1444 * @param int $sortorder The sort order to place this grade item after 1445 */ 1446 public function move_after_sortorder($sortorder) { 1447 global $CFG, $DB; 1448 1449 //make some room first 1450 $params = array($sortorder, $this->courseid); 1451 $sql = "UPDATE {grade_items} 1452 SET sortorder = sortorder + 1 1453 WHERE sortorder > ? AND courseid = ?"; 1454 $DB->execute($sql, $params); 1455 1456 $this->set_sortorder($sortorder + 1); 1457 } 1458 1459 /** 1460 * Detect duplicate grade item's sortorder and re-sort them. 1461 * Note: Duplicate sortorder will be introduced while duplicating activities or 1462 * merging two courses. 1463 * 1464 * @param int $courseid id of the course for which grade_items sortorder need to be fixed. 1465 */ 1466 public static function fix_duplicate_sortorder($courseid) { 1467 global $DB; 1468 1469 $transaction = $DB->start_delegated_transaction(); 1470 1471 $sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder 1472 FROM {grade_items} g1 1473 JOIN {grade_items} g2 ON g1.courseid = g2.courseid 1474 WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid 1475 ORDER BY g1.sortorder DESC, g1.id DESC"; 1476 1477 // Get all duplicates in course highest sort order, and higest id first so that we can make space at the 1478 // bottom higher end of the sort orders and work down by id. 1479 $rs = $DB->get_recordset_sql($sql, array('courseid' => $courseid)); 1480 1481 foreach($rs as $duplicate) { 1482 $DB->execute("UPDATE {grade_items} 1483 SET sortorder = sortorder + 1 1484 WHERE courseid = :courseid AND 1485 (sortorder > :sortorder OR (sortorder = :sortorder2 AND id > :id))", 1486 array('courseid' => $duplicate->courseid, 1487 'sortorder' => $duplicate->sortorder, 1488 'sortorder2' => $duplicate->sortorder, 1489 'id' => $duplicate->id)); 1490 } 1491 $rs->close(); 1492 $transaction->allow_commit(); 1493 } 1494 1495 /** 1496 * Returns the most descriptive field for this object. 1497 * 1498 * Determines what type of grade item it is then returns the appropriate string 1499 * 1500 * @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total" 1501 * @param bool $escape Whether the returned category name is to be HTML escaped or not. 1502 * @return string name 1503 */ 1504 public function get_name($fulltotal=false, $escape = true) { 1505 global $CFG; 1506 require_once($CFG->dirroot . '/course/lib.php'); 1507 if (strval($this->itemname) !== '') { 1508 // MDL-10557 1509 1510 // Make it obvious to users if the course module to which this grade item relates, is currently being removed. 1511 $deletionpending = course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance); 1512 $deletionnotice = get_string('gradesmoduledeletionprefix', 'grades'); 1513 1514 $options = ['context' => context_course::instance($this->courseid), 'escape' => $escape]; 1515 return $deletionpending ? 1516 format_string($deletionnotice . ' ' . $this->itemname, true, $options) : 1517 format_string($this->itemname, true, $options); 1518 1519 } else if ($this->is_course_item()) { 1520 return get_string('coursetotal', 'grades'); 1521 1522 } else if ($this->is_category_item()) { 1523 if ($fulltotal) { 1524 $category = $this->load_parent_category(); 1525 $a = new stdClass(); 1526 $a->category = $category->get_name($escape); 1527 return get_string('categorytotalfull', 'grades', $a); 1528 } else { 1529 return get_string('categorytotal', 'grades'); 1530 } 1531 1532 } else { 1533 return get_string('gradenoun'); 1534 } 1535 } 1536 1537 /** 1538 * A grade item can return a more detailed description which will be added to the header of the column/row in some reports. 1539 * 1540 * @return string description 1541 */ 1542 public function get_description() { 1543 if ($this->is_course_item() || $this->is_category_item()) { 1544 $categoryitem = $this->load_item_category(); 1545 return $categoryitem->get_description(); 1546 } 1547 return ''; 1548 } 1549 1550 /** 1551 * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind. 1552 * 1553 * @param int $parentid The ID of the new parent 1554 * @param bool $updateaggregationfields Whether or not to convert the aggregation fields when switching between category. 1555 * Set this to false when the aggregation fields have been updated in prevision of the new 1556 * category, typically when the item is freshly created. 1557 * @return bool True if success 1558 */ 1559 public function set_parent($parentid, $updateaggregationfields = true) { 1560 if ($this->is_course_item() or $this->is_category_item()) { 1561 throw new \moodle_exception('cannotsetparentforcatoritem'); 1562 } 1563 1564 if ($this->categoryid == $parentid) { 1565 return true; 1566 } 1567 1568 // find parent and check course id 1569 if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) { 1570 return false; 1571 } 1572 1573 $currentparent = $this->load_parent_category(); 1574 1575 if ($updateaggregationfields) { 1576 $this->set_aggregation_fields_for_aggregation($currentparent->aggregation, $parent_category->aggregation); 1577 } 1578 1579 $this->force_regrading(); 1580 1581 // set new parent 1582 $this->categoryid = $parent_category->id; 1583 $this->parent_category =& $parent_category; 1584 1585 return $this->update(); 1586 } 1587 1588 /** 1589 * Update the aggregation fields when the aggregation changed. 1590 * 1591 * This method should always be called when the aggregation has changed, but also when 1592 * the item was moved to another category, even it if uses the same aggregation method. 1593 * 1594 * Some values such as the weight only make sense within a category, once moved the 1595 * values should be reset to let the user adapt them accordingly. 1596 * 1597 * Note that this method does not save the grade item. 1598 * {@link grade_item::update()} has to be called manually after using this method. 1599 * 1600 * @param int $from Aggregation method constant value. 1601 * @param int $to Aggregation method constant value. 1602 * @return boolean True when at least one field was changed, false otherwise 1603 */ 1604 public function set_aggregation_fields_for_aggregation($from, $to) { 1605 $defaults = grade_category::get_default_aggregation_coefficient_values($to); 1606 1607 $origaggregationcoef = $this->aggregationcoef; 1608 $origaggregationcoef2 = $this->aggregationcoef2; 1609 $origweighoverride = $this->weightoverride; 1610 1611 if ($from == GRADE_AGGREGATE_SUM && $to == GRADE_AGGREGATE_SUM && $this->weightoverride) { 1612 // Do nothing. We are switching from SUM to SUM and the weight is overriden, 1613 // a teacher would not expect any change in this situation. 1614 1615 } else if ($from == GRADE_AGGREGATE_WEIGHTED_MEAN && $to == GRADE_AGGREGATE_WEIGHTED_MEAN) { 1616 // Do nothing. The weights can be kept in this case. 1617 1618 } else if (in_array($from, array(GRADE_AGGREGATE_SUM, GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2)) 1619 && in_array($to, array(GRADE_AGGREGATE_SUM, GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))) { 1620 1621 // Reset all but the the extra credit field. 1622 $this->aggregationcoef2 = $defaults['aggregationcoef2']; 1623 $this->weightoverride = $defaults['weightoverride']; 1624 1625 if ($to != GRADE_AGGREGATE_EXTRACREDIT_MEAN) { 1626 // Normalise extra credit, except for 'Mean with extra credit' which supports higher values than 1. 1627 $this->aggregationcoef = min(1, $this->aggregationcoef); 1628 } 1629 } else { 1630 // Reset all. 1631 $this->aggregationcoef = $defaults['aggregationcoef']; 1632 $this->aggregationcoef2 = $defaults['aggregationcoef2']; 1633 $this->weightoverride = $defaults['weightoverride']; 1634 } 1635 1636 $acoefdiff = grade_floats_different($origaggregationcoef, $this->aggregationcoef); 1637 $acoefdiff2 = grade_floats_different($origaggregationcoef2, $this->aggregationcoef2); 1638 $weightoverride = grade_floats_different($origweighoverride, $this->weightoverride); 1639 1640 return $acoefdiff || $acoefdiff2 || $weightoverride; 1641 } 1642 1643 /** 1644 * Makes sure value is a valid grade value. 1645 * 1646 * @param float $gradevalue 1647 * @return mixed float or int fixed grade value 1648 */ 1649 public function bounded_grade($gradevalue) { 1650 global $CFG; 1651 1652 if (is_null($gradevalue)) { 1653 return null; 1654 } 1655 1656 if ($this->gradetype == GRADE_TYPE_SCALE) { 1657 // no >100% grades hack for scale grades! 1658 // 1.5 is rounded to 2 ;-) 1659 return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax); 1660 } 1661 1662 $grademax = $this->grademax; 1663 1664 // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items 1665 $maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by default 1666 1667 if (!empty($CFG->unlimitedgrades)) { 1668 // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items 1669 $grademax = $grademax * $maxcoef; 1670 } else if ($this->is_category_item() or $this->is_course_item()) { 1671 $category = $this->load_item_category(); 1672 if ($category->aggregation >= 100) { 1673 // grade >100% hack 1674 $grademax = $grademax * $maxcoef; 1675 } 1676 } 1677 1678 return (float)bounded_number($this->grademin, $gradevalue, $grademax); 1679 } 1680 1681 /** 1682 * Finds out on which other items does this depend directly when doing calculation or category aggregation 1683 * 1684 * @param bool $reset_cache 1685 * @return array of grade_item IDs this one depends on 1686 */ 1687 public function depends_on($reset_cache=false) { 1688 global $CFG, $DB; 1689 1690 if ($reset_cache) { 1691 $this->dependson_cache = null; 1692 } else if (isset($this->dependson_cache)) { 1693 return $this->dependson_cache; 1694 } 1695 1696 if ($this->is_locked() && !$this->is_category_item()) { 1697 // locked items do not need to be regraded 1698 $this->dependson_cache = array(); 1699 return $this->dependson_cache; 1700 } 1701 1702 if ($this->is_calculated()) { 1703 if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) { 1704 $this->dependson_cache = array_unique($matches[1]); // remove duplicates 1705 return $this->dependson_cache; 1706 } else { 1707 $this->dependson_cache = array(); 1708 return $this->dependson_cache; 1709 } 1710 1711 } else if ($grade_category = $this->load_item_category()) { 1712 $params = array(); 1713 1714 //only items with numeric or scale values can be aggregated 1715 if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) { 1716 $this->dependson_cache = array(); 1717 return $this->dependson_cache; 1718 } 1719 1720 $grade_category->apply_forced_settings(); 1721 1722 if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) { 1723 $outcomes_sql = ""; 1724 } else { 1725 $outcomes_sql = "AND gi.outcomeid IS NULL"; 1726 } 1727 1728 if (empty($CFG->grade_includescalesinaggregation)) { 1729 $gtypes = "gi.gradetype = ?"; 1730 $params[] = GRADE_TYPE_VALUE; 1731 } else { 1732 $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)"; 1733 $params[] = GRADE_TYPE_VALUE; 1734 $params[] = GRADE_TYPE_SCALE; 1735 } 1736 1737 $params[] = $grade_category->id; 1738 $params[] = $this->courseid; 1739 $params[] = $grade_category->id; 1740 $params[] = $this->courseid; 1741 if (empty($CFG->grade_includescalesinaggregation)) { 1742 $params[] = GRADE_TYPE_VALUE; 1743 } else { 1744 $params[] = GRADE_TYPE_VALUE; 1745 $params[] = GRADE_TYPE_SCALE; 1746 } 1747 $sql = "SELECT gi.id 1748 FROM {grade_items} gi 1749 WHERE $gtypes 1750 AND gi.categoryid = ? 1751 AND gi.courseid = ? 1752 $outcomes_sql 1753 UNION 1754 1755 SELECT gi.id 1756 FROM {grade_items} gi, {grade_categories} gc 1757 WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id 1758 AND gc.parent = ? 1759 AND gi.courseid = ? 1760 AND $gtypes 1761 $outcomes_sql"; 1762 1763 if ($children = $DB->get_records_sql($sql, $params)) { 1764 $this->dependson_cache = array_keys($children); 1765 return $this->dependson_cache; 1766 } else { 1767 $this->dependson_cache = array(); 1768 return $this->dependson_cache; 1769 } 1770 1771 } else { 1772 $this->dependson_cache = array(); 1773 return $this->dependson_cache; 1774 } 1775 } 1776 1777 /** 1778 * Refetch grades from modules, plugins. 1779 * 1780 * @param int $userid optional, limit the refetch to a single user 1781 * @return bool Returns true on success or if there is nothing to do 1782 */ 1783 public function refresh_grades($userid=0) { 1784 global $DB; 1785 if ($this->itemtype == 'mod') { 1786 if ($this->is_outcome_item()) { 1787 //nothing to do 1788 return true; 1789 } 1790 1791 if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) { 1792 debugging("Can not find $this->itemmodule activity with id $this->iteminstance"); 1793 return false; 1794 } 1795 1796 if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) { 1797 debugging('Can not find course module'); 1798 return false; 1799 } 1800 1801 $activity->modname = $this->itemmodule; 1802 $activity->cmidnumber = $cm->idnumber; 1803 1804 return grade_update_mod_grades($activity, $userid); 1805 } 1806 1807 return true; 1808 } 1809 1810 /** 1811 * Updates final grade value for given user, this is a only way to update final 1812 * grades from gradebook and import because it logs the change in history table 1813 * and deals with overridden flag. This flag is set to prevent later overriding 1814 * from raw grades submitted from modules. 1815 * 1816 * @param int $userid The graded user 1817 * @param float|false $finalgrade The float value of final grade, false means do not change 1818 * @param string $source The modification source 1819 * @param string $feedback Optional teacher feedback 1820 * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML 1821 * @param int $usermodified The ID of the user making the modification 1822 * @param int $timemodified Optional parameter to set the time modified, if not present current time. 1823 * @param bool $isbulkupdate If bulk grade update is happening. 1824 * @return bool success 1825 */ 1826 public function update_final_grade($userid, $finalgrade = false, $source = null, $feedback = false, 1827 $feedbackformat = FORMAT_MOODLE, $usermodified = null, $timemodified = null, $isbulkupdate = false) { 1828 global $USER, $CFG; 1829 1830 $result = true; 1831 1832 // no grading used or locked 1833 if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) { 1834 return false; 1835 } 1836 1837 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid)); 1838 $grade->grade_item =& $this; // prevent db fetching of this grade_item 1839 1840 if (empty($usermodified)) { 1841 $grade->usermodified = $USER->id; 1842 } else { 1843 $grade->usermodified = $usermodified; 1844 } 1845 1846 if ($grade->is_locked()) { 1847 // do not update locked grades at all 1848 return false; 1849 } 1850 1851 $locktime = $grade->get_locktime(); 1852 if ($locktime and $locktime < time()) { 1853 // do not update grades that should be already locked, force regrade instead 1854 $this->force_regrading(); 1855 return false; 1856 } 1857 1858 $oldgrade = new stdClass(); 1859 $oldgrade->finalgrade = $grade->finalgrade; 1860 $oldgrade->overridden = $grade->overridden; 1861 $oldgrade->feedback = $grade->feedback; 1862 $oldgrade->feedbackformat = $grade->feedbackformat; 1863 $oldgrade->rawgrademin = $grade->rawgrademin; 1864 $oldgrade->rawgrademax = $grade->rawgrademax; 1865 1866 // MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly. 1867 $grade->rawgrademin = $this->grademin; 1868 $grade->rawgrademax = $this->grademax; 1869 $grade->rawscaleid = $this->scaleid; 1870 1871 // changed grade? 1872 if ($finalgrade !== false) { 1873 if ($this->is_overridable_item() && $this->markasoverriddenwhengraded) { 1874 $grade->overridden = time(); 1875 } 1876 1877 $grade->finalgrade = $this->bounded_grade($finalgrade); 1878 } 1879 1880 // do we have comment from teacher? 1881 if ($feedback !== false) { 1882 if ($this->is_overridable_item_feedback()) { 1883 // external items (modules, plugins) may have own feedback 1884 $grade->overridden = time(); 1885 } 1886 1887 $grade->feedback = $feedback; 1888 $grade->feedbackformat = $feedbackformat; 1889 } 1890 1891 $gradechanged = false; 1892 if (empty($grade->id)) { 1893 $grade->timecreated = null; // Hack alert - date submitted - no submission yet. 1894 $grade->timemodified = $timemodified ?? time(); // Hack alert - date graded. 1895 $result = (bool)$grade->insert($source, $isbulkupdate); 1896 1897 // If the grade insert was successful and the final grade was not null then trigger a user_graded event. 1898 if ($result && !is_null($grade->finalgrade)) { 1899 \core\event\user_graded::create_from_grade($grade)->trigger(); 1900 } 1901 $gradechanged = true; 1902 } else { 1903 // Existing grade_grades. 1904 1905 if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade) 1906 or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin) 1907 or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax) 1908 or ($oldgrade->overridden == 0 and $grade->overridden > 0)) { 1909 $gradechanged = true; 1910 } 1911 1912 if ($grade->feedback === $oldgrade->feedback and $grade->feedbackformat == $oldgrade->feedbackformat and 1913 $gradechanged === false) { 1914 // No grade nor feedback changed. 1915 return $result; 1916 } 1917 1918 $grade->timemodified = $timemodified ?? time(); // Hack alert - date graded. 1919 $result = $grade->update($source, $isbulkupdate); 1920 1921 // If the grade update was successful and the actual grade has changed then trigger a user_graded event. 1922 if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) { 1923 \core\event\user_graded::create_from_grade($grade)->trigger(); 1924 } 1925 } 1926 1927 if (!$result) { 1928 // Something went wrong - better force final grade recalculation. 1929 $this->force_regrading(); 1930 return $result; 1931 } 1932 1933 // If we are not updating grades we don't need to recalculate the whole course. 1934 if (!$gradechanged) { 1935 return $result; 1936 } 1937 1938 if ($this->is_course_item() and !$this->needsupdate) { 1939 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) { 1940 $this->force_regrading(); 1941 } 1942 1943 } else if (!$this->needsupdate) { 1944 1945 $course_item = grade_item::fetch_course_item($this->courseid); 1946 if (!$course_item->needsupdate) { 1947 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) { 1948 $this->force_regrading(); 1949 } 1950 } else { 1951 $this->force_regrading(); 1952 } 1953 } 1954 1955 return $result; 1956 } 1957 1958 1959 /** 1960 * Updates raw grade value for given user, this is a only way to update raw 1961 * grades from external source (modules, etc.), 1962 * because it logs the change in history table and deals with final grade recalculation. 1963 * 1964 * @param int $userid the graded user 1965 * @param mixed $rawgrade float value of raw grade - false means do not change 1966 * @param string $source modification source 1967 * @param string $feedback optional teacher feedback 1968 * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML 1969 * @param int $usermodified the ID of the user who did the grading 1970 * @param int $dategraded A timestamp of when the student's work was graded 1971 * @param int $datesubmitted A timestamp of when the student's work was submitted 1972 * @param grade_grade $grade A grade object, useful for bulk upgrades 1973 * @param array $feedbackfiles An array identifying the location of files we want to copy to the gradebook feedback area. 1974 * Example - 1975 * [ 1976 * 'contextid' => 1, 1977 * 'component' => 'mod_xyz', 1978 * 'filearea' => 'mod_xyz_feedback', 1979 * 'itemid' => 2 1980 * ]; 1981 * @param bool $isbulkupdate If bulk grade update is happening. 1982 * @return bool success 1983 */ 1984 public function update_raw_grade($userid, $rawgrade = false, $source = null, $feedback = false, 1985 $feedbackformat = FORMAT_MOODLE, $usermodified = null, $dategraded = null, $datesubmitted=null, 1986 $grade = null, array $feedbackfiles = [], $isbulkupdate = false) { 1987 global $USER; 1988 1989 $result = true; 1990 1991 // calculated grades can not be updated; course and category can not be updated because they are aggregated 1992 if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) { 1993 return false; 1994 } 1995 1996 if (is_null($grade)) { 1997 //fetch from db 1998 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid)); 1999 } 2000 $grade->grade_item =& $this; // prevent db fetching of this grade_item 2001 2002 if (empty($usermodified)) { 2003 $grade->usermodified = $USER->id; 2004 } else { 2005 $grade->usermodified = $usermodified; 2006 } 2007 2008 if ($grade->is_locked()) { 2009 // do not update locked grades at all 2010 return false; 2011 } 2012 2013 $locktime = $grade->get_locktime(); 2014 if ($locktime and $locktime < time()) { 2015 // do not update grades that should be already locked and force regrade 2016 $this->force_regrading(); 2017 return false; 2018 } 2019 2020 $oldgrade = new stdClass(); 2021 $oldgrade->finalgrade = $grade->finalgrade; 2022 $oldgrade->rawgrade = $grade->rawgrade; 2023 $oldgrade->rawgrademin = $grade->rawgrademin; 2024 $oldgrade->rawgrademax = $grade->rawgrademax; 2025 $oldgrade->rawscaleid = $grade->rawscaleid; 2026 $oldgrade->feedback = $grade->feedback; 2027 $oldgrade->feedbackformat = $grade->feedbackformat; 2028 2029 // use new min and max 2030 $grade->rawgrade = $grade->rawgrade; 2031 $grade->rawgrademin = $this->grademin; 2032 $grade->rawgrademax = $this->grademax; 2033 $grade->rawscaleid = $this->scaleid; 2034 2035 // change raw grade? 2036 if ($rawgrade !== false) { 2037 $grade->rawgrade = $rawgrade; 2038 } 2039 2040 // empty feedback means no feedback at all 2041 if ($feedback === '') { 2042 $feedback = null; 2043 } 2044 2045 // do we have comment from teacher? 2046 if ($feedback !== false and !$grade->is_overridden()) { 2047 $grade->feedback = $feedback; 2048 $grade->feedbackformat = $feedbackformat; 2049 $grade->feedbackfiles = $feedbackfiles; 2050 } 2051 2052 // update final grade if possible 2053 if (!$grade->is_locked() and !$grade->is_overridden()) { 2054 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax); 2055 } 2056 2057 // TODO: hack alert - create new fields for these in 2.0 2058 $oldgrade->timecreated = $grade->timecreated; 2059 $oldgrade->timemodified = $grade->timemodified; 2060 2061 $grade->timecreated = $datesubmitted; 2062 2063 if ($grade->is_overridden()) { 2064 // keep original graded date - update_final_grade() sets this for overridden grades 2065 2066 } else if (is_null($grade->rawgrade) and is_null($grade->feedback)) { 2067 // no grade and feedback means no grading yet 2068 $grade->timemodified = null; 2069 2070 } else if (!empty($dategraded)) { 2071 // fine - module sends info when graded (yay!) 2072 $grade->timemodified = $dategraded; 2073 2074 } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade) 2075 or $grade->feedback !== $oldgrade->feedback) { 2076 // guess - if either grade or feedback changed set new graded date 2077 $grade->timemodified = time(); 2078 2079 } else { 2080 //keep original graded date 2081 } 2082 // end of hack alert 2083 2084 $gradechanged = false; 2085 if (empty($grade->id)) { 2086 $result = (bool)$grade->insert($source, $isbulkupdate); 2087 2088 // If the grade insert was successful and the final grade was not null then trigger a user_graded event. 2089 if ($result && !is_null($grade->finalgrade)) { 2090 \core\event\user_graded::create_from_grade($grade)->trigger(); 2091 } 2092 $gradechanged = true; 2093 } else { 2094 // Existing grade_grades. 2095 2096 if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade) 2097 or grade_floats_different($grade->rawgrade, $oldgrade->rawgrade) 2098 or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin) 2099 or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax) 2100 or $grade->rawscaleid != $oldgrade->rawscaleid) { 2101 $gradechanged = true; 2102 } 2103 2104 // The timecreated and timemodified checking is part of the hack above. 2105 if ($gradechanged === false and 2106 $grade->feedback === $oldgrade->feedback and 2107 $grade->feedbackformat == $oldgrade->feedbackformat and 2108 $grade->timecreated == $oldgrade->timecreated and 2109 $grade->timemodified == $oldgrade->timemodified) { 2110 // No changes. 2111 return $result; 2112 } 2113 $result = $grade->update($source, $isbulkupdate); 2114 2115 // If the grade update was successful and the actual grade has changed then trigger a user_graded event. 2116 if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) { 2117 \core\event\user_graded::create_from_grade($grade)->trigger(); 2118 } 2119 } 2120 2121 if (!$result) { 2122 // Something went wrong - better force final grade recalculation. 2123 $this->force_regrading(); 2124 return $result; 2125 } 2126 2127 // If we are not updating grades we don't need to recalculate the whole course. 2128 if (!$gradechanged) { 2129 return $result; 2130 } 2131 2132 if (!$this->needsupdate) { 2133 $course_item = grade_item::fetch_course_item($this->courseid); 2134 if (!$course_item->needsupdate) { 2135 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) { 2136 $this->force_regrading(); 2137 } 2138 } 2139 } 2140 2141 return $result; 2142 } 2143 2144 /** 2145 * Calculates final grade values using the formula in the calculation property. 2146 * The parameters are taken from final grades of grade items in current course only. 2147 * 2148 * @param int $userid Supply a user ID to limit the calculations to the grades of a single user 2149 * @return bool false if error 2150 */ 2151 public function compute($userid=null) { 2152 global $CFG, $DB; 2153 2154 if (!$this->is_calculated()) { 2155 return false; 2156 } 2157 2158 require_once($CFG->libdir.'/mathslib.php'); 2159 2160 if ($this->is_locked()) { 2161 return true; // no need to recalculate locked items 2162 } 2163 2164 // Precreate grades - we need them to exist 2165 if ($userid) { 2166 $missing = array(); 2167 if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id, 'userid'=>$userid))) { 2168 $m = new stdClass(); 2169 $m->userid = $userid; 2170 $missing[] = $m; 2171 } 2172 } else { 2173 // Find any users who have grades for some but not all grade items in this course 2174 $params = array('gicourseid' => $this->courseid, 'ggitemid' => $this->id); 2175 $sql = "SELECT gg.userid 2176 FROM {grade_grades} gg 2177 JOIN {grade_items} gi 2178 ON (gi.id = gg.itemid AND gi.courseid = :gicourseid) 2179 GROUP BY gg.userid 2180 HAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0"; 2181 $missing = $DB->get_records_sql($sql, $params); 2182 } 2183 2184 if ($missing) { 2185 foreach ($missing as $m) { 2186 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false); 2187 $grade->grade_item =& $this; 2188 $grade->insert('system'); 2189 } 2190 } 2191 2192 // get used items 2193 $useditems = $this->depends_on(); 2194 2195 // prepare formula and init maths library 2196 $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation); 2197 if (strpos($formula, '[[') !== false) { 2198 // missing item 2199 return false; 2200 } 2201 $this->formula = new calc_formula($formula); 2202 2203 // where to look for final grades? 2204 // this itemid is added so that we use only one query for source and final grades 2205 $gis = array_merge($useditems, array($this->id)); 2206 list($usql, $params) = $DB->get_in_or_equal($gis); 2207 2208 if ($userid) { 2209 $usersql = "AND g.userid=?"; 2210 $params[] = $userid; 2211 } else { 2212 $usersql = ""; 2213 } 2214 2215 $grade_inst = new grade_grade(); 2216 $fields = 'g.'.implode(',g.', $grade_inst->required_fields); 2217 2218 $params[] = $this->courseid; 2219 $sql = "SELECT $fields 2220 FROM {grade_grades} g, {grade_items} gi 2221 WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=? 2222 ORDER BY g.userid"; 2223 2224 $return = true; 2225 2226 // group the grades by userid and use formula on the group 2227 $rs = $DB->get_recordset_sql($sql, $params); 2228 if ($rs->valid()) { 2229 $prevuser = 0; 2230 $grade_records = array(); 2231 $oldgrade = null; 2232 foreach ($rs as $used) { 2233 if ($used->userid != $prevuser) { 2234 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) { 2235 $return = false; 2236 } 2237 $prevuser = $used->userid; 2238 $grade_records = array(); 2239 $oldgrade = null; 2240 } 2241 if ($used->itemid == $this->id) { 2242 $oldgrade = $used; 2243 } 2244 $grade_records['gi'.$used->itemid] = $used->finalgrade; 2245 } 2246 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) { 2247 $return = false; 2248 } 2249 } 2250 $rs->close(); 2251 2252 return $return; 2253 } 2254 2255 /** 2256 * Internal function that does the final grade calculation 2257 * 2258 * @param int $userid The user ID 2259 * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade 2260 * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID 2261 * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database 2262 * @return bool False if an error occurred 2263 */ 2264 public function use_formula($userid, $params, $useditems, $oldgrade) { 2265 if (empty($userid)) { 2266 return true; 2267 } 2268 2269 // add missing final grade values 2270 // not graded (null) is counted as 0 - the spreadsheet way 2271 $allinputsnull = true; 2272 foreach($useditems as $gi) { 2273 if (!array_key_exists('gi'.$gi, $params) || is_null($params['gi'.$gi])) { 2274 $params['gi'.$gi] = 0; 2275 } else { 2276 $params['gi'.$gi] = (float)$params['gi'.$gi]; 2277 if ($gi != $this->id) { 2278 $allinputsnull = false; 2279 } 2280 } 2281 } 2282 2283 // can not use own final grade during calculation 2284 unset($params['gi'.$this->id]); 2285 2286 // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they 2287 // wish to update the grades. 2288 $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid); 2289 2290 $rawminandmaxchanged = false; 2291 // insert final grade - will be needed later anyway 2292 if ($oldgrade) { 2293 // Only run through this code if the gradebook isn't frozen. 2294 if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) { 2295 // Do nothing. 2296 } else { 2297 // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the 2298 // grade_item grade maximum and minimum respectively. 2299 if ($oldgrade->rawgrademax != $this->grademax || $oldgrade->rawgrademin != $this->grademin) { 2300 $rawminandmaxchanged = true; 2301 $oldgrade->rawgrademax = $this->grademax; 2302 $oldgrade->rawgrademin = $this->grademin; 2303 } 2304 } 2305 $oldfinalgrade = $oldgrade->finalgrade; 2306 $grade = new grade_grade($oldgrade, false); // fetching from db is not needed 2307 $grade->grade_item =& $this; 2308 2309 } else { 2310 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false); 2311 $grade->grade_item =& $this; 2312 $rawminandmaxchanged = false; 2313 if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) { 2314 // Do nothing. 2315 } else { 2316 // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the 2317 // grade_item grade maximum and minimum respectively. 2318 $rawminandmaxchanged = true; 2319 $grade->rawgrademax = $this->grademax; 2320 $grade->rawgrademin = $this->grademin; 2321 } 2322 $grade->insert('system'); 2323 $oldfinalgrade = null; 2324 } 2325 2326 // no need to recalculate locked or overridden grades 2327 if ($grade->is_locked() or $grade->is_overridden()) { 2328 return true; 2329 } 2330 2331 if ($allinputsnull) { 2332 $grade->finalgrade = null; 2333 $result = true; 2334 2335 } else { 2336 2337 // do the calculation 2338 $this->formula->set_params($params); 2339 $result = $this->formula->evaluate(); 2340 2341 if ($result === false) { 2342 $grade->finalgrade = null; 2343 2344 } else { 2345 // normalize 2346 $grade->finalgrade = $this->bounded_grade($result); 2347 } 2348 } 2349 2350 // Only run through this code if the gradebook isn't frozen. 2351 if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) { 2352 // Update in db if changed. 2353 if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { 2354 $grade->timemodified = time(); 2355 $success = $grade->update('compute'); 2356 2357 // If successful trigger a user_graded event. 2358 if ($success) { 2359 \core\event\user_graded::create_from_grade($grade)->trigger(); 2360 } 2361 } 2362 } else { 2363 // Update in db if changed. 2364 if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || $rawminandmaxchanged) { 2365 $grade->timemodified = time(); 2366 $success = $grade->update('compute'); 2367 2368 // If successful trigger a user_graded event. 2369 if ($success) { 2370 \core\event\user_graded::create_from_grade($grade)->trigger(); 2371 } 2372 } 2373 } 2374 2375 if ($result !== false) { 2376 //lock grade if needed 2377 } 2378 2379 if ($result === false) { 2380 return false; 2381 } else { 2382 return true; 2383 } 2384 2385 } 2386 2387 /** 2388 * Validate the formula. 2389 * 2390 * @param string $formulastr 2391 * @return bool true if calculation possible, false otherwise 2392 */ 2393 public function validate_formula($formulastr) { 2394 global $CFG, $DB; 2395 require_once($CFG->libdir.'/mathslib.php'); 2396 2397 $formulastr = grade_item::normalize_formula($formulastr, $this->courseid); 2398 2399 if (empty($formulastr)) { 2400 return true; 2401 } 2402 2403 if (strpos($formulastr, '=') !== 0) { 2404 return get_string('errorcalculationnoequal', 'grades'); 2405 } 2406 2407 // get used items 2408 if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) { 2409 $useditems = array_unique($matches[1]); // remove duplicates 2410 } else { 2411 $useditems = array(); 2412 } 2413 2414 // MDL-11902 2415 // unset the value if formula is trying to reference to itself 2416 // but array keys does not match itemid 2417 if (!empty($this->id)) { 2418 $useditems = array_diff($useditems, array($this->id)); 2419 //unset($useditems[$this->id]); 2420 } 2421 2422 // prepare formula and init maths library 2423 $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr); 2424 $formula = new calc_formula($formula); 2425 2426 2427 if (empty($useditems)) { 2428 $grade_items = array(); 2429 2430 } else { 2431 list($usql, $params) = $DB->get_in_or_equal($useditems); 2432 $params[] = $this->courseid; 2433 $sql = "SELECT gi.* 2434 FROM {grade_items} gi 2435 WHERE gi.id $usql and gi.courseid=?"; // from the same course only! 2436 2437 if (!$grade_items = $DB->get_records_sql($sql, $params)) { 2438 $grade_items = array(); 2439 } 2440 } 2441 2442 $params = array(); 2443 foreach ($useditems as $itemid) { 2444 // make sure all grade items exist in this course 2445 if (!array_key_exists($itemid, $grade_items)) { 2446 return false; 2447 } 2448 // use max grade when testing formula, this should be ok in 99.9% 2449 // division by 0 is one of possible problems 2450 $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax; 2451 } 2452 2453 // do the calculation 2454 $formula->set_params($params); 2455 $result = $formula->evaluate(); 2456 2457 // false as result indicates some problem 2458 if ($result === false) { 2459 // TODO: add more error hints 2460 return get_string('errorcalculationunknown', 'grades'); 2461 } else { 2462 return true; 2463 } 2464 } 2465 2466 /** 2467 * Returns the value of the display type 2468 * 2469 * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones. 2470 * 2471 * @return int Display type 2472 */ 2473 public function get_displaytype() { 2474 global $CFG; 2475 2476 if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) { 2477 return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype); 2478 2479 } else { 2480 return $this->display; 2481 } 2482 } 2483 2484 /** 2485 * Returns the value of the decimals field 2486 * 2487 * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones. 2488 * 2489 * @return int Decimals (0 - 5) 2490 */ 2491 public function get_decimals() { 2492 global $CFG; 2493 2494 if (is_null($this->decimals)) { 2495 return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints); 2496 2497 } else { 2498 return $this->decimals; 2499 } 2500 } 2501 2502 /** 2503 * Returns a string representing the range of grademin - grademax for this grade item. 2504 * 2505 * @param int $rangesdisplaytype 2506 * @param int $rangesdecimalpoints 2507 * @return string 2508 */ 2509 function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) { 2510 2511 global $USER; 2512 2513 // Determine which display type to use for this average 2514 if (isset($USER->editing) && $USER->editing) { 2515 $displaytype = GRADE_DISPLAY_TYPE_REAL; 2516 2517 } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs 2518 $displaytype = $this->get_displaytype(); 2519 2520 } else { 2521 $displaytype = $rangesdisplaytype; 2522 } 2523 2524 // Override grade_item setting if a display preference (not default) was set for the averages 2525 if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) { 2526 $decimalpoints = $this->get_decimals(); 2527 2528 } else { 2529 $decimalpoints = $rangesdecimalpoints; 2530 } 2531 2532 if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) { 2533 $grademin = "0 %"; 2534 $grademax = "100 %"; 2535 2536 } else { 2537 $grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints); 2538 $grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints); 2539 } 2540 2541 return $grademin.'–'. $grademax; 2542 } 2543 2544 /** 2545 * Queries parent categories recursively to find the aggregationcoef type that applies to this grade item. 2546 * 2547 * @return string|false Returns the coefficient string of false is no coefficient is being used 2548 */ 2549 public function get_coefstring() { 2550 $parent_category = $this->load_parent_category(); 2551 if ($this->is_category_item()) { 2552 $parent_category = $parent_category->load_parent_category(); 2553 } 2554 2555 if ($parent_category->is_aggregationcoef_used()) { 2556 return $parent_category->get_coefstring(); 2557 } else { 2558 return false; 2559 } 2560 } 2561 2562 /** 2563 * Returns whether the grade item can control the visibility of the grades 2564 * 2565 * @return bool 2566 */ 2567 public function can_control_visibility() { 2568 if (core_component::get_plugin_directory($this->itemtype, $this->itemmodule)) { 2569 return !plugin_supports($this->itemtype, $this->itemmodule, FEATURE_CONTROLS_GRADE_VISIBILITY, false); 2570 } 2571 return parent::can_control_visibility(); 2572 } 2573 2574 /** 2575 * Used to notify the completion system (if necessary) that a user's grade 2576 * has changed, and clear up a possible score cache. 2577 * 2578 * @param bool $deleted True if grade was actually deleted 2579 */ 2580 protected function notify_changed($deleted) { 2581 global $CFG; 2582 2583 // Condition code may cache the grades for conditional availability of 2584 // modules or sections. (This code should use a hook for communication 2585 // with plugin, but hooks are not implemented at time of writing.) 2586 if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) { 2587 \availability_grade\callbacks::grade_item_changed($this->courseid); 2588 } 2589 } 2590 2591 /** 2592 * Helper function to get the accurate context for this grade column. 2593 * 2594 * @return context 2595 */ 2596 public function get_context() { 2597 if ($this->itemtype == 'mod') { 2598 $modinfo = get_fast_modinfo($this->courseid); 2599 // Sometimes the course module cache is out of date and needs to be rebuilt. 2600 if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) { 2601 rebuild_course_cache($this->courseid, true); 2602 $modinfo = get_fast_modinfo($this->courseid); 2603 } 2604 // Even with a rebuilt cache the module does not exist. This means the 2605 // database is in an invalid state - we will log an error and return 2606 // the course context but the calling code should be updated. 2607 if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) { 2608 mtrace(get_string('moduleinstancedoesnotexist', 'error')); 2609 $context = \context_course::instance($this->courseid); 2610 } else { 2611 $cm = $modinfo->instances[$this->itemmodule][$this->iteminstance]; 2612 $context = \context_module::instance($cm->id); 2613 } 2614 } else { 2615 $context = \context_course::instance($this->courseid); 2616 } 2617 return $context; 2618 } 2619 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body