|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 00003 // This file is part of Moodle - http://moodle.org/ 00004 // 00005 // Moodle is free software: you can redistribute it and/or modify 00006 // it under the terms of the GNU General Public License as published by 00007 // the Free Software Foundation, either version 3 of the License, or 00008 // (at your option) any later version. 00009 // 00010 // Moodle is distributed in the hope that it will be useful, 00011 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00012 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00013 // GNU General Public License for more details. 00014 // 00015 // You should have received a copy of the GNU General Public License 00016 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00026 defined('MOODLE_INTERNAL') || die(); 00027 00028 require_once('grade_object.php'); 00029 00038 class grade_category extends grade_object { 00043 public $table = 'grade_categories'; 00044 00049 public $required_fields = array('id', 'courseid', 'parent', 'depth', 'path', 'fullname', 'aggregation', 00050 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes', 00051 'aggregatesubcats', 'timecreated', 'timemodified', 'hidden'); 00052 00057 public $courseid; 00058 00063 public $parent; 00064 00069 public $parent_category; 00070 00075 public $depth = 0; 00076 00082 public $path; 00083 00088 public $fullname; 00089 00094 public $aggregation = GRADE_AGGREGATE_MEAN; 00095 00100 public $keephigh = 0; 00101 00106 public $droplow = 0; 00107 00112 public $aggregateonlygraded = 0; 00113 00118 public $aggregateoutcomes = 0; 00119 00124 public $aggregatesubcats = 0; 00125 00130 public $children; 00131 00137 public $all_children; 00138 00144 public $grade_item; 00145 00149 public $sortorder; 00150 00154 public $forceable = array('aggregation', 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes', 'aggregatesubcats'); 00155 00159 public $coefstring = null; 00160 00171 public static function build_path($grade_category) { 00172 global $DB; 00173 00174 if (empty($grade_category->parent)) { 00175 return '/'.$grade_category->id.'/'; 00176 00177 } else { 00178 $parent = $DB->get_record('grade_categories', array('id' => $grade_category->parent)); 00179 return grade_category::build_path($parent).$grade_category->id.'/'; 00180 } 00181 } 00182 00190 public static function fetch($params) { 00191 return grade_object::fetch_helper('grade_categories', 'grade_category', $params); 00192 } 00193 00201 public static function fetch_all($params) { 00202 return grade_object::fetch_all_helper('grade_categories', 'grade_category', $params); 00203 } 00204 00210 public function update($source=null) { 00211 // load the grade item or create a new one 00212 $this->load_grade_item(); 00213 00214 // force recalculation of path; 00215 if (empty($this->path)) { 00216 $this->path = grade_category::build_path($this); 00217 $this->depth = substr_count($this->path, '/') - 1; 00218 $updatechildren = true; 00219 00220 } else { 00221 $updatechildren = false; 00222 } 00223 00224 $this->apply_forced_settings(); 00225 00226 // these are exclusive 00227 if ($this->droplow > 0) { 00228 $this->keephigh = 0; 00229 00230 } else if ($this->keephigh > 0) { 00231 $this->droplow = 0; 00232 } 00233 00234 // Recalculate grades if needed 00235 if ($this->qualifies_for_regrading()) { 00236 $this->force_regrading(); 00237 } 00238 00239 $this->timemodified = time(); 00240 00241 $result = parent::update($source); 00242 00243 // now update paths in all child categories 00244 if ($result and $updatechildren) { 00245 00246 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) { 00247 00248 foreach ($children as $child) { 00249 $child->path = null; 00250 $child->depth = 0; 00251 $child->update($source); 00252 } 00253 } 00254 } 00255 00256 return $result; 00257 } 00258 00264 public function delete($source=null) { 00265 $grade_item = $this->load_grade_item(); 00266 00267 if ($this->is_course_category()) { 00268 00269 if ($categories = grade_category::fetch_all(array('courseid'=>$this->courseid))) { 00270 00271 foreach ($categories as $category) { 00272 00273 if ($category->id == $this->id) { 00274 continue; // do not delete course category yet 00275 } 00276 $category->delete($source); 00277 } 00278 } 00279 00280 if ($items = grade_item::fetch_all(array('courseid'=>$this->courseid))) { 00281 00282 foreach ($items as $item) { 00283 00284 if ($item->id == $grade_item->id) { 00285 continue; // do not delete course item yet 00286 } 00287 $item->delete($source); 00288 } 00289 } 00290 00291 } else { 00292 $this->force_regrading(); 00293 00294 $parent = $this->load_parent_category(); 00295 00296 // Update children's categoryid/parent field first 00297 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) { 00298 foreach ($children as $child) { 00299 $child->set_parent($parent->id); 00300 } 00301 } 00302 00303 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) { 00304 foreach ($children as $child) { 00305 $child->set_parent($parent->id); 00306 } 00307 } 00308 } 00309 00310 // first delete the attached grade item and grades 00311 $grade_item->delete($source); 00312 00313 // delete category itself 00314 return parent::delete($source); 00315 } 00316 00326 public function insert($source=null) { 00327 00328 if (empty($this->courseid)) { 00329 print_error('cannotinsertgrade'); 00330 } 00331 00332 if (empty($this->parent)) { 00333 $course_category = grade_category::fetch_course_category($this->courseid); 00334 $this->parent = $course_category->id; 00335 } 00336 00337 $this->path = null; 00338 00339 $this->timecreated = $this->timemodified = time(); 00340 00341 if (!parent::insert($source)) { 00342 debugging("Could not insert this category: " . print_r($this, true)); 00343 return false; 00344 } 00345 00346 $this->force_regrading(); 00347 00348 // build path and depth 00349 $this->update($source); 00350 00351 return $this->id; 00352 } 00353 00362 public function insert_course_category($courseid) { 00363 $this->courseid = $courseid; 00364 $this->fullname = '?'; 00365 $this->path = null; 00366 $this->parent = null; 00367 $this->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; 00368 00369 $this->apply_default_settings(); 00370 $this->apply_forced_settings(); 00371 00372 $this->timecreated = $this->timemodified = time(); 00373 00374 if (!parent::insert('system')) { 00375 debugging("Could not insert this category: " . print_r($this, true)); 00376 return false; 00377 } 00378 00379 // build path and depth 00380 $this->update('system'); 00381 00382 return $this->id; 00383 } 00384 00391 public function qualifies_for_regrading() { 00392 if (empty($this->id)) { 00393 debugging("Can not regrade non existing category"); 00394 return false; 00395 } 00396 00397 $db_item = grade_category::fetch(array('id'=>$this->id)); 00398 00399 $aggregationdiff = $db_item->aggregation != $this->aggregation; 00400 $keephighdiff = $db_item->keephigh != $this->keephigh; 00401 $droplowdiff = $db_item->droplow != $this->droplow; 00402 $aggonlygrddiff = $db_item->aggregateonlygraded != $this->aggregateonlygraded; 00403 $aggoutcomesdiff = $db_item->aggregateoutcomes != $this->aggregateoutcomes; 00404 $aggsubcatsdiff = $db_item->aggregatesubcats != $this->aggregatesubcats; 00405 00406 return ($aggregationdiff || $keephighdiff || $droplowdiff || $aggonlygrddiff || $aggoutcomesdiff || $aggsubcatsdiff); 00407 } 00408 00413 public function force_regrading() { 00414 $grade_item = $this->load_grade_item(); 00415 $grade_item->force_regrading(); 00416 } 00417 00437 public function generate_grades($userid=null) { 00438 global $CFG, $DB; 00439 00440 $this->load_grade_item(); 00441 00442 if ($this->grade_item->is_locked()) { 00443 return true; // no need to recalculate locked items 00444 } 00445 00446 // find grade items of immediate children (category or grade items) and force site settings 00447 $depends_on = $this->grade_item->depends_on(); 00448 00449 if (empty($depends_on)) { 00450 $items = false; 00451 00452 } else { 00453 list($usql, $params) = $DB->get_in_or_equal($depends_on); 00454 $sql = "SELECT * 00455 FROM {grade_items} 00456 WHERE id $usql"; 00457 $items = $DB->get_records_sql($sql, $params); 00458 } 00459 00460 // needed mostly for SUM agg type 00461 $this->auto_update_max($items); 00462 00463 $grade_inst = new grade_grade(); 00464 $fields = 'g.'.implode(',g.', $grade_inst->required_fields); 00465 00466 // where to look for final grades - include grade of this item too, we will store the results there 00467 $gis = array_merge($depends_on, array($this->grade_item->id)); 00468 list($usql, $params) = $DB->get_in_or_equal($gis); 00469 00470 if ($userid) { 00471 $usersql = "AND g.userid=?"; 00472 $params[] = $userid; 00473 00474 } else { 00475 $usersql = ""; 00476 } 00477 00478 $sql = "SELECT $fields 00479 FROM {grade_grades} g, {grade_items} gi 00480 WHERE gi.id = g.itemid AND gi.id $usql $usersql 00481 ORDER BY g.userid"; 00482 00483 // group the results by userid and aggregate the grades for this user 00484 $rs = $DB->get_recordset_sql($sql, $params); 00485 if ($rs->valid()) { 00486 $prevuser = 0; 00487 $grade_values = array(); 00488 $excluded = array(); 00489 $oldgrade = null; 00490 00491 foreach ($rs as $used) { 00492 00493 if ($used->userid != $prevuser) { 00494 $this->aggregate_grades($prevuser, $items, $grade_values, $oldgrade, $excluded); 00495 $prevuser = $used->userid; 00496 $grade_values = array(); 00497 $excluded = array(); 00498 $oldgrade = null; 00499 } 00500 $grade_values[$used->itemid] = $used->finalgrade; 00501 00502 if ($used->excluded) { 00503 $excluded[] = $used->itemid; 00504 } 00505 00506 if ($this->grade_item->id == $used->itemid) { 00507 $oldgrade = $used; 00508 } 00509 } 00510 $this->aggregate_grades($prevuser, $items, $grade_values, $oldgrade, $excluded);//the last one 00511 } 00512 $rs->close(); 00513 00514 return true; 00515 } 00516 00529 private function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded) { 00530 global $CFG; 00531 if (empty($userid)) { 00532 //ignore first call 00533 return; 00534 } 00535 00536 if ($oldgrade) { 00537 $oldfinalgrade = $oldgrade->finalgrade; 00538 $grade = new grade_grade($oldgrade, false); 00539 $grade->grade_item =& $this->grade_item; 00540 00541 } else { 00542 // insert final grade - it will be needed later anyway 00543 $grade = new grade_grade(array('itemid'=>$this->grade_item->id, 'userid'=>$userid), false); 00544 $grade->grade_item =& $this->grade_item; 00545 $grade->insert('system'); 00546 $oldfinalgrade = null; 00547 } 00548 00549 // no need to recalculate locked or overridden grades 00550 if ($grade->is_locked() or $grade->is_overridden()) { 00551 return; 00552 } 00553 00554 // can not use own final category grade in calculation 00555 unset($grade_values[$this->grade_item->id]); 00556 00557 00558 // sum is a special aggregation types - it adjusts the min max, does not use relative values 00559 if ($this->aggregation == GRADE_AGGREGATE_SUM) { 00560 $this->sum_grades($grade, $oldfinalgrade, $items, $grade_values, $excluded); 00561 return; 00562 } 00563 00564 // if no grades calculation possible or grading not allowed clear final grade 00565 if (empty($grade_values) or empty($items) or ($this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE)) { 00566 $grade->finalgrade = null; 00567 00568 if (!is_null($oldfinalgrade)) { 00569 $grade->update('aggregation'); 00570 } 00571 return; 00572 } 00573 00574 // normalize the grades first - all will have value 0...1 00575 // ungraded items are not used in aggregation 00576 foreach ($grade_values as $itemid=>$v) { 00577 00578 if (is_null($v)) { 00579 // null means no grade 00580 unset($grade_values[$itemid]); 00581 continue; 00582 00583 } else if (in_array($itemid, $excluded)) { 00584 unset($grade_values[$itemid]); 00585 continue; 00586 } 00587 $grade_values[$itemid] = grade_grade::standardise_score($v, $items[$itemid]->grademin, $items[$itemid]->grademax, 0, 1); 00588 } 00589 00590 // use min grade if grade missing for these types 00591 if (!$this->aggregateonlygraded) { 00592 00593 foreach ($items as $itemid=>$value) { 00594 00595 if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) { 00596 $grade_values[$itemid] = 0; 00597 } 00598 } 00599 } 00600 00601 // limit and sort 00602 $this->apply_limit_rules($grade_values, $items); 00603 asort($grade_values, SORT_NUMERIC); 00604 00605 // let's see we have still enough grades to do any statistics 00606 if (count($grade_values) == 0) { 00607 // not enough attempts yet 00608 $grade->finalgrade = null; 00609 00610 if (!is_null($oldfinalgrade)) { 00611 $grade->update('aggregation'); 00612 } 00613 return; 00614 } 00615 00616 // do the maths 00617 $agg_grade = $this->aggregate_values($grade_values, $items); 00618 00619 // recalculate the grade back to requested range 00620 $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax); 00621 00622 $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade); 00623 00624 // update in db if changed 00625 if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { 00626 $grade->update('aggregation'); 00627 } 00628 00629 return; 00630 } 00631 00641 public function aggregate_values($grade_values, $items) { 00642 switch ($this->aggregation) { 00643 00644 case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies 00645 $num = count($grade_values); 00646 $grades = array_values($grade_values); 00647 00648 if ($num % 2 == 0) { 00649 $agg_grade = ($grades[intval($num/2)-1] + $grades[intval($num/2)]) / 2; 00650 00651 } else { 00652 $agg_grade = $grades[intval(($num/2)-0.5)]; 00653 } 00654 break; 00655 00656 case GRADE_AGGREGATE_MIN: 00657 $agg_grade = reset($grade_values); 00658 break; 00659 00660 case GRADE_AGGREGATE_MAX: 00661 $agg_grade = array_pop($grade_values); 00662 break; 00663 00664 case GRADE_AGGREGATE_MODE: // the most common value, average used if multimode 00665 // array_count_values only counts INT and STRING, so if grades are floats we must convert them to string 00666 $converted_grade_values = array(); 00667 00668 foreach ($grade_values as $k => $gv) { 00669 00670 if (!is_int($gv) && !is_string($gv)) { 00671 $converted_grade_values[$k] = (string) $gv; 00672 00673 } else { 00674 $converted_grade_values[$k] = $gv; 00675 } 00676 } 00677 00678 $freq = array_count_values($converted_grade_values); 00679 arsort($freq); // sort by frequency keeping keys 00680 $top = reset($freq); // highest frequency count 00681 $modes = array_keys($freq, $top); // search for all modes (have the same highest count) 00682 rsort($modes, SORT_NUMERIC); // get highest mode 00683 $agg_grade = reset($modes); 00684 break; 00685 00686 case GRADE_AGGREGATE_WEIGHTED_MEAN: // Weighted average of all existing final grades, weight specified in coef 00687 $weightsum = 0; 00688 $sum = 0; 00689 00690 foreach ($grade_values as $itemid=>$grade_value) { 00691 00692 if ($items[$itemid]->aggregationcoef <= 0) { 00693 continue; 00694 } 00695 $weightsum += $items[$itemid]->aggregationcoef; 00696 $sum += $items[$itemid]->aggregationcoef * $grade_value; 00697 } 00698 00699 if ($weightsum == 0) { 00700 $agg_grade = null; 00701 00702 } else { 00703 $agg_grade = $sum / $weightsum; 00704 } 00705 break; 00706 00707 case GRADE_AGGREGATE_WEIGHTED_MEAN2: 00708 // Weighted average of all existing final grades with optional extra credit flag, 00709 // weight is the range of grade (usually grademax) 00710 $weightsum = 0; 00711 $sum = null; 00712 00713 foreach ($grade_values as $itemid=>$grade_value) { 00714 $weight = $items[$itemid]->grademax - $items[$itemid]->grademin; 00715 00716 if ($weight <= 0) { 00717 continue; 00718 } 00719 00720 if ($items[$itemid]->aggregationcoef == 0) { 00721 $weightsum += $weight; 00722 } 00723 $sum += $weight * $grade_value; 00724 } 00725 00726 if ($weightsum == 0) { 00727 $agg_grade = $sum; // only extra credits 00728 00729 } else { 00730 $agg_grade = $sum / $weightsum; 00731 } 00732 break; 00733 00734 case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average 00735 $num = 0; 00736 $sum = null; 00737 00738 foreach ($grade_values as $itemid=>$grade_value) { 00739 00740 if ($items[$itemid]->aggregationcoef == 0) { 00741 $num += 1; 00742 $sum += $grade_value; 00743 00744 } else if ($items[$itemid]->aggregationcoef > 0) { 00745 $sum += $items[$itemid]->aggregationcoef * $grade_value; 00746 } 00747 } 00748 00749 if ($num == 0) { 00750 $agg_grade = $sum; // only extra credits or wrong coefs 00751 00752 } else { 00753 $agg_grade = $sum / $num; 00754 } 00755 break; 00756 00757 case GRADE_AGGREGATE_MEAN: // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum) 00758 default: 00759 $num = count($grade_values); 00760 $sum = array_sum($grade_values); 00761 $agg_grade = $sum / $num; 00762 break; 00763 } 00764 00765 return $agg_grade; 00766 } 00767 00773 private function auto_update_max($items) { 00774 if ($this->aggregation != GRADE_AGGREGATE_SUM) { 00775 // not needed at all 00776 return; 00777 } 00778 00779 if (!$items) { 00780 00781 if ($this->grade_item->grademax != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) { 00782 $this->grade_item->grademax = 0; 00783 $this->grade_item->grademin = 0; 00784 $this->grade_item->gradetype = GRADE_TYPE_VALUE; 00785 $this->grade_item->update('aggregation'); 00786 } 00787 return; 00788 } 00789 00790 //find max grade possible 00791 $maxes = array(); 00792 00793 foreach ($items as $item) { 00794 00795 if ($item->aggregationcoef > 0) { 00796 // extra credit from this activity - does not affect total 00797 continue; 00798 } 00799 00800 if ($item->gradetype == GRADE_TYPE_VALUE) { 00801 $maxes[$item->id] = $item->grademax; 00802 00803 } else if ($item->gradetype == GRADE_TYPE_SCALE) { 00804 $maxes[$item->id] = $item->grademax; // 0 = nograde, 1 = first scale item, 2 = second scale item 00805 } 00806 } 00807 // apply droplow and keephigh 00808 $this->apply_limit_rules($maxes, $items); 00809 $max = array_sum($maxes); 00810 00811 // update db if anything changed 00812 if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) { 00813 $this->grade_item->grademax = $max; 00814 $this->grade_item->grademin = 0; 00815 $this->grade_item->gradetype = GRADE_TYPE_VALUE; 00816 $this->grade_item->update('aggregation'); 00817 } 00818 } 00819 00831 private function sum_grades(&$grade, $oldfinalgrade, $items, $grade_values, $excluded) { 00832 if (empty($items)) { 00833 return null; 00834 } 00835 00836 // ungraded and excluded items are not used in aggregation 00837 foreach ($grade_values as $itemid=>$v) { 00838 00839 if (is_null($v)) { 00840 unset($grade_values[$itemid]); 00841 00842 } else if (in_array($itemid, $excluded)) { 00843 unset($grade_values[$itemid]); 00844 } 00845 } 00846 00847 // use 0 if grade missing, droplow used and aggregating all items 00848 if (!$this->aggregateonlygraded and !empty($this->droplow)) { 00849 00850 foreach ($items as $itemid=>$value) { 00851 00852 if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) { 00853 $grade_values[$itemid] = 0; 00854 } 00855 } 00856 } 00857 00858 $this->apply_limit_rules($grade_values, $items); 00859 00860 $sum = array_sum($grade_values); 00861 $grade->finalgrade = $this->grade_item->bounded_grade($sum); 00862 00863 // update in db if changed 00864 if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) { 00865 $grade->update('aggregation'); 00866 } 00867 00868 return; 00869 } 00870 00880 public function apply_limit_rules(&$grade_values, $items) { 00881 $extraused = $this->is_extracredit_used(); 00882 00883 if (!empty($this->droplow)) { 00884 asort($grade_values, SORT_NUMERIC); 00885 $dropped = 0; 00886 00887 foreach ($grade_values as $itemid=>$value) { 00888 00889 if ($dropped < $this->droplow) { 00890 00891 if ($extraused and $items[$itemid]->aggregationcoef > 0) { 00892 // no drop low for extra credits 00893 00894 } else { 00895 unset($grade_values[$itemid]); 00896 $dropped++; 00897 } 00898 00899 } else { 00900 // we have dropped enough 00901 break; 00902 } 00903 } 00904 00905 } else if (!empty($this->keephigh)) { 00906 arsort($grade_values, SORT_NUMERIC); 00907 $kept = 0; 00908 00909 foreach ($grade_values as $itemid=>$value) { 00910 00911 if ($extraused and $items[$itemid]->aggregationcoef > 0) { 00912 // we keep all extra credits 00913 00914 } else if ($kept < $this->keephigh) { 00915 $kept++; 00916 00917 } else { 00918 unset($grade_values[$itemid]); 00919 } 00920 } 00921 } 00922 } 00923 00929 function is_extracredit_used() { 00930 return ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2 00931 or $this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN 00932 or $this->aggregation == GRADE_AGGREGATE_SUM); 00933 } 00934 00940 public function is_aggregationcoef_used() { 00941 return ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN 00942 or $this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2 00943 or $this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN 00944 or $this->aggregation == GRADE_AGGREGATE_SUM); 00945 00946 } 00947 00956 public function get_coefstring($first=true) { 00957 if (!is_null($this->coefstring)) { 00958 return $this->coefstring; 00959 } 00960 00961 $overriding_coefstring = null; 00962 00963 // Stop recursing upwards if this category aggregates subcats or has no parent 00964 if (!$first && !$this->aggregatesubcats) { 00965 00966 if ($parent_category = $this->load_parent_category()) { 00967 return $parent_category->get_coefstring(false); 00968 00969 } else { 00970 return null; 00971 } 00972 00973 } else if ($first) { 00974 00975 if (!$this->aggregatesubcats) { 00976 00977 if ($parent_category = $this->load_parent_category()) { 00978 $overriding_coefstring = $parent_category->get_coefstring(false); 00979 } 00980 } 00981 } 00982 00983 // If an overriding coefstring has trickled down from one of the parent categories, return it. Otherwise, return self. 00984 if (!is_null($overriding_coefstring)) { 00985 return $overriding_coefstring; 00986 } 00987 00988 // No parent category is overriding this category's aggregation, return its string 00989 if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) { 00990 $this->coefstring = 'aggregationcoefweight'; 00991 00992 } else if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) { 00993 $this->coefstring = 'aggregationcoefextrasum'; 00994 00995 } else if ($this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN) { 00996 $this->coefstring = 'aggregationcoefextraweight'; 00997 00998 } else if ($this->aggregation == GRADE_AGGREGATE_SUM) { 00999 $this->coefstring = 'aggregationcoefextrasum'; 01000 01001 } else { 01002 $this->coefstring = 'aggregationcoef'; 01003 } 01004 return $this->coefstring; 01005 } 01006 01016 public static function fetch_course_tree($courseid, $include_category_items=false) { 01017 $course_category = grade_category::fetch_course_category($courseid); 01018 $category_array = array('object'=>$course_category, 'type'=>'category', 'depth'=>1, 01019 'children'=>$course_category->get_children($include_category_items)); 01020 01021 $course_category->sortorder = $course_category->get_sortorder(); 01022 return grade_category::_fetch_course_tree_recursion($category_array, $course_category->get_sortorder()); 01023 } 01024 01035 static private function _fetch_course_tree_recursion($category_array, &$sortorder) { 01036 // update the sortorder in db if needed 01037 //NOTE: This leads to us resetting sort orders every time the categories and items page is viewed :( 01038 //if ($category_array['object']->sortorder != $sortorder) { 01039 //$category_array['object']->set_sortorder($sortorder); 01040 //} 01041 01042 if (isset($category_array['object']->gradetype) && $category_array['object']->gradetype==GRADE_TYPE_NONE) { 01043 return null; 01044 } 01045 01046 // store the grade_item or grade_category instance with extra info 01047 $result = array('object'=>$category_array['object'], 'type'=>$category_array['type'], 'depth'=>$category_array['depth']); 01048 01049 // reuse final grades if there 01050 if (array_key_exists('finalgrades', $category_array)) { 01051 $result['finalgrades'] = $category_array['finalgrades']; 01052 } 01053 01054 // recursively resort children 01055 if (!empty($category_array['children'])) { 01056 $result['children'] = array(); 01057 //process the category item first 01058 $child = null; 01059 01060 foreach ($category_array['children'] as $oldorder=>$child_array) { 01061 01062 if ($child_array['type'] == 'courseitem' or $child_array['type'] == 'categoryitem') { 01063 $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder); 01064 if (!empty($child)) { 01065 $result['children'][$sortorder] = $child; 01066 } 01067 } 01068 } 01069 01070 foreach ($category_array['children'] as $oldorder=>$child_array) { 01071 01072 if ($child_array['type'] != 'courseitem' and $child_array['type'] != 'categoryitem') { 01073 $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder); 01074 if (!empty($child)) { 01075 $result['children'][++$sortorder] = $child; 01076 } 01077 } 01078 } 01079 } 01080 01081 return $result; 01082 } 01083 01093 public function get_children($include_category_items=false) { 01094 global $DB; 01095 01096 // This function must be as fast as possible ;-) 01097 // fetch all course grade items and categories into memory - we do not expect hundreds of these in course 01098 // we have to limit the number of queries though, because it will be used often in grade reports 01099 01100 $cats = $DB->get_records('grade_categories', array('courseid' => $this->courseid)); 01101 $items = $DB->get_records('grade_items', array('courseid' => $this->courseid)); 01102 01103 // init children array first 01104 foreach ($cats as $catid=>$cat) { 01105 $cats[$catid]->children = array(); 01106 } 01107 01108 //first attach items to cats and add category sortorder 01109 foreach ($items as $item) { 01110 01111 if ($item->itemtype == 'course' or $item->itemtype == 'category') { 01112 $cats[$item->iteminstance]->sortorder = $item->sortorder; 01113 01114 if (!$include_category_items) { 01115 continue; 01116 } 01117 $categoryid = $item->iteminstance; 01118 01119 } else { 01120 $categoryid = $item->categoryid; 01121 } 01122 01123 // prevent problems with duplicate sortorders in db 01124 $sortorder = $item->sortorder; 01125 01126 while (array_key_exists($sortorder, $cats[$categoryid]->children)) { 01127 //debugging("$sortorder exists in item loop"); 01128 $sortorder++; 01129 } 01130 01131 $cats[$categoryid]->children[$sortorder] = $item; 01132 01133 } 01134 01135 // now find the requested category and connect categories as children 01136 $category = false; 01137 01138 foreach ($cats as $catid=>$cat) { 01139 01140 if (empty($cat->parent)) { 01141 01142 if ($cat->path !== '/'.$cat->id.'/') { 01143 $grade_category = new grade_category($cat, false); 01144 $grade_category->path = '/'.$cat->id.'/'; 01145 $grade_category->depth = 1; 01146 $grade_category->update('system'); 01147 return $this->get_children($include_category_items); 01148 } 01149 01150 } else { 01151 01152 if (empty($cat->path) or !preg_match('|/'.$cat->parent.'/'.$cat->id.'/$|', $cat->path)) { 01153 //fix paths and depts 01154 static $recursioncounter = 0; // prevents infinite recursion 01155 $recursioncounter++; 01156 01157 if ($recursioncounter < 5) { 01158 // fix paths and depths! 01159 $grade_category = new grade_category($cat, false); 01160 $grade_category->depth = 0; 01161 $grade_category->path = null; 01162 $grade_category->update('system'); 01163 return $this->get_children($include_category_items); 01164 } 01165 } 01166 // prevent problems with duplicate sortorders in db 01167 $sortorder = $cat->sortorder; 01168 01169 while (array_key_exists($sortorder, $cats[$cat->parent]->children)) { 01170 //debugging("$sortorder exists in cat loop"); 01171 $sortorder++; 01172 } 01173 01174 $cats[$cat->parent]->children[$sortorder] = &$cats[$catid]; 01175 } 01176 01177 if ($catid == $this->id) { 01178 $category = &$cats[$catid]; 01179 } 01180 } 01181 01182 unset($items); // not needed 01183 unset($cats); // not needed 01184 01185 $children_array = grade_category::_get_children_recursion($category); 01186 01187 ksort($children_array); 01188 01189 return $children_array; 01190 01191 } 01192 01200 private static function _get_children_recursion($category) { 01201 01202 $children_array = array(); 01203 foreach ($category->children as $sortorder=>$child) { 01204 01205 if (array_key_exists('itemtype', $child)) { 01206 $grade_item = new grade_item($child, false); 01207 01208 if (in_array($grade_item->itemtype, array('course', 'category'))) { 01209 $type = $grade_item->itemtype.'item'; 01210 $depth = $category->depth; 01211 01212 } else { 01213 $type = 'item'; 01214 $depth = $category->depth; // we use this to set the same colour 01215 } 01216 $children_array[$sortorder] = array('object'=>$grade_item, 'type'=>$type, 'depth'=>$depth); 01217 01218 } else { 01219 $children = grade_category::_get_children_recursion($child); 01220 $grade_category = new grade_category($child, false); 01221 01222 if (empty($children)) { 01223 $children = array(); 01224 } 01225 $children_array[$sortorder] = array('object'=>$grade_category, 'type'=>'category', 'depth'=>$grade_category->depth, 'children'=>$children); 01226 } 01227 } 01228 01229 // sort the array 01230 ksort($children_array); 01231 01232 return $children_array; 01233 } 01234 01239 public function load_grade_item() { 01240 if (empty($this->grade_item)) { 01241 $this->grade_item = $this->get_grade_item(); 01242 } 01243 return $this->grade_item; 01244 } 01245 01251 public function get_grade_item() { 01252 if (empty($this->id)) { 01253 debugging("Attempt to obtain a grade_category's associated grade_item without the category's ID being set."); 01254 return false; 01255 } 01256 01257 if (empty($this->parent)) { 01258 $params = array('courseid'=>$this->courseid, 'itemtype'=>'course', 'iteminstance'=>$this->id); 01259 01260 } else { 01261 $params = array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id); 01262 } 01263 01264 if (!$grade_items = grade_item::fetch_all($params)) { 01265 // create a new one 01266 $grade_item = new grade_item($params, false); 01267 $grade_item->gradetype = GRADE_TYPE_VALUE; 01268 $grade_item->insert('system'); 01269 01270 } else if (count($grade_items) == 1) { 01271 // found existing one 01272 $grade_item = reset($grade_items); 01273 01274 } else { 01275 debugging("Found more than one grade_item attached to category id:".$this->id); 01276 // return first one 01277 $grade_item = reset($grade_items); 01278 } 01279 01280 return $grade_item; 01281 } 01282 01288 public function load_parent_category() { 01289 if (empty($this->parent_category) && !empty($this->parent)) { 01290 $this->parent_category = $this->get_parent_category(); 01291 } 01292 return $this->parent_category; 01293 } 01294 01299 public function get_parent_category() { 01300 if (!empty($this->parent)) { 01301 $parent_category = new grade_category(array('id' => $this->parent)); 01302 return $parent_category; 01303 } else { 01304 return null; 01305 } 01306 } 01307 01314 public function get_name() { 01315 global $DB; 01316 // For a course category, we return the course name if the fullname is set to '?' in the DB (empty in the category edit form) 01317 if (empty($this->parent) && $this->fullname == '?') { 01318 $course = $DB->get_record('course', array('id'=> $this->courseid)); 01319 return format_string($course->fullname); 01320 01321 } else { 01322 return $this->fullname; 01323 } 01324 } 01325 01334 public function set_parent($parentid, $source=null) { 01335 if ($this->parent == $parentid) { 01336 return true; 01337 } 01338 01339 if ($parentid == $this->id) { 01340 print_error('cannotassignselfasparent'); 01341 } 01342 01343 if (empty($this->parent) and $this->is_course_category()) { 01344 print_error('cannothaveparentcate'); 01345 } 01346 01347 // find parent and check course id 01348 if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) { 01349 return false; 01350 } 01351 01352 $this->force_regrading(); 01353 01354 // set new parent category 01355 $this->parent = $parent_category->id; 01356 $this->parent_category =& $parent_category; 01357 $this->path = null; // remove old path and depth - will be recalculated in update() 01358 $this->depth = 0; // remove old path and depth - will be recalculated in update() 01359 $this->update($source); 01360 01361 return $this->update($source); 01362 } 01363 01371 public function get_final($userid=null) { 01372 $this->load_grade_item(); 01373 return $this->grade_item->get_final($userid); 01374 } 01375 01382 public function get_sortorder() { 01383 $this->load_grade_item(); 01384 return $this->grade_item->get_sortorder(); 01385 } 01386 01393 public function get_idnumber() { 01394 $this->load_grade_item(); 01395 return $this->grade_item->get_idnumber(); 01396 } 01397 01406 public function set_sortorder($sortorder) { 01407 $this->load_grade_item(); 01408 $this->grade_item->set_sortorder($sortorder); 01409 } 01410 01418 public function move_after_sortorder($sortorder) { 01419 $this->load_grade_item(); 01420 $this->grade_item->move_after_sortorder($sortorder); 01421 } 01422 01428 public function is_course_category() { 01429 $this->load_grade_item(); 01430 return $this->grade_item->is_course_item(); 01431 } 01432 01441 public static function fetch_course_category($courseid) { 01442 if (empty($courseid)) { 01443 debugging('Missing course id!'); 01444 return false; 01445 } 01446 01447 // course category has no parent 01448 if ($course_category = grade_category::fetch(array('courseid'=>$courseid, 'parent'=>null))) { 01449 return $course_category; 01450 } 01451 01452 // create a new one 01453 $course_category = new grade_category(); 01454 $course_category->insert_course_category($courseid); 01455 01456 return $course_category; 01457 } 01458 01464 public function is_editable() { 01465 return true; 01466 } 01467 01473 public function is_locked() { 01474 $this->load_grade_item(); 01475 return $this->grade_item->is_locked(); 01476 } 01477 01488 public function set_locked($lockedstate, $cascade=false, $refresh=true) { 01489 $this->load_grade_item(); 01490 01491 $result = $this->grade_item->set_locked($lockedstate, $cascade, true); 01492 01493 if ($cascade) { 01494 //process all children - items and categories 01495 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) { 01496 01497 foreach ($children as $child) { 01498 $child->set_locked($lockedstate, true, false); 01499 01500 if (empty($lockedstate) and $refresh) { 01501 //refresh when unlocking 01502 $child->refresh_grades(); 01503 } 01504 } 01505 } 01506 01507 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) { 01508 01509 foreach ($children as $child) { 01510 $child->set_locked($lockedstate, true, true); 01511 } 01512 } 01513 } 01514 01515 return $result; 01516 } 01517 01518 public static function set_properties(&$instance, $params) { 01519 global $DB; 01520 01521 parent::set_properties($instance, $params); 01522 01523 //if they've changed aggregation type we made need to do some fiddling to provide appropriate defaults 01524 if (!empty($params->aggregation)) { 01525 01526 //weight and extra credit share a column :( Would like a default of 1 for weight and 0 for extra credit 01527 //Flip from the default of 0 to 1 (or vice versa) if ALL items in the category are still set to the old default. 01528 if ($params->aggregation==GRADE_AGGREGATE_WEIGHTED_MEAN || $params->aggregation==GRADE_AGGREGATE_EXTRACREDIT_MEAN) { 01529 $sql = $defaultaggregationcoef = null; 01530 01531 if ($params->aggregation==GRADE_AGGREGATE_WEIGHTED_MEAN) { 01532 //if all items in this category have aggregation coefficient of 0 we can change it to 1 ie evenly weighted 01533 $sql = "select count(id) from {grade_items} where categoryid=:categoryid and aggregationcoef!=0"; 01534 $defaultaggregationcoef = 1; 01535 } else if ($params->aggregation==GRADE_AGGREGATE_EXTRACREDIT_MEAN) { 01536 //if all items in this category have aggregation coefficient of 1 we can change it to 0 ie no extra credit 01537 $sql = "select count(id) from {grade_items} where categoryid=:categoryid and aggregationcoef!=1"; 01538 $defaultaggregationcoef = 0; 01539 } 01540 01541 $params = array('categoryid'=>$instance->id); 01542 $count = $DB->count_records_sql($sql, $params); 01543 if ($count===0) { //category is either empty or all items are set to a default value so we can switch defaults 01544 $params['aggregationcoef'] = $defaultaggregationcoef; 01545 $DB->execute("update {grade_items} set aggregationcoef=:aggregationcoef where categoryid=:categoryid",$params); 01546 } 01547 } 01548 } 01549 } 01550 01558 public function set_hidden($hidden, $cascade=false) { 01559 $this->load_grade_item(); 01560 //this hides the associated grade item (the course total) 01561 $this->grade_item->set_hidden($hidden, $cascade); 01562 //this hides the category itself and everything it contains 01563 parent::set_hidden($hidden, $cascade); 01564 01565 if ($cascade) { 01566 01567 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) { 01568 01569 foreach ($children as $child) { 01570 $child->set_hidden($hidden, $cascade); 01571 } 01572 } 01573 01574 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) { 01575 01576 foreach ($children as $child) { 01577 $child->set_hidden($hidden, $cascade); 01578 } 01579 } 01580 } 01581 01582 //if marking category visible make sure parent category is visible MDL-21367 01583 if( !$hidden ) { 01584 $category_array = grade_category::fetch_all(array('id'=>$this->parent)); 01585 if ($category_array && array_key_exists($this->parent, $category_array)) { 01586 $category = $category_array[$this->parent]; 01587 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden 01588 //if($category->is_hidden()) { 01589 $category->set_hidden($hidden, false); 01590 //} 01591 } 01592 } 01593 } 01594 01599 public function apply_default_settings() { 01600 global $CFG; 01601 01602 foreach ($this->forceable as $property) { 01603 01604 if (isset($CFG->{"grade_$property"})) { 01605 01606 if ($CFG->{"grade_$property"} == -1) { 01607 continue; //temporary bc before version bump 01608 } 01609 $this->$property = $CFG->{"grade_$property"}; 01610 } 01611 } 01612 } 01613 01618 public function apply_forced_settings() { 01619 global $CFG; 01620 01621 $updated = false; 01622 01623 foreach ($this->forceable as $property) { 01624 01625 if (isset($CFG->{"grade_$property"}) and isset($CFG->{"grade_{$property}_flag"}) and 01626 ((int) $CFG->{"grade_{$property}_flag"} & 1)) { 01627 01628 if ($CFG->{"grade_$property"} == -1) { 01629 continue; //temporary bc before version bump 01630 } 01631 $this->$property = $CFG->{"grade_$property"}; 01632 $updated = true; 01633 } 01634 } 01635 01636 return $updated; 01637 } 01638 01645 public static function updated_forced_settings() { 01646 global $CFG, $DB; 01647 $params = array(1, 'course', 'category'); 01648 $sql = "UPDATE {grade_items} SET needsupdate=? WHERE itemtype=? or itemtype=?"; 01649 $DB->execute($sql, $params); 01650 } 01651 }