6ca0608f4a8568935d454de5a1f5b2a5da165f6d
[moodle.git] / lib / grade / grade_category.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Definitions of constants for gradebook
19  *
20  * @package    moodlecore
21  * @subpackage grade
22  * @copyright  2006 Nicolas Connault
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
25 require_once('grade_object.php');
27 /**
28  * Grade_Category is an object mapped to DB table {prefix}grade_categories
29  *
30  * @package    moodlecore
31  * @subpackage grade
32  * @copyright  2007 Nicolas Connault
33  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34  */
35 class grade_category extends grade_object {
36     /**
37      * The DB table.
38      * @var string $table
39      */
40     public $table = 'grade_categories';
42     /**
43      * Array of required table fields, must start with 'id'.
44      * @var array $required_fields
45      */
46     public $required_fields = array('id', 'courseid', 'parent', 'depth', 'path', 'fullname', 'aggregation',
47                                  'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes',
48                                  'aggregatesubcats', 'timecreated', 'timemodified', 'hidden');
50     /**
51      * The course this category belongs to.
52      * @var int $courseid
53      */
54     public $courseid;
56     /**
57      * The category this category belongs to (optional).
58      * @var int $parent
59      */
60     public $parent;
62     /**
63      * The grade_category object referenced by $this->parent (PK).
64      * @var object $parent_category
65      */
66     public $parent_category;
68     /**
69      * The number of parents this category has.
70      * @var int $depth
71      */
72     public $depth = 0;
74     /**
75      * Shows the hierarchical path for this category as /1/2/3/ (like course_categories), the last number being
76      * this category's autoincrement ID number.
77      * @var string $path
78      */
79     public $path;
81     /**
82      * The name of this category.
83      * @var string $fullname
84      */
85     public $fullname;
87     /**
88      * A constant pointing to one of the predefined aggregation strategies (none, mean, median, sum etc) .
89      * @var int $aggregation
90      */
91     public $aggregation = GRADE_AGGREGATE_MEAN;
93     /**
94      * Keep only the X highest items.
95      * @var int $keephigh
96      */
97     public $keephigh = 0;
99     /**
100      * Drop the X lowest items.
101      * @var int $droplow
102      */
103     public $droplow = 0;
105     /**
106      * Aggregate only graded items
107      * @var int $aggregateonlygraded
108      */
109     public $aggregateonlygraded = 0;
111     /**
112      * Aggregate outcomes together with normal items
113      * @var int $aggregateoutcomes
114      */
115     public $aggregateoutcomes = 0;
117     /**
118      * Ignore subcategories when aggregating
119      * @var int $aggregatesubcats
120      */
121     public $aggregatesubcats = 0;
123     /**
124      * Array of grade_items or grade_categories nested exactly 1 level below this category
125      * @var array $children
126      */
127     public $children;
129     /**
130      * A hierarchical array of all children below this category. This is stored separately from
131      * $children because it is more memory-intensive and may not be used as often.
132      * @var array $all_children
133      */
134     public $all_children;
136     /**
137      * An associated grade_item object, with itemtype=category, used to calculate and cache a set of grade values
138      * for this category.
139      * @var object $grade_item
140      */
141     public $grade_item;
143     /**
144      * Temporary sortorder for speedup of children resorting
145      */
146     public $sortorder;
148     /**
149      * List of options which can be "forced" from site settings.
150      */
151     public $forceable = array('aggregation', 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes', 'aggregatesubcats');
153     /**
154      * String representing the aggregation coefficient. Variable is used as cache.
155      */
156     public $coefstring = null;
158     /**
159      * Builds this category's path string based on its parents (if any) and its own id number.
160      * This is typically done just before inserting this object in the DB for the first time,
161      * or when a new parent is added or changed. It is a recursive function: once the calling
162      * object no longer has a parent, the path is complete.
163      *
164      * @param object $grade_category A Grade_Category object
165      * @return int The depth of this category (2 means there is one parent)
166      * @static
167      */
168     public function build_path($grade_category) {
169         global $DB;
171         if (empty($grade_category->parent)) {
172             return '/'.$grade_category->id.'/';
174         } else {
175             $parent = $DB->get_record('grade_categories', array('id' => $grade_category->parent));
176             return grade_category::build_path($parent).$grade_category->id.'/';
177         }
178     }
180     /**
181      * Finds and returns a grade_category instance based on params.
182      * @static
183      *
184      * @param array $params associative arrays varname=>value
185      * @return object grade_category instance or false if none found.
186      */
187     public static function fetch($params) {
188         return grade_object::fetch_helper('grade_categories', 'grade_category', $params);
189     }
191     /**
192      * Finds and returns all grade_category instances based on params.
193      * @static
194      *
195      * @param array $params associative arrays varname=>value
196      * @return array array of grade_category insatnces or false if none found.
197      */
198     public static function fetch_all($params) {
199         return grade_object::fetch_all_helper('grade_categories', 'grade_category', $params);
200     }
202     /**
203      * In addition to update() as defined in grade_object, call force_regrading of parent categories, if applicable.
204      * @param string $source from where was the object updated (mod/forum, manual, etc.)
205      * @return boolean success
206      */
207     public function update($source=null) {
208         // load the grade item or create a new one
209         $this->load_grade_item();
211         // force recalculation of path;
212         if (empty($this->path)) {
213             $this->path  = grade_category::build_path($this);
214             $this->depth = substr_count($this->path, '/') - 1;
215             $updatechildren = true;
217         } else {
218             $updatechildren = false;
219         }
221         $this->apply_forced_settings();
223         // these are exclusive
224         if ($this->droplow > 0) {
225             $this->keephigh = 0;
227         } else if ($this->keephigh > 0) {
228             $this->droplow = 0;
229         }
231         // Recalculate grades if needed
232         if ($this->qualifies_for_regrading()) {
233             $this->force_regrading();
234         }
236         $this->timemodified = time();
238         $result = parent::update($source);
240         // now update paths in all child categories
241         if ($result and $updatechildren) {
243             if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
245                 foreach ($children as $child) {
246                     $child->path  = null;
247                     $child->depth = 0;
248                     $child->update($source);
249                 }
250             }
251         }
253         return $result;
254     }
256     /**
257      * If parent::delete() is successful, send force_regrading message to parent category.
258      * @param string $source from where was the object deleted (mod/forum, manual, etc.)
259      * @return boolean success
260      */
261     public function delete($source=null) {
262         $grade_item = $this->load_grade_item();
264         if ($this->is_course_category()) {
266             if ($categories = grade_category::fetch_all(array('courseid'=>$this->courseid))) {
268                 foreach ($categories as $category) {
270                     if ($category->id == $this->id) {
271                         continue; // do not delete course category yet
272                     }
273                     $category->delete($source);
274                 }
275             }
277             if ($items = grade_item::fetch_all(array('courseid'=>$this->courseid))) {
279                 foreach ($items as $item) {
281                     if ($item->id == $grade_item->id) {
282                         continue; // do not delete course item yet
283                     }
284                     $item->delete($source);
285                 }
286             }
288         } else {
289             $this->force_regrading();
291             $parent = $this->load_parent_category();
293             // Update children's categoryid/parent field first
294             if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
295                 foreach ($children as $child) {
296                     $child->set_parent($parent->id);
297                 }
298             }
300             if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
301                 foreach ($children as $child) {
302                     $child->set_parent($parent->id);
303                 }
304             }
305         }
307         // first delete the attached grade item and grades
308         $grade_item->delete($source);
310         // delete category itself
311         return parent::delete($source);
312     }
314     /**
315      * In addition to the normal insert() defined in grade_object, this method sets the depth
316      * and path for this object, and update the record accordingly. The reason why this must
317      * be done here instead of in the constructor, is that they both need to know the record's
318      * id number, which only gets created at insertion time.
319      * This method also creates an associated grade_item if this wasn't done during construction.
320      * @param string $source from where was the object inserted (mod/forum, manual, etc.)
321      * @return int PK ID if successful, false otherwise
322      */
323     public function insert($source=null) {
325         if (empty($this->courseid)) {
326             print_error('cannotinsertgrade');
327         }
329         if (empty($this->parent)) {
330             $course_category = grade_category::fetch_course_category($this->courseid);
331             $this->parent = $course_category->id;
332         }
334         $this->path = null;
336         $this->timecreated = $this->timemodified = time();
338         if (!parent::insert($source)) {
339             debugging("Could not insert this category: " . print_r($this, true));
340             return false;
341         }
343         $this->force_regrading();
345         // build path and depth
346         $this->update($source);
348         return $this->id;
349     }
351     /**
352      * Internal function - used only from fetch_course_category()
353      * Normal insert() can not be used for course category
354      *
355      * @param int $courseid The course ID
356      *
357      * @return bool success
358      */
359     public function insert_course_category($courseid) {
360         $this->courseid    = $courseid;
361         $this->fullname    = '?';
362         $this->path        = null;
363         $this->parent      = null;
364         $this->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2;
366         $this->apply_default_settings();
367         $this->apply_forced_settings();
369         $this->timecreated = $this->timemodified = time();
371         if (!parent::insert('system')) {
372             debugging("Could not insert this category: " . print_r($this, true));
373             return false;
374         }
376         // build path and depth
377         $this->update('system');
379         return $this->id;
380     }
382     /**
383      * Compares the values held by this object with those of the matching record in DB, and returns
384      * whether or not these differences are sufficient to justify an update of all parent objects.
385      * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
386      * @return boolean
387      */
388     public function qualifies_for_regrading() {
389         if (empty($this->id)) {
390             debugging("Can not regrade non existing category");
391             return false;
392         }
394         $db_item = grade_category::fetch(array('id'=>$this->id));
396         $aggregationdiff = $db_item->aggregation         != $this->aggregation;
397         $keephighdiff    = $db_item->keephigh            != $this->keephigh;
398         $droplowdiff     = $db_item->droplow             != $this->droplow;
399         $aggonlygrddiff  = $db_item->aggregateonlygraded != $this->aggregateonlygraded;
400         $aggoutcomesdiff = $db_item->aggregateoutcomes   != $this->aggregateoutcomes;
401         $aggsubcatsdiff  = $db_item->aggregatesubcats    != $this->aggregatesubcats;
403         return ($aggregationdiff || $keephighdiff || $droplowdiff || $aggonlygrddiff || $aggoutcomesdiff || $aggsubcatsdiff);
404     }
406     /**
407      * Marks the category and course item as needing update - categories are always regraded.
408      * @return void
409      */
410     public function force_regrading() {
411         $grade_item = $this->load_grade_item();
412         $grade_item->force_regrading();
413     }
415     /**
416      * Generates and saves final grades in associated category grade item.
417      * These immediate children must already have their own final grades.
418      * The category's aggregation method is used to generate final grades.
419      *
420      * Please note that category grade is either calculated or aggregated - not both at the same time.
421      *
422      * This method must be used ONLY from grade_item::regrade_final_grades(),
423      * because the calculation must be done in correct order!
424      *
425      * Steps to follow:
426      *  1. Get final grades from immediate children
427      *  3. Aggregate these grades
428      *  4. Save them in final grades of associated category grade item
429      *
430      * @param int $userid The user ID
431      *
432      * @return bool
433      */
434     public function generate_grades($userid=null) {
435         global $CFG, $DB;
437         $this->load_grade_item();
439         if ($this->grade_item->is_locked()) {
440             return true; // no need to recalculate locked items
441         }
443         // find grade items of immediate children (category or grade items) and force site settings
444         $depends_on = $this->grade_item->depends_on();
446         if (empty($depends_on)) {
447             $items = false;
449         } else {
450             list($usql, $params) = $DB->get_in_or_equal($depends_on);
451             $sql = "SELECT *
452                       FROM {grade_items}
453                      WHERE id $usql";
454             $items = $DB->get_records_sql($sql, $params);
455         }
457         // needed mostly for SUM agg type
458         $this->auto_update_max($items);
460         $grade_inst = new grade_grade();
461         $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
463         // where to look for final grades - include grade of this item too, we will store the results there
464         $gis = array_merge($depends_on, array($this->grade_item->id));
465         list($usql, $params) = $DB->get_in_or_equal($gis);
467         if ($userid) {
468             $usersql = "AND g.userid=?";
469             $params[] = $userid;
471         } else {
472             $usersql = "";
473         }
475         $sql = "SELECT $fields
476                   FROM {grade_grades} g, {grade_items} gi
477                  WHERE gi.id = g.itemid AND gi.id $usql $usersql
478               ORDER BY g.userid";
480         // group the results by userid and aggregate the grades for this user
481         if ($rs = $DB->get_recordset_sql($sql, $params)) {
482             $prevuser = 0;
483             $grade_values = array();
484             $excluded     = array();
485             $oldgrade     = null;
487             foreach ($rs as $used) {
489                 if ($used->userid != $prevuser) {
490                     $this->aggregate_grades($prevuser, $items, $grade_values, $oldgrade, $excluded);
491                     $prevuser = $used->userid;
492                     $grade_values = array();
493                     $excluded     = array();
494                     $oldgrade     = null;
495                 }
496                 $grade_values[$used->itemid] = $used->finalgrade;
498                 if ($used->excluded) {
499                     $excluded[] = $used->itemid;
500                 }
502                 if ($this->grade_item->id == $used->itemid) {
503                     $oldgrade = $used;
504                 }
505             }
506             $rs->close();
507             $this->aggregate_grades($prevuser, $items, $grade_values, $oldgrade, $excluded);//the last one
508         }
510         return true;
511     }
513     /**
514      * internal function for category grades aggregation
515      *
516      * @param int    $userid The User ID
517      * @param array  $items Grade items
518      * @param array  $grade_values Array of grade values
519      * @param object $oldgrade Old grade
520      * @param bool   $excluded Excluded
521      *
522      * @return boolean (just plain return;)
523      * @todo Document correctly
524      */
525     private function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded) {
526         global $CFG;
527         if (empty($userid)) {
528             //ignore first call
529             return;
530         }
532         if ($oldgrade) {
533             $oldfinalgrade = $oldgrade->finalgrade;
534             $grade = new grade_grade($oldgrade, false);
535             $grade->grade_item =& $this->grade_item;
537         } else {
538             // insert final grade - it will be needed later anyway
539             $grade = new grade_grade(array('itemid'=>$this->grade_item->id, 'userid'=>$userid), false);
540             $grade->grade_item =& $this->grade_item;
541             $grade->insert('system');
542             $oldfinalgrade = null;
543         }
545         // no need to recalculate locked or overridden grades
546         if ($grade->is_locked() or $grade->is_overridden()) {
547             return;
548         }
550         // can not use own final category grade in calculation
551         unset($grade_values[$this->grade_item->id]);
554         // sum is a special aggregation types - it adjusts the min max, does not use relative values
555         if ($this->aggregation == GRADE_AGGREGATE_SUM) {
556             $this->sum_grades($grade, $oldfinalgrade, $items, $grade_values, $excluded);
557             return;
558         }
560         // if no grades calculation possible or grading not allowed clear final grade
561         if (empty($grade_values) or empty($items) or ($this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE)) {
562             $grade->finalgrade = null;
564             if (!is_null($oldfinalgrade)) {
565                 $grade->update('aggregation');
566             }
567             return;
568         }
570         // normalize the grades first - all will have value 0...1
571         // ungraded items are not used in aggregation
572         foreach ($grade_values as $itemid=>$v) {
574             if (is_null($v)) {
575                 // null means no grade
576                 unset($grade_values[$itemid]);
577                 continue;
579             } else if (in_array($itemid, $excluded)) {
580                 unset($grade_values[$itemid]);
581                 continue;
582             }
583             $grade_values[$itemid] = grade_grade::standardise_score($v, $items[$itemid]->grademin, $items[$itemid]->grademax, 0, 1);
584         }
586         // use min grade if grade missing for these types
587         if (!$this->aggregateonlygraded) {
589             foreach ($items as $itemid=>$value) {
591                 if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
592                     $grade_values[$itemid] = 0;
593                 }
594             }
595         }
597         // limit and sort
598         $this->apply_limit_rules($grade_values, $items);
599         asort($grade_values, SORT_NUMERIC);
601         // let's see we have still enough grades to do any statistics
602         if (count($grade_values) == 0) {
603             // not enough attempts yet
604             $grade->finalgrade = null;
606             if (!is_null($oldfinalgrade)) {
607                 $grade->update('aggregation');
608             }
609             return;
610         }
612         // do the maths
613         $agg_grade = $this->aggregate_values($grade_values, $items);
615         // recalculate the grade back to requested range
616         $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax);
618         $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade);
620         // update in db if changed
621         if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
622             $grade->update('aggregation');
623         }
625         return;
626     }
628     /**
629      * Internal function - aggregation maths.
630      * Must be public: used by grade_grade::get_hiding_affected()
631      *
632      * @param array $grade_values The values being aggregated
633      * @param array $items The array of grade_items
634      *
635      * @return float
636      */
637     public function aggregate_values($grade_values, $items) {
638         switch ($this->aggregation) {
640             case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies
641                 $num = count($grade_values);
642                 $grades = array_values($grade_values);
644                 if ($num % 2 == 0) {
645                     $agg_grade = ($grades[intval($num/2)-1] + $grades[intval($num/2)]) / 2;
647                 } else {
648                     $agg_grade = $grades[intval(($num/2)-0.5)];
649                 }
650                 break;
652             case GRADE_AGGREGATE_MIN:
653                 $agg_grade = reset($grade_values);
654                 break;
656             case GRADE_AGGREGATE_MAX:
657                 $agg_grade = array_pop($grade_values);
658                 break;
660             case GRADE_AGGREGATE_MODE:       // the most common value, average used if multimode
661                 // array_count_values only counts INT and STRING, so if grades are floats we must convert them to string
662                 $converted_grade_values = array();
664                 foreach ($grade_values as $k => $gv) {
666                     if (!is_int($gv) && !is_string($gv)) {
667                         $converted_grade_values[$k] = (string) $gv;
669                     } else {
670                         $converted_grade_values[$k] = $gv;
671                     }
672                 }
674                 $freq = array_count_values($converted_grade_values);
675                 arsort($freq);                      // sort by frequency keeping keys
676                 $top = reset($freq);               // highest frequency count
677                 $modes = array_keys($freq, $top);  // search for all modes (have the same highest count)
678                 rsort($modes, SORT_NUMERIC);       // get highest mode
679                 $agg_grade = reset($modes);
680                 break;
682             case GRADE_AGGREGATE_WEIGHTED_MEAN: // Weighted average of all existing final grades, weight specified in coef
683                 $weightsum = 0;
684                 $sum       = 0;
686                 foreach ($grade_values as $itemid=>$grade_value) {
688                     if ($items[$itemid]->aggregationcoef <= 0) {
689                         continue;
690                     }
691                     $weightsum += $items[$itemid]->aggregationcoef;
692                     $sum       += $items[$itemid]->aggregationcoef * $grade_value;
693                 }
695                 if ($weightsum == 0) {
696                     $agg_grade = null;
698                 } else {
699                     $agg_grade = $sum / $weightsum;
700                 }
701                 break;
703             case GRADE_AGGREGATE_WEIGHTED_MEAN2:
704                 // Weighted average of all existing final grades with optional extra credit flag,
705                 // weight is the range of grade (usually grademax)
706                 $weightsum = 0;
707                 $sum       = null;
709                 foreach ($grade_values as $itemid=>$grade_value) {
710                     $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
712                     if ($weight <= 0) {
713                         continue;
714                     }
716                     if ($items[$itemid]->aggregationcoef == 0) {
717                         $weightsum += $weight;
718                     }
719                     $sum += $weight * $grade_value;
720                 }
722                 if ($weightsum == 0) {
723                     $agg_grade = $sum; // only extra credits
725                 } else {
726                     $agg_grade = $sum / $weightsum;
727                 }
728                 break;
730             case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average
731                 $num = 0;
732                 $sum = null;
734                 foreach ($grade_values as $itemid=>$grade_value) {
736                     if ($items[$itemid]->aggregationcoef == 0) {
737                         $num += 1;
738                         $sum += $grade_value;
740                     } else if ($items[$itemid]->aggregationcoef > 0) {
741                         $sum += $items[$itemid]->aggregationcoef * $grade_value;
742                     }
743                 }
745                 if ($num == 0) {
746                     $agg_grade = $sum; // only extra credits or wrong coefs
748                 } else {
749                     $agg_grade = $sum / $num;
750                 }
751                 break;
753             case GRADE_AGGREGATE_MEAN:    // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum)
754             default:
755                 $num = count($grade_values);
756                 $sum = array_sum($grade_values);
757                 $agg_grade = $sum / $num;
758                 break;
759         }
761         return $agg_grade;
762     }
764     /**
765      * Some aggregation tpyes may update max grade
766      * @param array $items sub items
767      * @return void
768      */
769     private function auto_update_max($items) {
770         if ($this->aggregation != GRADE_AGGREGATE_SUM) {
771             // not needed at all
772             return;
773         }
775         if (!$items) {
777             if ($this->grade_item->grademax != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
778                 $this->grade_item->grademax  = 0;
779                 $this->grade_item->grademin  = 0;
780                 $this->grade_item->gradetype = GRADE_TYPE_VALUE;
781                 $this->grade_item->update('aggregation');
782             }
783             return;
784         }
786         //find max grade possible
787         $maxes = array();
789         foreach ($items as $item) {
791             if ($item->aggregationcoef > 0) {
792                 // extra credit from this activity - does not affect total
793                 continue;
794             }
796             if ($item->gradetype == GRADE_TYPE_VALUE) {
797                 $maxes[$item->id] = $item->grademax;
799             } else if ($item->gradetype == GRADE_TYPE_SCALE) {
800                 $maxes[$item->id] = $item->grademax; // 0 = nograde, 1 = first scale item, 2 = second scale item
801             }
802         }
803         // apply droplow and keephigh
804         $this->apply_limit_rules($maxes, $items);
805         $max = array_sum($maxes);
807         // update db if anything changed
808         if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
809             $this->grade_item->grademax  = $max;
810             $this->grade_item->grademin  = 0;
811             $this->grade_item->gradetype = GRADE_TYPE_VALUE;
812             $this->grade_item->update('aggregation');
813         }
814     }
816     /**
817      * internal function for category grades summing
818      *
819      * @param grade_item &$grade The grade item
820      * @param float      $oldfinalgrade Old Final grade?
821      * @param array      $items Grade items
822      * @param array      $grade_values Grade values
823      * @param bool       $excluded Excluded
824      *
825      * @return boolean (just plain return;)
826      */
827     private function sum_grades(&$grade, $oldfinalgrade, $items, $grade_values, $excluded) {
828         if (empty($items)) {
829             return null;
830         }
832         // ungraded and exluded 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
869      * rules to limit the final array.
870      *
871      * @param array &$grade_values itemid=>$grade_value float
872      * @param array $items grade item objects
873      *
874      * @return array Limited grades.
875      */
876     public function apply_limit_rules(&$grade_values, $items) {
877         $extraused = $this->is_extracredit_used();
879         if (!empty($this->droplow)) {
880             asort($grade_values, SORT_NUMERIC);
881             $dropped = 0;
883             foreach ($grade_values as $itemid=>$value) {
885                 if ($dropped < $this->droplow) {
887                     if ($extraused and $items[$itemid]->aggregationcoef > 0) {
888                         // no drop low for extra credits
890                     } else {
891                         unset($grade_values[$itemid]);
892                         $dropped++;
893                     }
895                 } else {
896                     // we have dropped enough
897                     break;
898                 }
899             }
901         } else if (!empty($this->keephigh)) {
902             arsort($grade_values, SORT_NUMERIC);
903             $kept = 0;
905             foreach ($grade_values as $itemid=>$value) {
907                 if ($extraused and $items[$itemid]->aggregationcoef > 0) {
908                     // we keep all extra credits
910                 } else if ($kept < $this->keephigh) {
911                     $kept++;
913                 } else {
914                     unset($grade_values[$itemid]);
915                 }
916             }
917         }
918     }
920     /**
921      * Returns true if category uses extra credit of any kind
922      *
923      * @return boolean true if extra credit used
924      */
925     function is_extracredit_used() {
926         return ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2
927              or $this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
928              or $this->aggregation == GRADE_AGGREGATE_SUM);
929     }
931     /**
932      * Returns true if category uses special aggregation coefficient
933      *
934      * @return boolean true if coefficient used
935      */
936     public function is_aggregationcoef_used() {
937         return ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN
938              or $this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2
939              or $this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
940              or $this->aggregation == GRADE_AGGREGATE_SUM);
942     }
944     /**
945      * Recursive function to find which weight/extra credit field to use in the grade item form. Inherits from a parent category
946      * if that category has aggregatesubcats set to true.
947      *
948      * @param string $first Whether or not this is the first item in the recursion
949      *
950      * @return string
951      */
952     public function get_coefstring($first=true) {
953         if (!is_null($this->coefstring)) {
954             return $this->coefstring;
955         }
957         $overriding_coefstring = null;
959         // Stop recursing upwards if this category aggregates subcats or has no parent
960         if (!$first && !$this->aggregatesubcats) {
962             if ($parent_category = $this->load_parent_category()) {
963                 return $parent_category->get_coefstring(false);
965             } else {
966                 return null;
967             }
969         } else if ($first) {
971             if (!$this->aggregatesubcats) {
973                 if ($parent_category = $this->load_parent_category()) {
974                     $overriding_coefstring = $parent_category->get_coefstring(false);
975                 }
976             }
977         }
979         // If an overriding coefstring has trickled down from one of the parent categories, return it. Otherwise, return self.
980         if (!is_null($overriding_coefstring)) {
981             return $overriding_coefstring;
982         }
984         // No parent category is overriding this category's aggregation, return its string
985         if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
986             $this->coefstring = 'aggregationcoefweight';
988         } else if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
989             $this->coefstring = 'aggregationcoefextrasum';
991         } else if ($this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
992             $this->coefstring = 'aggregationcoefextraweight';
994         } else if ($this->aggregation == GRADE_AGGREGATE_SUM) {
995             $this->coefstring = 'aggregationcoefextrasum';
997         } else {
998             $this->coefstring = 'aggregationcoef';
999         }
1000         return $this->coefstring;
1001     }
1003     /**
1004      * Returns tree with all grade_items and categories as elements
1005      *
1006      * @param int $courseid The course ID
1007      * @param boolean $include_category_items as category children
1008      *
1009      * @return array
1010      * @static
1011      */
1012     public static function fetch_course_tree($courseid, $include_category_items=false) {
1013         $course_category = grade_category::fetch_course_category($courseid);
1014         $category_array = array('object'=>$course_category, 'type'=>'category', 'depth'=>1,
1015                                 'children'=>$course_category->get_children($include_category_items));
1016         $sortorder = 1;
1017         $course_category->set_sortorder($sortorder);
1018         $course_category->sortorder = $sortorder;
1019         return grade_category::_fetch_course_tree_recursion($category_array, $sortorder);
1020     }
1022     /**
1023      * Needs documenting
1024      *
1025      * @param array $category_array The seed of the recursion
1026      * @param int   &$sortorder The current sortorder
1027      *
1028      * @return array
1029      * @static
1030      * @todo Document
1031      */
1032     static private function _fetch_course_tree_recursion($category_array, &$sortorder) {
1033         // update the sortorder in db if needed
1034         if ($category_array['object']->sortorder != $sortorder) {
1035             $category_array['object']->set_sortorder($sortorder);
1036         }
1038         // store the grade_item or grade_category instance with extra info
1039         $result = array('object'=>$category_array['object'], 'type'=>$category_array['type'], 'depth'=>$category_array['depth']);
1041         // reuse final grades if there
1042         if (array_key_exists('finalgrades', $category_array)) {
1043             $result['finalgrades'] = $category_array['finalgrades'];
1044         }
1046         // recursively resort children
1047         if (!empty($category_array['children'])) {
1048             $result['children'] = array();
1049             //process the category item first
1050             $cat_item_id = null;
1052             foreach ($category_array['children'] as $oldorder=>$child_array) {
1054                 if ($child_array['type'] == 'courseitem' or $child_array['type'] == 'categoryitem') {
1055                     $result['children'][$sortorder] = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
1056                 }
1057             }
1059             foreach ($category_array['children'] as $oldorder=>$child_array) {
1061                 if ($child_array['type'] != 'courseitem' and $child_array['type'] != 'categoryitem') {
1062                     $result['children'][++$sortorder] = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
1063                 }
1064             }
1065         }
1067         return $result;
1068     }
1070     /**
1071      * Fetches and returns all the children categories and/or grade_items belonging to this category.
1072      * By default only returns the immediate children (depth=1), but deeper levels can be requested,
1073      * as well as all levels (0). The elements are indexed by sort order.
1074      *
1075      * @param bool $include_category_items Whether or not to include category grade_items in the children array
1076      *
1077      * @return array Array of child objects (grade_category and grade_item).
1078      */
1079     public function get_children($include_category_items=false) {
1080         global $DB;
1082         // This function must be as fast as possible ;-)
1083         // fetch all course grade items and categories into memory - we do not expect hundreds of these in course
1084         // we have to limit the number of queries though, because it will be used often in grade reports
1086         $cats  = $DB->get_records('grade_categories', array('courseid' => $this->courseid));
1087         $items = $DB->get_records('grade_items', array('courseid' => $this->courseid));
1089         // init children array first
1090         foreach ($cats as $catid=>$cat) {
1091             $cats[$catid]->children = array();
1092         }
1094         //first attach items to cats and add category sortorder
1095         foreach ($items as $item) {
1097             if ($item->itemtype == 'course' or $item->itemtype == 'category') {
1098                 $cats[$item->iteminstance]->sortorder = $item->sortorder;
1100                 if (!$include_category_items) {
1101                     continue;
1102                 }
1103                 $categoryid = $item->iteminstance;
1105             } else {
1106                 $categoryid = $item->categoryid;
1107             }
1109             // prevent problems with duplicate sortorders in db
1110             $sortorder = $item->sortorder;
1112             while (array_key_exists($sortorder, $cats[$categoryid]->children)) {
1113                 //debugging("$sortorder exists in item loop");
1114                 $sortorder++;
1115             }
1117             $cats[$categoryid]->children[$sortorder] = $item;
1119         }
1121         // now find the requested category and connect categories as children
1122         $category = false;
1124         foreach ($cats as $catid=>$cat) {
1126             if (empty($cat->parent)) {
1128                 if ($cat->path !== '/'.$cat->id.'/') {
1129                     $grade_category = new grade_category($cat, false);
1130                     $grade_category->path  = '/'.$cat->id.'/';
1131                     $grade_category->depth = 1;
1132                     $grade_category->update('system');
1133                     return $this->get_children($include_category_items);
1134                 }
1136             } else {
1138                 if (empty($cat->path) or !preg_match('|/'.$cat->parent.'/'.$cat->id.'/$|', $cat->path)) {
1139                     //fix paths and depts
1140                     static $recursioncounter = 0; // prevents infinite recursion
1141                     $recursioncounter++;
1143                     if ($recursioncounter < 5) {
1144                         // fix paths and depths!
1145                         $grade_category = new grade_category($cat, false);
1146                         $grade_category->depth = 0;
1147                         $grade_category->path  = null;
1148                         $grade_category->update('system');
1149                         return $this->get_children($include_category_items);
1150                     }
1151                 }
1152                 // prevent problems with duplicate sortorders in db
1153                 $sortorder = $cat->sortorder;
1155                 while (array_key_exists($sortorder, $cats[$cat->parent]->children)) {
1156                     //debugging("$sortorder exists in cat loop");
1157                     $sortorder++;
1158                 }
1160                 $cats[$cat->parent]->children[$sortorder] = &$cats[$catid];
1161             }
1163             if ($catid == $this->id) {
1164                 $category = &$cats[$catid];
1165             }
1166         }
1168         unset($items); // not needed
1169         unset($cats); // not needed
1171         $children_array = grade_category::_get_children_recursion($category);
1173         ksort($children_array);
1175         return $children_array;
1177     }
1179     /**
1180      * Private method used to retrieve all children of this category recursively
1181      *
1182      * @param grade_category $category Source of current recursion
1183      *
1184      * @return array
1185      */
1186     private function _get_children_recursion($category) {
1188         $children_array = array();
1189         foreach ($category->children as $sortorder=>$child) {
1191             if (array_key_exists('itemtype', $child)) {
1192                 $grade_item = new grade_item($child, false);
1194                 if (in_array($grade_item->itemtype, array('course', 'category'))) {
1195                     $type  = $grade_item->itemtype.'item';
1196                     $depth = $category->depth;
1198                 } else {
1199                     $type  = 'item';
1200                     $depth = $category->depth; // we use this to set the same colour
1201                 }
1202                 $children_array[$sortorder] = array('object'=>$grade_item, 'type'=>$type, 'depth'=>$depth);
1204             } else {
1205                 $children = grade_category::_get_children_recursion($child);
1206                 $grade_category = new grade_category($child, false);
1208                 if (empty($children)) {
1209                     $children = array();
1210                 }
1211                 $children_array[$sortorder] = array('object'=>$grade_category, 'type'=>'category', 'depth'=>$grade_category->depth, 'children'=>$children);
1212             }
1213         }
1215         // sort the array
1216         ksort($children_array);
1218         return $children_array;
1219     }
1221     /**
1222      * Uses get_grade_item to load or create a grade_item, then saves it as $this->grade_item.
1223      * @return object Grade_item
1224      */
1225     public function load_grade_item() {
1226         if (empty($this->grade_item)) {
1227             $this->grade_item = $this->get_grade_item();
1228         }
1229         return $this->grade_item;
1230     }
1232     /**
1233      * Retrieves from DB and instantiates the associated grade_item object.
1234      * If no grade_item exists yet, create one.
1235      * @return object Grade_item
1236      */
1237     public function get_grade_item() {
1238         if (empty($this->id)) {
1239             debugging("Attempt to obtain a grade_category's associated grade_item without the category's ID being set.");
1240             return false;
1241         }
1243         if (empty($this->parent)) {
1244             $params = array('courseid'=>$this->courseid, 'itemtype'=>'course', 'iteminstance'=>$this->id);
1246         } else {
1247             $params = array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id);
1248         }
1250         if (!$grade_items = grade_item::fetch_all($params)) {
1251             // create a new one
1252             $grade_item = new grade_item($params, false);
1253             $grade_item->gradetype = GRADE_TYPE_VALUE;
1254             $grade_item->insert('system');
1256         } else if (count($grade_items) == 1) {
1257             // found existing one
1258             $grade_item = reset($grade_items);
1260         } else {
1261             debugging("Found more than one grade_item attached to category id:".$this->id);
1262             // return first one
1263             $grade_item = reset($grade_items);
1264         }
1266         return $grade_item;
1267     }
1269     /**
1270      * Uses $this->parent to instantiate $this->parent_category based on the
1271      * referenced record in the DB.
1272      * @return object Parent_category
1273      */
1274     public function load_parent_category() {
1275         if (empty($this->parent_category) && !empty($this->parent)) {
1276             $this->parent_category = $this->get_parent_category();
1277         }
1278         return $this->parent_category;
1279     }
1281     /**
1282      * Uses $this->parent to instantiate and return a grade_category object.
1283      * @return object Parent_category
1284      */
1285     public function get_parent_category() {
1286         if (!empty($this->parent)) {
1287             $parent_category = new grade_category(array('id' => $this->parent));
1288             return $parent_category;
1289         } else {
1290             return null;
1291         }
1292     }
1294     /**
1295      * Returns the most descriptive field for this object. This is a standard method used
1296      * when we do not know the exact type of an object.
1297      *
1298      * @return string name
1299      */
1300     public function get_name() {
1301         global $DB;
1302         // For a course category, we return the course name if the fullname is set to '?' in the DB (empty in the category edit form)
1303         if (empty($this->parent) && $this->fullname == '?') {
1304             $course = $DB->get_record('course', array('id'=> $this->courseid));
1305             return format_string($course->fullname);
1307         } else {
1308             return $this->fullname;
1309         }
1310     }
1312     /**
1313      * Sets this category's parent id. A generic method shared by objects that have a parent id of some kind.
1314      *
1315      * @param int            $parentid The ID of the category parent to $this
1316      * @param grade_category $source An optional grade_category to use as the source for the parent
1317      *
1318      * @return boolean success
1319      */
1320     public function set_parent($parentid, $source=null) {
1321         if ($this->parent == $parentid) {
1322             return true;
1323         }
1325         if ($parentid == $this->id) {
1326             print_error('cannotassignselfasparent');
1327         }
1329         if (empty($this->parent) and $this->is_course_category()) {
1330             print_error('cannothaveparentcate');
1331         }
1333         // find parent and check course id
1334         if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
1335             return false;
1336         }
1338         $this->force_regrading();
1340         // set new parent category
1341         $this->parent          = $parent_category->id;
1342         $this->parent_category =& $parent_category;
1343         $this->path            = null;       // remove old path and depth - will be recalculated in update()
1344         $this->depth           = 0;          // remove old path and depth - will be recalculated in update()
1345         $this->update($source);
1347         return $this->update($source);
1348     }
1350     /**
1351      * Returns the final values for this grade category.
1352      *
1353      * @param int $userid Optional: to retrieve a single final grade
1354      *
1355      * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
1356      */
1357     public function get_final($userid=null) {
1358         $this->load_grade_item();
1359         return $this->grade_item->get_final($userid);
1360     }
1362     /**
1363      * Returns the sortorder of the associated grade_item. This method is also available in
1364      * grade_item, for cases where the object type is not known.
1365      *
1366      * @return int Sort order
1367      */
1368     public function get_sortorder() {
1369         $this->load_grade_item();
1370         return $this->grade_item->get_sortorder();
1371     }
1373     /**
1374      * Returns the idnumber of the associated grade_item. This method is also available in
1375      * grade_item, for cases where the object type is not known.
1376      *
1377      * @return string idnumber
1378      */
1379     public function get_idnumber() {
1380         $this->load_grade_item();
1381         return $this->grade_item->get_idnumber();
1382     }
1384     /**
1385      * Sets sortorder variable for this category.
1386      * This method is also available in grade_item, for cases where the object type is not know.
1387      *
1388      * @param int $sortorder The sortorder to assign to this category
1389      *
1390      * @return void
1391      */
1392     public function set_sortorder($sortorder) {
1393         $this->load_grade_item();
1394         $this->grade_item->set_sortorder($sortorder);
1395     }
1397     /**
1398      * Move this category after the given sortorder - does not change the parent
1399      *
1400      * @param int $sortorder to place after.
1401      *
1402      * @return void
1403      */
1404     public function move_after_sortorder($sortorder) {
1405         $this->load_grade_item();
1406         $this->grade_item->move_after_sortorder($sortorder);
1407     }
1409     /**
1410      * Return true if this is the top most category that represents the total course grade.
1411      *
1412      * @return boolean
1413      */
1414     public function is_course_category() {
1415         $this->load_grade_item();
1416         return $this->grade_item->is_course_item();
1417     }
1419     /**
1420      * Return the top most course category.
1421      *
1422      * @param int $courseid The Course ID
1423      *
1424      * @return object grade_category instance for course grade
1425      * @static
1426      */
1427     public function fetch_course_category($courseid) {
1428         if (empty($courseid)) {
1429             debugging('Missing course id!');
1430             return false;
1431         }
1433         // course category has no parent
1434         if ($course_category = grade_category::fetch(array('courseid'=>$courseid, 'parent'=>null))) {
1435             return $course_category;
1436         }
1438         // create a new one
1439         $course_category = new grade_category();
1440         $course_category->insert_course_category($courseid);
1442         return $course_category;
1443     }
1445     /**
1446      * Is grading object editable?
1447      *
1448      * @return boolean
1449      */
1450     public function is_editable() {
1451         return true;
1452     }
1454     /**
1455      * Returns the locked state/date of the associated grade_item. This method is also available in
1456      * grade_item, for cases where the object type is not known.
1457      * @return boolean
1458      */
1459     public function is_locked() {
1460         $this->load_grade_item();
1461         return $this->grade_item->is_locked();
1462     }
1464     /**
1465      * Sets the grade_item's locked variable and updates the grade_item.
1466      * Method named after grade_item::set_locked().
1467      *
1468      * @param int  $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
1469      * @param bool $cascade lock/unlock child objects too
1470      * @param bool $refresh refresh grades when unlocking
1471      *
1472      * @return boolean success if category locked (not all children mayb be locked though)
1473      */
1474     public function set_locked($lockedstate, $cascade=false, $refresh=true) {
1475         $this->load_grade_item();
1477         $result = $this->grade_item->set_locked($lockedstate, $cascade, true);
1479         if ($cascade) {
1480             //process all children - items and categories
1481             if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
1483                 foreach ($children as $child) {
1484                     $child->set_locked($lockedstate, true, false);
1486                     if (empty($lockedstate) and $refresh) {
1487                         //refresh when unlocking
1488                         $child->refresh_grades();
1489                     }
1490                 }
1491             }
1493             if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
1495                 foreach ($children as $child) {
1496                     $child->set_locked($lockedstate, true, true);
1497                 }
1498             }
1499         }
1501         return $result;
1502     }
1504     public static function set_properties(&$instance, $params) {
1505         global $DB;
1507         parent::set_properties($instance, $params);
1509         //if theyve changed aggregation type we made need to do some fiddling to provide appropriate defaults
1510         if (!empty($params->aggregation)) {
1512             //weight and extra credit share a column :( Would like a default of 1 for weight and 0 for extra credit
1513             //Flip from the default of 0 to 1 (or vice versa) if ALL items in the category are still set to the old default.
1514             if ($params->aggregation==GRADE_AGGREGATE_WEIGHTED_MEAN || $params->aggregation==GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
1515                 $sql = $defaultaggregationcoef = null;
1516                 
1517                 if ($params->aggregation==GRADE_AGGREGATE_WEIGHTED_MEAN) {
1518                     //if all items in this category have aggregation coefficient of 0 we can change it to 1 ie evenly weighted
1519                     $sql = "select count(id) from {grade_items} where categoryid=:categoryid and aggregationcoef!=0";
1520                     $defaultaggregationcoef = 1;
1521                 } else if ($params->aggregation==GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
1522                     //if all items in this category have aggregation coefficient of 1 we can change it to 0 ie no extra credit
1523                     $sql = "select count(id) from {grade_items} where categoryid=:categoryid and aggregationcoef!=1";
1524                     $defaultaggregationcoef = 0;
1525                 }
1527                 $params = array('categoryid'=>$instance->id);
1528                 $count = $DB->count_records_sql($sql, $params);
1529                 if ($count===0) { //category is either empty or all items are set to a default value so we can switch defaults
1530                     $params['aggregationcoef'] = $defaultaggregationcoef;
1531                     $DB->execute("update {grade_items} set aggregationcoef=:aggregationcoef where categoryid=:categoryid",$params);
1532                 }
1533             }
1534         }
1535     }
1537     /**
1538      * Sets the grade_item's hidden variable and updates the grade_item.
1539      * Method named after grade_item::set_hidden().
1540      * @param int $hidden 0, 1 or a timestamp int(10) after which date the item will be hidden.
1541      * @param boolean $cascade apply to child objects too
1542      * @return void
1543      */
1544     public function set_hidden($hidden, $cascade=false) {
1545         $this->load_grade_item();
1546         //this hides the associated grade item (the course total)
1547         $this->grade_item->set_hidden($hidden, $cascade);
1548         //this hides the category itself and everything it contains
1549         parent::set_hidden($hidden, $cascade);
1551         if ($cascade) {
1553             if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
1555                 foreach ($children as $child) {
1556                     $child->set_hidden($hidden, $cascade);
1557                 }
1558             }
1560             if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
1562                 foreach ($children as $child) {
1563                     $child->set_hidden($hidden, $cascade);
1564                 }
1565             }
1566         }
1568         //if marking category visible make sure parent category is visible MDL-21367
1569         if( !$hidden ) {
1570             $category_array = grade_category::fetch_all(array('id'=>$this->parent));
1571             if ($category_array && array_key_exists($this->parent, $category_array)) {
1572                 $category = $category_array[$this->parent];
1573                 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
1574                 //if($category->is_hidden()) {
1575                     $category->set_hidden($hidden, false);
1576                 //}
1577             }
1578         }
1579     }
1581     /**
1582      * Applies default settings on this category
1583      * @return bool true if anything changed
1584      */
1585     public function apply_default_settings() {
1586         global $CFG;
1588         foreach ($this->forceable as $property) {
1590             if (isset($CFG->{"grade_$property"})) {
1592                 if ($CFG->{"grade_$property"} == -1) {
1593                     continue; //temporary bc before version bump
1594                 }
1595                 $this->$property = $CFG->{"grade_$property"};
1596             }
1597         }
1598     }
1600     /**
1601      * Applies forced settings on this category
1602      * @return bool true if anything changed
1603      */
1604     public function apply_forced_settings() {
1605         global $CFG;
1607         $updated = false;
1609         foreach ($this->forceable as $property) {
1611             if (isset($CFG->{"grade_$property"}) and isset($CFG->{"grade_{$property}_flag"}) and
1612                                                     ((int) $CFG->{"grade_{$property}_flag"} & 1)) {
1614                 if ($CFG->{"grade_$property"} == -1) {
1615                     continue; //temporary bc before version bump
1616                 }
1617                 $this->$property = $CFG->{"grade_$property"};
1618                 $updated = true;
1619             }
1620         }
1622         return $updated;
1623     }
1625     /**
1626      * Notification of change in forced category settings.
1627      *
1628      * @return void
1629      * @static
1630      */
1631     public static function updated_forced_settings() {
1632         global $CFG, $DB;
1633         $params = array(1, 'course', 'category');
1634         $sql = "UPDATE {grade_items} SET needsupdate=? WHERE itemtype=? or itemtype=?";
1635         $DB->execute($sql, $params);
1636     }