Merge branch 'MDL-47637-master' 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                                  '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      * Array of grade_items or grade_categories nested exactly 1 level below this category
121      * @var array $children
122      */
123     public $children;
125     /**
126      * A hierarchical array of all children below this category. This is stored separately from
127      * $children because it is more memory-intensive and may not be used as often.
128      * @var array $all_children
129      */
130     public $all_children;
132     /**
133      * An associated grade_item object, with itemtype=category, used to calculate and cache a set of grade values
134      * for this category.
135      * @var grade_item $grade_item
136      */
137     public $grade_item;
139     /**
140      * Temporary sortorder for speedup of children resorting
141      * @var int $sortorder
142      */
143     public $sortorder;
145     /**
146      * List of options which can be "forced" from site settings.
147      * @var array $forceable
148      */
149     public $forceable = array('aggregation', 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes');
151     /**
152      * String representing the aggregation coefficient. Variable is used as cache.
153      * @var string $coefstring
154      */
155     public $coefstring = null;
157     /**
158      * Static variable storing the result from {@link self::can_apply_limit_rules}.
159      * @var bool
160      */
161     protected $canapplylimitrules;
163     /**
164      * Builds this category's path string based on its parents (if any) and its own id number.
165      * This is typically done just before inserting this object in the DB for the first time,
166      * or when a new parent is added or changed. It is a recursive function: once the calling
167      * object no longer has a parent, the path is complete.
168      *
169      * @param grade_category $grade_category A Grade_Category object
170      * @return string The category's path string
171      */
172     public static function build_path($grade_category) {
173         global $DB;
175         if (empty($grade_category->parent)) {
176             return '/'.$grade_category->id.'/';
178         } else {
179             $parent = $DB->get_record('grade_categories', array('id' => $grade_category->parent));
180             return grade_category::build_path($parent).$grade_category->id.'/';
181         }
182     }
184     /**
185      * Finds and returns a grade_category instance based on params.
186      *
187      * @param array $params associative arrays varname=>value
188      * @return grade_category The retrieved grade_category instance or false if none found.
189      */
190     public static function fetch($params) {
191         return grade_object::fetch_helper('grade_categories', 'grade_category', $params);
192     }
194     /**
195      * Finds and returns all grade_category instances based on params.
196      *
197      * @param array $params associative arrays varname=>value
198      * @return array array of grade_category insatnces or false if none found.
199      */
200     public static function fetch_all($params) {
201         return grade_object::fetch_all_helper('grade_categories', 'grade_category', $params);
202     }
204     /**
205      * In addition to update() as defined in grade_object, call force_regrading of parent categories, if applicable.
206      *
207      * @param string $source from where was the object updated (mod/forum, manual, etc.)
208      * @return bool success
209      */
210     public function update($source=null) {
211         // load the grade item or create a new one
212         $this->load_grade_item();
214         // force recalculation of path;
215         if (empty($this->path)) {
216             $this->path  = grade_category::build_path($this);
217             $this->depth = substr_count($this->path, '/') - 1;
218             $updatechildren = true;
220         } else {
221             $updatechildren = false;
222         }
224         $this->apply_forced_settings();
226         // these are exclusive
227         if ($this->droplow > 0) {
228             $this->keephigh = 0;
230         } else if ($this->keephigh > 0) {
231             $this->droplow = 0;
232         }
234         // Recalculate grades if needed
235         if ($this->qualifies_for_regrading()) {
236             $this->force_regrading();
237         }
239         $this->timemodified = time();
241         $result = parent::update($source);
243         // now update paths in all child categories
244         if ($result and $updatechildren) {
246             if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
248                 foreach ($children as $child) {
249                     $child->path  = null;
250                     $child->depth = 0;
251                     $child->update($source);
252                 }
253             }
254         }
256         return $result;
257     }
259     /**
260      * If parent::delete() is successful, send force_regrading message to parent category.
261      *
262      * @param string $source from where was the object deleted (mod/forum, manual, etc.)
263      * @return bool success
264      */
265     public function delete($source=null) {
266         $grade_item = $this->load_grade_item();
268         if ($this->is_course_category()) {
270             if ($categories = grade_category::fetch_all(array('courseid'=>$this->courseid))) {
272                 foreach ($categories as $category) {
274                     if ($category->id == $this->id) {
275                         continue; // do not delete course category yet
276                     }
277                     $category->delete($source);
278                 }
279             }
281             if ($items = grade_item::fetch_all(array('courseid'=>$this->courseid))) {
283                 foreach ($items as $item) {
285                     if ($item->id == $grade_item->id) {
286                         continue; // do not delete course item yet
287                     }
288                     $item->delete($source);
289                 }
290             }
292         } else {
293             $this->force_regrading();
295             $parent = $this->load_parent_category();
297             // Update children's categoryid/parent field first
298             if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
299                 foreach ($children as $child) {
300                     $child->set_parent($parent->id);
301                 }
302             }
304             if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
305                 foreach ($children as $child) {
306                     $child->set_parent($parent->id);
307                 }
308             }
309         }
311         // first delete the attached grade item and grades
312         $grade_item->delete($source);
314         // delete category itself
315         return parent::delete($source);
316     }
318     /**
319      * In addition to the normal insert() defined in grade_object, this method sets the depth
320      * and path for this object, and update the record accordingly.
321      *
322      * We do this here instead of in the constructor as they both need to know the record's
323      * ID number, which only gets created at insertion time.
324      * This method also creates an associated grade_item if this wasn't done during construction.
325      *
326      * @param string $source from where was the object inserted (mod/forum, manual, etc.)
327      * @return int PK ID if successful, false otherwise
328      */
329     public function insert($source=null) {
331         if (empty($this->courseid)) {
332             print_error('cannotinsertgrade');
333         }
335         if (empty($this->parent)) {
336             $course_category = grade_category::fetch_course_category($this->courseid);
337             $this->parent = $course_category->id;
338         }
340         $this->path = null;
342         $this->timecreated = $this->timemodified = time();
344         if (!parent::insert($source)) {
345             debugging("Could not insert this category: " . print_r($this, true));
346             return false;
347         }
349         $this->force_regrading();
351         // build path and depth
352         $this->update($source);
354         return $this->id;
355     }
357     /**
358      * Internal function - used only from fetch_course_category()
359      * Normal insert() can not be used for course category
360      *
361      * @param int $courseid The course ID
362      * @return int The ID of the new course category
363      */
364     public function insert_course_category($courseid) {
365         $this->courseid    = $courseid;
366         $this->fullname    = '?';
367         $this->path        = null;
368         $this->parent      = null;
369         $this->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2;
371         $this->apply_default_settings();
372         $this->apply_forced_settings();
374         $this->timecreated = $this->timemodified = time();
376         if (!parent::insert('system')) {
377             debugging("Could not insert this category: " . print_r($this, true));
378             return false;
379         }
381         // build path and depth
382         $this->update('system');
384         return $this->id;
385     }
387     /**
388      * Compares the values held by this object with those of the matching record in DB, and returns
389      * whether or not these differences are sufficient to justify an update of all parent objects.
390      * This assumes that this object has an ID number and a matching record in DB. If not, it will return false.
391      *
392      * @return bool
393      */
394     public function qualifies_for_regrading() {
395         if (empty($this->id)) {
396             debugging("Can not regrade non existing category");
397             return false;
398         }
400         $db_item = grade_category::fetch(array('id'=>$this->id));
402         $aggregationdiff = $db_item->aggregation         != $this->aggregation;
403         $keephighdiff    = $db_item->keephigh            != $this->keephigh;
404         $droplowdiff     = $db_item->droplow             != $this->droplow;
405         $aggonlygrddiff  = $db_item->aggregateonlygraded != $this->aggregateonlygraded;
406         $aggoutcomesdiff = $db_item->aggregateoutcomes   != $this->aggregateoutcomes;
408         return ($aggregationdiff || $keephighdiff || $droplowdiff || $aggonlygrddiff || $aggoutcomesdiff);
409     }
411     /**
412      * Marks this grade categories' associated grade item as needing regrading
413      */
414     public function force_regrading() {
415         $grade_item = $this->load_grade_item();
416         $grade_item->force_regrading();
417     }
419     /**
420      * Something that should be called before we start regrading the whole course.
421      *
422      * @return void
423      */
424     public function pre_regrade_final_grades() {
425         $this->auto_update_weights();
426         $this->auto_update_max();
427     }
429     /**
430      * Generates and saves final grades in associated category grade item.
431      * These immediate children must already have their own final grades.
432      * The category's aggregation method is used to generate final grades.
433      *
434      * Please note that category grade is either calculated or aggregated, not both at the same time.
435      *
436      * This method must be used ONLY from grade_item::regrade_final_grades(),
437      * because the calculation must be done in correct order!
438      *
439      * Steps to follow:
440      *  1. Get final grades from immediate children
441      *  3. Aggregate these grades
442      *  4. Save them in final grades of associated category grade item
443      *
444      * @param int $userid The user ID if final grade generation should be limited to a single user
445      * @return bool
446      */
447     public function generate_grades($userid=null) {
448         global $CFG, $DB;
450         $this->load_grade_item();
452         if ($this->grade_item->is_locked()) {
453             return true; // no need to recalculate locked items
454         }
456         // find grade items of immediate children (category or grade items) and force site settings
457         $depends_on = $this->grade_item->depends_on();
459         if (empty($depends_on)) {
460             $items = false;
462         } else {
463             list($usql, $params) = $DB->get_in_or_equal($depends_on);
464             $sql = "SELECT *
465                       FROM {grade_items}
466                      WHERE id $usql";
467             $items = $DB->get_records_sql($sql, $params);
468         }
470         $grade_inst = new grade_grade();
471         $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
473         // where to look for final grades - include grade of this item too, we will store the results there
474         $gis = array_merge($depends_on, array($this->grade_item->id));
475         list($usql, $params) = $DB->get_in_or_equal($gis);
477         if ($userid) {
478             $usersql = "AND g.userid=?";
479             $params[] = $userid;
481         } else {
482             $usersql = "";
483         }
485         $sql = "SELECT $fields
486                   FROM {grade_grades} g, {grade_items} gi
487                  WHERE gi.id = g.itemid AND gi.id $usql $usersql
488               ORDER BY g.userid";
490         // group the results by userid and aggregate the grades for this user
491         $rs = $DB->get_recordset_sql($sql, $params);
492         if ($rs->valid()) {
493             $prevuser = 0;
494             $grade_values = array();
495             $excluded     = array();
496             $oldgrade     = null;
497             $grademaxoverrides = array();
498             $grademinoverrides = array();
500             foreach ($rs as $used) {
502                 if ($used->userid != $prevuser) {
503                     $this->aggregate_grades($prevuser,
504                                             $items,
505                                             $grade_values,
506                                             $oldgrade,
507                                             $excluded,
508                                             $grademinoverrides,
509                                             $grademaxoverrides);
510                     $prevuser = $used->userid;
511                     $grade_values = array();
512                     $excluded     = array();
513                     $oldgrade     = null;
514                     $grademaxoverrides = array();
515                     $grademinoverrides = array();
516                 }
517                 $grade_values[$used->itemid] = $used->finalgrade;
518                 $grademaxoverrides[$used->itemid] = $used->rawgrademax;
519                 $grademinoverrides[$used->itemid] = $used->rawgrademin;
521                 if ($used->excluded) {
522                     $excluded[] = $used->itemid;
523                 }
525                 if ($this->grade_item->id == $used->itemid) {
526                     $oldgrade = $used;
527                 }
528             }
529             $this->aggregate_grades($prevuser,
530                                     $items,
531                                     $grade_values,
532                                     $oldgrade,
533                                     $excluded,
534                                     $grademinoverrides,
535                                     $grademaxoverrides);//the last one
536         }
537         $rs->close();
539         return true;
540     }
542     /**
543      * Internal function for grade category grade aggregation
544      *
545      * @param int    $userid The User ID
546      * @param array  $items Grade items
547      * @param array  $grade_values Array of grade values
548      * @param object $oldgrade Old grade
549      * @param array  $excluded Excluded
550      * @param array  $grademinoverrides User specific grademin values if different to the grade_item grademin (key is itemid)
551      * @param array  $grademaxoverrides User specific grademax values if different to the grade_item grademax (key is itemid)
552      */
553     private function aggregate_grades($userid,
554                                       $items,
555                                       $grade_values,
556                                       $oldgrade,
557                                       $excluded,
558                                       $grademinoverrides,
559                                       $grademaxoverrides) {
560         global $CFG, $DB;
562         // Remember these so we can set flags on them to describe how they were used in the aggregation.
563         $novalue = array();
564         $dropped = array();
565         $extracredit = array();
566         $usedweights = array();
568         if (empty($userid)) {
569             //ignore first call
570             return;
571         }
573         if ($oldgrade) {
574             $oldfinalgrade = $oldgrade->finalgrade;
575             $grade = new grade_grade($oldgrade, false);
576             $grade->grade_item =& $this->grade_item;
578         } else {
579             // insert final grade - it will be needed later anyway
580             $grade = new grade_grade(array('itemid'=>$this->grade_item->id, 'userid'=>$userid), false);
581             $grade->grade_item =& $this->grade_item;
582             $grade->insert('system');
583             $oldfinalgrade = null;
584         }
586         // no need to recalculate locked or overridden grades
587         if ($grade->is_locked() or $grade->is_overridden()) {
588             return;
589         }
591         // can not use own final category grade in calculation
592         unset($grade_values[$this->grade_item->id]);
594         // Make sure a grade_grade exists for every grade_item.
595         // We need to do this so we can set the aggregationstatus
596         // with a set_field call instead of checking if each one exists and creating/updating.
597         if (!empty($items)) {
598             list($ggsql, $params) = $DB->get_in_or_equal(array_keys($items), SQL_PARAMS_NAMED, 'g');
601             $params['userid'] = $userid;
602             $sql = "SELECT itemid
603                       FROM {grade_grades}
604                      WHERE itemid $ggsql AND userid = :userid";
605             $existingitems = $DB->get_records_sql($sql, $params);
607             $notexisting = array_diff(array_keys($items), array_keys($existingitems));
608             foreach ($notexisting as $itemid) {
609                 $gradeitem = $items[$itemid];
610                 $gradegrade = new grade_grade(array('itemid' => $itemid,
611                                                     'userid' => $userid,
612                                                     'rawgrademin' => $gradeitem->grademin,
613                                                     'rawgrademax' => $gradeitem->grademax), false);
614                 $gradegrade->grade_item = $gradeitem;
615                 $gradegrade->insert('system');
616             }
617         }
619         // if no grades calculation possible or grading not allowed clear final grade
620         if (empty($grade_values) or empty($items) or ($this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE)) {
621             $grade->finalgrade = null;
623             if (!is_null($oldfinalgrade)) {
624                 $grade->timemodified = time();
625                 $success = $grade->update('aggregation');
627                 // If successful trigger a user_graded event.
628                 if ($success) {
629                     \core\event\user_graded::create_from_grade($grade)->trigger();
630                 }
631             }
632             $dropped = $grade_values;
633             $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit);
634             return;
635         }
637         // Normalize the grades first - all will have value 0...1
638         // ungraded items are not used in aggregation.
639         foreach ($grade_values as $itemid=>$v) {
640             if (is_null($v)) {
641                 // If null, it means no grade.
642                 if ($this->aggregateonlygraded) {
643                     unset($grade_values[$itemid]);
644                     // Mark this item as "excluded empty" because it has no grade.
645                     $novalue[$itemid] = 0;
646                     continue;
647                 }
648             }
649             if (in_array($itemid, $excluded)) {
650                 unset($grade_values[$itemid]);
651                 $dropped[$itemid] = 0;
652                 continue;
653             }
654             // Check for user specific grade min/max overrides.
655             $usergrademin = $items[$itemid]->grademin;
656             $usergrademax = $items[$itemid]->grademax;
657             if (isset($grademinoverrides[$itemid])) {
658                 $usergrademin = $grademinoverrides[$itemid];
659             }
660             if (isset($grademaxoverrides[$itemid])) {
661                 $usergrademax = $grademaxoverrides[$itemid];
662             }
663             if ($this->aggregation == GRADE_AGGREGATE_SUM) {
664                 // Assume that the grademin is 0 when standardising the score, to preserve negative grades.
665                 $grade_values[$itemid] = grade_grade::standardise_score($v, 0, $usergrademax, 0, 1);
666             } else {
667                 $grade_values[$itemid] = grade_grade::standardise_score($v, $usergrademin, $usergrademax, 0, 1);
668             }
670         }
672         // For items with no value, and not excluded - either set their grade to 0 or exclude them.
673         foreach ($items as $itemid=>$value) {
674             if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
675                 if (!$this->aggregateonlygraded) {
676                     $grade_values[$itemid] = 0;
677                 } else {
678                     // We are specifically marking these items as "excluded empty".
679                     $novalue[$itemid] = 0;
680                 }
681             }
682         }
684         // limit and sort
685         $allvalues = $grade_values;
686         if ($this->can_apply_limit_rules()) {
687             $this->apply_limit_rules($grade_values, $items);
688         }
690         $moredropped = array_diff($allvalues, $grade_values);
691         foreach ($moredropped as $drop => $unused) {
692             $dropped[$drop] = 0;
693         }
695         foreach ($grade_values as $itemid => $val) {
696             if (self::is_extracredit_used() && ($items[$itemid]->aggregationcoef > 0)) {
697                 $extracredit[$itemid] = 0;
698             }
699         }
701         asort($grade_values, SORT_NUMERIC);
703         // let's see we have still enough grades to do any statistics
704         if (count($grade_values) == 0) {
705             // not enough attempts yet
706             $grade->finalgrade = null;
708             if (!is_null($oldfinalgrade)) {
709                 $grade->timemodified = time();
710                 $success = $grade->update('aggregation');
712                 // If successful trigger a user_graded event.
713                 if ($success) {
714                     \core\event\user_graded::create_from_grade($grade)->trigger();
715                 }
716             }
717             $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit);
718             return;
719         }
721         // do the maths
722         $result = $this->aggregate_values_and_adjust_bounds($grade_values,
723                                                             $items,
724                                                             $usedweights,
725                                                             $grademinoverrides,
726                                                             $grademaxoverrides);
727         $agg_grade = $result['grade'];
729         // Set the actual grademin and max to bind the grade properly.
730         $this->grade_item->grademin = $result['grademin'];
731         $this->grade_item->grademax = $result['grademax'];
733         if ($this->aggregation == GRADE_AGGREGATE_SUM) {
734             // The natural aggregation always displays the range as coming from 0 for categories.
735             // However, when we bind the grade we allow for negative values.
736             $result['grademin'] = 0;
737         }
739         // Recalculate the grade back to requested range.
740         $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $result['grademin'], $result['grademax']);
741         $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade);
743         $oldrawgrademin = $grade->rawgrademin;
744         $oldrawgrademax = $grade->rawgrademax;
745         $grade->rawgrademin = $result['grademin'];
746         $grade->rawgrademax = $result['grademax'];
748         // Update in db if changed.
749         if (grade_floats_different($grade->finalgrade, $oldfinalgrade) ||
750                 grade_floats_different($grade->rawgrademax, $oldrawgrademax) ||
751                 grade_floats_different($grade->rawgrademin, $oldrawgrademin)) {
752             $grade->timemodified = time();
753             $success = $grade->update('aggregation');
755             // If successful trigger a user_graded event.
756             if ($success) {
757                 \core\event\user_graded::create_from_grade($grade)->trigger();
758             }
759         }
761         $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit);
763         return;
764     }
766     /**
767      * Set the flags on the grade_grade items to indicate how individual grades are used
768      * in the aggregation.
769      *
770      * @param int $userid The user we have aggregated the grades for.
771      * @param array $usedweights An array with keys for each of the grade_item columns included in the aggregation. The value are the relative weight.
772      * @param array $novalue An array with keys for each of the grade_item columns skipped because
773      *                       they had no value in the aggregation.
774      * @param array $dropped An array with keys for each of the grade_item columns dropped
775      *                       because of any drop lowest/highest settings in the aggregation.
776      * @param array $extracredit An array with keys for each of the grade_item columns
777      *                       considered extra credit by the aggregation.
778      */
779     private function set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit) {
780         global $DB;
782         // First set them all to weight null and status = 'unknown'.
783         if ($allitems = grade_item::fetch_all(array('categoryid'=>$this->id))) {
784             list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($allitems), SQL_PARAMS_NAMED, 'g');
786             $itemlist['userid'] = $userid;
788             $DB->set_field_select('grade_grades',
789                                   'aggregationstatus',
790                                   'unknown',
791                                   "itemid $itemsql AND userid = :userid",
792                                   $itemlist);
793             $DB->set_field_select('grade_grades',
794                                   'aggregationweight',
795                                   0,
796                                   "itemid $itemsql AND userid = :userid",
797                                   $itemlist);
798         }
800         // Included.
801         if (!empty($usedweights)) {
802             // The usedweights items are updated individually to record the weights.
803             foreach ($usedweights as $gradeitemid => $contribution) {
804                 $DB->set_field_select('grade_grades',
805                                       'aggregationweight',
806                                       $contribution,
807                                       "itemid = :itemid AND userid = :userid",
808                                       array('itemid'=>$gradeitemid, 'userid'=>$userid));
809             }
811             // Now set the status flag for all these weights.
812             list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($usedweights), SQL_PARAMS_NAMED, 'g');
813             $itemlist['userid'] = $userid;
815             $DB->set_field_select('grade_grades',
816                                   'aggregationstatus',
817                                   'used',
818                                   "itemid $itemsql AND userid = :userid",
819                                   $itemlist);
820         }
822         // No value.
823         if (!empty($novalue)) {
824             list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($novalue), SQL_PARAMS_NAMED, 'g');
826             $itemlist['userid'] = $userid;
828             $DB->set_field_select('grade_grades',
829                                   'aggregationstatus',
830                                   'novalue',
831                                   "itemid $itemsql AND userid = :userid",
832                                   $itemlist);
833         }
835         // Dropped.
836         if (!empty($dropped)) {
837             list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($dropped), SQL_PARAMS_NAMED, 'g');
839             $itemlist['userid'] = $userid;
841             $DB->set_field_select('grade_grades',
842                                   'aggregationstatus',
843                                   'dropped',
844                                   "itemid $itemsql AND userid = :userid",
845                                   $itemlist);
846         }
847         // Extra credit.
848         if (!empty($extracredit)) {
849             list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($extracredit), SQL_PARAMS_NAMED, 'g');
851             $itemlist['userid'] = $userid;
853             $DB->set_field_select('grade_grades',
854                                   'aggregationstatus',
855                                   'extra',
856                                   "itemid $itemsql AND userid = :userid",
857                                   $itemlist);
858         }
859     }
861     /**
862      * Internal function that calculates the aggregated grade and new min/max for this grade category
863      *
864      * Must be public as it is used by grade_grade::get_hiding_affected()
865      *
866      * @param array $grade_values An array of values to be aggregated
867      * @param array $items The array of grade_items
868      * @since Moodle 2.6.5, 2.7.2
869      * @param array & $weights If provided, will be filled with the normalized weights
870      *                         for each grade_item as used in the aggregation.
871      *                         Some rules for the weights are:
872      *                         1. The weights must add up to 1 (unless there are extra credit)
873      *                         2. The contributed points column must add up to the course
874      *                         final grade and this column is calculated from these weights.
875      * @param array  $grademinoverrides User specific grademin values if different to the grade_item grademin (key is itemid)
876      * @param array  $grademaxoverrides User specific grademax values if different to the grade_item grademax (key is itemid)
877      * @return array containing values for:
878      *                'grade' => the new calculated grade
879      *                'grademin' => the new calculated min grade for the category
880      *                'grademax' => the new calculated max grade for the category
881      */
882     public function aggregate_values_and_adjust_bounds($grade_values,
883                                                        $items,
884                                                        & $weights = null,
885                                                        $grademinoverrides = array(),
886                                                        $grademaxoverrides = array()) {
887         $category_item = $this->get_grade_item();
888         $grademin = $category_item->grademin;
889         $grademax = $category_item->grademax;
891         switch ($this->aggregation) {
893             case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies
894                 $num = count($grade_values);
895                 $grades = array_values($grade_values);
897                 // The median gets 100% - others get 0.
898                 if ($weights !== null && $num > 0) {
899                     $count = 0;
900                     foreach ($grade_values as $itemid=>$grade_value) {
901                         if (($num % 2 == 0) && ($count == intval($num/2)-1 || $count == intval($num/2))) {
902                             $weights[$itemid] = 0.5;
903                         } else if (($num % 2 != 0) && ($count == intval(($num/2)-0.5))) {
904                             $weights[$itemid] = 1.0;
905                         } else {
906                             $weights[$itemid] = 0;
907                         }
908                         $count++;
909                     }
910                 }
911                 if ($num % 2 == 0) {
912                     $agg_grade = ($grades[intval($num/2)-1] + $grades[intval($num/2)]) / 2;
913                 } else {
914                     $agg_grade = $grades[intval(($num/2)-0.5)];
915                 }
917                 break;
919             case GRADE_AGGREGATE_MIN:
920                 $agg_grade = reset($grade_values);
921                 // Record the weights as used.
922                 if ($weights !== null) {
923                     foreach ($grade_values as $itemid=>$grade_value) {
924                         $weights[$itemid] = 0;
925                     }
926                 }
927                 // Set the first item to 1.
928                 $itemids = array_keys($grade_values);
929                 $weights[reset($itemids)] = 1;
930                 break;
932             case GRADE_AGGREGATE_MAX:
933                 // Record the weights as used.
934                 if ($weights !== null) {
935                     foreach ($grade_values as $itemid=>$grade_value) {
936                         $weights[$itemid] = 0;
937                     }
938                 }
939                 // Set the last item to 1.
940                 $itemids = array_keys($grade_values);
941                 $weights[end($itemids)] = 1;
942                 $agg_grade = end($grade_values);
943                 break;
945             case GRADE_AGGREGATE_MODE:       // the most common value
946                 // array_count_values only counts INT and STRING, so if grades are floats we must convert them to string
947                 $converted_grade_values = array();
949                 foreach ($grade_values as $k => $gv) {
951                     if (!is_int($gv) && !is_string($gv)) {
952                         $converted_grade_values[$k] = (string) $gv;
954                     } else {
955                         $converted_grade_values[$k] = $gv;
956                     }
957                     if ($weights !== null) {
958                         $weights[$k] = 0;
959                     }
960                 }
962                 $freq = array_count_values($converted_grade_values);
963                 arsort($freq);                      // sort by frequency keeping keys
964                 $top = reset($freq);               // highest frequency count
965                 $modes = array_keys($freq, $top);  // search for all modes (have the same highest count)
966                 rsort($modes, SORT_NUMERIC);       // get highest mode
967                 $agg_grade = reset($modes);
968                 // Record the weights as used.
969                 if ($weights !== null && $top > 0) {
970                     foreach ($grade_values as $k => $gv) {
971                         if ($gv == $agg_grade) {
972                             $weights[$k] = 1.0 / $top;
973                         }
974                     }
975                 }
976                 break;
978             case GRADE_AGGREGATE_WEIGHTED_MEAN: // Weighted average of all existing final grades, weight specified in coef
979                 $weightsum = 0;
980                 $sum       = 0;
982                 foreach ($grade_values as $itemid=>$grade_value) {
983                     if ($weights !== null) {
984                         $weights[$itemid] = $items[$itemid]->aggregationcoef;
985                     }
986                     if ($items[$itemid]->aggregationcoef <= 0) {
987                         continue;
988                     }
989                     $weightsum += $items[$itemid]->aggregationcoef;
990                     $sum       += $items[$itemid]->aggregationcoef * $grade_value;
991                 }
992                 if ($weightsum == 0) {
993                     $agg_grade = null;
995                 } else {
996                     $agg_grade = $sum / $weightsum;
997                     if ($weights !== null) {
998                         // Normalise the weights.
999                         foreach ($weights as $itemid => $weight) {
1000                             $weights[$itemid] = $weight / $weightsum;
1001                         }
1002                     }
1004                 }
1005                 break;
1007             case GRADE_AGGREGATE_WEIGHTED_MEAN2:
1008                 // Weighted average of all existing final grades with optional extra credit flag,
1009                 // weight is the range of grade (usually grademax)
1010                 $this->load_grade_item();
1011                 $weightsum = 0;
1012                 $sum       = null;
1014                 foreach ($grade_values as $itemid=>$grade_value) {
1015                     if ($items[$itemid]->aggregationcoef > 0) {
1016                         continue;
1017                     }
1019                     $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
1020                     if ($weight <= 0) {
1021                         continue;
1022                     }
1024                     $weightsum += $weight;
1025                     $sum += $weight * $grade_value;
1026                 }
1028                 // Handle the extra credit items separately to calculate their weight accurately.
1029                 foreach ($grade_values as $itemid => $grade_value) {
1030                     if ($items[$itemid]->aggregationcoef <= 0) {
1031                         continue;
1032                     }
1034                     $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
1035                     if ($weight <= 0) {
1036                         $weights[$itemid] = 0;
1037                         continue;
1038                     }
1040                     $oldsum = $sum;
1041                     $weightedgrade = $weight * $grade_value;
1042                     $sum += $weightedgrade;
1044                     if ($weights !== null) {
1045                         if ($weightsum <= 0) {
1046                             $weights[$itemid] = 0;
1047                             continue;
1048                         }
1050                         $oldgrade = $oldsum / $weightsum;
1051                         $grade = $sum / $weightsum;
1052                         $normoldgrade = grade_grade::standardise_score($oldgrade, 0, 1, $grademin, $grademax);
1053                         $normgrade = grade_grade::standardise_score($grade, 0, 1, $grademin, $grademax);
1054                         $boundedoldgrade = $this->grade_item->bounded_grade($normoldgrade);
1055                         $boundedgrade = $this->grade_item->bounded_grade($normgrade);
1057                         if ($boundedgrade - $boundedoldgrade <= 0) {
1058                             // Nothing new was added to the grade.
1059                             $weights[$itemid] = 0;
1060                         } else if ($boundedgrade < $normgrade) {
1061                             // The grade has been bounded, the extra credit item needs to have a different weight.
1062                             $gradediff = $boundedgrade - $normoldgrade;
1063                             $gradediffnorm = grade_grade::standardise_score($gradediff, $grademin, $grademax, 0, 1);
1064                             $weights[$itemid] = $gradediffnorm / $grade_value;
1065                         } else {
1066                             // Default weighting.
1067                             $weights[$itemid] = $weight / $weightsum;
1068                         }
1069                     }
1070                 }
1072                 if ($weightsum == 0) {
1073                     $agg_grade = $sum; // only extra credits
1075                 } else {
1076                     $agg_grade = $sum / $weightsum;
1077                 }
1079                 // Record the weights as used.
1080                 if ($weights !== null) {
1081                     foreach ($grade_values as $itemid=>$grade_value) {
1082                         if ($items[$itemid]->aggregationcoef > 0) {
1083                             // Ignore extra credit items, the weights have already been computed.
1084                             continue;
1085                         }
1086                         if ($weightsum > 0) {
1087                             $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
1088                             $weights[$itemid] = $weight / $weightsum;
1089                         } else {
1090                             $weights[$itemid] = 0;
1091                         }
1092                     }
1093                 }
1094                 break;
1096             case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average
1097                 $this->load_grade_item();
1098                 $num = 0;
1099                 $sum = null;
1101                 foreach ($grade_values as $itemid=>$grade_value) {
1102                     if ($items[$itemid]->aggregationcoef == 0) {
1103                         $num += 1;
1104                         $sum += $grade_value;
1105                         if ($weights !== null) {
1106                             $weights[$itemid] = 1;
1107                         }
1108                     }
1109                 }
1111                 // Treating the extra credit items separately to get a chance to calculate their effective weights.
1112                 foreach ($grade_values as $itemid=>$grade_value) {
1113                     if ($items[$itemid]->aggregationcoef > 0) {
1114                         $oldsum = $sum;
1115                         $sum += $items[$itemid]->aggregationcoef * $grade_value;
1117                         if ($weights !== null) {
1118                             if ($num <= 0) {
1119                                 // The category only contains extra credit items, not setting the weight.
1120                                 continue;
1121                             }
1123                             $oldgrade = $oldsum / $num;
1124                             $grade = $sum / $num;
1125                             $normoldgrade = grade_grade::standardise_score($oldgrade, 0, 1, $grademin, $grademax);
1126                             $normgrade = grade_grade::standardise_score($grade, 0, 1, $grademin, $grademax);
1127                             $boundedoldgrade = $this->grade_item->bounded_grade($normoldgrade);
1128                             $boundedgrade = $this->grade_item->bounded_grade($normgrade);
1130                             if ($boundedgrade - $boundedoldgrade <= 0) {
1131                                 // Nothing new was added to the grade.
1132                                 $weights[$itemid] = 0;
1133                             } else if ($boundedgrade < $normgrade) {
1134                                 // The grade has been bounded, the extra credit item needs to have a different weight.
1135                                 $gradediff = $boundedgrade - $normoldgrade;
1136                                 $gradediffnorm = grade_grade::standardise_score($gradediff, $grademin, $grademax, 0, 1);
1137                                 $weights[$itemid] = $gradediffnorm / $grade_value;
1138                             } else {
1139                                 // Default weighting.
1140                                 $weights[$itemid] = 1.0 / $num;
1141                             }
1142                         }
1143                     }
1144                 }
1146                 if ($weights !== null && $num > 0) {
1147                     foreach ($grade_values as $itemid=>$grade_value) {
1148                         if ($items[$itemid]->aggregationcoef > 0) {
1149                             // Extra credit weights were already calculated.
1150                             continue;
1151                         }
1152                         if ($weights[$itemid]) {
1153                             $weights[$itemid] = 1.0 / $num;
1154                         }
1155                     }
1156                 }
1158                 if ($num == 0) {
1159                     $agg_grade = $sum; // only extra credits or wrong coefs
1161                 } else {
1162                     $agg_grade = $sum / $num;
1163                 }
1165                 break;
1167             case GRADE_AGGREGATE_SUM:    // Add up all the items.
1168                 $this->load_grade_item();
1169                 $num = count($grade_values);
1170                 $sum = 0;
1171                 $sumweights = 0;
1172                 $grademin = 0;
1173                 $grademax = 0;
1174                 $extracredititems = array();
1175                 foreach ($grade_values as $itemid => $gradevalue) {
1176                     // We need to check if the grademax/min was adjusted per user because of excluded items.
1177                     $usergrademin = $items[$itemid]->grademin;
1178                     $usergrademax = $items[$itemid]->grademax;
1179                     if (isset($grademinoverrides[$itemid])) {
1180                         $usergrademin = $grademinoverrides[$itemid];
1181                     }
1182                     if (isset($grademaxoverrides[$itemid])) {
1183                         $usergrademax = $grademaxoverrides[$itemid];
1184                     }
1186                     // Keep track of the extra credit items, we will need them later on.
1187                     if ($items[$itemid]->aggregationcoef > 0) {
1188                         $extracredititems[$itemid] = $items[$itemid];
1189                     }
1191                     // Ignore extra credit and items with a weight of 0.
1192                     if (!isset($extracredititems[$itemid]) && $items[$itemid]->aggregationcoef2 > 0) {
1193                         $grademin += $usergrademin;
1194                         $grademax += $usergrademax;
1195                         $sumweights += $items[$itemid]->aggregationcoef2;
1196                     }
1197                 }
1198                 $userweights = array();
1199                 $totaloverriddenweight = 0;
1200                 $totaloverriddengrademax = 0;
1201                 // We first need to rescale all manually assigned weights down by the
1202                 // percentage of weights missing from the category.
1203                 foreach ($grade_values as $itemid => $gradevalue) {
1204                     if ($items[$itemid]->weightoverride) {
1205                         if ($items[$itemid]->aggregationcoef2 <= 0) {
1206                             // Records the weight of 0 and continue.
1207                             $userweights[$itemid] = 0;
1208                             continue;
1209                         }
1210                         $userweights[$itemid] = $items[$itemid]->aggregationcoef2 / $sumweights;
1211                         $totaloverriddenweight += $userweights[$itemid];
1212                         $usergrademax = $items[$itemid]->grademax;
1213                         if (isset($grademaxoverrides[$itemid])) {
1214                             $usergrademax = $grademaxoverrides[$itemid];
1215                         }
1216                         $totaloverriddengrademax += $usergrademax;
1217                     }
1218                 }
1219                 $nonoverriddenpoints = $grademax - $totaloverriddengrademax;
1221                 // Then we need to recalculate the automatic weights.
1222                 foreach ($grade_values as $itemid => $gradevalue) {
1223                     if (!$items[$itemid]->weightoverride) {
1224                         $usergrademax = $items[$itemid]->grademax;
1225                         if (isset($grademaxoverrides[$itemid])) {
1226                             $usergrademax = $grademaxoverrides[$itemid];
1227                         }
1228                         if ($nonoverriddenpoints > 0) {
1229                             $userweights[$itemid] = ($usergrademax/$nonoverriddenpoints) * (1 - $totaloverriddenweight);
1230                         } else {
1231                             $userweights[$itemid] = 0;
1232                             if ($items[$itemid]->aggregationcoef2 > 0) {
1233                                 // Items with a weight of 0 should not count for the grade max,
1234                                 // though this only applies if the weight was changed to 0.
1235                                 $grademax -= $usergrademax;
1236                             }
1237                         }
1238                     }
1239                 }
1241                 // We can use our freshly corrected weights below.
1242                 foreach ($grade_values as $itemid => $gradevalue) {
1243                     if (isset($extracredititems[$itemid])) {
1244                         // We skip the extra credit items first.
1245                         continue;
1246                     }
1247                     $sum += $gradevalue * $userweights[$itemid] * $grademax;
1248                     if ($weights !== null) {
1249                         $weights[$itemid] = $userweights[$itemid];
1250                     }
1251                 }
1253                 // No we proceed with the extra credit items. They might have a different final
1254                 // weight in case the final grade was bounded. So we need to treat them different.
1255                 // Also, as we need to use the bounded_grade() method, we have to inject the
1256                 // right values there, and restore them afterwards.
1257                 $oldgrademax = $this->grade_item->grademax;
1258                 $oldgrademin = $this->grade_item->grademin;
1259                 foreach ($grade_values as $itemid => $gradevalue) {
1260                     if (!isset($extracredititems[$itemid])) {
1261                         continue;
1262                     }
1263                     $oldsum = $sum;
1264                     $weightedgrade = $gradevalue * $userweights[$itemid] * $grademax;
1265                     $sum += $weightedgrade;
1267                     // Only go through this when we need to record the weights.
1268                     if ($weights !== null) {
1269                         if ($grademax <= 0) {
1270                             // There are only extra credit items in this category,
1271                             // all the weights should be accurate (and be 0).
1272                             $weights[$itemid] = $userweights[$itemid];
1273                             continue;
1274                         }
1276                         $oldfinalgrade = $this->grade_item->bounded_grade($oldsum);
1277                         $newfinalgrade = $this->grade_item->bounded_grade($sum);
1278                         $finalgradediff = $newfinalgrade - $oldfinalgrade;
1279                         if ($finalgradediff <= 0) {
1280                             // This item did not contribute to the category total at all.
1281                             $weights[$itemid] = 0;
1282                         } else if ($finalgradediff < $weightedgrade) {
1283                             // The weight needs to be adjusted because only a portion of the
1284                             // extra credit item contributed to the category total.
1285                             $weights[$itemid] = $finalgradediff / ($gradevalue * $grademax);
1286                         } else {
1287                             // The weight was accurate.
1288                             $weights[$itemid] = $userweights[$itemid];
1289                         }
1290                     }
1291                 }
1292                 $this->grade_item->grademax = $oldgrademax;
1293                 $this->grade_item->grademin = $oldgrademin;
1295                 if ($grademax > 0) {
1296                     $agg_grade = $sum / $grademax; // Re-normalize score.
1297                 } else {
1298                     // Every item in the category is extra credit.
1299                     $agg_grade = $sum;
1300                     $grademax = $sum;
1301                 }
1303                 break;
1305             case GRADE_AGGREGATE_MEAN:    // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum)
1306             default:
1307                 $num = count($grade_values);
1308                 $sum = array_sum($grade_values);
1309                 $agg_grade = $sum / $num;
1310                 // Record the weights evenly.
1311                 if ($weights !== null && $num > 0) {
1312                     foreach ($grade_values as $itemid=>$grade_value) {
1313                         $weights[$itemid] = 1.0 / $num;
1314                     }
1315                 }
1316                 break;
1317         }
1319         return array('grade' => $agg_grade, 'grademin' => $grademin, 'grademax' => $grademax);
1320     }
1322     /**
1323      * Internal function that calculates the aggregated grade for this grade category
1324      *
1325      * Must be public as it is used by grade_grade::get_hiding_affected()
1326      *
1327      * @deprecated since Moodle 2.8
1328      * @param array $grade_values An array of values to be aggregated
1329      * @param array $items The array of grade_items
1330      * @return float The aggregate grade for this grade category
1331      */
1332     public function aggregate_values($grade_values, $items) {
1333         debugging('grade_category::aggregate_values() is deprecated.
1334                    Call grade_category::aggregate_values_and_adjust_bounds() instead.', DEBUG_DEVELOPER);
1335         $result = $this->aggregate_values_and_adjust_bounds($grade_values, $items);
1336         return $result['grade'];
1337     }
1339     /**
1340      * Some aggregation types may need to update their max grade.
1341      *
1342      * This must be executed after updating the weights as it relies on them.
1343      *
1344      * @return void
1345      */
1346     private function auto_update_max() {
1347         global $DB;
1348         if ($this->aggregation != GRADE_AGGREGATE_SUM) {
1349             // not needed at all
1350             return;
1351         }
1353         // Find grade items of immediate children (category or grade items) and force site settings.
1354         $this->load_grade_item();
1355         $depends_on = $this->grade_item->depends_on();
1357         $items = false;
1358         if (!empty($depends_on)) {
1359             list($usql, $params) = $DB->get_in_or_equal($depends_on);
1360             $sql = "SELECT *
1361                       FROM {grade_items}
1362                      WHERE id $usql";
1363             $items = $DB->get_records_sql($sql, $params);
1364         }
1366         if (!$items) {
1368             if ($this->grade_item->grademax != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
1369                 $this->grade_item->grademax  = 0;
1370                 $this->grade_item->grademin  = 0;
1371                 $this->grade_item->gradetype = GRADE_TYPE_VALUE;
1372                 $this->grade_item->update('aggregation');
1373             }
1374             return;
1375         }
1377         //find max grade possible
1378         $maxes = array();
1380         foreach ($items as $item) {
1382             if ($item->aggregationcoef > 0) {
1383                 // extra credit from this activity - does not affect total
1384                 continue;
1385             } else if ($item->aggregationcoef2 <= 0) {
1386                 // Items with a weight of 0 do not affect the total.
1387                 continue;
1388             }
1390             if ($item->gradetype == GRADE_TYPE_VALUE) {
1391                 $maxes[$item->id] = $item->grademax;
1393             } else if ($item->gradetype == GRADE_TYPE_SCALE) {
1394                 $maxes[$item->id] = $item->grademax; // 0 = nograde, 1 = first scale item, 2 = second scale item
1395             }
1396         }
1398         if ($this->can_apply_limit_rules()) {
1399             // Apply droplow and keephigh.
1400             $this->apply_limit_rules($maxes, $items);
1401         }
1402         $max = array_sum($maxes);
1404         // update db if anything changed
1405         if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
1406             $this->grade_item->grademax  = $max;
1407             $this->grade_item->grademin  = 0;
1408             $this->grade_item->gradetype = GRADE_TYPE_VALUE;
1409             $this->grade_item->update('aggregation');
1410         }
1411     }
1413     /**
1414      * Recalculate the weights of the grade items in this category.
1415      *
1416      * The category total is not updated here, a further call to
1417      * {@link self::auto_update_max()} is required.
1418      *
1419      * @return void
1420      */
1421     private function auto_update_weights() {
1422         if ($this->aggregation != GRADE_AGGREGATE_SUM) {
1423             // This is only required if we are using natural weights.
1424             return;
1425         }
1426         $children = $this->get_children();
1428         $gradeitem = null;
1430         // Calculate the sum of the grademax's of all the items within this category.
1431         $totalnonoverriddengrademax = 0;
1432         $totalgrademax = 0;
1434         // Out of 1, how much weight has been manually overriden by a user?
1435         $totaloverriddenweight  = 0;
1436         $totaloverriddengrademax  = 0;
1438         // Has every assessment in this category been overridden?
1439         $automaticgradeitemspresent = false;
1440         // Does the grade item require normalising?
1441         $requiresnormalising = false;
1443         // This array keeps track of the id and weight of every grade item that has been overridden.
1444         $overridearray = array();
1445         foreach ($children as $sortorder => $child) {
1446             $gradeitem = null;
1448             if ($child['type'] == 'item') {
1449                 $gradeitem = $child['object'];
1450             } else if ($child['type'] == 'category') {
1451                 $gradeitem = $child['object']->load_grade_item();
1452             }
1454             if ($gradeitem->gradetype == GRADE_TYPE_NONE || $gradeitem->gradetype == GRADE_TYPE_TEXT) {
1455                 // Text items and none items do not have a weight.
1456                 continue;
1457             } else if (!$this->aggregateoutcomes && $gradeitem->is_outcome_item()) {
1458                 // We will not aggregate outcome items, so we can ignore them.
1459                 continue;
1460             }
1462             // Record the ID and the weight for this grade item.
1463             $overridearray[$gradeitem->id] = array();
1464             $overridearray[$gradeitem->id]['extracredit'] = intval($gradeitem->aggregationcoef);
1465             $overridearray[$gradeitem->id]['weight'] = $gradeitem->aggregationcoef2;
1466             $overridearray[$gradeitem->id]['weightoverride'] = intval($gradeitem->weightoverride);
1467             // If this item has had its weight overridden then set the flag to true, but
1468             // only if all previous items were also overridden. Note that extra credit items
1469             // are counted as overridden grade items.
1470             if (!$gradeitem->weightoverride && $gradeitem->aggregationcoef == 0) {
1471                 $automaticgradeitemspresent = true;
1472             }
1474             if ($gradeitem->aggregationcoef > 0) {
1475                 // An extra credit grade item doesn't contribute to $totaloverriddengrademax.
1476                 continue;
1477             } else if ($gradeitem->weightoverride > 0 && $gradeitem->aggregationcoef2 <= 0) {
1478                 // An overriden item that defines a weight of 0 does not contribute to $totaloverriddengrademax.
1479                 continue;
1480             }
1482             $totalgrademax += $gradeitem->grademax;
1483             if ($gradeitem->weightoverride > 0) {
1484                 $totaloverriddenweight += $gradeitem->aggregationcoef2;
1485                 $totaloverriddengrademax += $gradeitem->grademax;
1486             }
1487         }
1489         // Initialise this variable (used to keep track of the weight override total).
1490         $normalisetotal = 0;
1491         // 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
1492         // other weights to zero and normalise the others.
1493         $overriddentotal = 0;
1494         // If the overridden weight total is higher than 1 then set the other untouched weights to zero.
1495         $setotherweightstozero = false;
1496         // Total up all of the weights.
1497         foreach ($overridearray as $gradeitemdetail) {
1498             // If the grade item has extra credit, then don't add it to the normalisetotal.
1499             if (!$gradeitemdetail['extracredit']) {
1500                 $normalisetotal += $gradeitemdetail['weight'];
1501             }
1502             if ($gradeitemdetail['weightoverride'] && !$gradeitemdetail['extracredit']) {
1503                 // Add overriden weights up to see if they are greater than 1.
1504                 $overriddentotal += $gradeitemdetail['weight'];
1505             }
1506         }
1507         if ($overriddentotal > 1) {
1508             // Make sure that this catergory of weights gets normalised.
1509             $requiresnormalising = true;
1510             // The normalised weights are only the overridden weights, so we just use the total of those.
1511             $normalisetotal = $overriddentotal;
1512         }
1514         $totalnonoverriddengrademax = $totalgrademax - $totaloverriddengrademax;
1516         reset($children);
1517         foreach ($children as $sortorder => $child) {
1518             $gradeitem = null;
1520             if ($child['type'] == 'item') {
1521                 $gradeitem = $child['object'];
1522             } else if ($child['type'] == 'category') {
1523                 $gradeitem = $child['object']->load_grade_item();
1524             }
1526             if ($gradeitem->gradetype == GRADE_TYPE_NONE || $gradeitem->gradetype == GRADE_TYPE_TEXT) {
1527                 // Text items and none items do not have a weight, no need to set their weight to
1528                 // zero as they must never be used during aggregation.
1529                 continue;
1530             } else if (!$this->aggregateoutcomes && $gradeitem->is_outcome_item()) {
1531                 // We will not aggregate outcome items, so we can ignore updating their weights.
1532                 continue;
1533             }
1535             if (!$gradeitem->weightoverride) {
1536                 // Calculations with a grade maximum of zero will cause problems. Just set the weight to zero.
1537                 if ($totaloverriddenweight >= 1 || $totalnonoverriddengrademax == 0 || $gradeitem->grademax == 0) {
1538                     // There is no more weight to distribute.
1539                     $gradeitem->aggregationcoef2 = 0;
1540                 } else {
1541                     // Calculate this item's weight as a percentage of the non-overridden total grade maxes
1542                     // then convert it to a proportion of the available non-overriden weight.
1543                     $gradeitem->aggregationcoef2 = ($gradeitem->grademax/$totalnonoverriddengrademax) *
1544                             (1 - $totaloverriddenweight);
1545                 }
1546                 $gradeitem->update();
1547             } else if ((!$automaticgradeitemspresent && $normalisetotal != 1) || ($requiresnormalising)
1548                     || $overridearray[$gradeitem->id]['weight'] < 0) {
1549                 // Just divide the overriden weight for this item against the total weight override of all
1550                 // items in this category.
1551                 if ($normalisetotal == 0 || $overridearray[$gradeitem->id]['weight'] < 0) {
1552                     // If the normalised total equals zero, or the weight value is less than zero,
1553                     // set the weight for the grade item to zero.
1554                     $gradeitem->aggregationcoef2 = 0;
1555                 } else {
1556                     $gradeitem->aggregationcoef2 = $overridearray[$gradeitem->id]['weight'] / $normalisetotal;
1557                 }
1558                 // Update the grade item to reflect these changes.
1559                 $gradeitem->update();
1560             }
1561         }
1562     }
1564     /**
1565      * Given an array of grade values (numerical indices) applies droplow or keephigh rules to limit the final array.
1566      *
1567      * @param array $grade_values itemid=>$grade_value float
1568      * @param array $items grade item objects
1569      * @return array Limited grades.
1570      */
1571     public function apply_limit_rules(&$grade_values, $items) {
1572         $extraused = $this->is_extracredit_used();
1574         if (!empty($this->droplow)) {
1575             asort($grade_values, SORT_NUMERIC);
1576             $dropped = 0;
1578             // If we have fewer grade items available to drop than $this->droplow, use this flag to escape the loop
1579             // May occur because of "extra credit" or if droplow is higher than the number of grade items
1580             $droppedsomething = true;
1582             while ($dropped < $this->droplow && $droppedsomething) {
1583                 $droppedsomething = false;
1585                 $grade_keys = array_keys($grade_values);
1586                 $gradekeycount = count($grade_keys);
1588                 if ($gradekeycount === 0) {
1589                     //We've dropped all grade items
1590                     break;
1591                 }
1593                 $originalindex = $founditemid = $foundmax = null;
1595                 // Find the first remaining grade item that is available to be dropped
1596                 foreach ($grade_keys as $gradekeyindex=>$gradekey) {
1597                     if (!$extraused || $items[$gradekey]->aggregationcoef <= 0) {
1598                         // Found a non-extra credit grade item that is eligible to be dropped
1599                         $originalindex = $gradekeyindex;
1600                         $founditemid = $grade_keys[$originalindex];
1601                         $foundmax = $items[$founditemid]->grademax;
1602                         break;
1603                     }
1604                 }
1606                 if (empty($founditemid)) {
1607                     // No grade items available to drop
1608                     break;
1609                 }
1611                 // Now iterate over the remaining grade items
1612                 // We're looking for other grade items with the same grade value but a higher grademax
1613                 $i = 1;
1614                 while ($originalindex + $i < $gradekeycount) {
1616                     $possibleitemid = $grade_keys[$originalindex+$i];
1617                     $i++;
1619                     if ($grade_values[$founditemid] != $grade_values[$possibleitemid]) {
1620                         // The next grade item has a different grade value. Stop looking.
1621                         break;
1622                     }
1624                     if ($extraused && $items[$possibleitemid]->aggregationcoef > 0) {
1625                         // Don't drop extra credit grade items. Continue the search.
1626                         continue;
1627                     }
1629                     if ($foundmax < $items[$possibleitemid]->grademax) {
1630                         // Found a grade item with the same grade value and a higher grademax
1631                         $foundmax = $items[$possibleitemid]->grademax;
1632                         $founditemid = $possibleitemid;
1633                         // Continue searching to see if there is an even higher grademax
1634                     }
1635                 }
1637                 // Now drop whatever grade item we have found
1638                 unset($grade_values[$founditemid]);
1639                 $dropped++;
1640                 $droppedsomething = true;
1641             }
1643         } else if (!empty($this->keephigh)) {
1644             arsort($grade_values, SORT_NUMERIC);
1645             $kept = 0;
1647             foreach ($grade_values as $itemid=>$value) {
1649                 if ($extraused and $items[$itemid]->aggregationcoef > 0) {
1650                     // we keep all extra credits
1652                 } else if ($kept < $this->keephigh) {
1653                     $kept++;
1655                 } else {
1656                     unset($grade_values[$itemid]);
1657                 }
1658             }
1659         }
1660     }
1662     /**
1663      * Returns whether or not we can apply the limit rules.
1664      *
1665      * There are cases where drop lowest or keep highest should not be used
1666      * at all. This method will determine whether or not this logic can be
1667      * applied considering the current setup of the category.
1668      *
1669      * @return bool
1670      */
1671     public function can_apply_limit_rules() {
1672         if ($this->canapplylimitrules !== null) {
1673             return $this->canapplylimitrules;
1674         }
1676         // Set it to be supported by default.
1677         $this->canapplylimitrules = true;
1679         // Natural aggregation.
1680         if ($this->aggregation == GRADE_AGGREGATE_SUM) {
1681             $canapply = true;
1683             // Check until one child breaks the rules.
1684             $gradeitems = $this->get_children();
1685             $validitems = 0;
1686             $lastweight = null;
1687             $lastmaxgrade = null;
1688             foreach ($gradeitems as $gradeitem) {
1689                 $gi = $gradeitem['object'];
1691                 if ($gradeitem['type'] == 'category') {
1692                     // Sub categories are not allowed because they can have dynamic weights/maxgrades.
1693                     $canapply = false;
1694                     break;
1695                 }
1697                 if ($gi->aggregationcoef > 0) {
1698                     // Extra credit items are not allowed.
1699                     $canapply = false;
1700                     break;
1701                 }
1703                 if ($lastweight !== null && $lastweight != $gi->aggregationcoef2) {
1704                     // One of the weight differs from another item.
1705                     $canapply = false;
1706                     break;
1707                 }
1709                 if ($lastmaxgrade !== null && $lastmaxgrade != $gi->grademax) {
1710                     // One of the max grade differ from another item. This is not allowed for now
1711                     // because we could be end up with different max grade between users for this category.
1712                     $canapply = false;
1713                     break;
1714                 }
1716                 $lastweight = $gi->aggregationcoef2;
1717                 $lastmaxgrade = $gi->grademax;
1718             }
1720             $this->canapplylimitrules = $canapply;
1721         }
1723         return $this->canapplylimitrules;
1724     }
1726     /**
1727      * Returns true if category uses extra credit of any kind
1728      *
1729      * @return bool True if extra credit used
1730      */
1731     public function is_extracredit_used() {
1732         return self::aggregation_uses_extracredit($this->aggregation);
1733     }
1735     /**
1736      * Returns true if aggregation passed is using extracredit.
1737      *
1738      * @param int $aggregation Aggregation const.
1739      * @return bool True if extra credit used
1740      */
1741     public static function aggregation_uses_extracredit($aggregation) {
1742         return ($aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2
1743              or $aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
1744              or $aggregation == GRADE_AGGREGATE_SUM);
1745     }
1747     /**
1748      * Returns true if category uses special aggregation coefficient
1749      *
1750      * @return bool True if an aggregation coefficient is being used
1751      */
1752     public function is_aggregationcoef_used() {
1753         return self::aggregation_uses_aggregationcoef($this->aggregation);
1755     }
1757     /**
1758      * Returns true if aggregation uses aggregationcoef
1759      *
1760      * @param int $aggregation Aggregation const.
1761      * @return bool True if an aggregation coefficient is being used
1762      */
1763     public static function aggregation_uses_aggregationcoef($aggregation) {
1764         return ($aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN
1765              or $aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2
1766              or $aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
1767              or $aggregation == GRADE_AGGREGATE_SUM);
1769     }
1771     /**
1772      * Recursive function to find which weight/extra credit field to use in the grade item form.
1773      *
1774      * @param string $first Whether or not this is the first item in the recursion
1775      * @return string
1776      */
1777     public function get_coefstring($first=true) {
1778         if (!is_null($this->coefstring)) {
1779             return $this->coefstring;
1780         }
1782         $overriding_coefstring = null;
1784         // Stop recursing upwards if this category has no parent
1785         if (!$first) {
1787             if ($parent_category = $this->load_parent_category()) {
1788                 return $parent_category->get_coefstring(false);
1790             } else {
1791                 return null;
1792             }
1794         } else if ($first) {
1796             if ($parent_category = $this->load_parent_category()) {
1797                 $overriding_coefstring = $parent_category->get_coefstring(false);
1798             }
1799         }
1801         // If an overriding coefstring has trickled down from one of the parent categories, return it. Otherwise, return self.
1802         if (!is_null($overriding_coefstring)) {
1803             return $overriding_coefstring;
1804         }
1806         // No parent category is overriding this category's aggregation, return its string
1807         if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
1808             $this->coefstring = 'aggregationcoefweight';
1810         } else if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
1811             $this->coefstring = 'aggregationcoefextrasum';
1813         } else if ($this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
1814             $this->coefstring = 'aggregationcoefextraweight';
1816         } else if ($this->aggregation == GRADE_AGGREGATE_SUM) {
1817             $this->coefstring = 'aggregationcoefextraweightsum';
1819         } else {
1820             $this->coefstring = 'aggregationcoef';
1821         }
1822         return $this->coefstring;
1823     }
1825     /**
1826      * Returns tree with all grade_items and categories as elements
1827      *
1828      * @param int $courseid The course ID
1829      * @param bool $include_category_items as category children
1830      * @return array
1831      */
1832     public static function fetch_course_tree($courseid, $include_category_items=false) {
1833         $course_category = grade_category::fetch_course_category($courseid);
1834         $category_array = array('object'=>$course_category, 'type'=>'category', 'depth'=>1,
1835                                 'children'=>$course_category->get_children($include_category_items));
1837         $course_category->sortorder = $course_category->get_sortorder();
1838         $sortorder = $course_category->get_sortorder();
1839         return grade_category::_fetch_course_tree_recursion($category_array, $sortorder);
1840     }
1842     /**
1843      * An internal function that recursively sorts grade categories within a course
1844      *
1845      * @param array $category_array The seed of the recursion
1846      * @param int   $sortorder The current sortorder
1847      * @return array An array containing 'object', 'type', 'depth' and optionally 'children'
1848      */
1849     static private function _fetch_course_tree_recursion($category_array, &$sortorder) {
1850         // update the sortorder in db if needed
1851         //NOTE: This leads to us resetting sort orders every time the categories and items page is viewed :(
1852         //if ($category_array['object']->sortorder != $sortorder) {
1853             //$category_array['object']->set_sortorder($sortorder);
1854         //}
1856         if (isset($category_array['object']->gradetype) && $category_array['object']->gradetype==GRADE_TYPE_NONE) {
1857             return null;
1858         }
1860         // store the grade_item or grade_category instance with extra info
1861         $result = array('object'=>$category_array['object'], 'type'=>$category_array['type'], 'depth'=>$category_array['depth']);
1863         // reuse final grades if there
1864         if (array_key_exists('finalgrades', $category_array)) {
1865             $result['finalgrades'] = $category_array['finalgrades'];
1866         }
1868         // recursively resort children
1869         if (!empty($category_array['children'])) {
1870             $result['children'] = array();
1871             //process the category item first
1872             $child = null;
1874             foreach ($category_array['children'] as $oldorder=>$child_array) {
1876                 if ($child_array['type'] == 'courseitem' or $child_array['type'] == 'categoryitem') {
1877                     $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
1878                     if (!empty($child)) {
1879                         $result['children'][$sortorder] = $child;
1880                     }
1881                 }
1882             }
1884             foreach ($category_array['children'] as $oldorder=>$child_array) {
1886                 if ($child_array['type'] != 'courseitem' and $child_array['type'] != 'categoryitem') {
1887                     $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
1888                     if (!empty($child)) {
1889                         $result['children'][++$sortorder] = $child;
1890                     }
1891                 }
1892             }
1893         }
1895         return $result;
1896     }
1898     /**
1899      * Fetches and returns all the children categories and/or grade_items belonging to this category.
1900      * By default only returns the immediate children (depth=1), but deeper levels can be requested,
1901      * as well as all levels (0). The elements are indexed by sort order.
1902      *
1903      * @param bool $include_category_items Whether or not to include category grade_items in the children array
1904      * @return array Array of child objects (grade_category and grade_item).
1905      */
1906     public function get_children($include_category_items=false) {
1907         global $DB;
1909         // This function must be as fast as possible ;-)
1910         // fetch all course grade items and categories into memory - we do not expect hundreds of these in course
1911         // we have to limit the number of queries though, because it will be used often in grade reports
1913         $cats  = $DB->get_records('grade_categories', array('courseid' => $this->courseid));
1914         $items = $DB->get_records('grade_items', array('courseid' => $this->courseid));
1916         // init children array first
1917         foreach ($cats as $catid=>$cat) {
1918             $cats[$catid]->children = array();
1919         }
1921         //first attach items to cats and add category sortorder
1922         foreach ($items as $item) {
1924             if ($item->itemtype == 'course' or $item->itemtype == 'category') {
1925                 $cats[$item->iteminstance]->sortorder = $item->sortorder;
1927                 if (!$include_category_items) {
1928                     continue;
1929                 }
1930                 $categoryid = $item->iteminstance;
1932             } else {
1933                 $categoryid = $item->categoryid;
1934                 if (empty($categoryid)) {
1935                     debugging('Found a grade item that isnt in a category');
1936                 }
1937             }
1939             // prevent problems with duplicate sortorders in db
1940             $sortorder = $item->sortorder;
1942             while (array_key_exists($categoryid, $cats)
1943                 && array_key_exists($sortorder, $cats[$categoryid]->children)) {
1945                 $sortorder++;
1946             }
1948             $cats[$categoryid]->children[$sortorder] = $item;
1950         }
1952         // now find the requested category and connect categories as children
1953         $category = false;
1955         foreach ($cats as $catid=>$cat) {
1957             if (empty($cat->parent)) {
1959                 if ($cat->path !== '/'.$cat->id.'/') {
1960                     $grade_category = new grade_category($cat, false);
1961                     $grade_category->path  = '/'.$cat->id.'/';
1962                     $grade_category->depth = 1;
1963                     $grade_category->update('system');
1964                     return $this->get_children($include_category_items);
1965                 }
1967             } else {
1969                 if (empty($cat->path) or !preg_match('|/'.$cat->parent.'/'.$cat->id.'/$|', $cat->path)) {
1970                     //fix paths and depts
1971                     static $recursioncounter = 0; // prevents infinite recursion
1972                     $recursioncounter++;
1974                     if ($recursioncounter < 5) {
1975                         // fix paths and depths!
1976                         $grade_category = new grade_category($cat, false);
1977                         $grade_category->depth = 0;
1978                         $grade_category->path  = null;
1979                         $grade_category->update('system');
1980                         return $this->get_children($include_category_items);
1981                     }
1982                 }
1983                 // prevent problems with duplicate sortorders in db
1984                 $sortorder = $cat->sortorder;
1986                 while (array_key_exists($sortorder, $cats[$cat->parent]->children)) {
1987                     //debugging("$sortorder exists in cat loop");
1988                     $sortorder++;
1989                 }
1991                 $cats[$cat->parent]->children[$sortorder] = &$cats[$catid];
1992             }
1994             if ($catid == $this->id) {
1995                 $category = &$cats[$catid];
1996             }
1997         }
1999         unset($items); // not needed
2000         unset($cats); // not needed
2002         $children_array = grade_category::_get_children_recursion($category);
2004         ksort($children_array);
2006         return $children_array;
2008     }
2010     /**
2011      * Private method used to retrieve all children of this category recursively
2012      *
2013      * @param grade_category $category Source of current recursion
2014      * @return array An array of child grade categories
2015      */
2016     private static function _get_children_recursion($category) {
2018         $children_array = array();
2019         foreach ($category->children as $sortorder=>$child) {
2021             if (array_key_exists('itemtype', $child)) {
2022                 $grade_item = new grade_item($child, false);
2024                 if (in_array($grade_item->itemtype, array('course', 'category'))) {
2025                     $type  = $grade_item->itemtype.'item';
2026                     $depth = $category->depth;
2028                 } else {
2029                     $type  = 'item';
2030                     $depth = $category->depth; // we use this to set the same colour
2031                 }
2032                 $children_array[$sortorder] = array('object'=>$grade_item, 'type'=>$type, 'depth'=>$depth);
2034             } else {
2035                 $children = grade_category::_get_children_recursion($child);
2036                 $grade_category = new grade_category($child, false);
2038                 if (empty($children)) {
2039                     $children = array();
2040                 }
2041                 $children_array[$sortorder] = array('object'=>$grade_category, 'type'=>'category', 'depth'=>$grade_category->depth, 'children'=>$children);
2042             }
2043         }
2045         // sort the array
2046         ksort($children_array);
2048         return $children_array;
2049     }
2051     /**
2052      * Uses {@link get_grade_item()} to load or create a grade_item, then saves it as $this->grade_item.
2053      *
2054      * @return grade_item
2055      */
2056     public function load_grade_item() {
2057         if (empty($this->grade_item)) {
2058             $this->grade_item = $this->get_grade_item();
2059         }
2060         return $this->grade_item;
2061     }
2063     /**
2064      * Retrieves this grade categories' associated grade_item from the database
2065      *
2066      * If no grade_item exists yet, creates one.
2067      *
2068      * @return grade_item
2069      */
2070     public function get_grade_item() {
2071         if (empty($this->id)) {
2072             debugging("Attempt to obtain a grade_category's associated grade_item without the category's ID being set.");
2073             return false;
2074         }
2076         if (empty($this->parent)) {
2077             $params = array('courseid'=>$this->courseid, 'itemtype'=>'course', 'iteminstance'=>$this->id);
2079         } else {
2080             $params = array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id);
2081         }
2083         if (!$grade_items = grade_item::fetch_all($params)) {
2084             // create a new one
2085             $grade_item = new grade_item($params, false);
2086             $grade_item->gradetype = GRADE_TYPE_VALUE;
2087             $grade_item->insert('system');
2089         } else if (count($grade_items) == 1) {
2090             // found existing one
2091             $grade_item = reset($grade_items);
2093         } else {
2094             debugging("Found more than one grade_item attached to category id:".$this->id);
2095             // return first one
2096             $grade_item = reset($grade_items);
2097         }
2099         return $grade_item;
2100     }
2102     /**
2103      * Uses $this->parent to instantiate $this->parent_category based on the referenced record in the DB
2104      *
2105      * @return grade_category The parent category
2106      */
2107     public function load_parent_category() {
2108         if (empty($this->parent_category) && !empty($this->parent)) {
2109             $this->parent_category = $this->get_parent_category();
2110         }
2111         return $this->parent_category;
2112     }
2114     /**
2115      * Uses $this->parent to instantiate and return a grade_category object
2116      *
2117      * @return grade_category Returns the parent category or null if this category has no parent
2118      */
2119     public function get_parent_category() {
2120         if (!empty($this->parent)) {
2121             $parent_category = new grade_category(array('id' => $this->parent));
2122             return $parent_category;
2123         } else {
2124             return null;
2125         }
2126     }
2128     /**
2129      * Returns the most descriptive field for this grade category
2130      *
2131      * @return string name
2132      */
2133     public function get_name() {
2134         global $DB;
2135         // For a course category, we return the course name if the fullname is set to '?' in the DB (empty in the category edit form)
2136         if (empty($this->parent) && $this->fullname == '?') {
2137             $course = $DB->get_record('course', array('id'=> $this->courseid));
2138             return format_string($course->fullname);
2140         } else {
2141             return $this->fullname;
2142         }
2143     }
2145     /**
2146      * Describe the aggregation settings for this category so the reports make more sense.
2147      *
2148      * @return string description
2149      */
2150     public function get_description() {
2151         $allhelp = array();
2152         if ($this->aggregation != GRADE_AGGREGATE_SUM) {
2153             $aggrstrings = grade_helper::get_aggregation_strings();
2154             $allhelp[] = $aggrstrings[$this->aggregation];
2155         }
2157         if ($this->droplow && $this->can_apply_limit_rules()) {
2158             $allhelp[] = get_string('droplowestvalues', 'grades', $this->droplow);
2159         }
2160         if ($this->keephigh && $this->can_apply_limit_rules()) {
2161             $allhelp[] = get_string('keephighestvalues', 'grades', $this->keephigh);
2162         }
2163         if (!$this->aggregateonlygraded) {
2164             $allhelp[] = get_string('aggregatenotonlygraded', 'grades');
2165         }
2166         if ($allhelp) {
2167             return implode('. ', $allhelp) . '.';
2168         }
2169         return '';
2170     }
2172     /**
2173      * Sets this category's parent id
2174      *
2175      * @param int $parentid The ID of the category that is the new parent to $this
2176      * @param string $source From where was the object updated (mod/forum, manual, etc.)
2177      * @return bool success
2178      */
2179     public function set_parent($parentid, $source=null) {
2180         if ($this->parent == $parentid) {
2181             return true;
2182         }
2184         if ($parentid == $this->id) {
2185             print_error('cannotassignselfasparent');
2186         }
2188         if (empty($this->parent) and $this->is_course_category()) {
2189             print_error('cannothaveparentcate');
2190         }
2192         // find parent and check course id
2193         if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
2194             return false;
2195         }
2197         $this->force_regrading();
2199         // set new parent category
2200         $this->parent          = $parent_category->id;
2201         $this->parent_category =& $parent_category;
2202         $this->path            = null;       // remove old path and depth - will be recalculated in update()
2203         $this->depth           = 0;          // remove old path and depth - will be recalculated in update()
2204         $this->update($source);
2206         return $this->update($source);
2207     }
2209     /**
2210      * Returns the final grade values for this grade category.
2211      *
2212      * @param int $userid Optional user ID to retrieve a single user's final grade
2213      * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
2214      */
2215     public function get_final($userid=null) {
2216         $this->load_grade_item();
2217         return $this->grade_item->get_final($userid);
2218     }
2220     /**
2221      * Returns the sortorder of the grade categories' associated grade_item
2222      *
2223      * This method is also available in grade_item for cases where the object type is not known.
2224      *
2225      * @return int Sort order
2226      */
2227     public function get_sortorder() {
2228         $this->load_grade_item();
2229         return $this->grade_item->get_sortorder();
2230     }
2232     /**
2233      * Returns the idnumber of the grade categories' associated grade_item.
2234      *
2235      * This method is also available in grade_item for cases where the object type is not known.
2236      *
2237      * @return string idnumber
2238      */
2239     public function get_idnumber() {
2240         $this->load_grade_item();
2241         return $this->grade_item->get_idnumber();
2242     }
2244     /**
2245      * Sets the sortorder variable for this category.
2246      *
2247      * This method is also available in grade_item, for cases where the object type is not know.
2248      *
2249      * @param int $sortorder The sortorder to assign to this category
2250      */
2251     public function set_sortorder($sortorder) {
2252         $this->load_grade_item();
2253         $this->grade_item->set_sortorder($sortorder);
2254     }
2256     /**
2257      * Move this category after the given sortorder
2258      *
2259      * Does not change the parent
2260      *
2261      * @param int $sortorder to place after.
2262      * @return void
2263      */
2264     public function move_after_sortorder($sortorder) {
2265         $this->load_grade_item();
2266         $this->grade_item->move_after_sortorder($sortorder);
2267     }
2269     /**
2270      * Return true if this is the top most category that represents the total course grade.
2271      *
2272      * @return bool
2273      */
2274     public function is_course_category() {
2275         $this->load_grade_item();
2276         return $this->grade_item->is_course_item();
2277     }
2279     /**
2280      * Return the course level grade_category object
2281      *
2282      * @param int $courseid The Course ID
2283      * @return grade_category Returns the course level grade_category instance
2284      */
2285     public static function fetch_course_category($courseid) {
2286         if (empty($courseid)) {
2287             debugging('Missing course id!');
2288             return false;
2289         }
2291         // course category has no parent
2292         if ($course_category = grade_category::fetch(array('courseid'=>$courseid, 'parent'=>null))) {
2293             return $course_category;
2294         }
2296         // create a new one
2297         $course_category = new grade_category();
2298         $course_category->insert_course_category($courseid);
2300         return $course_category;
2301     }
2303     /**
2304      * Is grading object editable?
2305      *
2306      * @return bool
2307      */
2308     public function is_editable() {
2309         return true;
2310     }
2312     /**
2313      * Returns the locked state/date of the grade categories' associated grade_item.
2314      *
2315      * This method is also available in grade_item, for cases where the object type is not known.
2316      *
2317      * @return bool
2318      */
2319     public function is_locked() {
2320         $this->load_grade_item();
2321         return $this->grade_item->is_locked();
2322     }
2324     /**
2325      * Sets the grade_item's locked variable and updates the grade_item.
2326      *
2327      * Calls set_locked() on the categories' grade_item
2328      *
2329      * @param int  $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
2330      * @param bool $cascade lock/unlock child objects too
2331      * @param bool $refresh refresh grades when unlocking
2332      * @return bool success if category locked (not all children mayb be locked though)
2333      */
2334     public function set_locked($lockedstate, $cascade=false, $refresh=true) {
2335         $this->load_grade_item();
2337         $result = $this->grade_item->set_locked($lockedstate, $cascade, true);
2339         if ($cascade) {
2340             //process all children - items and categories
2341             if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
2343                 foreach ($children as $child) {
2344                     $child->set_locked($lockedstate, true, false);
2346                     if (empty($lockedstate) and $refresh) {
2347                         //refresh when unlocking
2348                         $child->refresh_grades();
2349                     }
2350                 }
2351             }
2353             if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
2355                 foreach ($children as $child) {
2356                     $child->set_locked($lockedstate, true, true);
2357                 }
2358             }
2359         }
2361         return $result;
2362     }
2364     /**
2365      * Overrides grade_object::set_properties() to add special handling for changes to category aggregation types
2366      *
2367      * @param stdClass $instance the object to set the properties on
2368      * @param array|stdClass $params Either an associative array or an object containing property name, property value pairs
2369      */
2370     public static function set_properties(&$instance, $params) {
2371         global $DB;
2373         parent::set_properties($instance, $params);
2375         //if they've changed aggregation type we made need to do some fiddling to provide appropriate defaults
2376         if (!empty($params->aggregation)) {
2378             //weight and extra credit share a column :( Would like a default of 1 for weight and 0 for extra credit
2379             //Flip from the default of 0 to 1 (or vice versa) if ALL items in the category are still set to the old default.
2380             if (self::aggregation_uses_aggregationcoef($params->aggregation)) {
2381                 $sql = $defaultaggregationcoef = null;
2383                 if (!self::aggregation_uses_extracredit($params->aggregation)) {
2384                     //if all items in this category have aggregation coefficient of 0 we can change it to 1 ie evenly weighted
2385                     $sql = "select count(id) from {grade_items} where categoryid=:categoryid and aggregationcoef!=0";
2386                     $defaultaggregationcoef = 1;
2387                 } else {
2388                     //if all items in this category have aggregation coefficient of 1 we can change it to 0 ie no extra credit
2389                     $sql = "select count(id) from {grade_items} where categoryid=:categoryid and aggregationcoef!=1";
2390                     $defaultaggregationcoef = 0;
2391                 }
2393                 $params = array('categoryid'=>$instance->id);
2394                 $count = $DB->count_records_sql($sql, $params);
2395                 if ($count===0) { //category is either empty or all items are set to a default value so we can switch defaults
2396                     $params['aggregationcoef'] = $defaultaggregationcoef;
2397                     $DB->execute("update {grade_items} set aggregationcoef=:aggregationcoef where categoryid=:categoryid",$params);
2398                 }
2399             }
2400         }
2401     }
2403     /**
2404      * Sets the grade_item's hidden variable and updates the grade_item.
2405      *
2406      * Overrides grade_item::set_hidden() to add cascading of the hidden value to grade items in this grade category
2407      *
2408      * @param int $hidden 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
2409      * @param bool $cascade apply to child objects too
2410      */
2411     public function set_hidden($hidden, $cascade=false) {
2412         $this->load_grade_item();
2413         //this hides the associated grade item (the course total)
2414         $this->grade_item->set_hidden($hidden, $cascade);
2415         //this hides the category itself and everything it contains
2416         parent::set_hidden($hidden, $cascade);
2418         if ($cascade) {
2420             if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
2422                 foreach ($children as $child) {
2423                     if ($child->can_control_visibility()) {
2424                         $child->set_hidden($hidden, $cascade);
2425                     }
2426                 }
2427             }
2429             if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
2431                 foreach ($children as $child) {
2432                     $child->set_hidden($hidden, $cascade);
2433                 }
2434             }
2435         }
2437         //if marking category visible make sure parent category is visible MDL-21367
2438         if( !$hidden ) {
2439             $category_array = grade_category::fetch_all(array('id'=>$this->parent));
2440             if ($category_array && array_key_exists($this->parent, $category_array)) {
2441                 $category = $category_array[$this->parent];
2442                 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
2443                 //if($category->is_hidden()) {
2444                     $category->set_hidden($hidden, false);
2445                 //}
2446             }
2447         }
2448     }
2450     /**
2451      * Applies default settings on this category
2452      *
2453      * @return bool True if anything changed
2454      */
2455     public function apply_default_settings() {
2456         global $CFG;
2458         foreach ($this->forceable as $property) {
2460             if (isset($CFG->{"grade_$property"})) {
2462                 if ($CFG->{"grade_$property"} == -1) {
2463                     continue; //temporary bc before version bump
2464                 }
2465                 $this->$property = $CFG->{"grade_$property"};
2466             }
2467         }
2468     }
2470     /**
2471      * Applies forced settings on this category
2472      *
2473      * @return bool True if anything changed
2474      */
2475     public function apply_forced_settings() {
2476         global $CFG;
2478         $updated = false;
2480         foreach ($this->forceable as $property) {
2482             if (isset($CFG->{"grade_$property"}) and isset($CFG->{"grade_{$property}_flag"}) and
2483                                                     ((int) $CFG->{"grade_{$property}_flag"} & 1)) {
2485                 if ($CFG->{"grade_$property"} == -1) {
2486                     continue; //temporary bc before version bump
2487                 }
2488                 $this->$property = $CFG->{"grade_$property"};
2489                 $updated = true;
2490             }
2491         }
2493         return $updated;
2494     }
2496     /**
2497      * Notification of change in forced category settings.
2498      *
2499      * Causes all course and category grade items to be marked as needing to be updated
2500      */
2501     public static function updated_forced_settings() {
2502         global $CFG, $DB;
2503         $params = array(1, 'course', 'category');
2504         $sql = "UPDATE {grade_items} SET needsupdate=? WHERE itemtype=? or itemtype=?";
2505         $DB->execute($sql, $params);
2506     }