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