Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [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 overriden 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 // If the overridden weight total is higher than 1 then set the other untouched weights to zero. 1684 $setotherweightstozero = false; 1685 // Total up all of the weights. 1686 foreach ($overridearray as $gradeitemdetail) { 1687 // If the grade item has extra credit, then don't add it to the normalisetotal. 1688 if (!$gradeitemdetail['extracredit']) { 1689 $normalisetotal += $gradeitemdetail['weight']; 1690 } 1691 // The overridden total comprises of items that are set as overridden, that aren't extra credit and have a value 1692 // greater than zero. 1693 if ($gradeitemdetail['weightoverride'] && !$gradeitemdetail['extracredit'] && $gradeitemdetail['weight'] > 0) { 1694 // Add overriden weights up to see if they are greater than 1. 1695 $overriddentotal += $gradeitemdetail['weight']; 1696 } 1697 } 1698 if ($overriddentotal > 1) { 1699 // Make sure that this catergory of weights gets normalised. 1700 $requiresnormalising = true; 1701 // The normalised weights are only the overridden weights, so we just use the total of those. 1702 $normalisetotal = $overriddentotal; 1703 } 1704 1705 $totalnonoverriddengrademax = $totalgrademax - $totaloverriddengrademax; 1706 1707 // This setting indicates if we should use algorithm prior to MDL-49257 fix for calculating extra credit weights. 1708 // Even though old algorith has bugs in it, we need to preserve existing grades. 1709 $gradebookcalculationfreeze = (int)get_config('core', 'gradebook_calculations_freeze_' . $this->courseid); 1710 $oldextracreditcalculation = $gradebookcalculationfreeze && ($gradebookcalculationfreeze <= 20150619); 1711 1712 reset($children); 1713 foreach ($children as $sortorder => $child) { 1714 $gradeitem = null; 1715 1716 if ($child['type'] == 'item') { 1717 $gradeitem = $child['object']; 1718 } else if ($child['type'] == 'category') { 1719 $gradeitem = $child['object']->load_grade_item(); 1720 } 1721 1722 if ($gradeitem->gradetype == GRADE_TYPE_NONE || $gradeitem->gradetype == GRADE_TYPE_TEXT) { 1723 // Text items and none items do not have a weight, no need to set their weight to 1724 // zero as they must never be used during aggregation. 1725 continue; 1726 } else if (!$this->aggregateoutcomes && $gradeitem->is_outcome_item()) { 1727 // We will not aggregate outcome items, so we can ignore updating their weights. 1728 continue; 1729 } else if (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE) { 1730 // We will not aggregate the scales, so we can ignore upating their weights. 1731 continue; 1732 } else if (!$oldextracreditcalculation && $gradeitem->aggregationcoef > 0 && $gradeitem->weightoverride) { 1733 // For an item with extra credit ignore other weigths and overrides but do not change anything at all 1734 // if it's weight was already overridden. 1735 continue; 1736 } 1737 1738 // Store the previous value here, no need to update if it is the same value. 1739 $prevaggregationcoef2 = $gradeitem->aggregationcoef2; 1740 1741 if (!$oldextracreditcalculation && $gradeitem->aggregationcoef > 0 && !$gradeitem->weightoverride) { 1742 // For an item with extra credit ignore other weigths and overrides. 1743 $gradeitem->aggregationcoef2 = $totalgrademax ? ($gradeitem->grademax / $totalgrademax) : 0; 1744 1745 } else if (!$gradeitem->weightoverride) { 1746 // Calculations with a grade maximum of zero will cause problems. Just set the weight to zero. 1747 if ($totaloverriddenweight >= 1 || $totalnonoverriddengrademax == 0 || $gradeitem->grademax == 0) { 1748 // There is no more weight to distribute. 1749 $gradeitem->aggregationcoef2 = 0; 1750 } else { 1751 // Calculate this item's weight as a percentage of the non-overridden total grade maxes 1752 // then convert it to a proportion of the available non-overriden weight. 1753 $gradeitem->aggregationcoef2 = ($gradeitem->grademax/$totalnonoverriddengrademax) * 1754 (1 - $totaloverriddenweight); 1755 } 1756 1757 } else if ((!$automaticgradeitemspresent && $normalisetotal != 1) || ($requiresnormalising) 1758 || $overridearray[$gradeitem->id]['weight'] < 0) { 1759 // Just divide the overriden weight for this item against the total weight override of all 1760 // items in this category. 1761 if ($normalisetotal == 0 || $overridearray[$gradeitem->id]['weight'] < 0) { 1762 // If the normalised total equals zero, or the weight value is less than zero, 1763 // set the weight for the grade item to zero. 1764 $gradeitem->aggregationcoef2 = 0; 1765 } else { 1766 $gradeitem->aggregationcoef2 = $overridearray[$gradeitem->id]['weight'] / $normalisetotal; 1767 } 1768 } 1769 1770 if (grade_floatval($prevaggregationcoef2) !== grade_floatval($gradeitem->aggregationcoef2)) { 1771 // Update the grade item to reflect these changes. 1772 $gradeitem->update(); 1773 } 1774 } 1775 } 1776 1777 /** 1778 * Given an array of grade values (numerical indices) applies droplow or keephigh rules to limit the final array. 1779 * 1780 * @param array $grade_values itemid=>$grade_value float 1781 * @param array $items grade item objects 1782 * @return array Limited grades. 1783 */ 1784 public function apply_limit_rules(&$grade_values, $items) { 1785 $extraused = $this->is_extracredit_used(); 1786 1787 if (!empty($this->droplow)) { 1788 asort($grade_values, SORT_NUMERIC); 1789 $dropped = 0; 1790 1791 // If we have fewer grade items available to drop than $this->droplow, use this flag to escape the loop 1792 // May occur because of "extra credit" or if droplow is higher than the number of grade items 1793 $droppedsomething = true; 1794 1795 while ($dropped < $this->droplow && $droppedsomething) { 1796 $droppedsomething = false; 1797 1798 $grade_keys = array_keys($grade_values); 1799 $gradekeycount = count($grade_keys); 1800 1801 if ($gradekeycount === 0) { 1802 //We've dropped all grade items 1803 break; 1804 } 1805 1806 $originalindex = $founditemid = $foundmax = null; 1807 1808 // Find the first remaining grade item that is available to be dropped 1809 foreach ($grade_keys as $gradekeyindex=>$gradekey) { 1810 if (!$extraused || $items[$gradekey]->aggregationcoef <= 0) { 1811 // Found a non-extra credit grade item that is eligible to be dropped 1812 $originalindex = $gradekeyindex; 1813 $founditemid = $grade_keys[$originalindex]; 1814 $foundmax = $items[$founditemid]->grademax; 1815 break; 1816 } 1817 } 1818 1819 if (empty($founditemid)) { 1820 // No grade items available to drop 1821 break; 1822 } 1823 1824 // Now iterate over the remaining grade items 1825 // We're looking for other grade items with the same grade value but a higher grademax 1826 $i = 1; 1827 while ($originalindex + $i < $gradekeycount) { 1828 1829 $possibleitemid = $grade_keys[$originalindex+$i]; 1830 $i++; 1831 1832 if ($grade_values[$founditemid] != $grade_values[$possibleitemid]) { 1833 // The next grade item has a different grade value. Stop looking. 1834 break; 1835 } 1836 1837 if ($extraused && $items[$possibleitemid]->aggregationcoef > 0) { 1838 // Don't drop extra credit grade items. Continue the search. 1839 continue; 1840 } 1841 1842 if ($foundmax < $items[$possibleitemid]->grademax) { 1843 // Found a grade item with the same grade value and a higher grademax 1844 $foundmax = $items[$possibleitemid]->grademax; 1845 $founditemid = $possibleitemid; 1846 // Continue searching to see if there is an even higher grademax 1847 } 1848 } 1849 1850 // Now drop whatever grade item we have found 1851 unset($grade_values[$founditemid]); 1852 $dropped++; 1853 $droppedsomething = true; 1854 } 1855 1856 } else if (!empty($this->keephigh)) { 1857 arsort($grade_values, SORT_NUMERIC); 1858 $kept = 0; 1859 1860 foreach ($grade_values as $itemid=>$value) { 1861 1862 if ($extraused and $items[$itemid]->aggregationcoef > 0) { 1863 // we keep all extra credits 1864 1865 } else if ($kept < $this->keephigh) { 1866 $kept++; 1867 1868 } else { 1869 unset($grade_values[$itemid]); 1870 } 1871 } 1872 } 1873 } 1874 1875 /** 1876 * Returns whether or not we can apply the limit rules. 1877 * 1878 * There are cases where drop lowest or keep highest should not be used 1879 * at all. This method will determine whether or not this logic can be 1880 * applied considering the current setup of the category. 1881 * 1882 * @return bool 1883 */ 1884 public function can_apply_limit_rules() { 1885 if ($this->canapplylimitrules !== null) { 1886 return $this->canapplylimitrules; 1887 } 1888 1889 // Set it to be supported by default. 1890 $this->canapplylimitrules = true; 1891 1892 // Natural aggregation. 1893 if ($this->aggregation == GRADE_AGGREGATE_SUM) { 1894 $canapply = true; 1895 1896 // Check until one child breaks the rules. 1897 $gradeitems = $this->get_children(); 1898 $validitems = 0; 1899 $lastweight = null; 1900 $lastmaxgrade = null; 1901 foreach ($gradeitems as $gradeitem) { 1902 $gi = $gradeitem['object']; 1903 1904 if ($gradeitem['type'] == 'category') { 1905 // Sub categories are not allowed because they can have dynamic weights/maxgrades. 1906 $canapply = false; 1907 break; 1908 } 1909 1910 if ($gi->aggregationcoef > 0) { 1911 // Extra credit items are not allowed. 1912 $canapply = false; 1913 break; 1914 } 1915 1916 if ($lastweight !== null && $lastweight != $gi->aggregationcoef2) { 1917 // One of the weight differs from another item. 1918 $canapply = false; 1919 break; 1920 } 1921 1922 if ($lastmaxgrade !== null && $lastmaxgrade != $gi->grademax) { 1923 // One of the max grade differ from another item. This is not allowed for now 1924 // because we could be end up with different max grade between users for this category. 1925 $canapply = false; 1926 break; 1927 } 1928 1929 $lastweight = $gi->aggregationcoef2; 1930 $lastmaxgrade = $gi->grademax; 1931 } 1932 1933 $this->canapplylimitrules = $canapply; 1934 } 1935 1936 return $this->canapplylimitrules; 1937 } 1938 1939 /** 1940 * Returns true if category uses extra credit of any kind 1941 * 1942 * @return bool True if extra credit used 1943 */ 1944 public function is_extracredit_used() { 1945 return self::aggregation_uses_extracredit($this->aggregation); 1946 } 1947 1948 /** 1949 * Returns true if aggregation passed is using extracredit. 1950 * 1951 * @param int $aggregation Aggregation const. 1952 * @return bool True if extra credit used 1953 */ 1954 public static function aggregation_uses_extracredit($aggregation) { 1955 return ($aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2 1956 or $aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN 1957 or $aggregation == GRADE_AGGREGATE_SUM); 1958 } 1959 1960 /** 1961 * Returns true if category uses special aggregation coefficient 1962 * 1963 * @return bool True if an aggregation coefficient is being used 1964 */ 1965 public function is_aggregationcoef_used() { 1966 return self::aggregation_uses_aggregationcoef($this->aggregation); 1967 1968 } 1969 1970 /** 1971 * Returns true if aggregation uses aggregationcoef 1972 * 1973 * @param int $aggregation Aggregation const. 1974 * @return bool True if an aggregation coefficient is being used 1975 */ 1976 public static function aggregation_uses_aggregationcoef($aggregation) { 1977 return ($aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN 1978 or $aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2 1979 or $aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN 1980 or $aggregation == GRADE_AGGREGATE_SUM); 1981 1982 } 1983 1984 /** 1985 * Recursive function to find which weight/extra credit field to use in the grade item form. 1986 * 1987 * @param string $first Whether or not this is the first item in the recursion 1988 * @return string 1989 */ 1990 public function get_coefstring($first=true) { 1991 if (!is_null($this->coefstring)) { 1992 return $this->coefstring; 1993 } 1994 1995 $overriding_coefstring = null; 1996 1997 // Stop recursing upwards if this category has no parent 1998 if (!$first) { 1999 2000 if ($parent_category = $this->load_parent_category()) { 2001 return $parent_category->get_coefstring(false); 2002 2003 } else { 2004 return null; 2005 } 2006 2007 } else if ($first) { 2008 2009 if ($parent_category = $this->load_parent_category()) { 2010 $overriding_coefstring = $parent_category->get_coefstring(false); 2011 } 2012 } 2013 2014 // If an overriding coefstring has trickled down from one of the parent categories, return it. Otherwise, return self. 2015 if (!is_null($overriding_coefstring)) { 2016 return $overriding_coefstring; 2017 } 2018 2019 // No parent category is overriding this category's aggregation, return its string 2020 if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) { 2021 $this->coefstring = 'aggregationcoefweight'; 2022 2023 } else if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) { 2024 $this->coefstring = 'aggregationcoefextrasum'; 2025 2026 } else if ($this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN) { 2027 $this->coefstring = 'aggregationcoefextraweight'; 2028 2029 } else if ($this->aggregation == GRADE_AGGREGATE_SUM) { 2030 $this->coefstring = 'aggregationcoefextraweightsum'; 2031 2032 } else { 2033 $this->coefstring = 'aggregationcoef'; 2034 } 2035 return $this->coefstring; 2036 } 2037 2038 /** 2039 * Returns tree with all grade_items and categories as elements 2040 * 2041 * @param int $courseid The course ID 2042 * @param bool $include_category_items as category children 2043 * @return array 2044 */ 2045 public static function fetch_course_tree($courseid, $include_category_items=false) { 2046 $course_category = grade_category::fetch_course_category($courseid); 2047 $category_array = array('object'=>$course_category, 'type'=>'category', 'depth'=>1, 2048 'children'=>$course_category->get_children($include_category_items)); 2049 2050 $course_category->sortorder = $course_category->get_sortorder(); 2051 $sortorder = $course_category->get_sortorder(); 2052 return grade_category::_fetch_course_tree_recursion($category_array, $sortorder); 2053 } 2054 2055 /** 2056 * An internal function that recursively sorts grade categories within a course 2057 * 2058 * @param array $category_array The seed of the recursion 2059 * @param int $sortorder The current sortorder 2060 * @return array An array containing 'object', 'type', 'depth' and optionally 'children' 2061 */ 2062 static private function _fetch_course_tree_recursion($category_array, &$sortorder) { 2063 if (isset($category_array['object']->gradetype) && $category_array['object']->gradetype==GRADE_TYPE_NONE) { 2064 return null; 2065 } 2066 2067 // store the grade_item or grade_category instance with extra info 2068 $result = array('object'=>$category_array['object'], 'type'=>$category_array['type'], 'depth'=>$category_array['depth']); 2069 2070 // reuse final grades if there 2071 if (array_key_exists('finalgrades', $category_array)) { 2072 $result['finalgrades'] = $category_array['finalgrades']; 2073 } 2074 2075 // recursively resort children 2076 if (!empty($category_array['children'])) { 2077 $result['children'] = array(); 2078 //process the category item first 2079 $child = null; 2080 2081 foreach ($category_array['children'] as $oldorder=>$child_array) { 2082 2083 if ($child_array['type'] == 'courseitem' or $child_array['type'] == 'categoryitem') { 2084 $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder); 2085 if (!empty($child)) { 2086 $result['children'][$sortorder] = $child; 2087 } 2088 } 2089 } 2090 2091 foreach ($category_array['children'] as $oldorder=>$child_array) { 2092 2093 if ($child_array['type'] != 'courseitem' and $child_array['type'] != 'categoryitem') { 2094 $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder); 2095 if (!empty($child)) { 2096 $result['children'][++$sortorder] = $child; 2097 } 2098 } 2099 } 2100 } 2101 2102 return $result; 2103 } 2104 2105 /** 2106 * Fetches and returns all the children categories and/or grade_items belonging to this category. 2107 * By default only returns the immediate children (depth=1), but deeper levels can be requested, 2108 * as well as all levels (0). The elements are indexed by sort order. 2109 * 2110 * @param bool $include_category_items Whether or not to include category grade_items in the children array 2111 * @return array Array of child objects (grade_category and grade_item). 2112 */ 2113 public function get_children($include_category_items=false) { 2114 global $DB; 2115 2116 // This function must be as fast as possible ;-) 2117 // fetch all course grade items and categories into memory - we do not expect hundreds of these in course 2118 // we have to limit the number of queries though, because it will be used often in grade reports 2119 2120 $cats = $DB->get_records('grade_categories', array('courseid' => $this->courseid)); 2121 $items = $DB->get_records('grade_items', array('courseid' => $this->courseid)); 2122 2123 // init children array first 2124 foreach ($cats as $catid=>$cat) { 2125 $cats[$catid]->children = array(); 2126 } 2127 2128 //first attach items to cats and add category sortorder 2129 foreach ($items as $item) { 2130 2131 if ($item->itemtype == 'course' or $item->itemtype == 'category') { 2132 $cats[$item->iteminstance]->sortorder = $item->sortorder; 2133 2134 if (!$include_category_items) { 2135 continue; 2136 } 2137 $categoryid = $item->iteminstance; 2138 2139 } else { 2140 $categoryid = $item->categoryid; 2141 if (empty($categoryid)) { 2142 debugging('Found a grade item that isnt in a category'); 2143 } 2144 } 2145 2146 // prevent problems with duplicate sortorders in db 2147 $sortorder = $item->sortorder; 2148 2149 while (array_key_exists($categoryid, $cats) 2150 && array_key_exists($sortorder, $cats[$categoryid]->children)) { 2151 2152 $sortorder++; 2153 } 2154 2155 $cats[$categoryid]->children[$sortorder] = $item; 2156 2157 } 2158 2159 // now find the requested category and connect categories as children 2160 $category = false; 2161 2162 foreach ($cats as $catid=>$cat) { 2163 2164 if (empty($cat->parent)) { 2165 2166 if ($cat->path !== '/'.$cat->id.'/') { 2167 $grade_category = new grade_category($cat, false); 2168 $grade_category->path = '/'.$cat->id.'/'; 2169 $grade_category->depth = 1; 2170 $grade_category->update('system'); 2171 return $this->get_children($include_category_items); 2172 } 2173 2174 } else { 2175 2176 if (empty($cat->path) or !preg_match('|/'.$cat->parent.'/'.$cat->id.'/$|', $cat->path)) { 2177 //fix paths and depts 2178 static $recursioncounter = 0; // prevents infinite recursion 2179 $recursioncounter++; 2180 2181 if ($recursioncounter < 5) { 2182 // fix paths and depths! 2183 $grade_category = new grade_category($cat, false); 2184 $grade_category->depth = 0; 2185 $grade_category->path = null; 2186 $grade_category->update('system'); 2187 return $this->get_children($include_category_items); 2188 } 2189 } 2190 // prevent problems with duplicate sortorders in db 2191 $sortorder = $cat->sortorder; 2192 2193 while (array_key_exists($sortorder, $cats[$cat->parent]->children)) { 2194 //debugging("$sortorder exists in cat loop"); 2195 $sortorder++; 2196 } 2197 2198 $cats[$cat->parent]->children[$sortorder] = &$cats[$catid]; 2199 } 2200 2201 if ($catid == $this->id) { 2202 $category = &$cats[$catid]; 2203 } 2204 } 2205 2206 unset($items); // not needed 2207 unset($cats); // not needed 2208 2209 $children_array = array(); 2210 if (is_object($category)) { 2211 $children_array = grade_category::_get_children_recursion($category); 2212 ksort($children_array); 2213 } 2214 2215 return $children_array; 2216 2217 } 2218 2219 /** 2220 * Private method used to retrieve all children of this category recursively 2221 * 2222 * @param grade_category $category Source of current recursion 2223 * @return array An array of child grade categories 2224 */ 2225 private static function _get_children_recursion($category) { 2226 2227 $children_array = array(); 2228 foreach ($category->children as $sortorder=>$child) { 2229 2230 if (property_exists($child, 'itemtype')) { 2231 $grade_item = new grade_item($child, false); 2232 2233 if (in_array($grade_item->itemtype, array('course', 'category'))) { 2234 $type = $grade_item->itemtype.'item'; 2235 $depth = $category->depth; 2236 2237 } else { 2238 $type = 'item'; 2239 $depth = $category->depth; // we use this to set the same colour 2240 } 2241 $children_array[$sortorder] = array('object'=>$grade_item, 'type'=>$type, 'depth'=>$depth); 2242 2243 } else { 2244 $children = grade_category::_get_children_recursion($child); 2245 $grade_category = new grade_category($child, false); 2246 2247 if (empty($children)) { 2248 $children = array(); 2249 } 2250 $children_array[$sortorder] = array('object'=>$grade_category, 'type'=>'category', 'depth'=>$grade_category->depth, 'children'=>$children); 2251 } 2252 } 2253 2254 // sort the array 2255 ksort($children_array); 2256 2257 return $children_array; 2258 } 2259 2260 /** 2261 * Uses {@link get_grade_item()} to load or create a grade_item, then saves it as $this->grade_item. 2262 * 2263 * @return grade_item 2264 */ 2265 public function load_grade_item() { 2266 if (empty($this->grade_item)) { 2267 $this->grade_item = $this->get_grade_item(); 2268 } 2269 return $this->grade_item; 2270 } 2271 2272 /** 2273 * Retrieves this grade categories' associated grade_item from the database 2274 * 2275 * If no grade_item exists yet, creates one. 2276 * 2277 * @return grade_item 2278 */ 2279 public function get_grade_item() { 2280 if (empty($this->id)) { 2281 debugging("Attempt to obtain a grade_category's associated grade_item without the category's ID being set."); 2282 return false; 2283 } 2284 2285 if (empty($this->parent)) { 2286 $params = array('courseid'=>$this->courseid, 'itemtype'=>'course', 'iteminstance'=>$this->id); 2287 2288 } else { 2289 $params = array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id); 2290 } 2291 2292 if (!$grade_items = grade_item::fetch_all($params)) { 2293 // create a new one 2294 $grade_item = new grade_item($params, false); 2295 $grade_item->gradetype = GRADE_TYPE_VALUE; 2296 $grade_item->insert('system'); 2297 2298 } else if (count($grade_items) == 1) { 2299 // found existing one 2300 $grade_item = reset($grade_items); 2301 2302 } else { 2303 debugging("Found more than one grade_item attached to category id:".$this->id); 2304 // return first one 2305 $grade_item = reset($grade_items); 2306 } 2307 2308 return $grade_item; 2309 } 2310 2311 /** 2312 * Uses $this->parent to instantiate $this->parent_category based on the referenced record in the DB 2313 * 2314 * @return grade_category The parent category 2315 */ 2316 public function load_parent_category() { 2317 if (empty($this->parent_category) && !empty($this->parent)) { 2318 $this->parent_category = $this->get_parent_category(); 2319 } 2320 return $this->parent_category; 2321 } 2322 2323 /** 2324 * Uses $this->parent to instantiate and return a grade_category object 2325 * 2326 * @return grade_category Returns the parent category or null if this category has no parent 2327 */ 2328 public function get_parent_category() { 2329 if (!empty($this->parent)) { 2330 $parent_category = new grade_category(array('id' => $this->parent)); 2331 return $parent_category; 2332 } else { 2333 return null; 2334 } 2335 } 2336 2337 /** 2338 * Returns the most descriptive field for this grade category 2339 * 2340 * @return string name 2341 * @param bool $escape Whether the returned category name is to be HTML escaped or not. 2342 */ 2343 public function get_name($escape = true) { 2344 global $DB; 2345 // For a course category, we return the course name if the fullname is set to '?' in the DB (empty in the category edit form) 2346 if (empty($this->parent) && $this->fullname == '?') { 2347 $course = $DB->get_record('course', array('id'=> $this->courseid)); 2348 return format_string($course->fullname, false, ['context' => context_course::instance($this->courseid), 2349 'escape' => $escape]); 2350 2351 } else { 2352 // Grade categories can't be set up at system context (unlike scales and outcomes) 2353 // We therefore must have a courseid, and don't need to handle system contexts when filtering. 2354 return format_string($this->fullname, false, ['context' => context_course::instance($this->courseid), 2355 'escape' => $escape]); 2356 } 2357 } 2358 2359 /** 2360 * Describe the aggregation settings for this category so the reports make more sense. 2361 * 2362 * @return string description 2363 */ 2364 public function get_description() { 2365 $allhelp = array(); 2366 if ($this->aggregation != GRADE_AGGREGATE_SUM) { 2367 $aggrstrings = grade_helper::get_aggregation_strings(); 2368 $allhelp[] = $aggrstrings[$this->aggregation]; 2369 } 2370 2371 if ($this->droplow && $this->can_apply_limit_rules()) { 2372 $allhelp[] = get_string('droplowestvalues', 'grades', $this->droplow); 2373 } 2374 if ($this->keephigh && $this->can_apply_limit_rules()) { 2375 $allhelp[] = get_string('keephighestvalues', 'grades', $this->keephigh); 2376 } 2377 if (!$this->aggregateonlygraded) { 2378 $allhelp[] = get_string('aggregatenotonlygraded', 'grades'); 2379 } 2380 if ($allhelp) { 2381 return implode('. ', $allhelp) . '.'; 2382 } 2383 return ''; 2384 } 2385 2386 /** 2387 * Sets this category's parent id 2388 * 2389 * @param int $parentid The ID of the category that is the new parent to $this 2390 * @param string $source From where was the object updated (mod/forum, manual, etc.) 2391 * @return bool success 2392 */ 2393 public function set_parent($parentid, $source=null) { 2394 if ($this->parent == $parentid) { 2395 return true; 2396 } 2397 2398 if ($parentid == $this->id) { 2399 throw new \moodle_exception('cannotassignselfasparent'); 2400 } 2401 2402 if (empty($this->parent) and $this->is_course_category()) { 2403 throw new \moodle_exception('cannothaveparentcate'); 2404 } 2405 2406 // find parent and check course id 2407 if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) { 2408 return false; 2409 } 2410 2411 $this->force_regrading(); 2412 2413 // set new parent category 2414 $this->parent = $parent_category->id; 2415 $this->parent_category =& $parent_category; 2416 $this->path = null; // remove old path and depth - will be recalculated in update() 2417 $this->depth = 0; // remove old path and depth - will be recalculated in update() 2418 $this->update($source); 2419 2420 return $this->update($source); 2421 } 2422 2423 /** 2424 * Returns the final grade values for this grade category. 2425 * 2426 * @param int $userid Optional user ID to retrieve a single user's final grade 2427 * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade. 2428 */ 2429 public function get_final($userid=null) { 2430 $this->load_grade_item(); 2431 return $this->grade_item->get_final($userid); 2432 } 2433 2434 /** 2435 * Returns the sortorder of the grade categories' associated grade_item 2436 * 2437 * This method is also available in grade_item for cases where the object type is not known. 2438 * 2439 * @return int Sort order 2440 */ 2441 public function get_sortorder() { 2442 $this->load_grade_item(); 2443 return $this->grade_item->get_sortorder(); 2444 } 2445 2446 /** 2447 * Returns the idnumber of the grade categories' associated grade_item. 2448 * 2449 * This method is also available in grade_item for cases where the object type is not known. 2450 * 2451 * @return string idnumber 2452 */ 2453 public function get_idnumber() { 2454 $this->load_grade_item(); 2455 return $this->grade_item->get_idnumber(); 2456 } 2457 2458 /** 2459 * Sets the sortorder variable for this category. 2460 * 2461 * This method is also available in grade_item, for cases where the object type is not know. 2462 * 2463 * @param int $sortorder The sortorder to assign to this category 2464 */ 2465 public function set_sortorder($sortorder) { 2466 $this->load_grade_item(); 2467 $this->grade_item->set_sortorder($sortorder); 2468 } 2469 2470 /** 2471 * Move this category after the given sortorder 2472 * 2473 * Does not change the parent 2474 * 2475 * @param int $sortorder to place after. 2476 * @return void 2477 */ 2478 public function move_after_sortorder($sortorder) { 2479 $this->load_grade_item(); 2480 $this->grade_item->move_after_sortorder($sortorder); 2481 } 2482 2483 /** 2484 * Return true if this is the top most category that represents the total course grade. 2485 * 2486 * @return bool 2487 */ 2488 public function is_course_category() { 2489 $this->load_grade_item(); 2490 return $this->grade_item->is_course_item(); 2491 } 2492 2493 /** 2494 * Return the course level grade_category object 2495 * 2496 * @param int $courseid The Course ID 2497 * @return grade_category Returns the course level grade_category instance 2498 */ 2499 public static function fetch_course_category($courseid) { 2500 if (empty($courseid)) { 2501 debugging('Missing course id!'); 2502 return false; 2503 } 2504 2505 // course category has no parent 2506 if ($course_category = grade_category::fetch(array('courseid'=>$courseid, 'parent'=>null))) { 2507 return $course_category; 2508 } 2509 2510 // create a new one 2511 $course_category = new grade_category(); 2512 $course_category->insert_course_category($courseid); 2513 2514 return $course_category; 2515 } 2516 2517 /** 2518 * Is grading object editable? 2519 * 2520 * @return bool 2521 */ 2522 public function is_editable() { 2523 return true; 2524 } 2525 2526 /** 2527 * Returns the locked state/date of the grade categories' associated grade_item. 2528 * 2529 * This method is also available in grade_item, for cases where the object type is not known. 2530 * 2531 * @return bool 2532 */ 2533 public function is_locked() { 2534 $this->load_grade_item(); 2535 return $this->grade_item->is_locked(); 2536 } 2537 2538 /** 2539 * Sets the grade_item's locked variable and updates the grade_item. 2540 * 2541 * Calls set_locked() on the categories' grade_item 2542 * 2543 * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked. 2544 * @param bool $cascade lock/unlock child objects too 2545 * @param bool $refresh refresh grades when unlocking 2546 * @return bool success if category locked (not all children mayb be locked though) 2547 */ 2548 public function set_locked($lockedstate, $cascade=false, $refresh=true) { 2549 $this->load_grade_item(); 2550 2551 $result = $this->grade_item->set_locked($lockedstate, $cascade, true); 2552 2553 // Process all children - items and categories. 2554 if ($children = grade_item::fetch_all(['categoryid' => $this->id])) { 2555 foreach ($children as $child) { 2556 $child->set_locked($lockedstate, $cascade, false); 2557 2558 if (empty($lockedstate) && $refresh) { 2559 // Refresh when unlocking. 2560 $child->refresh_grades(); 2561 } 2562 } 2563 } 2564 2565 if ($children = static::fetch_all(['parent' => $this->id])) { 2566 foreach ($children as $child) { 2567 $child->set_locked($lockedstate, $cascade, true); 2568 } 2569 } 2570 2571 return $result; 2572 } 2573 2574 /** 2575 * Overrides grade_object::set_properties() to add special handling for changes to category aggregation types 2576 * 2577 * @param grade_category $instance the object to set the properties on 2578 * @param array|stdClass $params Either an associative array or an object containing property name, property value pairs 2579 */ 2580 public static function set_properties(&$instance, $params) { 2581 global $DB; 2582 2583 $fromaggregation = $instance->aggregation; 2584 2585 parent::set_properties($instance, $params); 2586 2587 // The aggregation method is changing and this category has already been saved. 2588 if (isset($params->aggregation) && !empty($instance->id)) { 2589 $achildwasdupdated = false; 2590 2591 // Get all its children. 2592 $children = $instance->get_children(); 2593 foreach ($children as $child) { 2594 $item = $child['object']; 2595 if ($child['type'] == 'category') { 2596 $item = $item->load_grade_item(); 2597 } 2598 2599 // Set the new aggregation fields. 2600 if ($item->set_aggregation_fields_for_aggregation($fromaggregation, $params->aggregation)) { 2601 $item->update(); 2602 $achildwasdupdated = true; 2603 } 2604 } 2605 2606 // If this is the course category, it is possible that its grade item was set as needsupdate 2607 // by one of its children. If we keep a reference to that stale object we might cause the 2608 // needsupdate flag to be lost. It's safer to just reload the grade_item from the database. 2609 if ($achildwasdupdated && !empty($instance->grade_item) && $instance->is_course_category()) { 2610 $instance->grade_item = null; 2611 $instance->load_grade_item(); 2612 } 2613 } 2614 } 2615 2616 /** 2617 * Sets the grade_item's hidden variable and updates the grade_item. 2618 * 2619 * Overrides grade_item::set_hidden() to add cascading of the hidden value to grade items in this grade category 2620 * 2621 * @param int $hidden 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until 2622 * @param bool $cascade apply to child objects too 2623 */ 2624 public function set_hidden($hidden, $cascade=false) { 2625 $this->load_grade_item(); 2626 //this hides the category itself and everything it contains 2627 parent::set_hidden($hidden, $cascade); 2628 2629 if ($cascade) { 2630 2631 // This hides the associated grade item (the course/category total). 2632 $this->grade_item->set_hidden($hidden, $cascade); 2633 2634 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) { 2635 2636 foreach ($children as $child) { 2637 if ($child->can_control_visibility()) { 2638 $child->set_hidden($hidden, $cascade); 2639 } 2640 } 2641 } 2642 2643 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) { 2644 2645 foreach ($children as $child) { 2646 $child->set_hidden($hidden, $cascade); 2647 } 2648 } 2649 } 2650 2651 //if marking category visible make sure parent category is visible MDL-21367 2652 if( !$hidden ) { 2653 $category_array = grade_category::fetch_all(array('id'=>$this->parent)); 2654 if ($category_array && array_key_exists($this->parent, $category_array)) { 2655 $category = $category_array[$this->parent]; 2656 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden 2657 $category->set_hidden($hidden, false); 2658 } 2659 } 2660 } 2661 2662 /** 2663 * Applies default settings on this category 2664 * 2665 * @return bool True if anything changed 2666 */ 2667 public function apply_default_settings() { 2668 global $CFG; 2669 2670 foreach ($this->forceable as $property) { 2671 2672 if (isset($CFG->{"grade_$property"})) { 2673 2674 if ($CFG->{"grade_$property"} == -1) { 2675 continue; //temporary bc before version bump 2676 } 2677 $this->$property = $CFG->{"grade_$property"}; 2678 } 2679 } 2680 } 2681 2682 /** 2683 * Applies forced settings on this category 2684 * 2685 * @return bool True if anything changed 2686 */ 2687 public function apply_forced_settings() { 2688 global $CFG; 2689 2690 $updated = false; 2691 2692 foreach ($this->forceable as $property) { 2693 2694 if (isset($CFG->{"grade_$property"}) and isset($CFG->{"grade_{$property}_flag"}) and 2695 ((int) $CFG->{"grade_{$property}_flag"} & 1)) { 2696 2697 if ($CFG->{"grade_$property"} == -1) { 2698 continue; //temporary bc before version bump 2699 } 2700 $this->$property = $CFG->{"grade_$property"}; 2701 $updated = true; 2702 } 2703 } 2704 2705 return $updated; 2706 } 2707 2708 /** 2709 * Notification of change in forced category settings. 2710 * 2711 * Causes all course and category grade items to be marked as needing to be updated 2712 */ 2713 public static function updated_forced_settings() { 2714 global $CFG, $DB; 2715 $params = array(1, 'course', 'category'); 2716 $sql = "UPDATE {grade_items} SET needsupdate=? WHERE itemtype=? or itemtype=?"; 2717 $DB->execute($sql, $params); 2718 } 2719 2720 /** 2721 * Determine the default aggregation values for a given aggregation method. 2722 * 2723 * @param int $aggregationmethod The aggregation method constant value. 2724 * @return array Containing the keys 'aggregationcoef', 'aggregationcoef2' and 'weightoverride'. 2725 */ 2726 public static function get_default_aggregation_coefficient_values($aggregationmethod) { 2727 $defaultcoefficients = array( 2728 'aggregationcoef' => 0, 2729 'aggregationcoef2' => 0, 2730 'weightoverride' => 0 2731 ); 2732 2733 switch ($aggregationmethod) { 2734 case GRADE_AGGREGATE_WEIGHTED_MEAN: 2735 $defaultcoefficients['aggregationcoef'] = 1; 2736 break; 2737 case GRADE_AGGREGATE_SUM: 2738 $defaultcoefficients['aggregationcoef2'] = 1; 2739 break; 2740 } 2741 2742 return $defaultcoefficients; 2743 } 2744 2745 /** 2746 * Cleans the cache. 2747 * 2748 * We invalidate them all so it can be completely reloaded. 2749 * 2750 * Being conservative here, if there is a new grade_category we purge them, the important part 2751 * is that this is not purged when there are no changes in grade_categories. 2752 * 2753 * @param bool $deleted 2754 * @return void 2755 */ 2756 protected function notify_changed($deleted) { 2757 self::clean_record_set(); 2758 } 2759 2760 /** 2761 * Generates a unique key per query. 2762 * 2763 * Not unique between grade_object children. self::retrieve_record_set and self::set_record_set will be in charge of 2764 * selecting the appropriate cache. 2765 * 2766 * @param array $params An array of conditions like $fieldname => $fieldvalue 2767 * @return string 2768 */ 2769 protected static function generate_record_set_key($params) { 2770 return sha1(json_encode($params)); 2771 } 2772 2773 /** 2774 * Tries to retrieve a record set from the cache. 2775 * 2776 * @param array $params The query params 2777 * @return grade_object[]|bool An array of grade_objects or false if not found. 2778 */ 2779 protected static function retrieve_record_set($params) { 2780 $cache = cache::make('core', 'grade_categories'); 2781 return $cache->get(self::generate_record_set_key($params)); 2782 } 2783 2784 /** 2785 * Sets a result to the records cache, even if there were no results. 2786 * 2787 * @param string $params The query params 2788 * @param grade_object[]|bool $records An array of grade_objects or false if there are no records matching the $key filters 2789 * @return void 2790 */ 2791 protected static function set_record_set($params, $records) { 2792 $cache = cache::make('core', 'grade_categories'); 2793 return $cache->set(self::generate_record_set_key($params), $records); 2794 } 2795 2796 /** 2797 * Cleans the cache. 2798 * 2799 * Aggressive deletion to be conservative given the gradebook design. 2800 * The key is based on the requested params, not easy nor worth to purge selectively. 2801 * 2802 * @return void 2803 */ 2804 public static function clean_record_set() { 2805 cache_helper::purge_by_event('changesingradecategories'); 2806 } 2807 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body