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