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