Differences Between: [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]
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 category 19 * 20 * @package core_grades 21 * @copyright 2006 Nicolas Connault 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 defined('MOODLE_INTERNAL') || die(); 26 27 require_once (__DIR__ . '/grade_object.php'); 28 29 /** 30 * grade_category is an object mapped to DB table {prefix}grade_categories 31 * 32 * @package core_grades 33 * @category grade 34 * @copyright 2007 Nicolas Connault 35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 */ 37 class grade_category extends grade_object { 38 /** 39 * The DB table. 40 * @var string $table 41 */ 42 public $table = 'grade_categories'; 43 44 /** 45 * Array of required table fields, must start with 'id'. 46 * @var array $required_fields 47 */ 48 public $required_fields = array('id', 'courseid', 'parent', 'depth', 'path', 'fullname', 'aggregation', 49 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes', 50 'timecreated', 'timemodified', 'hidden'); 51 52 /** 53 * The course this category belongs to. 54 * @var int $courseid 55 */ 56 public $courseid; 57 58 /** 59 * The category this category belongs to (optional). 60 * @var int $parent 61 */ 62 public $parent; 63 64 /** 65 * The grade_category object referenced by $this->parent (PK). 66 * @var grade_category $parent_category 67 */ 68 public $parent_category; 69 70 /** 71 * The number of parents this category has. 72 * @var int $depth 73 */ 74 public $depth = 0; 75 76 /** 77 * Shows the hierarchical path for this category as /1/2/3/ (like course_categories), the last number being 78 * this category's autoincrement ID number. 79 * @var string $path 80 */ 81 public $path; 82 83 /** 84 * The name of this category. 85 * @var string $fullname 86 */ 87 public $fullname; 88 89 /** 90 * A constant pointing to one of the predefined aggregation strategies (none, mean, median, sum etc) . 91 * @var int $aggregation 92 */ 93 public $aggregation = GRADE_AGGREGATE_SUM; 94 95 /** 96 * Keep only the X highest items. 97 * @var int $keephigh 98 */ 99 public $keephigh = 0; 100 101 /** 102 * Drop the X lowest items. 103 * @var int $droplow 104 */ 105 public $droplow = 0; 106 107 /** 108 * Aggregate only graded items 109 * @var int $aggregateonlygraded 110 */ 111 public $aggregateonlygraded = 0; 112 113 /** 114 * Aggregate outcomes together with normal items 115 * @var int $aggregateoutcomes 116 */ 117 public $aggregateoutcomes = 0; 118 119 /** 120 * Array of grade_items or grade_categories nested exactly 1 level below this category 121 * @var array $children 122 */ 123 public $children; 124 125 /** 126 * A hierarchical array of all children below this category. This is stored separately from 127 * $children because it is more memory-intensive and may not be used as often. 128 * @var array $all_children 129 */ 130 public $all_children; 131 132 /** 133 * An associated grade_item object, with itemtype=category, used to calculate and cache a set of grade values 134 * for this category. 135 * @var grade_item $grade_item 136 */ 137 public $grade_item; 138 139 /** 140 * Temporary sortorder for speedup of children resorting 141 * @var int $sortorder 142 */ 143 public $sortorder; 144 145 /** 146 * List of options which can be "forced" from site settings. 147 * @var array $forceable 148 */ 149 public $forceable = array('aggregation', 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes'); 150 151 /** 152 * String representing the aggregation coefficient. Variable is used as cache. 153 * @var string $coefstring 154 */ 155 public $coefstring = null; 156 157 /** 158 * Static variable storing the result from {@link self::can_apply_limit_rules}. 159 * @var bool 160 */ 161 protected $canapplylimitrules; 162 163 /** 164 * Builds this category's path string based on its parents (if any) and its own id number. 165 * This is typically done just before inserting this object in the DB for the first time, 166 * or when a new parent is added or changed. It is a recursive function: once the calling 167 * object no longer has a parent, the path is complete. 168 * 169 * @param grade_category $grade_category A Grade_Category object 170 * @return string The category's path string 171 */ 172 public static function build_path($grade_category) { 173 global $DB; 174 175 if (empty($grade_category->parent)) { 176 return '/'.$grade_category->id.'/'; 177 178 } else { 179 $parent = $DB->get_record('grade_categories', array('id' => $grade_category->parent)); 180 return grade_category::build_path($parent).$grade_category->id.'/'; 181 } 182 } 183 184 /** 185 * Finds and returns a grade_category instance based on params. 186 * 187 * @param array $params associative arrays varname=>value 188 * @return grade_category The retrieved grade_category instance or false if none found. 189 */ 190 public static function fetch($params) { 191 if ($records = self::retrieve_record_set($params)) { 192 return reset($records); 193 } 194 195 $record = grade_object::fetch_helper('grade_categories', 'grade_category', $params); 196 197 // We store it as an array to keep a key => result set interface in the cache, grade_object::fetch_helper is 198 // managing exceptions. We return only the first element though. 199 $records = false; 200 if ($record) { 201 $records = array($record->id => $record); 202 } 203 204 self::set_record_set($params, $records); 205 206 return $record; 207 } 208 209 /** 210 * Finds and returns all grade_category instances based on params. 211 * 212 * @param array $params associative arrays varname=>value 213 * @return array array of grade_category insatnces or false if none found. 214 */ 215 public static function fetch_all($params) { 216 if ($records = self::retrieve_record_set($params)) { 217 return $records; 218 } 219 220 $records = grade_object::fetch_all_helper('grade_categories', 'grade_category', $params); 221 self::set_record_set($params, $records); 222 223 return $records; 224 } 225 226 /** 227 * In addition to update() as defined in grade_object, call force_regrading of parent categories, if applicable. 228 * 229 * @param string $source from where was the object updated (mod/forum, manual, etc.) 230 * @return bool success 231 */ 232 public function update($source=null) { 233 // load the grade item or create a new one 234 $this->load_grade_item(); 235 236 // force recalculation of path; 237 if (empty($this->path)) { 238 $this->path = grade_category::build_path($this); 239 $this->depth = substr_count($this->path, '/') - 1; 240 $updatechildren = true; 241 242 } else { 243 $updatechildren = false; 244 } 245 246 $this->apply_forced_settings(); 247 248 // these are exclusive 249 if ($this->droplow > 0) { 250 $this->keephigh = 0; 251 252 } else if ($this->keephigh > 0) { 253 $this->droplow = 0; 254 } 255 256 // Recalculate grades if needed 257 if ($this->qualifies_for_regrading()) { 258 $this->force_regrading(); 259 } 260 261 $this->timemodified = time(); 262 263 $result = parent::update($source); 264 265 // now update paths in all child categories 266 if ($result and $updatechildren) { 267 268 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) { 269 270 foreach ($children as $child) { 271 $child->path = null; 272 $child->depth = 0; 273 $child->update($source); 274 } 275 } 276 } 277 278 return $result; 279 } 280 281 /** 282 * If parent::delete() is successful, send force_regrading message to parent category. 283 * 284 * @param string $source from where was the object deleted (mod/forum, manual, etc.) 285 * @return bool success 286 */ 287 public function delete($source=null) { 288 global $DB; 289 290 $transaction = $DB->start_delegated_transaction(); 291 $grade_item = $this->load_grade_item(); 292 293 if ($this->is_course_category()) { 294 295 if ($categories = grade_category::fetch_all(array('courseid'=>$this->courseid))) { 296 297 foreach ($categories as $category) { 298 299 if ($category->id == $this->id) { 300 continue; // do not delete course category yet 301 } 302 $category->delete($source); 303 } 304 } 305 306 if ($items = grade_item::fetch_all(array('courseid'=>$this->courseid))) { 307 308 foreach ($items as $item) { 309 310 if ($item->id == $grade_item->id) { 311 continue; // do not delete course item yet 312 } 313 $item->delete($source); 314 } 315 } 316 317 } else { 318 $this->force_regrading(); 319 320 $parent = $this->load_parent_category(); 321 322 // Update children's categoryid/parent field first 323 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) { 324 foreach ($children as $child) { 325 $child->set_parent($parent->id); 326 } 327 } 328 329 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) { 330 foreach ($children as $child) { 331 $child->set_parent($parent->id); 332 } 333 } 334 } 335 336 // first delete the attached grade item and grades 337 $grade_item->delete($source); 338 339 // delete category itself 340 $success = parent::delete($source); 341 342 $transaction->allow_commit(); 343 return $success; 344 } 345 346 /** 347 * In addition to the normal insert() defined in grade_object, this method sets the depth 348 * and path for this object, and update the record accordingly. 349 * 350 * We do this here instead of in the constructor as they both need to know the record's 351 * ID number, which only gets created at insertion time. 352 * This method also creates an associated grade_item if this wasn't done during construction. 353 * 354 * @param string $source from where was the object inserted (mod/forum, manual, etc.) 355 * @return int PK ID if successful, false otherwise 356 */ 357 public function insert($source=null) { 358 359 if (empty($this->courseid)) { 360 print_error('cannotinsertgrade'); 361 } 362 363 if (empty($this->parent)) { 364 $course_category = grade_category::fetch_course_category($this->courseid); 365 $this->parent = $course_category->id; 366 } 367 368 $this->path = null; 369 370 $this->timecreated = $this->timemodified = time(); 371 372 if (!parent::insert($source)) { 373 debugging("Could not insert this category: " . print_r($this, true)); 374 return false; 375 } 376 377 $this->force_regrading(); 378 379 // build path and depth 380 $this->update($source); 381 382 return $this->id; 383 } 384 385 /** 386 * Internal function - used only from fetch_course_category() 387 * Normal insert() can not be used for course category 388 * 389 * @param int $courseid The course ID 390 * @return int The ID of the new course category 391 */ 392 public function insert_course_category($courseid) { 393 $this->courseid = $courseid; 394 $this->fullname = '?'; 395 $this->path = null; 396 $this->parent = null; 397 $this->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; 398 399 $this->apply_default_settings(); 400 $this->apply_forced_settings(); 401 402 $this->timecreated = $this->timemodified = time(); 403 404 if (!parent::insert('system')) { 405 debugging("Could not insert this category: " . print_r($this, true)); 406 return false; 407 } 408 409 // build path and depth 410 $this->update('system'); 411 412 return $this->id; 413 } 414 415 /** 416 * Compares the values held by this object with those of the matching record in DB, and returns 417 * whether or not these differences are sufficient to justify an update of all parent objects. 418 * This assumes that this object has an ID number and a matching record in DB. If not, it will return false. 419 * 420 * @return bool 421 */ 422 public function qualifies_for_regrading() { 423 if (empty($this->id)) { 424 debugging("Can not regrade non existing category"); 425 return false; 426 } 427 428 $db_item = grade_category::fetch(array('id'=>$this->id)); 429 430 $aggregationdiff = $db_item->aggregation != $this->aggregation; 431 $keephighdiff = $db_item->keephigh != $this->keephigh; 432 $droplowdiff = $db_item->droplow != $this->droplow; 433 $aggonlygrddiff = $db_item->aggregateonlygraded != $this->aggregateonlygraded; 434 $aggoutcomesdiff = $db_item->aggregateoutcomes != $this->aggregateoutcomes; 435 436 return ($aggregationdiff || $keephighdiff || $droplowdiff || $aggonlygrddiff || $aggoutcomesdiff); 437 } 438 439 /** 440 * Marks this grade categories' associated grade item as needing regrading 441 */ 442 public function force_regrading() { 443 $grade_item = $this->load_grade_item(); 444 $grade_item->force_regrading(); 445 } 446 447 /** 448 * Something that should be called before we start regrading the whole course. 449 * 450 * @return void 451 */ 452 public function pre_regrade_final_grades() { 453 $this->auto_update_weights(); 454 $this->auto_update_max(); 455 } 456 457 /** 458 * Generates and saves final grades in associated category grade item. 459 * These immediate children must already have their own final grades. 460 * The category's aggregation method is used to generate final grades. 461 * 462 * Please note that category grade is either calculated or aggregated, not both at the same time. 463 * 464 * This method must be used ONLY from grade_item::regrade_final_grades(), 465 * because the calculation must be done in correct order! 466 * 467 * Steps to follow: 468 * 1. Get final grades from immediate children 469 * 3. Aggregate these grades 470 * 4. Save them in final grades of associated category grade item 471 * 472 * @param int $userid The user ID if final grade generation should be limited to a single user 473 * @return bool 474 */ 475 public function generate_grades($userid=null) { 476 global $CFG, $DB; 477 478 $this->load_grade_item(); 479 480 if ($this->grade_item->is_locked()) { 481 return true; // no need to recalculate locked items 482 } 483 484 // find grade items of immediate children (category or grade items) and force site settings 485 $depends_on = $this->grade_item->depends_on(); 486 487 if (empty($depends_on)) { 488 $items = false; 489 490 } else { 491 list($usql, $params) = $DB->get_in_or_equal($depends_on); 492 $sql = "SELECT * 493 FROM {grade_items} 494 WHERE id $usql"; 495 $items = $DB->get_records_sql($sql, $params); 496 foreach ($items as $id => $item) { 497 $items[$id] = new grade_item($item, false); 498 } 499 } 500 501 $grade_inst = new grade_grade(); 502 $fields = 'g.'.implode(',g.', $grade_inst->required_fields); 503 504 // where to look for final grades - include grade of this item too, we will store the results there 505 $gis = array_merge($depends_on, array($this->grade_item->id)); 506 list($usql, $params) = $DB->get_in_or_equal($gis); 507 508 if ($userid) { 509 $usersql = "AND g.userid=?"; 510 $params[] = $userid; 511 512 } else { 513 $usersql = ""; 514 } 515 516 $sql = "SELECT $fields 517 FROM {grade_grades} g, {grade_items} gi 518 WHERE gi.id = g.itemid AND gi.id $usql $usersql 519 ORDER BY g.userid"; 520 521 // group the results by userid and aggregate the grades for this user 522 $rs = $DB->get_recordset_sql($sql, $params); 523 if ($rs->valid()) { 524 $prevuser = 0; 525 $grade_values = array(); 526 $excluded = array(); 527 $oldgrade = null; 528 $grademaxoverrides = array(); 529 $grademinoverrides = array(); 530 531 foreach ($rs as $used) { 532 $grade = new grade_grade($used, false); 533 if (isset($items[$grade->itemid])) { 534 // Prevent grade item to be fetched from DB. 535 $grade->grade_item =& $items[$grade->itemid]; 536 } else if ($grade->itemid == $this->grade_item->id) { 537 // This grade's grade item is not in $items. 538 $grade->grade_item =& $this->grade_item; 539 } 540 if ($grade->userid != $prevuser) { 541 $this->aggregate_grades($prevuser, 542 $items, 543 $grade_values, 544 $oldgrade, 545 $excluded, 546 $grademinoverrides, 547 $grademaxoverrides); 548 $prevuser = $grade->userid; 549 $grade_values = array(); 550 $excluded = array(); 551 $oldgrade = null; 552 $grademaxoverrides = array(); 553 $grademinoverrides = array(); 554 } 555 $grade_values[$grade->itemid] = $grade->finalgrade; 556 $grademaxoverrides[$grade->itemid] = $grade->get_grade_max(); 557 $grademinoverrides[$grade->itemid] = $grade->get_grade_min(); 558 559 if ($grade->excluded) { 560 $excluded[] = $grade->itemid; 561 } 562 563 if ($this->grade_item->id == $grade->itemid) { 564 $oldgrade = $grade; 565 } 566 } 567 $this->aggregate_grades($prevuser, 568 $items, 569 $grade_values, 570 $oldgrade, 571 $excluded, 572 $grademinoverrides, 573 $grademaxoverrides);//the last one 574 } 575 $rs->close(); 576 577 return true; 578 } 579 580 /** 581 * Internal function for grade category grade aggregation 582 * 583 * @param int $userid The User ID 584 * @param array $items Grade items 585 * @param array $grade_values Array of grade values 586 * @param object $oldgrade Old grade 587 * @param array $excluded Excluded 588 * @param array $grademinoverrides User specific grademin values if different to the grade_item grademin (key is itemid) 589 * @param array $grademaxoverrides User specific grademax values if different to the grade_item grademax (key is itemid) 590 */ 591 private function aggregate_grades($userid, 592 $items, 593 $grade_values, 594 $oldgrade, 595 $excluded, 596 $grademinoverrides, 597 $grademaxoverrides) { 598 global $CFG, $DB; 599 600 // Remember these so we can set flags on them to describe how they were used in the aggregation. 601 $novalue = array(); 602 $dropped = array(); 603 $extracredit = array(); 604 $usedweights = array(); 605 606 if (empty($userid)) { 607 //ignore first call 608 return; 609 } 610 611 if ($oldgrade) { 612 $oldfinalgrade = $oldgrade->finalgrade; 613 $grade = new grade_grade($oldgrade, false); 614 $grade->grade_item =& $this->grade_item; 615 616 } else { 617 // insert final grade - it will be needed later anyway 618 $grade = new grade_grade(array('itemid'=>$this->grade_item->id, 'userid'=>$userid), false); 619 $grade->grade_item =& $this->grade_item; 620 $grade->insert('system'); 621 $oldfinalgrade = null; 622 } 623 624 // no need to recalculate locked or overridden grades 625 if ($grade->is_locked() or $grade->is_overridden()) { 626 return; 627 } 628 629 // can not use own final category grade in calculation 630 unset($grade_values[$this->grade_item->id]); 631 632 // Make sure a grade_grade exists for every grade_item. 633 // We need to do this so we can set the aggregationstatus 634 // with a set_field call instead of checking if each one exists and creating/updating. 635 if (!empty($items)) { 636 list($ggsql, $params) = $DB->get_in_or_equal(array_keys($items), SQL_PARAMS_NAMED, 'g'); 637 638 639 $params['userid'] = $userid; 640 $sql = "SELECT itemid 641 FROM {grade_grades} 642 WHERE itemid $ggsql AND userid = :userid"; 643 $existingitems = $DB->get_records_sql($sql, $params); 644 645 $notexisting = array_diff(array_keys($items), array_keys($existingitems)); 646 foreach ($notexisting as $itemid) { 647 $gradeitem = $items[$itemid]; 648 $gradegrade = new grade_grade(array('itemid' => $itemid, 649 'userid' => $userid, 650 'rawgrademin' => $gradeitem->grademin, 651 'rawgrademax' => $gradeitem->grademax), false); 652 $gradegrade->grade_item = $gradeitem; 653 $gradegrade->insert('system'); 654 } 655 } 656 657 // if no grades calculation possible or grading not allowed clear final grade 658 if (empty($grade_values) or empty($items) or ($this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE)) { 659 $grade->finalgrade = null; 660 661 if (!is_null($oldfinalgrade)) { 662 $grade->timemodified = time(); 663 $success = $grade->update('aggregation'); 664 665 // If successful trigger a user_graded event. 666 if ($success) { 667 \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger(); 668 } 669 } 670 $dropped = $grade_values; 671 $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit); 672 return; 673 } 674 675 // Normalize the grades first - all will have value 0...1 676 // ungraded items are not used in aggregation. 677 foreach ($grade_values as $itemid=>$v) { 678 if (is_null($v)) { 679 // If null, it means no grade. 680 if ($this->aggregateonlygraded) { 681 unset($grade_values[$itemid]); 682 // Mark this item as "excluded empty" because it has no grade. 683 $novalue[$itemid] = 0; 684 continue; 685 } 686 } 687 if (in_array($itemid, $excluded)) { 688 unset($grade_values[$itemid]); 689 $dropped[$itemid] = 0; 690 continue; 691 } 692 // Check for user specific grade min/max overrides. 693 $usergrademin = $items[$itemid]->grademin; 694 $usergrademax = $items[$itemid]->grademax; 695 if (isset($grademinoverrides[$itemid])) { 696 $usergrademin = $grademinoverrides[$itemid]; 697 } 698 if (isset($grademaxoverrides[$itemid])) { 699 $usergrademax = $grademaxoverrides[$itemid]; 700 } 701 if ($this->aggregation == GRADE_AGGREGATE_SUM) { 702 // Assume that the grademin is 0 when standardising the score, to preserve negative grades. 703 $grade_values[$itemid] = grade_grade::standardise_score($v, 0, $usergrademax, 0, 1); 704 } else { 705 $grade_values[$itemid] = grade_grade::standardise_score($v, $usergrademin, $usergrademax, 0, 1); 706 } 707 708 } 709 710 // For items with no value, and not excluded - either set their grade to 0 or exclude them. 711 foreach ($items as $itemid=>$value) { 712 if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) { 713 if (!$this->aggregateonlygraded) { 714 $grade_values[$itemid] = 0; 715 } else { 716 // We are specifically marking these items as "excluded empty". 717 $novalue[$itemid] = 0; 718 } 719 } 720 } 721 722 // limit and sort 723 $allvalues = $grade_values; 724 if ($this->can_apply_limit_rules()) { 725 $this->apply_limit_rules($grade_values, $items); 726 } 727 728 $moredropped = array_diff($allvalues, $grade_values); 729 foreach ($moredropped as $drop => $unused) { 730 $dropped[$drop] = 0; 731 } 732 733 foreach ($grade_values as $itemid => $val) { 734 if (self::is_extracredit_used() && ($items[$itemid]->aggregationcoef > 0)) { 735 $extracredit[$itemid] = 0; 736 } 737 } 738 739 asort($grade_values, SORT_NUMERIC); 740 741 // let's see we have still enough grades to do any statistics 742 if (count($grade_values) == 0) { 743 // not enough attempts yet 744 $grade->finalgrade = null; 745 746 if (!is_null($oldfinalgrade)) { 747 $grade->timemodified = time(); 748 $success = $grade->update('aggregation'); 749 750 // If successful trigger a user_graded event. 751 if ($success) { 752 \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger(); 753 } 754 } 755 $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit); 756 return; 757 } 758 759 // do the maths 760 $result = $this->aggregate_values_and_adjust_bounds($grade_values, 761 $items, 762 $usedweights, 763 $grademinoverrides, 764 $grademaxoverrides); 765 $agg_grade = $result['grade']; 766 767 // Set the actual grademin and max to bind the grade properly. 768 $this->grade_item->grademin = $result['grademin']; 769 $this->grade_item->grademax = $result['grademax']; 770 771 if ($this->aggregation == GRADE_AGGREGATE_SUM) { 772 // The natural aggregation always displays the range as coming from 0 for categories. 773 // However, when we bind the grade we allow for negative values. 774 $result['grademin'] = 0; 775 } 776 777 // Recalculate the grade back to requested range. 778 $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $result['grademin'], $result['grademax']); 779 $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade); 780 781 $oldrawgrademin = $grade->rawgrademin; 782 $oldrawgrademax = $grade->rawgrademax; 783 $grade->rawgrademin = $result['grademin']; 784 $grade->rawgrademax = $result['grademax']; 785 786 // Update in db if changed. 787 if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || 788 grade_floats_different($grade->rawgrademax, $oldrawgrademax) || 789 grade_floats_different($grade->rawgrademin, $oldrawgrademin)) { 790 $grade->timemodified = time(); 791 $success = $grade->update('aggregation'); 792 793 // If successful trigger a user_graded event. 794 if ($success) { 795 \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger(); 796 } 797 } 798 799 $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit); 800 801 return; 802 } 803 804 /** 805 * Set the flags on the grade_grade items to indicate how individual grades are used 806 * in the aggregation. 807 * 808 * WARNING: This function is called a lot during gradebook recalculation, be very performance considerate. 809 * 810 * @param int $userid The user we have aggregated the grades for. 811 * @param array $usedweights An array with keys for each of the grade_item columns included in the aggregation. The value are the relative weight. 812 * @param array $novalue An array with keys for each of the grade_item columns skipped because 813 * they had no value in the aggregation. 814 * @param array $dropped An array with keys for each of the grade_item columns dropped 815 * because of any drop lowest/highest settings in the aggregation. 816 * @param array $extracredit An array with keys for each of the grade_item columns 817 * considered extra credit by the aggregation. 818 */ 819 private function set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit) { 820 global $DB; 821 822 // We want to know all current user grades so we can decide whether they need to be updated or they already contain the 823 // expected value. 824 $sql = "SELECT gi.id, gg.aggregationstatus, gg.aggregationweight FROM {grade_grades} gg 825 JOIN {grade_items} gi ON (gg.itemid = gi.id) 826 WHERE gg.userid = :userid"; 827 $params = array('categoryid' => $this->id, 'userid' => $userid); 828 829 // These are all grade_item ids which grade_grades will NOT end up being 'unknown' (because they are not unknown or 830 // because we will update them to something different that 'unknown'). 831 $giids = array_keys($usedweights + $novalue + $dropped + $extracredit); 832 833 if ($giids) { 834 // We include grade items that might not be in categoryid. 835 list($itemsql, $itemlist) = $DB->get_in_or_equal($giids, SQL_PARAMS_NAMED, 'gg'); 836 $sql .= ' AND (gi.categoryid = :categoryid OR gi.id ' . $itemsql . ')'; 837 $params = $params + $itemlist; 838 } else { 839 $sql .= ' AND gi.categoryid = :categoryid'; 840 } 841 $currentgrades = $DB->get_recordset_sql($sql, $params); 842 843 // We will store here the grade_item ids that need to be updated on db. 844 $toupdate = array(); 845 846 if ($currentgrades->valid()) { 847 848 // Iterate through the user grades to see if we really need to update any of them. 849 foreach ($currentgrades as $currentgrade) { 850 851 // Unset $usedweights that we do not need to update. 852 if (!empty($usedweights) && isset($usedweights[$currentgrade->id]) && $currentgrade->aggregationstatus === 'used') { 853 // We discard the ones that already have the contribution specified in $usedweights and are marked as 'used'. 854 if (grade_floats_equal($currentgrade->aggregationweight, $usedweights[$currentgrade->id])) { 855 unset($usedweights[$currentgrade->id]); 856 } 857 // Used weights can be present in multiple set_usedinaggregation arguments. 858 if (!isset($novalue[$currentgrade->id]) && !isset($dropped[$currentgrade->id]) && 859 !isset($extracredit[$currentgrade->id])) { 860 continue; 861 } 862 } 863 864 // No value grades. 865 if (!empty($novalue) && isset($novalue[$currentgrade->id])) { 866 if ($currentgrade->aggregationstatus !== 'novalue' || 867 grade_floats_different($currentgrade->aggregationweight, 0)) { 868 $toupdate['novalue'][] = $currentgrade->id; 869 } 870 continue; 871 } 872 873 // Dropped grades. 874 if (!empty($dropped) && isset($dropped[$currentgrade->id])) { 875 if ($currentgrade->aggregationstatus !== 'dropped' || 876 grade_floats_different($currentgrade->aggregationweight, 0)) { 877 $toupdate['dropped'][] = $currentgrade->id; 878 } 879 continue; 880 } 881 882 // Extra credit grades. 883 if (!empty($extracredit) && isset($extracredit[$currentgrade->id])) { 884 885 // If this grade item is already marked as 'extra' and it already has the provided $usedweights value would be 886 // silly to update to 'used' to later update to 'extra'. 887 if (!empty($usedweights) && isset($usedweights[$currentgrade->id]) && 888 grade_floats_equal($currentgrade->aggregationweight, $usedweights[$currentgrade->id])) { 889 unset($usedweights[$currentgrade->id]); 890 } 891 892 // Update the item to extra if it is not already marked as extra in the database or if the item's 893 // aggregationweight will be updated when going through $usedweights items. 894 if ($currentgrade->aggregationstatus !== 'extra' || 895 (!empty($usedweights) && isset($usedweights[$currentgrade->id]))) { 896 $toupdate['extracredit'][] = $currentgrade->id; 897 } 898 continue; 899 } 900 901 // If is not in any of the above groups it should be set to 'unknown', checking that the item is not already 902 // unknown, if it is we don't need to update it. 903 if ($currentgrade->aggregationstatus !== 'unknown' || grade_floats_different($currentgrade->aggregationweight, 0)) { 904 $toupdate['unknown'][] = $currentgrade->id; 905 } 906 } 907 $currentgrades->close(); 908 } 909 910 // Update items to 'unknown' status. 911 if (!empty($toupdate['unknown'])) { 912 list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['unknown'], SQL_PARAMS_NAMED, 'g'); 913 914 $itemlist['userid'] = $userid; 915 916 $sql = "UPDATE {grade_grades} 917 SET aggregationstatus = 'unknown', 918 aggregationweight = 0 919 WHERE itemid $itemsql AND userid = :userid"; 920 $DB->execute($sql, $itemlist); 921 } 922 923 // Update items to 'used' status and setting the proper weight. 924 if (!empty($usedweights)) { 925 // The usedweights items are updated individually to record the weights. 926 foreach ($usedweights as $gradeitemid => $contribution) { 927 $sql = "UPDATE {grade_grades} 928 SET aggregationstatus = 'used', 929 aggregationweight = :contribution 930 WHERE itemid = :itemid AND userid = :userid"; 931 932 $params = array('contribution' => $contribution, 'itemid' => $gradeitemid, 'userid' => $userid); 933 $DB->execute($sql, $params); 934 } 935 } 936 937 // Update items to 'novalue' status. 938 if (!empty($toupdate['novalue'])) { 939 list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['novalue'], SQL_PARAMS_NAMED, 'g'); 940 941 $itemlist['userid'] = $userid; 942 943 $sql = "UPDATE {grade_grades} 944 SET aggregationstatus = 'novalue', 945 aggregationweight = 0 946 WHERE itemid $itemsql AND userid = :userid"; 947 948 $DB->execute($sql, $itemlist); 949 } 950 951 // Update items to 'dropped' status. 952 if (!empty($toupdate['dropped'])) { 953 list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['dropped'], SQL_PARAMS_NAMED, 'g'); 954 955 $itemlist['userid'] = $userid; 956 957 $sql = "UPDATE {grade_grades} 958 SET aggregationstatus = 'dropped', 959 aggregationweight = 0 960 WHERE itemid $itemsql AND userid = :userid"; 961 962 $DB->execute($sql, $itemlist); 963 } 964 965 // Update items to 'extracredit' status. 966 if (!empty($toupdate['extracredit'])) { 967 list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['extracredit'], SQL_PARAMS_NAMED, 'g'); 968 969 $itemlist['userid'] = $userid; 970 971 $DB->set_field_select('grade_grades', 972 'aggregationstatus', 973 'extra', 974 "itemid $itemsql AND userid = :userid", 975 $itemlist); 976 } 977 } 978 979 /** 980 * Internal function that calculates the aggregated grade and new min/max for this grade category 981 * 982 * Must be public as it is used by grade_grade::get_hiding_affected() 983 * 984 * @param array $grade_values An array of values to be aggregated 985 * @param array $items The array of grade_items 986 * @since Moodle 2.6.5, 2.7.2 987 * @param array & $weights If provided, will be filled with the normalized weights 988 * for each grade_item as used in the aggregation. 989 * Some rules for the weights are: 990 * 1. The weights must add up to 1 (unless there are extra credit) 991 * 2. The contributed points column must add up to the course 992 * final grade and this column is calculated from these weights. 993 * @param array $grademinoverrides User specific grademin values if different to the grade_item grademin (key is itemid) 994 * @param array $grademaxoverrides User specific grademax values if different to the grade_item grademax (key is itemid) 995 * @return array containing values for: 996 * 'grade' => the new calculated grade 997 * 'grademin' => the new calculated min grade for the category 998 * 'grademax' => the new calculated max grade for the category 999 */ 1000 public function aggregate_values_and_adjust_bounds($grade_values, 1001 $items, 1002 & $weights = null, 1003 $grademinoverrides = array(), 1004 $grademaxoverrides = array()) { 1005 global $CFG; 1006 1007 $category_item = $this->load_grade_item(); 1008 $grademin = $category_item->grademin; 1009 $grademax = $category_item->grademax; 1010 1011 switch ($this->aggregation) { 1012 1013 case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies 1014 $num = count($grade_values); 1015 $grades = array_values($grade_values); 1016 1017 // The median gets 100% - others get 0. 1018 if ($weights !== null && $num > 0) { 1019 $count = 0; 1020 foreach ($grade_values as $itemid=>$grade_value) { 1021 if (($num % 2 == 0) && ($count == intval($num/2)-1 || $count == intval($num/2))) { 1022 $weights[$itemid] = 0.5; 1023 } else if (($num % 2 != 0) && ($count == intval(($num/2)-0.5))) { 1024 $weights[$itemid] = 1.0; 1025 } else { 1026 $weights[$itemid] = 0; 1027 } 1028 $count++; 1029 } 1030 } 1031 if ($num % 2 == 0) { 1032 $agg_grade = ($grades[intval($num/2)-1] + $grades[intval($num/2)]) / 2; 1033 } else { 1034 $agg_grade = $grades[intval(($num/2)-0.5)]; 1035 } 1036 1037 break; 1038 1039 case GRADE_AGGREGATE_MIN: 1040 $agg_grade = reset($grade_values); 1041 // Record the weights as used. 1042 if ($weights !== null) { 1043 foreach ($grade_values as $itemid=>$grade_value) { 1044 $weights[$itemid] = 0; 1045 } 1046 } 1047 // Set the first item to 1. 1048 $itemids = array_keys($grade_values); 1049 $weights[reset($itemids)] = 1; 1050 break; 1051 1052 case GRADE_AGGREGATE_MAX: 1053 // Record the weights as used. 1054 if ($weights !== null) { 1055 foreach ($grade_values as $itemid=>$grade_value) { 1056 $weights[$itemid] = 0; 1057 } 1058 } 1059 // Set the last item to 1. 1060 $itemids = array_keys($grade_values); 1061 $weights[end($itemids)] = 1; 1062 $agg_grade = end($grade_values); 1063 break; 1064 1065 case GRADE_AGGREGATE_MODE: // the most common value 1066 // array_count_values only counts INT and STRING, so if grades are floats we must convert them to string 1067 $converted_grade_values = array(); 1068 1069 foreach ($grade_values as $k => $gv) { 1070 1071 if (!is_int($gv) && !is_string($gv)) { 1072 $converted_grade_values[$k] = (string) $gv; 1073 1074 } else { 1075 $converted_grade_values[$k] = $gv; 1076 } 1077 if ($weights !== null) { 1078 $weights[$k] = 0; 1079 } 1080 } 1081 1082 $freq = array_count_values($converted_grade_values); 1083 arsort($freq); // sort by frequency keeping keys 1084 $top = reset($freq); // highest frequency count 1085 $modes = array_keys($freq, $top); // search for all modes (have the same highest count) 1086 rsort($modes, SORT_NUMERIC); // get highest mode 1087 $agg_grade = reset($modes); 1088 // Record the weights as used. 1089 if ($weights !== null && $top > 0) { 1090 foreach ($grade_values as $k => $gv) { 1091 if ($gv == $agg_grade) { 1092 $weights[$k] = 1.0 / $top; 1093 } 1094 } 1095 } 1096 break; 1097 1098 case GRADE_AGGREGATE_WEIGHTED_MEAN: // Weighted average of all existing final grades, weight specified in coef 1099 $weightsum = 0; 1100 $sum = 0; 1101 1102 foreach ($grade_values as $itemid=>$grade_value) { 1103 if ($weights !== null) { 1104 $weights[$itemid] = $items[$itemid]->aggregationcoef; 1105 } 1106 if ($items[$itemid]->aggregationcoef <= 0) { 1107 continue; 1108 } 1109 $weightsum += $items[$itemid]->aggregationcoef; 1110 $sum += $items[$itemid]->aggregationcoef * $grade_value; 1111 } 1112 if ($weightsum == 0) { 1113 $agg_grade = null; 1114 1115 } else { 1116 $agg_grade = $sum / $weightsum; 1117 if ($weights !== null) { 1118 // Normalise the weights. 1119 foreach ($weights as $itemid => $weight) { 1120 $weights[$itemid] = $weight / $weightsum; 1121 } 1122 } 1123 1124 } 1125 break; 1126 1127 case GRADE_AGGREGATE_WEIGHTED_MEAN2: 1128 // Weighted average of all existing final grades with optional extra credit flag, 1129 // weight is the range of grade (usually grademax) 1130 $this->load_grade_item(); 1131 $weightsum = 0; 1132 $sum = null; 1133 1134 foreach ($grade_values as $itemid=>$grade_value) { 1135 if ($items[$itemid]->aggregationcoef > 0) { 1136 continue; 1137 } 1138 1139 $weight = $items[$itemid]->grademax - $items[$itemid]->grademin; 1140 if ($weight <= 0) { 1141 continue; 1142 } 1143 1144 $weightsum += $weight; 1145 $sum += $weight * $grade_value; 1146 } 1147 1148 // Handle the extra credit items separately to calculate their weight accurately. 1149 foreach ($grade_values as $itemid => $grade_value) { 1150 if ($items[$itemid]->aggregationcoef <= 0) { 1151 continue; 1152 } 1153 1154 $weight = $items[$itemid]->grademax - $items[$itemid]->grademin; 1155 if ($weight <= 0) { 1156 $weights[$itemid] = 0; 1157 continue; 1158 } 1159 1160 $oldsum = $sum; 1161 $weightedgrade = $weight * $grade_value; 1162 $sum += $weightedgrade; 1163 1164 if ($weights !== null) { 1165 if ($weightsum <= 0) { 1166 $weights[$itemid] = 0; 1167 continue; 1168 } 1169 1170 $oldgrade = $oldsum / $weightsum; 1171 $grade = $sum / $weightsum; 1172 $normoldgrade = grade_grade::standardise_score($oldgrade, 0, 1, $grademin, $grademax); 1173 $normgrade = grade_grade::standardise_score($grade, 0, 1, $grademin, $grademax); 1174 $boundedoldgrade = $this->grade_item->bounded_grade($normoldgrade); 1175 $boundedgrade = $this->grade_item->bounded_grade($normgrade); 1176 1177 if ($boundedgrade - $boundedoldgrade <= 0) { 1178 // Nothing new was added to the grade. 1179 $weights[$itemid] = 0; 1180 } else if ($boundedgrade < $normgrade) { 1181 // The grade has been bounded, the extra credit item needs to have a different weight. 1182 $gradediff = $boundedgrade - $normoldgrade; 1183 $gradediffnorm = grade_grade::standardise_score($gradediff, $grademin, $grademax, 0, 1); 1184 $weights[$itemid] = $gradediffnorm / $grade_value; 1185 } else { 1186 // Default weighting. 1187 $weights[$itemid] = $weight / $weightsum; 1188 } 1189 } 1190 } 1191 1192 if ($weightsum == 0) { 1193 $agg_grade = $sum; // only extra credits 1194 1195 } else { 1196 $agg_grade = $sum / $weightsum; 1197 } 1198 1199 // Record the weights as used. 1200 if ($weights !== null) { 1201 foreach ($grade_values as $itemid=>$grade_value) { 1202 if ($items[$itemid]->aggregationcoef > 0) { 1203 // Ignore extra credit items, the weights have already been computed. 1204 continue; 1205 } 1206 if ($weightsum > 0) { 1207 $weight = $items[$itemid]->grademax - $items[$itemid]->grademin; 1208 $weights[$itemid] = $weight / $weightsum; 1209 } else { 1210 $weights[$itemid] = 0; 1211 } 1212 } 1213 } 1214 break; 1215 1216 case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average 1217 $this->load_grade_item(); 1218 $num = 0; 1219 $sum = null; 1220 1221 foreach ($grade_values as $itemid=>$grade_value) { 1222 if ($items[$itemid]->aggregationcoef == 0) { 1223 $num += 1; 1224 $sum += $grade_value; 1225 if ($weights !== null) { 1226 $weights[$itemid] = 1; 1227 } 1228 } 1229 } 1230 1231 // Treating the extra credit items separately to get a chance to calculate their effective weights. 1232 foreach ($grade_values as $itemid=>$grade_value) { 1233 if ($items[$itemid]->aggregationcoef > 0) { 1234 $oldsum = $sum; 1235 $sum += $items[$itemid]->aggregationcoef * $grade_value; 1236 1237 if ($weights !== null) { 1238 if ($num <= 0) { 1239 // The category only contains extra credit items, not setting the weight. 1240 continue; 1241 } 1242 1243 $oldgrade = $oldsum / $num; 1244 $grade = $sum / $num; 1245 $normoldgrade = grade_grade::standardise_score($oldgrade, 0, 1, $grademin, $grademax); 1246 $normgrade = grade_grade::standardise_score($grade, 0, 1, $grademin, $grademax); 1247 $boundedoldgrade = $this->grade_item->bounded_grade($normoldgrade); 1248 $boundedgrade = $this->grade_item->bounded_grade($normgrade); 1249 1250 if ($boundedgrade - $boundedoldgrade <= 0) { 1251 // Nothing new was added to the grade. 1252 $weights[$itemid] = 0; 1253 } else if ($boundedgrade < $normgrade) { 1254 // The grade has been bounded, the extra credit item needs to have a different weight. 1255 $gradediff = $boundedgrade - $normoldgrade; 1256 $gradediffnorm = grade_grade::standardise_score($gradediff, $grademin, $grademax, 0, 1); 1257 $weights[$itemid] = $gradediffnorm / $grade_value; 1258 } else { 1259 // Default weighting. 1260 $weights[$itemid] = 1.0 / $num; 1261 } 1262 } 1263 } 1264 } 1265 1266 if ($weights !== null && $num > 0) { 1267 foreach ($grade_values as $itemid=>$grade_value) { 1268 if ($items[$itemid]->aggregationcoef > 0) { 1269 // Extra credit weights were already calculated. 1270 continue; 1271 } 1272 if ($weights[$itemid]) { 1273 $weights[$itemid] = 1.0 / $num; 1274 } 1275 } 1276 } 1277 1278 if ($num == 0) { 1279 $agg_grade = $sum; // only extra credits or wrong coefs 1280 1281 } else { 1282 $agg_grade = $sum / $num; 1283 } 1284 1285 break; 1286 1287 case GRADE_AGGREGATE_SUM: // Add up all the items. 1288 $this->load_grade_item(); 1289 $num = count($grade_values); 1290 $sum = 0; 1291 1292 // This setting indicates if we should use algorithm prior to MDL-49257 fix for calculating extra credit weights. 1293 // Even though old algorith has bugs in it, we need to preserve existing grades. 1294 $gradebookcalculationfreeze = 'gradebook_calculations_freeze_' . $this->courseid; 1295 $oldextracreditcalculation = isset($CFG->$gradebookcalculationfreeze) 1296 && ($CFG->$gradebookcalculationfreeze <= 20150619); 1297 1298 $sumweights = 0; 1299 $grademin = 0; 1300 $grademax = 0; 1301 $extracredititems = array(); 1302 foreach ($grade_values as $itemid => $gradevalue) { 1303 // We need to check if the grademax/min was adjusted per user because of excluded items. 1304 $usergrademin = $items[$itemid]->grademin; 1305 $usergrademax = $items[$itemid]->grademax; 1306 if (isset($grademinoverrides[$itemid])) { 1307 $usergrademin = $grademinoverrides[$itemid]; 1308 } 1309 if (isset($grademaxoverrides[$itemid])) { 1310 $usergrademax = $grademaxoverrides[$itemid]; 1311 } 1312 1313 // Keep track of the extra credit items, we will need them later on. 1314 if ($items[$itemid]->aggregationcoef > 0) { 1315 $extracredititems[$itemid] = $items[$itemid]; 1316 } 1317 1318 // Ignore extra credit and items with a weight of 0. 1319 if (!isset($extracredititems[$itemid]) && $items[$itemid]->aggregationcoef2 > 0) { 1320 $grademin += $usergrademin; 1321 $grademax += $usergrademax; 1322 $sumweights += $items[$itemid]->aggregationcoef2; 1323 } 1324 } 1325 $userweights = array(); 1326 $totaloverriddenweight = 0; 1327 $totaloverriddengrademax = 0; 1328 // We first need to rescale all manually assigned weights down by the 1329 // percentage of weights missing from the category. 1330 foreach ($grade_values as $itemid => $gradevalue) { 1331 if ($items[$itemid]->weightoverride) { 1332 if ($items[$itemid]->aggregationcoef2 <= 0) { 1333 // Records the weight of 0 and continue. 1334 $userweights[$itemid] = 0; 1335 continue; 1336 } 1337 $userweights[$itemid] = $sumweights ? ($items[$itemid]->aggregationcoef2 / $sumweights) : 0; 1338 if (!$oldextracreditcalculation && isset($extracredititems[$itemid])) { 1339 // Extra credit items do not affect totals. 1340 continue; 1341 } 1342 $totaloverriddenweight += $userweights[$itemid]; 1343 $usergrademax = $items[$itemid]->grademax; 1344 if (isset($grademaxoverrides[$itemid])) { 1345 $usergrademax = $grademaxoverrides[$itemid]; 1346 } 1347 $totaloverriddengrademax += $usergrademax; 1348 } 1349 } 1350 $nonoverriddenpoints = $grademax - $totaloverriddengrademax; 1351 1352 // Then we need to recalculate the automatic weights except for extra credit items. 1353 foreach ($grade_values as $itemid => $gradevalue) { 1354 if (!$items[$itemid]->weightoverride && ($oldextracreditcalculation || !isset($extracredititems[$itemid]))) { 1355 $usergrademax = $items[$itemid]->grademax; 1356 if (isset($grademaxoverrides[$itemid])) { 1357 $usergrademax = $grademaxoverrides[$itemid]; 1358 } 1359 if ($nonoverriddenpoints > 0) { 1360 $userweights[$itemid] = ($usergrademax/$nonoverriddenpoints) * (1 - $totaloverriddenweight); 1361 } else { 1362 $userweights[$itemid] = 0; 1363 if ($items[$itemid]->aggregationcoef2 > 0) { 1364 // Items with a weight of 0 should not count for the grade max, 1365 // though this only applies if the weight was changed to 0. 1366 $grademax -= $usergrademax; 1367 } 1368 } 1369 } 1370 } 1371 1372 // Now when we finally know the grademax we can adjust the automatic weights of extra credit items. 1373 if (!$oldextracreditcalculation) { 1374 foreach ($grade_values as $itemid => $gradevalue) { 1375 if (!$items[$itemid]->weightoverride && isset($extracredititems[$itemid])) { 1376 $usergrademax = $items[$itemid]->grademax; 1377 if (isset($grademaxoverrides[$itemid])) { 1378 $usergrademax = $grademaxoverrides[$itemid]; 1379 } 1380 $userweights[$itemid] = $grademax ? ($usergrademax / $grademax) : 0; 1381 } 1382 } 1383 } 1384 1385 // We can use our freshly corrected weights below. 1386 foreach ($grade_values as $itemid => $gradevalue) { 1387 if (isset($extracredititems[$itemid])) { 1388 // We skip the extra credit items first. 1389 continue; 1390 } 1391 $sum += $gradevalue * $userweights[$itemid] * $grademax; 1392 if ($weights !== null) { 1393 $weights[$itemid] = $userweights[$itemid]; 1394 } 1395 } 1396 1397 // No we proceed with the extra credit items. They might have a different final 1398 // weight in case the final grade was bounded. So we need to treat them different. 1399 // Also, as we need to use the bounded_grade() method, we have to inject the 1400 // right values there, and restore them afterwards. 1401 $oldgrademax = $this->grade_item->grademax; 1402 $oldgrademin = $this->grade_item->grademin; 1403 foreach ($grade_values as $itemid => $gradevalue) { 1404 if (!isset($extracredititems[$itemid])) { 1405 continue; 1406 } 1407 $oldsum = $sum; 1408 $weightedgrade = $gradevalue * $userweights[$itemid] * $grademax; 1409 $sum += $weightedgrade; 1410 1411 // Only go through this when we need to record the weights. 1412 if ($weights !== null) { 1413 if ($grademax <= 0) { 1414 // There are only extra credit items in this category, 1415 // all the weights should be accurate (and be 0). 1416 $weights[$itemid] = $userweights[$itemid]; 1417 continue; 1418 } 1419 1420 $oldfinalgrade = $this->grade_item->bounded_grade($oldsum); 1421 $newfinalgrade = $this->grade_item->bounded_grade($sum); 1422 $finalgradediff = $newfinalgrade - $oldfinalgrade; 1423 if ($finalgradediff <= 0) { 1424 // This item did not contribute to the category total at all. 1425 $weights[$itemid] = 0; 1426 } else if ($finalgradediff < $weightedgrade) { 1427 // The weight needs to be adjusted because only a portion of the 1428 // extra credit item contributed to the category total. 1429 $weights[$itemid] = $finalgradediff / ($gradevalue * $grademax); 1430 } else { 1431 // The weight was accurate. 1432 $weights[$itemid] = $userweights[$itemid]; 1433 } 1434 } 1435 } 1436 $this->grade_item->grademax = $oldgrademax; 1437 $this->grade_item->grademin = $oldgrademin; 1438 1439 if ($grademax > 0) { 1440 $agg_grade = $sum / $grademax; // Re-normalize score. 1441 } else { 1442 // Every item in the category is extra credit. 1443 $agg_grade = $sum; 1444 $grademax = $sum; 1445 } 1446 1447 break; 1448 1449 case GRADE_AGGREGATE_MEAN: // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum) 1450 default: 1451 $num = count($grade_values); 1452 $sum = array_sum($grade_values); 1453 $agg_grade = $sum / $num; 1454 // Record the weights evenly. 1455 if ($weights !== null && $num > 0) { 1456 foreach ($grade_values as $itemid=>$grade_value) { 1457 $weights[$itemid] = 1.0 / $num; 1458 } 1459 } 1460 break; 1461 } 1462 1463 return array('grade' => $agg_grade, 'grademin' => $grademin, 'grademax' => $grademax); 1464 } 1465 1466 /** 1467 * Internal function that calculates the aggregated grade for this grade category 1468 * 1469 * Must be public as it is used by grade_grade::get_hiding_affected() 1470 * 1471 * @deprecated since Moodle 2.8 1472 * @param array $grade_values An array of values to be aggregated 1473 * @param array $items The array of grade_items 1474 * @return float The aggregate grade for this grade category 1475 */ 1476 public function aggregate_values($grade_values, $items) { 1477 debugging('grade_category::aggregate_values() is deprecated. 1478 Call grade_category::aggregate_values_and_adjust_bounds() instead.', DEBUG_DEVELOPER); 1479 $result = $this->aggregate_values_and_adjust_bounds($grade_values, $items); 1480 return $result['grade']; 1481 } 1482 1483 /** 1484 * Some aggregation types may need to update their max grade. 1485 * 1486 * This must be executed after updating the weights as it relies on them. 1487 * 1488 * @return void 1489 */ 1490 private function auto_update_max() { 1491 global $CFG, $DB; 1492 if ($this->aggregation != GRADE_AGGREGATE_SUM) { 1493 // not needed at all 1494 return; 1495 } 1496 1497 // Find grade items of immediate children (category or grade items) and force site settings. 1498 $this->load_grade_item(); 1499 $depends_on = $this->grade_item->depends_on(); 1500 1501 // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they 1502 // wish to update the grades. 1503 $gradebookcalculationfreeze = 'gradebook_calculations_freeze_' . $this->courseid; 1504 $oldextracreditcalculation = isset($CFG->$gradebookcalculationfreeze) && ($CFG->$gradebookcalculationfreeze <= 20150627); 1505 // Only run if the gradebook isn't frozen. 1506 if (!$oldextracreditcalculation) { 1507 // Don't automatically update the max for calculated items. 1508 if ($this->grade_item->is_calculated()) { 1509 return; 1510 } 1511 } 1512 1513 $items = false; 1514 if (!empty($depends_on)) { 1515 list($usql, $params) = $DB->get_in_or_equal($depends_on); 1516 $sql = "SELECT * 1517 FROM {grade_items} 1518 WHERE id $usql"; 1519 $items = $DB->get_records_sql($sql, $params); 1520 } 1521 1522 if (!$items) { 1523 1524 if ($this->grade_item->grademax != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) { 1525 $this->grade_item->grademax = 0; 1526 $this->grade_item->grademin = 0; 1527 $this->grade_item->gradetype = GRADE_TYPE_VALUE; 1528 $this->grade_item->update('aggregation'); 1529 } 1530 return; 1531 } 1532 1533 //find max grade possible 1534 $maxes = array(); 1535 1536 foreach ($items as $item) { 1537 1538 if ($item->aggregationcoef > 0) { 1539 // extra credit from this activity - does not affect total 1540 continue; 1541 } else if ($item->aggregationcoef2 <= 0) { 1542 // Items with a weight of 0 do not affect the total. 1543 continue; 1544 } 1545 1546 if ($item->gradetype == GRADE_TYPE_VALUE) { 1547 $maxes[$item->id] = $item->grademax; 1548 1549 } else if ($item->gradetype == GRADE_TYPE_SCALE) { 1550 $maxes[$item->id] = $item->grademax; // 0 = nograde, 1 = first scale item, 2 = second scale item 1551 } 1552 } 1553 1554 if ($this->can_apply_limit_rules()) { 1555 // Apply droplow and keephigh. 1556 $this->apply_limit_rules($maxes, $items); 1557 } 1558 $max = array_sum($maxes); 1559 1560 // update db if anything changed 1561 if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) { 1562 $this->grade_item->grademax = $max; 1563 $this->grade_item->grademin = 0; 1564 $this->grade_item->gradetype = GRADE_TYPE_VALUE; 1565 $this->grade_item->update('aggregation'); 1566 } 1567 } 1568 1569 /** 1570 * Recalculate the weights of the grade items in this category. 1571 * 1572 * The category total is not updated here, a further call to 1573 * {@link self::auto_update_max()} is required. 1574 * 1575 * @return void 1576 */ 1577 private function auto_update_weights() { 1578 global $CFG; 1579 if ($this->aggregation != GRADE_AGGREGATE_SUM) { 1580 // This is only required if we are using natural weights. 1581 return; 1582 } 1583 $children = $this->get_children(); 1584 1585 $gradeitem = null; 1586 1587 // Calculate the sum of the grademax's of all the items within this category. 1588 $totalnonoverriddengrademax = 0; 1589 $totalgrademax = 0; 1590 1591 // Out of 1, how much weight has been manually overriden by a user? 1592 $totaloverriddenweight = 0; 1593 $totaloverriddengrademax = 0; 1594 1595 // Has every assessment in this category been overridden? 1596 $automaticgradeitemspresent = false; 1597 // Does the grade item require normalising? 1598 $requiresnormalising = false; 1599 1600 // This array keeps track of the id and weight of every grade item that has been overridden. 1601 $overridearray = array(); 1602 foreach ($children as $sortorder => $child) { 1603 $gradeitem = null; 1604 1605 if ($child['type'] == 'item') { 1606 $gradeitem = $child['object']; 1607 } else if ($child['type'] == 'category') { 1608 $gradeitem = $child['object']->load_grade_item(); 1609 } 1610 1611 if ($gradeitem->gradetype == GRADE_TYPE_NONE || $gradeitem->gradetype == GRADE_TYPE_TEXT) { 1612 // Text items and none items do not have a weight. 1613 continue; 1614 } else if (!$this->aggregateoutcomes && $gradeitem->is_outcome_item()) { 1615 // We will not aggregate outcome items, so we can ignore them. 1616 continue; 1617 } else if (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE) { 1618 // The scales are not included in the aggregation, ignore them. 1619 continue; 1620 } 1621 1622 // Record the ID and the weight for this grade item. 1623 $overridearray[$gradeitem->id] = array(); 1624 $overridearray[$gradeitem->id]['extracredit'] = intval($gradeitem->aggregationcoef); 1625 $overridearray[$gradeitem->id]['weight'] = $gradeitem->aggregationcoef2; 1626 $overridearray[$gradeitem->id]['weightoverride'] = intval($gradeitem->weightoverride); 1627 // If this item has had its weight overridden then set the flag to true, but 1628 // only if all previous items were also overridden. Note that extra credit items 1629 // are counted as overridden grade items. 1630 if (!$gradeitem->weightoverride && $gradeitem->aggregationcoef == 0) { 1631 $automaticgradeitemspresent = true; 1632 } 1633 1634 if ($gradeitem->aggregationcoef > 0) { 1635 // An extra credit grade item doesn't contribute to $totaloverriddengrademax. 1636 continue; 1637 } else if ($gradeitem->weightoverride > 0 && $gradeitem->aggregationcoef2 <= 0) { 1638 // An overriden item that defines a weight of 0 does not contribute to $totaloverriddengrademax. 1639 continue; 1640 } 1641 1642 $totalgrademax += $gradeitem->grademax; 1643 if ($gradeitem->weightoverride > 0) { 1644 $totaloverriddenweight += $gradeitem->aggregationcoef2; 1645 $totaloverriddengrademax += $gradeitem->grademax; 1646 } 1647 } 1648 1649 // Initialise this variable (used to keep track of the weight override total). 1650 $normalisetotal = 0; 1651 // Keep a record of how much the override total is to see if it is above 100. It it is then we need to set the 1652 // other weights to zero and normalise the others. 1653 $overriddentotal = 0; 1654 // If the overridden weight total is higher than 1 then set the other untouched weights to zero. 1655 $setotherweightstozero = false; 1656 // Total up all of the weights. 1657 foreach ($overridearray as $gradeitemdetail) { 1658 // If the grade item has extra credit, then don't add it to the normalisetotal. 1659 if (!$gradeitemdetail['extracredit']) { 1660 $normalisetotal += $gradeitemdetail['weight']; 1661 } 1662 // The overridden total comprises of items that are set as overridden, that aren't extra credit and have a value 1663 // greater than zero. 1664 if ($gradeitemdetail['weightoverride'] && !$gradeitemdetail['extracredit'] && $gradeitemdetail['weight'] > 0) { 1665 // Add overriden weights up to see if they are greater than 1. 1666 $overriddentotal += $gradeitemdetail['weight']; 1667 } 1668 } 1669 if ($overriddentotal > 1) { 1670 // Make sure that this catergory of weights gets normalised. 1671 $requiresnormalising = true; 1672 // The normalised weights are only the overridden weights, so we just use the total of those. 1673 $normalisetotal = $overriddentotal; 1674 } 1675 1676 $totalnonoverriddengrademax = $totalgrademax - $totaloverriddengrademax; 1677 1678 // This setting indicates if we should use algorithm prior to MDL-49257 fix for calculating extra credit weights. 1679 // Even though old algorith has bugs in it, we need to preserve existing grades. 1680 $gradebookcalculationfreeze = (int)get_config('core', 'gradebook_calculations_freeze_' . $this->courseid); 1681 $oldextracreditcalculation = $gradebookcalculationfreeze && ($gradebookcalculationfreeze <= 20150619); 1682 1683 reset($children); 1684 foreach ($children as $sortorder => $child) { 1685 $gradeitem = null; 1686 1687 if ($child['type'] == 'item') { 1688 $gradeitem = $child['object']; 1689 } else if ($child['type'] == 'category') { 1690 $gradeitem = $child['object']->load_grade_item(); 1691 } 1692 1693 if ($gradeitem->gradetype == GRADE_TYPE_NONE || $gradeitem->gradetype == GRADE_TYPE_TEXT) { 1694 // Text items and none items do not have a weight, no need to set their weight to 1695 // zero as they must never be used during aggregation. 1696 continue; 1697 } else if (!$this->aggregateoutcomes && $gradeitem->is_outcome_item()) { 1698 // We will not aggregate outcome items, so we can ignore updating their weights. 1699 continue; 1700 } else if (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE) { 1701 // We will not aggregate the scales, so we can ignore upating their weights. 1702 continue; 1703 } else if (!$oldextracreditcalculation && $gradeitem->aggregationcoef > 0 && $gradeitem->weightoverride) { 1704 // For an item with extra credit ignore other weigths and overrides but do not change anything at all 1705 // if it's weight was already overridden. 1706 continue; 1707 } 1708 1709 // Store the previous value here, no need to update if it is the same value. 1710 $prevaggregationcoef2 = $gradeitem->aggregationcoef2; 1711 1712 if (!$oldextracreditcalculation && $gradeitem->aggregationcoef > 0 && !$gradeitem->weightoverride) { 1713 // For an item with extra credit ignore other weigths and overrides. 1714 $gradeitem->aggregationcoef2 = $totalgrademax ? ($gradeitem->grademax / $totalgrademax) : 0; 1715 1716 } else if (!$gradeitem->weightoverride) { 1717 // Calculations with a grade maximum of zero will cause problems. Just set the weight to zero. 1718 if ($totaloverriddenweight >= 1 || $totalnonoverriddengrademax == 0 || $gradeitem->grademax == 0) { 1719 // There is no more weight to distribute. 1720 $gradeitem->aggregationcoef2 = 0; 1721 } else { 1722 // Calculate this item's weight as a percentage of the non-overridden total grade maxes 1723 // then convert it to a proportion of the available non-overriden weight. 1724 $gradeitem->aggregationcoef2 = ($gradeitem->grademax/$totalnonoverriddengrademax) * 1725 (1 - $totaloverriddenweight); 1726 } 1727 1728 } else if ((!$automaticgradeitemspresent && $normalisetotal != 1) || ($requiresnormalising) 1729 || $overridearray[$gradeitem->id]['weight'] < 0) { 1730 // Just divide the overriden weight for this item against the total weight override of all 1731 // items in this category. 1732 if ($normalisetotal == 0 || $overridearray[$gradeitem->id]['weight'] < 0) { 1733 // If the normalised total equals zero, or the weight value is less than zero, 1734 // set the weight for the grade item to zero. 1735 $gradeitem->aggregationcoef2 = 0; 1736 } else { 1737 $gradeitem->aggregationcoef2 = $overridearray[$gradeitem->id]['weight'] / $normalisetotal; 1738 } 1739 } 1740 1741 if (grade_floatval($prevaggregationcoef2) !== grade_floatval($gradeitem->aggregationcoef2)) { 1742 // Update the grade item to reflect these changes. 1743 $gradeitem->update(); 1744 } 1745 } 1746 } 1747 1748 /** 1749 * Given an array of grade values (numerical indices) applies droplow or keephigh rules to limit the final array. 1750 * 1751 * @param array $grade_values itemid=>$grade_value float 1752 * @param array $items grade item objects 1753 * @return array Limited grades. 1754 */ 1755 public function apply_limit_rules(&$grade_values, $items) { 1756 $extraused = $this->is_extracredit_used(); 1757 1758 if (!empty($this->droplow)) { 1759 asort($grade_values, SORT_NUMERIC); 1760 $dropped = 0; 1761 1762 // If we have fewer grade items available to drop than $this->droplow, use this flag to escape the loop 1763 // May occur because of "extra credit" or if droplow is higher than the number of grade items 1764 $droppedsomething = true; 1765 1766 while ($dropped < $this->droplow && $droppedsomething) { 1767 $droppedsomething = false; 1768 1769 $grade_keys = array_keys($grade_values); 1770 $gradekeycount = count($grade_keys); 1771 1772 if ($gradekeycount === 0) { 1773 //We've dropped all grade items 1774 break; 1775 } 1776 1777 $originalindex = $founditemid = $foundmax = null; 1778 1779 // Find the first remaining grade item that is available to be dropped 1780 foreach ($grade_keys as $gradekeyindex=>$gradekey) { 1781 if (!$extraused || $items[$gradekey]->aggregationcoef <= 0) { 1782 // Found a non-extra credit grade item that is eligible to be dropped 1783 $originalindex = $gradekeyindex; 1784 $founditemid = $grade_keys[$originalindex]; 1785 $foundmax = $items[$founditemid]->grademax; 1786 break; 1787 } 1788 } 1789 1790 if (empty($founditemid)) { 1791 // No grade items available to drop 1792 break; 1793 } 1794 1795 // Now iterate over the remaining grade items 1796 // We're looking for other grade items with the same grade value but a higher grademax 1797 $i = 1; 1798 while ($originalindex + $i < $gradekeycount) { 1799 1800 $possibleitemid = $grade_keys[$originalindex+$i]; 1801 $i++; 1802 1803 if ($grade_values[$founditemid] != $grade_values[$possibleitemid]) { 1804 // The next grade item has a different grade value. Stop looking. 1805 break; 1806 } 1807 1808 if ($extraused && $items[$possibleitemid]->aggregationcoef > 0) { 1809 // Don't drop extra credit grade items. Continue the search. 1810 continue; 1811 } 1812 1813 if ($foundmax < $items[$possibleitemid]->grademax) { 1814 // Found a grade item with the same grade value and a higher grademax 1815 $foundmax = $items[$possibleitemid]->grademax; 1816 $founditemid = $possibleitemid; 1817 // Continue searching to see if there is an even higher grademax 1818 } 1819 } 1820 1821 // Now drop whatever grade item we have found 1822 unset($grade_values[$founditemid]); 1823 $dropped++; 1824 $droppedsomething = true; 1825 } 1826 1827 } else if (!empty($this->keephigh)) { 1828 arsort($grade_values, SORT_NUMERIC); 1829 $kept = 0; 1830 1831 foreach ($grade_values as $itemid=>$value) { 1832 1833 if ($extraused and $items[$itemid]->aggregationcoef > 0) { 1834 // we keep all extra credits 1835 1836 } else if ($kept < $this->keephigh) { 1837 $kept++; 1838 1839 } else { 1840 unset($grade_values[$itemid]); 1841 } 1842 } 1843 } 1844 } 1845 1846 /** 1847 * Returns whether or not we can apply the limit rules. 1848 * 1849 * There are cases where drop lowest or keep highest should not be used 1850 * at all. This method will determine whether or not this logic can be 1851 * applied considering the current setup of the category. 1852 * 1853 * @return bool 1854 */ 1855 public function can_apply_limit_rules() { 1856 if ($this->canapplylimitrules !== null) { 1857 return $this->canapplylimitrules; 1858 } 1859 1860 // Set it to be supported by default. 1861 $this->canapplylimitrules = true; 1862 1863 // Natural aggregation. 1864 if ($this->aggregation == GRADE_AGGREGATE_SUM) { 1865 $canapply = true; 1866 1867 // Check until one child breaks the rules. 1868 $gradeitems = $this->get_children(); 1869 $validitems = 0; 1870 $lastweight = null; 1871 $lastmaxgrade = null; 1872 foreach ($gradeitems as $gradeitem) { 1873 $gi = $gradeitem['object']; 1874 1875 if ($gradeitem['type'] == 'category') { 1876 // Sub categories are not allowed because they can have dynamic weights/maxgrades. 1877 $canapply = false; 1878 break; 1879 } 1880 1881 if ($gi->aggregationcoef > 0) { 1882 // Extra credit items are not allowed. 1883 $canapply = false; 1884 break; 1885 } 1886 1887 if ($lastweight !== null && $lastweight != $gi->aggregationcoef2) { 1888 // One of the weight differs from another item. 1889 $canapply = false; 1890 break; 1891 } 1892 1893 if ($lastmaxgrade !== null && $lastmaxgrade != $gi->grademax) { 1894 // One of the max grade differ from another item. This is not allowed for now 1895 // because we could be end up with different max grade between users for this category. 1896 $canapply = false; 1897 break; 1898 } 1899 1900 $lastweight = $gi->aggregationcoef2; 1901 $lastmaxgrade = $gi->grademax; 1902 } 1903 1904 $this->canapplylimitrules = $canapply; 1905 } 1906 1907 return $this->canapplylimitrules; 1908 } 1909 1910 /** 1911 * Returns true if category uses extra credit of any kind 1912 * 1913 * @return bool True if extra credit used 1914 */ 1915 public function is_extracredit_used() { 1916 return self::aggregation_uses_extracredit($this->aggregation); 1917 } 1918 1919 /** 1920 * Returns true if aggregation passed is using extracredit. 1921 * 1922 * @param int $aggregation Aggregation const. 1923 * @return bool True if extra credit used 1924 */ 1925 public static function aggregation_uses_extracredit($aggregation) { 1926 return ($aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2 1927 or $aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN 1928 or $aggregation == GRADE_AGGREGATE_SUM); 1929 } 1930 1931 /** 1932 * Returns true if category uses special aggregation coefficient 1933 * 1934 * @return bool True if an aggregation coefficient is being used 1935 */ 1936 public function is_aggregationcoef_used() { 1937 return self::aggregation_uses_aggregationcoef($this->aggregation); 1938 1939 } 1940 1941 /** 1942 * Returns true if aggregation uses aggregationcoef 1943 * 1944 * @param int $aggregation Aggregation const. 1945 * @return bool True if an aggregation coefficient is being used 1946 */ 1947 public static function aggregation_uses_aggregationcoef($aggregation) { 1948 return ($aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN 1949 or $aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2 1950 or $aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN 1951 or $aggregation == GRADE_AGGREGATE_SUM); 1952 1953 } 1954 1955 /** 1956 * Recursive function to find which weight/extra credit field to use in the grade item form. 1957 * 1958 * @param string $first Whether or not this is the first item in the recursion 1959 * @return string 1960 */ 1961 public function get_coefstring($first=true) { 1962 if (!is_null($this->coefstring)) { 1963 return $this->coefstring; 1964 } 1965 1966 $overriding_coefstring = null; 1967 1968 // Stop recursing upwards if this category has no parent 1969 if (!$first) { 1970 1971 if ($parent_category = $this->load_parent_category()) { 1972 return $parent_category->get_coefstring(false); 1973 1974 } else { 1975 return null; 1976 } 1977 1978 } else if ($first) { 1979 1980 if ($parent_category = $this->load_parent_category()) { 1981 $overriding_coefstring = $parent_category->get_coefstring(false); 1982 } 1983 } 1984 1985 // If an overriding coefstring has trickled down from one of the parent categories, return it. Otherwise, return self. 1986 if (!is_null($overriding_coefstring)) { 1987 return $overriding_coefstring; 1988 } 1989 1990 // No parent category is overriding this category's aggregation, return its string 1991 if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) { 1992 $this->coefstring = 'aggregationcoefweight'; 1993 1994 } else if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) { 1995 $this->coefstring = 'aggregationcoefextrasum'; 1996 1997 } else if ($this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN) { 1998 $this->coefstring = 'aggregationcoefextraweight'; 1999 2000 } else if ($this->aggregation == GRADE_AGGREGATE_SUM) { 2001 $this->coefstring = 'aggregationcoefextraweightsum'; 2002 2003 } else { 2004 $this->coefstring = 'aggregationcoef'; 2005 } 2006 return $this->coefstring; 2007 } 2008 2009 /** 2010 * Returns tree with all grade_items and categories as elements 2011 * 2012 * @param int $courseid The course ID 2013 * @param bool $include_category_items as category children 2014 * @return array 2015 */ 2016 public static function fetch_course_tree($courseid, $include_category_items=false) { 2017 $course_category = grade_category::fetch_course_category($courseid); 2018 $category_array = array('object'=>$course_category, 'type'=>'category', 'depth'=>1, 2019 'children'=>$course_category->get_children($include_category_items)); 2020 2021 $course_category->sortorder = $course_category->get_sortorder(); 2022 $sortorder = $course_category->get_sortorder(); 2023 return grade_category::_fetch_course_tree_recursion($category_array, $sortorder); 2024 } 2025 2026 /** 2027 * An internal function that recursively sorts grade categories within a course 2028 * 2029 * @param array $category_array The seed of the recursion 2030 * @param int $sortorder The current sortorder 2031 * @return array An array containing 'object', 'type', 'depth' and optionally 'children' 2032 */ 2033 static private function _fetch_course_tree_recursion($category_array, &$sortorder) { 2034 if (isset($category_array['object']->gradetype) && $category_array['object']->gradetype==GRADE_TYPE_NONE) { 2035 return null; 2036 } 2037 2038 // store the grade_item or grade_category instance with extra info 2039 $result = array('object'=>$category_array['object'], 'type'=>$category_array['type'], 'depth'=>$category_array['depth']); 2040 2041 // reuse final grades if there 2042 if (array_key_exists('finalgrades', $category_array)) { 2043 $result['finalgrades'] = $category_array['finalgrades']; 2044 } 2045 2046 // recursively resort children 2047 if (!empty($category_array['children'])) { 2048 $result['children'] = array(); 2049 //process the category item first 2050 $child = null; 2051 2052 foreach ($category_array['children'] as $oldorder=>$child_array) { 2053 2054 if ($child_array['type'] == 'courseitem' or $child_array['type'] == 'categoryitem') { 2055 $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder); 2056 if (!empty($child)) { 2057 $result['children'][$sortorder] = $child; 2058 } 2059 } 2060 } 2061 2062 foreach ($category_array['children'] as $oldorder=>$child_array) { 2063 2064 if ($child_array['type'] != 'courseitem' and $child_array['type'] != 'categoryitem') { 2065 $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder); 2066 if (!empty($child)) { 2067 $result['children'][++$sortorder] = $child; 2068 } 2069 } 2070 } 2071 } 2072 2073 return $result; 2074 } 2075 2076 /** 2077 * Fetches and returns all the children categories and/or grade_items belonging to this category. 2078 * By default only returns the immediate children (depth=1), but deeper levels can be requested, 2079 * as well as all levels (0). The elements are indexed by sort order. 2080 * 2081 * @param bool $include_category_items Whether or not to include category grade_items in the children array 2082 * @return array Array of child objects (grade_category and grade_item). 2083 */ 2084 public function get_children($include_category_items=false) { 2085 global $DB; 2086 2087 // This function must be as fast as possible ;-) 2088 // fetch all course grade items and categories into memory - we do not expect hundreds of these in course 2089 // we have to limit the number of queries though, because it will be used often in grade reports 2090 2091 $cats = $DB->get_records('grade_categories', array('courseid' => $this->courseid)); 2092 $items = $DB->get_records('grade_items', array('courseid' => $this->courseid)); 2093 2094 // init children array first 2095 foreach ($cats as $catid=>$cat) { 2096 $cats[$catid]->children = array(); 2097 } 2098 2099 //first attach items to cats and add category sortorder 2100 foreach ($items as $item) { 2101 2102 if ($item->itemtype == 'course' or $item->itemtype == 'category') { 2103 $cats[$item->iteminstance]->sortorder = $item->sortorder; 2104 2105 if (!$include_category_items) { 2106 continue; 2107 } 2108 $categoryid = $item->iteminstance; 2109 2110 } else { 2111 $categoryid = $item->categoryid; 2112 if (empty($categoryid)) { 2113 debugging('Found a grade item that isnt in a category'); 2114 } 2115 } 2116 2117 // prevent problems with duplicate sortorders in db 2118 $sortorder = $item->sortorder; 2119 2120 while (array_key_exists($categoryid, $cats) 2121 && array_key_exists($sortorder, $cats[$categoryid]->children)) { 2122 2123 $sortorder++; 2124 } 2125 2126 $cats[$categoryid]->children[$sortorder] = $item; 2127 2128 } 2129 2130 // now find the requested category and connect categories as children 2131 $category = false; 2132 2133 foreach ($cats as $catid=>$cat) { 2134 2135 if (empty($cat->parent)) { 2136 2137 if ($cat->path !== '/'.$cat->id.'/') { 2138 $grade_category = new grade_category($cat, false); 2139 $grade_category->path = '/'.$cat->id.'/'; 2140 $grade_category->depth = 1; 2141 $grade_category->update('system'); 2142 return $this->get_children($include_category_items); 2143 } 2144 2145 } else { 2146 2147 if (empty($cat->path) or !preg_match('|/'.$cat->parent.'/'.$cat->id.'/$|', $cat->path)) { 2148 //fix paths and depts 2149 static $recursioncounter = 0; // prevents infinite recursion 2150 $recursioncounter++; 2151 2152 if ($recursioncounter < 5) { 2153 // fix paths and depths! 2154 $grade_category = new grade_category($cat, false); 2155 $grade_category->depth = 0; 2156 $grade_category->path = null; 2157 $grade_category->update('system'); 2158 return $this->get_children($include_category_items); 2159 } 2160 } 2161 // prevent problems with duplicate sortorders in db 2162 $sortorder = $cat->sortorder; 2163 2164 while (array_key_exists($sortorder, $cats[$cat->parent]->children)) { 2165 //debugging("$sortorder exists in cat loop"); 2166 $sortorder++; 2167 } 2168 2169 $cats[$cat->parent]->children[$sortorder] = &$cats[$catid]; 2170 } 2171 2172 if ($catid == $this->id) { 2173 $category = &$cats[$catid]; 2174 } 2175 } 2176 2177 unset($items); // not needed 2178 unset($cats); // not needed 2179 2180 $children_array = array(); 2181 if (is_object($category)) { 2182 $children_array = grade_category::_get_children_recursion($category); 2183 ksort($children_array); 2184 } 2185 2186 return $children_array; 2187 2188 } 2189 2190 /** 2191 * Private method used to retrieve all children of this category recursively 2192 * 2193 * @param grade_category $category Source of current recursion 2194 * @return array An array of child grade categories 2195 */ 2196 private static function _get_children_recursion($category) { 2197 2198 $children_array = array(); 2199 foreach ($category->children as $sortorder=>$child) { 2200 2201 if (property_exists($child, 'itemtype')) { 2202 $grade_item = new grade_item($child, false); 2203 2204 if (in_array($grade_item->itemtype, array('course', 'category'))) { 2205 $type = $grade_item->itemtype.'item'; 2206 $depth = $category->depth; 2207 2208 } else { 2209 $type = 'item'; 2210 $depth = $category->depth; // we use this to set the same colour 2211 } 2212 $children_array[$sortorder] = array('object'=>$grade_item, 'type'=>$type, 'depth'=>$depth); 2213 2214 } else { 2215 $children = grade_category::_get_children_recursion($child); 2216 $grade_category = new grade_category($child, false); 2217 2218 if (empty($children)) { 2219 $children = array(); 2220 } 2221 $children_array[$sortorder] = array('object'=>$grade_category, 'type'=>'category', 'depth'=>$grade_category->depth, 'children'=>$children); 2222 } 2223 } 2224 2225 // sort the array 2226 ksort($children_array); 2227 2228 return $children_array; 2229 } 2230 2231 /** 2232 * Uses {@link get_grade_item()} to load or create a grade_item, then saves it as $this->grade_item. 2233 * 2234 * @return grade_item 2235 */ 2236 public function load_grade_item() { 2237 if (empty($this->grade_item)) { 2238 $this->grade_item = $this->get_grade_item(); 2239 } 2240 return $this->grade_item; 2241 } 2242 2243 /** 2244 * Retrieves this grade categories' associated grade_item from the database 2245 * 2246 * If no grade_item exists yet, creates one. 2247 * 2248 * @return grade_item 2249 */ 2250 public function get_grade_item() { 2251 if (empty($this->id)) { 2252 debugging("Attempt to obtain a grade_category's associated grade_item without the category's ID being set."); 2253 return false; 2254 } 2255 2256 if (empty($this->parent)) { 2257 $params = array('courseid'=>$this->courseid, 'itemtype'=>'course', 'iteminstance'=>$this->id); 2258 2259 } else { 2260 $params = array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id); 2261 } 2262 2263 if (!$grade_items = grade_item::fetch_all($params)) { 2264 // create a new one 2265 $grade_item = new grade_item($params, false); 2266 $grade_item->gradetype = GRADE_TYPE_VALUE; 2267 $grade_item->insert('system'); 2268 2269 } else if (count($grade_items) == 1) { 2270 // found existing one 2271 $grade_item = reset($grade_items); 2272 2273 } else { 2274 debugging("Found more than one grade_item attached to category id:".$this->id); 2275 // return first one 2276 $grade_item = reset($grade_items); 2277 } 2278 2279 return $grade_item; 2280 } 2281 2282 /** 2283 * Uses $this->parent to instantiate $this->parent_category based on the referenced record in the DB 2284 * 2285 * @return grade_category The parent category 2286 */ 2287 public function load_parent_category() { 2288 if (empty($this->parent_category) && !empty($this->parent)) { 2289 $this->parent_category = $this->get_parent_category(); 2290 } 2291 return $this->parent_category; 2292 } 2293 2294 /** 2295 * Uses $this->parent to instantiate and return a grade_category object 2296 * 2297 * @return grade_category Returns the parent category or null if this category has no parent 2298 */ 2299 public function get_parent_category() { 2300 if (!empty($this->parent)) { 2301 $parent_category = new grade_category(array('id' => $this->parent)); 2302 return $parent_category; 2303 } else { 2304 return null; 2305 } 2306 } 2307 2308 /** 2309 * Returns the most descriptive field for this grade category 2310 * 2311 * @return string name 2312 * @param bool $escape Whether the returned category name is to be HTML escaped or not. 2313 */ 2314 public function get_name($escape = true) { 2315 global $DB; 2316 // For a course category, we return the course name if the fullname is set to '?' in the DB (empty in the category edit form) 2317 if (empty($this->parent) && $this->fullname == '?') { 2318 $course = $DB->get_record('course', array('id'=> $this->courseid)); 2319 return format_string($course->fullname, false, ['context' => context_course::instance($this->courseid), 2320 'escape' => $escape]); 2321 2322 } else { 2323 // Grade categories can't be set up at system context (unlike scales and outcomes) 2324 // We therefore must have a courseid, and don't need to handle system contexts when filtering. 2325 return format_string($this->fullname, false, ['context' => context_course::instance($this->courseid), 2326 'escape' => $escape]); 2327 } 2328 } 2329 2330 /** 2331 * Describe the aggregation settings for this category so the reports make more sense. 2332 * 2333 * @return string description 2334 */ 2335 public function get_description() { 2336 $allhelp = array(); 2337 if ($this->aggregation != GRADE_AGGREGATE_SUM) { 2338 $aggrstrings = grade_helper::get_aggregation_strings(); 2339 $allhelp[] = $aggrstrings[$this->aggregation]; 2340 } 2341 2342 if ($this->droplow && $this->can_apply_limit_rules()) { 2343 $allhelp[] = get_string('droplowestvalues', 'grades', $this->droplow); 2344 } 2345 if ($this->keephigh && $this->can_apply_limit_rules()) { 2346 $allhelp[] = get_string('keephighestvalues', 'grades', $this->keephigh); 2347 } 2348 if (!$this->aggregateonlygraded) { 2349 $allhelp[] = get_string('aggregatenotonlygraded', 'grades'); 2350 } 2351 if ($allhelp) { 2352 return implode('. ', $allhelp) . '.'; 2353 } 2354 return ''; 2355 } 2356 2357 /** 2358 * Sets this category's parent id 2359 * 2360 * @param int $parentid The ID of the category that is the new parent to $this 2361 * @param string $source From where was the object updated (mod/forum, manual, etc.) 2362 * @return bool success 2363 */ 2364 public function set_parent($parentid, $source=null) { 2365 if ($this->parent == $parentid) { 2366 return true; 2367 } 2368 2369 if ($parentid == $this->id) { 2370 print_error('cannotassignselfasparent'); 2371 } 2372 2373 if (empty($this->parent) and $this->is_course_category()) { 2374 print_error('cannothaveparentcate'); 2375 } 2376 2377 // find parent and check course id 2378 if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) { 2379 return false; 2380 } 2381 2382 $this->force_regrading(); 2383 2384 // set new parent category 2385 $this->parent = $parent_category->id; 2386 $this->parent_category =& $parent_category; 2387 $this->path = null; // remove old path and depth - will be recalculated in update() 2388 $this->depth = 0; // remove old path and depth - will be recalculated in update() 2389 $this->update($source); 2390 2391 return $this->update($source); 2392 } 2393 2394 /** 2395 * Returns the final grade values for this grade category. 2396 * 2397 * @param int $userid Optional user ID to retrieve a single user's final grade 2398 * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade. 2399 */ 2400 public function get_final($userid=null) { 2401 $this->load_grade_item(); 2402 return $this->grade_item->get_final($userid); 2403 } 2404 2405 /** 2406 * Returns the sortorder of the grade categories' associated grade_item 2407 * 2408 * This method is also available in grade_item for cases where the object type is not known. 2409 * 2410 * @return int Sort order 2411 */ 2412 public function get_sortorder() { 2413 $this->load_grade_item(); 2414 return $this->grade_item->get_sortorder(); 2415 } 2416 2417 /** 2418 * Returns the idnumber of the grade categories' associated grade_item. 2419 * 2420 * This method is also available in grade_item for cases where the object type is not known. 2421 * 2422 * @return string idnumber 2423 */ 2424 public function get_idnumber() { 2425 $this->load_grade_item(); 2426 return $this->grade_item->get_idnumber(); 2427 } 2428 2429 /** 2430 * Sets the sortorder variable for this category. 2431 * 2432 * This method is also available in grade_item, for cases where the object type is not know. 2433 * 2434 * @param int $sortorder The sortorder to assign to this category 2435 */ 2436 public function set_sortorder($sortorder) { 2437 $this->load_grade_item(); 2438 $this->grade_item->set_sortorder($sortorder); 2439 } 2440 2441 /** 2442 * Move this category after the given sortorder 2443 * 2444 * Does not change the parent 2445 * 2446 * @param int $sortorder to place after. 2447 * @return void 2448 */ 2449 public function move_after_sortorder($sortorder) { 2450 $this->load_grade_item(); 2451 $this->grade_item->move_after_sortorder($sortorder); 2452 } 2453 2454 /** 2455 * Return true if this is the top most category that represents the total course grade. 2456 * 2457 * @return bool 2458 */ 2459 public function is_course_category() { 2460 $this->load_grade_item(); 2461 return $this->grade_item->is_course_item(); 2462 } 2463 2464 /** 2465 * Return the course level grade_category object 2466 * 2467 * @param int $courseid The Course ID 2468 * @return grade_category Returns the course level grade_category instance 2469 */ 2470 public static function fetch_course_category($courseid) { 2471 if (empty($courseid)) { 2472 debugging('Missing course id!'); 2473 return false; 2474 } 2475 2476 // course category has no parent 2477 if ($course_category = grade_category::fetch(array('courseid'=>$courseid, 'parent'=>null))) { 2478 return $course_category; 2479 } 2480 2481 // create a new one 2482 $course_category = new grade_category(); 2483 $course_category->insert_course_category($courseid); 2484 2485 return $course_category; 2486 } 2487 2488 /** 2489 * Is grading object editable? 2490 * 2491 * @return bool 2492 */ 2493 public function is_editable() { 2494 return true; 2495 } 2496 2497 /** 2498 * Returns the locked state/date of the grade categories' associated grade_item. 2499 * 2500 * This method is also available in grade_item, for cases where the object type is not known. 2501 * 2502 * @return bool 2503 */ 2504 public function is_locked() { 2505 $this->load_grade_item(); 2506 return $this->grade_item->is_locked(); 2507 } 2508 2509 /** 2510 * Sets the grade_item's locked variable and updates the grade_item. 2511 * 2512 * Calls set_locked() on the categories' grade_item 2513 * 2514 * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked. 2515 * @param bool $cascade lock/unlock child objects too 2516 * @param bool $refresh refresh grades when unlocking 2517 * @return bool success if category locked (not all children mayb be locked though) 2518 */ 2519 public function set_locked($lockedstate, $cascade=false, $refresh=true) { 2520 $this->load_grade_item(); 2521 2522 $result = $this->grade_item->set_locked($lockedstate, $cascade, true); 2523 2524 if ($cascade) { 2525 //process all children - items and categories 2526 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) { 2527 2528 foreach ($children as $child) { 2529 $child->set_locked($lockedstate, true, false); 2530 2531 if (empty($lockedstate) and $refresh) { 2532 //refresh when unlocking 2533 $child->refresh_grades(); 2534 } 2535 } 2536 } 2537 2538 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) { 2539 2540 foreach ($children as $child) { 2541 $child->set_locked($lockedstate, true, true); 2542 } 2543 } 2544 } 2545 2546 return $result; 2547 } 2548 2549 /** 2550 * Overrides grade_object::set_properties() to add special handling for changes to category aggregation types 2551 * 2552 * @param stdClass $instance the object to set the properties on 2553 * @param array|stdClass $params Either an associative array or an object containing property name, property value pairs 2554 */ 2555 public static function set_properties(&$instance, $params) { 2556 global $DB; 2557 2558 $fromaggregation = $instance->aggregation; 2559 2560 parent::set_properties($instance, $params); 2561 2562 // The aggregation method is changing and this category has already been saved. 2563 if (isset($params->aggregation) && !empty($instance->id)) { 2564 $achildwasdupdated = false; 2565 2566 // Get all its children. 2567 $children = $instance->get_children(); 2568 foreach ($children as $child) { 2569 $item = $child['object']; 2570 if ($child['type'] == 'category') { 2571 $item = $item->load_grade_item(); 2572 } 2573 2574 // Set the new aggregation fields. 2575 if ($item->set_aggregation_fields_for_aggregation($fromaggregation, $params->aggregation)) { 2576 $item->update(); 2577 $achildwasdupdated = true; 2578 } 2579 } 2580 2581 // If this is the course category, it is possible that its grade item was set as needsupdate 2582 // by one of its children. If we keep a reference to that stale object we might cause the 2583 // needsupdate flag to be lost. It's safer to just reload the grade_item from the database. 2584 if ($achildwasdupdated && !empty($instance->grade_item) && $instance->is_course_category()) { 2585 $instance->grade_item = null; 2586 $instance->load_grade_item(); 2587 } 2588 } 2589 } 2590 2591 /** 2592 * Sets the grade_item's hidden variable and updates the grade_item. 2593 * 2594 * Overrides grade_item::set_hidden() to add cascading of the hidden value to grade items in this grade category 2595 * 2596 * @param int $hidden 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until 2597 * @param bool $cascade apply to child objects too 2598 */ 2599 public function set_hidden($hidden, $cascade=false) { 2600 $this->load_grade_item(); 2601 //this hides the category itself and everything it contains 2602 parent::set_hidden($hidden, $cascade); 2603 2604 if ($cascade) { 2605 2606 // This hides the associated grade item (the course/category total). 2607 $this->grade_item->set_hidden($hidden, $cascade); 2608 2609 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) { 2610 2611 foreach ($children as $child) { 2612 if ($child->can_control_visibility()) { 2613 $child->set_hidden($hidden, $cascade); 2614 } 2615 } 2616 } 2617 2618 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) { 2619 2620 foreach ($children as $child) { 2621 $child->set_hidden($hidden, $cascade); 2622 } 2623 } 2624 } 2625 2626 //if marking category visible make sure parent category is visible MDL-21367 2627 if( !$hidden ) { 2628 $category_array = grade_category::fetch_all(array('id'=>$this->parent)); 2629 if ($category_array && array_key_exists($this->parent, $category_array)) { 2630 $category = $category_array[$this->parent]; 2631 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden 2632 $category->set_hidden($hidden, false); 2633 } 2634 } 2635 } 2636 2637 /** 2638 * Applies default settings on this category 2639 * 2640 * @return bool True if anything changed 2641 */ 2642 public function apply_default_settings() { 2643 global $CFG; 2644 2645 foreach ($this->forceable as $property) { 2646 2647 if (isset($CFG->{"grade_$property"})) { 2648 2649 if ($CFG->{"grade_$property"} == -1) { 2650 continue; //temporary bc before version bump 2651 } 2652 $this->$property = $CFG->{"grade_$property"}; 2653 } 2654 } 2655 } 2656 2657 /** 2658 * Applies forced settings on this category 2659 * 2660 * @return bool True if anything changed 2661 */ 2662 public function apply_forced_settings() { 2663 global $CFG; 2664 2665 $updated = false; 2666 2667 foreach ($this->forceable as $property) { 2668 2669 if (isset($CFG->{"grade_$property"}) and isset($CFG->{"grade_{$property}_flag"}) and 2670 ((int) $CFG->{"grade_{$property}_flag"} & 1)) { 2671 2672 if ($CFG->{"grade_$property"} == -1) { 2673 continue; //temporary bc before version bump 2674 } 2675 $this->$property = $CFG->{"grade_$property"}; 2676 $updated = true; 2677 } 2678 } 2679 2680 return $updated; 2681 } 2682 2683 /** 2684 * Notification of change in forced category settings. 2685 * 2686 * Causes all course and category grade items to be marked as needing to be updated 2687 */ 2688 public static function updated_forced_settings() { 2689 global $CFG, $DB; 2690 $params = array(1, 'course', 'category'); 2691 $sql = "UPDATE {grade_items} SET needsupdate=? WHERE itemtype=? or itemtype=?"; 2692 $DB->execute($sql, $params); 2693 } 2694 2695 /** 2696 * Determine the default aggregation values for a given aggregation method. 2697 * 2698 * @param int $aggregationmethod The aggregation method constant value. 2699 * @return array Containing the keys 'aggregationcoef', 'aggregationcoef2' and 'weightoverride'. 2700 */ 2701 public static function get_default_aggregation_coefficient_values($aggregationmethod) { 2702 $defaultcoefficients = array( 2703 'aggregationcoef' => 0, 2704 'aggregationcoef2' => 0, 2705 'weightoverride' => 0 2706 ); 2707 2708 switch ($aggregationmethod) { 2709 case GRADE_AGGREGATE_WEIGHTED_MEAN: 2710 $defaultcoefficients['aggregationcoef'] = 1; 2711 break; 2712 case GRADE_AGGREGATE_SUM: 2713 $defaultcoefficients['aggregationcoef2'] = 1; 2714 break; 2715 } 2716 2717 return $defaultcoefficients; 2718 } 2719 2720 /** 2721 * Cleans the cache. 2722 * 2723 * We invalidate them all so it can be completely reloaded. 2724 * 2725 * Being conservative here, if there is a new grade_category we purge them, the important part 2726 * is that this is not purged when there are no changes in grade_categories. 2727 * 2728 * @param bool $deleted 2729 * @return void 2730 */ 2731 protected function notify_changed($deleted) { 2732 self::clean_record_set(); 2733 } 2734 2735 /** 2736 * Generates a unique key per query. 2737 * 2738 * Not unique between grade_object children. self::retrieve_record_set and self::set_record_set will be in charge of 2739 * selecting the appropriate cache. 2740 * 2741 * @param array $params An array of conditions like $fieldname => $fieldvalue 2742 * @return string 2743 */ 2744 protected static function generate_record_set_key($params) { 2745 return sha1(json_encode($params)); 2746 } 2747 2748 /** 2749 * Tries to retrieve a record set from the cache. 2750 * 2751 * @param array $params The query params 2752 * @return grade_object[]|bool An array of grade_objects or false if not found. 2753 */ 2754 protected static function retrieve_record_set($params) { 2755 $cache = cache::make('core', 'grade_categories'); 2756 return $cache->get(self::generate_record_set_key($params)); 2757 } 2758 2759 /** 2760 * Sets a result to the records cache, even if there were no results. 2761 * 2762 * @param string $params The query params 2763 * @param grade_object[]|bool $records An array of grade_objects or false if there are no records matching the $key filters 2764 * @return void 2765 */ 2766 protected static function set_record_set($params, $records) { 2767 $cache = cache::make('core', 'grade_categories'); 2768 return $cache->set(self::generate_record_set_key($params), $records); 2769 } 2770 2771 /** 2772 * Cleans the cache. 2773 * 2774 * Aggressive deletion to be conservative given the gradebook design. 2775 * The key is based on the requested params, not easy nor worth to purge selectively. 2776 * 2777 * @return void 2778 */ 2779 public static function clean_record_set() { 2780 cache_helper::purge_by_event('changesingradecategories'); 2781 } 2782 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body