13d2ad5bf962f8de806b8e99201af566f855e849
[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      * Feedback files to copy.
177      *
178      * Example -
179      *
180      * [
181      *     'contextid' => 1,
182      *     'component' => 'mod_xyz',
183      *     'filearea' => 'mod_xyz_feedback',
184      *     'itemid' => 2
185      * ];
186      *
187      * @var array
188      */
189     public $feedbackfiles = [];
191     /**
192      * Returns array of grades for given grade_item+users
193      *
194      * @param grade_item $grade_item
195      * @param array $userids
196      * @param bool $include_missing include grades that do not exist yet
197      * @return array userid=>grade_grade array
198      */
199     public static function fetch_users_grades($grade_item, $userids, $include_missing=true) {
200         global $DB;
202         // hmm, there might be a problem with length of sql query
203         // if there are too many users requested - we might run out of memory anyway
204         $limit = 2000;
205         $count = count($userids);
206         if ($count > $limit) {
207             $half = (int)($count/2);
208             $first  = array_slice($userids, 0, $half);
209             $second = array_slice($userids, $half);
210             return grade_grade::fetch_users_grades($grade_item, $first, $include_missing) + grade_grade::fetch_users_grades($grade_item, $second, $include_missing);
211         }
213         list($user_ids_cvs, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uid0');
214         $params['giid'] = $grade_item->id;
215         $result = array();
216         if ($grade_records = $DB->get_records_select('grade_grades', "itemid=:giid AND userid $user_ids_cvs", $params)) {
217             foreach ($grade_records as $record) {
218                 $result[$record->userid] = new grade_grade($record, false);
219             }
220         }
221         if ($include_missing) {
222             foreach ($userids as $userid) {
223                 if (!array_key_exists($userid, $result)) {
224                     $grade_grade = new grade_grade();
225                     $grade_grade->userid = $userid;
226                     $grade_grade->itemid = $grade_item->id;
227                     $result[$userid] = $grade_grade;
228                 }
229             }
230         }
232         return $result;
233     }
235     /**
236      * Loads the grade_item object referenced by $this->itemid and saves it as $this->grade_item for easy access
237      *
238      * @return grade_item The grade_item instance referenced by $this->itemid
239      */
240     public function load_grade_item() {
241         if (empty($this->itemid)) {
242             debugging('Missing itemid');
243             $this->grade_item = null;
244             return null;
245         }
247         if (empty($this->grade_item)) {
248             $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
250         } else if ($this->grade_item->id != $this->itemid) {
251             debugging('Itemid mismatch');
252             $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
253         }
255         if (empty($this->grade_item)) {
256             debugging("Missing grade item id $this->itemid", DEBUG_DEVELOPER);
257         }
259         return $this->grade_item;
260     }
262     /**
263      * Is grading object editable?
264      *
265      * @return bool
266      */
267     public function is_editable() {
268         if ($this->is_locked()) {
269             return false;
270         }
272         $grade_item = $this->load_grade_item();
274         if ($grade_item->gradetype == GRADE_TYPE_NONE) {
275             return false;
276         }
278         if ($grade_item->is_course_item() or $grade_item->is_category_item()) {
279             return (bool)get_config('moodle', 'grade_overridecat');
280         }
282         return true;
283     }
285     /**
286      * Check grade lock status. Uses both grade item lock and grade lock.
287      * Internally any date in locked field (including future ones) means locked,
288      * the date is stored for logging purposes only.
289      *
290      * @return bool True if locked, false if not
291      */
292     public function is_locked() {
293         $this->load_grade_item();
294         if (empty($this->grade_item)) {
295             return !empty($this->locked);
296         } else {
297             return !empty($this->locked) or $this->grade_item->is_locked();
298         }
299     }
301     /**
302      * Checks if grade overridden
303      *
304      * @return bool True if grade is overriden
305      */
306     public function is_overridden() {
307         return !empty($this->overridden);
308     }
310     /**
311      * Returns timestamp of submission related to this grade, null if not submitted.
312      *
313      * @return int Timestamp
314      */
315     public function get_datesubmitted() {
316         //TODO: HACK - create new fields (MDL-31379)
317         return $this->timecreated;
318     }
320     /**
321      * Returns the weight this grade contributed to the aggregated grade
322      *
323      * @return float|null
324      */
325     public function get_aggregationweight() {
326         return $this->aggregationweight;
327     }
329     /**
330      * Set aggregationweight.
331      *
332      * @param float $aggregationweight
333      * @return void
334      */
335     public function set_aggregationweight($aggregationweight) {
336         $this->aggregationweight = $aggregationweight;
337         $this->update();
338     }
340     /**
341      * Returns the info on how this value was used in the aggregated grade
342      *
343      * @return string One of 'dropped', 'excluded', 'novalue', 'used' or 'extra'
344      */
345     public function get_aggregationstatus() {
346         return $this->aggregationstatus;
347     }
349     /**
350      * Set aggregationstatus flag
351      *
352      * @param string $aggregationstatus
353      * @return void
354      */
355     public function set_aggregationstatus($aggregationstatus) {
356         $this->aggregationstatus = $aggregationstatus;
357         $this->update();
358     }
360     /**
361      * Returns the minimum and maximum number of points this grade is graded with respect to.
362      *
363      * @since  Moodle 2.8.7, 2.9.1
364      * @return array A list containing, in order, the minimum and maximum number of points.
365      */
366     protected function get_grade_min_and_max() {
367         global $CFG;
368         $this->load_grade_item();
370         // When the following setting is turned on we use the grade_grade raw min and max values.
371         $minmaxtouse = grade_get_setting($this->grade_item->courseid, 'minmaxtouse', $CFG->grade_minmaxtouse);
373         // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
374         // wish to update the grades.
375         $gradebookcalculationsfreeze = 'gradebook_calculations_freeze_' . $this->grade_item->courseid;
376         // Gradebook is frozen, run through old code.
377         if (isset($CFG->$gradebookcalculationsfreeze) && (int)$CFG->$gradebookcalculationsfreeze <= 20150627) {
378             // Only aggregate items use separate min grades.
379             if ($minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE || $this->grade_item->is_aggregate_item()) {
380                 return array($this->rawgrademin, $this->rawgrademax);
381             } else {
382                 return array($this->grade_item->grademin, $this->grade_item->grademax);
383             }
384         } else {
385             // Only aggregate items use separate min grades, unless they are calculated grade items.
386             if (($this->grade_item->is_aggregate_item() && !$this->grade_item->is_calculated())
387                     || $minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE) {
388                 return array($this->rawgrademin, $this->rawgrademax);
389             } else {
390                 return array($this->grade_item->grademin, $this->grade_item->grademax);
391             }
392         }
393     }
395     /**
396      * Returns the minimum number of points this grade is graded with.
397      *
398      * @since  Moodle 2.8.7, 2.9.1
399      * @return float The minimum number of points
400      */
401     public function get_grade_min() {
402         list($min, $max) = $this->get_grade_min_and_max();
404         return $min;
405     }
407     /**
408      * Returns the maximum number of points this grade is graded with respect to.
409      *
410      * @since  Moodle 2.8.7, 2.9.1
411      * @return float The maximum number of points
412      */
413     public function get_grade_max() {
414         list($min, $max) = $this->get_grade_min_and_max();
416         return $max;
417     }
419     /**
420      * Returns timestamp when last graded, null if no grade present
421      *
422      * @return int
423      */
424     public function get_dategraded() {
425         //TODO: HACK - create new fields (MDL-31379)
426         if (is_null($this->finalgrade) and is_null($this->feedback)) {
427             return null; // no grade == no date
428         } else if ($this->overridden) {
429             return $this->overridden;
430         } else {
431             return $this->timemodified;
432         }
433     }
435     /**
436      * Set the overridden status of grade
437      *
438      * @param bool $state requested overridden state
439      * @param bool $refresh refresh grades from external activities if needed
440      * @return bool true is db state changed
441      */
442     public function set_overridden($state, $refresh = true) {
443         if (empty($this->overridden) and $state) {
444             $this->overridden = time();
445             $this->update();
446             return true;
448         } else if (!empty($this->overridden) and !$state) {
449             $this->overridden = 0;
450             $this->update();
452             if ($refresh) {
453                 //refresh when unlocking
454                 $this->grade_item->refresh_grades($this->userid);
455             }
457             return true;
458         }
459         return false;
460     }
462     /**
463      * Checks if grade excluded from aggregation functions
464      *
465      * @return bool True if grade is excluded from aggregation
466      */
467     public function is_excluded() {
468         return !empty($this->excluded);
469     }
471     /**
472      * Set the excluded status of grade
473      *
474      * @param bool $state requested excluded state
475      * @return bool True is database state changed
476      */
477     public function set_excluded($state) {
478         if (empty($this->excluded) and $state) {
479             $this->excluded = time();
480             $this->update();
481             return true;
483         } else if (!empty($this->excluded) and !$state) {
484             $this->excluded = 0;
485             $this->update();
486             return true;
487         }
488         return false;
489     }
491     /**
492      * Lock/unlock this grade.
493      *
494      * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
495      * @param bool $cascade Ignored param
496      * @param bool $refresh Refresh grades when unlocking
497      * @return bool True if successful, false if can not set new lock state for grade
498      */
499     public function set_locked($lockedstate, $cascade=false, $refresh=true) {
500         $this->load_grade_item();
502         if ($lockedstate) {
503             if ($this->grade_item->needsupdate) {
504                 //can not lock grade if final not calculated!
505                 return false;
506             }
508             $this->locked = time();
509             $this->update();
511             return true;
513         } else {
514             if (!empty($this->locked) and $this->locktime < time()) {
515                 //we have to reset locktime or else it would lock up again
516                 $this->locktime = 0;
517             }
519             // remove the locked flag
520             $this->locked = 0;
521             $this->update();
523             if ($refresh and !$this->is_overridden()) {
524                 //refresh when unlocking and not overridden
525                 $this->grade_item->refresh_grades($this->userid);
526             }
528             return true;
529         }
530     }
532     /**
533      * Lock the grade if needed. Make sure this is called only when final grades are valid
534      *
535      * @param array $items array of all grade item ids
536      * @return void
537      */
538     public static function check_locktime_all($items) {
539         global $CFG, $DB;
541         $now = time(); // no rounding needed, this is not supposed to be called every 10 seconds
542         list($usql, $params) = $DB->get_in_or_equal($items);
543         $params[] = $now;
544         $rs = $DB->get_recordset_select('grade_grades', "itemid $usql AND locked = 0 AND locktime > 0 AND locktime < ?", $params);
545         foreach ($rs as $grade) {
546             $grade_grade = new grade_grade($grade, false);
547             $grade_grade->locked = time();
548             $grade_grade->update('locktime');
549         }
550         $rs->close();
551     }
553     /**
554      * Set the locktime for this grade.
555      *
556      * @param int $locktime timestamp for lock to activate
557      * @return void
558      */
559     public function set_locktime($locktime) {
560         $this->locktime = $locktime;
561         $this->update();
562     }
564     /**
565      * Get the locktime for this grade.
566      *
567      * @return int $locktime timestamp for lock to activate
568      */
569     public function get_locktime() {
570         $this->load_grade_item();
572         $item_locktime = $this->grade_item->get_locktime();
574         if (empty($this->locktime) or ($item_locktime and $item_locktime < $this->locktime)) {
575             return $item_locktime;
577         } else {
578             return $this->locktime;
579         }
580     }
582     /**
583      * Check grade hidden status. Uses data from both grade item and grade.
584      *
585      * @return bool true if hidden, false if not
586      */
587     public function is_hidden() {
588         $this->load_grade_item();
589         if (empty($this->grade_item)) {
590             return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time());
591         } else {
592             return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()) or $this->grade_item->is_hidden();
593         }
594     }
596     /**
597      * Check grade hidden status. Uses data from both grade item and grade.
598      *
599      * @return bool true if hiddenuntil, false if not
600      */
601     public function is_hiddenuntil() {
602         $this->load_grade_item();
604         if ($this->hidden == 1 or $this->grade_item->hidden == 1) {
605             return false; //always hidden
606         }
608         if ($this->hidden > 1 or $this->grade_item->hidden > 1) {
609             return true;
610         }
612         return false;
613     }
615     /**
616      * Check grade hidden status. Uses data from both grade item and grade.
617      *
618      * @return int 0 means visible, 1 hidden always, timestamp hidden until
619      */
620     public function get_hidden() {
621         $this->load_grade_item();
623         $item_hidden = $this->grade_item->get_hidden();
625         if ($item_hidden == 1) {
626             return 1;
628         } else if ($item_hidden == 0) {
629             return $this->hidden;
631         } else {
632             if ($this->hidden == 0) {
633                 return $item_hidden;
634             } else if ($this->hidden == 1) {
635                 return 1;
636             } else if ($this->hidden > $item_hidden) {
637                 return $this->hidden;
638             } else {
639                 return $item_hidden;
640             }
641         }
642     }
644     /**
645      * Set the hidden status of grade, 0 mean visible, 1 always hidden, number means date to hide until.
646      *
647      * @param int $hidden new hidden status
648      * @param bool $cascade ignored
649      */
650     public function set_hidden($hidden, $cascade=false) {
651        $this->hidden = $hidden;
652        $this->update();
653     }
655     /**
656      * Finds and returns a grade_grade instance based on params.
657      *
658      * @param array $params associative arrays varname=>value
659      * @return grade_grade Returns a grade_grade instance or false if none found
660      */
661     public static function fetch($params) {
662         return grade_object::fetch_helper('grade_grades', 'grade_grade', $params);
663     }
665     /**
666      * Finds and returns all grade_grade instances based on params.
667      *
668      * @param array $params associative arrays varname=>value
669      * @return array array of grade_grade instances or false if none found.
670      */
671     public static function fetch_all($params) {
672         return grade_object::fetch_all_helper('grade_grades', 'grade_grade', $params);
673     }
675     /**
676      * Given a float value situated between a source minimum and a source maximum, converts it to the
677      * corresponding value situated between a target minimum and a target maximum. Thanks to Darlene
678      * for the formula :-)
679      *
680      * @param float $rawgrade
681      * @param float $source_min
682      * @param float $source_max
683      * @param float $target_min
684      * @param float $target_max
685      * @return float Converted value
686      */
687     public static function standardise_score($rawgrade, $source_min, $source_max, $target_min, $target_max) {
688         if (is_null($rawgrade)) {
689           return null;
690         }
692         if ($source_max == $source_min or $target_min == $target_max) {
693             // prevent division by 0
694             return $target_max;
695         }
697         $factor = ($rawgrade - $source_min) / ($source_max - $source_min);
698         $diff = $target_max - $target_min;
699         $standardised_value = $factor * $diff + $target_min;
700         return $standardised_value;
701     }
703     /**
704      * Given an array like this:
705      * $a = array(1=>array(2, 3),
706      *            2=>array(4),
707      *            3=>array(1),
708      *            4=>array())
709      * this function fully resolves the dependencies so each value will be an array of
710      * the all items this item depends on and their dependencies (and their dependencies...).
711      * It should not explode if there are circular dependencies.
712      * The dependency depth array will list the number of branches in the tree above each leaf.
713      *
714      * @param array $dependson Array to flatten
715      * @param array $dependencydepth Array of itemids => depth. Initially these should be all set to 1.
716      * @return array Flattened array
717      */
718     protected static function flatten_dependencies_array(&$dependson, &$dependencydepth) {
719         // Flatten the nested dependencies - this will handle recursion bombs because it removes duplicates.
720         $somethingchanged = true;
721         while ($somethingchanged) {
722             $somethingchanged = false;
724             foreach ($dependson as $itemid => $depends) {
725                 // Make a copy so we can tell if it changed.
726                 $before = $dependson[$itemid];
727                 foreach ($depends as $subitemid => $subdepends) {
728                     $dependson[$itemid] = array_unique(array_merge($depends, $dependson[$subdepends]));
729                     sort($dependson[$itemid], SORT_NUMERIC);
730                 }
731                 if ($before != $dependson[$itemid]) {
732                     $somethingchanged = true;
733                     if (!isset($dependencydepth[$itemid])) {
734                         $dependencydepth[$itemid] = 1;
735                     } else {
736                         $dependencydepth[$itemid]++;
737                     }
738                 }
739             }
740         }
741     }
743     /**
744      * Return array of grade item ids that are either hidden or indirectly depend
745      * on hidden grades, excluded grades are not returned.
746      * THIS IS A REALLY BIG HACK! to be replaced by conditional aggregation of hidden grades in 2.0
747      *
748      * @param array $grade_grades all course grades of one user, & used for better internal caching
749      * @param array $grade_items array of grade items, & used for better internal caching
750      * @return array This is an array of following arrays:
751      *      unknown => list of item ids that may be affected by hiding (with the ITEM ID as both the key and the value) - for BC with old gradereport plugins
752      *      unknowngrades => list of item ids that may be affected by hiding (with the calculated grade as the value)
753      *      altered => list of item ids that are definitely affected by hiding (with the calculated grade as the value)
754      *      alteredgrademax => for each item in altered or unknown, the new value of the grademax
755      *      alteredgrademin => for each item in altered or unknown, the new value of the grademin
756      *      alteredgradestatus => for each item with a modified status - the value of the new status
757      *      alteredgradeweight => for each item with a modified weight - the value of the new weight
758      */
759     public static function get_hiding_affected(&$grade_grades, &$grade_items) {
760         global $CFG;
762         if (count($grade_grades) !== count($grade_items)) {
763             print_error('invalidarraysize', 'debug', '', 'grade_grade::get_hiding_affected()!');
764         }
766         $dependson = array();
767         $todo = array();
768         $unknown = array();  // can not find altered
769         $altered = array();  // altered grades
770         $alteredgrademax = array();  // Altered grade max values.
771         $alteredgrademin = array();  // Altered grade min values.
772         $alteredaggregationstatus = array();  // Altered aggregation status.
773         $alteredaggregationweight = array();  // Altered aggregation weight.
774         $dependencydepth = array();
776         $hiddenfound = false;
777         foreach($grade_grades as $itemid=>$unused) {
778             $grade_grade =& $grade_grades[$itemid];
779             // We need the immediate dependencies of all every grade_item so we can calculate nested dependencies.
780             $dependson[$grade_grade->itemid] = $grade_items[$grade_grade->itemid]->depends_on();
781             if ($grade_grade->is_excluded()) {
782                 //nothing to do, aggregation is ok
783             } else if ($grade_grade->is_hidden()) {
784                 $hiddenfound = true;
785                 $altered[$grade_grade->itemid] = null;
786                 $alteredaggregationstatus[$grade_grade->itemid] = 'dropped';
787                 $alteredaggregationweight[$grade_grade->itemid] = 0;
788             } else if ($grade_grade->is_locked() or $grade_grade->is_overridden()) {
789                 // no need to recalculate locked or overridden grades
790             } else {
791                 if (!empty($dependson[$grade_grade->itemid])) {
792                     $dependencydepth[$grade_grade->itemid] = 1;
793                     $todo[] = $grade_grade->itemid;
794                 }
795             }
796         }
798         // Flatten the dependency tree and count number of branches to each leaf.
799         self::flatten_dependencies_array($dependson, $dependencydepth);
801         if (!$hiddenfound) {
802             return array('unknown' => array(),
803                          'unknowngrades' => array(),
804                          'altered' => array(),
805                          'alteredgrademax' => array(),
806                          'alteredgrademin' => array(),
807                          'alteredaggregationstatus' => array(),
808                          'alteredaggregationweight' => array());
809         }
810         // This line ensures that $dependencydepth has the same number of items as $todo.
811         $dependencydepth = array_intersect_key($dependencydepth, array_flip($todo));
812         // We need to resort the todo list by the dependency depth. This guarantees we process the leaves, then the branches.
813         array_multisort($dependencydepth, $todo);
815         $max = count($todo);
816         $hidden_precursors = null;
817         for($i=0; $i<$max; $i++) {
818             $found = false;
819             foreach($todo as $key=>$do) {
820                 $hidden_precursors = array_intersect($dependson[$do], array_keys($unknown));
821                 if ($hidden_precursors) {
822                     // this item depends on hidden grade indirectly
823                     $unknown[$do] = $grade_grades[$do]->finalgrade;
824                     unset($todo[$key]);
825                     $found = true;
826                     continue;
828                 } else if (!array_intersect($dependson[$do], $todo)) {
829                     $hidden_precursors = array_intersect($dependson[$do], array_keys($altered));
830                     // If the dependency is a sum aggregation, we need to process it as if it had hidden items.
831                     // The reason for this, is that the code will recalculate the maxgrade by removing ungraded
832                     // items and accounting for 'drop x grades' and then stored back in our virtual grade_items.
833                     // This recalculation is necessary because there will be a call to:
834                     //              $grade_category->aggregate_values_and_adjust_bounds
835                     // for the top level grade that will depend on knowing what that caclulated grademax is
836                     // and it finds that value by checking the virtual grade_items.
837                     $issumaggregate = false;
838                     if ($grade_items[$do]->itemtype == 'category') {
839                         $issumaggregate = $grade_items[$do]->load_item_category()->aggregation == GRADE_AGGREGATE_SUM;
840                     }
841                     if (!$hidden_precursors && !$issumaggregate) {
842                         unset($todo[$key]);
843                         $found = true;
844                         continue;
846                     } else {
847                         // depends on altered grades - we should try to recalculate if possible
848                         if ($grade_items[$do]->is_calculated() or
849                             (!$grade_items[$do]->is_category_item() and !$grade_items[$do]->is_course_item())
850                         ) {
851                             // This is a grade item that is not a category or course and has been affected by grade hiding.
852                             // I guess this means it is a calculation that needs to be recalculated.
853                             $unknown[$do] = $grade_grades[$do]->finalgrade;
854                             unset($todo[$key]);
855                             $found = true;
856                             continue;
858                         } else {
859                             // This is a grade category (or course).
860                             $grade_category = $grade_items[$do]->load_item_category();
862                             // Build a new list of the grades in this category.
863                             $values = array();
864                             $immediatedepends = $grade_items[$do]->depends_on();
865                             foreach ($immediatedepends as $itemid) {
866                                 if (array_key_exists($itemid, $altered)) {
867                                     //nulling an altered precursor
868                                     $values[$itemid] = $altered[$itemid];
869                                     if (is_null($values[$itemid])) {
870                                         // This means this was a hidden grade item removed from the result.
871                                         unset($values[$itemid]);
872                                     }
873                                 } elseif (empty($values[$itemid])) {
874                                     $values[$itemid] = $grade_grades[$itemid]->finalgrade;
875                                 }
876                             }
878                             foreach ($values as $itemid=>$value) {
879                                 if ($grade_grades[$itemid]->is_excluded()) {
880                                     unset($values[$itemid]);
881                                     $alteredaggregationstatus[$itemid] = 'excluded';
882                                     $alteredaggregationweight[$itemid] = null;
883                                     continue;
884                                 }
885                                 // The grade min/max may have been altered by hiding.
886                                 $grademin = $grade_items[$itemid]->grademin;
887                                 if (isset($alteredgrademin[$itemid])) {
888                                     $grademin = $alteredgrademin[$itemid];
889                                 }
890                                 $grademax = $grade_items[$itemid]->grademax;
891                                 if (isset($alteredgrademax[$itemid])) {
892                                     $grademax = $alteredgrademax[$itemid];
893                                 }
894                                 $values[$itemid] = grade_grade::standardise_score($value, $grademin, $grademax, 0, 1);
895                             }
897                             if ($grade_category->aggregateonlygraded) {
898                                 foreach ($values as $itemid=>$value) {
899                                     if (is_null($value)) {
900                                         unset($values[$itemid]);
901                                         $alteredaggregationstatus[$itemid] = 'novalue';
902                                         $alteredaggregationweight[$itemid] = null;
903                                     }
904                                 }
905                             } else {
906                                 foreach ($values as $itemid=>$value) {
907                                     if (is_null($value)) {
908                                         $values[$itemid] = 0;
909                                     }
910                                 }
911                             }
913                             // limit and sort
914                             $allvalues = $values;
915                             $grade_category->apply_limit_rules($values, $grade_items);
917                             $moredropped = array_diff($allvalues, $values);
918                             foreach ($moredropped as $drop => $unused) {
919                                 $alteredaggregationstatus[$drop] = 'dropped';
920                                 $alteredaggregationweight[$drop] = null;
921                             }
923                             foreach ($values as $itemid => $val) {
924                                 if ($grade_category->is_extracredit_used() && ($grade_items[$itemid]->aggregationcoef > 0)) {
925                                     $alteredaggregationstatus[$itemid] = 'extra';
926                                 }
927                             }
929                             asort($values, SORT_NUMERIC);
931                             // let's see we have still enough grades to do any statistics
932                             if (count($values) == 0) {
933                                 // not enough attempts yet
934                                 $altered[$do] = null;
935                                 unset($todo[$key]);
936                                 $found = true;
937                                 continue;
938                             }
940                             $usedweights = array();
941                             $adjustedgrade = $grade_category->aggregate_values_and_adjust_bounds($values, $grade_items, $usedweights);
943                             // recalculate the rawgrade back to requested range
944                             $finalgrade = grade_grade::standardise_score($adjustedgrade['grade'],
945                                                                          0,
946                                                                          1,
947                                                                          $adjustedgrade['grademin'],
948                                                                          $adjustedgrade['grademax']);
950                             foreach ($usedweights as $itemid => $weight) {
951                                 if (!isset($alteredaggregationstatus[$itemid])) {
952                                     $alteredaggregationstatus[$itemid] = 'used';
953                                 }
954                                 $alteredaggregationweight[$itemid] = $weight;
955                             }
957                             $finalgrade = $grade_items[$do]->bounded_grade($finalgrade);
958                             $alteredgrademin[$do] = $adjustedgrade['grademin'];
959                             $alteredgrademax[$do] = $adjustedgrade['grademax'];
960                             // We need to muck with the "in-memory" grade_items records so
961                             // that subsequent calculations will use the adjusted grademin and grademax.
962                             $grade_items[$do]->grademin = $adjustedgrade['grademin'];
963                             $grade_items[$do]->grademax = $adjustedgrade['grademax'];
965                             $altered[$do] = $finalgrade;
966                             unset($todo[$key]);
967                             $found = true;
968                             continue;
969                         }
970                     }
971                 }
972             }
973             if (!$found) {
974                 break;
975             }
976         }
978         return array('unknown' => array_combine(array_keys($unknown), array_keys($unknown)), // Left for BC in case some gradereport plugins expect it.
979                      'unknowngrades' => $unknown,
980                      'altered' => $altered,
981                      'alteredgrademax' => $alteredgrademax,
982                      'alteredgrademin' => $alteredgrademin,
983                      'alteredaggregationstatus' => $alteredaggregationstatus,
984                      'alteredaggregationweight' => $alteredaggregationweight);
985     }
987     /**
988      * Returns true if the grade's value is superior or equal to the grade item's gradepass value, false otherwise.
989      *
990      * @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
991      * @return bool
992      */
993     public function is_passed($grade_item = null) {
994         if (empty($grade_item)) {
995             if (!isset($this->grade_item)) {
996                 $this->load_grade_item();
997             }
998         } else {
999             $this->grade_item = $grade_item;
1000             $this->itemid = $grade_item->id;
1001         }
1003         // Return null if finalgrade is null
1004         if (is_null($this->finalgrade)) {
1005             return null;
1006         }
1008         // Return null if gradepass == grademin, gradepass is null, or grade item is a scale and gradepass is 0.
1009         if (is_null($this->grade_item->gradepass)) {
1010             return null;
1011         } else if ($this->grade_item->gradepass == $this->grade_item->grademin) {
1012             return null;
1013         } else if ($this->grade_item->gradetype == GRADE_TYPE_SCALE && !grade_floats_different($this->grade_item->gradepass, 0.0)) {
1014             return null;
1015         }
1017         return $this->finalgrade >= $this->grade_item->gradepass;
1018     }
1020     /**
1021      * Insert the grade_grade instance into the database.
1022      *
1023      * @param string $source From where was the object inserted (mod/forum, manual, etc.)
1024      * @return int The new grade_grade ID if successful, false otherwise
1025      */
1026     public function insert($source=null) {
1027         // TODO: dategraded hack - do not update times, they are used for submission and grading (MDL-31379)
1028         //$this->timecreated = $this->timemodified = time();
1029         return parent::insert($source);
1030     }
1032     /**
1033      * In addition to update() as defined in grade_object rounds the float numbers using php function,
1034      * the reason is we need to compare the db value with computed number to skip updates if possible.
1035      *
1036      * @param string $source from where was the object inserted (mod/forum, manual, etc.)
1037      * @return bool success
1038      */
1039     public function update($source=null) {
1040         $this->rawgrade = grade_floatval($this->rawgrade);
1041         $this->finalgrade = grade_floatval($this->finalgrade);
1042         $this->rawgrademin = grade_floatval($this->rawgrademin);
1043         $this->rawgrademax = grade_floatval($this->rawgrademax);
1044         return parent::update($source);
1045     }
1048     /**
1049      * Handles adding feedback files in the gradebook.
1050      *
1051      * @param int|null $historyid
1052      */
1053     protected function add_feedback_files(int $historyid = null) {
1054         global $CFG;
1056         // We only support feedback files for modules atm.
1057         if ($this->grade_item && $this->grade_item->is_external_item()) {
1058             $context = $this->get_context();
1059             $this->copy_feedback_files($context, GRADE_FEEDBACK_FILEAREA, $this->id);
1061             if (empty($CFG->disablegradehistory) && $historyid) {
1062                 $this->copy_feedback_files($context, GRADE_HISTORY_FEEDBACK_FILEAREA, $historyid);
1063             }
1064         }
1066         return $this->id;
1067     }
1069     /**
1070      * Handles updating feedback files in the gradebook.
1071      *
1072      * @param int|null $historyid
1073      */
1074     protected function update_feedback_files(int $historyid = null) {
1075         global $CFG;
1077         // We only support feedback files for modules atm.
1078         if ($this->grade_item && $this->grade_item->is_external_item()) {
1079             $context = $this->get_context();
1081             $fs = new file_storage();
1082             $fs->delete_area_files($context->id, GRADE_FILE_COMPONENT, GRADE_FEEDBACK_FILEAREA, $this->id);
1084             $this->copy_feedback_files($context, GRADE_FEEDBACK_FILEAREA, $this->id);
1086             if (empty($CFG->disablegradehistory) && $historyid) {
1087                 $this->copy_feedback_files($context, GRADE_HISTORY_FEEDBACK_FILEAREA, $historyid);
1088             }
1089         }
1091         return true;
1092     }
1094     /**
1095      * Handles deleting feedback files in the gradebook.
1096      */
1097     protected function delete_feedback_files() {
1098         // We only support feedback files for modules atm.
1099         if ($this->grade_item && $this->grade_item->is_external_item()) {
1100             $context = $this->get_context();
1102             $fs = new file_storage();
1103             $fs->delete_area_files($context->id, GRADE_FILE_COMPONENT, GRADE_FEEDBACK_FILEAREA, $this->id);
1105             // Grade history only gets deleted when we delete the whole grade item.
1106         }
1108         return true;
1109     }
1111     /**
1112      * Deletes the grade_grade instance from the database.
1113      *
1114      * @param string $source The location the deletion occurred (mod/forum, manual, etc.).
1115      * @return bool Returns true if the deletion was successful, false otherwise.
1116      */
1117     public function delete($source = null) {
1118         global $DB;
1120         $transaction = $DB->start_delegated_transaction();
1121         $success = parent::delete($source);
1123         // If the grade was deleted successfully trigger a grade_deleted event.
1124         if ($success && !empty($this->grade_item)) {
1125             \core\event\grade_deleted::create_from_grade($this)->trigger();
1126         }
1128         $transaction->allow_commit();
1129         return $success;
1130     }
1132     /**
1133      * Used to notify the completion system (if necessary) that a user's grade
1134      * has changed, and clear up a possible score cache.
1135      *
1136      * @param bool $deleted True if grade was actually deleted
1137      */
1138     protected function notify_changed($deleted) {
1139         global $CFG;
1141         // Condition code may cache the grades for conditional availability of
1142         // modules or sections. (This code should use a hook for communication
1143         // with plugin, but hooks are not implemented at time of writing.)
1144         if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
1145             \availability_grade\callbacks::grade_changed($this->userid);
1146         }
1148         require_once($CFG->libdir.'/completionlib.php');
1150         // Bail out immediately if completion is not enabled for site (saves loading
1151         // grade item & requiring the restore stuff).
1152         if (!completion_info::is_enabled_for_site()) {
1153             return;
1154         }
1156         // Ignore during restore, as completion data will be updated anyway and
1157         // doing it now will result in incorrect dates (it will say they got the
1158         // grade completion now, instead of the correct time).
1159         if (class_exists('restore_controller', false) && restore_controller::is_executing()) {
1160             return;
1161         }
1163         // Load information about grade item, exit if the grade item is missing.
1164         if (!$this->load_grade_item()) {
1165             return;
1166         }
1168         // Only course-modules have completion data
1169         if ($this->grade_item->itemtype!='mod') {
1170             return;
1171         }
1173         // Use $COURSE if available otherwise get it via item fields
1174         $course = get_course($this->grade_item->courseid, false);
1176         // Bail out if completion is not enabled for course
1177         $completion = new completion_info($course);
1178         if (!$completion->is_enabled()) {
1179             return;
1180         }
1182         // Get course-module
1183         $cm = get_coursemodule_from_instance($this->grade_item->itemmodule,
1184               $this->grade_item->iteminstance, $this->grade_item->courseid);
1185         // If the course-module doesn't exist, display a warning...
1186         if (!$cm) {
1187             // ...unless the grade is being deleted in which case it's likely
1188             // that the course-module was just deleted too, so that's okay.
1189             if (!$deleted) {
1190                 debugging("Couldn't find course-module for module '" .
1191                         $this->grade_item->itemmodule . "', instance '" .
1192                         $this->grade_item->iteminstance . "', course '" .
1193                         $this->grade_item->courseid . "'");
1194             }
1195             return;
1196         }
1198         // Pass information on to completion system
1199         $completion->inform_grade_changed($cm, $this->grade_item, $this, $deleted);
1200     }
1202     /**
1203      * Get some useful information about how this grade_grade is reflected in the aggregation
1204      * for the grade_category. For example this could be an extra credit item, and it could be
1205      * dropped because it's in the X lowest or highest.
1206      *
1207      * @return array(status, weight) - A keyword and a numerical weight that represents how this grade was included in the aggregation.
1208      */
1209     function get_aggregation_hint() {
1210         return array('status' => $this->get_aggregationstatus(),
1211                      'weight' => $this->get_aggregationweight());
1212     }
1214     /**
1215      * Handles copying feedback files to a specified gradebook file area.
1216      *
1217      * @param context $context
1218      * @param string $filearea
1219      * @param int $itemid
1220      */
1221     private function copy_feedback_files(context $context, string $filearea, int $itemid) {
1222         if ($this->feedbackfiles) {
1223             $filestocopycontextid = $this->feedbackfiles['contextid'];
1224             $filestocopycomponent = $this->feedbackfiles['component'];
1225             $filestocopyfilearea = $this->feedbackfiles['filearea'];
1226             $filestocopyitemid = $this->feedbackfiles['itemid'];
1228             $fs = new file_storage();
1229             if ($filestocopy = $fs->get_area_files($filestocopycontextid, $filestocopycomponent, $filestocopyfilearea,
1230                     $filestocopyitemid)) {
1231                 foreach ($filestocopy as $filetocopy) {
1232                     $destination = [
1233                         'contextid' => $context->id,
1234                         'component' => GRADE_FILE_COMPONENT,
1235                         'filearea' => $filearea,
1236                         'itemid' => $itemid
1237                     ];
1238                     $fs->create_file_from_storedfile($destination, $filetocopy);
1239                 }
1240             }
1241         }
1242     }
1244     /**
1245      * Determine the correct context for this grade_grade.
1246      *
1247      * @return context
1248      */
1249     public function get_context() {
1250         $this->load_grade_item();
1251         return $this->grade_item->get_context();
1252     }