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