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