MDL-54613 unit tests: Add iteminstance to test grade_item
[moodle.git] / lib / grade / grade_grade.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 an individual user's grade
19  *
20  * @package   core_grades
21  * @category  grade
22  * @copyright 2006 Nicolas Connault
23  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once('grade_object.php');
30 /**
31  * grade_grades is an object mapped to DB table {prefix}grade_grades
32  *
33  * @package   core_grades
34  * @category  grade
35  * @copyright 2006 Nicolas Connault
36  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 class grade_grade extends grade_object {
40     /**
41      * The DB table.
42      * @var string $table
43      */
44     public $table = 'grade_grades';
46     /**
47      * Array of required table fields, must start with 'id'.
48      * @var array $required_fields
49      */
50     public $required_fields = array('id', 'itemid', 'userid', 'rawgrade', 'rawgrademax', 'rawgrademin',
51                                  'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked',
52                                  'locktime', 'exported', 'overridden', 'excluded', 'timecreated',
53                                  'timemodified', 'aggregationstatus', 'aggregationweight');
55     /**
56      * Array of optional fields with default values (these should match db defaults)
57      * @var array $optional_fields
58      */
59     public $optional_fields = array('feedback'=>null, 'feedbackformat'=>0, 'information'=>null, 'informationformat'=>0);
61     /**
62      * The id of the grade_item this grade belongs to.
63      * @var int $itemid
64      */
65     public $itemid;
67     /**
68      * The grade_item object referenced by $this->itemid.
69      * @var grade_item $grade_item
70      */
71     public $grade_item;
73     /**
74      * The id of the user this grade belongs to.
75      * @var int $userid
76      */
77     public $userid;
79     /**
80      * The grade value of this raw grade, if such was provided by the module.
81      * @var float $rawgrade
82      */
83     public $rawgrade;
85     /**
86      * The maximum allowable grade when this grade was created.
87      * @var float $rawgrademax
88      */
89     public $rawgrademax = 100;
91     /**
92      * The minimum allowable grade when this grade was created.
93      * @var float $rawgrademin
94      */
95     public $rawgrademin = 0;
97     /**
98      * id of the scale, if this grade is based on a scale.
99      * @var int $rawscaleid
100      */
101     public $rawscaleid;
103     /**
104      * The userid of the person who last modified this grade.
105      * @var int $usermodified
106      */
107     public $usermodified;
109     /**
110      * The final value of this grade.
111      * @var float $finalgrade
112      */
113     public $finalgrade;
115     /**
116      * 0 if visible, 1 always hidden or date not visible until
117      * @var float $hidden
118      */
119     public $hidden = 0;
121     /**
122      * 0 not locked, date when the item was locked
123      * @var float locked
124      */
125     public $locked = 0;
127     /**
128      * 0 no automatic locking, date when to lock the grade automatically
129      * @var float $locktime
130      */
131     public $locktime = 0;
133     /**
134      * Exported flag
135      * @var bool $exported
136      */
137     public $exported = 0;
139     /**
140      * Overridden flag
141      * @var bool $overridden
142      */
143     public $overridden = 0;
145     /**
146      * Grade excluded from aggregation functions
147      * @var bool $excluded
148      */
149     public $excluded = 0;
151     /**
152      * TODO: HACK: create a new field datesubmitted - the date of submission if any (MDL-31377)
153      * @var bool $timecreated
154      */
155     public $timecreated = null;
157     /**
158      * TODO: HACK: create a new field dategraded - the date of grading (MDL-31378)
159      * @var bool $timemodified
160      */
161     public $timemodified = null;
163     /**
164      * Aggregation status flag. Can be one of 'unknown', 'dropped', 'novalue' or 'used'.
165      * @var string $aggregationstatus
166      */
167     public $aggregationstatus = 'unknown';
169     /**
170      * Aggregation weight is the specific weight used in the aggregation calculation for this grade.
171      * @var float $aggregationweight
172      */
173     public $aggregationweight = null;
175     /**
176      * Returns array of grades for given grade_item+users
177      *
178      * @param grade_item $grade_item
179      * @param array $userids
180      * @param bool $include_missing include grades that do not exist yet
181      * @return array userid=>grade_grade array
182      */
183     public static function fetch_users_grades($grade_item, $userids, $include_missing=true) {
184         global $DB;
186         // hmm, there might be a problem with length of sql query
187         // if there are too many users requested - we might run out of memory anyway
188         $limit = 2000;
189         $count = count($userids);
190         if ($count > $limit) {
191             $half = (int)($count/2);
192             $first  = array_slice($userids, 0, $half);
193             $second = array_slice($userids, $half);
194             return grade_grade::fetch_users_grades($grade_item, $first, $include_missing) + grade_grade::fetch_users_grades($grade_item, $second, $include_missing);
195         }
197         list($user_ids_cvs, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uid0');
198         $params['giid'] = $grade_item->id;
199         $result = array();
200         if ($grade_records = $DB->get_records_select('grade_grades', "itemid=:giid AND userid $user_ids_cvs", $params)) {
201             foreach ($grade_records as $record) {
202                 $result[$record->userid] = new grade_grade($record, false);
203             }
204         }
205         if ($include_missing) {
206             foreach ($userids as $userid) {
207                 if (!array_key_exists($userid, $result)) {
208                     $grade_grade = new grade_grade();
209                     $grade_grade->userid = $userid;
210                     $grade_grade->itemid = $grade_item->id;
211                     $result[$userid] = $grade_grade;
212                 }
213             }
214         }
216         return $result;
217     }
219     /**
220      * Loads the grade_item object referenced by $this->itemid and saves it as $this->grade_item for easy access
221      *
222      * @return grade_item The grade_item instance referenced by $this->itemid
223      */
224     public function load_grade_item() {
225         if (empty($this->itemid)) {
226             debugging('Missing itemid');
227             $this->grade_item = null;
228             return null;
229         }
231         if (empty($this->grade_item)) {
232             $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
234         } else if ($this->grade_item->id != $this->itemid) {
235             debugging('Itemid mismatch');
236             $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
237         }
239         return $this->grade_item;
240     }
242     /**
243      * Is grading object editable?
244      *
245      * @return bool
246      */
247     public function is_editable() {
248         if ($this->is_locked()) {
249             return false;
250         }
252         $grade_item = $this->load_grade_item();
254         if ($grade_item->gradetype == GRADE_TYPE_NONE) {
255             return false;
256         }
258         if ($grade_item->is_course_item() or $grade_item->is_category_item()) {
259             return (bool)get_config('moodle', 'grade_overridecat');
260         }
262         return true;
263     }
265     /**
266      * Check grade lock status. Uses both grade item lock and grade lock.
267      * Internally any date in locked field (including future ones) means locked,
268      * the date is stored for logging purposes only.
269      *
270      * @return bool True if locked, false if not
271      */
272     public function is_locked() {
273         $this->load_grade_item();
274         if (empty($this->grade_item)) {
275             return !empty($this->locked);
276         } else {
277             return !empty($this->locked) or $this->grade_item->is_locked();
278         }
279     }
281     /**
282      * Checks if grade overridden
283      *
284      * @return bool True if grade is overriden
285      */
286     public function is_overridden() {
287         return !empty($this->overridden);
288     }
290     /**
291      * Returns timestamp of submission related to this grade, null if not submitted.
292      *
293      * @return int Timestamp
294      */
295     public function get_datesubmitted() {
296         //TODO: HACK - create new fields (MDL-31379)
297         return $this->timecreated;
298     }
300     /**
301      * Returns the weight this grade contributed to the aggregated grade
302      *
303      * @return float|null
304      */
305     public function get_aggregationweight() {
306         return $this->aggregationweight;
307     }
309     /**
310      * Set aggregationweight.
311      *
312      * @param float $aggregationweight
313      * @return void
314      */
315     public function set_aggregationweight($aggregationweight) {
316         $this->aggregationweight = $aggregationweight;
317         $this->update();
318     }
320     /**
321      * Returns the info on how this value was used in the aggregated grade
322      *
323      * @return string One of 'dropped', 'excluded', 'novalue', 'used' or 'extra'
324      */
325     public function get_aggregationstatus() {
326         return $this->aggregationstatus;
327     }
329     /**
330      * Set aggregationstatus flag
331      *
332      * @param string $aggregationstatus
333      * @return void
334      */
335     public function set_aggregationstatus($aggregationstatus) {
336         $this->aggregationstatus = $aggregationstatus;
337         $this->update();
338     }
340     /**
341      * Returns the minimum and maximum number of points this grade is graded with respect to.
342      *
343      * @since  Moodle 2.8.7, 2.9.1
344      * @return array A list containing, in order, the minimum and maximum number of points.
345      */
346     protected function get_grade_min_and_max() {
347         global $CFG;
348         $this->load_grade_item();
350         // When the following setting is turned on we use the grade_grade raw min and max values.
351         $minmaxtouse = grade_get_setting($this->grade_item->courseid, 'minmaxtouse', $CFG->grade_minmaxtouse);
353         // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
354         // wish to update the grades.
355         $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->grade_item->courseid);
356         // Gradebook is frozen, run through old code.
357         if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
358             // Only aggregate items use separate min grades.
359             if ($minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE || $this->grade_item->is_aggregate_item()) {
360                 return array($this->rawgrademin, $this->rawgrademax);
361             } else {
362                 return array($this->grade_item->grademin, $this->grade_item->grademax);
363             }
364         } else {
365             // Only aggregate items use separate min grades, unless they are calculated grade items.
366             if (($this->grade_item->is_aggregate_item() && !$this->grade_item->is_calculated())
367                     || $minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE) {
368                 return array($this->rawgrademin, $this->rawgrademax);
369             } else {
370                 return array($this->grade_item->grademin, $this->grade_item->grademax);
371             }
372         }
373     }
375     /**
376      * Returns the minimum number of points this grade is graded with.
377      *
378      * @since  Moodle 2.8.7, 2.9.1
379      * @return float The minimum number of points
380      */
381     public function get_grade_min() {
382         list($min, $max) = $this->get_grade_min_and_max();
384         return $min;
385     }
387     /**
388      * Returns the maximum number of points this grade is graded with respect to.
389      *
390      * @since  Moodle 2.8.7, 2.9.1
391      * @return float The maximum number of points
392      */
393     public function get_grade_max() {
394         list($min, $max) = $this->get_grade_min_and_max();
396         return $max;
397     }
399     /**
400      * Returns timestamp when last graded, null if no grade present
401      *
402      * @return int
403      */
404     public function get_dategraded() {
405         //TODO: HACK - create new fields (MDL-31379)
406         if (is_null($this->finalgrade) and is_null($this->feedback)) {
407             return null; // no grade == no date
408         } else if ($this->overridden) {
409             return $this->overridden;
410         } else {
411             return $this->timemodified;
412         }
413     }
415     /**
416      * Set the overridden status of grade
417      *
418      * @param bool $state requested overridden state
419      * @param bool $refresh refresh grades from external activities if needed
420      * @return bool true is db state changed
421      */
422     public function set_overridden($state, $refresh = true) {
423         if (empty($this->overridden) and $state) {
424             $this->overridden = time();
425             $this->update();
426             return true;
428         } else if (!empty($this->overridden) and !$state) {
429             $this->overridden = 0;
430             $this->update();
432             if ($refresh) {
433                 //refresh when unlocking
434                 $this->grade_item->refresh_grades($this->userid);
435             }
437             return true;
438         }
439         return false;
440     }
442     /**
443      * Checks if grade excluded from aggregation functions
444      *
445      * @return bool True if grade is excluded from aggregation
446      */
447     public function is_excluded() {
448         return !empty($this->excluded);
449     }
451     /**
452      * Set the excluded status of grade
453      *
454      * @param bool $state requested excluded state
455      * @return bool True is database state changed
456      */
457     public function set_excluded($state) {
458         if (empty($this->excluded) and $state) {
459             $this->excluded = time();
460             $this->update();
461             return true;
463         } else if (!empty($this->excluded) and !$state) {
464             $this->excluded = 0;
465             $this->update();
466             return true;
467         }
468         return false;
469     }
471     /**
472      * Lock/unlock this grade.
473      *
474      * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
475      * @param bool $cascade Ignored param
476      * @param bool $refresh Refresh grades when unlocking
477      * @return bool True if successful, false if can not set new lock state for grade
478      */
479     public function set_locked($lockedstate, $cascade=false, $refresh=true) {
480         $this->load_grade_item();
482         if ($lockedstate) {
483             if ($this->grade_item->needsupdate) {
484                 //can not lock grade if final not calculated!
485                 return false;
486             }
488             $this->locked = time();
489             $this->update();
491             return true;
493         } else {
494             if (!empty($this->locked) and $this->locktime < time()) {
495                 //we have to reset locktime or else it would lock up again
496                 $this->locktime = 0;
497             }
499             // remove the locked flag
500             $this->locked = 0;
501             $this->update();
503             if ($refresh and !$this->is_overridden()) {
504                 //refresh when unlocking and not overridden
505                 $this->grade_item->refresh_grades($this->userid);
506             }
508             return true;
509         }
510     }
512     /**
513      * Lock the grade if needed. Make sure this is called only when final grades are valid
514      *
515      * @param array $items array of all grade item ids
516      * @return void
517      */
518     public static function check_locktime_all($items) {
519         global $CFG, $DB;
521         $now = time(); // no rounding needed, this is not supposed to be called every 10 seconds
522         list($usql, $params) = $DB->get_in_or_equal($items);
523         $params[] = $now;
524         $rs = $DB->get_recordset_select('grade_grades', "itemid $usql AND locked = 0 AND locktime > 0 AND locktime < ?", $params);
525         foreach ($rs as $grade) {
526             $grade_grade = new grade_grade($grade, false);
527             $grade_grade->locked = time();
528             $grade_grade->update('locktime');
529         }
530         $rs->close();
531     }
533     /**
534      * Set the locktime for this grade.
535      *
536      * @param int $locktime timestamp for lock to activate
537      * @return void
538      */
539     public function set_locktime($locktime) {
540         $this->locktime = $locktime;
541         $this->update();
542     }
544     /**
545      * Get the locktime for this grade.
546      *
547      * @return int $locktime timestamp for lock to activate
548      */
549     public function get_locktime() {
550         $this->load_grade_item();
552         $item_locktime = $this->grade_item->get_locktime();
554         if (empty($this->locktime) or ($item_locktime and $item_locktime < $this->locktime)) {
555             return $item_locktime;
557         } else {
558             return $this->locktime;
559         }
560     }
562     /**
563      * Check grade hidden status. Uses data from both grade item and grade.
564      *
565      * @return bool true if hidden, false if not
566      */
567     public function is_hidden() {
568         $this->load_grade_item();
569         if (empty($this->grade_item)) {
570             return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time());
571         } else {
572             return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()) or $this->grade_item->is_hidden();
573         }
574     }
576     /**
577      * Check grade hidden status. Uses data from both grade item and grade.
578      *
579      * @return bool true if hiddenuntil, false if not
580      */
581     public function is_hiddenuntil() {
582         $this->load_grade_item();
584         if ($this->hidden == 1 or $this->grade_item->hidden == 1) {
585             return false; //always hidden
586         }
588         if ($this->hidden > 1 or $this->grade_item->hidden > 1) {
589             return true;
590         }
592         return false;
593     }
595     /**
596      * Check grade hidden status. Uses data from both grade item and grade.
597      *
598      * @return int 0 means visible, 1 hidden always, timestamp hidden until
599      */
600     public function get_hidden() {
601         $this->load_grade_item();
603         $item_hidden = $this->grade_item->get_hidden();
605         if ($item_hidden == 1) {
606             return 1;
608         } else if ($item_hidden == 0) {
609             return $this->hidden;
611         } else {
612             if ($this->hidden == 0) {
613                 return $item_hidden;
614             } else if ($this->hidden == 1) {
615                 return 1;
616             } else if ($this->hidden > $item_hidden) {
617                 return $this->hidden;
618             } else {
619                 return $item_hidden;
620             }
621         }
622     }
624     /**
625      * Set the hidden status of grade, 0 mean visible, 1 always hidden, number means date to hide until.
626      *
627      * @param int $hidden new hidden status
628      * @param bool $cascade ignored
629      */
630     public function set_hidden($hidden, $cascade=false) {
631        $this->hidden = $hidden;
632        $this->update();
633     }
635     /**
636      * Finds and returns a grade_grade instance based on params.
637      *
638      * @param array $params associative arrays varname=>value
639      * @return grade_grade Returns a grade_grade instance or false if none found
640      */
641     public static function fetch($params) {
642         return grade_object::fetch_helper('grade_grades', 'grade_grade', $params);
643     }
645     /**
646      * Finds and returns all grade_grade instances based on params.
647      *
648      * @param array $params associative arrays varname=>value
649      * @return array array of grade_grade instances or false if none found.
650      */
651     public static function fetch_all($params) {
652         return grade_object::fetch_all_helper('grade_grades', 'grade_grade', $params);
653     }
655     /**
656      * Given a float value situated between a source minimum and a source maximum, converts it to the
657      * corresponding value situated between a target minimum and a target maximum. Thanks to Darlene
658      * for the formula :-)
659      *
660      * @param float $rawgrade
661      * @param float $source_min
662      * @param float $source_max
663      * @param float $target_min
664      * @param float $target_max
665      * @return float Converted value
666      */
667     public static function standardise_score($rawgrade, $source_min, $source_max, $target_min, $target_max) {
668         if (is_null($rawgrade)) {
669           return null;
670         }
672         if ($source_max == $source_min or $target_min == $target_max) {
673             // prevent division by 0
674             return $target_max;
675         }
677         $factor = ($rawgrade - $source_min) / ($source_max - $source_min);
678         $diff = $target_max - $target_min;
679         $standardised_value = $factor * $diff + $target_min;
680         return $standardised_value;
681     }
683     /**
684      * Given an array like this:
685      * $a = array(1=>array(2, 3),
686      *            2=>array(4),
687      *            3=>array(1),
688      *            4=>array())
689      * this function fully resolves the dependencies so each value will be an array of
690      * the all items this item depends on and their dependencies (and their dependencies...).
691      * It should not explode if there are circular dependencies.
692      * The dependency depth array will list the number of branches in the tree above each leaf.
693      *
694      * @param array $dependson Array to flatten
695      * @param array $dependencydepth Array of itemids => depth. Initially these should be all set to 1.
696      * @return array Flattened array
697      */
698     protected static function flatten_dependencies_array(&$dependson, &$dependencydepth) {
699         // Flatten the nested dependencies - this will handle recursion bombs because it removes duplicates.
700         $somethingchanged = true;
701         while ($somethingchanged) {
702             $somethingchanged = false;
704             foreach ($dependson as $itemid => $depends) {
705                 // Make a copy so we can tell if it changed.
706                 $before = $dependson[$itemid];
707                 foreach ($depends as $subitemid => $subdepends) {
708                     $dependson[$itemid] = array_unique(array_merge($depends, $dependson[$subdepends]));
709                     sort($dependson[$itemid], SORT_NUMERIC);
710                 }
711                 if ($before != $dependson[$itemid]) {
712                     $somethingchanged = true;
713                     if (!isset($dependencydepth[$itemid])) {
714                         $dependencydepth[$itemid] = 1;
715                     } else {
716                         $dependencydepth[$itemid]++;
717                     }
718                 }
719             }
720         }
721     }
723     /**
724      * Return array of grade item ids that are either hidden or indirectly depend
725      * on hidden grades, excluded grades are not returned.
726      * THIS IS A REALLY BIG HACK! to be replaced by conditional aggregation of hidden grades in 2.0
727      *
728      * @param array $grade_grades all course grades of one user, & used for better internal caching
729      * @param array $grade_items array of grade items, & used for better internal caching
730      * @return array This is an array of 3 arrays:
731      *      unknown => list of item ids that may be affected by hiding (with the calculated grade as the value)
732      *      altered => list of item ids that are definitely affected by hiding (with the calculated grade as the value)
733      *      alteredgrademax => for each item in altered or unknown, the new value of the grademax
734      *      alteredgrademin => for each item in altered or unknown, the new value of the grademin
735      *      alteredgradestatus => for each item with a modified status - the value of the new status
736      *      alteredgradeweight => for each item with a modified weight - the value of the new weight
737      */
738     public static function get_hiding_affected(&$grade_grades, &$grade_items) {
739         global $CFG;
741         if (count($grade_grades) !== count($grade_items)) {
742             print_error('invalidarraysize', 'debug', '', 'grade_grade::get_hiding_affected()!');
743         }
745         $dependson = array();
746         $todo = array();
747         $unknown = array();  // can not find altered
748         $altered = array();  // altered grades
749         $alteredgrademax = array();  // Altered grade max values.
750         $alteredgrademin = array();  // Altered grade min values.
751         $alteredaggregationstatus = array();  // Altered aggregation status.
752         $alteredaggregationweight = array();  // Altered aggregation weight.
753         $dependencydepth = array();
755         $hiddenfound = false;
756         foreach($grade_grades as $itemid=>$unused) {
757             $grade_grade =& $grade_grades[$itemid];
758             // We need the immediate dependencies of all every grade_item so we can calculate nested dependencies.
759             $dependson[$grade_grade->itemid] = $grade_items[$grade_grade->itemid]->depends_on();
760             if ($grade_grade->is_excluded()) {
761                 //nothing to do, aggregation is ok
762             } else if ($grade_grade->is_hidden()) {
763                 $hiddenfound = true;
764                 $altered[$grade_grade->itemid] = null;
765                 $alteredaggregationstatus[$grade_grade->itemid] = 'dropped';
766                 $alteredaggregationweight[$grade_grade->itemid] = 0;
767             } else if ($grade_grade->is_locked() or $grade_grade->is_overridden()) {
768                 // no need to recalculate locked or overridden grades
769             } else {
770                 if (!empty($dependson[$grade_grade->itemid])) {
771                     $dependencydepth[$grade_grade->itemid] = 1;
772                     $todo[] = $grade_grade->itemid;
773                 }
774             }
775         }
777         // Flatten the dependency tree and count number of branches to each leaf.
778         self::flatten_dependencies_array($dependson, $dependencydepth);
780         if (!$hiddenfound) {
781             return array('unknown' => array(),
782                          'altered' => array(),
783                          'alteredgrademax' => array(),
784                          'alteredgrademin' => array(),
785                          'alteredaggregationstatus' => array(),
786                          'alteredaggregationweight' => array());
787         }
788         // This line ensures that $dependencydepth has the same number of items as $todo.
789         $dependencydepth = array_intersect_key($dependencydepth, array_flip($todo));
790         // We need to resort the todo list by the dependency depth. This guarantees we process the leaves, then the branches.
791         array_multisort($dependencydepth, $todo);
793         $max = count($todo);
794         $hidden_precursors = null;
795         for($i=0; $i<$max; $i++) {
796             $found = false;
797             foreach($todo as $key=>$do) {
798                 $hidden_precursors = array_intersect($dependson[$do], $unknown);
799                 if ($hidden_precursors) {
800                     // this item depends on hidden grade indirectly
801                     $unknown[$do] = $do;
802                     unset($todo[$key]);
803                     $found = true;
804                     continue;
806                 } else if (!array_intersect($dependson[$do], $todo)) {
807                     $hidden_precursors = array_intersect($dependson[$do], array_keys($altered));
808                     // If the dependency is a sum aggregation, we need to process it as if it had hidden items.
809                     // The reason for this, is that the code will recalculate the maxgrade by removing ungraded
810                     // items and accounting for 'drop x grades' and then stored back in our virtual grade_items.
811                     // This recalculation is necessary because there will be a call to:
812                     //              $grade_category->aggregate_values_and_adjust_bounds
813                     // for the top level grade that will depend on knowing what that caclulated grademax is
814                     // and it finds that value by checking the virtual grade_items.
815                     $issumaggregate = false;
816                     if ($grade_items[$do]->itemtype == 'category') {
817                         $issumaggregate = $grade_items[$do]->load_item_category()->aggregation == GRADE_AGGREGATE_SUM;
818                     }
819                     if (!$hidden_precursors && !$issumaggregate) {
820                         unset($todo[$key]);
821                         $found = true;
822                         continue;
824                     } else {
825                         // depends on altered grades - we should try to recalculate if possible
826                         if ($grade_items[$do]->is_calculated() or
827                             (!$grade_items[$do]->is_category_item() and !$grade_items[$do]->is_course_item())
828                         ) {
829                             // This is a grade item that is not a category or course and has been affected by grade hiding.
830                             // I guess this means it is a calculation that needs to be recalculated.
831                             $unknown[$do] = $do;
832                             unset($todo[$key]);
833                             $found = true;
834                             continue;
836                         } else {
837                             // This is a grade category (or course).
838                             $grade_category = $grade_items[$do]->load_item_category();
840                             // Build a new list of the grades in this category.
841                             $values = array();
842                             $immediatedepends = $grade_items[$do]->depends_on();
843                             foreach ($immediatedepends as $itemid) {
844                                 if (array_key_exists($itemid, $altered)) {
845                                     //nulling an altered precursor
846                                     $values[$itemid] = $altered[$itemid];
847                                     if (is_null($values[$itemid])) {
848                                         // This means this was a hidden grade item removed from the result.
849                                         unset($values[$itemid]);
850                                     }
851                                 } elseif (empty($values[$itemid])) {
852                                     $values[$itemid] = $grade_grades[$itemid]->finalgrade;
853                                 }
854                             }
856                             foreach ($values as $itemid=>$value) {
857                                 if ($grade_grades[$itemid]->is_excluded()) {
858                                     unset($values[$itemid]);
859                                     $alteredaggregationstatus[$itemid] = 'excluded';
860                                     $alteredaggregationweight[$itemid] = null;
861                                     continue;
862                                 }
863                                 // The grade min/max may have been altered by hiding.
864                                 $grademin = $grade_items[$itemid]->grademin;
865                                 if (isset($alteredgrademin[$itemid])) {
866                                     $grademin = $alteredgrademin[$itemid];
867                                 }
868                                 $grademax = $grade_items[$itemid]->grademax;
869                                 if (isset($alteredgrademax[$itemid])) {
870                                     $grademax = $alteredgrademax[$itemid];
871                                 }
872                                 $values[$itemid] = grade_grade::standardise_score($value, $grademin, $grademax, 0, 1);
873                             }
875                             if ($grade_category->aggregateonlygraded) {
876                                 foreach ($values as $itemid=>$value) {
877                                     if (is_null($value)) {
878                                         unset($values[$itemid]);
879                                         $alteredaggregationstatus[$itemid] = 'novalue';
880                                         $alteredaggregationweight[$itemid] = null;
881                                     }
882                                 }
883                             } else {
884                                 foreach ($values as $itemid=>$value) {
885                                     if (is_null($value)) {
886                                         $values[$itemid] = 0;
887                                     }
888                                 }
889                             }
891                             // limit and sort
892                             $allvalues = $values;
893                             $grade_category->apply_limit_rules($values, $grade_items);
895                             $moredropped = array_diff($allvalues, $values);
896                             foreach ($moredropped as $drop => $unused) {
897                                 $alteredaggregationstatus[$drop] = 'dropped';
898                                 $alteredaggregationweight[$drop] = null;
899                             }
901                             foreach ($values as $itemid => $val) {
902                                 if ($grade_category->is_extracredit_used() && ($grade_items[$itemid]->aggregationcoef > 0)) {
903                                     $alteredaggregationstatus[$itemid] = 'extra';
904                                 }
905                             }
907                             asort($values, SORT_NUMERIC);
909                             // let's see we have still enough grades to do any statistics
910                             if (count($values) == 0) {
911                                 // not enough attempts yet
912                                 $altered[$do] = null;
913                                 unset($todo[$key]);
914                                 $found = true;
915                                 continue;
916                             }
918                             $usedweights = array();
919                             $adjustedgrade = $grade_category->aggregate_values_and_adjust_bounds($values, $grade_items, $usedweights);
921                             // recalculate the rawgrade back to requested range
922                             $finalgrade = grade_grade::standardise_score($adjustedgrade['grade'],
923                                                                          0,
924                                                                          1,
925                                                                          $adjustedgrade['grademin'],
926                                                                          $adjustedgrade['grademax']);
928                             foreach ($usedweights as $itemid => $weight) {
929                                 if (!isset($alteredaggregationstatus[$itemid])) {
930                                     $alteredaggregationstatus[$itemid] = 'used';
931                                 }
932                                 $alteredaggregationweight[$itemid] = $weight;
933                             }
935                             $finalgrade = $grade_items[$do]->bounded_grade($finalgrade);
936                             $alteredgrademin[$do] = $adjustedgrade['grademin'];
937                             $alteredgrademax[$do] = $adjustedgrade['grademax'];
938                             // We need to muck with the "in-memory" grade_items records so
939                             // that subsequent calculations will use the adjusted grademin and grademax.
940                             $grade_items[$do]->grademin = $adjustedgrade['grademin'];
941                             $grade_items[$do]->grademax = $adjustedgrade['grademax'];
943                             $altered[$do] = $finalgrade;
944                             unset($todo[$key]);
945                             $found = true;
946                             continue;
947                         }
948                     }
949                 }
950             }
951             if (!$found) {
952                 break;
953             }
954         }
956         return array('unknown' => $unknown,
957                      'altered' => $altered,
958                      'alteredgrademax' => $alteredgrademax,
959                      'alteredgrademin' => $alteredgrademin,
960                      'alteredaggregationstatus' => $alteredaggregationstatus,
961                      'alteredaggregationweight' => $alteredaggregationweight);
962     }
964     /**
965      * Returns true if the grade's value is superior or equal to the grade item's gradepass value, false otherwise.
966      *
967      * @param grade_item $grade_item An optional grade_item of which gradepass value we can use, saves having to load the grade_grade's grade_item
968      * @return bool
969      */
970     public function is_passed($grade_item = null) {
971         if (empty($grade_item)) {
972             if (!isset($this->grade_item)) {
973                 $this->load_grade_item();
974             }
975         } else {
976             $this->grade_item = $grade_item;
977             $this->itemid = $grade_item->id;
978         }
980         // Return null if finalgrade is null
981         if (is_null($this->finalgrade)) {
982             return null;
983         }
985         // Return null if gradepass == grademin, gradepass is null, or grade item is a scale and gradepass is 0.
986         if (is_null($this->grade_item->gradepass)) {
987             return null;
988         } else if ($this->grade_item->gradepass == $this->grade_item->grademin) {
989             return null;
990         } else if ($this->grade_item->gradetype == GRADE_TYPE_SCALE && !grade_floats_different($this->grade_item->gradepass, 0.0)) {
991             return null;
992         }
994         return $this->finalgrade >= $this->grade_item->gradepass;
995     }
997     /**
998      * Insert the grade_grade instance into the database.
999      *
1000      * @param string $source From where was the object inserted (mod/forum, manual, etc.)
1001      * @return int The new grade_grade ID if successful, false otherwise
1002      */
1003     public function insert($source=null) {
1004         // TODO: dategraded hack - do not update times, they are used for submission and grading (MDL-31379)
1005         //$this->timecreated = $this->timemodified = time();
1006         return parent::insert($source);
1007     }
1009     /**
1010      * In addition to update() as defined in grade_object rounds the float numbers using php function,
1011      * the reason is we need to compare the db value with computed number to skip updates if possible.
1012      *
1013      * @param string $source from where was the object inserted (mod/forum, manual, etc.)
1014      * @return bool success
1015      */
1016     public function update($source=null) {
1017         $this->rawgrade    = grade_floatval($this->rawgrade);
1018         $this->finalgrade  = grade_floatval($this->finalgrade);
1019         $this->rawgrademin = grade_floatval($this->rawgrademin);
1020         $this->rawgrademax = grade_floatval($this->rawgrademax);
1021         return parent::update($source);
1022     }
1024     /**
1025      * Deletes the grade_grade instance from the database.
1026      *
1027      * @param string $source The location the deletion occurred (mod/forum, manual, etc.).
1028      * @return bool Returns true if the deletion was successful, false otherwise.
1029      */
1030     public function delete($source = null) {
1031         $success = parent::delete($source);
1033         // If the grade was deleted successfully trigger a grade_deleted event.
1034         if ($success) {
1035             $this->load_grade_item();
1036             \core\event\grade_deleted::create_from_grade($this)->trigger();
1037         }
1039         return $success;
1040     }
1042     /**
1043      * Used to notify the completion system (if necessary) that a user's grade
1044      * has changed, and clear up a possible score cache.
1045      *
1046      * @param bool $deleted True if grade was actually deleted
1047      */
1048     protected function notify_changed($deleted) {
1049         global $CFG;
1051         // Condition code may cache the grades for conditional availability of
1052         // modules or sections. (This code should use a hook for communication
1053         // with plugin, but hooks are not implemented at time of writing.)
1054         if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
1055             \availability_grade\callbacks::grade_changed($this->userid);
1056         }
1058         require_once($CFG->libdir.'/completionlib.php');
1060         // Bail out immediately if completion is not enabled for site (saves loading
1061         // grade item & requiring the restore stuff).
1062         if (!completion_info::is_enabled_for_site()) {
1063             return;
1064         }
1066         // Ignore during restore, as completion data will be updated anyway and
1067         // doing it now will result in incorrect dates (it will say they got the
1068         // grade completion now, instead of the correct time).
1069         if (class_exists('restore_controller', false) && restore_controller::is_executing()) {
1070             return;
1071         }
1073         // Load information about grade item
1074         $this->load_grade_item();
1076         // Only course-modules have completion data
1077         if ($this->grade_item->itemtype!='mod') {
1078             return;
1079         }
1081         // Use $COURSE if available otherwise get it via item fields
1082         $course = get_course($this->grade_item->courseid, false);
1084         // Bail out if completion is not enabled for course
1085         $completion = new completion_info($course);
1086         if (!$completion->is_enabled()) {
1087             return;
1088         }
1090         // Get course-module
1091         $cm = get_coursemodule_from_instance($this->grade_item->itemmodule,
1092               $this->grade_item->iteminstance, $this->grade_item->courseid);
1093         // If the course-module doesn't exist, display a warning...
1094         if (!$cm) {
1095             // ...unless the grade is being deleted in which case it's likely
1096             // that the course-module was just deleted too, so that's okay.
1097             if (!$deleted) {
1098                 debugging("Couldn't find course-module for module '" .
1099                         $this->grade_item->itemmodule . "', instance '" .
1100                         $this->grade_item->iteminstance . "', course '" .
1101                         $this->grade_item->courseid . "'");
1102             }
1103             return;
1104         }
1106         // Pass information on to completion system
1107         $completion->inform_grade_changed($cm, $this->grade_item, $this, $deleted);
1108     }
1110     /**
1111      * Get some useful information about how this grade_grade is reflected in the aggregation
1112      * for the grade_category. For example this could be an extra credit item, and it could be
1113      * dropped because it's in the X lowest or highest.
1114      *
1115      * @return array(status, weight) - A keyword and a numerical weight that represents how this grade was included in the aggregation.
1116      */
1117     function get_aggregation_hint() {
1118         return array('status' => $this->get_aggregationstatus(),
1119                      'weight' => $this->get_aggregationweight());
1120     }