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