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