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