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