Moodle  2.2.1
http://www.collinsharper.com
C:/xampp/htdocs/moodle/lib/grade/grade_category.php
Go to the documentation of this file.
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 }
 All Data Structures Namespaces Files Functions Variables Enumerations