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