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