183c5647cd3e258ea61ecde4393c6c5996211693
[moodle.git] / lib / grade / grade_category.php
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/>.
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  */
25 defined('MOODLE_INTERNAL') || die();
27 require_once('grade_object.php');
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';
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                                  'aggregatesubcats', 'timecreated', 'timemodified', 'hidden');
52     /**
53      * The course this category belongs to.
54      * @var int $courseid
55      */
56     public $courseid;
58     /**
59      * The category this category belongs to (optional).
60      * @var int $parent
61      */
62     public $parent;
64     /**
65      * The grade_category object referenced by $this->parent (PK).
66      * @var grade_category $parent_category
67      */
68     public $parent_category;
70     /**
71      * The number of parents this category has.
72      * @var int $depth
73      */
74     public $depth = 0;
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;
83     /**
84      * The name of this category.
85      * @var string $fullname
86      */
87     public $fullname;
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_MEAN;
95     /**
96      * Keep only the X highest items.
97      * @var int $keephigh
98      */
99     public $keephigh = 0;
101     /**
102      * Drop the X lowest items.
103      * @var int $droplow
104      */
105     public $droplow = 0;
107     /**
108      * Aggregate only graded items
109      * @var int $aggregateonlygraded
110      */
111     public $aggregateonlygraded = 0;
113     /**
114      * Aggregate outcomes together with normal items
115      * @var int $aggregateoutcomes
116      */
117     public $aggregateoutcomes = 0;
119     /**
120      * Ignore subcategories when aggregating
121      * @var int $aggregatesubcats
122      */
123     public $aggregatesubcats = 0;
125     /**
126      * Array of grade_items or grade_categories nested exactly 1 level below this category
127      * @var array $children
128      */
129     public $children;
131     /**
132      * A hierarchical array of all children below this category. This is stored separately from
133      * $children because it is more memory-intensive and may not be used as often.
134      * @var array $all_children
135      */
136     public $all_children;
138     /**
139      * An associated grade_item object, with itemtype=category, used to calculate and cache a set of grade values
140      * for this category.
141      * @var grade_item $grade_item
142      */
143     public $grade_item;
145     /**
146      * Temporary sortorder for speedup of children resorting
147      * @var int $sortorder
148      */
149     public $sortorder;
151     /**
152      * List of options which can be "forced" from site settings.
153      * @var array $forceable
154      */
155     public $forceable = array('aggregation', 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes', 'aggregatesubcats');
157     /**
158      * String representing the aggregation coefficient. Variable is used as cache.
159      * @var string $coefstring
160      */
161     public $coefstring = null;
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;
175         if (empty($grade_category->parent)) {
176             return '/'.$grade_category->id.'/';
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     }
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     }
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     }
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();
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;
220         } else {
221             $updatechildren = false;
222         }
224         $this->apply_forced_settings();
226         // these are exclusive
227         if ($this->droplow > 0) {
228             $this->keephigh = 0;
230         } else if ($this->keephigh > 0) {
231             $this->droplow = 0;
232         }
234         // Recalculate grades if needed
235         if ($this->qualifies_for_regrading()) {
236             $this->force_regrading();
237         }
239         $this->timemodified = time();
241         $result = parent::update($source);
243         // now update paths in all child categories
244         if ($result and $updatechildren) {
246             if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
248                 foreach ($children as $child) {
249                     $child->path  = null;
250                     $child->depth = 0;
251                     $child->update($source);
252                 }
253             }
254         }
256         return $result;
257     }
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();
268         if ($this->is_course_category()) {
270             if ($categories = grade_category::fetch_all(array('courseid'=>$this->courseid))) {
272                 foreach ($categories as $category) {
274                     if ($category->id == $this->id) {
275                         continue; // do not delete course category yet
276                     }
277                     $category->delete($source);
278                 }
279             }
281             if ($items = grade_item::fetch_all(array('courseid'=>$this->courseid))) {
283                 foreach ($items as $item) {
285                     if ($item->id == $grade_item->id) {
286                         continue; // do not delete course item yet
287                     }
288                     $item->delete($source);
289                 }
290             }
292         } else {
293             $this->force_regrading();
295             $parent = $this->load_parent_category();
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             }
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         }
311         // first delete the attached grade item and grades
312         $grade_item->delete($source);
314         // delete category itself
315         return parent::delete($source);
316     }
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) {
331         if (empty($this->courseid)) {
332             print_error('cannotinsertgrade');
333         }
335         if (empty($this->parent)) {
336             $course_category = grade_category::fetch_course_category($this->courseid);
337             $this->parent = $course_category->id;
338         }
340         $this->path = null;
342         $this->timecreated = $this->timemodified = time();
344         if (!parent::insert($source)) {
345             debugging("Could not insert this category: " . print_r($this, true));
346             return false;
347         }
349         $this->force_regrading();
351         // build path and depth
352         $this->update($source);
354         return $this->id;
355     }
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;
371         $this->apply_default_settings();
372         $this->apply_forced_settings();
374         $this->timecreated = $this->timemodified = time();
376         if (!parent::insert('system')) {
377             debugging("Could not insert this category: " . print_r($this, true));
378             return false;
379         }
381         // build path and depth
382         $this->update('system');
384         return $this->id;
385     }
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         }
400         $db_item = grade_category::fetch(array('id'=>$this->id));
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         $aggsubcatsdiff  = $db_item->aggregatesubcats    != $this->aggregatesubcats;
409         return ($aggregationdiff || $keephighdiff || $droplowdiff || $aggonlygrddiff || $aggoutcomesdiff || $aggsubcatsdiff);
410     }
412     /**
413      * Marks this grade categories' associated grade item as needing regrading
414      */
415     public function force_regrading() {
416         $grade_item = $this->load_grade_item();
417         $grade_item->force_regrading();
418     }
420     /**
421      * Generates and saves final grades in associated category grade item.
422      * These immediate children must already have their own final grades.
423      * The category's aggregation method is used to generate final grades.
424      *
425      * Please note that category grade is either calculated or aggregated, not both at the same time.
426      *
427      * This method must be used ONLY from grade_item::regrade_final_grades(),
428      * because the calculation must be done in correct order!
429      *
430      * Steps to follow:
431      *  1. Get final grades from immediate children
432      *  3. Aggregate these grades
433      *  4. Save them in final grades of associated category grade item
434      *
435      * @param int $userid The user ID if final grade generation should be limited to a single user
436      * @return bool
437      */
438     public function generate_grades($userid=null) {
439         global $CFG, $DB;
441         $this->load_grade_item();
443         if ($this->grade_item->is_locked()) {
444             return true; // no need to recalculate locked items
445         }
447         // find grade items of immediate children (category or grade items) and force site settings
448         $depends_on = $this->grade_item->depends_on();
450         if (empty($depends_on)) {
451             $items = false;
453         } else {
454             list($usql, $params) = $DB->get_in_or_equal($depends_on);
455             $sql = "SELECT *
456                       FROM {grade_items}
457                      WHERE id $usql";
458             $items = $DB->get_records_sql($sql, $params);
459         }
461         // needed mostly for SUM agg type
462         $this->auto_update_max($items);
464         $grade_inst = new grade_grade();
465         $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
467         // where to look for final grades - include grade of this item too, we will store the results there
468         $gis = array_merge($depends_on, array($this->grade_item->id));
469         list($usql, $params) = $DB->get_in_or_equal($gis);
471         if ($userid) {
472             $usersql = "AND g.userid=?";
473             $params[] = $userid;
475         } else {
476             $usersql = "";
477         }
479         $sql = "SELECT $fields
480                   FROM {grade_grades} g, {grade_items} gi
481                  WHERE gi.id = g.itemid AND gi.id $usql $usersql
482               ORDER BY g.userid";
484         // group the results by userid and aggregate the grades for this user
485         $rs = $DB->get_recordset_sql($sql, $params);
486         if ($rs->valid()) {
487             $prevuser = 0;
488             $grade_values = array();
489             $excluded     = array();
490             $oldgrade     = null;
492             foreach ($rs as $used) {
494                 if ($used->userid != $prevuser) {
495                     $this->aggregate_grades($prevuser, $items, $grade_values, $oldgrade, $excluded);
496                     $prevuser = $used->userid;
497                     $grade_values = array();
498                     $excluded     = array();
499                     $oldgrade     = null;
500                 }
501                 $grade_values[$used->itemid] = $used->finalgrade;
503                 if ($used->excluded) {
504                     $excluded[] = $used->itemid;
505                 }
507                 if ($this->grade_item->id == $used->itemid) {
508                     $oldgrade = $used;
509                 }
510             }
511             $this->aggregate_grades($prevuser, $items, $grade_values, $oldgrade, $excluded);//the last one
512         }
513         $rs->close();
515         return true;
516     }
518     /**
519      * Internal function for grade category grade aggregation
520      *
521      * @param int    $userid The User ID
522      * @param array  $items Grade items
523      * @param array  $grade_values Array of grade values
524      * @param object $oldgrade Old grade
525      * @param array  $excluded Excluded
526      */
527     private function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded) {
528         global $CFG;
529         if (empty($userid)) {
530             //ignore first call
531             return;
532         }
534         if ($oldgrade) {
535             $oldfinalgrade = $oldgrade->finalgrade;
536             $grade = new grade_grade($oldgrade, false);
537             $grade->grade_item =& $this->grade_item;
539         } else {
540             // insert final grade - it will be needed later anyway
541             $grade = new grade_grade(array('itemid'=>$this->grade_item->id, 'userid'=>$userid), false);
542             $grade->grade_item =& $this->grade_item;
543             $grade->insert('system');
544             $oldfinalgrade = null;
545         }
547         // no need to recalculate locked or overridden grades
548         if ($grade->is_locked() or $grade->is_overridden()) {
549             return;
550         }
552         // can not use own final category grade in calculation
553         unset($grade_values[$this->grade_item->id]);
556         // sum is a special aggregation types - it adjusts the min max, does not use relative values
557         if ($this->aggregation == GRADE_AGGREGATE_SUM) {
558             $this->sum_grades($grade, $oldfinalgrade, $items, $grade_values, $excluded);
559             return;
560         }
562         // if no grades calculation possible or grading not allowed clear final grade
563         if (empty($grade_values) or empty($items) or ($this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE)) {
564             $grade->finalgrade = null;
566             if (!is_null($oldfinalgrade)) {
567                 $grade->update('aggregation');
568             }
569             return;
570         }
572         // normalize the grades first - all will have value 0...1
573         // ungraded items are not used in aggregation
574         foreach ($grade_values as $itemid=>$v) {
576             if (is_null($v)) {
577                 // null means no grade
578                 unset($grade_values[$itemid]);
579                 continue;
581             } else if (in_array($itemid, $excluded)) {
582                 unset($grade_values[$itemid]);
583                 continue;
584             }
585             $grade_values[$itemid] = grade_grade::standardise_score($v, $items[$itemid]->grademin, $items[$itemid]->grademax, 0, 1);
586         }
588         // use min grade if grade missing for these types
589         if (!$this->aggregateonlygraded) {
591             foreach ($items as $itemid=>$value) {
593                 if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
594                     $grade_values[$itemid] = 0;
595                 }
596             }
597         }
599         // limit and sort
600         $this->apply_limit_rules($grade_values, $items);
601         asort($grade_values, SORT_NUMERIC);
603         // let's see we have still enough grades to do any statistics
604         if (count($grade_values) == 0) {
605             // not enough attempts yet
606             $grade->finalgrade = null;
608             if (!is_null($oldfinalgrade)) {
609                 $grade->update('aggregation');
610             }
611             return;
612         }
614         // do the maths
615         $agg_grade = $this->aggregate_values($grade_values, $items);
617         // recalculate the grade back to requested range
618         $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax);
620         $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade);
622         // update in db if changed
623         if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
624             $grade->update('aggregation');
625         }
627         return;
628     }
630     /**
631      * Internal function that calculates the aggregated grade for this grade category
632      *
633      * Must be public as it is used by grade_grade::get_hiding_affected()
634      *
635      * @param array $grade_values An array of values to be aggregated
636      * @param array $items The array of grade_items
637      * @return float The aggregate grade for this grade category
638      */
639     public function aggregate_values($grade_values, $items) {
640         switch ($this->aggregation) {
642             case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies
643                 $num = count($grade_values);
644                 $grades = array_values($grade_values);
646                 if ($num % 2 == 0) {
647                     $agg_grade = ($grades[intval($num/2)-1] + $grades[intval($num/2)]) / 2;
649                 } else {
650                     $agg_grade = $grades[intval(($num/2)-0.5)];
651                 }
652                 break;
654             case GRADE_AGGREGATE_MIN:
655                 $agg_grade = reset($grade_values);
656                 break;
658             case GRADE_AGGREGATE_MAX:
659                 $agg_grade = array_pop($grade_values);
660                 break;
662             case GRADE_AGGREGATE_MODE:       // the most common value, average used if multimode
663                 // array_count_values only counts INT and STRING, so if grades are floats we must convert them to string
664                 $converted_grade_values = array();
666                 foreach ($grade_values as $k => $gv) {
668                     if (!is_int($gv) && !is_string($gv)) {
669                         $converted_grade_values[$k] = (string) $gv;
671                     } else {
672                         $converted_grade_values[$k] = $gv;
673                     }
674                 }
676                 $freq = array_count_values($converted_grade_values);
677                 arsort($freq);                      // sort by frequency keeping keys
678                 $top = reset($freq);               // highest frequency count
679                 $modes = array_keys($freq, $top);  // search for all modes (have the same highest count)
680                 rsort($modes, SORT_NUMERIC);       // get highest mode
681                 $agg_grade = reset($modes);
682                 break;
684             case GRADE_AGGREGATE_WEIGHTED_MEAN: // Weighted average of all existing final grades, weight specified in coef
685                 $weightsum = 0;
686                 $sum       = 0;
688                 foreach ($grade_values as $itemid=>$grade_value) {
690                     if ($items[$itemid]->aggregationcoef <= 0) {
691                         continue;
692                     }
693                     $weightsum += $items[$itemid]->aggregationcoef;
694                     $sum       += $items[$itemid]->aggregationcoef * $grade_value;
695                 }
697                 if ($weightsum == 0) {
698                     $agg_grade = null;
700                 } else {
701                     $agg_grade = $sum / $weightsum;
702                 }
703                 break;
705             case GRADE_AGGREGATE_WEIGHTED_MEAN2:
706                 // Weighted average of all existing final grades with optional extra credit flag,
707                 // weight is the range of grade (usually grademax)
708                 $weightsum = 0;
709                 $sum       = null;
711                 foreach ($grade_values as $itemid=>$grade_value) {
712                     $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
714                     if ($weight <= 0) {
715                         continue;
716                     }
718                     if ($items[$itemid]->aggregationcoef == 0) {
719                         $weightsum += $weight;
720                     }
721                     $sum += $weight * $grade_value;
722                 }
724                 if ($weightsum == 0) {
725                     $agg_grade = $sum; // only extra credits
727                 } else {
728                     $agg_grade = $sum / $weightsum;
729                 }
730                 break;
732             case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average
733                 $num = 0;
734                 $sum = null;
736                 foreach ($grade_values as $itemid=>$grade_value) {
738                     if ($items[$itemid]->aggregationcoef == 0) {
739                         $num += 1;
740                         $sum += $grade_value;
742                     } else if ($items[$itemid]->aggregationcoef > 0) {
743                         $sum += $items[$itemid]->aggregationcoef * $grade_value;
744                     }
745                 }
747                 if ($num == 0) {
748                     $agg_grade = $sum; // only extra credits or wrong coefs
750                 } else {
751                     $agg_grade = $sum / $num;
752                 }
753                 break;
755             case GRADE_AGGREGATE_MEAN:    // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum)
756             default:
757                 $num = count($grade_values);
758                 $sum = array_sum($grade_values);
759                 $agg_grade = $sum / $num;
760                 break;
761         }
763         return $agg_grade;
764     }
766     /**
767      * Some aggregation types may automatically update max grade
768      *
769      * @param array $items sub items
770      */
771     private function auto_update_max($items) {
772         if ($this->aggregation != GRADE_AGGREGATE_SUM) {
773             // not needed at all
774             return;
775         }
777         if (!$items) {
779             if ($this->grade_item->grademax != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
780                 $this->grade_item->grademax  = 0;
781                 $this->grade_item->grademin  = 0;
782                 $this->grade_item->gradetype = GRADE_TYPE_VALUE;
783                 $this->grade_item->update('aggregation');
784             }
785             return;
786         }
788         //find max grade possible
789         $maxes = array();
791         foreach ($items as $item) {
793             if ($item->aggregationcoef > 0) {
794                 // extra credit from this activity - does not affect total
795                 continue;
796             }
798             if ($item->gradetype == GRADE_TYPE_VALUE) {
799                 $maxes[$item->id] = $item->grademax;
801             } else if ($item->gradetype == GRADE_TYPE_SCALE) {
802                 $maxes[$item->id] = $item->grademax; // 0 = nograde, 1 = first scale item, 2 = second scale item
803             }
804         }
805         // apply droplow and keephigh
806         $this->apply_limit_rules($maxes, $items);
807         $max = array_sum($maxes);
809         // update db if anything changed
810         if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
811             $this->grade_item->grademax  = $max;
812             $this->grade_item->grademin  = 0;
813             $this->grade_item->gradetype = GRADE_TYPE_VALUE;
814             $this->grade_item->update('aggregation');
815         }
816     }
818     /**
819      * Internal function for category grades summing
820      *
821      * @param grade_grade $grade The grade item
822      * @param float $oldfinalgrade Old Final grade
823      * @param array $items Grade items
824      * @param array $grade_values Grade values
825      * @param array $excluded Excluded
826      */
827     private function sum_grades(&$grade, $oldfinalgrade, $items, $grade_values, $excluded) {
828         if (empty($items)) {
829             return null;
830         }
832         // ungraded and excluded items are not used in aggregation
833         foreach ($grade_values as $itemid=>$v) {
835             if (is_null($v)) {
836                 unset($grade_values[$itemid]);
838             } else if (in_array($itemid, $excluded)) {
839                 unset($grade_values[$itemid]);
840             }
841         }
843         // use 0 if grade missing, droplow used and aggregating all items
844         if (!$this->aggregateonlygraded and !empty($this->droplow)) {
846             foreach ($items as $itemid=>$value) {
848                 if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
849                     $grade_values[$itemid] = 0;
850                 }
851             }
852         }
854         $this->apply_limit_rules($grade_values, $items);
856         $sum = array_sum($grade_values);
857         $grade->finalgrade = $this->grade_item->bounded_grade($sum);
859         // update in db if changed
860         if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
861             $grade->update('aggregation');
862         }
864         return;
865     }
867     /**
868      * Given an array of grade values (numerical indices) applies droplow or keephigh rules to limit the final array.
869      *
870      * @param array $grade_values itemid=>$grade_value float
871      * @param array $items grade item objects
872      * @return array Limited grades.
873      */
874     public function apply_limit_rules(&$grade_values, $items) {
875         $extraused = $this->is_extracredit_used();
877         if (!empty($this->droplow)) {
878             asort($grade_values, SORT_NUMERIC);
879             $dropped = 0;
881             // If we have fewer grade items available to drop than $this->droplow, use this flag to escape the loop
882             // May occur because of "extra credit" or if droplow is higher than the number of grade items
883             $droppedsomething = true;
885             while ($dropped < $this->droplow && $droppedsomething) {
886                 $droppedsomething = false;
888                 $grade_keys = array_keys($grade_values);
889                 $gradekeycount = count($grade_keys);
891                 if ($gradekeycount === 0) {
892                     //We've dropped all grade items
893                     break;
894                 }
896                 $originalindex = $founditemid = $foundmax = null;
898                 // Find the first remaining grade item that is available to be dropped
899                 foreach ($grade_keys as $gradekeyindex=>$gradekey) {
900                     if (!$extraused || $items[$gradekey]->aggregationcoef <= 0) {
901                         // Found a non-extra credit grade item that is eligible to be dropped
902                         $originalindex = $gradekeyindex;
903                         $founditemid = $grade_keys[$originalindex];
904                         $foundmax = $items[$founditemid]->grademax;
905                         break;
906                     }
907                 }
909                 if (empty($founditemid)) {
910                     // No grade items available to drop
911                     break;
912                 }
914                 $i = 1;
915                 while ($originalindex + $i < $gradekeycount) {
917                     $possibleitemid = $grade_keys[$originalindex+$i];
918                     $i++;
920                     if ($grade_values[$founditemid] != $grade_values[$possibleitemid]) {
921                         // The next grade item has a different grade. Stop looking.
922                         break;
923                     }
925                     if ($extraused && $items[$possibleitemid]->aggregationcoef > 0) {
926                         // Don't drop extra credit grade items. Continue the search.
927                         continue;
928                     }
930                     if ($foundmax < $items[$possibleitemid]->grademax) {
931                         // Found a grade item with the same grade and a higher grademax
932                         $foundmax = $items[$possibleitemid]->grademax;
933                         $founditemid = $possibleitemid;
934                         // Continue searching to see if there is an even higher grademax...
935                     }
936                 }
938                 // Now drop whatever grade item we have found
939                 unset($grade_values[$founditemid]);
940                 $dropped++;
941                 $droppedsomething = true;
942             }
944         } else if (!empty($this->keephigh)) {
945             arsort($grade_values, SORT_NUMERIC);
946             $kept = 0;
948             foreach ($grade_values as $itemid=>$value) {
950                 if ($extraused and $items[$itemid]->aggregationcoef > 0) {
951                     // we keep all extra credits
953                 } else if ($kept < $this->keephigh) {
954                     $kept++;
956                 } else {
957                     unset($grade_values[$itemid]);
958                 }
959             }
960         }
961     }
963     /**
964      * Returns true if category uses extra credit of any kind
965      *
966      * @return bool True if extra credit used
967      */
968     function is_extracredit_used() {
969         return ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2
970              or $this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
971              or $this->aggregation == GRADE_AGGREGATE_SUM);
972     }
974     /**
975      * Returns true if category uses special aggregation coefficient
976      *
977      * @return bool True if an aggregation coefficient is being used
978      */
979     public function is_aggregationcoef_used() {
980         return ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN
981              or $this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2
982              or $this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
983              or $this->aggregation == GRADE_AGGREGATE_SUM);
985     }
987     /**
988      * Recursive function to find which weight/extra credit field to use in the grade item form.
989      *
990      * Inherits from a parent category if that category has aggregatesubcats set to true.
991      *
992      * @param string $first Whether or not this is the first item in the recursion
993      * @return string
994      */
995     public function get_coefstring($first=true) {
996         if (!is_null($this->coefstring)) {
997             return $this->coefstring;
998         }
1000         $overriding_coefstring = null;
1002         // Stop recursing upwards if this category aggregates subcats or has no parent
1003         if (!$first && !$this->aggregatesubcats) {
1005             if ($parent_category = $this->load_parent_category()) {
1006                 return $parent_category->get_coefstring(false);
1008             } else {
1009                 return null;
1010             }
1012         } else if ($first) {
1014             if (!$this->aggregatesubcats) {
1016                 if ($parent_category = $this->load_parent_category()) {
1017                     $overriding_coefstring = $parent_category->get_coefstring(false);
1018                 }
1019             }
1020         }
1022         // If an overriding coefstring has trickled down from one of the parent categories, return it. Otherwise, return self.
1023         if (!is_null($overriding_coefstring)) {
1024             return $overriding_coefstring;
1025         }
1027         // No parent category is overriding this category's aggregation, return its string
1028         if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
1029             $this->coefstring = 'aggregationcoefweight';
1031         } else if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
1032             $this->coefstring = 'aggregationcoefextrasum';
1034         } else if ($this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
1035             $this->coefstring = 'aggregationcoefextraweight';
1037         } else if ($this->aggregation == GRADE_AGGREGATE_SUM) {
1038             $this->coefstring = 'aggregationcoefextrasum';
1040         } else {
1041             $this->coefstring = 'aggregationcoef';
1042         }
1043         return $this->coefstring;
1044     }
1046     /**
1047      * Returns tree with all grade_items and categories as elements
1048      *
1049      * @param int $courseid The course ID
1050      * @param bool $include_category_items as category children
1051      * @return array
1052      */
1053     public static function fetch_course_tree($courseid, $include_category_items=false) {
1054         $course_category = grade_category::fetch_course_category($courseid);
1055         $category_array = array('object'=>$course_category, 'type'=>'category', 'depth'=>1,
1056                                 'children'=>$course_category->get_children($include_category_items));
1058         $course_category->sortorder = $course_category->get_sortorder();
1059         $sortorder = $course_category->get_sortorder();
1060         return grade_category::_fetch_course_tree_recursion($category_array, $sortorder);
1061     }
1063     /**
1064      * An internal function that recursively sorts grade categories within a course
1065      *
1066      * @param array $category_array The seed of the recursion
1067      * @param int   $sortorder The current sortorder
1068      * @return array An array containing 'object', 'type', 'depth' and optionally 'children'
1069      */
1070     static private function _fetch_course_tree_recursion($category_array, &$sortorder) {
1071         // update the sortorder in db if needed
1072         //NOTE: This leads to us resetting sort orders every time the categories and items page is viewed :(
1073         //if ($category_array['object']->sortorder != $sortorder) {
1074             //$category_array['object']->set_sortorder($sortorder);
1075         //}
1077         if (isset($category_array['object']->gradetype) && $category_array['object']->gradetype==GRADE_TYPE_NONE) {
1078             return null;
1079         }
1081         // store the grade_item or grade_category instance with extra info
1082         $result = array('object'=>$category_array['object'], 'type'=>$category_array['type'], 'depth'=>$category_array['depth']);
1084         // reuse final grades if there
1085         if (array_key_exists('finalgrades', $category_array)) {
1086             $result['finalgrades'] = $category_array['finalgrades'];
1087         }
1089         // recursively resort children
1090         if (!empty($category_array['children'])) {
1091             $result['children'] = array();
1092             //process the category item first
1093             $child = null;
1095             foreach ($category_array['children'] as $oldorder=>$child_array) {
1097                 if ($child_array['type'] == 'courseitem' or $child_array['type'] == 'categoryitem') {
1098                     $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
1099                     if (!empty($child)) {
1100                         $result['children'][$sortorder] = $child;
1101                     }
1102                 }
1103             }
1105             foreach ($category_array['children'] as $oldorder=>$child_array) {
1107                 if ($child_array['type'] != 'courseitem' and $child_array['type'] != 'categoryitem') {
1108                     $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
1109                     if (!empty($child)) {
1110                         $result['children'][++$sortorder] = $child;
1111                     }
1112                 }
1113             }
1114         }
1116         return $result;
1117     }
1119     /**
1120      * Fetches and returns all the children categories and/or grade_items belonging to this category.
1121      * By default only returns the immediate children (depth=1), but deeper levels can be requested,
1122      * as well as all levels (0). The elements are indexed by sort order.
1123      *
1124      * @param bool $include_category_items Whether or not to include category grade_items in the children array
1125      * @return array Array of child objects (grade_category and grade_item).
1126      */
1127     public function get_children($include_category_items=false) {
1128         global $DB;
1130         // This function must be as fast as possible ;-)
1131         // fetch all course grade items and categories into memory - we do not expect hundreds of these in course
1132         // we have to limit the number of queries though, because it will be used often in grade reports
1134         $cats  = $DB->get_records('grade_categories', array('courseid' => $this->courseid));
1135         $items = $DB->get_records('grade_items', array('courseid' => $this->courseid));
1137         // init children array first
1138         foreach ($cats as $catid=>$cat) {
1139             $cats[$catid]->children = array();
1140         }
1142         //first attach items to cats and add category sortorder
1143         foreach ($items as $item) {
1145             if ($item->itemtype == 'course' or $item->itemtype == 'category') {
1146                 $cats[$item->iteminstance]->sortorder = $item->sortorder;
1148                 if (!$include_category_items) {
1149                     continue;
1150                 }
1151                 $categoryid = $item->iteminstance;
1153             } else {
1154                 $categoryid = $item->categoryid;
1155                 if (empty($categoryid)) {
1156                     debugging('Found a grade item that isnt in a category');
1157                 }
1158             }
1160             // prevent problems with duplicate sortorders in db
1161             $sortorder = $item->sortorder;
1163             while (array_key_exists($categoryid, $cats) 
1164                 && array_key_exists($sortorder, $cats[$categoryid]->children)) {
1166                 $sortorder++;
1167             }
1169             $cats[$categoryid]->children[$sortorder] = $item;
1171         }
1173         // now find the requested category and connect categories as children
1174         $category = false;
1176         foreach ($cats as $catid=>$cat) {
1178             if (empty($cat->parent)) {
1180                 if ($cat->path !== '/'.$cat->id.'/') {
1181                     $grade_category = new grade_category($cat, false);
1182                     $grade_category->path  = '/'.$cat->id.'/';
1183                     $grade_category->depth = 1;
1184                     $grade_category->update('system');
1185                     return $this->get_children($include_category_items);
1186                 }
1188             } else {
1190                 if (empty($cat->path) or !preg_match('|/'.$cat->parent.'/'.$cat->id.'/$|', $cat->path)) {
1191                     //fix paths and depts
1192                     static $recursioncounter = 0; // prevents infinite recursion
1193                     $recursioncounter++;
1195                     if ($recursioncounter < 5) {
1196                         // fix paths and depths!
1197                         $grade_category = new grade_category($cat, false);
1198                         $grade_category->depth = 0;
1199                         $grade_category->path  = null;
1200                         $grade_category->update('system');
1201                         return $this->get_children($include_category_items);
1202                     }
1203                 }
1204                 // prevent problems with duplicate sortorders in db
1205                 $sortorder = $cat->sortorder;
1207                 while (array_key_exists($sortorder, $cats[$cat->parent]->children)) {
1208                     //debugging("$sortorder exists in cat loop");
1209                     $sortorder++;
1210                 }
1212                 $cats[$cat->parent]->children[$sortorder] = &$cats[$catid];
1213             }
1215             if ($catid == $this->id) {
1216                 $category = &$cats[$catid];
1217             }
1218         }
1220         unset($items); // not needed
1221         unset($cats); // not needed
1223         $children_array = grade_category::_get_children_recursion($category);
1225         ksort($children_array);
1227         return $children_array;
1229     }
1231     /**
1232      * Private method used to retrieve all children of this category recursively
1233      *
1234      * @param grade_category $category Source of current recursion
1235      * @return array An array of child grade categories
1236      */
1237     private static function _get_children_recursion($category) {
1239         $children_array = array();
1240         foreach ($category->children as $sortorder=>$child) {
1242             if (array_key_exists('itemtype', $child)) {
1243                 $grade_item = new grade_item($child, false);
1245                 if (in_array($grade_item->itemtype, array('course', 'category'))) {
1246                     $type  = $grade_item->itemtype.'item';
1247                     $depth = $category->depth;
1249                 } else {
1250                     $type  = 'item';
1251                     $depth = $category->depth; // we use this to set the same colour
1252                 }
1253                 $children_array[$sortorder] = array('object'=>$grade_item, 'type'=>$type, 'depth'=>$depth);
1255             } else {
1256                 $children = grade_category::_get_children_recursion($child);
1257                 $grade_category = new grade_category($child, false);
1259                 if (empty($children)) {
1260                     $children = array();
1261                 }
1262                 $children_array[$sortorder] = array('object'=>$grade_category, 'type'=>'category', 'depth'=>$grade_category->depth, 'children'=>$children);
1263             }
1264         }
1266         // sort the array
1267         ksort($children_array);
1269         return $children_array;
1270     }
1272     /**
1273      * Uses {@link get_grade_item()} to load or create a grade_item, then saves it as $this->grade_item.
1274      *
1275      * @return grade_item
1276      */
1277     public function load_grade_item() {
1278         if (empty($this->grade_item)) {
1279             $this->grade_item = $this->get_grade_item();
1280         }
1281         return $this->grade_item;
1282     }
1284     /**
1285      * Retrieves this grade categories' associated grade_item from the database
1286      *
1287      * If no grade_item exists yet, creates one.
1288      *
1289      * @return grade_item
1290      */
1291     public function get_grade_item() {
1292         if (empty($this->id)) {
1293             debugging("Attempt to obtain a grade_category's associated grade_item without the category's ID being set.");
1294             return false;
1295         }
1297         if (empty($this->parent)) {
1298             $params = array('courseid'=>$this->courseid, 'itemtype'=>'course', 'iteminstance'=>$this->id);
1300         } else {
1301             $params = array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id);
1302         }
1304         if (!$grade_items = grade_item::fetch_all($params)) {
1305             // create a new one
1306             $grade_item = new grade_item($params, false);
1307             $grade_item->gradetype = GRADE_TYPE_VALUE;
1308             $grade_item->insert('system');
1310         } else if (count($grade_items) == 1) {
1311             // found existing one
1312             $grade_item = reset($grade_items);
1314         } else {
1315             debugging("Found more than one grade_item attached to category id:".$this->id);
1316             // return first one
1317             $grade_item = reset($grade_items);
1318         }
1320         return $grade_item;
1321     }
1323     /**
1324      * Uses $this->parent to instantiate $this->parent_category based on the referenced record in the DB
1325      *
1326      * @return grade_category The parent category
1327      */
1328     public function load_parent_category() {
1329         if (empty($this->parent_category) && !empty($this->parent)) {
1330             $this->parent_category = $this->get_parent_category();
1331         }
1332         return $this->parent_category;
1333     }
1335     /**
1336      * Uses $this->parent to instantiate and return a grade_category object
1337      *
1338      * @return grade_category Returns the parent category or null if this category has no parent
1339      */
1340     public function get_parent_category() {
1341         if (!empty($this->parent)) {
1342             $parent_category = new grade_category(array('id' => $this->parent));
1343             return $parent_category;
1344         } else {
1345             return null;
1346         }
1347     }
1349     /**
1350      * Returns the most descriptive field for this grade category
1351      *
1352      * @return string name
1353      */
1354     public function get_name() {
1355         global $DB;
1356         // For a course category, we return the course name if the fullname is set to '?' in the DB (empty in the category edit form)
1357         if (empty($this->parent) && $this->fullname == '?') {
1358             $course = $DB->get_record('course', array('id'=> $this->courseid));
1359             return format_string($course->fullname);
1361         } else {
1362             return $this->fullname;
1363         }
1364     }
1366     /**
1367      * Sets this category's parent id
1368      *
1369      * @param int $parentid The ID of the category that is the new parent to $this
1370      * @param string $source From where was the object updated (mod/forum, manual, etc.)
1371      * @return bool success
1372      */
1373     public function set_parent($parentid, $source=null) {
1374         if ($this->parent == $parentid) {
1375             return true;
1376         }
1378         if ($parentid == $this->id) {
1379             print_error('cannotassignselfasparent');
1380         }
1382         if (empty($this->parent) and $this->is_course_category()) {
1383             print_error('cannothaveparentcate');
1384         }
1386         // find parent and check course id
1387         if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
1388             return false;
1389         }
1391         $this->force_regrading();
1393         // set new parent category
1394         $this->parent          = $parent_category->id;
1395         $this->parent_category =& $parent_category;
1396         $this->path            = null;       // remove old path and depth - will be recalculated in update()
1397         $this->depth           = 0;          // remove old path and depth - will be recalculated in update()
1398         $this->update($source);
1400         return $this->update($source);
1401     }
1403     /**
1404      * Returns the final grade values for this grade category.
1405      *
1406      * @param int $userid Optional user ID to retrieve a single user's final grade
1407      * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
1408      */
1409     public function get_final($userid=null) {
1410         $this->load_grade_item();
1411         return $this->grade_item->get_final($userid);
1412     }
1414     /**
1415      * Returns the sortorder of the grade categories' associated grade_item
1416      *
1417      * This method is also available in grade_item for cases where the object type is not known.
1418      *
1419      * @return int Sort order
1420      */
1421     public function get_sortorder() {
1422         $this->load_grade_item();
1423         return $this->grade_item->get_sortorder();
1424     }
1426     /**
1427      * Returns the idnumber of the grade categories' associated grade_item.
1428      *
1429      * This method is also available in grade_item for cases where the object type is not known.
1430      *
1431      * @return string idnumber
1432      */
1433     public function get_idnumber() {
1434         $this->load_grade_item();
1435         return $this->grade_item->get_idnumber();
1436     }
1438     /**
1439      * Sets the sortorder variable for this category.
1440      *
1441      * This method is also available in grade_item, for cases where the object type is not know.
1442      *
1443      * @param int $sortorder The sortorder to assign to this category
1444      */
1445     public function set_sortorder($sortorder) {
1446         $this->load_grade_item();
1447         $this->grade_item->set_sortorder($sortorder);
1448     }
1450     /**
1451      * Move this category after the given sortorder
1452      *
1453      * Does not change the parent
1454      *
1455      * @param int $sortorder to place after.
1456      * @return void
1457      */
1458     public function move_after_sortorder($sortorder) {
1459         $this->load_grade_item();
1460         $this->grade_item->move_after_sortorder($sortorder);
1461     }
1463     /**
1464      * Return true if this is the top most category that represents the total course grade.
1465      *
1466      * @return bool
1467      */
1468     public function is_course_category() {
1469         $this->load_grade_item();
1470         return $this->grade_item->is_course_item();
1471     }
1473     /**
1474      * Return the course level grade_category object
1475      *
1476      * @param int $courseid The Course ID
1477      * @return grade_category Returns the course level grade_category instance
1478      */
1479     public static function fetch_course_category($courseid) {
1480         if (empty($courseid)) {
1481             debugging('Missing course id!');
1482             return false;
1483         }
1485         // course category has no parent
1486         if ($course_category = grade_category::fetch(array('courseid'=>$courseid, 'parent'=>null))) {
1487             return $course_category;
1488         }
1490         // create a new one
1491         $course_category = new grade_category();
1492         $course_category->insert_course_category($courseid);
1494         return $course_category;
1495     }
1497     /**
1498      * Is grading object editable?
1499      *
1500      * @return bool
1501      */
1502     public function is_editable() {
1503         return true;
1504     }
1506     /**
1507      * Returns the locked state/date of the grade categories' associated grade_item.
1508      *
1509      * This method is also available in grade_item, for cases where the object type is not known.
1510      *
1511      * @return bool
1512      */
1513     public function is_locked() {
1514         $this->load_grade_item();
1515         return $this->grade_item->is_locked();
1516     }
1518     /**
1519      * Sets the grade_item's locked variable and updates the grade_item.
1520      *
1521      * Calls set_locked() on the categories' grade_item
1522      *
1523      * @param int  $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
1524      * @param bool $cascade lock/unlock child objects too
1525      * @param bool $refresh refresh grades when unlocking
1526      * @return bool success if category locked (not all children mayb be locked though)
1527      */
1528     public function set_locked($lockedstate, $cascade=false, $refresh=true) {
1529         $this->load_grade_item();
1531         $result = $this->grade_item->set_locked($lockedstate, $cascade, true);
1533         if ($cascade) {
1534             //process all children - items and categories
1535             if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
1537                 foreach ($children as $child) {
1538                     $child->set_locked($lockedstate, true, false);
1540                     if (empty($lockedstate) and $refresh) {
1541                         //refresh when unlocking
1542                         $child->refresh_grades();
1543                     }
1544                 }
1545             }
1547             if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
1549                 foreach ($children as $child) {
1550                     $child->set_locked($lockedstate, true, true);
1551                 }
1552             }
1553         }
1555         return $result;
1556     }
1558     /**
1559      * Overrides grade_object::set_properties() to add special handling for changes to category aggregation types
1560      *
1561      * @param stdClass $instance the object to set the properties on
1562      * @param array|stdClass $params Either an associative array or an object containing property name, property value pairs
1563      */
1564     public static function set_properties(&$instance, $params) {
1565         global $DB;
1567         parent::set_properties($instance, $params);
1569         //if they've changed aggregation type we made need to do some fiddling to provide appropriate defaults
1570         if (!empty($params->aggregation)) {
1572             //weight and extra credit share a column :( Would like a default of 1 for weight and 0 for extra credit
1573             //Flip from the default of 0 to 1 (or vice versa) if ALL items in the category are still set to the old default.
1574             if ($params->aggregation==GRADE_AGGREGATE_WEIGHTED_MEAN || $params->aggregation==GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
1575                 $sql = $defaultaggregationcoef = null;
1577                 if ($params->aggregation==GRADE_AGGREGATE_WEIGHTED_MEAN) {
1578                     //if all items in this category have aggregation coefficient of 0 we can change it to 1 ie evenly weighted
1579                     $sql = "select count(id) from {grade_items} where categoryid=:categoryid and aggregationcoef!=0";
1580                     $defaultaggregationcoef = 1;
1581                 } else if ($params->aggregation==GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
1582                     //if all items in this category have aggregation coefficient of 1 we can change it to 0 ie no extra credit
1583                     $sql = "select count(id) from {grade_items} where categoryid=:categoryid and aggregationcoef!=1";
1584                     $defaultaggregationcoef = 0;
1585                 }
1587                 $params = array('categoryid'=>$instance->id);
1588                 $count = $DB->count_records_sql($sql, $params);
1589                 if ($count===0) { //category is either empty or all items are set to a default value so we can switch defaults
1590                     $params['aggregationcoef'] = $defaultaggregationcoef;
1591                     $DB->execute("update {grade_items} set aggregationcoef=:aggregationcoef where categoryid=:categoryid",$params);
1592                 }
1593             }
1594         }
1595     }
1597     /**
1598      * Sets the grade_item's hidden variable and updates the grade_item.
1599      *
1600      * Overrides grade_item::set_hidden() to add cascading of the hidden value to grade items in this grade category
1601      *
1602      * @param int $hidden 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
1603      * @param bool $cascade apply to child objects too
1604      */
1605     public function set_hidden($hidden, $cascade=false) {
1606         $this->load_grade_item();
1607         //this hides the associated grade item (the course total)
1608         $this->grade_item->set_hidden($hidden, $cascade);
1609         //this hides the category itself and everything it contains
1610         parent::set_hidden($hidden, $cascade);
1612         if ($cascade) {
1614             if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
1616                 foreach ($children as $child) {
1617                     $child->set_hidden($hidden, $cascade);
1618                 }
1619             }
1621             if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
1623                 foreach ($children as $child) {
1624                     $child->set_hidden($hidden, $cascade);
1625                 }
1626             }
1627         }
1629         //if marking category visible make sure parent category is visible MDL-21367
1630         if( !$hidden ) {
1631             $category_array = grade_category::fetch_all(array('id'=>$this->parent));
1632             if ($category_array && array_key_exists($this->parent, $category_array)) {
1633                 $category = $category_array[$this->parent];
1634                 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
1635                 //if($category->is_hidden()) {
1636                     $category->set_hidden($hidden, false);
1637                 //}
1638             }
1639         }
1640     }
1642     /**
1643      * Applies default settings on this category
1644      *
1645      * @return bool True if anything changed
1646      */
1647     public function apply_default_settings() {
1648         global $CFG;
1650         foreach ($this->forceable as $property) {
1652             if (isset($CFG->{"grade_$property"})) {
1654                 if ($CFG->{"grade_$property"} == -1) {
1655                     continue; //temporary bc before version bump
1656                 }
1657                 $this->$property = $CFG->{"grade_$property"};
1658             }
1659         }
1660     }
1662     /**
1663      * Applies forced settings on this category
1664      *
1665      * @return bool True if anything changed
1666      */
1667     public function apply_forced_settings() {
1668         global $CFG;
1670         $updated = false;
1672         foreach ($this->forceable as $property) {
1674             if (isset($CFG->{"grade_$property"}) and isset($CFG->{"grade_{$property}_flag"}) and
1675                                                     ((int) $CFG->{"grade_{$property}_flag"} & 1)) {
1677                 if ($CFG->{"grade_$property"} == -1) {
1678                     continue; //temporary bc before version bump
1679                 }
1680                 $this->$property = $CFG->{"grade_$property"};
1681                 $updated = true;
1682             }
1683         }
1685         return $updated;
1686     }
1688     /**
1689      * Notification of change in forced category settings.
1690      *
1691      * Causes all course and category grade items to be marked as needing to be updated
1692      */
1693     public static function updated_forced_settings() {
1694         global $CFG, $DB;
1695         $params = array(1, 'course', 'category');
1696         $sql = "UPDATE {grade_items} SET needsupdate=? WHERE itemtype=? or itemtype=?";
1697         $DB->execute($sql, $params);
1698     }