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